diff --git a/.circleci/config.yml b/.circleci/config.yml index e57008bcd3..e2be3f6528 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -1,7 +1,5 @@ defaults: &defaults working_directory: ~/build - docker: - - image: alanz/haskell-hie-ci resource_class: large steps: - checkout @@ -34,25 +32,10 @@ defaults: &defaults echo "export SKIP_CI=$SKIP_CI" >> $BASH_ENV - run: - name: Stack upgrade + name: Build command: | if [[ -z "$SKIP_CI" ]]; then - stack upgrade - fi - - - run: - name: Stack setup - command: | - if [[ -z "$SKIP_CI" ]]; then - stack -j4 --stack-yaml=${STACK_FILE} setup - fi - - - run: - name: Build (we need the exe for tests) - # need j1, else ghc-lib-parser triggers OOM - command: | - if [[ -z "$SKIP_CI" ]]; then - stack -j4 --stack-yaml=${STACK_FILE} install --no-terminal + stack -j4 --stack-yaml=${STACK_FILE} install --system-ghc --no-terminal fi no_output_timeout: 30m @@ -60,7 +43,7 @@ defaults: &defaults name: Build Testsuite without running it command: | if [[ -z "$SKIP_CI" ]]; then - stack -j4 --stack-yaml=${STACK_FILE} build --test --no-run-tests --no-terminal + stack -j4 --stack-yaml=${STACK_FILE} build --system-ghc --test --no-run-tests --no-terminal fi no_output_timeout: 30m @@ -70,25 +53,28 @@ defaults: &defaults - save_cache: key: v4-stack-cache-{{ arch }}-{{ .Environment.CIRCLE_JOB }}-{{ checksum "resolver.txt" }} - paths: &cache_paths + paths: - ~/.stack version: 2 jobs: - stackage-lts21: + stackage-lts22: + docker: + - image: haskell:9.6.6-slim-bullseye environment: - - STACK_FILE: "stack-lts21.yaml" + - STACK_FILE: "stack-lts22.yaml" <<: *defaults - stackage-nightly: + stackage-lts23: + docker: + - image: haskell:9.8.4-slim-bullseye environment: - STACK_FILE: "stack.yaml" <<: *defaults - workflows: version: 2 multiple-ghcs: jobs: - - stackage-lts21 - - stackage-nightly + - stackage-lts22 + - stackage-lts23 diff --git a/.cirrus.yml b/.cirrus.yml deleted file mode 100644 index 4f0b3fad09..0000000000 --- a/.cirrus.yml +++ /dev/null @@ -1,101 +0,0 @@ -# release CI for FreeBSD -compute_engine_instance: - image_project: freebsd-org-cloud-dev - image: family/freebsd-13-1 - platform: freebsd - disk: 100 # Gb - -build_task: - timeout_in: 120m - only_if: $CIRRUS_TAG != '' - env: - AWS_ACCESS_KEY_ID: ENCRYPTED[dc5896620ebc12e98e6bbe96f72c5a2fe3785f439b7b2346797355f8d329a4bfd8ef6e58086bfc014be0d914424101cd] - AWS_SECRET_ACCESS_KEY: ENCRYPTED[6501cd594aca08c6c67cc679dd6f6d30db0cd44a81cceddebf32bb3d0a37f9af19cd71ddb7169d3f7b284a7829969f9e] - S3_HOST: ENCRYPTED[d3fef1b5850e85d80dd1684370b53183df2218f2d36509108a2703371afd9ebd3f9596ad4de52487c15ea29baed606b7] - TARBALL_EXT: "tar.xz" - ARCH: 64 - ARTIFACT: "x86_64-freebsd" - DISTRO: "na" - RUNNER_OS: "FreeBSD" - ADD_CABAL_ARGS: "--enable-split-sections" - GITHUB_WORKSPACE: ${CIRRUS_WORKING_DIR} - CABAL_CACHE_NONFATAL: "yes" - matrix: - - name: build-ghc-9.0.2 - env: - GHC_VERSION: 9.0.2 - - name: build-ghc-9.2.5 - env: - GHC_VERSION: 9.2.5 - - name: build-ghc-9.2.7 - env: - GHC_VERSION: 9.2.7 - install_script: pkg install -y hs-cabal-install git bash misc/compat10x misc/compat11x misc/compat12x gmake patchelf tree gmp libiconv - script: - - tzsetup Etc/GMT - - adjkerntz -a - - bash .github/scripts/build.sh - - tar caf out.tar.xz out/ store/ - binaries_artifacts: - path: "out.tar.xz" - - -bindist_task: - name: bindist - depends_on: - - build-ghc-9.0.2 - - build-ghc-9.2.5 - - build-ghc-9.2.7 - timeout_in: 120m - only_if: $CIRRUS_TAG != '' - env: - TARBALL_EXT: "tar.xz" - ARCH: 64 - ARTIFACT: "x86_64-freebsd" - DISTRO: "na" - RUNNER_OS: "FreeBSD" - GITHUB_WORKSPACE: ${CIRRUS_WORKING_DIR} - install_script: pkg install -y hs-cabal-install git bash misc/compat10x misc/compat11x misc/compat12x gmake patchelf tree gmp libiconv unzip - script: - - tzsetup Etc/GMT - - adjkerntz -a - - - curl -o binaries-9.0.2.tar.xz -L https://api.cirrus-ci.com/v1/artifact/build/${CIRRUS_BUILD_ID}/build-ghc-9.0.2/binaries/out.tar.xz - - tar xvf binaries-9.0.2.tar.xz - - rm -f binaries-9.0.2.tar.xz - - - curl -o binaries-9.2.5.tar.xz -L https://api.cirrus-ci.com/v1/artifact/build/${CIRRUS_BUILD_ID}/build-ghc-9.2.5/binaries/out.tar.xz - - tar xvf binaries-9.2.5.tar.xz - - rm -f binaries-9.2.5.tar.xz - - - curl -o binaries-9.2.7.tar.xz -L https://api.cirrus-ci.com/v1/artifact/build/${CIRRUS_BUILD_ID}/build-ghc-9.2.7/binaries/out.tar.xz - - tar xvf binaries-9.2.7.tar.xz - - rm -f binaries-9.2.7.tar.xz - - - bash .github/scripts/bindist.sh - bindist_artifacts: - path: "./out/*.tar.xz" - -test_task: - name: test - depends_on: - - bindist - timeout_in: 120m - only_if: $CIRRUS_TAG != '' - env: - TARBALL_EXT: "tar.xz" - ARCH: 64 - ARTIFACT: "x86_64-freebsd" - DISTRO: "na" - RUNNER_OS: "FreeBSD" - GITHUB_WORKSPACE: ${CIRRUS_WORKING_DIR} - install_script: pkg install -y hs-cabal-install git bash misc/compat10x misc/compat11x misc/compat12x gmake patchelf tree gmp libiconv unzip - script: - - tzsetup Etc/GMT - - adjkerntz -a - - - curl -O -L https://api.cirrus-ci.com/v1/artifact/build/${CIRRUS_BUILD_ID}/bindist/bindist.zip - - unzip bindist.zip - - - bash .github/scripts/test.sh - diff --git a/.github/actions/bindist-actions/action-deb10/action.yaml b/.github/actions/bindist-actions/action-deb10/action.yaml new file mode 100644 index 0000000000..da96b04669 --- /dev/null +++ b/.github/actions/bindist-actions/action-deb10/action.yaml @@ -0,0 +1,21 @@ +description: Container for deb10 +inputs: + stage: + description: which stage to build + required: true + version: + description: which GHC version to build/test + required: false +name: action-deb10 +runs: + entrypoint: .github/scripts/entrypoint.sh + env: + GHC_VERSION: ${{ inputs.version }} + INSTALL: apt-get update && apt-get install -y + STAGE: ${{ inputs.stage }} + TOOLS: libnuma-dev zlib1g-dev libgmp-dev libgmp10 libssl-dev liblzma-dev libbz2-dev + git wget lsb-release software-properties-common gnupg2 apt-transport-https gcc + autoconf automake build-essential curl ghc gzip libffi-dev libncurses-dev libncurses5 + libtinfo5 patchelf + image: debian:10 + using: docker diff --git a/.github/actions/bindist-actions/action-deb11/action.yaml b/.github/actions/bindist-actions/action-deb11/action.yaml new file mode 100644 index 0000000000..8ffe78e1db --- /dev/null +++ b/.github/actions/bindist-actions/action-deb11/action.yaml @@ -0,0 +1,21 @@ +description: Container for deb11 +inputs: + stage: + description: which stage to build + required: true + version: + description: which GHC version to build/test + required: false +name: action-deb11 +runs: + entrypoint: .github/scripts/entrypoint.sh + env: + GHC_VERSION: ${{ inputs.version }} + INSTALL: apt-get update && apt-get install -y + STAGE: ${{ inputs.stage }} + TOOLS: libnuma-dev zlib1g-dev libgmp-dev libgmp10 libssl-dev liblzma-dev libbz2-dev + git wget lsb-release software-properties-common gnupg2 apt-transport-https gcc + autoconf automake build-essential curl ghc gzip libffi-dev libncurses-dev libncurses5 + libtinfo5 patchelf + image: debian:11 + using: docker diff --git a/.github/actions/bindist-actions/action-deb12/action.yaml b/.github/actions/bindist-actions/action-deb12/action.yaml new file mode 100644 index 0000000000..20bcc6a157 --- /dev/null +++ b/.github/actions/bindist-actions/action-deb12/action.yaml @@ -0,0 +1,21 @@ +description: Container for deb12 +inputs: + stage: + description: which stage to build + required: true + version: + description: which GHC version to build/test + required: false +name: action-deb12 +runs: + entrypoint: .github/scripts/entrypoint.sh + env: + GHC_VERSION: ${{ inputs.version }} + INSTALL: apt-get update && apt-get install -y + STAGE: ${{ inputs.stage }} + TOOLS: libnuma-dev zlib1g-dev libgmp-dev libgmp10 libssl-dev liblzma-dev libbz2-dev + git wget lsb-release software-properties-common gnupg2 apt-transport-https gcc + autoconf automake build-essential curl ghc gzip libffi-dev libncurses-dev libncurses5 + libtinfo5 patchelf + image: debian:12 + using: docker diff --git a/.github/actions/bindist-actions/action-deb9/action.yaml b/.github/actions/bindist-actions/action-deb9/action.yaml new file mode 100644 index 0000000000..693e3845a5 --- /dev/null +++ b/.github/actions/bindist-actions/action-deb9/action.yaml @@ -0,0 +1,24 @@ +description: Container for deb9 +inputs: + stage: + description: which stage to build + required: true + version: + description: which GHC version to build/test + required: false +name: action-deb9 +runs: + entrypoint: .github/scripts/entrypoint.sh + env: + GHC_VERSION: ${{ inputs.version }} + INSTALL: sed -i s/deb.debian.org/archive.debian.org/g /etc/apt/sources.list && + sed -i 's|security.debian.org|archive.debian.org/|g' /etc/apt/sources.list && + sed -i /-updates/d /etc/apt/sources.list && apt-get update && apt-get install + -y + STAGE: ${{ inputs.stage }} + TOOLS: libnuma-dev zlib1g-dev libgmp-dev libgmp10 libssl-dev liblzma-dev libbz2-dev + git wget lsb-release software-properties-common gnupg2 apt-transport-https gcc + autoconf automake build-essential curl ghc gzip libffi-dev libncurses-dev libncurses5 + libtinfo5 patchelf + image: debian:9 + using: docker diff --git a/.github/actions/bindist-actions/action-fedora33/action.yaml b/.github/actions/bindist-actions/action-fedora33/action.yaml new file mode 100644 index 0000000000..d20c8feccd --- /dev/null +++ b/.github/actions/bindist-actions/action-fedora33/action.yaml @@ -0,0 +1,21 @@ +description: Container for fedora33 +inputs: + stage: + description: which stage to build + required: true + version: + description: which GHC version to build/test + required: false +name: action-fedora33 +runs: + entrypoint: .github/scripts/entrypoint.sh + env: + GHC_VERSION: ${{ inputs.version }} + INSTALL: dnf install -y + STAGE: ${{ inputs.stage }} + TOOLS: autoconf automake binutils bzip2 coreutils curl elfutils-devel elfutils-libs + findutils gcc gcc-c++ git gmp gmp-devel jq lbzip2 make ncurses ncurses-compat-libs + ncurses-devel openssh-clients patch perl pxz python3 sqlite sudo wget which + xz zlib-devel patchelf + image: fedora:33 + using: docker diff --git a/.github/actions/bindist-actions/action-fedora40/action.yaml b/.github/actions/bindist-actions/action-fedora40/action.yaml new file mode 100644 index 0000000000..83f23b23c8 --- /dev/null +++ b/.github/actions/bindist-actions/action-fedora40/action.yaml @@ -0,0 +1,21 @@ +description: Container for fedora40 +inputs: + stage: + description: which stage to build + required: true + version: + description: which GHC version to build/test + required: false +name: action-fedora40 +runs: + entrypoint: .github/scripts/entrypoint.sh + env: + GHC_VERSION: ${{ inputs.version }} + INSTALL: dnf install -y + STAGE: ${{ inputs.stage }} + TOOLS: autoconf automake binutils bzip2 coreutils curl elfutils-devel elfutils-libs + findutils gcc gcc-c++ git gmp gmp-devel jq lbzip2 make ncurses ncurses-compat-libs + ncurses-devel openssh-clients patch perl pxz python3 sqlite sudo wget which + xz zlib-devel patchelf + image: fedora:40 + using: docker diff --git a/.github/actions/bindist-actions/action-mint193/action.yaml b/.github/actions/bindist-actions/action-mint193/action.yaml new file mode 100644 index 0000000000..e1269e0e56 --- /dev/null +++ b/.github/actions/bindist-actions/action-mint193/action.yaml @@ -0,0 +1,21 @@ +description: Container for mint193 +inputs: + stage: + description: which stage to build + required: true + version: + description: which GHC version to build/test + required: false +name: action-mint193 +runs: + entrypoint: .github/scripts/entrypoint.sh + env: + GHC_VERSION: ${{ inputs.version }} + INSTALL: apt-get update && apt-get install -y + STAGE: ${{ inputs.stage }} + TOOLS: libnuma-dev zlib1g-dev libgmp-dev libgmp10 libssl-dev liblzma-dev libbz2-dev + git wget lsb-release software-properties-common gnupg2 apt-transport-https gcc + autoconf automake build-essential curl ghc gzip libffi-dev libncurses-dev libncurses5 + libtinfo5 patchelf + image: linuxmintd/mint19.3-amd64 + using: docker diff --git a/.github/actions/bindist-actions/action-mint202/action.yaml b/.github/actions/bindist-actions/action-mint202/action.yaml new file mode 100644 index 0000000000..adea7272f1 --- /dev/null +++ b/.github/actions/bindist-actions/action-mint202/action.yaml @@ -0,0 +1,21 @@ +description: Container for mint202 +inputs: + stage: + description: which stage to build + required: true + version: + description: which GHC version to build/test + required: false +name: action-mint202 +runs: + entrypoint: .github/scripts/entrypoint.sh + env: + GHC_VERSION: ${{ inputs.version }} + INSTALL: apt-get update && apt-get install -y + STAGE: ${{ inputs.stage }} + TOOLS: libnuma-dev zlib1g-dev libgmp-dev libgmp10 libssl-dev liblzma-dev libbz2-dev + git wget lsb-release software-properties-common gnupg2 apt-transport-https gcc + autoconf automake build-essential curl ghc gzip libffi-dev libncurses-dev libncurses5 + libtinfo5 patchelf + image: linuxmintd/mint20.2-amd64 + using: docker diff --git a/.github/actions/bindist-actions/action-mint213/action.yaml b/.github/actions/bindist-actions/action-mint213/action.yaml new file mode 100644 index 0000000000..bd09dc0e97 --- /dev/null +++ b/.github/actions/bindist-actions/action-mint213/action.yaml @@ -0,0 +1,21 @@ +description: Container for mint213 +inputs: + stage: + description: which stage to build + required: true + version: + description: which GHC version to build/test + required: false +name: action-mint213 +runs: + entrypoint: .github/scripts/entrypoint.sh + env: + GHC_VERSION: ${{ inputs.version }} + INSTALL: apt-get update && apt-get install -y + STAGE: ${{ inputs.stage }} + TOOLS: libnuma-dev zlib1g-dev libgmp-dev libgmp10 libssl-dev liblzma-dev libbz2-dev + git wget lsb-release software-properties-common gnupg2 apt-transport-https gcc + autoconf automake build-essential curl ghc gzip libffi-dev libncurses-dev libncurses5 + libtinfo5 patchelf + image: linuxmintd/mint21.3-amd64 + using: docker diff --git a/.github/actions/bindist-actions/action-ubuntu1804/action.yaml b/.github/actions/bindist-actions/action-ubuntu1804/action.yaml new file mode 100644 index 0000000000..6a6f4662a0 --- /dev/null +++ b/.github/actions/bindist-actions/action-ubuntu1804/action.yaml @@ -0,0 +1,21 @@ +description: Container for ubuntu1804 +inputs: + stage: + description: which stage to build + required: true + version: + description: which GHC version to build/test + required: false +name: action-ubuntu1804 +runs: + entrypoint: .github/scripts/entrypoint.sh + env: + GHC_VERSION: ${{ inputs.version }} + INSTALL: apt-get update && apt-get install -y + STAGE: ${{ inputs.stage }} + TOOLS: libnuma-dev zlib1g-dev libgmp-dev libgmp10 libssl-dev liblzma-dev libbz2-dev + git wget lsb-release software-properties-common gnupg2 apt-transport-https gcc + autoconf automake build-essential curl ghc gzip libffi-dev libncurses-dev libncurses5 + libtinfo5 patchelf + image: ubuntu:18.04 + using: docker diff --git a/.github/actions/bindist-actions/action-ubuntu2004/action.yaml b/.github/actions/bindist-actions/action-ubuntu2004/action.yaml new file mode 100644 index 0000000000..3a5b57a370 --- /dev/null +++ b/.github/actions/bindist-actions/action-ubuntu2004/action.yaml @@ -0,0 +1,21 @@ +description: Container for ubuntu2004 +inputs: + stage: + description: which stage to build + required: true + version: + description: which GHC version to build/test + required: false +name: action-ubuntu2004 +runs: + entrypoint: .github/scripts/entrypoint.sh + env: + GHC_VERSION: ${{ inputs.version }} + INSTALL: apt-get update && apt-get install -y + STAGE: ${{ inputs.stage }} + TOOLS: libnuma-dev zlib1g-dev libgmp-dev libgmp10 libssl-dev liblzma-dev libbz2-dev + git wget lsb-release software-properties-common gnupg2 apt-transport-https gcc + autoconf automake build-essential curl ghc gzip libffi-dev libncurses-dev libncurses5 + libtinfo5 patchelf + image: ubuntu:20.04 + using: docker diff --git a/.github/actions/bindist-actions/action-ubuntu2204/action.yaml b/.github/actions/bindist-actions/action-ubuntu2204/action.yaml new file mode 100644 index 0000000000..857776507d --- /dev/null +++ b/.github/actions/bindist-actions/action-ubuntu2204/action.yaml @@ -0,0 +1,21 @@ +description: Container for ubuntu2204 +inputs: + stage: + description: which stage to build + required: true + version: + description: which GHC version to build/test + required: false +name: action-ubuntu2204 +runs: + entrypoint: .github/scripts/entrypoint.sh + env: + GHC_VERSION: ${{ inputs.version }} + INSTALL: apt-get update && apt-get install -y + STAGE: ${{ inputs.stage }} + TOOLS: libnuma-dev zlib1g-dev libgmp-dev libgmp10 libssl-dev liblzma-dev libbz2-dev + git wget lsb-release software-properties-common gnupg2 apt-transport-https gcc + autoconf automake build-essential curl ghc gzip libffi-dev libncurses-dev libncurses5 + libtinfo5 patchelf + image: ubuntu:22.04 + using: docker diff --git a/.github/actions/bindist-actions/action-unknown/action.yaml b/.github/actions/bindist-actions/action-unknown/action.yaml new file mode 100644 index 0000000000..96cf0593e9 --- /dev/null +++ b/.github/actions/bindist-actions/action-unknown/action.yaml @@ -0,0 +1,21 @@ +description: Container for unknown +inputs: + stage: + description: which stage to build + required: true + version: + description: which GHC version to build/test + required: false +name: action-unknown +runs: + entrypoint: .github/scripts/entrypoint.sh + env: + GHC_VERSION: ${{ inputs.version }} + INSTALL: yum -y install epel-release && yum install -y --allowerasing + STAGE: ${{ inputs.stage }} + TOOLS: autoconf automake binutils bzip2 coreutils curl elfutils-devel elfutils-libs + findutils gcc gcc-c++ git gmp gmp-devel jq lbzip2 make ncurses ncurses-compat-libs + ncurses-devel openssh-clients patch perl pxz python3 sqlite sudo wget which + xz zlib-devel patchelf + image: rockylinux:8 + using: docker diff --git a/.github/actions/setup-build/action.yml b/.github/actions/setup-build/action.yml index e4480db5cc..da1ece3140 100644 --- a/.github/actions/setup-build/action.yml +++ b/.github/actions/setup-build/action.yml @@ -7,7 +7,7 @@ inputs: cabal: description: "Cabal version" required: false - default: "3.10.2.0" + default: "3.14.2.0" os: description: "Operating system: Linux, Windows or macOS" required: true @@ -31,7 +31,7 @@ runs: sudo chown -R $USER /usr/local/.ghcup shell: bash - - uses: haskell/actions/setup@v2.4.7 + - uses: haskell-actions/setup@v2.7.10 id: HaskEnvSetup with: ghc-version : ${{ inputs.ghc }} @@ -116,3 +116,18 @@ runs: - name: "Remove freeze file" run: rm -f cabal.project.freeze shell: bash + + # Make sure to clear all unneeded `ghcup`` caches. + # At some point, we were running out of disk space, see issue + # https://github.com/haskell/haskell-language-server/issues/4386 for details. + # + # Using "printf" debugging (`du -sh *` and `df -h /`) and binary searching, + # we figured out that `ghcup` caches are taking up a sizable portion of the + # disk space. + # Thus, we remove anything we don't need, especially caches and temporary files. + # For got measure, we also make sure no other tooling versions are + # installed besides the ones we explicitly want. + - name: "Remove ghcup caches" + if: runner.os == 'Linux' + run: ghcup gc --ghc-old --share-dir --hls-no-ghc --cache --tmpdirs --unset + shell: bash diff --git a/plugins/hls-alternate-number-format-plugin/LICENSE b/.github/generate-ci/LICENSE similarity index 100% rename from plugins/hls-alternate-number-format-plugin/LICENSE rename to .github/generate-ci/LICENSE diff --git a/.github/generate-ci/README.mkd b/.github/generate-ci/README.mkd new file mode 100644 index 0000000000..fef645ea12 --- /dev/null +++ b/.github/generate-ci/README.mkd @@ -0,0 +1,5 @@ +# generate-ci + +This is the generator for the release bindist CI. + +Edit ./gen_ci.hs to change configuration and run "./generate-jobs" to regenerate diff --git a/ghcide/test/data/plugin-recorddot/cabal.project b/.github/generate-ci/cabal.project similarity index 100% rename from ghcide/test/data/plugin-recorddot/cabal.project rename to .github/generate-ci/cabal.project diff --git a/.github/generate-ci/gen_ci.hs b/.github/generate-ci/gen_ci.hs new file mode 100644 index 0000000000..28a81d8576 --- /dev/null +++ b/.github/generate-ci/gen_ci.hs @@ -0,0 +1,618 @@ +{-# LANGUAGE OverloadedStrings #-} +{-# LANGUAGE RecordWildCards #-} + +import Control.Monad +import Data.Maybe + +import Data.Aeson hiding (encode) +import qualified Data.Aeson.Key as K +import Data.Aeson.Types (Pair) +import Data.Yaml + +import qualified Data.ByteString as BS + +import qualified Data.List as L + +import System.Directory +import System.Environment +import System.FilePath + +------------------------------------------------------------------------------- +-- Configuration parameters +------------------------------------------------------------------------------- + +data Opsys + = Linux Distro + | Darwin + | Windows deriving (Eq) + +osName :: Opsys -> String +osName Darwin = "mac" +osName Windows = "windows" +osName (Linux d) = "linux-" ++ distroName d + +data Distro + = Debian9 + | Debian10 + | Debian11 + | Debian12 + | Ubuntu1804 + | Ubuntu2004 + | Ubuntu2204 + | Mint193 + | Mint202 + | Mint213 + | Fedora33 + | Fedora40 + | Rocky8 + deriving (Eq, Enum, Bounded) + +allDistros :: [Distro] +allDistros = [minBound .. maxBound] + +data Arch = Amd64 | AArch64 +archName :: Arch -> String +archName Amd64 = "x86_64" +archName AArch64 = "aarch64" + +artifactName :: Arch -> Opsys -> String +artifactName arch opsys = archName arch ++ "-" ++ case opsys of + Linux distro -> "linux-" ++ distroName distro + Darwin -> "apple-darwin" + Windows -> "mingw64" + +data GHC + = GHC967 + | GHC984 + | GHC9102 + | GHC9122 + deriving (Eq, Enum, Bounded) + +ghcVersion :: GHC -> String +ghcVersion GHC967 = "9.6.7" +ghcVersion GHC984 = "9.8.4" +ghcVersion GHC9102 = "9.10.2" +ghcVersion GHC9122 = "9.12.2" + +ghcVersionIdent :: GHC -> String +ghcVersionIdent = filter (/= '.') . ghcVersion + +allGHCs :: [GHC] +allGHCs = [minBound .. maxBound] + +data Stage = Build GHC | Bindist | Test + +------------------------------------------------------------------------------- +-- Distro Configuration +------------------------------------------------------------------------------- + +distroImage :: Distro -> String +distroImage Debian9 = "debian:9" +distroImage Debian10 = "debian:10" +distroImage Debian11 = "debian:11" +distroImage Debian12 = "debian:12" +distroImage Ubuntu1804 = "ubuntu:18.04" +distroImage Ubuntu2004 = "ubuntu:20.04" +distroImage Ubuntu2204 = "ubuntu:22.04" +distroImage Mint193 = "linuxmintd/mint19.3-amd64" +distroImage Mint202 = "linuxmintd/mint20.2-amd64" +distroImage Mint213 = "linuxmintd/mint21.3-amd64" +distroImage Fedora33 = "fedora:33" +distroImage Fedora40 = "fedora:40" +distroImage Rocky8 = "rockylinux:8" + +distroName :: Distro -> String +distroName Debian9 = "deb9" +distroName Debian10 = "deb10" +distroName Debian11 = "deb11" +distroName Debian12 = "deb12" +distroName Ubuntu1804 = "ubuntu1804" +distroName Ubuntu2004 = "ubuntu2004" +distroName Ubuntu2204 = "ubuntu2204" +distroName Mint193 = "mint193" +distroName Mint202 = "mint202" +distroName Mint213 = "mint213" +distroName Fedora33 = "fedora33" +distroName Fedora40 = "fedora40" +distroName Rocky8 = "unknown" + +distroInstall :: Distro -> String +distroInstall Debian9 = "sed -i s/deb.debian.org/archive.debian.org/g /etc/apt/sources.list && sed -i 's|security.debian.org|archive.debian.org/|g' /etc/apt/sources.list && sed -i /-updates/d /etc/apt/sources.list && apt-get update && apt-get install -y" +distroInstall Debian10 = "apt-get update && apt-get install -y" +distroInstall Debian11 = "apt-get update && apt-get install -y" +distroInstall Debian12 = "apt-get update && apt-get install -y" +distroInstall Ubuntu1804 = "apt-get update && apt-get install -y" +distroInstall Ubuntu2004 = "apt-get update && apt-get install -y" +distroInstall Ubuntu2204 = "apt-get update && apt-get install -y" +distroInstall Mint193 = "apt-get update && apt-get install -y" +distroInstall Mint202 = "apt-get update && apt-get install -y" +distroInstall Mint213 = "apt-get update && apt-get install -y" +distroInstall Fedora33 = "dnf install -y" +distroInstall Fedora40 = "dnf install -y" +distroInstall Rocky8 = "yum -y install epel-release && yum install -y --allowerasing" + +distroTools :: Distro -> String +distroTools Debian9 = "libnuma-dev zlib1g-dev libgmp-dev libgmp10 libssl-dev liblzma-dev libbz2-dev git wget lsb-release software-properties-common gnupg2 apt-transport-https gcc autoconf automake build-essential curl ghc gzip libffi-dev libncurses-dev libncurses5 libtinfo5 patchelf" +distroTools Debian10 = "libnuma-dev zlib1g-dev libgmp-dev libgmp10 libssl-dev liblzma-dev libbz2-dev git wget lsb-release software-properties-common gnupg2 apt-transport-https gcc autoconf automake build-essential curl ghc gzip libffi-dev libncurses-dev libncurses5 libtinfo5 patchelf" +distroTools Debian11 = "libnuma-dev zlib1g-dev libgmp-dev libgmp10 libssl-dev liblzma-dev libbz2-dev git wget lsb-release software-properties-common gnupg2 apt-transport-https gcc autoconf automake build-essential curl ghc gzip libffi-dev libncurses-dev libncurses5 libtinfo5 patchelf" +distroTools Debian12 = "libnuma-dev zlib1g-dev libgmp-dev libgmp10 libssl-dev liblzma-dev libbz2-dev git wget lsb-release software-properties-common gnupg2 apt-transport-https gcc autoconf automake build-essential curl ghc gzip libffi-dev libncurses-dev libncurses5 libtinfo5 patchelf" +distroTools Ubuntu1804 = "libnuma-dev zlib1g-dev libgmp-dev libgmp10 libssl-dev liblzma-dev libbz2-dev git wget lsb-release software-properties-common gnupg2 apt-transport-https gcc autoconf automake build-essential curl ghc gzip libffi-dev libncurses-dev libncurses5 libtinfo5 patchelf" +distroTools Ubuntu2004 = "libnuma-dev zlib1g-dev libgmp-dev libgmp10 libssl-dev liblzma-dev libbz2-dev git wget lsb-release software-properties-common gnupg2 apt-transport-https gcc autoconf automake build-essential curl ghc gzip libffi-dev libncurses-dev libncurses5 libtinfo5 patchelf" +distroTools Ubuntu2204 = "libnuma-dev zlib1g-dev libgmp-dev libgmp10 libssl-dev liblzma-dev libbz2-dev git wget lsb-release software-properties-common gnupg2 apt-transport-https gcc autoconf automake build-essential curl ghc gzip libffi-dev libncurses-dev libncurses5 libtinfo5 patchelf" +distroTools Mint193 = "libnuma-dev zlib1g-dev libgmp-dev libgmp10 libssl-dev liblzma-dev libbz2-dev git wget lsb-release software-properties-common gnupg2 apt-transport-https gcc autoconf automake build-essential curl ghc gzip libffi-dev libncurses-dev libncurses5 libtinfo5 patchelf" +distroTools Mint202 = "libnuma-dev zlib1g-dev libgmp-dev libgmp10 libssl-dev liblzma-dev libbz2-dev git wget lsb-release software-properties-common gnupg2 apt-transport-https gcc autoconf automake build-essential curl ghc gzip libffi-dev libncurses-dev libncurses5 libtinfo5 patchelf" +distroTools Mint213 = "libnuma-dev zlib1g-dev libgmp-dev libgmp10 libssl-dev liblzma-dev libbz2-dev git wget lsb-release software-properties-common gnupg2 apt-transport-https gcc autoconf automake build-essential curl ghc gzip libffi-dev libncurses-dev libncurses5 libtinfo5 patchelf" +distroTools Fedora33 = "autoconf automake binutils bzip2 coreutils curl elfutils-devel elfutils-libs findutils gcc gcc-c++ git gmp gmp-devel jq lbzip2 make ncurses ncurses-compat-libs ncurses-devel openssh-clients patch perl pxz python3 sqlite sudo wget which xz zlib-devel patchelf" +distroTools Fedora40 = "autoconf automake binutils bzip2 coreutils curl elfutils-devel elfutils-libs findutils gcc gcc-c++ git gmp gmp-devel jq lbzip2 make ncurses ncurses-compat-libs ncurses-devel openssh-clients patch perl pxz python3 sqlite sudo wget which xz zlib-devel patchelf" +distroTools Rocky8 = "autoconf automake binutils bzip2 coreutils curl elfutils-devel elfutils-libs findutils gcc gcc-c++ git gmp gmp-devel jq lbzip2 make ncurses ncurses-compat-libs ncurses-devel openssh-clients patch perl pxz python3 sqlite sudo wget which xz zlib-devel patchelf" + +------------------------------------------------------------------------------- +-- OS/runner Config +------------------------------------------------------------------------------- + +baseEnv :: [(Key,Value)] +baseEnv = [ "AWS_SECRET_ACCESS_KEY" .= str "${{ secrets.AWS_SECRET_ACCESS_KEY }}" + , "AWS_ACCESS_KEY_ID" .= str "${{ secrets.AWS_ACCESS_KEY_ID }}" + , "S3_HOST" .= str "${{ secrets.S3_HOST }}" + , "TZ" .= str "Asia/Singapore" + ] + +-- | Environment configuration +envVars :: Arch -> Opsys -> Value +envVars arch os = object $ + baseEnv + ++ [ "TARBALL_EXT" .= str (case os of + Windows -> "zip" + _ -> "tar.xz") + , "ARCH" .= str (case arch of + Amd64 -> "64" + AArch64 -> "ARM64") + , "ADD_CABAL_ARGS" .= str (case (os,arch) of + (Linux _, Amd64) -> "--enable-split-sections" + _ -> "") + , "ARTIFACT" .= artifactName arch os + ] + ++ [ "DEBIAN_FRONTEND" .= str "noninteractive" + | Linux _ <- [os] + ] + ++ [ "MACOSX_DEPLOYMENT_TARGET" .= str "10.13" + | Darwin <- [os] + ] + ++ [ "HOMEBREW_CHANGE_ARCH_TO_ARM" .= str "1" + | Darwin <- [os], AArch64 <- [arch] + ] + +-- | Runner selection +runner :: Arch -> Opsys -> [Value] +runner Amd64 (Linux _) = ["ubuntu-latest"] +runner AArch64 (Linux _) = ["self-hosted", "Linux", "ARM64", "maerwald"] +runner Amd64 Darwin = ["macOS-13"] +runner AArch64 Darwin = ["self-hosted", "macOS", "ARM64"] +runner Amd64 Windows = ["windows-latest"] +runner AArch64 Windows = error "aarch64 windows not supported" + +-- | Runner selection for bindist jobs +bindistRunner :: Arch -> Opsys -> [Value] +bindistRunner Amd64 (Linux _) = ["self-hosted", "linux-space", "maerwald"] +bindistRunner AArch64 (Linux _) = ["self-hosted", "Linux", "ARM64", "maerwald"] +bindistRunner Amd64 Darwin = ["macOS-13"] +bindistRunner AArch64 Darwin = ["self-hosted", "macOS", "ARM64"] +bindistRunner Amd64 Windows = ["windows-latest"] +bindistRunner AArch64 Windows = error "aarch64 windows not supported" + +------------------------------------------------------------------------------- +-- Action generatation +------------------------------------------------------------------------------- +-- Each x86-linux job has its own action, living in a separate file +-- The contents of the file are derived from the 'Action' datatype +-- +-- We do this so that we can run the build in the right kind of OS container, +-- but not be forced to run the checkout and upload artifact in the same container +-- +-- This is because we want to use container images that are not supported by +-- github provided actions, see for instance https://github.com/actions/upload-artifact/issues/489 +------------------------------------------------------------------------------- + +-- | Container actions for x86-linux runners. +-- Each of these corresponds to a separate action file, +-- called 'actionName', located at 'actionPath' +data Action + = Action + { actionName :: String + , actionDistro :: Distro + } + +actionDir :: FilePath +actionDir = "./.github/actions/bindist-actions/" + +actionPath :: Distro -> FilePath +actionPath d = actionDir ++ distroActionName d + +instance ToJSON Action where + toJSON Action{..} = object + [ "name" .= actionName + , "description" .= str ("Container for " ++ distroName actionDistro) + , "inputs" .= object + [ "stage" .= object + [ "description" .= str "which stage to build" + , "required" .= True + ] + , "version" .= object + [ "description" .= str "which GHC version to build/test" + , "required" .= False + ] + ] + , "runs" .= object + [ "using" .= str "docker" + , "image" .= distroImage actionDistro + , "entrypoint" .= str ".github/scripts/entrypoint.sh" + , "env" .= object + [ "STAGE" .= str "${{ inputs.stage }}" + , "INSTALL" .= distroInstall actionDistro + , "TOOLS" .= distroTools actionDistro + , "GHC_VERSION" .= str "${{ inputs.version }}" + ] + ] + ] + +configAction :: Config -> Maybe Action +configAction (MkConfig Amd64 (Linux d) _) = Just $ Action (distroActionName d) d +configAction _ = Nothing + +distroActionName :: Distro -> String +distroActionName d = "action-" ++ distroName d + +customAction :: Distro -> Stage -> Value +customAction d st = flip (ghAction stepName (actionPath d)) [] $ case st of + Build v -> + [ "stage" .= str "BUILD" + , "version" .= ghcVersion v + ] + Test -> + [ "stage" .= str "TEST" + ] + Bindist -> + [ "stage" .= str "BINDIST" + ] + where + stepName = case st of + Build v -> "Build " ++ ghcVersion v + Test -> "Test" + Bindist -> "Bindist" + +------------------------------------------------------------------------------- +-- CI generation +------------------------------------------------------------------------------- +-- This is the code that generates the bindist workflow + +-- | Global CI config type +data CI = CI [Config] + +data Config = MkConfig Arch Opsys [GHC] + +instance ToJSON CI where + toJSON (CI cs) = object + [ "name" .= str "Build and release" + , "on" .= object [ "push" .= object ["tags" .= [str "*"]] + , "schedule" .= [object ["cron" .= str "0 2 * * 1"]] + ] + , "env" .= object + [ "CABAL_CACHE_DISABLE" .= str "${{ vars.CABAL_CACHE_DISABLE }}" + , "CABAL_CACHE_NONFATAL" .= str "${{ vars.CABAL_CACHE_NONFATAL }}" + ] + , "jobs" .= object (concatMap (getConfigJobs . makeJobs) cs ++ [releaseJob cs]) + ] + +type Job = Pair + +data ConfigJobs = ConfigJobs { buildJobs :: [Job], bindistJob :: Job, testJob :: Job} + +getConfigJobs :: ConfigJobs -> [Job] +getConfigJobs ConfigJobs{..} = buildJobs ++ [bindistJob, testJob] + +makeJobs :: Config -> ConfigJobs +makeJobs (MkConfig arch os vs) = + ConfigJobs + { buildJobs = [ buildJob arch os ver | ver <- vs ] + , bindistJob = mkBindistJob arch os vs + , testJob = mkTestJob arch os + } + +buildJobName :: Arch -> Opsys -> GHC -> String +buildJobName arch os version = L.intercalate "-" ["build",archName arch, osName os, ghcVersionIdent version] + +testJobName :: Arch -> Opsys -> String +testJobName arch os = L.intercalate "-" ["test",archName arch, osName os] + +bindistJobName :: Arch -> Opsys -> String +bindistJobName arch os = L.intercalate "-" ["bindist",archName arch, osName os] + +bindistName :: Arch -> Opsys -> String +bindistName arch os = "bindist-" ++ artifactName arch os + +setupAction :: Arch -> Opsys -> [Value] +-- some +setupAction AArch64 (Linux Ubuntu2004) = + [ ghRun "clean and git config for aarch64-linux" "bash" [] $ unlines + [ "find . -mindepth 1 -maxdepth 1 -exec rm -rf -- {} +" + , "git config --global --get-all safe.directory | grep '^\\*$' || git config --global --add safe.directory \"*\"" + ] + ] +setupAction _ _ = [] + +releaseJob :: [Config] -> Job +releaseJob cs = + "release" .= object + [ "name" .= str "release" + , "runs-on" .= str "ubuntu-latest" + , "needs" .= [testJobName arch os | MkConfig arch os _ <- cs] + , "if" .= str "startsWith(github.ref, 'refs/tags/')" + , "steps" .= ( [ checkoutAction ] + ++ [ downloadArtifacts (bindistName arch os) "./out" | MkConfig arch os _ <- cs] + ++ [ ghRun "Prepare release" "bash" [] $ unlines + [ "sudo apt-get update && sudo apt-get install -y tar xz-utils" + , "cd out/plan.json" + , "tar cf plan_json.tar *" + , "mv plan_json.tar ../" + , "cd ../.." + , "export RELEASE=$GITHUB_REF_NAME" + , "git archive --format=tar.gz -o \"out/haskell-language-server-${RELEASE}-src.tar.gz\" --prefix=\"haskell-language-server-${RELEASE}/\" HEAD" + ] + , ghAction "Release" "softprops/action-gh-release@v2" + [ "draft" .= True + , "files" .= unlines + [ "./out/*.zip" + , "./out/*.tar.xz" + , "./out/*.tar.gz" + , "./out/*.tar" + ] + ] [] + ]) + ] + + + +buildJob :: Arch -> Opsys -> GHC -> Job +buildJob arch os v = + K.fromString (buildJobName arch os v) .= object + [ "runs-on" .= runner arch os + , "name" .= str (buildJobName arch os v ++ " (Build binaries)") + , "environment" .= str "CI" + , "env" .= thisEnv + , "steps" .= + ( setupAction arch os + ++ [ checkoutAction ] + ++ buildStep arch os + ++ [uploadArtifacts ("artifacts-"++buildJobName arch os v) outputname]) + ] + + where thisEnv = envVars arch os + art = artifactName arch os + outputname + | Windows <- os = "./out/*" + | otherwise = ("out-"++art++"-"++ghcVersion v++".tar") + buildStep Amd64 (Linux d) = [customAction d (Build v)] + buildStep AArch64 (Linux Ubuntu2004) = + [ ghAction "Build aarch64-linux binaries" "docker://hasufell/arm64v8-ubuntu-haskell:focal" + [ "args" .= str "bash .github/scripts/build.sh" ] + [ "GHC_VERSION" .= ghcVersion v ] + , ghAction "Tar aarch64-linux binaries" "docker://hasufell/arm64v8-ubuntu-haskell:focal" + [ "args" .= str "bash .github/scripts/tar.sh" ] + [ "GHC_VERSION" .= ghcVersion v ] + ] + buildStep AArch64 (Linux _) = error "aarch64-linux non-ubuntu not supported" + + buildStep Amd64 Darwin = [ghRun "Run build" "sh" ["GHC_VERSION" .= ghcVersion v] $ unlines $ + [ "brew install coreutils tree" + , "bash .github/scripts/build.sh" + , "tar cf out-${ARTIFACT}-${GHC_VERSION}.tar out/ store/" + ] + ] + buildStep AArch64 Darwin = [ghRun "Run build" "sh" ["GHC_VERSION" .= ghcVersion v] $ unlines $ + [ "bash .github/scripts/brew.sh git coreutils autoconf automake tree" + , "export PATH=\"$HOME/.brew/bin:$HOME/.brew/sbin:$PATH\"" + , "export LD=ld" + , "bash .github/scripts/build.sh" + , "tar cf out-${ARTIFACT}-${GHC_VERSION}.tar out/ store/" + ] + ] + + buildStep Amd64 Windows = [ghRun "Run build" "pwsh" ["GHC_VERSION" .= ghcVersion v] $ unlines $ + [ "$env:CHERE_INVOKING = 1" + , "$env:MSYS2_PATH_TYPE = \"inherit\"" + , "$ErrorActionPreference = \"Stop\"" + , "C:\\msys64\\usr\\bin\\bash -lc \"bash .github/scripts/build.sh\"" + ] + ] + buildStep AArch64 Windows = error "aarch64 windows not supported" + +mkBindistJob :: Arch -> Opsys -> [GHC] -> Job +mkBindistJob arch os vs = + K.fromString (bindistJobName arch os) .= object + [ "runs-on" .= bindistRunner arch os + , "name" .= (bindistJobName arch os ++ " (Prepare bindist)") + , "needs" .= [buildJobName arch os ver | ver <- vs] + , "env" .= thisEnv + , "steps" .= + ( setupAction arch os + ++ [ checkoutAction ] + ++ [downloadArtifacts ("artifacts-"++buildJobName arch os v) outputPath | v <- vs] + ++ bindistStep arch os + ++ [ uploadArtifacts (bindistName arch os) "./out/*.tar.xz\n./out/plan.json/*\n./out/*.zip" ]) + ] + where thisEnv = envVars arch os + + outputPath + | Windows <- os = "./out" + | otherwise = "./" + + bindistStep Amd64 (Linux d) = [customAction d Bindist] + bindistStep AArch64 (Linux Ubuntu2004) = + [ ghAction "Unpack aarch64-linux binaries" "docker://hasufell/arm64v8-ubuntu-haskell:focal" + [ "args" .= str "bash .github/scripts/untar.sh" ] + [ ] + , ghAction "Tar aarch64-linux binaries" "docker://hasufell/arm64v8-ubuntu-haskell:focal" + [ "args" .= str "bash .github/scripts/bindist.sh" ] + [ ] + ] + bindistStep AArch64 (Linux _) = error "aarch64-linux non-ubuntu not supported" + + bindistStep Amd64 Darwin = [ghRun "Create bindist" "sh" [] $ unlines $ + [ "brew install coreutils tree" + , "for bindist in out-*.tar ; do" + , " tar xf \"${bindist}\"" + , "done" + , "unset bindist" + , "bash .github/scripts/bindist.sh" + ] + ] + bindistStep AArch64 Darwin = [ghRun "Run build" "sh" [] $ unlines $ + [ "bash .github/scripts/brew.sh git coreutils llvm@13 autoconf automake tree" + , "export PATH=\"$HOME/.brew/bin:$HOME/.brew/sbin:$HOME/.brew/opt/llvm@13/bin:$PATH\"" + , "export CC=\"$HOME/.brew/opt/llvm@13/bin/clang\"" + , "export CXX=\"$HOME/.brew/opt/llvm@13/bin/clang++\"" + , "export LD=ld" + , "export AR=\"$HOME/.brew/opt/llvm@13/bin/llvm-ar\"" + , "export RANLIB=\"$HOME/.brew/opt/llvm@13/bin/llvm-ranlib\"" + , "for bindist in out-*.tar ; do" + , " tar xf \"${bindist}\"" + , "done" + , "unset bindist" + , "bash .github/scripts/bindist.sh" + ] + ] + + bindistStep Amd64 Windows = [ghRun "Run build" "pwsh" [] $ unlines $ + [ "C:\\msys64\\usr\\bin\\bash -lc \"pacman --disable-download-timeout --noconfirm -Syuu\"" + , "C:\\msys64\\usr\\bin\\bash -lc \"pacman --disable-download-timeout --noconfirm -Syuu\"" + , "C:\\msys64\\usr\\bin\\bash -lc \"pacman --disable-download-timeout --noconfirm -S unzip zip git\"" + , "taskkill /F /FI \"MODULES eq msys-2.0.dll\"" + , "$env:CHERE_INVOKING = 1" + , "$env:MSYS2_PATH_TYPE = \"inherit\"" + , "C:\\msys64\\usr\\bin\\bash -lc \"bash .github/scripts/bindist.sh\"" + ] + ] + bindistStep AArch64 Windows = error "aarch64 windows not supported" + +mkTestJob :: Arch -> Opsys -> Job +mkTestJob arch os = + K.fromString (testJobName arch os) .= object + [ "runs-on" .= runner arch os + , "name" .= str (testJobName arch os ++ " (Test binaries)") + , "needs" .= [bindistJobName arch os] + , "environment" .= str "CI" + , "env" .= thisEnv + , "steps" .= + ( setupAction arch os + ++ [ checkoutAction , downloadArtifacts (bindistName arch os) "./out" ] + ++ testStep arch os) + ] + where thisEnv = envVars arch os + + testStep Amd64 (Linux d) = [customAction d Test] + testStep AArch64 (Linux Ubuntu2004) = + [ ghAction "Run test" "docker://hasufell/arm64v8-ubuntu-haskell:focal" + [ "args" .= str "bash .github/scripts/test.sh" ] + [ ] + ] + testStep AArch64 (Linux _) = error "aarch64-linux non-ubuntu not supported" + + testStep Amd64 Darwin = [ghRun "Run test" "sh" [] $ unlines $ + [ "brew install coreutils tree" + , "bash .github/scripts/test.sh" + ] + ] + testStep AArch64 Darwin = [ghRun "Run test" "sh" [] $ unlines $ + [ "bash .github/scripts/brew.sh git coreutils llvm@13 autoconf automake tree" + , "export PATH=\"$HOME/.brew/bin:$HOME/.brew/sbin:$HOME/.brew/opt/llvm@13/bin:$PATH\"" + , "export CC=\"$HOME/.brew/opt/llvm@13/bin/clang\"" + , "export CXX=\"$HOME/.brew/opt/llvm@13/bin/clang++\"" + , "export LD=ld" + , "export AR=\"$HOME/.brew/opt/llvm@13/bin/llvm-ar\"" + , "export RANLIB=\"$HOME/.brew/opt/llvm@13/bin/llvm-ranlib\"" + , "bash .github/scripts/test.sh" + ] + ] + + testStep Amd64 Windows = + [ ghRun "install windows deps" "pwsh" [] $ unlines $ + [ "C:\\msys64\\usr\\bin\\bash -lc \"pacman --disable-download-timeout --noconfirm -Syuu\"" + , "C:\\msys64\\usr\\bin\\bash -lc \"pacman --disable-download-timeout --noconfirm -Syuu\"" + , "C:\\msys64\\usr\\bin\\bash -lc \"pacman --disable-download-timeout --noconfirm -S make mingw-w64-x86_64-clang curl autoconf mingw-w64-x86_64-pkgconf ca-certificates base-devel gettext autoconf make libtool automake python p7zip patch unzip zip git\"" + , "taskkill /F /FI \"MODULES eq msys-2.0.dll\"" + ] + , ghRun "Run test" "pwsh" [] $ unlines $ + [ "$env:CHERE_INVOKING = 1" + , "$env:MSYS2_PATH_TYPE = \"inherit\"" + , "C:\\msys64\\usr\\bin\\bash -lc \"bash .github/scripts/test.sh\"" + ] + ] + testStep AArch64 Windows = error "aarch64 windows not supported" + + +ciConfigs :: [Config] +ciConfigs = + [ MkConfig Amd64 Darwin allGHCs + , MkConfig AArch64 Darwin allGHCs + , MkConfig Amd64 Windows allGHCs + , MkConfig AArch64 (Linux Ubuntu2004) allGHCs] + ++ [ MkConfig Amd64 (Linux distro) allGHCs | distro <- allDistros ] + +main :: IO () +main = do + [root] <- getArgs + setCurrentDirectory root + removeDirectoryRecursive actionDir + createDirectoryIfMissing True actionDir + forM_ (mapMaybe configAction ciConfigs) $ \a -> do + let path = actionPath (actionDistro a) + createDirectoryIfMissing True path + BS.writeFile (path "action.yaml") $ encode a + BS.putStr "### DO NOT EDIT - GENERATED FILE\n" + BS.putStr "### This file was generated by ./.github/generate-ci/gen_ci.hs\n" + BS.putStr "### Edit that file and run ./.github/generate-ci/generate-jobs to regenerate\n" + BS.putStr $ encode $ CI ciConfigs + + +------------------------------------------------------------------------------- +-- Utils +------------------------------------------------------------------------------- + +str :: String -> String +str = id + +ghAction :: String -> String -> [(Key,Value)] -> [(Key,Value)] -> Value +ghAction name uses args env = object $ + [ "name" .= name + , "uses" .= uses + ] + ++ case args of + [] -> [] + xs -> [ "with" .= object xs ] + ++ case env of + [] -> [] + xs -> [ "env" .= object xs ] + +ghRun :: String -> String -> [(Key,Value)] -> String -> Value +ghRun name shell env script = object $ + [ "name" .= name + , "shell" .= shell + , "run" .= script + ] + ++ case env of + [] -> [] + xs -> [ "env" .= object xs ] + +checkoutAction :: Value +checkoutAction = ghAction "Checkout" "actions/checkout@v4" [] [] + +uploadArtifacts :: String -> String -> Value +uploadArtifacts name path = ghAction "Upload artifact" "actions/upload-artifact@v4" + [ "if-no-files-found" .= str "error" + , "retention-days" .= (2 :: Int) + , "name" .= name + , "path" .= path + ] [] + +downloadArtifacts :: String -> String -> Value +downloadArtifacts name path = ghAction "Download artifacts" "actions/download-artifact@v4" [ "name" .= name, "path" .= path ] [] diff --git a/.github/generate-ci/generate-ci.cabal b/.github/generate-ci/generate-ci.cabal new file mode 100644 index 0000000000..ae9e9d3f52 --- /dev/null +++ b/.github/generate-ci/generate-ci.cabal @@ -0,0 +1,18 @@ +cabal-version: 3.0 +name: generate-ci +version: 0.1.0.0 +license: Apache-2.0 +license-file: LICENSE +build-type: Simple + +executable generate-ci + main-is: gen_ci.hs + ghc-options: -Wall + build-depends: base, + bytestring, + containers, + directory, + filepath, + aeson, + yaml >= 0.11.11.2 + default-language: Haskell2010 diff --git a/.github/generate-ci/generate-jobs b/.github/generate-ci/generate-jobs new file mode 100755 index 0000000000..4cffc82d2a --- /dev/null +++ b/.github/generate-ci/generate-jobs @@ -0,0 +1,9 @@ +#!/usr/bin/env bash + +set -e + +root="$(git rev-parse --show-toplevel)/" +cd "$root/.github/generate-ci/" + +cabal run -v0 generate-ci "$root" > ../workflows/release.yaml + diff --git a/.github/mergify.yml b/.github/mergify.yml index 15e2dd2653..c0b76f7eec 100644 --- a/.github/mergify.yml +++ b/.github/mergify.yml @@ -2,26 +2,19 @@ queue_rules: - name: default # Mergify always respects the branch protection settings # so we can left empty mergify own ones - conditions: [] - -pull_request_rules: - - name: Automatically merge pull requests - conditions: + queue_conditions: - label=merge me - '#approved-reviews-by>=1' - actions: - queue: - method: squash - name: default - # The queue action automatically updates PRs that - # have entered the queue, but in order to do that - # they must have passed CI. Since our CI is a bit - # flaky, PRs can fail to get in, which then means - # they don't get updated, which is extra annoying. - # This just adds the updating as an independent - # step. + merge_conditions: [] + merge_method: squash + +pull_request_rules: - name: Automatically update pull requests conditions: - label=merge me actions: update: + - name: refactored queue action rule + conditions: [] + actions: + queue: diff --git a/.github/scripts/bindist.sh b/.github/scripts/bindist.sh index 72e8fe4676..b50aeb2aca 100644 --- a/.github/scripts/bindist.sh +++ b/.github/scripts/bindist.sh @@ -5,10 +5,7 @@ set -eux . .github/scripts/env.sh . .github/scripts/common.sh -# ensure ghcup -if ! command -v ghcup ; then - install_ghcup -fi +install_ghcup # create tarball/zip case "${TARBALL_EXT}" in @@ -24,8 +21,8 @@ case "${TARBALL_EXT}" in # from the oldest version in the list : "${GHCS:="$(cd "$CI_PROJECT_DIR/out/${ARTIFACT}" && rm -f ./*.json && for ghc in * ; do printf "%s\n" "$ghc" ; done | sort -r | tr '\n' ' ')"}" emake --version - emake GHCUP=ghcup ARTIFACT="${ARTIFACT}" GHCS="${GHCS}" bindist - emake GHCUP=ghcup ARTIFACT="${ARTIFACT}" bindist-tar + emake GHCUP=ghcup ARTIFACT="${ARTIFACT}" GHCS="${GHCS}" bindist || fail_with_ghcup_logs "make bindist failed" + emake GHCUP=ghcup ARTIFACT="${ARTIFACT}" bindist-tar || fail_with_ghcup_logs "make bindist failed" ;; *) fail "Unknown TARBALL_EXT: ${TARBALL_EXT}" diff --git a/.github/scripts/brew.sh b/.github/scripts/brew.sh index 0f889c6299..4066dfb885 100644 --- a/.github/scripts/brew.sh +++ b/.github/scripts/brew.sh @@ -19,9 +19,7 @@ mkdir -p $CI_PROJECT_DIR/.brew_cache export HOMEBREW_CACHE=$CI_PROJECT_DIR/.brew_cache mkdir -p $CI_PROJECT_DIR/.brew_logs export HOMEBREW_LOGS=$CI_PROJECT_DIR/.brew_logs -mkdir -p /private/tmp/.brew_tmp -export HOMEBREW_TEMP=/private/tmp/.brew_tmp +export HOMEBREW_TEMP=$(mktemp -d) #brew update brew install ${1+"$@"} - diff --git a/.github/scripts/build.sh b/.github/scripts/build.sh index d27a940e14..1c0eae6252 100644 --- a/.github/scripts/build.sh +++ b/.github/scripts/build.sh @@ -11,7 +11,9 @@ uname pwd env -# ensure ghcup +# Ensure ghcup is present and properly configured. +# Sets up the vanilla channel, as HLS CI provides binaries +# for GHCup's vanilla channel. install_ghcup # ensure cabal-cache @@ -19,7 +21,7 @@ download_cabal_cache "$HOME/.local/bin/cabal-cache" # build -ghcup install ghc "${GHC_VERSION}" +ghcup install ghc "${GHC_VERSION}" || fail_with_ghcup_logs "install ghc" ghcup set ghc "${GHC_VERSION}" sed -i.bak -e '/DELETE MARKER FOR CI/,/END DELETE/d' cabal.project # see comment in cabal.project ecabal update diff --git a/.github/scripts/common.sh b/.github/scripts/common.sh index b57623b6fe..a10d84045e 100644 --- a/.github/scripts/common.sh +++ b/.github/scripts/common.sh @@ -137,6 +137,9 @@ install_ghcup() { else curl --proto '=https' --tlsv1.2 -sSf https://get-ghcup.haskell.org | BOOTSTRAP_HASKELL_MINIMAL=1 sh source "$(dirname "${GHCUP_BIN}")/env" + # make sure we use the vanilla channel for installing binaries + # see https://github.com/haskell/ghcup-metadata/pull/166#issuecomment-1893075575 + ghcup config set url-source https://raw.githubusercontent.com/haskell/ghcup-metadata/refs/heads/master/ghcup-vanilla-0.0.9.yaml ghcup install cabal --set "${BOOTSTRAP_HASKELL_CABAL_VERSION}" fi } @@ -179,6 +182,10 @@ error() { echo_color "${RED}" "$1"; } warn() { echo_color "${LT_BROWN}" "$1"; } info() { echo_color "${LT_BLUE}" "$1"; } +fail_with_ghcup_logs() { + cat /github/workspace/.ghcup/logs/* + fail "$!" +} fail() { error "error: $1"; exit 1; } run() { diff --git a/.github/scripts/entrypoint.sh b/.github/scripts/entrypoint.sh new file mode 100755 index 0000000000..f02e4ec17a --- /dev/null +++ b/.github/scripts/entrypoint.sh @@ -0,0 +1,32 @@ +#!/bin/bash + +set -x + +bash -c "$INSTALL curl bash git tree $TOOLS" + +unset INSTALL +unset TOOLS + +if [ "${ARTIFACT}" = "x86_64-linux-unknown" ]; then + echo "NAME=Linux" > /etc/os-release + echo "ID=linux" >> /etc/os-release + echo "PRETTY_NAME=Linux" >> /etc/os-release +fi + +case "$STAGE" in + "BUILD") + bash .github/scripts/build.sh + tar cf out-${ARTIFACT}-${GHC_VERSION}.tar out/ store/ + ;; + "BINDIST") + set -eux + for bindist in out-*.tar ; do + tar -xf "${bindist}" + done + unset bindist + bash .github/scripts/bindist.sh + ;; + "TEST") + bash .github/scripts/test.sh +esac + diff --git a/.github/scripts/env.sh b/.github/scripts/env.sh index 018892dee4..2f6eaa3c48 100644 --- a/.github/scripts/env.sh +++ b/.github/scripts/env.sh @@ -11,9 +11,10 @@ fi export PATH="$HOME/.local/bin:$PATH" export BOOTSTRAP_HASKELL_NONINTERACTIVE=1 -export BOOTSTRAP_HASKELL_CABAL_VERSION="${CABAL_VER:-3.8.1.0}" +export BOOTSTRAP_HASKELL_CABAL_VERSION="${CABAL_VER:-3.10.3.0}" export BOOTSTRAP_HASKELL_ADJUST_CABAL_CONFIG=no export BOOTSTRAP_HASKELL_INSTALL_NO_STACK=yes +export BOOTSTRAP_HASKELL_ADJUST_BASHRC=1 if [ "${RUNNER_OS}" = "Windows" ] ; then # on windows use pwd to get unix style path @@ -34,3 +35,5 @@ fi export DEBIAN_FRONTEND=noninteractive export TZ=Asia/Singapore +export LANG=en_US.UTF-8 +export LC_ALL=C.UTF-8 diff --git a/.github/scripts/test.sh b/.github/scripts/test.sh index 396f185008..00638dca62 100644 --- a/.github/scripts/test.sh +++ b/.github/scripts/test.sh @@ -8,14 +8,25 @@ set -eux . .github/scripts/env.sh . .github/scripts/common.sh -test_package="bytestring-0.11.1.0" -test_module="Data/ByteString.hs" +test_package="text-2.1.2" +test_module="src/Data/Text.hs" create_cradle() { echo "cradle:" > hie.yaml echo " cabal:" >> hie.yaml } +# Tests and benchmarks can't be built on some GHC versions, such as GHC 9.10.1 on Windows. +# Disable these packages for now, building bytestring-0.12.1.0 works completely fine. +create_cabal_project() { + echo "packages: ./" > cabal.project + echo "" >> cabal.project + echo "tests: False" >> cabal.project + echo "benchmarks: False" >> cabal.project + + echo "flags: -simdutf -pure-haskell" >> cabal.project +} + enter_test_package() { local tmp_dir tmp_dir=$(mktempdir) @@ -38,7 +49,7 @@ test_all_hls() { bin_noexe=${bin/.exe/} if ! [[ "${bin_noexe}" =~ "haskell-language-server-wrapper" ]] && ! [[ "${bin_noexe}" =~ "~" ]] ; then if ghcup install ghc --set "${bin_noexe/haskell-language-server-/}" ; then - "${hls}" typecheck "${test_module}" || fail "failed to typecheck with HLS for GHC ${bin_noexe/haskell-language-server-/}" + "${hls}" --debug typecheck "${test_module}" || fail "failed to typecheck with HLS for GHC ${bin_noexe/haskell-language-server-/}" # After running the test, free up disk space by deleting the unneeded GHC version. # Helps us staying beneath the 14GB SSD disk limit. @@ -48,6 +59,8 @@ test_all_hls() { fi fi done + # install the recommended GHC version so the wrapper can launch HLS + ghcup install ghc --set 9.10.2 "$bindir/haskell-language-server-wrapper${ext}" typecheck "${test_module}" || fail "failed to typecheck with HLS wrapper" } @@ -58,7 +71,7 @@ env # ensure ghcup install_ghcup -ghcup install ghc --set 9.4.5 +ghcup install ghc --set 9.4.8 (cd .. && ecabal update) # run cabal update outside project dir @@ -75,6 +88,7 @@ case "${TARBALL_EXT}" in enter_test_package create_cradle + create_cabal_project test_all_hls "$GHCUP_BIN" ;; @@ -104,6 +118,7 @@ case "${TARBALL_EXT}" in enter_test_package create_cradle + create_cabal_project test_all_hls "$(ghcup whereis bindir)" ;; diff --git a/.github/workflows/bench.yml b/.github/workflows/bench.yml index 3c822b7cf3..82a50589e4 100644 --- a/.github/workflows/bench.yml +++ b/.github/workflows/bench.yml @@ -46,9 +46,14 @@ jobs: strategy: fail-fast: false matrix: + # benching the two latest GHCs we support now + # since benchmark are expansive. + # choosing the two latest are easier to maintain and more forward looking + # see discussion https://github.com/haskell/haskell-language-server/pull/4118 + # also possible to add more GHCs if we performs better in the future. ghc: - - '9.2' - - '9.4' + - '9.8' + - '9.10' os: - ubuntu-latest @@ -56,7 +61,7 @@ jobs: # change of the strategy may require changing the bootstrapping/run code steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 with: # By default, the `pull_request` event has a `GITHUB_SHA` env variable @@ -95,46 +100,47 @@ jobs: tar -czf cabal.tar.gz * - name: Upload workspace - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: name: workspace-${{ matrix.ghc }}-${{ matrix.os }} retention-days: 1 path: workspace.tar.gz - name: Upload .cabal - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: name: cabal-home-${{ matrix.ghc }}-${{ matrix.os }} retention-days: 1 path: ~/.cabal/cabal.tar.gz bench_example: + if: contains(github.event.pull_request.labels.*.name, 'performance') needs: [bench_init, pre_job] runs-on: ${{ matrix.os }} strategy: fail-fast: false matrix: - ghc: ['9.2', '9.4'] + ghc: ['9.8', '9.10'] os: [ubuntu-latest] - cabal: ['3.10'] + cabal: ['3.14'] example: ['cabal', 'lsp-types'] steps: - - uses: haskell/actions/setup@v2.4.7 + - uses: haskell-actions/setup@v2.8.0 with: ghc-version : ${{ matrix.ghc }} cabal-version: ${{ matrix.cabal }} enable-stack: false - name: Download cabal home - uses: actions/download-artifact@v3 + uses: actions/download-artifact@v4 with: name: cabal-home-${{ matrix.ghc }}-${{ matrix.os }} path: . - name: Download workspace - uses: actions/download-artifact@v3 + uses: actions/download-artifact@v4 with: name: workspace-${{ matrix.ghc }}-${{ matrix.os }} path: . @@ -151,12 +157,15 @@ jobs: - name: Display results run: | column -s, -t < bench-results/unprofiled/${{ matrix.example }}/results.csv | tee bench-results/unprofiled/${{ matrix.example }}/results.txt + echo + echo "Performance Diff(comparing to its previous Version):" + column -s, -t < bench-results/unprofiled/${{ matrix.example }}/resultDiff.csv | tee bench-results/unprofiled/${{ matrix.example }}/resultDiff.txt - name: tar benchmarking artifacts run: find bench-results -name "*.csv" -or -name "*.svg" -or -name "*.html" | xargs tar -czf benchmark-artifacts.tar.gz - name: Archive benchmarking artifacts - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: name: bench-results-${{ matrix.example }}-${{ runner.os }}-${{ matrix.ghc }} path: benchmark-artifacts.tar.gz @@ -166,7 +175,7 @@ jobs: run: find bench-results -name "*.log" -or -name "*.hp" | xargs tar -czf benchmark-logs.tar.gz - name: Archive benchmark logs - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: name: bench-logs-${{ matrix.example }}-${{ runner.os }}-${{ matrix.ghc }} path: benchmark-logs.tar.gz diff --git a/.github/workflows/caching.yml b/.github/workflows/caching.yml index 96616cc4b4..569d380951 100644 --- a/.github/workflows/caching.yml +++ b/.github/workflows/caching.yml @@ -50,7 +50,6 @@ env: cabalBuild: "v2-build --keep-going" jobs: - pre_job: runs-on: ubuntu-latest outputs: @@ -96,13 +95,13 @@ jobs: - uses: ./.github/actions/setup-build with: ghc: ${{ matrix.ghc }} - os: ${{ runner.os }} + os: ${{ runner.os }} # Download sources for feeding build sources cache # Fetching from github cache is faster than doing it from hackage # Sources does not change per ghc and ghc version son only doing it # for one matrix job (it is arbitrary) - - if: steps.compiled-deps.outputs.cache-hit != 'true' && runner.os == 'Linux' && matrix.ghc == '9.2' + - if: steps.compiled-deps.outputs.cache-hit != 'true' && runner.os == 'Linux' && matrix.ghc == '9.6' name: Download sources run: | cabal $cabalBuild --only-download --enable-benchmarks --enable-tests @@ -117,7 +116,7 @@ jobs: # We build ghcide with benchs and test enabled to include its dependencies in the cache # (including shake-bench) # Only for the same ghc and os used in the bench workflow, so we save cache space - - if: steps.compiled-deps.outputs.cache-hit != 'true' && runner.os == 'Linux' && matrix.ghc == '9.2' + - if: steps.compiled-deps.outputs.cache-hit != 'true' && runner.os == 'Linux' && matrix.ghc == '9.6' name: Build ghcide benchmark run: | cabal $cabalBuild ghcide --enable-benchmarks --enable-tests diff --git a/.github/workflows/flags.yml b/.github/workflows/flags.yml index 52d971a046..111dbd40a7 100644 --- a/.github/workflows/flags.yml +++ b/.github/workflows/flags.yml @@ -69,16 +69,21 @@ jobs: ghc: ${{ matrix.ghc }} os: ${{ runner.os }} - - name: Build `hls-graph` with flags - run: cabal v2-build hls-graph --flags="embed-files stm-stats" + # The purpose of this job is to ensure that the build works even with flags + # in their non-default settings. Below we: + # - enable flags that are off by default + # - disable flags that are on by default + - name: Configue non-default flags for all components + run: | + cabal configure \ + --constraint "haskell-language-server +pedantic" \ + --constraint "hls-graph +embed-files +pedantic +stm-stats" \ + --constraint "ghcide +ekg +executable +test-exe" \ + --constraint "hls-plugin-api +pedantic -use-fingertree" + cat cabal.project.local - - name: Build `ghcide` with flags - run: cabal v2-build ghcide --flags="ghc-patched-unboxed-bytecode test-exe executable bench-exe ekg" - - # wingman fails with flags on 9.0, so this can be removed when that's gone - - if: matrix.ghc != '9.0' - name: Build with pedantic (-WError) - run: cabal v2-build --flags="pedantic" + - name: Build everything with non-default flags + run: cabal build all flags_post_job: if: always() diff --git a/.github/workflows/hlint.yml b/.github/workflows/hlint.yml index 62d8742039..c17bfec921 100644 --- a/.github/workflows/hlint.yml +++ b/.github/workflows/hlint.yml @@ -15,12 +15,12 @@ jobs: - name: 'Installing' uses: rwe/actions-hlint-setup@v1 with: - version: '3.5' + version: '3.8' - name: 'Checking code' uses: rwe/actions-hlint-run@v2 with: - hlint-bin: "hlint --with-group=extra" + hlint-bin: "hlint --with-group=extra --ignore-glob=**/testdata/** --ignore-glob=**/test/data/**" fail-on: error path: . diff --git a/.github/workflows/nix.yml b/.github/workflows/nix.yml index 59d0419342..bdd770acd0 100644 --- a/.github/workflows/nix.yml +++ b/.github/workflows/nix.yml @@ -44,23 +44,27 @@ jobs: strategy: fail-fast: false matrix: - os: [ubuntu-latest, macOS-latest] + # TODO: Fix compilation problems on macOS. + # os: [ubuntu-latest, macOS-latest] + os: [ubuntu-latest] steps: - uses: actions/checkout@v3 - - uses: cachix/install-nix-action@v23 + - uses: cachix/install-nix-action@v31 with: extra_nix_config: | experimental-features = nix-command flakes nix_path: nixpkgs=channel:nixos-unstable - - uses: cachix/cachix-action@v12 + - uses: cachix/cachix-action@v16 with: name: haskell-language-server authToken: ${{ secrets.HLS_CACHIX_AUTH_TOKEN }} + # Don't try and run the build (although that would be a good + # test), it takes a long time and we have sometimes had + # glibc incompatibilities with the runners - run: | - nix develop --print-build-logs --command cabal update - nix develop --print-build-logs --command cabal build + nix develop --print-build-logs --command true nix_post_job: if: always() diff --git a/.github/workflows/pre-commit.yml b/.github/workflows/pre-commit.yml index 4a3ab9ff5a..40d79afbf2 100644 --- a/.github/workflows/pre-commit.yml +++ b/.github/workflows/pre-commit.yml @@ -27,7 +27,7 @@ jobs: - uses: ./.github/actions/setup-build with: # select a stable GHC version - ghc: 9.2 + ghc: 9.6 os: ${{ runner.os }} shorten-hls: false @@ -53,7 +53,7 @@ jobs: ${{ inputs.cache-prefix }}${{ env.cache-name }}-${{ inputs.os }}-${{ inputs.ghc }}- ${{ inputs.cache-prefix }}${{ env.cache-name }}-${{ inputs.os }}- - - uses: actions/setup-python@v4 - - uses: pre-commit/action@v3.0.0 + - uses: actions/setup-python@v3 + - uses: pre-commit/action@v3.0.1 with: extra_args: --files ${{ needs.file-diff.outputs.git-diff }} diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index 90c00d94b7..30c55d375a 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -1,1023 +1,3833 @@ -name: Build and release - -on: - push: - tags: - - '*' - schedule: - - cron: '0 2 * * *' +### DO NOT EDIT - GENERATED FILE +### This file was generated by ./.github/generate-ci/gen_ci.hs +### Edit that file and run ./.github/generate-ci/generate-jobs to regenerate env: CABAL_CACHE_DISABLE: ${{ vars.CABAL_CACHE_DISABLE }} CABAL_CACHE_NONFATAL: ${{ vars.CABAL_CACHE_NONFATAL }} - jobs: - build-linux: - name: Build linux binaries - ## We need the environment here, to have access to the `vars` context. - ## Allows us to specify: `CABAL_CACHE_DISABLE=yes`. - ## The environments can be seen in https://github.com/haskell/haskell-language-server/settings/environments - ## assuming you have the proper permissions. - environment: CI - runs-on: ubuntu-latest + bindist-aarch64-linux-ubuntu2004: + env: + ADD_CABAL_ARGS: '' + ARCH: ARM64 + ARTIFACT: aarch64-linux-ubuntu2004 + AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} + AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} + DEBIAN_FRONTEND: noninteractive + S3_HOST: ${{ secrets.S3_HOST }} + TARBALL_EXT: tar.xz + TZ: Asia/Singapore + name: bindist-aarch64-linux-ubuntu2004 (Prepare bindist) + needs: + - build-aarch64-linux-ubuntu2004-967 + - build-aarch64-linux-ubuntu2004-984 + - build-aarch64-linux-ubuntu2004-9102 + - build-aarch64-linux-ubuntu2004-9122 + runs-on: + - self-hosted + - Linux + - ARM64 + - maerwald + steps: + - name: clean and git config for aarch64-linux + run: | + find . -mindepth 1 -maxdepth 1 -exec rm -rf -- {} + + git config --global --get-all safe.directory | grep '^\*$' || git config --global --add safe.directory "*" + shell: bash + - name: Checkout + uses: actions/checkout@v4 + - name: Download artifacts + uses: actions/download-artifact@v4 + with: + name: artifacts-build-aarch64-linux-ubuntu2004-967 + path: ./ + - name: Download artifacts + uses: actions/download-artifact@v4 + with: + name: artifacts-build-aarch64-linux-ubuntu2004-984 + path: ./ + - name: Download artifacts + uses: actions/download-artifact@v4 + with: + name: artifacts-build-aarch64-linux-ubuntu2004-9102 + path: ./ + - name: Download artifacts + uses: actions/download-artifact@v4 + with: + name: artifacts-build-aarch64-linux-ubuntu2004-9122 + path: ./ + - name: Unpack aarch64-linux binaries + uses: docker://hasufell/arm64v8-ubuntu-haskell:focal + with: + args: bash .github/scripts/untar.sh + - name: Tar aarch64-linux binaries + uses: docker://hasufell/arm64v8-ubuntu-haskell:focal + with: + args: bash .github/scripts/bindist.sh + - name: Upload artifact + uses: actions/upload-artifact@v4 + with: + if-no-files-found: error + name: bindist-aarch64-linux-ubuntu2004 + path: |- + ./out/*.tar.xz + ./out/plan.json/* + ./out/*.zip + retention-days: 2 + bindist-aarch64-mac: + env: + ADD_CABAL_ARGS: '' + ARCH: ARM64 + ARTIFACT: aarch64-apple-darwin + AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} + AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} + HOMEBREW_CHANGE_ARCH_TO_ARM: '1' + MACOSX_DEPLOYMENT_TARGET: '10.13' + S3_HOST: ${{ secrets.S3_HOST }} + TARBALL_EXT: tar.xz + TZ: Asia/Singapore + name: bindist-aarch64-mac (Prepare bindist) + needs: + - build-aarch64-mac-967 + - build-aarch64-mac-984 + - build-aarch64-mac-9102 + - build-aarch64-mac-9122 + runs-on: + - self-hosted + - macOS + - ARM64 + steps: + - name: Checkout + uses: actions/checkout@v4 + - name: Download artifacts + uses: actions/download-artifact@v4 + with: + name: artifacts-build-aarch64-mac-967 + path: ./ + - name: Download artifacts + uses: actions/download-artifact@v4 + with: + name: artifacts-build-aarch64-mac-984 + path: ./ + - name: Download artifacts + uses: actions/download-artifact@v4 + with: + name: artifacts-build-aarch64-mac-9102 + path: ./ + - name: Download artifacts + uses: actions/download-artifact@v4 + with: + name: artifacts-build-aarch64-mac-9122 + path: ./ + - name: Run build + run: | + bash .github/scripts/brew.sh git coreutils llvm@13 autoconf automake tree + export PATH="$HOME/.brew/bin:$HOME/.brew/sbin:$HOME/.brew/opt/llvm@13/bin:$PATH" + export CC="$HOME/.brew/opt/llvm@13/bin/clang" + export CXX="$HOME/.brew/opt/llvm@13/bin/clang++" + export LD=ld + export AR="$HOME/.brew/opt/llvm@13/bin/llvm-ar" + export RANLIB="$HOME/.brew/opt/llvm@13/bin/llvm-ranlib" + for bindist in out-*.tar ; do + tar xf "${bindist}" + done + unset bindist + bash .github/scripts/bindist.sh + shell: sh + - name: Upload artifact + uses: actions/upload-artifact@v4 + with: + if-no-files-found: error + name: bindist-aarch64-apple-darwin + path: |- + ./out/*.tar.xz + ./out/plan.json/* + ./out/*.zip + retention-days: 2 + bindist-x86_64-linux-deb10: + env: + ADD_CABAL_ARGS: --enable-split-sections + ARCH: '64' + ARTIFACT: x86_64-linux-deb10 + AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} + AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} + DEBIAN_FRONTEND: noninteractive + S3_HOST: ${{ secrets.S3_HOST }} + TARBALL_EXT: tar.xz + TZ: Asia/Singapore + name: bindist-x86_64-linux-deb10 (Prepare bindist) + needs: + - build-x86_64-linux-deb10-967 + - build-x86_64-linux-deb10-984 + - build-x86_64-linux-deb10-9102 + - build-x86_64-linux-deb10-9122 + runs-on: + - self-hosted + - linux-space + - maerwald + steps: + - name: Checkout + uses: actions/checkout@v4 + - name: Download artifacts + uses: actions/download-artifact@v4 + with: + name: artifacts-build-x86_64-linux-deb10-967 + path: ./ + - name: Download artifacts + uses: actions/download-artifact@v4 + with: + name: artifacts-build-x86_64-linux-deb10-984 + path: ./ + - name: Download artifacts + uses: actions/download-artifact@v4 + with: + name: artifacts-build-x86_64-linux-deb10-9102 + path: ./ + - name: Download artifacts + uses: actions/download-artifact@v4 + with: + name: artifacts-build-x86_64-linux-deb10-9122 + path: ./ + - name: Bindist + uses: ./.github/actions/bindist-actions/action-deb10 + with: + stage: BINDIST + - name: Upload artifact + uses: actions/upload-artifact@v4 + with: + if-no-files-found: error + name: bindist-x86_64-linux-deb10 + path: |- + ./out/*.tar.xz + ./out/plan.json/* + ./out/*.zip + retention-days: 2 + bindist-x86_64-linux-deb11: + env: + ADD_CABAL_ARGS: --enable-split-sections + ARCH: '64' + ARTIFACT: x86_64-linux-deb11 + AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} + AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} + DEBIAN_FRONTEND: noninteractive + S3_HOST: ${{ secrets.S3_HOST }} + TARBALL_EXT: tar.xz + TZ: Asia/Singapore + name: bindist-x86_64-linux-deb11 (Prepare bindist) + needs: + - build-x86_64-linux-deb11-967 + - build-x86_64-linux-deb11-984 + - build-x86_64-linux-deb11-9102 + - build-x86_64-linux-deb11-9122 + runs-on: + - self-hosted + - linux-space + - maerwald + steps: + - name: Checkout + uses: actions/checkout@v4 + - name: Download artifacts + uses: actions/download-artifact@v4 + with: + name: artifacts-build-x86_64-linux-deb11-967 + path: ./ + - name: Download artifacts + uses: actions/download-artifact@v4 + with: + name: artifacts-build-x86_64-linux-deb11-984 + path: ./ + - name: Download artifacts + uses: actions/download-artifact@v4 + with: + name: artifacts-build-x86_64-linux-deb11-9102 + path: ./ + - name: Download artifacts + uses: actions/download-artifact@v4 + with: + name: artifacts-build-x86_64-linux-deb11-9122 + path: ./ + - name: Bindist + uses: ./.github/actions/bindist-actions/action-deb11 + with: + stage: BINDIST + - name: Upload artifact + uses: actions/upload-artifact@v4 + with: + if-no-files-found: error + name: bindist-x86_64-linux-deb11 + path: |- + ./out/*.tar.xz + ./out/plan.json/* + ./out/*.zip + retention-days: 2 + bindist-x86_64-linux-deb12: + env: + ADD_CABAL_ARGS: --enable-split-sections + ARCH: '64' + ARTIFACT: x86_64-linux-deb12 + AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} + AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} + DEBIAN_FRONTEND: noninteractive + S3_HOST: ${{ secrets.S3_HOST }} + TARBALL_EXT: tar.xz + TZ: Asia/Singapore + name: bindist-x86_64-linux-deb12 (Prepare bindist) + needs: + - build-x86_64-linux-deb12-967 + - build-x86_64-linux-deb12-984 + - build-x86_64-linux-deb12-9102 + - build-x86_64-linux-deb12-9122 + runs-on: + - self-hosted + - linux-space + - maerwald + steps: + - name: Checkout + uses: actions/checkout@v4 + - name: Download artifacts + uses: actions/download-artifact@v4 + with: + name: artifacts-build-x86_64-linux-deb12-967 + path: ./ + - name: Download artifacts + uses: actions/download-artifact@v4 + with: + name: artifacts-build-x86_64-linux-deb12-984 + path: ./ + - name: Download artifacts + uses: actions/download-artifact@v4 + with: + name: artifacts-build-x86_64-linux-deb12-9102 + path: ./ + - name: Download artifacts + uses: actions/download-artifact@v4 + with: + name: artifacts-build-x86_64-linux-deb12-9122 + path: ./ + - name: Bindist + uses: ./.github/actions/bindist-actions/action-deb12 + with: + stage: BINDIST + - name: Upload artifact + uses: actions/upload-artifact@v4 + with: + if-no-files-found: error + name: bindist-x86_64-linux-deb12 + path: |- + ./out/*.tar.xz + ./out/plan.json/* + ./out/*.zip + retention-days: 2 + bindist-x86_64-linux-deb9: + env: + ADD_CABAL_ARGS: --enable-split-sections + ARCH: '64' + ARTIFACT: x86_64-linux-deb9 + AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} + AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} + DEBIAN_FRONTEND: noninteractive + S3_HOST: ${{ secrets.S3_HOST }} + TARBALL_EXT: tar.xz + TZ: Asia/Singapore + name: bindist-x86_64-linux-deb9 (Prepare bindist) + needs: + - build-x86_64-linux-deb9-967 + - build-x86_64-linux-deb9-984 + - build-x86_64-linux-deb9-9102 + - build-x86_64-linux-deb9-9122 + runs-on: + - self-hosted + - linux-space + - maerwald + steps: + - name: Checkout + uses: actions/checkout@v4 + - name: Download artifacts + uses: actions/download-artifact@v4 + with: + name: artifacts-build-x86_64-linux-deb9-967 + path: ./ + - name: Download artifacts + uses: actions/download-artifact@v4 + with: + name: artifacts-build-x86_64-linux-deb9-984 + path: ./ + - name: Download artifacts + uses: actions/download-artifact@v4 + with: + name: artifacts-build-x86_64-linux-deb9-9102 + path: ./ + - name: Download artifacts + uses: actions/download-artifact@v4 + with: + name: artifacts-build-x86_64-linux-deb9-9122 + path: ./ + - name: Bindist + uses: ./.github/actions/bindist-actions/action-deb9 + with: + stage: BINDIST + - name: Upload artifact + uses: actions/upload-artifact@v4 + with: + if-no-files-found: error + name: bindist-x86_64-linux-deb9 + path: |- + ./out/*.tar.xz + ./out/plan.json/* + ./out/*.zip + retention-days: 2 + bindist-x86_64-linux-fedora33: + env: + ADD_CABAL_ARGS: --enable-split-sections + ARCH: '64' + ARTIFACT: x86_64-linux-fedora33 + AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} + AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} + DEBIAN_FRONTEND: noninteractive + S3_HOST: ${{ secrets.S3_HOST }} + TARBALL_EXT: tar.xz + TZ: Asia/Singapore + name: bindist-x86_64-linux-fedora33 (Prepare bindist) + needs: + - build-x86_64-linux-fedora33-967 + - build-x86_64-linux-fedora33-984 + - build-x86_64-linux-fedora33-9102 + - build-x86_64-linux-fedora33-9122 + runs-on: + - self-hosted + - linux-space + - maerwald + steps: + - name: Checkout + uses: actions/checkout@v4 + - name: Download artifacts + uses: actions/download-artifact@v4 + with: + name: artifacts-build-x86_64-linux-fedora33-967 + path: ./ + - name: Download artifacts + uses: actions/download-artifact@v4 + with: + name: artifacts-build-x86_64-linux-fedora33-984 + path: ./ + - name: Download artifacts + uses: actions/download-artifact@v4 + with: + name: artifacts-build-x86_64-linux-fedora33-9102 + path: ./ + - name: Download artifacts + uses: actions/download-artifact@v4 + with: + name: artifacts-build-x86_64-linux-fedora33-9122 + path: ./ + - name: Bindist + uses: ./.github/actions/bindist-actions/action-fedora33 + with: + stage: BINDIST + - name: Upload artifact + uses: actions/upload-artifact@v4 + with: + if-no-files-found: error + name: bindist-x86_64-linux-fedora33 + path: |- + ./out/*.tar.xz + ./out/plan.json/* + ./out/*.zip + retention-days: 2 + bindist-x86_64-linux-fedora40: env: + ADD_CABAL_ARGS: --enable-split-sections + ARCH: '64' + ARTIFACT: x86_64-linux-fedora40 + AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} + AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} + DEBIAN_FRONTEND: noninteractive + S3_HOST: ${{ secrets.S3_HOST }} + TARBALL_EXT: tar.xz + TZ: Asia/Singapore + name: bindist-x86_64-linux-fedora40 (Prepare bindist) + needs: + - build-x86_64-linux-fedora40-967 + - build-x86_64-linux-fedora40-984 + - build-x86_64-linux-fedora40-9102 + - build-x86_64-linux-fedora40-9122 + runs-on: + - self-hosted + - linux-space + - maerwald + steps: + - name: Checkout + uses: actions/checkout@v4 + - name: Download artifacts + uses: actions/download-artifact@v4 + with: + name: artifacts-build-x86_64-linux-fedora40-967 + path: ./ + - name: Download artifacts + uses: actions/download-artifact@v4 + with: + name: artifacts-build-x86_64-linux-fedora40-984 + path: ./ + - name: Download artifacts + uses: actions/download-artifact@v4 + with: + name: artifacts-build-x86_64-linux-fedora40-9102 + path: ./ + - name: Download artifacts + uses: actions/download-artifact@v4 + with: + name: artifacts-build-x86_64-linux-fedora40-9122 + path: ./ + - name: Bindist + uses: ./.github/actions/bindist-actions/action-fedora40 + with: + stage: BINDIST + - name: Upload artifact + uses: actions/upload-artifact@v4 + with: + if-no-files-found: error + name: bindist-x86_64-linux-fedora40 + path: |- + ./out/*.tar.xz + ./out/plan.json/* + ./out/*.zip + retention-days: 2 + bindist-x86_64-linux-mint193: + env: + ADD_CABAL_ARGS: --enable-split-sections + ARCH: '64' + ARTIFACT: x86_64-linux-mint193 + AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} + AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} + DEBIAN_FRONTEND: noninteractive + S3_HOST: ${{ secrets.S3_HOST }} + TARBALL_EXT: tar.xz + TZ: Asia/Singapore + name: bindist-x86_64-linux-mint193 (Prepare bindist) + needs: + - build-x86_64-linux-mint193-967 + - build-x86_64-linux-mint193-984 + - build-x86_64-linux-mint193-9102 + - build-x86_64-linux-mint193-9122 + runs-on: + - self-hosted + - linux-space + - maerwald + steps: + - name: Checkout + uses: actions/checkout@v4 + - name: Download artifacts + uses: actions/download-artifact@v4 + with: + name: artifacts-build-x86_64-linux-mint193-967 + path: ./ + - name: Download artifacts + uses: actions/download-artifact@v4 + with: + name: artifacts-build-x86_64-linux-mint193-984 + path: ./ + - name: Download artifacts + uses: actions/download-artifact@v4 + with: + name: artifacts-build-x86_64-linux-mint193-9102 + path: ./ + - name: Download artifacts + uses: actions/download-artifact@v4 + with: + name: artifacts-build-x86_64-linux-mint193-9122 + path: ./ + - name: Bindist + uses: ./.github/actions/bindist-actions/action-mint193 + with: + stage: BINDIST + - name: Upload artifact + uses: actions/upload-artifact@v4 + with: + if-no-files-found: error + name: bindist-x86_64-linux-mint193 + path: |- + ./out/*.tar.xz + ./out/plan.json/* + ./out/*.zip + retention-days: 2 + bindist-x86_64-linux-mint202: + env: + ADD_CABAL_ARGS: --enable-split-sections + ARCH: '64' + ARTIFACT: x86_64-linux-mint202 + AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} + AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} + DEBIAN_FRONTEND: noninteractive + S3_HOST: ${{ secrets.S3_HOST }} + TARBALL_EXT: tar.xz + TZ: Asia/Singapore + name: bindist-x86_64-linux-mint202 (Prepare bindist) + needs: + - build-x86_64-linux-mint202-967 + - build-x86_64-linux-mint202-984 + - build-x86_64-linux-mint202-9102 + - build-x86_64-linux-mint202-9122 + runs-on: + - self-hosted + - linux-space + - maerwald + steps: + - name: Checkout + uses: actions/checkout@v4 + - name: Download artifacts + uses: actions/download-artifact@v4 + with: + name: artifacts-build-x86_64-linux-mint202-967 + path: ./ + - name: Download artifacts + uses: actions/download-artifact@v4 + with: + name: artifacts-build-x86_64-linux-mint202-984 + path: ./ + - name: Download artifacts + uses: actions/download-artifact@v4 + with: + name: artifacts-build-x86_64-linux-mint202-9102 + path: ./ + - name: Download artifacts + uses: actions/download-artifact@v4 + with: + name: artifacts-build-x86_64-linux-mint202-9122 + path: ./ + - name: Bindist + uses: ./.github/actions/bindist-actions/action-mint202 + with: + stage: BINDIST + - name: Upload artifact + uses: actions/upload-artifact@v4 + with: + if-no-files-found: error + name: bindist-x86_64-linux-mint202 + path: |- + ./out/*.tar.xz + ./out/plan.json/* + ./out/*.zip + retention-days: 2 + bindist-x86_64-linux-mint213: + env: + ADD_CABAL_ARGS: --enable-split-sections + ARCH: '64' + ARTIFACT: x86_64-linux-mint213 + AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} + AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} + DEBIAN_FRONTEND: noninteractive + S3_HOST: ${{ secrets.S3_HOST }} + TARBALL_EXT: tar.xz + TZ: Asia/Singapore + name: bindist-x86_64-linux-mint213 (Prepare bindist) + needs: + - build-x86_64-linux-mint213-967 + - build-x86_64-linux-mint213-984 + - build-x86_64-linux-mint213-9102 + - build-x86_64-linux-mint213-9122 + runs-on: + - self-hosted + - linux-space + - maerwald + steps: + - name: Checkout + uses: actions/checkout@v4 + - name: Download artifacts + uses: actions/download-artifact@v4 + with: + name: artifacts-build-x86_64-linux-mint213-967 + path: ./ + - name: Download artifacts + uses: actions/download-artifact@v4 + with: + name: artifacts-build-x86_64-linux-mint213-984 + path: ./ + - name: Download artifacts + uses: actions/download-artifact@v4 + with: + name: artifacts-build-x86_64-linux-mint213-9102 + path: ./ + - name: Download artifacts + uses: actions/download-artifact@v4 + with: + name: artifacts-build-x86_64-linux-mint213-9122 + path: ./ + - name: Bindist + uses: ./.github/actions/bindist-actions/action-mint213 + with: + stage: BINDIST + - name: Upload artifact + uses: actions/upload-artifact@v4 + with: + if-no-files-found: error + name: bindist-x86_64-linux-mint213 + path: |- + ./out/*.tar.xz + ./out/plan.json/* + ./out/*.zip + retention-days: 2 + bindist-x86_64-linux-ubuntu1804: + env: + ADD_CABAL_ARGS: --enable-split-sections + ARCH: '64' + ARTIFACT: x86_64-linux-ubuntu1804 + AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} + AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} + DEBIAN_FRONTEND: noninteractive + S3_HOST: ${{ secrets.S3_HOST }} + TARBALL_EXT: tar.xz + TZ: Asia/Singapore + name: bindist-x86_64-linux-ubuntu1804 (Prepare bindist) + needs: + - build-x86_64-linux-ubuntu1804-967 + - build-x86_64-linux-ubuntu1804-984 + - build-x86_64-linux-ubuntu1804-9102 + - build-x86_64-linux-ubuntu1804-9122 + runs-on: + - self-hosted + - linux-space + - maerwald + steps: + - name: Checkout + uses: actions/checkout@v4 + - name: Download artifacts + uses: actions/download-artifact@v4 + with: + name: artifacts-build-x86_64-linux-ubuntu1804-967 + path: ./ + - name: Download artifacts + uses: actions/download-artifact@v4 + with: + name: artifacts-build-x86_64-linux-ubuntu1804-984 + path: ./ + - name: Download artifacts + uses: actions/download-artifact@v4 + with: + name: artifacts-build-x86_64-linux-ubuntu1804-9102 + path: ./ + - name: Download artifacts + uses: actions/download-artifact@v4 + with: + name: artifacts-build-x86_64-linux-ubuntu1804-9122 + path: ./ + - name: Bindist + uses: ./.github/actions/bindist-actions/action-ubuntu1804 + with: + stage: BINDIST + - name: Upload artifact + uses: actions/upload-artifact@v4 + with: + if-no-files-found: error + name: bindist-x86_64-linux-ubuntu1804 + path: |- + ./out/*.tar.xz + ./out/plan.json/* + ./out/*.zip + retention-days: 2 + bindist-x86_64-linux-ubuntu2004: + env: + ADD_CABAL_ARGS: --enable-split-sections + ARCH: '64' + ARTIFACT: x86_64-linux-ubuntu2004 + AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} + AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} + DEBIAN_FRONTEND: noninteractive + S3_HOST: ${{ secrets.S3_HOST }} + TARBALL_EXT: tar.xz + TZ: Asia/Singapore + name: bindist-x86_64-linux-ubuntu2004 (Prepare bindist) + needs: + - build-x86_64-linux-ubuntu2004-967 + - build-x86_64-linux-ubuntu2004-984 + - build-x86_64-linux-ubuntu2004-9102 + - build-x86_64-linux-ubuntu2004-9122 + runs-on: + - self-hosted + - linux-space + - maerwald + steps: + - name: Checkout + uses: actions/checkout@v4 + - name: Download artifacts + uses: actions/download-artifact@v4 + with: + name: artifacts-build-x86_64-linux-ubuntu2004-967 + path: ./ + - name: Download artifacts + uses: actions/download-artifact@v4 + with: + name: artifacts-build-x86_64-linux-ubuntu2004-984 + path: ./ + - name: Download artifacts + uses: actions/download-artifact@v4 + with: + name: artifacts-build-x86_64-linux-ubuntu2004-9102 + path: ./ + - name: Download artifacts + uses: actions/download-artifact@v4 + with: + name: artifacts-build-x86_64-linux-ubuntu2004-9122 + path: ./ + - name: Bindist + uses: ./.github/actions/bindist-actions/action-ubuntu2004 + with: + stage: BINDIST + - name: Upload artifact + uses: actions/upload-artifact@v4 + with: + if-no-files-found: error + name: bindist-x86_64-linux-ubuntu2004 + path: |- + ./out/*.tar.xz + ./out/plan.json/* + ./out/*.zip + retention-days: 2 + bindist-x86_64-linux-ubuntu2204: + env: + ADD_CABAL_ARGS: --enable-split-sections + ARCH: '64' + ARTIFACT: x86_64-linux-ubuntu2204 + AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} + AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} + DEBIAN_FRONTEND: noninteractive + S3_HOST: ${{ secrets.S3_HOST }} + TARBALL_EXT: tar.xz + TZ: Asia/Singapore + name: bindist-x86_64-linux-ubuntu2204 (Prepare bindist) + needs: + - build-x86_64-linux-ubuntu2204-967 + - build-x86_64-linux-ubuntu2204-984 + - build-x86_64-linux-ubuntu2204-9102 + - build-x86_64-linux-ubuntu2204-9122 + runs-on: + - self-hosted + - linux-space + - maerwald + steps: + - name: Checkout + uses: actions/checkout@v4 + - name: Download artifacts + uses: actions/download-artifact@v4 + with: + name: artifacts-build-x86_64-linux-ubuntu2204-967 + path: ./ + - name: Download artifacts + uses: actions/download-artifact@v4 + with: + name: artifacts-build-x86_64-linux-ubuntu2204-984 + path: ./ + - name: Download artifacts + uses: actions/download-artifact@v4 + with: + name: artifacts-build-x86_64-linux-ubuntu2204-9102 + path: ./ + - name: Download artifacts + uses: actions/download-artifact@v4 + with: + name: artifacts-build-x86_64-linux-ubuntu2204-9122 + path: ./ + - name: Bindist + uses: ./.github/actions/bindist-actions/action-ubuntu2204 + with: + stage: BINDIST + - name: Upload artifact + uses: actions/upload-artifact@v4 + with: + if-no-files-found: error + name: bindist-x86_64-linux-ubuntu2204 + path: |- + ./out/*.tar.xz + ./out/plan.json/* + ./out/*.zip + retention-days: 2 + bindist-x86_64-linux-unknown: + env: + ADD_CABAL_ARGS: --enable-split-sections + ARCH: '64' + ARTIFACT: x86_64-linux-unknown + AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} + DEBIAN_FRONTEND: noninteractive + S3_HOST: ${{ secrets.S3_HOST }} + TARBALL_EXT: tar.xz + TZ: Asia/Singapore + name: bindist-x86_64-linux-unknown (Prepare bindist) + needs: + - build-x86_64-linux-unknown-967 + - build-x86_64-linux-unknown-984 + - build-x86_64-linux-unknown-9102 + - build-x86_64-linux-unknown-9122 + runs-on: + - self-hosted + - linux-space + - maerwald + steps: + - name: Checkout + uses: actions/checkout@v4 + - name: Download artifacts + uses: actions/download-artifact@v4 + with: + name: artifacts-build-x86_64-linux-unknown-967 + path: ./ + - name: Download artifacts + uses: actions/download-artifact@v4 + with: + name: artifacts-build-x86_64-linux-unknown-984 + path: ./ + - name: Download artifacts + uses: actions/download-artifact@v4 + with: + name: artifacts-build-x86_64-linux-unknown-9102 + path: ./ + - name: Download artifacts + uses: actions/download-artifact@v4 + with: + name: artifacts-build-x86_64-linux-unknown-9122 + path: ./ + - name: Bindist + uses: ./.github/actions/bindist-actions/action-unknown + with: + stage: BINDIST + - name: Upload artifact + uses: actions/upload-artifact@v4 + with: + if-no-files-found: error + name: bindist-x86_64-linux-unknown + path: |- + ./out/*.tar.xz + ./out/plan.json/* + ./out/*.zip + retention-days: 2 + bindist-x86_64-mac: + env: + ADD_CABAL_ARGS: '' + ARCH: '64' + ARTIFACT: x86_64-apple-darwin AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} + AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} + MACOSX_DEPLOYMENT_TARGET: '10.13' S3_HOST: ${{ secrets.S3_HOST }} TARBALL_EXT: tar.xz - ARCH: 64 - DEBIAN_FRONTEND: noninteractive - TZ: Asia/Singapore - strategy: - fail-fast: false - matrix: - ghc: ["9.8.1", "9.6.3", "9.4.7", "9.2.8"] - platform: [ { image: "debian:9" - , installCmd: "sed -i s/deb.debian.org/archive.debian.org/g /etc/apt/sources.list && sed -i 's|security.debian.org|archive.debian.org/|g' /etc/apt/sources.list && sed -i /-updates/d /etc/apt/sources.list && apt-get update && apt-get install -y" - , toolRequirements: "libnuma-dev zlib1g-dev libgmp-dev libgmp10 libssl-dev liblzma-dev libbz2-dev git wget lsb-release software-properties-common gnupg2 apt-transport-https gcc autoconf automake build-essential curl ghc gzip libffi-dev libncurses-dev libncurses5 libtinfo5 patchelf" - , DISTRO: "Debian" - , ARTIFACT: "x86_64-linux-deb9" - , ADD_CABAL_ARGS: "--enable-split-sections" - }, - { image: "debian:10" - , installCmd: "apt-get update && apt-get install -y" - , toolRequirements: "libnuma-dev zlib1g-dev libgmp-dev libgmp10 libssl-dev liblzma-dev libbz2-dev git wget lsb-release software-properties-common gnupg2 apt-transport-https gcc autoconf automake build-essential curl ghc gzip libffi-dev libncurses-dev libncurses5 libtinfo5 patchelf" - , DISTRO: "Debian" - , ARTIFACT: "x86_64-linux-deb10" - , ADD_CABAL_ARGS: "--enable-split-sections" - }, - { image: "debian:11" - , installCmd: "apt-get update && apt-get install -y" - , toolRequirements: "libnuma-dev zlib1g-dev libgmp-dev libgmp10 libssl-dev liblzma-dev libbz2-dev git wget lsb-release software-properties-common gnupg2 apt-transport-https gcc autoconf automake build-essential curl ghc gzip libffi-dev libncurses-dev libncurses5 libtinfo5 patchelf" - , DISTRO: "Debian" - , ARTIFACT: "x86_64-linux-deb11" - , ADD_CABAL_ARGS: "--enable-split-sections" - }, - { image: "ubuntu:18.04" - , installCmd: "apt-get update && apt-get install -y" - , toolRequirements: "libnuma-dev zlib1g-dev libgmp-dev libgmp10 libssl-dev liblzma-dev libbz2-dev git wget lsb-release software-properties-common gnupg2 apt-transport-https gcc autoconf automake build-essential curl ghc gzip libffi-dev libncurses-dev libncurses5 libtinfo5 patchelf" - , DISTRO: "Ubuntu" - , ARTIFACT: "x86_64-linux-ubuntu18.04" - , ADD_CABAL_ARGS: "--enable-split-sections" - }, - { image: "ubuntu:20.04" - , installCmd: "apt-get update && apt-get install -y" - , toolRequirements: "libnuma-dev zlib1g-dev libgmp-dev libgmp10 libssl-dev liblzma-dev libbz2-dev git wget lsb-release software-properties-common gnupg2 apt-transport-https gcc autoconf automake build-essential curl ghc gzip libffi-dev libncurses-dev libncurses5 libtinfo5 patchelf" - , DISTRO: "Ubuntu" - , ARTIFACT: "x86_64-linux-ubuntu20.04" - , ADD_CABAL_ARGS: "--enable-split-sections" - }, - { image: "ubuntu:22.04" - , installCmd: "apt-get update && apt-get install -y" - , toolRequirements: "libnuma-dev zlib1g-dev libgmp-dev libgmp10 libssl-dev liblzma-dev libbz2-dev git wget lsb-release software-properties-common gnupg2 apt-transport-https gcc autoconf automake build-essential curl ghc gzip libffi-dev libncurses-dev libncurses5 libtinfo5 patchelf" - , DISTRO: "Ubuntu" - , ARTIFACT: "x86_64-linux-ubuntu22.04" - , ADD_CABAL_ARGS: "--enable-split-sections" - }, - { image: "linuxmintd/mint19.3-amd64" - , installCmd: "apt-get update && apt-get install -y" - , toolRequirements: "libnuma-dev zlib1g-dev libgmp-dev libgmp10 libssl-dev liblzma-dev libbz2-dev git wget lsb-release software-properties-common gnupg2 apt-transport-https gcc autoconf automake build-essential curl ghc gzip libffi-dev libncurses-dev libncurses5 libtinfo5 patchelf" - , DISTRO: "Mint" - , ARTIFACT: "x86_64-linux-mint19.3" - , ADD_CABAL_ARGS: "--enable-split-sections" - }, - { image: "linuxmintd/mint20.2-amd64" - , installCmd: "apt-get update && apt-get install -y" - , toolRequirements: "libnuma-dev zlib1g-dev libgmp-dev libgmp10 libssl-dev liblzma-dev libbz2-dev git wget lsb-release software-properties-common gnupg2 apt-transport-https gcc autoconf automake build-essential curl ghc gzip libffi-dev libncurses-dev libncurses5 libtinfo5 patchelf" - , DISTRO: "Mint" - , ARTIFACT: "x86_64-linux-mint20.2" - , ADD_CABAL_ARGS: "--enable-split-sections" - }, - { image: "fedora:27" - , installCmd: "dnf install -y" - , toolRequirements: "autoconf automake binutils bzip2 coreutils curl elfutils-devel elfutils-libs findutils gcc gcc-c++ git gmp gmp-devel jq lbzip2 make ncurses ncurses-compat-libs ncurses-devel openssh-clients patch perl pxz python3 sqlite sudo wget which xz zlib-devel patchelf" - , DISTRO: "Fedora" - , ARTIFACT: "x86_64-linux-fedora27" - , ADD_CABAL_ARGS: "--enable-split-sections" - }, - { image: "fedora:33" - , installCmd: "dnf install -y" - , toolRequirements: "autoconf automake binutils bzip2 coreutils curl elfutils-devel elfutils-libs findutils gcc gcc-c++ git gmp gmp-devel jq lbzip2 make ncurses ncurses-compat-libs ncurses-devel openssh-clients patch perl pxz python3 sqlite sudo wget which xz zlib-devel patchelf" - , DISTRO: "Fedora" - , ARTIFACT: "x86_64-linux-fedora33" - , ADD_CABAL_ARGS: "--enable-split-sections" - }, - { image: "centos:7" - , installCmd: "yum -y install epel-release && yum install -y" - , toolRequirements: "autoconf automake binutils bzip2 coreutils curl elfutils-devel elfutils-libs findutils gcc gcc-c++ git gmp gmp-devel jq lbzip2 make ncurses ncurses-compat-libs ncurses-devel openssh-clients patch perl pxz python3 sqlite sudo wget which xz zlib-devel patchelf" - , DISTRO: "CentOS" - , ARTIFACT: "x86_64-linux-centos7" - , ADD_CABAL_ARGS: "--enable-split-sections" - } - ] - # TODO: rm - # Instead of manually adding the Unknown Linux Bindist jobs here, - # it should be part of the matrix above. - # However, due to GHC 9.4 shenanigans, we need some special logic. - # https://gitlab.haskell.org/ghc/ghc/-/issues/22268 - # - # Perhaps we can migrate *all* unknown linux builds to a uniform - # image. - include: - - ghc: 9.2.8 - platform: - { image: "rockylinux:8" - , installCmd: "yum -y install epel-release && yum install -y --allowerasing" - , toolRequirements: "autoconf automake binutils bzip2 coreutils curl elfutils-devel elfutils-libs findutils gcc gcc-c++ git gmp gmp-devel jq lbzip2 make ncurses ncurses-compat-libs ncurses-devel openssh-clients patch perl pxz python3 sqlite sudo wget which xz zlib-devel patchelf" - , DISTRO: "Unknown" - , ARTIFACT: "x86_64-linux-unknown" - , ADD_CABAL_ARGS: "--enable-split-sections" - } - - ghc: 9.4.7 - platform: - { image: "fedora:27" - , installCmd: "dnf install -y" - , toolRequirements: "autoconf automake binutils bzip2 coreutils curl elfutils-devel elfutils-libs findutils gcc gcc-c++ git gmp gmp-devel jq lbzip2 make ncurses ncurses-compat-libs ncurses-devel openssh-clients patch perl pxz python3 sqlite sudo wget which xz zlib-devel patchelf" - , DISTRO: "Unknown" - , ARTIFACT: "x86_64-linux-unknown" - , ADD_CABAL_ARGS: "--enable-split-sections" - } - - ghc: 9.6.3 - platform: - { image: "rockylinux:8" - , installCmd: "yum -y install epel-release && yum install -y --allowerasing" - , toolRequirements: "autoconf automake binutils bzip2 coreutils curl elfutils-devel elfutils-libs findutils gcc gcc-c++ git gmp gmp-devel jq lbzip2 make ncurses ncurses-compat-libs ncurses-devel openssh-clients patch perl pxz python3 sqlite sudo wget which xz zlib-devel patchelf" - , DISTRO: "Unknown" - , ARTIFACT: "x86_64-linux-unknown" - , ADD_CABAL_ARGS: "--enable-split-sections" - } - - ghc: 9.8.1 - platform: - { image: "rockylinux:8" - , installCmd: "yum -y install epel-release && yum install -y --allowerasing" - , toolRequirements: "autoconf automake binutils bzip2 coreutils curl elfutils-devel elfutils-libs findutils gcc gcc-c++ git gmp gmp-devel jq lbzip2 make ncurses ncurses-compat-libs ncurses-devel openssh-clients patch perl pxz python3 sqlite sudo wget which xz zlib-devel patchelf" - , DISTRO: "Unknown" - , ARTIFACT: "x86_64-linux-unknown" - , ADD_CABAL_ARGS: "--enable-split-sections" - } - container: - image: ${{ matrix.platform.image }} - steps: - - name: Install requirements - shell: sh - run: | - ${{ matrix.platform.installCmd }} curl bash git ${{ matrix.platform.toolRequirements }} - - - if: matrix.platform.DISTRO == 'Unknown' - run: | - echo "NAME=Linux" > /etc/os-release - echo "ID=linux" >> /etc/os-release - echo "PRETTY_NAME=Linux" >> /etc/os-release - - - uses: actions/checkout@v3 - - - name: Run build - run: | - bash .github/scripts/build.sh - tar cf out-${ARTIFACT}-${GHC_VERSION}.tar out/ store/ - - env: - ARTIFACT: ${{ matrix.platform.ARTIFACT }} - DISTRO: ${{ matrix.platform.DISTRO }} - ADD_CABAL_ARGS: ${{ matrix.platform.ADD_CABAL_ARGS }} - GHC_VERSION: ${{ matrix.ghc }} - - - if: always() - name: Upload artifact - uses: actions/upload-artifact@v3 - with: - if-no-files-found: error - retention-days: 2 - name: artifacts-${{ matrix.platform.ARTIFACT }} - path: | - ./out-${{ matrix.platform.ARTIFACT }}-${{ matrix.ghc }}.tar - - build-arm: - name: Build ARM binary - ## We need the environment here, to have access to the `vars` context. - ## Allows us to specify: `CABAL_CACHE_DISABLE=yes`. - ## The environments can be seen in https://github.com/haskell/haskell-language-server/settings/environments - ## assuming you have the proper permissions. - environment: CI - runs-on: [self-hosted, Linux, ARM64] - env: - AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} - AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} - S3_HOST: ${{ secrets.S3_HOST }} - TARBALL_EXT: tar.xz - ADD_CABAL_ARGS: "" - DEBIAN_FRONTEND: noninteractive - TZ: Asia/Singapore - ARTIFACT: "aarch64-linux-ubuntu20" + TZ: Asia/Singapore + name: bindist-x86_64-mac (Prepare bindist) + needs: + - build-x86_64-mac-967 + - build-x86_64-mac-984 + - build-x86_64-mac-9102 + - build-x86_64-mac-9122 + runs-on: + - macOS-13 + steps: + - name: Checkout + uses: actions/checkout@v4 + - name: Download artifacts + uses: actions/download-artifact@v4 + with: + name: artifacts-build-x86_64-mac-967 + path: ./ + - name: Download artifacts + uses: actions/download-artifact@v4 + with: + name: artifacts-build-x86_64-mac-984 + path: ./ + - name: Download artifacts + uses: actions/download-artifact@v4 + with: + name: artifacts-build-x86_64-mac-9102 + path: ./ + - name: Download artifacts + uses: actions/download-artifact@v4 + with: + name: artifacts-build-x86_64-mac-9122 + path: ./ + - name: Create bindist + run: | + brew install coreutils tree + for bindist in out-*.tar ; do + tar xf "${bindist}" + done + unset bindist + bash .github/scripts/bindist.sh + shell: sh + - name: Upload artifact + uses: actions/upload-artifact@v4 + with: + if-no-files-found: error + name: bindist-x86_64-apple-darwin + path: |- + ./out/*.tar.xz + ./out/plan.json/* + ./out/*.zip + retention-days: 2 + bindist-x86_64-windows: + env: + ADD_CABAL_ARGS: '' + ARCH: '64' + ARTIFACT: x86_64-mingw64 + AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} + AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} + S3_HOST: ${{ secrets.S3_HOST }} + TARBALL_EXT: zip + TZ: Asia/Singapore + name: bindist-x86_64-windows (Prepare bindist) + needs: + - build-x86_64-windows-967 + - build-x86_64-windows-984 + - build-x86_64-windows-9102 + - build-x86_64-windows-9122 + runs-on: + - windows-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + - name: Download artifacts + uses: actions/download-artifact@v4 + with: + name: artifacts-build-x86_64-windows-967 + path: ./out + - name: Download artifacts + uses: actions/download-artifact@v4 + with: + name: artifacts-build-x86_64-windows-984 + path: ./out + - name: Download artifacts + uses: actions/download-artifact@v4 + with: + name: artifacts-build-x86_64-windows-9102 + path: ./out + - name: Download artifacts + uses: actions/download-artifact@v4 + with: + name: artifacts-build-x86_64-windows-9122 + path: ./out + - name: Run build + run: | + C:\msys64\usr\bin\bash -lc "pacman --disable-download-timeout --noconfirm -Syuu" + C:\msys64\usr\bin\bash -lc "pacman --disable-download-timeout --noconfirm -Syuu" + C:\msys64\usr\bin\bash -lc "pacman --disable-download-timeout --noconfirm -S unzip zip git" + taskkill /F /FI "MODULES eq msys-2.0.dll" + $env:CHERE_INVOKING = 1 + $env:MSYS2_PATH_TYPE = "inherit" + C:\msys64\usr\bin\bash -lc "bash .github/scripts/bindist.sh" + shell: pwsh + - name: Upload artifact + uses: actions/upload-artifact@v4 + with: + if-no-files-found: error + name: bindist-x86_64-mingw64 + path: |- + ./out/*.tar.xz + ./out/plan.json/* + ./out/*.zip + retention-days: 2 + build-aarch64-linux-ubuntu2004-9102: + env: + ADD_CABAL_ARGS: '' ARCH: ARM64 - DISTRO: Ubuntu - strategy: - fail-fast: true - matrix: - ghc: ["9.8.1", "9.6.3", "9.4.7", "9.2.8" ] - steps: - - uses: docker://arm64v8/ubuntu:focal - name: Cleanup (aarch64 linux) - with: - args: "find . -mindepth 1 -maxdepth 1 -exec rm -rf -- {} +" - - - name: git config - run: | - git config --global --get-all safe.directory | grep '^\*$' || git config --global --add safe.directory "*" - shell: bash - - - name: Checkout code - uses: actions/checkout@v3 - - - uses: docker://hasufell/arm64v8-ubuntu-haskell:focal - name: Run build (aarch64 linux) - with: - args: bash .github/scripts/build.sh - env: - GHC_VERSION: ${{ matrix.ghc }} - - - uses: docker://hasufell/arm64v8-ubuntu-haskell:focal - name: Run build (aarch64 linux) - with: - args: bash .github/scripts/tar.sh - env: - GHC_VERSION: ${{ matrix.ghc }} - - - if: always() - name: Upload artifact - uses: actions/upload-artifact@v3 - with: - if-no-files-found: error - retention-days: 2 - name: artifacts-arm - path: | - ./out-${{ env.ARTIFACT }}-${{ matrix.ghc }}.tar - - build-mac-x86_64: - name: Build binary (Mac x86_64) - ## We need the environment here, to have access to the `vars` context. - ## Allows us to specify: `CABAL_CACHE_DISABLE=yes`. - ## The environments can be seen in https://github.com/haskell/haskell-language-server/settings/environments - ## assuming you have the proper permissions. - environment: CI - runs-on: macOS-11 - env: - MACOSX_DEPLOYMENT_TARGET: 10.13 - AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} - AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} - S3_HOST: ${{ secrets.S3_HOST }} - ADD_CABAL_ARGS: "" - ARTIFACT: "x86_64-apple-darwin" - ARCH: 64 - TARBALL_EXT: tar.xz - DISTRO: na - strategy: - fail-fast: false - matrix: - ghc: ["9.8.1", "9.6.3", "9.4.7", "9.2.8"] - steps: - - name: Checkout code - uses: actions/checkout@v3 - - - name: Run build - run: | - brew install coreutils tree - bash .github/scripts/build.sh - tar cf out-${ARTIFACT}-${GHC_VERSION}.tar out/ store/ - env: - GHC_VERSION: ${{ matrix.ghc }} - - - if: always() - name: Upload artifact - uses: actions/upload-artifact@v3 - with: - if-no-files-found: error - retention-days: 2 - name: artifacts-mac-x86_64 - path: | - ./out-${{ env.ARTIFACT }}-${{ matrix.ghc }}.tar - - build-mac-aarch64: - name: Build binary (Mac aarch64) - ## We need the environment here, to have access to the `vars` context. - ## Allows us to specify: `CABAL_CACHE_DISABLE=yes`. - ## The environments can be seen in https://github.com/haskell/haskell-language-server/settings/environments - ## assuming you have the proper permissions. - environment: CI - runs-on: [self-hosted, macOS, ARM64] - env: - MACOSX_DEPLOYMENT_TARGET: 10.13 - AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} - AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} - S3_HOST: ${{ secrets.S3_HOST }} - ADD_CABAL_ARGS: "" - ARTIFACT: "aarch64-apple-darwin" + ARTIFACT: aarch64-linux-ubuntu2004 + AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} + AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} + DEBIAN_FRONTEND: noninteractive + S3_HOST: ${{ secrets.S3_HOST }} + TARBALL_EXT: tar.xz + TZ: Asia/Singapore + environment: CI + name: build-aarch64-linux-ubuntu2004-9102 (Build binaries) + runs-on: + - self-hosted + - Linux + - ARM64 + - maerwald + steps: + - name: clean and git config for aarch64-linux + run: | + find . -mindepth 1 -maxdepth 1 -exec rm -rf -- {} + + git config --global --get-all safe.directory | grep '^\*$' || git config --global --add safe.directory "*" + shell: bash + - name: Checkout + uses: actions/checkout@v4 + - env: + GHC_VERSION: 9.10.2 + name: Build aarch64-linux binaries + uses: docker://hasufell/arm64v8-ubuntu-haskell:focal + with: + args: bash .github/scripts/build.sh + - env: + GHC_VERSION: 9.10.2 + name: Tar aarch64-linux binaries + uses: docker://hasufell/arm64v8-ubuntu-haskell:focal + with: + args: bash .github/scripts/tar.sh + - name: Upload artifact + uses: actions/upload-artifact@v4 + with: + if-no-files-found: error + name: artifacts-build-aarch64-linux-ubuntu2004-9102 + path: out-aarch64-linux-ubuntu2004-9.10.2.tar + retention-days: 2 + build-aarch64-linux-ubuntu2004-9122: + env: + ADD_CABAL_ARGS: '' ARCH: ARM64 + ARTIFACT: aarch64-linux-ubuntu2004 + AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} + AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} + DEBIAN_FRONTEND: noninteractive + S3_HOST: ${{ secrets.S3_HOST }} TARBALL_EXT: tar.xz - DISTRO: na - HOMEBREW_CHANGE_ARCH_TO_ARM: 1 - strategy: - fail-fast: false - matrix: - ghc: ["9.8.1", "9.6.3", "9.4.7", "9.2.8"] - steps: - - name: Checkout code - uses: actions/checkout@v3 - - - name: Run build - run: | - bash .github/scripts/brew.sh git coreutils autoconf automake tree - export PATH="$HOME/.brew/bin:$HOME/.brew/sbin:$PATH" - export LD=ld - bash .github/scripts/build.sh - tar cf out-${ARTIFACT}-${GHC_VERSION}.tar out/ store/ - env: - GHC_VERSION: ${{ matrix.ghc }} - - - if: always() - name: Upload artifact - uses: actions/upload-artifact@v3 - with: - if-no-files-found: error - retention-days: 2 - name: artifacts-mac-aarch64 - path: | - ./out-${{ env.ARTIFACT }}-${{ matrix.ghc }}.tar - - build-win: - name: Build binary (Win) - ## We need the environment here, to have access to the `vars` context. - ## Allows us to specify: `CABAL_CACHE_DISABLE=yes`. - ## The environments can be seen in https://github.com/haskell/haskell-language-server/settings/environments - ## assuming you have the proper permissions. - environment: CI - runs-on: windows-latest - env: - AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} - AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} - S3_HOST: ${{ secrets.S3_HOST }} - ADD_CABAL_ARGS: "" - ARTIFACT: "x86_64-mingw64" - ARCH: 64 - TARBALL_EXT: "zip" - DISTRO: na - strategy: - fail-fast: false - matrix: - ghc: ["9.8.1", "9.6.3", "9.4.7", "9.2.8"] - steps: - - name: install windows deps - shell: pwsh - run: | - C:\msys64\usr\bin\bash -lc "pacman --disable-download-timeout --noconfirm -Syuu" - C:\msys64\usr\bin\bash -lc "pacman --disable-download-timeout --noconfirm -Syuu" - C:\msys64\usr\bin\bash -lc "pacman --disable-download-timeout --noconfirm -S make mingw-w64-x86_64-clang curl autoconf mingw-w64-x86_64-pkgconf ca-certificates base-devel gettext autoconf make libtool automake python p7zip patch unzip zip git" - taskkill /F /FI "MODULES eq msys-2.0.dll" - - - name: Checkout code - uses: actions/checkout@v3 - - - name: Run build (windows) - run: | - $env:CHERE_INVOKING = 1 - $env:MSYS2_PATH_TYPE = "inherit" - $ErrorActionPreference = "Stop" - C:\msys64\usr\bin\bash -lc "bash .github/scripts/build.sh" - shell: pwsh - env: - GHC_VERSION: ${{ matrix.ghc }} - - - if: always() - name: Upload artifact - uses: actions/upload-artifact@v3 - with: - if-no-files-found: error - retention-days: 2 - name: artifacts-win - path: | - ./out/* - - bindist-linux: - name: Tar linux bindists (linux) - runs-on: [self-hosted, linux-space] - needs: ["build-linux"] - env: - TARBALL_EXT: tar.xz - ARCH: 64 - DEBIAN_FRONTEND: noninteractive - TZ: Asia/Singapore - strategy: - fail-fast: false - matrix: - include: - - image: debian:9 - installCmd: sed -i s/deb.debian.org/archive.debian.org/g /etc/apt/sources.list && sed -i 's|security.debian.org|archive.debian.org/|g' /etc/apt/sources.list && sed -i /-updates/d /etc/apt/sources.list && apt-get update && apt-get install -y - toolRequirements: libnuma-dev zlib1g-dev libgmp-dev libgmp10 libssl-dev liblzma-dev libbz2-dev git wget lsb-release software-properties-common gnupg2 apt-transport-https gcc autoconf automake build-essential curl ghc gzip libffi-dev libncurses-dev libncurses5 libtinfo5 patchelf tree - DISTRO: Debian - ARTIFACT: "x86_64-linux-deb9" - - image: debian:10 - installCmd: apt-get update && apt-get install -y - toolRequirements: libnuma-dev zlib1g-dev libgmp-dev libgmp10 libssl-dev liblzma-dev libbz2-dev git wget lsb-release software-properties-common gnupg2 apt-transport-https gcc autoconf automake build-essential curl ghc gzip libffi-dev libncurses-dev libncurses5 libtinfo5 patchelf tree - DISTRO: Debian - ARTIFACT: "x86_64-linux-deb10" - - image: debian:11 - installCmd: apt-get update && apt-get install -y - toolRequirements: libnuma-dev zlib1g-dev libgmp-dev libgmp10 libssl-dev liblzma-dev libbz2-dev git wget lsb-release software-properties-common gnupg2 apt-transport-https gcc autoconf automake build-essential curl ghc gzip libffi-dev libncurses-dev libncurses5 libtinfo5 patchelf tree - DISTRO: Debian - ARTIFACT: "x86_64-linux-deb11" - - image: ubuntu:18.04 - installCmd: apt-get update && apt-get install -y - toolRequirements: libnuma-dev zlib1g-dev libgmp-dev libgmp10 libssl-dev liblzma-dev libbz2-dev git wget lsb-release software-properties-common gnupg2 apt-transport-https gcc autoconf automake build-essential curl ghc gzip libffi-dev libncurses-dev libncurses5 libtinfo5 patchelf tree - DISTRO: Ubuntu - ARTIFACT: "x86_64-linux-ubuntu18.04" - - image: ubuntu:20.04 - installCmd: apt-get update && apt-get install -y - toolRequirements: libnuma-dev zlib1g-dev libgmp-dev libgmp10 libssl-dev liblzma-dev libbz2-dev git wget lsb-release software-properties-common gnupg2 apt-transport-https gcc autoconf automake build-essential curl ghc gzip libffi-dev libncurses-dev libncurses5 libtinfo5 patchelf tree - DISTRO: Ubuntu - ARTIFACT: "x86_64-linux-ubuntu20.04" - - image: ubuntu:22.04 - installCmd: apt-get update && apt-get install -y - toolRequirements: libnuma-dev zlib1g-dev libgmp-dev libgmp10 libssl-dev liblzma-dev libbz2-dev git wget lsb-release software-properties-common gnupg2 apt-transport-https gcc autoconf automake build-essential curl ghc gzip libffi-dev libncurses-dev libncurses5 libtinfo5 patchelf tree - DISTRO: Ubuntu - ARTIFACT: "x86_64-linux-ubuntu22.04" - - image: fedora:27 - installCmd: dnf install -y - toolRequirements: autoconf automake binutils bzip2 coreutils curl elfutils-devel elfutils-libs findutils gcc gcc-c++ git gmp gmp-devel jq lbzip2 make ncurses ncurses-compat-libs ncurses-devel openssh-clients patch perl pxz python3 sqlite sudo wget which xz zlib-devel patchelf tree - DISTRO: Fedora - ARTIFACT: "x86_64-linux-fedora27" - - image: fedora:33 - installCmd: dnf install -y - toolRequirements: autoconf automake binutils bzip2 coreutils curl elfutils-devel elfutils-libs findutils gcc gcc-c++ git gmp gmp-devel jq lbzip2 make ncurses ncurses-compat-libs ncurses-devel openssh-clients patch perl pxz python3 sqlite sudo wget which xz zlib-devel patchelf tree - DISTRO: Fedora - ARTIFACT: "x86_64-linux-fedora33" - - image: centos:7 - installCmd: yum -y install epel-release && yum install -y - toolRequirements: autoconf automake binutils bzip2 coreutils curl elfutils-devel elfutils-libs findutils gcc gcc-c++ git gmp gmp-devel jq lbzip2 make ncurses ncurses-compat-libs ncurses-devel openssh-clients patch perl pxz python3 sqlite sudo wget which xz zlib-devel patchelf tree - DISTRO: CentOS - ARTIFACT: "x86_64-linux-centos7" - - image: linuxmintd/mint19.3-amd64 - installCmd: apt-get update && apt-get install -y - toolRequirements: libnuma-dev zlib1g-dev libgmp-dev libgmp10 libssl-dev liblzma-dev libbz2-dev git wget lsb-release software-properties-common gnupg2 apt-transport-https gcc autoconf automake build-essential curl ghc gzip libffi-dev libncurses-dev libncurses5 libtinfo5 patchelf tree - DISTRO: Mint - ARTIFACT: "x86_64-linux-mint19.3" - - image: "fedora:33" - installCmd: "dnf install -y" - toolRequirements: "autoconf automake binutils bzip2 coreutils curl elfutils-devel elfutils-libs findutils gcc gcc-c++ git gmp gmp-devel jq lbzip2 make ncurses ncurses-compat-libs ncurses-devel openssh-clients patch perl pxz python3 sqlite sudo wget which xz zlib-devel patchelf tree" - DISTRO: "Unknown" - ARTIFACT: "x86_64-linux-unknown" - - image: linuxmintd/mint20.2-amd64 - installCmd: apt-get update && apt-get install -y - toolRequirements: libnuma-dev zlib1g-dev libgmp-dev libgmp10 libssl-dev liblzma-dev libbz2-dev git wget lsb-release software-properties-common gnupg2 apt-transport-https gcc autoconf automake build-essential curl ghc gzip libffi-dev libncurses-dev libncurses5 libtinfo5 patchelf tree - DISTRO: Mint - ARTIFACT: "x86_64-linux-mint20.2" - container: - image: ${{ matrix.image }} - steps: - - name: Install requirements - shell: sh - run: | - ${{ matrix.installCmd }} curl bash git ${{ matrix.toolRequirements }} - - - if: matrix.DISTRO == 'Unknown' - run: | - echo "NAME=Linux" > /etc/os-release - echo "ID=linux" >> /etc/os-release - echo "PRETTY_NAME=Linux" >> /etc/os-release - - - uses: actions/checkout@v3 - - - uses: actions/download-artifact@v3 - with: - name: artifacts-${{ matrix.ARTIFACT }} - path: ./ - - - name: Create bindist - run: | - set -eux - for bindist in out-*.tar ; do - tar -xf "${bindist}" - done - unset bindist - bash .github/scripts/bindist.sh - env: - ARTIFACT: ${{ matrix.ARTIFACT }} - - - name: Upload bindist - uses: actions/upload-artifact@v3 - with: - if-no-files-found: error - name: bindists-${{ matrix.ARTIFACT }} - path: | - ./out/*.tar.xz - ./out/plan.json/* - - - uses: geekyeggo/delete-artifact@v2 - with: - name: artifacts-${{ matrix.ARTIFACT }} - - bindist-arm: - name: Tar linux bindists (arm) - runs-on: [self-hosted, Linux, ARM64] - needs: ["build-arm"] + TZ: Asia/Singapore + environment: CI + name: build-aarch64-linux-ubuntu2004-9122 (Build binaries) + runs-on: + - self-hosted + - Linux + - ARM64 + - maerwald + steps: + - name: clean and git config for aarch64-linux + run: | + find . -mindepth 1 -maxdepth 1 -exec rm -rf -- {} + + git config --global --get-all safe.directory | grep '^\*$' || git config --global --add safe.directory "*" + shell: bash + - name: Checkout + uses: actions/checkout@v4 + - env: + GHC_VERSION: 9.12.2 + name: Build aarch64-linux binaries + uses: docker://hasufell/arm64v8-ubuntu-haskell:focal + with: + args: bash .github/scripts/build.sh + - env: + GHC_VERSION: 9.12.2 + name: Tar aarch64-linux binaries + uses: docker://hasufell/arm64v8-ubuntu-haskell:focal + with: + args: bash .github/scripts/tar.sh + - name: Upload artifact + uses: actions/upload-artifact@v4 + with: + if-no-files-found: error + name: artifacts-build-aarch64-linux-ubuntu2004-9122 + path: out-aarch64-linux-ubuntu2004-9.12.2.tar + retention-days: 2 + build-aarch64-linux-ubuntu2004-967: env: + ADD_CABAL_ARGS: '' + ARCH: ARM64 + ARTIFACT: aarch64-linux-ubuntu2004 + AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} + AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} + DEBIAN_FRONTEND: noninteractive + S3_HOST: ${{ secrets.S3_HOST }} TARBALL_EXT: tar.xz + TZ: Asia/Singapore + environment: CI + name: build-aarch64-linux-ubuntu2004-967 (Build binaries) + runs-on: + - self-hosted + - Linux + - ARM64 + - maerwald + steps: + - name: clean and git config for aarch64-linux + run: | + find . -mindepth 1 -maxdepth 1 -exec rm -rf -- {} + + git config --global --get-all safe.directory | grep '^\*$' || git config --global --add safe.directory "*" + shell: bash + - name: Checkout + uses: actions/checkout@v4 + - env: + GHC_VERSION: 9.6.7 + name: Build aarch64-linux binaries + uses: docker://hasufell/arm64v8-ubuntu-haskell:focal + with: + args: bash .github/scripts/build.sh + - env: + GHC_VERSION: 9.6.7 + name: Tar aarch64-linux binaries + uses: docker://hasufell/arm64v8-ubuntu-haskell:focal + with: + args: bash .github/scripts/tar.sh + - name: Upload artifact + uses: actions/upload-artifact@v4 + with: + if-no-files-found: error + name: artifacts-build-aarch64-linux-ubuntu2004-967 + path: out-aarch64-linux-ubuntu2004-9.6.7.tar + retention-days: 2 + build-aarch64-linux-ubuntu2004-984: + env: + ADD_CABAL_ARGS: '' ARCH: ARM64 + ARTIFACT: aarch64-linux-ubuntu2004 + AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} + AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} DEBIAN_FRONTEND: noninteractive - ARTIFACT: "aarch64-linux-ubuntu20" - TZ: Asia/Singapore - steps: - - uses: docker://arm64v8/ubuntu:focal - name: Cleanup (aarch64 linux) - with: - args: "find . -mindepth 1 -maxdepth 1 -exec rm -rf -- {} +" - - - name: git config - run: | - git config --global --get-all safe.directory | grep '^\*$' || git config --global --add safe.directory "*" - shell: bash - - - uses: actions/checkout@v3 - - - uses: actions/download-artifact@v3 - with: - name: artifacts-arm - path: ./ - - - uses: docker://hasufell/arm64v8-ubuntu-haskell:focal - name: Unpack - with: - args: bash .github/scripts/untar.sh - - - uses: docker://hasufell/arm64v8-ubuntu-haskell:focal - name: Create bindist (aarch64 linux) - with: - args: bash .github/scripts/bindist.sh - - - name: Upload bindist - uses: actions/upload-artifact@v3 - with: - if-no-files-found: error - name: bindists-arm - path: | - ./out/*.tar.xz - ./out/plan.json/* - - - uses: geekyeggo/delete-artifact@v2 - with: - name: artifacts-arm - - bindist-mac-x86_64: - name: Tar bindists (Mac x86_64) - runs-on: macOS-11 - needs: ["build-mac-x86_64"] - env: - TARBALL_EXT: tar.xz - ARCH: 64 - ARTIFACT: "x86_64-apple-darwin" - steps: - - uses: actions/checkout@v3 - - - uses: actions/download-artifact@v3 - with: - name: artifacts-mac-x86_64 - path: ./ - - - name: Create bindist - run: | - brew install coreutils tree - for bindist in out-*.tar ; do - tar xf "${bindist}" - done - unset bindist - bash .github/scripts/bindist.sh - - - name: Upload bindist - uses: actions/upload-artifact@v3 - with: - if-no-files-found: error - name: bindists-mac-x86_64 - path: | - ./out/*.tar.xz - ./out/plan.json/* - - - uses: geekyeggo/delete-artifact@v2 - with: - name: artifacts-mac-x86_64 - - bindist-mac-aarch64: - name: Tar bindists (Mac aarch64) - runs-on: [self-hosted, macOS, ARM64] - needs: ["build-mac-aarch64"] + S3_HOST: ${{ secrets.S3_HOST }} + TARBALL_EXT: tar.xz + TZ: Asia/Singapore + environment: CI + name: build-aarch64-linux-ubuntu2004-984 (Build binaries) + runs-on: + - self-hosted + - Linux + - ARM64 + - maerwald + steps: + - name: clean and git config for aarch64-linux + run: | + find . -mindepth 1 -maxdepth 1 -exec rm -rf -- {} + + git config --global --get-all safe.directory | grep '^\*$' || git config --global --add safe.directory "*" + shell: bash + - name: Checkout + uses: actions/checkout@v4 + - env: + GHC_VERSION: 9.8.4 + name: Build aarch64-linux binaries + uses: docker://hasufell/arm64v8-ubuntu-haskell:focal + with: + args: bash .github/scripts/build.sh + - env: + GHC_VERSION: 9.8.4 + name: Tar aarch64-linux binaries + uses: docker://hasufell/arm64v8-ubuntu-haskell:focal + with: + args: bash .github/scripts/tar.sh + - name: Upload artifact + uses: actions/upload-artifact@v4 + with: + if-no-files-found: error + name: artifacts-build-aarch64-linux-ubuntu2004-984 + path: out-aarch64-linux-ubuntu2004-9.8.4.tar + retention-days: 2 + build-aarch64-mac-9102: env: + ADD_CABAL_ARGS: '' + ARCH: ARM64 + ARTIFACT: aarch64-apple-darwin + AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} + AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} + HOMEBREW_CHANGE_ARCH_TO_ARM: '1' + MACOSX_DEPLOYMENT_TARGET: '10.13' + S3_HOST: ${{ secrets.S3_HOST }} TARBALL_EXT: tar.xz + TZ: Asia/Singapore + environment: CI + name: build-aarch64-mac-9102 (Build binaries) + runs-on: + - self-hosted + - macOS + - ARM64 + steps: + - name: Checkout + uses: actions/checkout@v4 + - env: + GHC_VERSION: 9.10.2 + name: Run build + run: | + bash .github/scripts/brew.sh git coreutils autoconf automake tree + export PATH="$HOME/.brew/bin:$HOME/.brew/sbin:$PATH" + export LD=ld + bash .github/scripts/build.sh + tar cf out-${ARTIFACT}-${GHC_VERSION}.tar out/ store/ + shell: sh + - name: Upload artifact + uses: actions/upload-artifact@v4 + with: + if-no-files-found: error + name: artifacts-build-aarch64-mac-9102 + path: out-aarch64-apple-darwin-9.10.2.tar + retention-days: 2 + build-aarch64-mac-9122: + env: + ADD_CABAL_ARGS: '' ARCH: ARM64 - ARTIFACT: "aarch64-apple-darwin" - steps: - - uses: actions/checkout@v3 - - - uses: actions/download-artifact@v3 - with: - name: artifacts-mac-aarch64 - path: ./ - - - name: Create bindist - run: | - bash .github/scripts/brew.sh git coreutils llvm@11 autoconf automake tree - export PATH="$HOME/.brew/bin:$HOME/.brew/sbin:$HOME/.brew/opt/llvm@11/bin:$PATH" - export CC="$HOME/.brew/opt/llvm@11/bin/clang" - export CXX="$HOME/.brew/opt/llvm@11/bin/clang++" - export LD=ld - export AR="$HOME/.brew/opt/llvm@11/bin/llvm-ar" - export RANLIB="$HOME/.brew/opt/llvm@11/bin/llvm-ranlib" - for bindist in out-*.tar ; do - tar xf "${bindist}" - done - unset bindist - bash .github/scripts/bindist.sh - - - name: Upload bindist - uses: actions/upload-artifact@v3 - with: - if-no-files-found: error - name: bindists-mac-aarch64 - path: | - ./out/*.tar.xz - ./out/plan.json/* - - - uses: geekyeggo/delete-artifact@v2 - with: - name: artifacts-mac-aarch64 - - bindist-win: - name: Tar bindists (Windows) - runs-on: windows-latest - needs: ["build-win"] + ARTIFACT: aarch64-apple-darwin + AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} + AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} + HOMEBREW_CHANGE_ARCH_TO_ARM: '1' + MACOSX_DEPLOYMENT_TARGET: '10.13' + S3_HOST: ${{ secrets.S3_HOST }} + TARBALL_EXT: tar.xz + TZ: Asia/Singapore + environment: CI + name: build-aarch64-mac-9122 (Build binaries) + runs-on: + - self-hosted + - macOS + - ARM64 + steps: + - name: Checkout + uses: actions/checkout@v4 + - env: + GHC_VERSION: 9.12.2 + name: Run build + run: | + bash .github/scripts/brew.sh git coreutils autoconf automake tree + export PATH="$HOME/.brew/bin:$HOME/.brew/sbin:$PATH" + export LD=ld + bash .github/scripts/build.sh + tar cf out-${ARTIFACT}-${GHC_VERSION}.tar out/ store/ + shell: sh + - name: Upload artifact + uses: actions/upload-artifact@v4 + with: + if-no-files-found: error + name: artifacts-build-aarch64-mac-9122 + path: out-aarch64-apple-darwin-9.12.2.tar + retention-days: 2 + build-aarch64-mac-967: env: - TARBALL_EXT: zip - ARTIFACT: "x86_64-mingw64" - ARCH: 64 - steps: - - name: install windows deps - shell: pwsh - run: | - C:\msys64\usr\bin\bash -lc "pacman --disable-download-timeout --noconfirm -Syuu" - C:\msys64\usr\bin\bash -lc "pacman --disable-download-timeout --noconfirm -Syuu" - C:\msys64\usr\bin\bash -lc "pacman --disable-download-timeout --noconfirm -S unzip zip git" - taskkill /F /FI "MODULES eq msys-2.0.dll" - - - uses: actions/checkout@v3 - - - uses: actions/download-artifact@v3 - with: - name: artifacts-win - path: ./out - - - name: Create bindist - run: | - $env:CHERE_INVOKING = 1 - $env:MSYS2_PATH_TYPE = "inherit" - C:\msys64\usr\bin\bash -lc "bash .github/scripts/bindist.sh" - shell: pwsh - - - name: Upload bindist - uses: actions/upload-artifact@v3 - with: - if-no-files-found: error - name: bindists-win - path: | - ./out/*.zip - ./out/plan.json/* - - - uses: geekyeggo/delete-artifact@v2 - with: - name: artifacts-win - - test-linux: - name: Test linux binaries - runs-on: ubuntu-latest - needs: ["bindist-linux"] - env: - TARBALL_EXT: tar.xz - ARCH: 64 - DEBIAN_FRONTEND: noninteractive - TZ: Asia/Singapore - strategy: - fail-fast: false - matrix: - include: - - image: debian:9 - installCmd: sed -i s/deb.debian.org/archive.debian.org/g /etc/apt/sources.list && sed -i 's|security.debian.org|archive.debian.org/|g' /etc/apt/sources.list && sed -i /-updates/d /etc/apt/sources.list && apt-get update && apt-get install -y - toolRequirements: libnuma-dev zlib1g-dev libgmp-dev libgmp10 libssl-dev liblzma-dev libbz2-dev git wget lsb-release software-properties-common gnupg2 apt-transport-https gcc autoconf automake build-essential curl ghc gzip libffi-dev libncurses-dev libncurses5 libtinfo5 patchelf tree - DISTRO: Debian - ARTIFACT: "x86_64-linux-deb9" - - image: debian:10 - installCmd: apt-get update && apt-get install -y - toolRequirements: libnuma-dev zlib1g-dev libgmp-dev libgmp10 libssl-dev liblzma-dev libbz2-dev git wget lsb-release software-properties-common gnupg2 apt-transport-https gcc autoconf automake build-essential curl ghc gzip libffi-dev libncurses-dev libncurses5 libtinfo5 patchelf tree - DISTRO: Debian - ARTIFACT: "x86_64-linux-deb10" - - image: debian:11 - installCmd: apt-get update && apt-get install -y - toolRequirements: libnuma-dev zlib1g-dev libgmp-dev libgmp10 libssl-dev liblzma-dev libbz2-dev git wget lsb-release software-properties-common gnupg2 apt-transport-https gcc autoconf automake build-essential curl ghc gzip libffi-dev libncurses-dev libncurses5 libtinfo5 patchelf tree - DISTRO: Debian - ARTIFACT: "x86_64-linux-deb11" - - image: ubuntu:18.04 - installCmd: apt-get update && apt-get install -y - toolRequirements: libnuma-dev zlib1g-dev libgmp-dev libgmp10 libssl-dev liblzma-dev libbz2-dev git wget lsb-release software-properties-common gnupg2 apt-transport-https gcc autoconf automake build-essential curl ghc gzip libffi-dev libncurses-dev libncurses5 libtinfo5 patchelf tree - DISTRO: Ubuntu - ARTIFACT: "x86_64-linux-ubuntu18.04" - - image: ubuntu:20.04 - installCmd: apt-get update && apt-get install -y - toolRequirements: libnuma-dev zlib1g-dev libgmp-dev libgmp10 libssl-dev liblzma-dev libbz2-dev git wget lsb-release software-properties-common gnupg2 apt-transport-https gcc autoconf automake build-essential curl ghc gzip libffi-dev libncurses-dev libncurses5 libtinfo5 patchelf tree - DISTRO: Ubuntu - ARTIFACT: "x86_64-linux-ubuntu20.04" - - image: ubuntu:22.04 - installCmd: apt-get update && apt-get install -y - toolRequirements: libnuma-dev zlib1g-dev libgmp-dev libgmp10 libssl-dev liblzma-dev libbz2-dev git wget lsb-release software-properties-common gnupg2 apt-transport-https gcc autoconf automake build-essential curl ghc gzip libffi-dev libncurses-dev libncurses5 libtinfo5 patchelf tree - DISTRO: Ubuntu - ARTIFACT: "x86_64-linux-ubuntu22.04" - - image: fedora:27 - installCmd: dnf install -y - toolRequirements: autoconf automake binutils bzip2 coreutils curl elfutils-devel elfutils-libs findutils gcc gcc-c++ git gmp gmp-devel jq lbzip2 make ncurses ncurses-compat-libs ncurses-devel openssh-clients patch perl pxz python3 sqlite sudo wget which xz zlib-devel patchelf tree - DISTRO: Fedora - ARTIFACT: "x86_64-linux-fedora27" - - image: fedora:33 - installCmd: dnf install -y - toolRequirements: autoconf automake binutils bzip2 coreutils curl elfutils-devel elfutils-libs findutils gcc gcc-c++ git gmp gmp-devel jq lbzip2 make ncurses ncurses-compat-libs ncurses-devel openssh-clients patch perl pxz python3 sqlite sudo wget which xz zlib-devel patchelf tree - DISTRO: Fedora - ARTIFACT: "x86_64-linux-fedora33" - - image: centos:7 - installCmd: yum -y install epel-release && yum install -y - toolRequirements: autoconf automake binutils bzip2 coreutils curl elfutils-devel elfutils-libs findutils gcc gcc-c++ git gmp gmp-devel jq lbzip2 make ncurses ncurses-compat-libs ncurses-devel openssh-clients patch perl pxz python3 sqlite sudo wget which xz zlib-devel patchelf tree - DISTRO: CentOS - ARTIFACT: "x86_64-linux-centos7" - - image: linuxmintd/mint19.3-amd64 - installCmd: apt-get update && apt-get install -y - toolRequirements: libnuma-dev zlib1g-dev libgmp-dev libgmp10 libssl-dev liblzma-dev libbz2-dev git wget lsb-release software-properties-common gnupg2 apt-transport-https gcc autoconf automake build-essential curl ghc gzip libffi-dev libncurses-dev libncurses5 libtinfo5 patchelf tree - DISTRO: Mint - ARTIFACT: "x86_64-linux-mint19.3" - - image: "fedora:33" - installCmd: dnf install -y - toolRequirements: autoconf automake binutils bzip2 coreutils curl elfutils-devel elfutils-libs findutils gcc gcc-c++ git gmp gmp-devel jq lbzip2 make ncurses ncurses-compat-libs ncurses-devel openssh-clients patch perl pxz python3 sqlite sudo wget which xz zlib-devel patchelf tree - DISTRO: "Unknown" - ARTIFACT: "x86_64-linux-unknown" - - image: linuxmintd/mint20.2-amd64 - installCmd: apt-get update && apt-get install -y - toolRequirements: libnuma-dev zlib1g-dev libgmp-dev libgmp10 libssl-dev liblzma-dev libbz2-dev git wget lsb-release software-properties-common gnupg2 apt-transport-https gcc autoconf automake build-essential curl ghc gzip libffi-dev libncurses-dev libncurses5 libtinfo5 patchelf tree - DISTRO: Mint - ARTIFACT: "x86_64-linux-mint20.2" - container: - image: ${{ matrix.image }} - steps: - - name: Install requirements - shell: sh - run: | - ${{ matrix.installCmd }} curl bash git ${{ matrix.toolRequirements }} - - - if: matrix.DISTRO == 'Unknown' - run: | - echo "NAME=Linux" > /etc/os-release - echo "ID=linux" >> /etc/os-release - echo "PRETTY_NAME=Linux" >> /etc/os-release - - - uses: actions/checkout@v3 - - - uses: actions/download-artifact@v3 - with: - name: bindists-${{ matrix.ARTIFACT }} - path: ./out - - - name: Run test - run: bash .github/scripts/test.sh - env: - ARTIFACT: ${{ matrix.ARTIFACT }} - DISTRO: ${{ matrix.DISTRO }} - - test-arm: - name: Test ARM binary - runs-on: [self-hosted, Linux, ARM64] - needs: ["bindist-arm"] - env: - TARBALL_EXT: tar.xz - DEBIAN_FRONTEND: noninteractive - TZ: Asia/Singapore - ARTIFACT: "aarch64-linux-ubuntu20" + ADD_CABAL_ARGS: '' ARCH: ARM64 - DISTRO: Ubuntu - steps: - - uses: docker://arm64v8/ubuntu:focal - name: Cleanup (aarch64 linux) - with: - args: "find . -mindepth 1 -maxdepth 1 -exec rm -rf -- {} +" - - - name: git config - run: | - git config --global --get-all safe.directory | grep '^\*$' || git config --global --add safe.directory "*" - shell: bash - - - name: Checkout code - uses: actions/checkout@v3 - - - uses: actions/download-artifact@v3 - with: - name: bindists-arm - path: ./out - - - uses: docker://hasufell/arm64v8-ubuntu-haskell:focal - name: Run test (aarch64 linux) - with: - args: bash .github/scripts/test.sh - - test-mac-x86_64: - name: Test binary (Mac x86_64) - runs-on: macOS-11 - needs: ["bindist-mac-x86_64"] - env: - MACOSX_DEPLOYMENT_TARGET: 10.13 - ARTIFACT: "x86_64-apple-darwin" - ARCH: 64 - TARBALL_EXT: tar.xz - DISTRO: na - steps: - - name: Checkout code - uses: actions/checkout@v3 - - - uses: actions/download-artifact@v3 - with: - name: bindists-mac-x86_64 - path: ./out - - - name: Run test (mac) - run: | - brew install coreutils tree - bash .github/scripts/test.sh - - test-mac-aarch64: - name: Test binary (Mac aarch64) - runs-on: [self-hosted, macOS, ARM64] - needs: ["bindist-mac-aarch64"] - env: - MACOSX_DEPLOYMENT_TARGET: 10.13 - ARTIFACT: "aarch64-apple-darwin" + ARTIFACT: aarch64-apple-darwin + AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} + AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} + HOMEBREW_CHANGE_ARCH_TO_ARM: '1' + MACOSX_DEPLOYMENT_TARGET: '10.13' + S3_HOST: ${{ secrets.S3_HOST }} + TARBALL_EXT: tar.xz + TZ: Asia/Singapore + environment: CI + name: build-aarch64-mac-967 (Build binaries) + runs-on: + - self-hosted + - macOS + - ARM64 + steps: + - name: Checkout + uses: actions/checkout@v4 + - env: + GHC_VERSION: 9.6.7 + name: Run build + run: | + bash .github/scripts/brew.sh git coreutils autoconf automake tree + export PATH="$HOME/.brew/bin:$HOME/.brew/sbin:$PATH" + export LD=ld + bash .github/scripts/build.sh + tar cf out-${ARTIFACT}-${GHC_VERSION}.tar out/ store/ + shell: sh + - name: Upload artifact + uses: actions/upload-artifact@v4 + with: + if-no-files-found: error + name: artifacts-build-aarch64-mac-967 + path: out-aarch64-apple-darwin-9.6.7.tar + retention-days: 2 + build-aarch64-mac-984: + env: + ADD_CABAL_ARGS: '' ARCH: ARM64 + ARTIFACT: aarch64-apple-darwin + AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} + AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} + HOMEBREW_CHANGE_ARCH_TO_ARM: '1' + MACOSX_DEPLOYMENT_TARGET: '10.13' + S3_HOST: ${{ secrets.S3_HOST }} TARBALL_EXT: tar.xz - DISTRO: n - HOMEBREW_CHANGE_ARCH_TO_ARM: 1 - steps: - - name: Checkout code - uses: actions/checkout@v3 - - - uses: actions/download-artifact@v3 - with: - name: bindists-mac-aarch64 - path: ./out - - - name: Run test (mac) - run: | - bash .github/scripts/brew.sh git coreutils llvm@11 autoconf automake tree - export PATH="$HOME/.brew/bin:$HOME/.brew/sbin:$HOME/.brew/opt/llvm@11/bin:$PATH" - export CC="$HOME/.brew/opt/llvm@11/bin/clang" - export CXX="$HOME/.brew/opt/llvm@11/bin/clang++" - export LD=ld - export AR="$HOME/.brew/opt/llvm@11/bin/llvm-ar" - export RANLIB="$HOME/.brew/opt/llvm@11/bin/llvm-ranlib" - bash .github/scripts/test.sh - - test-win: - name: Test binary (Win) - runs-on: windows-latest - needs: ["bindist-win"] - env: - ARTIFACT: "x86_64-mingw64" - ARCH: 64 - TARBALL_EXT: zip - DISTRO: na - strategy: - fail-fast: false - steps: - - name: install windows deps - shell: pwsh - run: | - C:\msys64\usr\bin\bash -lc "pacman --disable-download-timeout --noconfirm -Syuu" - C:\msys64\usr\bin\bash -lc "pacman --disable-download-timeout --noconfirm -Syuu" - C:\msys64\usr\bin\bash -lc "pacman --disable-download-timeout --noconfirm -S make mingw-w64-x86_64-clang curl autoconf mingw-w64-x86_64-pkgconf ca-certificates base-devel gettext autoconf make libtool automake python p7zip patch unzip zip git" - taskkill /F /FI "MODULES eq msys-2.0.dll" - - - name: Checkout code - uses: actions/checkout@v3 - - - uses: actions/download-artifact@v3 - with: - name: bindists-win - path: ./out - - - name: Run test (windows) - run: | - $env:CHERE_INVOKING = 1 - $env:MSYS2_PATH_TYPE = "inherit" - C:\msys64\usr\bin\bash -lc "bash .github/scripts/test.sh" - shell: pwsh - - release: - name: release - needs: ["test-linux", "test-mac-x86_64", "test-mac-aarch64", "test-win", "test-arm"] - runs-on: ubuntu-latest - if: startsWith(github.ref, 'refs/tags/') + TZ: Asia/Singapore + environment: CI + name: build-aarch64-mac-984 (Build binaries) + runs-on: + - self-hosted + - macOS + - ARM64 + steps: + - name: Checkout + uses: actions/checkout@v4 + - env: + GHC_VERSION: 9.8.4 + name: Run build + run: | + bash .github/scripts/brew.sh git coreutils autoconf automake tree + export PATH="$HOME/.brew/bin:$HOME/.brew/sbin:$PATH" + export LD=ld + bash .github/scripts/build.sh + tar cf out-${ARTIFACT}-${GHC_VERSION}.tar out/ store/ + shell: sh + - name: Upload artifact + uses: actions/upload-artifact@v4 + with: + if-no-files-found: error + name: artifacts-build-aarch64-mac-984 + path: out-aarch64-apple-darwin-9.8.4.tar + retention-days: 2 + build-x86_64-linux-deb10-9102: + env: + ADD_CABAL_ARGS: --enable-split-sections + ARCH: '64' + ARTIFACT: x86_64-linux-deb10 + AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} + AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} + DEBIAN_FRONTEND: noninteractive + S3_HOST: ${{ secrets.S3_HOST }} + TARBALL_EXT: tar.xz + TZ: Asia/Singapore + environment: CI + name: build-x86_64-linux-deb10-9102 (Build binaries) + runs-on: + - ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + - name: Build 9.10.2 + uses: ./.github/actions/bindist-actions/action-deb10 + with: + stage: BUILD + version: 9.10.2 + - name: Upload artifact + uses: actions/upload-artifact@v4 + with: + if-no-files-found: error + name: artifacts-build-x86_64-linux-deb10-9102 + path: out-x86_64-linux-deb10-9.10.2.tar + retention-days: 2 + build-x86_64-linux-deb10-9122: + env: + ADD_CABAL_ARGS: --enable-split-sections + ARCH: '64' + ARTIFACT: x86_64-linux-deb10 + AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} + AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} + DEBIAN_FRONTEND: noninteractive + S3_HOST: ${{ secrets.S3_HOST }} + TARBALL_EXT: tar.xz + TZ: Asia/Singapore + environment: CI + name: build-x86_64-linux-deb10-9122 (Build binaries) + runs-on: + - ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + - name: Build 9.12.2 + uses: ./.github/actions/bindist-actions/action-deb10 + with: + stage: BUILD + version: 9.12.2 + - name: Upload artifact + uses: actions/upload-artifact@v4 + with: + if-no-files-found: error + name: artifacts-build-x86_64-linux-deb10-9122 + path: out-x86_64-linux-deb10-9.12.2.tar + retention-days: 2 + build-x86_64-linux-deb10-967: + env: + ADD_CABAL_ARGS: --enable-split-sections + ARCH: '64' + ARTIFACT: x86_64-linux-deb10 + AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} + AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} + DEBIAN_FRONTEND: noninteractive + S3_HOST: ${{ secrets.S3_HOST }} + TARBALL_EXT: tar.xz + TZ: Asia/Singapore + environment: CI + name: build-x86_64-linux-deb10-967 (Build binaries) + runs-on: + - ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + - name: Build 9.6.7 + uses: ./.github/actions/bindist-actions/action-deb10 + with: + stage: BUILD + version: 9.6.7 + - name: Upload artifact + uses: actions/upload-artifact@v4 + with: + if-no-files-found: error + name: artifacts-build-x86_64-linux-deb10-967 + path: out-x86_64-linux-deb10-9.6.7.tar + retention-days: 2 + build-x86_64-linux-deb10-984: + env: + ADD_CABAL_ARGS: --enable-split-sections + ARCH: '64' + ARTIFACT: x86_64-linux-deb10 + AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} + AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} + DEBIAN_FRONTEND: noninteractive + S3_HOST: ${{ secrets.S3_HOST }} + TARBALL_EXT: tar.xz + TZ: Asia/Singapore + environment: CI + name: build-x86_64-linux-deb10-984 (Build binaries) + runs-on: + - ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + - name: Build 9.8.4 + uses: ./.github/actions/bindist-actions/action-deb10 + with: + stage: BUILD + version: 9.8.4 + - name: Upload artifact + uses: actions/upload-artifact@v4 + with: + if-no-files-found: error + name: artifacts-build-x86_64-linux-deb10-984 + path: out-x86_64-linux-deb10-9.8.4.tar + retention-days: 2 + build-x86_64-linux-deb11-9102: + env: + ADD_CABAL_ARGS: --enable-split-sections + ARCH: '64' + ARTIFACT: x86_64-linux-deb11 + AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} + AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} + DEBIAN_FRONTEND: noninteractive + S3_HOST: ${{ secrets.S3_HOST }} + TARBALL_EXT: tar.xz + TZ: Asia/Singapore + environment: CI + name: build-x86_64-linux-deb11-9102 (Build binaries) + runs-on: + - ubuntu-latest steps: - - name: Checkout code - uses: actions/checkout@v3 - - - uses: actions/download-artifact@v3 - with: - path: ./out - name: bindists-x86_64-linux-deb9 - - uses: actions/download-artifact@v3 - with: - path: ./out - name: bindists-x86_64-linux-deb10 - - uses: actions/download-artifact@v3 - with: - path: ./out - name: bindists-x86_64-linux-deb11 - - uses: actions/download-artifact@v3 - with: - path: ./out - name: bindists-x86_64-linux-ubuntu18.04 - - uses: actions/download-artifact@v3 - with: - path: ./out - name: bindists-x86_64-linux-ubuntu20.04 - - uses: actions/download-artifact@v3 - with: - path: ./out - name: bindists-x86_64-linux-ubuntu22.04 - - uses: actions/download-artifact@v3 - with: - path: ./out - name: bindists-x86_64-linux-fedora27 - - uses: actions/download-artifact@v3 - with: - path: ./out - name: bindists-x86_64-linux-fedora33 - - uses: actions/download-artifact@v3 - with: - path: ./out - name: bindists-x86_64-linux-centos7 - - uses: actions/download-artifact@v3 - with: - path: ./out - name: bindists-x86_64-linux-unknown - - uses: actions/download-artifact@v3 - with: - path: ./out - name: bindists-x86_64-linux-mint19.3 - - uses: actions/download-artifact@v3 - with: - path: ./out - name: bindists-x86_64-linux-mint20.2 - - - uses: actions/download-artifact@v3 - with: - name: bindists-arm - path: ./out - - - uses: actions/download-artifact@v3 - with: - name: bindists-mac-x86_64 - path: ./out - - - uses: actions/download-artifact@v3 - with: - name: bindists-mac-aarch64 - path: ./out - - - uses: actions/download-artifact@v3 - with: - name: bindists-win - path: ./out - - - name: Install requirements - run: | - sudo apt-get update && sudo apt-get install -y tar xz-utils - shell: bash - - - name: tar plan.json - run: | - cd out/plan.json - tar cf plan_json.tar * - mv plan_json.tar ../ - shell: bash - - - name: build source tarball - run: | - export RELEASE=$GITHUB_REF_NAME - git archive --format=tar.gz -o "out/haskell-language-server-${RELEASE}-src.tar.gz" --prefix="haskell-language-server-${RELEASE}/" HEAD - shell: bash - - - name: Release - uses: softprops/action-gh-release@v1 - with: - draft: true - files: | - ./out/*.zip - ./out/*.tar.xz - ./out/*.tar.gz - ./out/*.tar + - name: Checkout + uses: actions/checkout@v4 + - name: Build 9.10.2 + uses: ./.github/actions/bindist-actions/action-deb11 + with: + stage: BUILD + version: 9.10.2 + - name: Upload artifact + uses: actions/upload-artifact@v4 + with: + if-no-files-found: error + name: artifacts-build-x86_64-linux-deb11-9102 + path: out-x86_64-linux-deb11-9.10.2.tar + retention-days: 2 + build-x86_64-linux-deb11-9122: + env: + ADD_CABAL_ARGS: --enable-split-sections + ARCH: '64' + ARTIFACT: x86_64-linux-deb11 + AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} + AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} + DEBIAN_FRONTEND: noninteractive + S3_HOST: ${{ secrets.S3_HOST }} + TARBALL_EXT: tar.xz + TZ: Asia/Singapore + environment: CI + name: build-x86_64-linux-deb11-9122 (Build binaries) + runs-on: + - ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + - name: Build 9.12.2 + uses: ./.github/actions/bindist-actions/action-deb11 + with: + stage: BUILD + version: 9.12.2 + - name: Upload artifact + uses: actions/upload-artifact@v4 + with: + if-no-files-found: error + name: artifacts-build-x86_64-linux-deb11-9122 + path: out-x86_64-linux-deb11-9.12.2.tar + retention-days: 2 + build-x86_64-linux-deb11-967: + env: + ADD_CABAL_ARGS: --enable-split-sections + ARCH: '64' + ARTIFACT: x86_64-linux-deb11 + AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} + AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} + DEBIAN_FRONTEND: noninteractive + S3_HOST: ${{ secrets.S3_HOST }} + TARBALL_EXT: tar.xz + TZ: Asia/Singapore + environment: CI + name: build-x86_64-linux-deb11-967 (Build binaries) + runs-on: + - ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + - name: Build 9.6.7 + uses: ./.github/actions/bindist-actions/action-deb11 + with: + stage: BUILD + version: 9.6.7 + - name: Upload artifact + uses: actions/upload-artifact@v4 + with: + if-no-files-found: error + name: artifacts-build-x86_64-linux-deb11-967 + path: out-x86_64-linux-deb11-9.6.7.tar + retention-days: 2 + build-x86_64-linux-deb11-984: + env: + ADD_CABAL_ARGS: --enable-split-sections + ARCH: '64' + ARTIFACT: x86_64-linux-deb11 + AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} + AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} + DEBIAN_FRONTEND: noninteractive + S3_HOST: ${{ secrets.S3_HOST }} + TARBALL_EXT: tar.xz + TZ: Asia/Singapore + environment: CI + name: build-x86_64-linux-deb11-984 (Build binaries) + runs-on: + - ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + - name: Build 9.8.4 + uses: ./.github/actions/bindist-actions/action-deb11 + with: + stage: BUILD + version: 9.8.4 + - name: Upload artifact + uses: actions/upload-artifact@v4 + with: + if-no-files-found: error + name: artifacts-build-x86_64-linux-deb11-984 + path: out-x86_64-linux-deb11-9.8.4.tar + retention-days: 2 + build-x86_64-linux-deb12-9102: + env: + ADD_CABAL_ARGS: --enable-split-sections + ARCH: '64' + ARTIFACT: x86_64-linux-deb12 + AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} + AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} + DEBIAN_FRONTEND: noninteractive + S3_HOST: ${{ secrets.S3_HOST }} + TARBALL_EXT: tar.xz + TZ: Asia/Singapore + environment: CI + name: build-x86_64-linux-deb12-9102 (Build binaries) + runs-on: + - ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + - name: Build 9.10.2 + uses: ./.github/actions/bindist-actions/action-deb12 + with: + stage: BUILD + version: 9.10.2 + - name: Upload artifact + uses: actions/upload-artifact@v4 + with: + if-no-files-found: error + name: artifacts-build-x86_64-linux-deb12-9102 + path: out-x86_64-linux-deb12-9.10.2.tar + retention-days: 2 + build-x86_64-linux-deb12-9122: + env: + ADD_CABAL_ARGS: --enable-split-sections + ARCH: '64' + ARTIFACT: x86_64-linux-deb12 + AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} + AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} + DEBIAN_FRONTEND: noninteractive + S3_HOST: ${{ secrets.S3_HOST }} + TARBALL_EXT: tar.xz + TZ: Asia/Singapore + environment: CI + name: build-x86_64-linux-deb12-9122 (Build binaries) + runs-on: + - ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + - name: Build 9.12.2 + uses: ./.github/actions/bindist-actions/action-deb12 + with: + stage: BUILD + version: 9.12.2 + - name: Upload artifact + uses: actions/upload-artifact@v4 + with: + if-no-files-found: error + name: artifacts-build-x86_64-linux-deb12-9122 + path: out-x86_64-linux-deb12-9.12.2.tar + retention-days: 2 + build-x86_64-linux-deb12-967: + env: + ADD_CABAL_ARGS: --enable-split-sections + ARCH: '64' + ARTIFACT: x86_64-linux-deb12 + AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} + AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} + DEBIAN_FRONTEND: noninteractive + S3_HOST: ${{ secrets.S3_HOST }} + TARBALL_EXT: tar.xz + TZ: Asia/Singapore + environment: CI + name: build-x86_64-linux-deb12-967 (Build binaries) + runs-on: + - ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + - name: Build 9.6.7 + uses: ./.github/actions/bindist-actions/action-deb12 + with: + stage: BUILD + version: 9.6.7 + - name: Upload artifact + uses: actions/upload-artifact@v4 + with: + if-no-files-found: error + name: artifacts-build-x86_64-linux-deb12-967 + path: out-x86_64-linux-deb12-9.6.7.tar + retention-days: 2 + build-x86_64-linux-deb12-984: + env: + ADD_CABAL_ARGS: --enable-split-sections + ARCH: '64' + ARTIFACT: x86_64-linux-deb12 + AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} + AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} + DEBIAN_FRONTEND: noninteractive + S3_HOST: ${{ secrets.S3_HOST }} + TARBALL_EXT: tar.xz + TZ: Asia/Singapore + environment: CI + name: build-x86_64-linux-deb12-984 (Build binaries) + runs-on: + - ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + - name: Build 9.8.4 + uses: ./.github/actions/bindist-actions/action-deb12 + with: + stage: BUILD + version: 9.8.4 + - name: Upload artifact + uses: actions/upload-artifact@v4 + with: + if-no-files-found: error + name: artifacts-build-x86_64-linux-deb12-984 + path: out-x86_64-linux-deb12-9.8.4.tar + retention-days: 2 + build-x86_64-linux-deb9-9102: + env: + ADD_CABAL_ARGS: --enable-split-sections + ARCH: '64' + ARTIFACT: x86_64-linux-deb9 + AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} + AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} + DEBIAN_FRONTEND: noninteractive + S3_HOST: ${{ secrets.S3_HOST }} + TARBALL_EXT: tar.xz + TZ: Asia/Singapore + environment: CI + name: build-x86_64-linux-deb9-9102 (Build binaries) + runs-on: + - ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + - name: Build 9.10.2 + uses: ./.github/actions/bindist-actions/action-deb9 + with: + stage: BUILD + version: 9.10.2 + - name: Upload artifact + uses: actions/upload-artifact@v4 + with: + if-no-files-found: error + name: artifacts-build-x86_64-linux-deb9-9102 + path: out-x86_64-linux-deb9-9.10.2.tar + retention-days: 2 + build-x86_64-linux-deb9-9122: + env: + ADD_CABAL_ARGS: --enable-split-sections + ARCH: '64' + ARTIFACT: x86_64-linux-deb9 + AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} + AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} + DEBIAN_FRONTEND: noninteractive + S3_HOST: ${{ secrets.S3_HOST }} + TARBALL_EXT: tar.xz + TZ: Asia/Singapore + environment: CI + name: build-x86_64-linux-deb9-9122 (Build binaries) + runs-on: + - ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + - name: Build 9.12.2 + uses: ./.github/actions/bindist-actions/action-deb9 + with: + stage: BUILD + version: 9.12.2 + - name: Upload artifact + uses: actions/upload-artifact@v4 + with: + if-no-files-found: error + name: artifacts-build-x86_64-linux-deb9-9122 + path: out-x86_64-linux-deb9-9.12.2.tar + retention-days: 2 + build-x86_64-linux-deb9-967: + env: + ADD_CABAL_ARGS: --enable-split-sections + ARCH: '64' + ARTIFACT: x86_64-linux-deb9 + AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} + AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} + DEBIAN_FRONTEND: noninteractive + S3_HOST: ${{ secrets.S3_HOST }} + TARBALL_EXT: tar.xz + TZ: Asia/Singapore + environment: CI + name: build-x86_64-linux-deb9-967 (Build binaries) + runs-on: + - ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + - name: Build 9.6.7 + uses: ./.github/actions/bindist-actions/action-deb9 + with: + stage: BUILD + version: 9.6.7 + - name: Upload artifact + uses: actions/upload-artifact@v4 + with: + if-no-files-found: error + name: artifacts-build-x86_64-linux-deb9-967 + path: out-x86_64-linux-deb9-9.6.7.tar + retention-days: 2 + build-x86_64-linux-deb9-984: + env: + ADD_CABAL_ARGS: --enable-split-sections + ARCH: '64' + ARTIFACT: x86_64-linux-deb9 + AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} + AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} + DEBIAN_FRONTEND: noninteractive + S3_HOST: ${{ secrets.S3_HOST }} + TARBALL_EXT: tar.xz + TZ: Asia/Singapore + environment: CI + name: build-x86_64-linux-deb9-984 (Build binaries) + runs-on: + - ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + - name: Build 9.8.4 + uses: ./.github/actions/bindist-actions/action-deb9 + with: + stage: BUILD + version: 9.8.4 + - name: Upload artifact + uses: actions/upload-artifact@v4 + with: + if-no-files-found: error + name: artifacts-build-x86_64-linux-deb9-984 + path: out-x86_64-linux-deb9-9.8.4.tar + retention-days: 2 + build-x86_64-linux-fedora33-9102: + env: + ADD_CABAL_ARGS: --enable-split-sections + ARCH: '64' + ARTIFACT: x86_64-linux-fedora33 + AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} + AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} + DEBIAN_FRONTEND: noninteractive + S3_HOST: ${{ secrets.S3_HOST }} + TARBALL_EXT: tar.xz + TZ: Asia/Singapore + environment: CI + name: build-x86_64-linux-fedora33-9102 (Build binaries) + runs-on: + - ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + - name: Build 9.10.2 + uses: ./.github/actions/bindist-actions/action-fedora33 + with: + stage: BUILD + version: 9.10.2 + - name: Upload artifact + uses: actions/upload-artifact@v4 + with: + if-no-files-found: error + name: artifacts-build-x86_64-linux-fedora33-9102 + path: out-x86_64-linux-fedora33-9.10.2.tar + retention-days: 2 + build-x86_64-linux-fedora33-9122: + env: + ADD_CABAL_ARGS: --enable-split-sections + ARCH: '64' + ARTIFACT: x86_64-linux-fedora33 + AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} + AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} + DEBIAN_FRONTEND: noninteractive + S3_HOST: ${{ secrets.S3_HOST }} + TARBALL_EXT: tar.xz + TZ: Asia/Singapore + environment: CI + name: build-x86_64-linux-fedora33-9122 (Build binaries) + runs-on: + - ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + - name: Build 9.12.2 + uses: ./.github/actions/bindist-actions/action-fedora33 + with: + stage: BUILD + version: 9.12.2 + - name: Upload artifact + uses: actions/upload-artifact@v4 + with: + if-no-files-found: error + name: artifacts-build-x86_64-linux-fedora33-9122 + path: out-x86_64-linux-fedora33-9.12.2.tar + retention-days: 2 + build-x86_64-linux-fedora33-967: + env: + ADD_CABAL_ARGS: --enable-split-sections + ARCH: '64' + ARTIFACT: x86_64-linux-fedora33 + AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} + AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} + DEBIAN_FRONTEND: noninteractive + S3_HOST: ${{ secrets.S3_HOST }} + TARBALL_EXT: tar.xz + TZ: Asia/Singapore + environment: CI + name: build-x86_64-linux-fedora33-967 (Build binaries) + runs-on: + - ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + - name: Build 9.6.7 + uses: ./.github/actions/bindist-actions/action-fedora33 + with: + stage: BUILD + version: 9.6.7 + - name: Upload artifact + uses: actions/upload-artifact@v4 + with: + if-no-files-found: error + name: artifacts-build-x86_64-linux-fedora33-967 + path: out-x86_64-linux-fedora33-9.6.7.tar + retention-days: 2 + build-x86_64-linux-fedora33-984: + env: + ADD_CABAL_ARGS: --enable-split-sections + ARCH: '64' + ARTIFACT: x86_64-linux-fedora33 + AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} + AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} + DEBIAN_FRONTEND: noninteractive + S3_HOST: ${{ secrets.S3_HOST }} + TARBALL_EXT: tar.xz + TZ: Asia/Singapore + environment: CI + name: build-x86_64-linux-fedora33-984 (Build binaries) + runs-on: + - ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + - name: Build 9.8.4 + uses: ./.github/actions/bindist-actions/action-fedora33 + with: + stage: BUILD + version: 9.8.4 + - name: Upload artifact + uses: actions/upload-artifact@v4 + with: + if-no-files-found: error + name: artifacts-build-x86_64-linux-fedora33-984 + path: out-x86_64-linux-fedora33-9.8.4.tar + retention-days: 2 + build-x86_64-linux-fedora40-9102: + env: + ADD_CABAL_ARGS: --enable-split-sections + ARCH: '64' + ARTIFACT: x86_64-linux-fedora40 + AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} + AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} + DEBIAN_FRONTEND: noninteractive + S3_HOST: ${{ secrets.S3_HOST }} + TARBALL_EXT: tar.xz + TZ: Asia/Singapore + environment: CI + name: build-x86_64-linux-fedora40-9102 (Build binaries) + runs-on: + - ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + - name: Build 9.10.2 + uses: ./.github/actions/bindist-actions/action-fedora40 + with: + stage: BUILD + version: 9.10.2 + - name: Upload artifact + uses: actions/upload-artifact@v4 + with: + if-no-files-found: error + name: artifacts-build-x86_64-linux-fedora40-9102 + path: out-x86_64-linux-fedora40-9.10.2.tar + retention-days: 2 + build-x86_64-linux-fedora40-9122: + env: + ADD_CABAL_ARGS: --enable-split-sections + ARCH: '64' + ARTIFACT: x86_64-linux-fedora40 + AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} + AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} + DEBIAN_FRONTEND: noninteractive + S3_HOST: ${{ secrets.S3_HOST }} + TARBALL_EXT: tar.xz + TZ: Asia/Singapore + environment: CI + name: build-x86_64-linux-fedora40-9122 (Build binaries) + runs-on: + - ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + - name: Build 9.12.2 + uses: ./.github/actions/bindist-actions/action-fedora40 + with: + stage: BUILD + version: 9.12.2 + - name: Upload artifact + uses: actions/upload-artifact@v4 + with: + if-no-files-found: error + name: artifacts-build-x86_64-linux-fedora40-9122 + path: out-x86_64-linux-fedora40-9.12.2.tar + retention-days: 2 + build-x86_64-linux-fedora40-967: + env: + ADD_CABAL_ARGS: --enable-split-sections + ARCH: '64' + ARTIFACT: x86_64-linux-fedora40 + AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} + AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} + DEBIAN_FRONTEND: noninteractive + S3_HOST: ${{ secrets.S3_HOST }} + TARBALL_EXT: tar.xz + TZ: Asia/Singapore + environment: CI + name: build-x86_64-linux-fedora40-967 (Build binaries) + runs-on: + - ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + - name: Build 9.6.7 + uses: ./.github/actions/bindist-actions/action-fedora40 + with: + stage: BUILD + version: 9.6.7 + - name: Upload artifact + uses: actions/upload-artifact@v4 + with: + if-no-files-found: error + name: artifacts-build-x86_64-linux-fedora40-967 + path: out-x86_64-linux-fedora40-9.6.7.tar + retention-days: 2 + build-x86_64-linux-fedora40-984: + env: + ADD_CABAL_ARGS: --enable-split-sections + ARCH: '64' + ARTIFACT: x86_64-linux-fedora40 + AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} + AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} + DEBIAN_FRONTEND: noninteractive + S3_HOST: ${{ secrets.S3_HOST }} + TARBALL_EXT: tar.xz + TZ: Asia/Singapore + environment: CI + name: build-x86_64-linux-fedora40-984 (Build binaries) + runs-on: + - ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + - name: Build 9.8.4 + uses: ./.github/actions/bindist-actions/action-fedora40 + with: + stage: BUILD + version: 9.8.4 + - name: Upload artifact + uses: actions/upload-artifact@v4 + with: + if-no-files-found: error + name: artifacts-build-x86_64-linux-fedora40-984 + path: out-x86_64-linux-fedora40-9.8.4.tar + retention-days: 2 + build-x86_64-linux-mint193-9102: + env: + ADD_CABAL_ARGS: --enable-split-sections + ARCH: '64' + ARTIFACT: x86_64-linux-mint193 + AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} + AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} + DEBIAN_FRONTEND: noninteractive + S3_HOST: ${{ secrets.S3_HOST }} + TARBALL_EXT: tar.xz + TZ: Asia/Singapore + environment: CI + name: build-x86_64-linux-mint193-9102 (Build binaries) + runs-on: + - ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + - name: Build 9.10.2 + uses: ./.github/actions/bindist-actions/action-mint193 + with: + stage: BUILD + version: 9.10.2 + - name: Upload artifact + uses: actions/upload-artifact@v4 + with: + if-no-files-found: error + name: artifacts-build-x86_64-linux-mint193-9102 + path: out-x86_64-linux-mint193-9.10.2.tar + retention-days: 2 + build-x86_64-linux-mint193-9122: + env: + ADD_CABAL_ARGS: --enable-split-sections + ARCH: '64' + ARTIFACT: x86_64-linux-mint193 + AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} + AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} + DEBIAN_FRONTEND: noninteractive + S3_HOST: ${{ secrets.S3_HOST }} + TARBALL_EXT: tar.xz + TZ: Asia/Singapore + environment: CI + name: build-x86_64-linux-mint193-9122 (Build binaries) + runs-on: + - ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + - name: Build 9.12.2 + uses: ./.github/actions/bindist-actions/action-mint193 + with: + stage: BUILD + version: 9.12.2 + - name: Upload artifact + uses: actions/upload-artifact@v4 + with: + if-no-files-found: error + name: artifacts-build-x86_64-linux-mint193-9122 + path: out-x86_64-linux-mint193-9.12.2.tar + retention-days: 2 + build-x86_64-linux-mint193-967: + env: + ADD_CABAL_ARGS: --enable-split-sections + ARCH: '64' + ARTIFACT: x86_64-linux-mint193 + AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} + AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} + DEBIAN_FRONTEND: noninteractive + S3_HOST: ${{ secrets.S3_HOST }} + TARBALL_EXT: tar.xz + TZ: Asia/Singapore + environment: CI + name: build-x86_64-linux-mint193-967 (Build binaries) + runs-on: + - ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + - name: Build 9.6.7 + uses: ./.github/actions/bindist-actions/action-mint193 + with: + stage: BUILD + version: 9.6.7 + - name: Upload artifact + uses: actions/upload-artifact@v4 + with: + if-no-files-found: error + name: artifacts-build-x86_64-linux-mint193-967 + path: out-x86_64-linux-mint193-9.6.7.tar + retention-days: 2 + build-x86_64-linux-mint193-984: + env: + ADD_CABAL_ARGS: --enable-split-sections + ARCH: '64' + ARTIFACT: x86_64-linux-mint193 + AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} + AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} + DEBIAN_FRONTEND: noninteractive + S3_HOST: ${{ secrets.S3_HOST }} + TARBALL_EXT: tar.xz + TZ: Asia/Singapore + environment: CI + name: build-x86_64-linux-mint193-984 (Build binaries) + runs-on: + - ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + - name: Build 9.8.4 + uses: ./.github/actions/bindist-actions/action-mint193 + with: + stage: BUILD + version: 9.8.4 + - name: Upload artifact + uses: actions/upload-artifact@v4 + with: + if-no-files-found: error + name: artifacts-build-x86_64-linux-mint193-984 + path: out-x86_64-linux-mint193-9.8.4.tar + retention-days: 2 + build-x86_64-linux-mint202-9102: + env: + ADD_CABAL_ARGS: --enable-split-sections + ARCH: '64' + ARTIFACT: x86_64-linux-mint202 + AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} + AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} + DEBIAN_FRONTEND: noninteractive + S3_HOST: ${{ secrets.S3_HOST }} + TARBALL_EXT: tar.xz + TZ: Asia/Singapore + environment: CI + name: build-x86_64-linux-mint202-9102 (Build binaries) + runs-on: + - ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + - name: Build 9.10.2 + uses: ./.github/actions/bindist-actions/action-mint202 + with: + stage: BUILD + version: 9.10.2 + - name: Upload artifact + uses: actions/upload-artifact@v4 + with: + if-no-files-found: error + name: artifacts-build-x86_64-linux-mint202-9102 + path: out-x86_64-linux-mint202-9.10.2.tar + retention-days: 2 + build-x86_64-linux-mint202-9122: + env: + ADD_CABAL_ARGS: --enable-split-sections + ARCH: '64' + ARTIFACT: x86_64-linux-mint202 + AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} + AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} + DEBIAN_FRONTEND: noninteractive + S3_HOST: ${{ secrets.S3_HOST }} + TARBALL_EXT: tar.xz + TZ: Asia/Singapore + environment: CI + name: build-x86_64-linux-mint202-9122 (Build binaries) + runs-on: + - ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + - name: Build 9.12.2 + uses: ./.github/actions/bindist-actions/action-mint202 + with: + stage: BUILD + version: 9.12.2 + - name: Upload artifact + uses: actions/upload-artifact@v4 + with: + if-no-files-found: error + name: artifacts-build-x86_64-linux-mint202-9122 + path: out-x86_64-linux-mint202-9.12.2.tar + retention-days: 2 + build-x86_64-linux-mint202-967: + env: + ADD_CABAL_ARGS: --enable-split-sections + ARCH: '64' + ARTIFACT: x86_64-linux-mint202 + AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} + AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} + DEBIAN_FRONTEND: noninteractive + S3_HOST: ${{ secrets.S3_HOST }} + TARBALL_EXT: tar.xz + TZ: Asia/Singapore + environment: CI + name: build-x86_64-linux-mint202-967 (Build binaries) + runs-on: + - ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + - name: Build 9.6.7 + uses: ./.github/actions/bindist-actions/action-mint202 + with: + stage: BUILD + version: 9.6.7 + - name: Upload artifact + uses: actions/upload-artifact@v4 + with: + if-no-files-found: error + name: artifacts-build-x86_64-linux-mint202-967 + path: out-x86_64-linux-mint202-9.6.7.tar + retention-days: 2 + build-x86_64-linux-mint202-984: + env: + ADD_CABAL_ARGS: --enable-split-sections + ARCH: '64' + ARTIFACT: x86_64-linux-mint202 + AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} + AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} + DEBIAN_FRONTEND: noninteractive + S3_HOST: ${{ secrets.S3_HOST }} + TARBALL_EXT: tar.xz + TZ: Asia/Singapore + environment: CI + name: build-x86_64-linux-mint202-984 (Build binaries) + runs-on: + - ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + - name: Build 9.8.4 + uses: ./.github/actions/bindist-actions/action-mint202 + with: + stage: BUILD + version: 9.8.4 + - name: Upload artifact + uses: actions/upload-artifact@v4 + with: + if-no-files-found: error + name: artifacts-build-x86_64-linux-mint202-984 + path: out-x86_64-linux-mint202-9.8.4.tar + retention-days: 2 + build-x86_64-linux-mint213-9102: + env: + ADD_CABAL_ARGS: --enable-split-sections + ARCH: '64' + ARTIFACT: x86_64-linux-mint213 + AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} + AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} + DEBIAN_FRONTEND: noninteractive + S3_HOST: ${{ secrets.S3_HOST }} + TARBALL_EXT: tar.xz + TZ: Asia/Singapore + environment: CI + name: build-x86_64-linux-mint213-9102 (Build binaries) + runs-on: + - ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + - name: Build 9.10.2 + uses: ./.github/actions/bindist-actions/action-mint213 + with: + stage: BUILD + version: 9.10.2 + - name: Upload artifact + uses: actions/upload-artifact@v4 + with: + if-no-files-found: error + name: artifacts-build-x86_64-linux-mint213-9102 + path: out-x86_64-linux-mint213-9.10.2.tar + retention-days: 2 + build-x86_64-linux-mint213-9122: + env: + ADD_CABAL_ARGS: --enable-split-sections + ARCH: '64' + ARTIFACT: x86_64-linux-mint213 + AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} + AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} + DEBIAN_FRONTEND: noninteractive + S3_HOST: ${{ secrets.S3_HOST }} + TARBALL_EXT: tar.xz + TZ: Asia/Singapore + environment: CI + name: build-x86_64-linux-mint213-9122 (Build binaries) + runs-on: + - ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + - name: Build 9.12.2 + uses: ./.github/actions/bindist-actions/action-mint213 + with: + stage: BUILD + version: 9.12.2 + - name: Upload artifact + uses: actions/upload-artifact@v4 + with: + if-no-files-found: error + name: artifacts-build-x86_64-linux-mint213-9122 + path: out-x86_64-linux-mint213-9.12.2.tar + retention-days: 2 + build-x86_64-linux-mint213-967: + env: + ADD_CABAL_ARGS: --enable-split-sections + ARCH: '64' + ARTIFACT: x86_64-linux-mint213 + AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} + AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} + DEBIAN_FRONTEND: noninteractive + S3_HOST: ${{ secrets.S3_HOST }} + TARBALL_EXT: tar.xz + TZ: Asia/Singapore + environment: CI + name: build-x86_64-linux-mint213-967 (Build binaries) + runs-on: + - ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + - name: Build 9.6.7 + uses: ./.github/actions/bindist-actions/action-mint213 + with: + stage: BUILD + version: 9.6.7 + - name: Upload artifact + uses: actions/upload-artifact@v4 + with: + if-no-files-found: error + name: artifacts-build-x86_64-linux-mint213-967 + path: out-x86_64-linux-mint213-9.6.7.tar + retention-days: 2 + build-x86_64-linux-mint213-984: + env: + ADD_CABAL_ARGS: --enable-split-sections + ARCH: '64' + ARTIFACT: x86_64-linux-mint213 + AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} + AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} + DEBIAN_FRONTEND: noninteractive + S3_HOST: ${{ secrets.S3_HOST }} + TARBALL_EXT: tar.xz + TZ: Asia/Singapore + environment: CI + name: build-x86_64-linux-mint213-984 (Build binaries) + runs-on: + - ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + - name: Build 9.8.4 + uses: ./.github/actions/bindist-actions/action-mint213 + with: + stage: BUILD + version: 9.8.4 + - name: Upload artifact + uses: actions/upload-artifact@v4 + with: + if-no-files-found: error + name: artifacts-build-x86_64-linux-mint213-984 + path: out-x86_64-linux-mint213-9.8.4.tar + retention-days: 2 + build-x86_64-linux-ubuntu1804-9102: + env: + ADD_CABAL_ARGS: --enable-split-sections + ARCH: '64' + ARTIFACT: x86_64-linux-ubuntu1804 + AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} + AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} + DEBIAN_FRONTEND: noninteractive + S3_HOST: ${{ secrets.S3_HOST }} + TARBALL_EXT: tar.xz + TZ: Asia/Singapore + environment: CI + name: build-x86_64-linux-ubuntu1804-9102 (Build binaries) + runs-on: + - ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + - name: Build 9.10.2 + uses: ./.github/actions/bindist-actions/action-ubuntu1804 + with: + stage: BUILD + version: 9.10.2 + - name: Upload artifact + uses: actions/upload-artifact@v4 + with: + if-no-files-found: error + name: artifacts-build-x86_64-linux-ubuntu1804-9102 + path: out-x86_64-linux-ubuntu1804-9.10.2.tar + retention-days: 2 + build-x86_64-linux-ubuntu1804-9122: + env: + ADD_CABAL_ARGS: --enable-split-sections + ARCH: '64' + ARTIFACT: x86_64-linux-ubuntu1804 + AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} + AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} + DEBIAN_FRONTEND: noninteractive + S3_HOST: ${{ secrets.S3_HOST }} + TARBALL_EXT: tar.xz + TZ: Asia/Singapore + environment: CI + name: build-x86_64-linux-ubuntu1804-9122 (Build binaries) + runs-on: + - ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + - name: Build 9.12.2 + uses: ./.github/actions/bindist-actions/action-ubuntu1804 + with: + stage: BUILD + version: 9.12.2 + - name: Upload artifact + uses: actions/upload-artifact@v4 + with: + if-no-files-found: error + name: artifacts-build-x86_64-linux-ubuntu1804-9122 + path: out-x86_64-linux-ubuntu1804-9.12.2.tar + retention-days: 2 + build-x86_64-linux-ubuntu1804-967: + env: + ADD_CABAL_ARGS: --enable-split-sections + ARCH: '64' + ARTIFACT: x86_64-linux-ubuntu1804 + AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} + AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} + DEBIAN_FRONTEND: noninteractive + S3_HOST: ${{ secrets.S3_HOST }} + TARBALL_EXT: tar.xz + TZ: Asia/Singapore + environment: CI + name: build-x86_64-linux-ubuntu1804-967 (Build binaries) + runs-on: + - ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + - name: Build 9.6.7 + uses: ./.github/actions/bindist-actions/action-ubuntu1804 + with: + stage: BUILD + version: 9.6.7 + - name: Upload artifact + uses: actions/upload-artifact@v4 + with: + if-no-files-found: error + name: artifacts-build-x86_64-linux-ubuntu1804-967 + path: out-x86_64-linux-ubuntu1804-9.6.7.tar + retention-days: 2 + build-x86_64-linux-ubuntu1804-984: + env: + ADD_CABAL_ARGS: --enable-split-sections + ARCH: '64' + ARTIFACT: x86_64-linux-ubuntu1804 + AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} + AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} + DEBIAN_FRONTEND: noninteractive + S3_HOST: ${{ secrets.S3_HOST }} + TARBALL_EXT: tar.xz + TZ: Asia/Singapore + environment: CI + name: build-x86_64-linux-ubuntu1804-984 (Build binaries) + runs-on: + - ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + - name: Build 9.8.4 + uses: ./.github/actions/bindist-actions/action-ubuntu1804 + with: + stage: BUILD + version: 9.8.4 + - name: Upload artifact + uses: actions/upload-artifact@v4 + with: + if-no-files-found: error + name: artifacts-build-x86_64-linux-ubuntu1804-984 + path: out-x86_64-linux-ubuntu1804-9.8.4.tar + retention-days: 2 + build-x86_64-linux-ubuntu2004-9102: + env: + ADD_CABAL_ARGS: --enable-split-sections + ARCH: '64' + ARTIFACT: x86_64-linux-ubuntu2004 + AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} + AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} + DEBIAN_FRONTEND: noninteractive + S3_HOST: ${{ secrets.S3_HOST }} + TARBALL_EXT: tar.xz + TZ: Asia/Singapore + environment: CI + name: build-x86_64-linux-ubuntu2004-9102 (Build binaries) + runs-on: + - ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + - name: Build 9.10.2 + uses: ./.github/actions/bindist-actions/action-ubuntu2004 + with: + stage: BUILD + version: 9.10.2 + - name: Upload artifact + uses: actions/upload-artifact@v4 + with: + if-no-files-found: error + name: artifacts-build-x86_64-linux-ubuntu2004-9102 + path: out-x86_64-linux-ubuntu2004-9.10.2.tar + retention-days: 2 + build-x86_64-linux-ubuntu2004-9122: + env: + ADD_CABAL_ARGS: --enable-split-sections + ARCH: '64' + ARTIFACT: x86_64-linux-ubuntu2004 + AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} + AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} + DEBIAN_FRONTEND: noninteractive + S3_HOST: ${{ secrets.S3_HOST }} + TARBALL_EXT: tar.xz + TZ: Asia/Singapore + environment: CI + name: build-x86_64-linux-ubuntu2004-9122 (Build binaries) + runs-on: + - ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + - name: Build 9.12.2 + uses: ./.github/actions/bindist-actions/action-ubuntu2004 + with: + stage: BUILD + version: 9.12.2 + - name: Upload artifact + uses: actions/upload-artifact@v4 + with: + if-no-files-found: error + name: artifacts-build-x86_64-linux-ubuntu2004-9122 + path: out-x86_64-linux-ubuntu2004-9.12.2.tar + retention-days: 2 + build-x86_64-linux-ubuntu2004-967: + env: + ADD_CABAL_ARGS: --enable-split-sections + ARCH: '64' + ARTIFACT: x86_64-linux-ubuntu2004 + AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} + AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} + DEBIAN_FRONTEND: noninteractive + S3_HOST: ${{ secrets.S3_HOST }} + TARBALL_EXT: tar.xz + TZ: Asia/Singapore + environment: CI + name: build-x86_64-linux-ubuntu2004-967 (Build binaries) + runs-on: + - ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + - name: Build 9.6.7 + uses: ./.github/actions/bindist-actions/action-ubuntu2004 + with: + stage: BUILD + version: 9.6.7 + - name: Upload artifact + uses: actions/upload-artifact@v4 + with: + if-no-files-found: error + name: artifacts-build-x86_64-linux-ubuntu2004-967 + path: out-x86_64-linux-ubuntu2004-9.6.7.tar + retention-days: 2 + build-x86_64-linux-ubuntu2004-984: + env: + ADD_CABAL_ARGS: --enable-split-sections + ARCH: '64' + ARTIFACT: x86_64-linux-ubuntu2004 + AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} + AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} + DEBIAN_FRONTEND: noninteractive + S3_HOST: ${{ secrets.S3_HOST }} + TARBALL_EXT: tar.xz + TZ: Asia/Singapore + environment: CI + name: build-x86_64-linux-ubuntu2004-984 (Build binaries) + runs-on: + - ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + - name: Build 9.8.4 + uses: ./.github/actions/bindist-actions/action-ubuntu2004 + with: + stage: BUILD + version: 9.8.4 + - name: Upload artifact + uses: actions/upload-artifact@v4 + with: + if-no-files-found: error + name: artifacts-build-x86_64-linux-ubuntu2004-984 + path: out-x86_64-linux-ubuntu2004-9.8.4.tar + retention-days: 2 + build-x86_64-linux-ubuntu2204-9102: + env: + ADD_CABAL_ARGS: --enable-split-sections + ARCH: '64' + ARTIFACT: x86_64-linux-ubuntu2204 + AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} + AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} + DEBIAN_FRONTEND: noninteractive + S3_HOST: ${{ secrets.S3_HOST }} + TARBALL_EXT: tar.xz + TZ: Asia/Singapore + environment: CI + name: build-x86_64-linux-ubuntu2204-9102 (Build binaries) + runs-on: + - ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + - name: Build 9.10.2 + uses: ./.github/actions/bindist-actions/action-ubuntu2204 + with: + stage: BUILD + version: 9.10.2 + - name: Upload artifact + uses: actions/upload-artifact@v4 + with: + if-no-files-found: error + name: artifacts-build-x86_64-linux-ubuntu2204-9102 + path: out-x86_64-linux-ubuntu2204-9.10.2.tar + retention-days: 2 + build-x86_64-linux-ubuntu2204-9122: + env: + ADD_CABAL_ARGS: --enable-split-sections + ARCH: '64' + ARTIFACT: x86_64-linux-ubuntu2204 + AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} + AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} + DEBIAN_FRONTEND: noninteractive + S3_HOST: ${{ secrets.S3_HOST }} + TARBALL_EXT: tar.xz + TZ: Asia/Singapore + environment: CI + name: build-x86_64-linux-ubuntu2204-9122 (Build binaries) + runs-on: + - ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + - name: Build 9.12.2 + uses: ./.github/actions/bindist-actions/action-ubuntu2204 + with: + stage: BUILD + version: 9.12.2 + - name: Upload artifact + uses: actions/upload-artifact@v4 + with: + if-no-files-found: error + name: artifacts-build-x86_64-linux-ubuntu2204-9122 + path: out-x86_64-linux-ubuntu2204-9.12.2.tar + retention-days: 2 + build-x86_64-linux-ubuntu2204-967: + env: + ADD_CABAL_ARGS: --enable-split-sections + ARCH: '64' + ARTIFACT: x86_64-linux-ubuntu2204 + AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} + AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} + DEBIAN_FRONTEND: noninteractive + S3_HOST: ${{ secrets.S3_HOST }} + TARBALL_EXT: tar.xz + TZ: Asia/Singapore + environment: CI + name: build-x86_64-linux-ubuntu2204-967 (Build binaries) + runs-on: + - ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + - name: Build 9.6.7 + uses: ./.github/actions/bindist-actions/action-ubuntu2204 + with: + stage: BUILD + version: 9.6.7 + - name: Upload artifact + uses: actions/upload-artifact@v4 + with: + if-no-files-found: error + name: artifacts-build-x86_64-linux-ubuntu2204-967 + path: out-x86_64-linux-ubuntu2204-9.6.7.tar + retention-days: 2 + build-x86_64-linux-ubuntu2204-984: + env: + ADD_CABAL_ARGS: --enable-split-sections + ARCH: '64' + ARTIFACT: x86_64-linux-ubuntu2204 + AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} + AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} + DEBIAN_FRONTEND: noninteractive + S3_HOST: ${{ secrets.S3_HOST }} + TARBALL_EXT: tar.xz + TZ: Asia/Singapore + environment: CI + name: build-x86_64-linux-ubuntu2204-984 (Build binaries) + runs-on: + - ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + - name: Build 9.8.4 + uses: ./.github/actions/bindist-actions/action-ubuntu2204 + with: + stage: BUILD + version: 9.8.4 + - name: Upload artifact + uses: actions/upload-artifact@v4 + with: + if-no-files-found: error + name: artifacts-build-x86_64-linux-ubuntu2204-984 + path: out-x86_64-linux-ubuntu2204-9.8.4.tar + retention-days: 2 + build-x86_64-linux-unknown-9102: + env: + ADD_CABAL_ARGS: --enable-split-sections + ARCH: '64' + ARTIFACT: x86_64-linux-unknown + AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} + AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} + DEBIAN_FRONTEND: noninteractive + S3_HOST: ${{ secrets.S3_HOST }} + TARBALL_EXT: tar.xz + TZ: Asia/Singapore + environment: CI + name: build-x86_64-linux-unknown-9102 (Build binaries) + runs-on: + - ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + - name: Build 9.10.2 + uses: ./.github/actions/bindist-actions/action-unknown + with: + stage: BUILD + version: 9.10.2 + - name: Upload artifact + uses: actions/upload-artifact@v4 + with: + if-no-files-found: error + name: artifacts-build-x86_64-linux-unknown-9102 + path: out-x86_64-linux-unknown-9.10.2.tar + retention-days: 2 + build-x86_64-linux-unknown-9122: + env: + ADD_CABAL_ARGS: --enable-split-sections + ARCH: '64' + ARTIFACT: x86_64-linux-unknown + AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} + AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} + DEBIAN_FRONTEND: noninteractive + S3_HOST: ${{ secrets.S3_HOST }} + TARBALL_EXT: tar.xz + TZ: Asia/Singapore + environment: CI + name: build-x86_64-linux-unknown-9122 (Build binaries) + runs-on: + - ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + - name: Build 9.12.2 + uses: ./.github/actions/bindist-actions/action-unknown + with: + stage: BUILD + version: 9.12.2 + - name: Upload artifact + uses: actions/upload-artifact@v4 + with: + if-no-files-found: error + name: artifacts-build-x86_64-linux-unknown-9122 + path: out-x86_64-linux-unknown-9.12.2.tar + retention-days: 2 + build-x86_64-linux-unknown-967: + env: + ADD_CABAL_ARGS: --enable-split-sections + ARCH: '64' + ARTIFACT: x86_64-linux-unknown + AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} + AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} + DEBIAN_FRONTEND: noninteractive + S3_HOST: ${{ secrets.S3_HOST }} + TARBALL_EXT: tar.xz + TZ: Asia/Singapore + environment: CI + name: build-x86_64-linux-unknown-967 (Build binaries) + runs-on: + - ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + - name: Build 9.6.7 + uses: ./.github/actions/bindist-actions/action-unknown + with: + stage: BUILD + version: 9.6.7 + - name: Upload artifact + uses: actions/upload-artifact@v4 + with: + if-no-files-found: error + name: artifacts-build-x86_64-linux-unknown-967 + path: out-x86_64-linux-unknown-9.6.7.tar + retention-days: 2 + build-x86_64-linux-unknown-984: + env: + ADD_CABAL_ARGS: --enable-split-sections + ARCH: '64' + ARTIFACT: x86_64-linux-unknown + AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} + AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} + DEBIAN_FRONTEND: noninteractive + S3_HOST: ${{ secrets.S3_HOST }} + TARBALL_EXT: tar.xz + TZ: Asia/Singapore + environment: CI + name: build-x86_64-linux-unknown-984 (Build binaries) + runs-on: + - ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + - name: Build 9.8.4 + uses: ./.github/actions/bindist-actions/action-unknown + with: + stage: BUILD + version: 9.8.4 + - name: Upload artifact + uses: actions/upload-artifact@v4 + with: + if-no-files-found: error + name: artifacts-build-x86_64-linux-unknown-984 + path: out-x86_64-linux-unknown-9.8.4.tar + retention-days: 2 + build-x86_64-mac-9102: + env: + ADD_CABAL_ARGS: '' + ARCH: '64' + ARTIFACT: x86_64-apple-darwin + AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} + AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} + MACOSX_DEPLOYMENT_TARGET: '10.13' + S3_HOST: ${{ secrets.S3_HOST }} + TARBALL_EXT: tar.xz + TZ: Asia/Singapore + environment: CI + name: build-x86_64-mac-9102 (Build binaries) + runs-on: + - macOS-13 + steps: + - name: Checkout + uses: actions/checkout@v4 + - env: + GHC_VERSION: 9.10.2 + name: Run build + run: | + brew install coreutils tree + bash .github/scripts/build.sh + tar cf out-${ARTIFACT}-${GHC_VERSION}.tar out/ store/ + shell: sh + - name: Upload artifact + uses: actions/upload-artifact@v4 + with: + if-no-files-found: error + name: artifacts-build-x86_64-mac-9102 + path: out-x86_64-apple-darwin-9.10.2.tar + retention-days: 2 + build-x86_64-mac-9122: + env: + ADD_CABAL_ARGS: '' + ARCH: '64' + ARTIFACT: x86_64-apple-darwin + AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} + AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} + MACOSX_DEPLOYMENT_TARGET: '10.13' + S3_HOST: ${{ secrets.S3_HOST }} + TARBALL_EXT: tar.xz + TZ: Asia/Singapore + environment: CI + name: build-x86_64-mac-9122 (Build binaries) + runs-on: + - macOS-13 + steps: + - name: Checkout + uses: actions/checkout@v4 + - env: + GHC_VERSION: 9.12.2 + name: Run build + run: | + brew install coreutils tree + bash .github/scripts/build.sh + tar cf out-${ARTIFACT}-${GHC_VERSION}.tar out/ store/ + shell: sh + - name: Upload artifact + uses: actions/upload-artifact@v4 + with: + if-no-files-found: error + name: artifacts-build-x86_64-mac-9122 + path: out-x86_64-apple-darwin-9.12.2.tar + retention-days: 2 + build-x86_64-mac-967: + env: + ADD_CABAL_ARGS: '' + ARCH: '64' + ARTIFACT: x86_64-apple-darwin + AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} + AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} + MACOSX_DEPLOYMENT_TARGET: '10.13' + S3_HOST: ${{ secrets.S3_HOST }} + TARBALL_EXT: tar.xz + TZ: Asia/Singapore + environment: CI + name: build-x86_64-mac-967 (Build binaries) + runs-on: + - macOS-13 + steps: + - name: Checkout + uses: actions/checkout@v4 + - env: + GHC_VERSION: 9.6.7 + name: Run build + run: | + brew install coreutils tree + bash .github/scripts/build.sh + tar cf out-${ARTIFACT}-${GHC_VERSION}.tar out/ store/ + shell: sh + - name: Upload artifact + uses: actions/upload-artifact@v4 + with: + if-no-files-found: error + name: artifacts-build-x86_64-mac-967 + path: out-x86_64-apple-darwin-9.6.7.tar + retention-days: 2 + build-x86_64-mac-984: + env: + ADD_CABAL_ARGS: '' + ARCH: '64' + ARTIFACT: x86_64-apple-darwin + AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} + AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} + MACOSX_DEPLOYMENT_TARGET: '10.13' + S3_HOST: ${{ secrets.S3_HOST }} + TARBALL_EXT: tar.xz + TZ: Asia/Singapore + environment: CI + name: build-x86_64-mac-984 (Build binaries) + runs-on: + - macOS-13 + steps: + - name: Checkout + uses: actions/checkout@v4 + - env: + GHC_VERSION: 9.8.4 + name: Run build + run: | + brew install coreutils tree + bash .github/scripts/build.sh + tar cf out-${ARTIFACT}-${GHC_VERSION}.tar out/ store/ + shell: sh + - name: Upload artifact + uses: actions/upload-artifact@v4 + with: + if-no-files-found: error + name: artifacts-build-x86_64-mac-984 + path: out-x86_64-apple-darwin-9.8.4.tar + retention-days: 2 + build-x86_64-windows-9102: + env: + ADD_CABAL_ARGS: '' + ARCH: '64' + ARTIFACT: x86_64-mingw64 + AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} + AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} + S3_HOST: ${{ secrets.S3_HOST }} + TARBALL_EXT: zip + TZ: Asia/Singapore + environment: CI + name: build-x86_64-windows-9102 (Build binaries) + runs-on: + - windows-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + - env: + GHC_VERSION: 9.10.2 + name: Run build + run: | + $env:CHERE_INVOKING = 1 + $env:MSYS2_PATH_TYPE = "inherit" + $ErrorActionPreference = "Stop" + C:\msys64\usr\bin\bash -lc "bash .github/scripts/build.sh" + shell: pwsh + - name: Upload artifact + uses: actions/upload-artifact@v4 + with: + if-no-files-found: error + name: artifacts-build-x86_64-windows-9102 + path: ./out/* + retention-days: 2 + build-x86_64-windows-9122: + env: + ADD_CABAL_ARGS: '' + ARCH: '64' + ARTIFACT: x86_64-mingw64 + AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} + AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} + S3_HOST: ${{ secrets.S3_HOST }} + TARBALL_EXT: zip + TZ: Asia/Singapore + environment: CI + name: build-x86_64-windows-9122 (Build binaries) + runs-on: + - windows-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + - env: + GHC_VERSION: 9.12.2 + name: Run build + run: | + $env:CHERE_INVOKING = 1 + $env:MSYS2_PATH_TYPE = "inherit" + $ErrorActionPreference = "Stop" + C:\msys64\usr\bin\bash -lc "bash .github/scripts/build.sh" + shell: pwsh + - name: Upload artifact + uses: actions/upload-artifact@v4 + with: + if-no-files-found: error + name: artifacts-build-x86_64-windows-9122 + path: ./out/* + retention-days: 2 + build-x86_64-windows-967: + env: + ADD_CABAL_ARGS: '' + ARCH: '64' + ARTIFACT: x86_64-mingw64 + AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} + AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} + S3_HOST: ${{ secrets.S3_HOST }} + TARBALL_EXT: zip + TZ: Asia/Singapore + environment: CI + name: build-x86_64-windows-967 (Build binaries) + runs-on: + - windows-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + - env: + GHC_VERSION: 9.6.7 + name: Run build + run: | + $env:CHERE_INVOKING = 1 + $env:MSYS2_PATH_TYPE = "inherit" + $ErrorActionPreference = "Stop" + C:\msys64\usr\bin\bash -lc "bash .github/scripts/build.sh" + shell: pwsh + - name: Upload artifact + uses: actions/upload-artifact@v4 + with: + if-no-files-found: error + name: artifacts-build-x86_64-windows-967 + path: ./out/* + retention-days: 2 + build-x86_64-windows-984: + env: + ADD_CABAL_ARGS: '' + ARCH: '64' + ARTIFACT: x86_64-mingw64 + AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} + AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} + S3_HOST: ${{ secrets.S3_HOST }} + TARBALL_EXT: zip + TZ: Asia/Singapore + environment: CI + name: build-x86_64-windows-984 (Build binaries) + runs-on: + - windows-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + - env: + GHC_VERSION: 9.8.4 + name: Run build + run: | + $env:CHERE_INVOKING = 1 + $env:MSYS2_PATH_TYPE = "inherit" + $ErrorActionPreference = "Stop" + C:\msys64\usr\bin\bash -lc "bash .github/scripts/build.sh" + shell: pwsh + - name: Upload artifact + uses: actions/upload-artifact@v4 + with: + if-no-files-found: error + name: artifacts-build-x86_64-windows-984 + path: ./out/* + retention-days: 2 + release: + if: startsWith(github.ref, 'refs/tags/') + name: release + needs: + - test-x86_64-mac + - test-aarch64-mac + - test-x86_64-windows + - test-aarch64-linux-ubuntu2004 + - test-x86_64-linux-deb9 + - test-x86_64-linux-deb10 + - test-x86_64-linux-deb11 + - test-x86_64-linux-deb12 + - test-x86_64-linux-ubuntu1804 + - test-x86_64-linux-ubuntu2004 + - test-x86_64-linux-ubuntu2204 + - test-x86_64-linux-mint193 + - test-x86_64-linux-mint202 + - test-x86_64-linux-mint213 + - test-x86_64-linux-fedora33 + - test-x86_64-linux-fedora40 + - test-x86_64-linux-unknown + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + - name: Download artifacts + uses: actions/download-artifact@v4 + with: + name: bindist-x86_64-apple-darwin + path: ./out + - name: Download artifacts + uses: actions/download-artifact@v4 + with: + name: bindist-aarch64-apple-darwin + path: ./out + - name: Download artifacts + uses: actions/download-artifact@v4 + with: + name: bindist-x86_64-mingw64 + path: ./out + - name: Download artifacts + uses: actions/download-artifact@v4 + with: + name: bindist-aarch64-linux-ubuntu2004 + path: ./out + - name: Download artifacts + uses: actions/download-artifact@v4 + with: + name: bindist-x86_64-linux-deb9 + path: ./out + - name: Download artifacts + uses: actions/download-artifact@v4 + with: + name: bindist-x86_64-linux-deb10 + path: ./out + - name: Download artifacts + uses: actions/download-artifact@v4 + with: + name: bindist-x86_64-linux-deb11 + path: ./out + - name: Download artifacts + uses: actions/download-artifact@v4 + with: + name: bindist-x86_64-linux-deb12 + path: ./out + - name: Download artifacts + uses: actions/download-artifact@v4 + with: + name: bindist-x86_64-linux-ubuntu1804 + path: ./out + - name: Download artifacts + uses: actions/download-artifact@v4 + with: + name: bindist-x86_64-linux-ubuntu2004 + path: ./out + - name: Download artifacts + uses: actions/download-artifact@v4 + with: + name: bindist-x86_64-linux-ubuntu2204 + path: ./out + - name: Download artifacts + uses: actions/download-artifact@v4 + with: + name: bindist-x86_64-linux-mint193 + path: ./out + - name: Download artifacts + uses: actions/download-artifact@v4 + with: + name: bindist-x86_64-linux-mint202 + path: ./out + - name: Download artifacts + uses: actions/download-artifact@v4 + with: + name: bindist-x86_64-linux-mint213 + path: ./out + - name: Download artifacts + uses: actions/download-artifact@v4 + with: + name: bindist-x86_64-linux-fedora33 + path: ./out + - name: Download artifacts + uses: actions/download-artifact@v4 + with: + name: bindist-x86_64-linux-fedora40 + path: ./out + - name: Download artifacts + uses: actions/download-artifact@v4 + with: + name: bindist-x86_64-linux-unknown + path: ./out + - name: Prepare release + run: | + sudo apt-get update && sudo apt-get install -y tar xz-utils + cd out/plan.json + tar cf plan_json.tar * + mv plan_json.tar ../ + cd ../.. + export RELEASE=$GITHUB_REF_NAME + git archive --format=tar.gz -o "out/haskell-language-server-${RELEASE}-src.tar.gz" --prefix="haskell-language-server-${RELEASE}/" HEAD + shell: bash + - name: Release + uses: softprops/action-gh-release@v2 + with: + draft: true + files: | + ./out/*.zip + ./out/*.tar.xz + ./out/*.tar.gz + ./out/*.tar + test-aarch64-linux-ubuntu2004: + env: + ADD_CABAL_ARGS: '' + ARCH: ARM64 + ARTIFACT: aarch64-linux-ubuntu2004 + AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} + AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} + DEBIAN_FRONTEND: noninteractive + S3_HOST: ${{ secrets.S3_HOST }} + TARBALL_EXT: tar.xz + TZ: Asia/Singapore + environment: CI + name: test-aarch64-linux-ubuntu2004 (Test binaries) + needs: + - bindist-aarch64-linux-ubuntu2004 + runs-on: + - self-hosted + - Linux + - ARM64 + - maerwald + steps: + - name: clean and git config for aarch64-linux + run: | + find . -mindepth 1 -maxdepth 1 -exec rm -rf -- {} + + git config --global --get-all safe.directory | grep '^\*$' || git config --global --add safe.directory "*" + shell: bash + - name: Checkout + uses: actions/checkout@v4 + - name: Download artifacts + uses: actions/download-artifact@v4 + with: + name: bindist-aarch64-linux-ubuntu2004 + path: ./out + - name: Run test + uses: docker://hasufell/arm64v8-ubuntu-haskell:focal + with: + args: bash .github/scripts/test.sh + test-aarch64-mac: + env: + ADD_CABAL_ARGS: '' + ARCH: ARM64 + ARTIFACT: aarch64-apple-darwin + AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} + AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} + HOMEBREW_CHANGE_ARCH_TO_ARM: '1' + MACOSX_DEPLOYMENT_TARGET: '10.13' + S3_HOST: ${{ secrets.S3_HOST }} + TARBALL_EXT: tar.xz + TZ: Asia/Singapore + environment: CI + name: test-aarch64-mac (Test binaries) + needs: + - bindist-aarch64-mac + runs-on: + - self-hosted + - macOS + - ARM64 + steps: + - name: Checkout + uses: actions/checkout@v4 + - name: Download artifacts + uses: actions/download-artifact@v4 + with: + name: bindist-aarch64-apple-darwin + path: ./out + - name: Run test + run: | + bash .github/scripts/brew.sh git coreutils llvm@13 autoconf automake tree + export PATH="$HOME/.brew/bin:$HOME/.brew/sbin:$HOME/.brew/opt/llvm@13/bin:$PATH" + export CC="$HOME/.brew/opt/llvm@13/bin/clang" + export CXX="$HOME/.brew/opt/llvm@13/bin/clang++" + export LD=ld + export AR="$HOME/.brew/opt/llvm@13/bin/llvm-ar" + export RANLIB="$HOME/.brew/opt/llvm@13/bin/llvm-ranlib" + bash .github/scripts/test.sh + shell: sh + test-x86_64-linux-deb10: + env: + ADD_CABAL_ARGS: --enable-split-sections + ARCH: '64' + ARTIFACT: x86_64-linux-deb10 + AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} + AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} + DEBIAN_FRONTEND: noninteractive + S3_HOST: ${{ secrets.S3_HOST }} + TARBALL_EXT: tar.xz + TZ: Asia/Singapore + environment: CI + name: test-x86_64-linux-deb10 (Test binaries) + needs: + - bindist-x86_64-linux-deb10 + runs-on: + - ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + - name: Download artifacts + uses: actions/download-artifact@v4 + with: + name: bindist-x86_64-linux-deb10 + path: ./out + - name: Test + uses: ./.github/actions/bindist-actions/action-deb10 + with: + stage: TEST + test-x86_64-linux-deb11: + env: + ADD_CABAL_ARGS: --enable-split-sections + ARCH: '64' + ARTIFACT: x86_64-linux-deb11 + AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} + AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} + DEBIAN_FRONTEND: noninteractive + S3_HOST: ${{ secrets.S3_HOST }} + TARBALL_EXT: tar.xz + TZ: Asia/Singapore + environment: CI + name: test-x86_64-linux-deb11 (Test binaries) + needs: + - bindist-x86_64-linux-deb11 + runs-on: + - ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + - name: Download artifacts + uses: actions/download-artifact@v4 + with: + name: bindist-x86_64-linux-deb11 + path: ./out + - name: Test + uses: ./.github/actions/bindist-actions/action-deb11 + with: + stage: TEST + test-x86_64-linux-deb12: + env: + ADD_CABAL_ARGS: --enable-split-sections + ARCH: '64' + ARTIFACT: x86_64-linux-deb12 + AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} + AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} + DEBIAN_FRONTEND: noninteractive + S3_HOST: ${{ secrets.S3_HOST }} + TARBALL_EXT: tar.xz + TZ: Asia/Singapore + environment: CI + name: test-x86_64-linux-deb12 (Test binaries) + needs: + - bindist-x86_64-linux-deb12 + runs-on: + - ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + - name: Download artifacts + uses: actions/download-artifact@v4 + with: + name: bindist-x86_64-linux-deb12 + path: ./out + - name: Test + uses: ./.github/actions/bindist-actions/action-deb12 + with: + stage: TEST + test-x86_64-linux-deb9: + env: + ADD_CABAL_ARGS: --enable-split-sections + ARCH: '64' + ARTIFACT: x86_64-linux-deb9 + AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} + AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} + DEBIAN_FRONTEND: noninteractive + S3_HOST: ${{ secrets.S3_HOST }} + TARBALL_EXT: tar.xz + TZ: Asia/Singapore + environment: CI + name: test-x86_64-linux-deb9 (Test binaries) + needs: + - bindist-x86_64-linux-deb9 + runs-on: + - ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + - name: Download artifacts + uses: actions/download-artifact@v4 + with: + name: bindist-x86_64-linux-deb9 + path: ./out + - name: Test + uses: ./.github/actions/bindist-actions/action-deb9 + with: + stage: TEST + test-x86_64-linux-fedora33: + env: + ADD_CABAL_ARGS: --enable-split-sections + ARCH: '64' + ARTIFACT: x86_64-linux-fedora33 + AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} + AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} + DEBIAN_FRONTEND: noninteractive + S3_HOST: ${{ secrets.S3_HOST }} + TARBALL_EXT: tar.xz + TZ: Asia/Singapore + environment: CI + name: test-x86_64-linux-fedora33 (Test binaries) + needs: + - bindist-x86_64-linux-fedora33 + runs-on: + - ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + - name: Download artifacts + uses: actions/download-artifact@v4 + with: + name: bindist-x86_64-linux-fedora33 + path: ./out + - name: Test + uses: ./.github/actions/bindist-actions/action-fedora33 + with: + stage: TEST + test-x86_64-linux-fedora40: + env: + ADD_CABAL_ARGS: --enable-split-sections + ARCH: '64' + ARTIFACT: x86_64-linux-fedora40 + AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} + AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} + DEBIAN_FRONTEND: noninteractive + S3_HOST: ${{ secrets.S3_HOST }} + TARBALL_EXT: tar.xz + TZ: Asia/Singapore + environment: CI + name: test-x86_64-linux-fedora40 (Test binaries) + needs: + - bindist-x86_64-linux-fedora40 + runs-on: + - ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + - name: Download artifacts + uses: actions/download-artifact@v4 + with: + name: bindist-x86_64-linux-fedora40 + path: ./out + - name: Test + uses: ./.github/actions/bindist-actions/action-fedora40 + with: + stage: TEST + test-x86_64-linux-mint193: + env: + ADD_CABAL_ARGS: --enable-split-sections + ARCH: '64' + ARTIFACT: x86_64-linux-mint193 + AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} + AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} + DEBIAN_FRONTEND: noninteractive + S3_HOST: ${{ secrets.S3_HOST }} + TARBALL_EXT: tar.xz + TZ: Asia/Singapore + environment: CI + name: test-x86_64-linux-mint193 (Test binaries) + needs: + - bindist-x86_64-linux-mint193 + runs-on: + - ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + - name: Download artifacts + uses: actions/download-artifact@v4 + with: + name: bindist-x86_64-linux-mint193 + path: ./out + - name: Test + uses: ./.github/actions/bindist-actions/action-mint193 + with: + stage: TEST + test-x86_64-linux-mint202: + env: + ADD_CABAL_ARGS: --enable-split-sections + ARCH: '64' + ARTIFACT: x86_64-linux-mint202 + AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} + AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} + DEBIAN_FRONTEND: noninteractive + S3_HOST: ${{ secrets.S3_HOST }} + TARBALL_EXT: tar.xz + TZ: Asia/Singapore + environment: CI + name: test-x86_64-linux-mint202 (Test binaries) + needs: + - bindist-x86_64-linux-mint202 + runs-on: + - ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + - name: Download artifacts + uses: actions/download-artifact@v4 + with: + name: bindist-x86_64-linux-mint202 + path: ./out + - name: Test + uses: ./.github/actions/bindist-actions/action-mint202 + with: + stage: TEST + test-x86_64-linux-mint213: + env: + ADD_CABAL_ARGS: --enable-split-sections + ARCH: '64' + ARTIFACT: x86_64-linux-mint213 + AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} + AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} + DEBIAN_FRONTEND: noninteractive + S3_HOST: ${{ secrets.S3_HOST }} + TARBALL_EXT: tar.xz + TZ: Asia/Singapore + environment: CI + name: test-x86_64-linux-mint213 (Test binaries) + needs: + - bindist-x86_64-linux-mint213 + runs-on: + - ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + - name: Download artifacts + uses: actions/download-artifact@v4 + with: + name: bindist-x86_64-linux-mint213 + path: ./out + - name: Test + uses: ./.github/actions/bindist-actions/action-mint213 + with: + stage: TEST + test-x86_64-linux-ubuntu1804: + env: + ADD_CABAL_ARGS: --enable-split-sections + ARCH: '64' + ARTIFACT: x86_64-linux-ubuntu1804 + AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} + AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} + DEBIAN_FRONTEND: noninteractive + S3_HOST: ${{ secrets.S3_HOST }} + TARBALL_EXT: tar.xz + TZ: Asia/Singapore + environment: CI + name: test-x86_64-linux-ubuntu1804 (Test binaries) + needs: + - bindist-x86_64-linux-ubuntu1804 + runs-on: + - ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + - name: Download artifacts + uses: actions/download-artifact@v4 + with: + name: bindist-x86_64-linux-ubuntu1804 + path: ./out + - name: Test + uses: ./.github/actions/bindist-actions/action-ubuntu1804 + with: + stage: TEST + test-x86_64-linux-ubuntu2004: + env: + ADD_CABAL_ARGS: --enable-split-sections + ARCH: '64' + ARTIFACT: x86_64-linux-ubuntu2004 + AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} + AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} + DEBIAN_FRONTEND: noninteractive + S3_HOST: ${{ secrets.S3_HOST }} + TARBALL_EXT: tar.xz + TZ: Asia/Singapore + environment: CI + name: test-x86_64-linux-ubuntu2004 (Test binaries) + needs: + - bindist-x86_64-linux-ubuntu2004 + runs-on: + - ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + - name: Download artifacts + uses: actions/download-artifact@v4 + with: + name: bindist-x86_64-linux-ubuntu2004 + path: ./out + - name: Test + uses: ./.github/actions/bindist-actions/action-ubuntu2004 + with: + stage: TEST + test-x86_64-linux-ubuntu2204: + env: + ADD_CABAL_ARGS: --enable-split-sections + ARCH: '64' + ARTIFACT: x86_64-linux-ubuntu2204 + AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} + AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} + DEBIAN_FRONTEND: noninteractive + S3_HOST: ${{ secrets.S3_HOST }} + TARBALL_EXT: tar.xz + TZ: Asia/Singapore + environment: CI + name: test-x86_64-linux-ubuntu2204 (Test binaries) + needs: + - bindist-x86_64-linux-ubuntu2204 + runs-on: + - ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + - name: Download artifacts + uses: actions/download-artifact@v4 + with: + name: bindist-x86_64-linux-ubuntu2204 + path: ./out + - name: Test + uses: ./.github/actions/bindist-actions/action-ubuntu2204 + with: + stage: TEST + test-x86_64-linux-unknown: + env: + ADD_CABAL_ARGS: --enable-split-sections + ARCH: '64' + ARTIFACT: x86_64-linux-unknown + AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} + AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} + DEBIAN_FRONTEND: noninteractive + S3_HOST: ${{ secrets.S3_HOST }} + TARBALL_EXT: tar.xz + TZ: Asia/Singapore + environment: CI + name: test-x86_64-linux-unknown (Test binaries) + needs: + - bindist-x86_64-linux-unknown + runs-on: + - ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + - name: Download artifacts + uses: actions/download-artifact@v4 + with: + name: bindist-x86_64-linux-unknown + path: ./out + - name: Test + uses: ./.github/actions/bindist-actions/action-unknown + with: + stage: TEST + test-x86_64-mac: + env: + ADD_CABAL_ARGS: '' + ARCH: '64' + ARTIFACT: x86_64-apple-darwin + AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} + AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} + MACOSX_DEPLOYMENT_TARGET: '10.13' + S3_HOST: ${{ secrets.S3_HOST }} + TARBALL_EXT: tar.xz + TZ: Asia/Singapore + environment: CI + name: test-x86_64-mac (Test binaries) + needs: + - bindist-x86_64-mac + runs-on: + - macOS-13 + steps: + - name: Checkout + uses: actions/checkout@v4 + - name: Download artifacts + uses: actions/download-artifact@v4 + with: + name: bindist-x86_64-apple-darwin + path: ./out + - name: Run test + run: | + brew install coreutils tree + bash .github/scripts/test.sh + shell: sh + test-x86_64-windows: + env: + ADD_CABAL_ARGS: '' + ARCH: '64' + ARTIFACT: x86_64-mingw64 + AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} + AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} + S3_HOST: ${{ secrets.S3_HOST }} + TARBALL_EXT: zip + TZ: Asia/Singapore + environment: CI + name: test-x86_64-windows (Test binaries) + needs: + - bindist-x86_64-windows + runs-on: + - windows-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + - name: Download artifacts + uses: actions/download-artifact@v4 + with: + name: bindist-x86_64-mingw64 + path: ./out + - name: install windows deps + run: | + C:\msys64\usr\bin\bash -lc "pacman --disable-download-timeout --noconfirm -Syuu" + C:\msys64\usr\bin\bash -lc "pacman --disable-download-timeout --noconfirm -Syuu" + C:\msys64\usr\bin\bash -lc "pacman --disable-download-timeout --noconfirm -S make mingw-w64-x86_64-clang curl autoconf mingw-w64-x86_64-pkgconf ca-certificates base-devel gettext autoconf make libtool automake python p7zip patch unzip zip git" + taskkill /F /FI "MODULES eq msys-2.0.dll" + shell: pwsh + - name: Run test + run: | + $env:CHERE_INVOKING = 1 + $env:MSYS2_PATH_TYPE = "inherit" + C:\msys64\usr\bin\bash -lc "bash .github/scripts/test.sh" + shell: pwsh +name: Build and release +'on': + push: + tags: + - '*' + schedule: + - cron: 0 2 * * 1 diff --git a/.github/workflows/supported-ghc-versions.json b/.github/workflows/supported-ghc-versions.json index 5a59fdc0a7..35a3bd4ac4 100644 --- a/.github/workflows/supported-ghc-versions.json +++ b/.github/workflows/supported-ghc-versions.json @@ -1 +1 @@ -[ "9.8", "9.6", "9.4" , "9.2" ] +["9.12", "9.10", "9.8", "9.6"] diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 400ad0c3df..984758a310 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -74,13 +74,20 @@ jobs: - ubuntu-latest - macOS-latest - windows-latest - # Mark which GHC versions on which platforms we want to test. - include: - # Test all supported versions, but only on ubuntu and windows - - os: ubuntu-latest - test: true - - os: windows-latest - test: true + test: + - true + - false + exclude: + # Exclude the test configuration on macos, it's sufficiently similar to other OSs + # that it mostly just burns CI time. Buiding is still useful since it catches + # solver issues. + - os: macOS-latest + test: true + # Exclude the build-only configurations for windows and ubuntu + - os: windows-latest + test: false + - os: ubuntu-latest + test: false steps: - uses: actions/checkout@v3 @@ -88,159 +95,171 @@ jobs: - uses: ./.github/actions/setup-build with: ghc: ${{ matrix.ghc }} - os: ${{ runner.os }} + os: ${{ runner.os }} - name: Build - run: cabal build + run: cabal build all - name: Set test options - # run the tests without parallelism, otherwise tasty will attempt to run - # all functional test cases simultaneously which causes way too many hls - # instances to be spun up for the poor github actions runner to handle + # See https://github.com/ocharles/tasty-rerun/issues/22 for why we need + # to include 'new' in the filters, since many of our test suites are in the + # same package. run: | - echo "TEST_OPTS=-j1 --rerun-update --rerun-filter failures,exceptions" >> $GITHUB_ENV - - - name: Cache test log between attempts of the same run - uses: actions/cache@v3 - env: - cache-name: cache-test-log - with: - path: "**/.tasty-rerun-log*" - key: v1-${{ runner.os }}-${{ matrix.ghc }}-test-log-${{ github.sha }} + cabal configure --test-options="--rerun-update --rerun-filter failures,exceptions,new" - if: matrix.test name: Test hls-graph - run: cabal test hls-graph --test-options="$TEST_OPTS" + run: cabal test hls-graph - if: needs.pre_job.outputs.should_skip_ghcide != 'true' && matrix.test name: Test ghcide # run the tests without parallelism to avoid running out of memory - run: cabal test ghcide --test-options="$TEST_OPTS" || cabal test ghcide --test-options="$TEST_OPTS" + run: cabal test ghcide-tests || cabal test ghcide-tests - if: matrix.test name: Test hls-plugin-api - run: cabal test hls-plugin-api --test-options="$TEST_OPTS" || cabal test hls-plugin-api --test-options="$TEST_OPTS" + run: cabal test hls-plugin-api || cabal test hls-plugin-api - if: matrix.test name: Test func-test suite env: HLS_TEST_EXE: hls HLS_WRAPPER_TEST_EXE: hls-wrapper - run: cabal test func-test --test-options="$TEST_OPTS" || cabal test func-test --test-options="$TEST_OPTS" + run: cabal test func-test || cabal test func-test - if: matrix.test name: Test wrapper-test suite env: HLS_TEST_EXE: hls HLS_WRAPPER_TEST_EXE: hls-wrapper - run: cabal test wrapper-test --test-options="$TEST_OPTS --rerun-log-file .tasty-rerun-log-wrapper" + run: cabal test wrapper-test - - if: matrix.test && !startsWith(matrix.ghc,'9.8') + - if: matrix.test name: Test hls-refactor-plugin - run: cabal test hls-refactor-plugin --test-options="$TEST_OPTS" || cabal test hls-refactor-plugin --test-options="$TEST_OPTS" + run: cabal test hls-refactor-plugin-tests || cabal test hls-refactor-plugin-tests - - if: matrix.test && matrix.ghc != '9.6' && !startsWith(matrix.ghc,'9.8') + # TODO enable when it supports 9.10 + - if: matrix.test && matrix.ghc != '9.10' && matrix.ghc != '9.12' name: Test hls-floskell-plugin - run: cabal test hls-floskell-plugin --test-options="$TEST_OPTS" || cabal test hls-floskell-plugin --test-options="$TEST_OPTS" + run: cabal test hls-floskell-plugin-tests || cabal test hls-floskell-plugin-tests - - if: matrix.test && !startsWith(matrix.ghc,'9.8') + - if: matrix.test name: Test hls-class-plugin - run: cabal test hls-class-plugin --test-options="$TEST_OPTS" || cabal test hls-class-plugin --test-options="$TEST_OPTS" + run: cabal test hls-class-plugin-tests || cabal test hls-class-plugin-tests - if: matrix.test name: Test hls-pragmas-plugin - run: cabal test hls-pragmas-plugin --test-options="$TEST_OPTS" || cabal test hls-pragmas-plugin --test-options="$TEST_OPTS" + run: cabal test hls-pragmas-plugin-tests || cabal test hls-pragmas-plugin-tests - if: matrix.test name: Test hls-eval-plugin - run: cabal test hls-eval-plugin --test-options="$TEST_OPTS" || cabal test hls-eval-plugin --test-options="$TEST_OPTS" + run: cabal test hls-eval-plugin-tests || cabal test hls-eval-plugin-tests - - if: matrix.test && !startsWith(matrix.ghc,'9.8') + # TODO enable when it supports 9.10 + - if: matrix.test && matrix.ghc != '9.10' && matrix.ghc != '9.12' name: Test hls-splice-plugin - run: cabal test hls-splice-plugin --test-options="$TEST_OPTS" || cabal test hls-splice-plugin --test-options="$TEST_OPTS" + run: cabal test hls-splice-plugin-tests || cabal test hls-splice-plugin-tests - - if: matrix.test && matrix.ghc != '9.2' + - if: matrix.test && matrix.ghc != '9.12' name: Test hls-stan-plugin - run: cabal test hls-stan-plugin --test-options="$TEST_OPTS" || cabal test hls-stan-plugin --test-options="$TEST_OPTS" + run: cabal test hls-stan-plugin-tests || cabal test hls-stan-plugin-tests - - if: matrix.test && !startsWith(matrix.ghc,'9.8') + - if: matrix.test name: Test hls-stylish-haskell-plugin - run: cabal test hls-stylish-haskell-plugin --test-options="$TEST_OPTS" || cabal test hls-stylish-haskell-plugin --test-options="$TEST_OPTS" + run: cabal test hls-stylish-haskell-plugin-tests || cabal test hls-stylish-haskell-plugin-tests - - if: matrix.test && !startsWith(matrix.ghc,'9.8') + - if: matrix.test name: Test hls-ormolu-plugin - run: cabal test hls-ormolu-plugin --test-options="$TEST_OPTS" || cabal test hls-ormolu-plugin --test-options="$TEST_OPTS" + run: cabal test hls-ormolu-plugin-tests || cabal test hls-ormolu-plugin-tests - - if: matrix.test && !startsWith(matrix.ghc,'9.8') + - if: matrix.test name: Test hls-fourmolu-plugin - run: cabal test hls-fourmolu-plugin --test-options="$TEST_OPTS" || cabal test hls-fourmolu-plugin --test-options="$TEST_OPTS" + run: cabal test hls-fourmolu-plugin-tests || cabal test hls-fourmolu-plugin-tests - if: matrix.test name: Test hls-explicit-imports-plugin test suite - run: cabal test hls-explicit-imports-plugin --test-options="$TEST_OPTS" || cabal test hls-explicit-imports-plugin --test-options="$TEST_OPTS" + run: cabal test hls-explicit-imports-plugin-tests || cabal test hls-explicit-imports-plugin-tests - if: matrix.test name: Test hls-call-hierarchy-plugin test suite - run: cabal test hls-call-hierarchy-plugin --test-options="$TEST_OPTS" || cabal test hls-call-hierarchy-plugin --test-options="$TEST_OPTS" + run: cabal test hls-call-hierarchy-plugin-tests || cabal test hls-call-hierarchy-plugin-tests - - if: matrix.test && matrix.os != 'windows-latest' && !startsWith(matrix.ghc,'9.8') + - if: matrix.test && matrix.os != 'windows-latest' name: Test hls-rename-plugin test suite - run: cabal test hls-rename-plugin --test-options="$TEST_OPTS" || cabal test hls-rename-plugin --test-options="$TEST_OPTS" + run: cabal test hls-rename-plugin-tests || cabal test hls-rename-plugin-tests - - if: matrix.test && !startsWith(matrix.ghc,'9.8') + # TODO enable when it supports 9.10 + - if: matrix.test && matrix.ghc != '9.10' name: Test hls-hlint-plugin test suite - run: cabal test hls-hlint-plugin --test-options="$TEST_OPTS" || cabal test hls-hlint-plugin --test-options="$TEST_OPTS" + run: cabal test hls-hlint-plugin-tests || cabal test hls-hlint-plugin-tests - if: matrix.test name: Test hls-module-name-plugin test suite - run: cabal test hls-module-name-plugin --test-options="$TEST_OPTS" || cabal test hls-module-name-plugin --test-options="$TEST_OPTS" + run: cabal test hls-module-name-plugin-tests || cabal test hls-module-name-plugin-tests - if: matrix.test name: Test hls-alternate-number-format-plugin test suite - run: cabal test hls-alternate-number-format-plugin --test-options="$TEST_OPTS" || cabal test hls-alternate-number-format-plugin --test-options="$TEST_OPTS" + run: cabal test hls-alternate-number-format-plugin-tests || cabal test hls-alternate-number-format-plugin-tests - if: matrix.test name: Test hls-qualify-imported-names-plugin test suite - run: cabal test hls-qualify-imported-names-plugin --test-options="$TEST_OPTS" || cabal test hls-qualify-imported-names-plugin --test-options="$TEST_OPTS" + run: cabal test hls-qualify-imported-names-plugin-tests || cabal test hls-qualify-imported-names-plugin-tests - if: matrix.test name: Test hls-code-range-plugin test suite - run: cabal test hls-code-range-plugin --test-options="$TEST_OPTS" || cabal test hls-code-range-plugin --test-options="$TEST_OPTS" + run: cabal test hls-code-range-plugin-tests || cabal test hls-code-range-plugin-tests - if: matrix.test name: Test hls-change-type-signature test suite - run: cabal test hls-change-type-signature-plugin --test-options="$TEST_OPTS" || cabal test hls-change-type-signature-plugin --test-options="$TEST_OPTS" + run: cabal test hls-change-type-signature-plugin-tests || cabal test hls-change-type-signature-plugin-tests - - if: matrix.test && !startsWith(matrix.ghc,'9.8') + - if: matrix.test name: Test hls-gadt-plugin test suit - run: cabal test hls-gadt-plugin --test-options="$TEST_OPTS" || cabal test hls-gadt-plugin --test-options="$TEST_OPTS" + run: cabal test hls-gadt-plugin-tests || cabal test hls-gadt-plugin-tests - if: matrix.test name: Test hls-explicit-fixity-plugin test suite - run: cabal test hls-explicit-fixity-plugin --test-options="$TEST_OPTS" || cabal test hls-explicit-fixity-plugin --test-options="$TEST_OPTS" + run: cabal test hls-explicit-fixity-plugin-tests || cabal test hls-explicit-fixity-plugin-tests - if: matrix.test name: Test hls-explicit-record-fields-plugin test suite - run: cabal test hls-explicit-record-fields-plugin --test-options="$TEST_OPTS" || cabal test hls-explicit-record-fields-plugin --test-options="$TEST_OPTS" + run: cabal test hls-explicit-record-fields-plugin-tests || cabal test hls-explicit-record-fields-plugin-tests - ## version needs to be limited since the tests depend on cabal-fmt which only builds using specific ghc versions - - if: matrix.test && matrix.ghc == '9.2' + # versions need to be limited since the tests depend on cabal-fmt which only builds with ghc <9.10 + - if: matrix.test && matrix.ghc != '9.10' && matrix.ghc != '9.12' name: Test hls-cabal-fmt-plugin test suite - run: cabal test hls-cabal-fmt-plugin --flag=isolateTests --test-options="$TEST_OPTS" || cabal test hls-cabal-fmt-plugin --flag=isolateTests --test-options="$TEST_OPTS" + run: cabal test hls-cabal-fmt-plugin-tests --flag=isolateCabalfmtTests || cabal test hls-cabal-fmt-plugin-tests --flag=isolateCabalfmtTests + + - if: matrix.test && matrix.ghc != '9.12' + name: Test hls-cabal-gild-plugin test suite + run: cabal test hls-cabal-gild-plugin-tests --flag=isolateCabalGildTests || cabal test hls-cabal-gild-plugin-tests --flag=isolateCabalGildTests - if: matrix.test name: Test hls-cabal-plugin test suite - run: cabal test hls-cabal-plugin --test-options="$TEST_OPTS" || cabal test hls-cabal-plugin --test-options="$TEST_OPTS" + run: cabal test hls-cabal-plugin-tests || cabal test hls-cabal-plugin-tests - - if: matrix.test && !startsWith(matrix.ghc,'9.8') + # TODO enable when it supports 9.10 + - if: matrix.test && matrix.ghc != '9.10' && matrix.ghc != '9.12' name: Test hls-retrie-plugin test suite - run: cabal test hls-retrie-plugin --test-options="$TEST_OPTS" || cabal test hls-retrie-plugin --test-options="$TEST_OPTS" + run: cabal test hls-retrie-plugin-tests || cabal test hls-retrie-plugin-tests - - if: matrix.test && matrix.ghc != '9.0' + - if: matrix.test name: Test hls-overloaded-record-dot-plugin test suite - run: cabal test hls-overloaded-record-dot-plugin --test-options="$TEST_OPTS" || LSP_TEST_LOG_COLOR=0 LSP_TEST_LOG_MESSAGES=true LSP_TEST_LOG_STDERR=true cabal test hls-overloaded-record-dot-plugin --test-options="$TEST_OPTS" + run: cabal test hls-overloaded-record-dot-plugin-tests || cabal test hls-overloaded-record-dot-plugin-tests + - if: matrix.test + name: Test hls-semantic-tokens-plugin test suite + run: cabal test hls-semantic-tokens-plugin-tests || cabal test hls-semantic-tokens-plugin-tests + - if: matrix.test + name: Test hls-notes-plugin test suite + run: cabal test hls-notes-plugin-tests || cabal test hls-notes-plugin-tests + + # The plugin tutorial is only compatible with 9.6 and 9.8. + # No particular reason, just to avoid excessive CPP. + - if: matrix.test && matrix.ghc != '9.4' && matrix.ghc != '9.10' && matrix.ghc != '9.12' + name: Compile the plugin-tutorial + run: cabal build plugin-tutorial test_post_job: if: always() diff --git a/.gitignore b/.gitignore index 29ead939cc..2413a1fcf5 100644 --- a/.gitignore +++ b/.gitignore @@ -7,6 +7,9 @@ cabal.project.local .tasty-rerun-log +# emacs +/.dir-locals.el + # shake build information _build/ diff --git a/.hlint.yaml b/.hlint.yaml index 852b8060b0..edc6886871 100644 --- a/.hlint.yaml +++ b/.hlint.yaml @@ -60,12 +60,10 @@ - Development.IDE.Graph.Internal.Database - Development.IDE.Graph.Internal.Paths - Development.IDE.Graph.Internal.Profile - - Development.IDE.Graph.Internal.Types + - Development.IDE.Graph.Internal.Key - Ide.Types - Test.Hls - Test.Hls.Command - - Wingman.Debug - - Wingman.Types - AutoTupleSpec - name: unsafeInterleaveIO within: @@ -76,7 +74,6 @@ - Ide.Plugin.Eval.Code - Development.IDE.Core.Compile - Development.IDE.Types.Shake - - Wingman.Judgements.SYB - Ide.Plugin.Properties # Things that are a bit dangerous in the GHC API @@ -97,7 +94,6 @@ - Main - Experiments - Development.Benchmark.Rules - - Development.IDE.Plugin.CodeAction - Development.IDE.Plugin.Completions - Development.IDE.Plugin.CodeAction.ExactPrint - Development.IDE.Spans.Documentation @@ -105,21 +101,16 @@ - Ide.Plugin.CallHierarchy.Internal - Ide.Plugin.Eval.Code - Ide.Plugin.Eval.Util - - Ide.Plugin.Floskell - - Ide.Plugin.ModuleName - - Ide.Plugin.Rename - Ide.Plugin.Class.ExactPrint - TExpectedActual - TRigidType - TRigidType2 - RightToLeftFixities - Typeclass - - Wingman.Judgements - - Wingman.Machinery - - Wingman.Tactics - CompletionTests #Previously part of GHCIDE Main tests - DiagnosticTests #Previously part of GHCIDE Main tests - FindDefinitionAndHoverTests #Previously part of GHCIDE Main tests + - FindImplementationAndHoverTests #Previously part of GHCIDE Main tests - TestUtils #Previously part of GHCIDE Main tests - CodeLensTests #Previously part of GHCIDE Main tests @@ -144,15 +135,15 @@ - Ide.Plugin.Eval.Parse.Comments - Ide.Plugin.Eval.CodeLens - FindDefinitionAndHoverTests #Previously part of GHCIDE Main tests + - FindImplementationAndHoverTests #Previously part of GHCIDE Main tests - name: [Prelude.init, Data.List.init] within: - Main - Development.IDE.Spans.Common - Ide.PluginUtils - - Wingman.Metaprogramming.Parser - Development.Benchmark.Rules - - ErrorGivenPartialSignature + - TErrorGivenPartialSignature - IfaceTests #Previously part of GHCIDE Main tests - THTests #Previously part of GHCIDE Main tests - WatchedFileTests #Previously part of GHCIDE Main tests @@ -172,8 +163,6 @@ - Development.IDE.Plugin.Completions.Logic - Development.IDE.Spans.Documentation - TErrorGivenPartialSignature - - Wingman.CaseSplit - - Wingman.Simplify - InitializeResponseTests #Previously part of GHCIDE Main tests - PositionMappingTests #Previously part of GHCIDE Main tests @@ -186,31 +175,23 @@ within: [] - name: Data.Foldable.foldr1 - within: - - Wingman.Tactics + within: [] - name: Data.Maybe.fromJust within: - Experiments - Main - - MultipleImports - Progress - - Utils - Development.IDE.Core.Compile - Development.IDE.Core.Rules - Development.IDE.Core.Shake - - Development.IDE.Plugin.Completions - - Development.IDE.Plugin.CodeAction.ExactPrint - - Development.IDE.Plugin.CodeAction - Development.IDE.Test - Development.IDE.Graph.Internal.Profile - Development.IDE.Graph.Internal.Rules - - Ide.Plugin.Class - CodeLensTests #Previously part of GHCIDE Main tests - name: "Data.Map.!" - within: - - Wingman.LanguageServer + within: [] - name: "Data.IntMap.!" within: [] @@ -251,7 +232,6 @@ - Development.IDE.Graph.Internal.Database - Development.IDE.GHC.Util - Development.IDE.Plugin.CodeAction.Util - - Wingman.Debug # We really do not want novel usages of restricted functions, and mere # Warning is not enough to prevent those consistently; you need a build failure. diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 9ef5013bd1..03edd673b7 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,32 +1,23 @@ -{ - "repos": [ - { - "hooks": [ - { - "entry": "stylish-haskell --inplace", - "exclude": "(^Setup.hs$|test/testdata/.*$|test/data/.*$|test/manual/lhs/.*$|^hie-compat/.*$|^plugins/hls-tactics-plugin/.*$|^ghcide/src/Development/IDE/GHC/Compat.hs$|^plugins/hls-refactor-plugin/src/Development/IDE/GHC/Compat/ExactPrint.hs$|^ghcide/src/Development/IDE/GHC/Compat/Core.hs$|^ghcide/src/Development/IDE/Spans/Pragmas.hs$|^ghcide/src/Development/IDE/LSP/Outline.hs$|^plugins/hls-splice-plugin/src/Ide/Plugin/Splice.hs$|^ghcide/test/exe/Main.hs$|^ghcide/src/Development/IDE/Core/Rules.hs$|^hls-test-utils/src/Test/Hls/Util.hs$|^ghcide/src/Development/IDE/Core/Compile.hs$|^plugins/hls-refactor-plugin/src/Development/IDE/GHC/ExactPrint.hs$|^plugins/hls-refactor-plugin/src/Development/IDE/Plugin/CodeAction/ExactPrint.hs$)", - "files": "\\.l?hs$", - "id": "stylish-haskell", - "language": "system", - "name": "stylish-haskell", - "pass_filenames": true, - "types": [ - "file" - ] - } - ], - "repo": "local" - }, - { - "repo": "https://github.com/pre-commit/pre-commit-hooks", - "rev": "v4.1.0", - "hooks": [ - { - "id": "mixed-line-ending", - "args": ["--fix", "lf"], - "exclude": "test/testdata/.*CRLF.*?\\.hs$" - } - ] - } - ] -} +# https://pre-commit.com/ +# https://github.com/pre-commit/pre-commit +repos: + - hooks: + - entry: stylish-haskell --inplace + exclude: >- + (^Setup.hs$|test/testdata/.*$|test/data/.*$|test/manual/lhs/.*$|^hie-compat/.*$|^plugins/hls-tactics-plugin/.*$|^ghcide/src/Development/IDE/GHC/Compat.hs$|^plugins/hls-refactor-plugin/src/Development/IDE/GHC/Compat/ExactPrint.hs$|^ghcide/src/Development/IDE/GHC/Compat/Core.hs$|^ghcide/src/Development/IDE/Spans/Pragmas.hs$|^ghcide/src/Development/IDE/LSP/Outline.hs$|^plugins/hls-splice-plugin/src/Ide/Plugin/Splice.hs$|^ghcide/src/Development/IDE/Core/Rules.hs$|^ghcide/src/Development/IDE/Core/Compile.hs$|^plugins/hls-refactor-plugin/src/Development/IDE/GHC/ExactPrint.hs$|^plugins/hls-refactor-plugin/src/Development/IDE/Plugin/CodeAction/ExactPrint.hs$|^plugins/hls-eval-plugin/src/Ide/Plugin/Eval/Handlers.hs$) + files: \.l?hs$ + id: stylish-haskell + language: system + name: stylish-haskell + pass_filenames: true + types: + - file + repo: local + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v4.1.0 + hooks: + - id: mixed-line-ending + args: + - '--fix' + - lf + exclude: test/testdata/.*CRLF.*?\.hs$ diff --git a/.readthedocs.yaml b/.readthedocs.yaml index c420108677..f5135a9af1 100644 --- a/.readthedocs.yaml +++ b/.readthedocs.yaml @@ -1,6 +1,7 @@ version: 2 sphinx: + builder: "html" configuration: docs/conf.py build: diff --git a/CODEOWNERS b/CODEOWNERS index fa6be0f263..7d66f7805e 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -1,50 +1,60 @@ # Core -/ghcide @pepeiborra -/ghcide/session-loader @pepeiborra @fendor -/hls-graph @pepeiborra -/hls-plugin-api @berberman +/ghcide @wz1000 +/ghcide/session-loader @wz1000 @fendor +/hls-graph @wz1000 +/hls-plugin-api @michaelpj @fendor /hls-test-utils @fendor +/hie-compat @wz1000 + +# HLS main +/src @fendor +/exe @fendor /test @fendor -/hie-compat # Plugins /plugins/hls-alternate-number-format-plugin @drsooch -/plugins/hls-cabal-plugin @fendor /plugins/hls-cabal-fmt-plugin @VeryMilkyJoe @fendor +/plugins/hls-cabal-gild-plugin @fendor +/plugins/hls-cabal-plugin @fendor /plugins/hls-call-hierarchy-plugin @July541 -/plugins/hls-class-plugin @Ailrun +/plugins/hls-change-type-signature-plugin +/plugins/hls-class-plugin +/plugins/hls-code-range-plugin @kokobd /plugins/hls-eval-plugin -/plugins/hls-explicit-imports-plugin @pepeiborra -/plugins/hls-floskell-plugin @Ailrun +/plugins/hls-explicit-fixity-plugin +/plugins/hls-explicit-imports-plugin +/plugins/hls-explicit-record-fields-plugin @ozkutuk +/plugins/hls-floskell-plugin @peterbecich /plugins/hls-fourmolu-plugin @georgefst /plugins/hls-gadt-plugin @July541 /plugins/hls-hlint-plugin @eddiemundo /plugins/hls-module-name-plugin +/plugins/hls-notes-plugin @jvanbruegge /plugins/hls-ormolu-plugin @georgefst -/plugins/hls-pragmas-plugin @berberman @Ailrun @eddiemundo +/plugins/hls-overloaded-record-dot-plugin @joyfulmantis +/plugins/hls-pragmas-plugin @eddiemundo /plugins/hls-qualify-imported-names-plugin @eddiemundo -/plugins/hls-rename-plugin @OliverMadine /plugins/hls-refactor-plugin @santiweight -/plugins/hls-retrie-plugin @pepeiborra -/plugins/hls-code-range-plugin @kokobd +/plugins/hls-rename-plugin +/plugins/hls-retrie-plugin @wz1000 +/plugins/hls-semantic-tokens-plugin @soulomoon /plugins/hls-splice-plugin @konn -/plugins/hls-stylish-haskell-plugin @Ailrun /plugins/hls-stan-plugin @0rphee -/plugins/hls-explicit-record-fields-plugin @ozkutuk -/plugins/hls-overloaded-record-dot-plugin @joyfulmantis +/plugins/hls-stylish-haskell-plugin @michaelpj # Benchmarking -/shake-bench @pepeiborra +/shake-bench +/bench # Docs /docs @michaelpj # CI -/.circleci @Anton-Latukha -/.github @Anton-Latukha @Ailrun -/.gitlab @hasufell +/.circleci +/.github @michaelpj @fendor # Build *.nix @berberman @michaelpj @guibou -*.project +*.project @michaelpj +*.stack* @michaelpj .gitpod.* @kokobd diff --git a/ChangeLog.md b/ChangeLog.md index 753a627279..65000395e2 100644 --- a/ChangeLog.md +++ b/ChangeLog.md @@ -1,5 +1,754 @@ # Changelog for haskell-language-server + +## 2.11.0.0 + +- Bindists for GHC 9.12.2 + - Full plugin support, inlcuding refactor plugin +- Bindists for GHC 9.10.2 +- Bindists for GHC 9.8.4 +- Bindists for GHC 9.6.7 +- Bindists for GHC 9.4.8 +- Dropped support for Centos 7 as this platform is no longer supported by ghc +- Improved import suggestions for contructors and OverloadedRecordDot fields + +### Pull Requests + +- Add doc for project-wide renaming + ([#4584](https://github.com/haskell/haskell-language-server/pull/4584)) by @jian-lin +- Use hie-bios 0.15.0 + ([#4582](https://github.com/haskell/haskell-language-server/pull/4582)) by @fendor +- Allow building HLS with GHC 9.10.2 + ([#4581](https://github.com/haskell/haskell-language-server/pull/4581)) by @fendor +- Fix Plugin support table for 9.12.2 + ([#4580](https://github.com/haskell/haskell-language-server/pull/4580)) by @fendor +- Fix misplaced inlay hints by applying PositionMapping + ([#4571](https://github.com/haskell/haskell-language-server/pull/4571)) by @jetjinser +- Enable hls-plugin-gadt for ghc-9.12 + ([#4568](https://github.com/haskell/haskell-language-server/pull/4568)) by @GuillaumedeVolpiano +- Remove no longer needed allow-newer + ([#4566](https://github.com/haskell/haskell-language-server/pull/4566)) by @jhrcek +- Add missing golden files for GHC 9.10 config tests + ([#4563](https://github.com/haskell/haskell-language-server/pull/4563)) by @jian-lin +- updating the plugins support table for refactor + ([#4560](https://github.com/haskell/haskell-language-server/pull/4560)) by @GuillaumedeVolpiano +- Enable stylish-haskell for ghc-9.10 and ghc-9.12 + ([#4559](https://github.com/haskell/haskell-language-server/pull/4559)) by @GuillaumedeVolpiano +- Bump haskell-actions/setup from 2.7.10 to 2.7.11 + ([#4557](https://github.com/haskell/haskell-language-server/pull/4557)) by @dependabot[bot] +- Provide code action in hls-eval-plugin + ([#4556](https://github.com/haskell/haskell-language-server/pull/4556)) by @jian-lin +- enable hlint for ghc-9.12 + ([#4555](https://github.com/haskell/haskell-language-server/pull/4555)) by @GuillaumedeVolpiano +- Enable fourmolu and ormolu for GHC 9.12 + ([#4554](https://github.com/haskell/haskell-language-server/pull/4554)) by @fendor +- Enable hls-cabal-gild-plugin for GHC 9.12.2 + ([#4553](https://github.com/haskell/haskell-language-server/pull/4553)) by @fendor +- Update plugin support table for GHC 9.12.2 + ([#4552](https://github.com/haskell/haskell-language-server/pull/4552)) by @fendor +- Remove allow-newer for hiedb + ([#4551](https://github.com/haskell/haskell-language-server/pull/4551)) by @jhrcek +- Fix typo of rename plugin config + ([#4546](https://github.com/haskell/haskell-language-server/pull/4546)) by @jian-lin +- Update the ghcup-metadata generation script + ([#4545](https://github.com/haskell/haskell-language-server/pull/4545)) by @fendor +- porting hls-refactor to ghc-9.12 + ([#4543](https://github.com/haskell/haskell-language-server/pull/4543)) by @GuillaumedeVolpiano +- add ghcide-bench flag to .cabal file + ([#4542](https://github.com/haskell/haskell-language-server/pull/4542)) by @juhp +- Revert "link executables dynamically to speed up linking (#4423)" + ([#4541](https://github.com/haskell/haskell-language-server/pull/4541)) by @fendor +- Support PackageImports in hiddenPackageSuggestion + ([#4537](https://github.com/haskell/haskell-language-server/pull/4537)) by @jian-lin +- Improve FreeBSD installation docs + ([#4536](https://github.com/haskell/haskell-language-server/pull/4536)) by @arrowd +- reinstating ignore-plugins-ghc-bounds + ([#4532](https://github.com/haskell/haskell-language-server/pull/4532)) by @GuillaumedeVolpiano +- Simplify FuzzySearch test (avoid dependency on /usr/share/dict/words) + ([#4531](https://github.com/haskell/haskell-language-server/pull/4531)) by @jhrcek +- Import suggestion for missing newtype constructor, all types constructor and indirect overloadedrecorddot fields + ([#4516](https://github.com/haskell/haskell-language-server/pull/4516)) by @guibou + +## 2.10.0.0 + +- Bindists for GHC 9.12.2 + - This is only basic support, many plugins are not yet compatible. +- Bindists for GHC 9.8.4 +- Bindists for GHC 9.6.7 +- `hls-cabal-plugin` features + - Support for `cabal-add` + - Goto Definition for common sections + - Outline of .cabal files +- Fix handling of LSP resolve requests +- Display Inlay Hints + - Records + - Imports + +### Pull Requests + +- Fix cabal check for Hackage release + ([#4528](https://github.com/haskell/haskell-language-server/pull/4528)) by @fendor +- GHC 9.12 support + ([#4527](https://github.com/haskell/haskell-language-server/pull/4527)) by @wz1000 +- Bump cachix/install-nix-action from 30 to 31 + ([#4525](https://github.com/haskell/haskell-language-server/pull/4525)) by @dependabot[bot] +- Bump cachix/cachix-action from 15 to 16 + ([#4523](https://github.com/haskell/haskell-language-server/pull/4523)) by @dependabot[bot] +- Bump haskell-actions/setup from 2.7.9 to 2.7.10 + ([#4522](https://github.com/haskell/haskell-language-server/pull/4522)) by @dependabot[bot] +- Bump haskell-actions/setup from 2.7.9 to 2.7.10 in /.github/actions/setup-build + ([#4521](https://github.com/haskell/haskell-language-server/pull/4521)) by @dependabot[bot] +- Move ghcide-test to stand alone dir + ([#4520](https://github.com/haskell/haskell-language-server/pull/4520)) by @soulomoon +- refactor: remove unnecessary instance and use of unsafeCoerce + ([#4518](https://github.com/haskell/haskell-language-server/pull/4518)) by @MangoIV +- convert `pre-commit-config.yaml` from JSON to YAML + ([#4513](https://github.com/haskell/haskell-language-server/pull/4513)) by @peterbecich +- Enable bench for 9.10 + ([#4512](https://github.com/haskell/haskell-language-server/pull/4512)) by @soulomoon +- Bugfix: Explicit record fields inlay hints for polymorphic records + ([#4510](https://github.com/haskell/haskell-language-server/pull/4510)) by @wczyz +- Capitalization of "Replace" + ([#4509](https://github.com/haskell/haskell-language-server/pull/4509)) by @dschrempf +- document eval plugin not supporting multiline expressions + ([#4495](https://github.com/haskell/haskell-language-server/pull/4495)) by @noughtmare +- Documentation: Imrpove "Contributing" (and amend Sphinx builders) + ([#4494](https://github.com/haskell/haskell-language-server/pull/4494)) by @dschrempf +- Documentation: HLS plugin tutorial improvements + ([#4491](https://github.com/haskell/haskell-language-server/pull/4491)) by @dschrempf +- Nix tooling (minor changes) + ([#4490](https://github.com/haskell/haskell-language-server/pull/4490)) by @dschrempf +- Bump haskell-actions/setup from 2.7.8 to 2.7.9 + ([#4483](https://github.com/haskell/haskell-language-server/pull/4483)) by @dependabot[bot] +- Bump haskell-actions/setup from 2.7.8 to 2.7.9 in /.github/actions/setup-build + ([#4482](https://github.com/haskell/haskell-language-server/pull/4482)) by @dependabot[bot] +- Rework bindist CI + ([#4481](https://github.com/haskell/haskell-language-server/pull/4481)) by @wz1000 +- Remove Unsafe Dynflags deadcode, they don't exist any more! + ([#4480](https://github.com/haskell/haskell-language-server/pull/4480)) by @fendor +- Implement fallback handler for `*/resolve` requests + ([#4478](https://github.com/haskell/haskell-language-server/pull/4478)) by @fendor +- Bump haskell-actions/setup from 2.7.7 to 2.7.8 + ([#4477](https://github.com/haskell/haskell-language-server/pull/4477)) by @dependabot[bot] +- Bump haskell-actions/setup from 2.7.7 to 2.7.8 in /.github/actions/setup-build + ([#4476](https://github.com/haskell/haskell-language-server/pull/4476)) by @dependabot[bot] +- Bump haskell-actions/setup from 2.7.6 to 2.7.7 + ([#4471](https://github.com/haskell/haskell-language-server/pull/4471)) by @dependabot[bot] +- Bump haskell-actions/setup from 2.7.6 to 2.7.7 in /.github/actions/setup-build + ([#4470](https://github.com/haskell/haskell-language-server/pull/4470)) by @dependabot[bot] +- Allow building with GHC 9.8.4 + ([#4459](https://github.com/haskell/haskell-language-server/pull/4459)) by @fendor +- Update python read-the-docs dependencies to latest + ([#4457](https://github.com/haskell/haskell-language-server/pull/4457)) by @fendor +- More tests and better docs for cabal-add + ([#4455](https://github.com/haskell/haskell-language-server/pull/4455)) by @VenInf +- ci(mergify): upgrade configuration to current format + ([#4454](https://github.com/haskell/haskell-language-server/pull/4454)) by @mergify[bot] +- Support record positional construction inlay hints + ([#4447](https://github.com/haskell/haskell-language-server/pull/4447)) by @jetjinser +- Build HLS with GHC 9.8.3 + ([#4444](https://github.com/haskell/haskell-language-server/pull/4444)) by @fendor +- Don't suggest -Wno-deferred-out-of-scope-variables + ([#4441](https://github.com/haskell/haskell-language-server/pull/4441)) by @jeukshi +- Enable hls-stan-plugin for GHC 9.10.1 + ([#4437](https://github.com/haskell/haskell-language-server/pull/4437)) by @fendor +- Enhance formatting of the `cabal-version` error message + ([#4436](https://github.com/haskell/haskell-language-server/pull/4436)) by @fendor +- Support structured diagnostics 2 + ([#4433](https://github.com/haskell/haskell-language-server/pull/4433)) by @noughtmare +- Cabal ignore if for completions (#4289) + ([#4427](https://github.com/haskell/haskell-language-server/pull/4427)) by @SamuelLess +- Fix cabal-add testdata for hls-cabal-plugin-tests + ([#4426](https://github.com/haskell/haskell-language-server/pull/4426)) by @fendor +- gracefully handle errors for unsupported cabal version + ([#4425](https://github.com/haskell/haskell-language-server/pull/4425)) by @fridewald +- Fix pre-commit in CI + ([#4424](https://github.com/haskell/haskell-language-server/pull/4424)) by @fendor +- link executables dynamically to speed up linking + ([#4423](https://github.com/haskell/haskell-language-server/pull/4423)) by @develop7 +- Cabal plugin: implement check for package.yaml in a stack project + ([#4422](https://github.com/haskell/haskell-language-server/pull/4422)) by @JMoss-dev +- Fix exporting operator pattern synonym + ([#4420](https://github.com/haskell/haskell-language-server/pull/4420)) by @pbrinkmeier +- Add docs about running tests for new contributors + ([#4418](https://github.com/haskell/haskell-language-server/pull/4418)) by @pbrinkmeier +- Bump cachix/install-nix-action from 29 to 30 + ([#4413](https://github.com/haskell/haskell-language-server/pull/4413)) by @dependabot[bot] +- Bump cachix/install-nix-action from V27 to 29 + ([#4411](https://github.com/haskell/haskell-language-server/pull/4411)) by @dependabot[bot] +- Avoid expectFail in the test suite + ([#4402](https://github.com/haskell/haskell-language-server/pull/4402)) by @sgillespie +- Fix typos in hls-cabal-fmt-plugin + ([#4399](https://github.com/haskell/haskell-language-server/pull/4399)) by @fendor +- Jump to instance definition and explain typeclass evidence + ([#4392](https://github.com/haskell/haskell-language-server/pull/4392)) by @fendor +- Update cabal-add dependency + ([#4389](https://github.com/haskell/haskell-language-server/pull/4389)) by @VenInf +- Improve error message for `--probe-tools` + ([#4387](https://github.com/haskell/haskell-language-server/pull/4387)) by @sgillespie +- Documentation for build-depends on hover + ([#4385](https://github.com/haskell/haskell-language-server/pull/4385)) by @VenInf +- Bump haskell-actions/setup from 2.7.3 to 2.7.6 + ([#4384](https://github.com/haskell/haskell-language-server/pull/4384)) by @dependabot[bot] +- Bump haskell-actions/setup from 2.7.5 to 2.7.6 in /.github/actions/setup-build + ([#4383](https://github.com/haskell/haskell-language-server/pull/4383)) by @dependabot[bot] +- Clear GHCup caches in CI to not run out of space in CI + ([#4382](https://github.com/haskell/haskell-language-server/pull/4382)) by @fendor +- Cabal go to module's definition + ([#4380](https://github.com/haskell/haskell-language-server/pull/4380)) by @VenInf +- Add Goto Definition for cabal common sections + ([#4375](https://github.com/haskell/haskell-language-server/pull/4375)) by @ChristophHochrainer +- cabal-add integration as a CodeAction + ([#4360](https://github.com/haskell/haskell-language-server/pull/4360)) by @VenInf +- Bump haskell-actions/setup from 2.7.3 to 2.7.5 in /.github/actions/setup-build + ([#4354](https://github.com/haskell/haskell-language-server/pull/4354)) by @dependabot[bot] +- Support Inlay hints for record wildcards + ([#4351](https://github.com/haskell/haskell-language-server/pull/4351)) by @jetjinser +- Remove componentInternalUnits + ([#4350](https://github.com/haskell/haskell-language-server/pull/4350)) by @soulomoon +- Fix core file location in `GetLinkable` + ([#4347](https://github.com/haskell/haskell-language-server/pull/4347)) by @soulomoon +- Release 2.9.0.1 + ([#4346](https://github.com/haskell/haskell-language-server/pull/4346)) by @wz1000 +- Using captureKicksDiagnostics to speed up multiple plugin tests + ([#4339](https://github.com/haskell/haskell-language-server/pull/4339)) by @komikat +- Get files from Shake VFS from within plugin handlers + ([#4328](https://github.com/haskell/haskell-language-server/pull/4328)) by @awjchen +- Cabal plugin outline view + ([#4323](https://github.com/haskell/haskell-language-server/pull/4323)) by @VenInf +- Add missing documentation for cabal formatters + ([#4322](https://github.com/haskell/haskell-language-server/pull/4322)) by @fendor +- Provide explicit import in inlay hints + ([#4235](https://github.com/haskell/haskell-language-server/pull/4235)) by @jetjinser +- Add codeactions for cabal field names + ([#3273](https://github.com/haskell/haskell-language-server/pull/3273)) by @dyniec + +## 2.9.0.1 + +- Bindists for GHC 9.6.6 + +## 2.9.0.0 + +- Bindists for GHC 9.10.1 by @wz1000, @jhrcek, @michaelpj +- More hls-graph reliability improvements by @soulomoon +- Refactoring of test suite runners by @soulomoon +- Fixes in multiple home units support by @wz1000 + +### Pull Requests + +- Fix quadratic memory usage in GetLocatedImports + ([#4318](https://github.com/haskell/haskell-language-server/pull/4318)) by @mpickering +- Bump stack configs + CI to 9.6.5 and 9.8.2 + ([#4316](https://github.com/haskell/haskell-language-server/pull/4316)) by @jhrcek +- Add support for Fourmolu 0.16 + ([#4314](https://github.com/haskell/haskell-language-server/pull/4314)) by @ brandonchinn178 +- Code action to remove redundant record field import (fixes #4220) + ([#4308](https://github.com/haskell/haskell-language-server/pull/4308)) by @battermann +- Use restricted monad for plugins (#4057) + ([#4304](https://github.com/haskell/haskell-language-server/pull/4304)) by @awjchen +- 4301 we need to implement utility to wait for all runnning keys in hls graph done + ([#4302](https://github.com/haskell/haskell-language-server/pull/4302)) by @soulomoon +- Call useWithStale instead of useWithStaleFast when calling ParseCabalFields + ([#4294](https://github.com/haskell/haskell-language-server/pull/4294)) by @VeryMilkyJoe +- test: add test documenting #806 + ([#4292](https://github.com/haskell/haskell-language-server/pull/4292)) by @develop7 +- ghcide: drop ghc-check and ghc-paths dependency + ([#4291](https://github.com/haskell/haskell-language-server/pull/4291)) by @wz1000 +- Limit number of valid hole fits to 10 + ([#4288](https://github.com/haskell/haskell-language-server/pull/4288)) by @akshaymankar +- Add common stanza to completion data + ([#4286](https://github.com/haskell/haskell-language-server/pull/4286)) by @VeryMilkyJoe +- FindImports: ThisPkg means some home unit, not "this" unit + ([#4284](https://github.com/haskell/haskell-language-server/pull/4284)) by @wz1000 +- Remove redudant absolutization in session loader + ([#4280](https://github.com/haskell/haskell-language-server/pull/4280)) by @soulomoon +- Bump to new lsp versions + ([#4279](https://github.com/haskell/haskell-language-server/pull/4279)) by @michaelpj +- Put more test code into pre-commit + ([#4275](https://github.com/haskell/haskell-language-server/pull/4275)) by @soulomoon +- Delete library ghcide test utils + ([#4274](https://github.com/haskell/haskell-language-server/pull/4274)) by @soulomoon +- Delete testUtil from ghcide-tests + ([#4272](https://github.com/haskell/haskell-language-server/pull/4272)) by @soulomoon +- CI change, only run bench on performance label + ([#4271](https://github.com/haskell/haskell-language-server/pull/4271)) by @soulomoon +- Migrate WatchedFileTests + ([#4269](https://github.com/haskell/haskell-language-server/pull/4269)) by @soulomoon +- Migrate UnitTests + ([#4268](https://github.com/haskell/haskell-language-server/pull/4268)) by @soulomoon +- Migrate SafeTests + ([#4267](https://github.com/haskell/haskell-language-server/pull/4267)) by @soulomoon +- Migrate SymlinkTests + ([#4266](https://github.com/haskell/haskell-language-server/pull/4266)) by @soulomoon +- Remove unused and outdated CHANGELOG files + ([#4264](https://github.com/haskell/haskell-language-server/pull/4264)) by @fendor +- Enable cabal flaky test + ([#4263](https://github.com/haskell/haskell-language-server/pull/4263)) by @soulomoon +- Migrate RootUriTests + ([#4261](https://github.com/haskell/haskell-language-server/pull/4261)) by @soulomoon +- Migrate PreprocessorTests + ([#4260](https://github.com/haskell/haskell-language-server/pull/4260)) by @soulomoon +- Migrate PluginSimpleTests + ([#4259](https://github.com/haskell/haskell-language-server/pull/4259)) by @soulomoon +- Migrate ClientSettingsTests + ([#4258](https://github.com/haskell/haskell-language-server/pull/4258)) by @soulomoon +- Unify critical session running in hls + ([#4256](https://github.com/haskell/haskell-language-server/pull/4256)) by @soulomoon +- Bump cachix/cachix-action from 14 to 15 + ([#4255](https://github.com/haskell/haskell-language-server/pull/4255)) by @dependabot[bot] +- Bump haskell-actions/setup from 2.7.2 to 2.7.3 + ([#4254](https://github.com/haskell/haskell-language-server/pull/4254)) by @dependabot[bot] +- Bump haskell-actions/setup from 2.7.2 to 2.7.3 in /.github/actions/setup-build + ([#4253](https://github.com/haskell/haskell-language-server/pull/4253)) by @dependabot[bot] +- Shorter file names completion + ([#4252](https://github.com/haskell/haskell-language-server/pull/4252)) by @VenInf +- Fix progress start delay + ([#4249](https://github.com/haskell/haskell-language-server/pull/4249)) by @michaelpj +- Bump cachix/install-nix-action from 26 to 27 + ([#4245](https://github.com/haskell/haskell-language-server/pull/4245)) by @dependabot[bot] +- Bump haskell-actions/setup from 2.7.1 to 2.7.2 + ([#4244](https://github.com/haskell/haskell-language-server/pull/4244)) by @dependabot[bot] +- Bump haskell-actions/setup from 2.7.1 to 2.7.2 in /.github/actions/setup-build + ([#4243](https://github.com/haskell/haskell-language-server/pull/4243)) by @dependabot[bot] +- Enable test for #717 + ([#4241](https://github.com/haskell/haskell-language-server/pull/4241)) by @soulomoon +- Remove Pepe from CODEOWNERS + ([#4239](https://github.com/haskell/haskell-language-server/pull/4239)) by @michaelpj +- Fix resultBuilt(dirty mechanism) in hls-graph + ([#4238](https://github.com/haskell/haskell-language-server/pull/4238)) by @soulomoon +- Support for 9.10 + ([#4233](https://github.com/haskell/haskell-language-server/pull/4233)) by @wz1000 +- Refactor hls-test-util and reduce getCurrentDirectory after initilization + ([#4231](https://github.com/haskell/haskell-language-server/pull/4231)) by @soulomoon +- [Migrate BootTests] part of #4173 Migrate ghcide tests to hls test utils + ([#4227](https://github.com/haskell/haskell-language-server/pull/4227)) by @soulomoon +- Actually enable pedantic flag in ci flags job + ([#4224](https://github.com/haskell/haskell-language-server/pull/4224)) by @jhrcek +- Cleanup cabal files, ghc compat code, fix ghc warnings + ([#4222](https://github.com/haskell/haskell-language-server/pull/4222)) by @jhrcek +- Another attempt at using the lsp API for some progress reporting + ([#4218](https://github.com/haskell/haskell-language-server/pull/4218)) by @michaelpj +- [Migrate diagnosticTests] part of #4173 Migrate ghcide tests to hls test utils + ([#4207](https://github.com/haskell/haskell-language-server/pull/4207)) by @soulomoon +- Prepare release 2.8.0.0 + ([#4191](https://github.com/haskell/haskell-language-server/pull/4191)) by @wz1000 +- Stabilize the build system by correctly house keeping the dirtykeys and rule values [flaky test #4185 #4093] + ([#4190](https://github.com/haskell/haskell-language-server/pull/4190)) by @soulomoon +- hls-cabal-plugin: refactor context search to use `readFields` + ([#4186](https://github.com/haskell/haskell-language-server/pull/4186)) by @fendor +- 3944 extend the properties api to better support nested configuration + ([#3952](https://github.com/haskell/haskell-language-server/pull/3952)) by @soulomoon + +## 2.8.0.0 + +- Bindists for GHC 9.6.5 +- New hls-notes plugin (#4126, @jvanbruegge) +- Floskell, hlint and stylish-haskell plugins enabled for GHC 9.8 +- Improvements for hls-graph increasing robustness (#4087, @soulomoon) +- Improvements to multi-component support (#4096, #4109, #4179, @wz1000, @fendor) + +### Pull Requests + +- Bump haskell-actions/setup from 2.7.0 to 2.7.1 + ([#4189](https://github.com/haskell/haskell-language-server/pull/4189)) by @dependabot[bot] +- Bump haskell-actions/setup from 2.7.0 to 2.7.1 in /.github/actions/setup-build + ([#4188](https://github.com/haskell/haskell-language-server/pull/4188)) by @dependabot[bot] +- Fix ghcdie-tests CI + ([#4184](https://github.com/haskell/haskell-language-server/pull/4184)) by @soulomoon +- Fix ghc and hlint warnings, fix formatting + ([#4181](https://github.com/haskell/haskell-language-server/pull/4181)) by @jhrcek +- Allow users to specify whether to use `cabal`'s multi-repl feature + ([#4179](https://github.com/haskell/haskell-language-server/pull/4179)) by @fendor +- Improve parsing of import suggestions extending multiple multiline imports (fixes #4175) + ([#4177](https://github.com/haskell/haskell-language-server/pull/4177)) by @jhrcek +- move ghcide-tests to haskell-language-server.cabal and make it depend on hls-test-utils + ([#4176](https://github.com/haskell/haskell-language-server/pull/4176)) by @soulomoon +- enable ThreadId for when testing + ([#4174](https://github.com/haskell/haskell-language-server/pull/4174)) by @soulomoon +- Drop Legacy Logger from Codebase + ([#4171](https://github.com/haskell/haskell-language-server/pull/4171)) by @fendor +- get rid of the `unsafeInterleaveIO` at start up + ([#4167](https://github.com/haskell/haskell-language-server/pull/4167)) by @soulomoon +- Remove EKG + ([#4163](https://github.com/haskell/haskell-language-server/pull/4163)) by @michaelpj +- Mark plugins as not buildable if the flag is disabled + ([#4160](https://github.com/haskell/haskell-language-server/pull/4160)) by @michaelpj +- Fix references to old CPP names in tests, update tests + ([#4159](https://github.com/haskell/haskell-language-server/pull/4159)) by @jhrcek +- Bump haskell-actions/setup from 2.6.3 to 2.7.0 + ([#4158](https://github.com/haskell/haskell-language-server/pull/4158)) by @dependabot[bot] +- Bump haskell-actions/setup from 2.6.3 to 2.7.0 in /.github/actions/setup-build + ([#4157](https://github.com/haskell/haskell-language-server/pull/4157)) by @dependabot[bot] +- Remove dead code in ghcide and hls-graph for priority + ([#4151](https://github.com/haskell/haskell-language-server/pull/4151)) by @soulomoon +- Bump haskell-actions/setup from 2.6.2 to 2.6.3 in /.github/actions/setup-build + ([#4150](https://github.com/haskell/haskell-language-server/pull/4150)) by @dependabot[bot] +- Bump haskell-actions/setup from 2.6.2 to 2.6.3 + ([#4149](https://github.com/haskell/haskell-language-server/pull/4149)) by @dependabot[bot] +- Run ExceptionTests in temporary directory + ([#4146](https://github.com/haskell/haskell-language-server/pull/4146)) by @fendor +- hls-eval-plugin: Replicate #4139 + ([#4140](https://github.com/haskell/haskell-language-server/pull/4140)) by @mattapet +- Update comment in refactor tests + ([#4138](https://github.com/haskell/haskell-language-server/pull/4138)) by @jhrcek +- Update contact info in docs + ([#4137](https://github.com/haskell/haskell-language-server/pull/4137)) by @jhrcek +- hls-notes-plugin: Do not error if no note is under the cursor + ([#4136](https://github.com/haskell/haskell-language-server/pull/4136)) by @jvanbruegge +- improve logging in semantic tokens rule + ([#4135](https://github.com/haskell/haskell-language-server/pull/4135)) by @soulomoon +- Bump softprops/action-gh-release from 1 to 2 + ([#4133](https://github.com/haskell/haskell-language-server/pull/4133)) by @dependabot[bot] +- Bump cachix/install-nix-action from 25 to 26 + ([#4132](https://github.com/haskell/haskell-language-server/pull/4132)) by @dependabot[bot] +- Use Set.member instead of Foldable.elem + ([#4128](https://github.com/haskell/haskell-language-server/pull/4128)) by @jhrcek +- hls-notes-plugin: Initial implementation + ([#4126](https://github.com/haskell/haskell-language-server/pull/4126)) by @jvanbruegge +- Enable floskell and hlint plugins for ghc 9.8 + ([#4125](https://github.com/haskell/haskell-language-server/pull/4125)) by @jhrcek +- Integrate stylish-haskell into hls executable with ghc 9.8 + ([#4124](https://github.com/haskell/haskell-language-server/pull/4124)) by @jhrcek +- Reduce usage of partial functions + ([#4123](https://github.com/haskell/haskell-language-server/pull/4123)) by @jhrcek +- Benchmark: Enable 9.6, 9.8 + ([#4118](https://github.com/haskell/haskell-language-server/pull/4118)) by @soulomoon +- Bump haskell-actions/setup from 2.6.1 to 2.6.2 in /.github/actions/setup-build + ([#4116](https://github.com/haskell/haskell-language-server/pull/4116)) by @dependabot[bot] +- Bump haskell-actions/setup from 2.6.1 to 2.6.2 + ([#4115](https://github.com/haskell/haskell-language-server/pull/4115)) by @dependabot[bot] +- eval: more robust way to extract comments from ParsedModule + ([#4113](https://github.com/haskell/haskell-language-server/pull/4113)) by @jhrcek +- Improve isolation of build artefacts of test runs + ([#4112](https://github.com/haskell/haskell-language-server/pull/4112)) by @fendor +- Improve handling of nonsense rename attempts + ([#4111](https://github.com/haskell/haskell-language-server/pull/4111)) by @jhrcek +- Exit with non-zero exitcode if wrapper fails to launch + ([#4110](https://github.com/haskell/haskell-language-server/pull/4110)) by @fendor +- Replace checkHomeUnitsClosed with a faster implementation + ([#4109](https://github.com/haskell/haskell-language-server/pull/4109)) by @wz1000 +- Don't distribute gifs or plugin readmes + ([#4107](https://github.com/haskell/haskell-language-server/pull/4107)) by @fendor +- Remove locale workaround for Module name that conatins non-ascii characters + ([#4106](https://github.com/haskell/haskell-language-server/pull/4106)) by @fendor +- Track extra-source-files of plugins more accurately + ([#4105](https://github.com/haskell/haskell-language-server/pull/4105)) by @fendor +- remove non-ascii name + ([#4103](https://github.com/haskell/haskell-language-server/pull/4103)) by @soulomoon +- Add cabal-gild as a cabal file formatter plugin + ([#4101](https://github.com/haskell/haskell-language-server/pull/4101)) by @fendor +- Remove more workarounds for GHCs < 9.2 (#4092) + ([#4098](https://github.com/haskell/haskell-language-server/pull/4098)) by @jhrcek +- session-loader: Don't loop forever when we don't find a file in any multi component + ([#4096](https://github.com/haskell/haskell-language-server/pull/4096)) by @wz1000 +- Prepare release 2.7.0.0 + ([#4095](https://github.com/haskell/haskell-language-server/pull/4095)) by @fendor +- Remove more workarounds for GHCs < 9.0 + ([#4092](https://github.com/haskell/haskell-language-server/pull/4092)) by @jhrcek +- Fix hls-graph: phantom dependencies invoke in branching deps (resolve #3423) + ([#4087](https://github.com/haskell/haskell-language-server/pull/4087)) by @soulomoon +- Rename only if the current module compiles (#3799) + ([#3848](https://github.com/haskell/haskell-language-server/pull/3848)) by @sgillespie +- Reintroduce ghc-lib flag for hlint plugin + ([#3757](https://github.com/haskell/haskell-language-server/pull/3757)) by @RaoulHC + +## 2.7.0.0 + +- Bindists for GHC 9.8.2 + - Enable many more plugins, making GHC 9.8.2 fully supported +- Fix refactor code actions for vim +- Preserve HLint's diagnostic severity +- Many other bug fixes. + +### Pull Requests + +- Enable pedantic for remaining plugins + ([#4091](https://github.com/haskell/haskell-language-server/pull/4091)) by @jhrcek +- Add support for fourmolu 0.15 + ([#4086](https://github.com/haskell/haskell-language-server/pull/4086)) by @brandonchinn178 +- refactor plugin: fix regex for extracting import suggestions + ([#4080](https://github.com/haskell/haskell-language-server/pull/4080)) by @jhrcek +- Bump to hiedb 0.6.0.0 + ([#4077](https://github.com/haskell/haskell-language-server/pull/4077)) by @jhrcek +- ghcide: Only try `stat`ing a core file after we ensure it actually exists + ([#4076](https://github.com/haskell/haskell-language-server/pull/4076)) by @wz1000 +- Fix small typo in Retrie error message + ([#4075](https://github.com/haskell/haskell-language-server/pull/4075)) by @iustin +- add Method_TextDocumentSemanticTokensFullDelta + ([#4073](https://github.com/haskell/haskell-language-server/pull/4073)) by @soulomoon +- Fix -Wall in retrie plugin + ([#4071](https://github.com/haskell/haskell-language-server/pull/4071)) by @jhrcek +- Fix -Wall in qualified imported names plugin + ([#4070](https://github.com/haskell/haskell-language-server/pull/4070)) by @jhrcek +- benchmarks: switch from deprecated haskell/actions/setup to haskell-actions/setup + ([#4068](https://github.com/haskell/haskell-language-server/pull/4068)) by @jhrcek +- Bump pre-commit/action from 3.0.0 to 3.0.1 + ([#4066](https://github.com/haskell/haskell-language-server/pull/4066)) by @dependabot[bot] +- Fix -Wall in refactor plugin + ([#4065](https://github.com/haskell/haskell-language-server/pull/4065)) by @jhrcek +- Redundant imports/exports: use range only to determine which code actions are in scope + ([#4063](https://github.com/haskell/haskell-language-server/pull/4063)) by @keithfancher +- Bump haskell-actions/setup to get GHC 9.6.4 in CI + ([#4062](https://github.com/haskell/haskell-language-server/pull/4062)) by @jhrcek +- Enable pedantic for more components + ([#4061](https://github.com/haskell/haskell-language-server/pull/4061)) by @jhrcek +- stack CI: switch to offic. haskell images, bump to lts-22.9 (ghc 9.6.4) + ([#4060](https://github.com/haskell/haskell-language-server/pull/4060)) by @jhrcek +- Improve hls class plugin test + ([#4059](https://github.com/haskell/haskell-language-server/pull/4059)) by @soulomoon +- Bump ghcide-test-utils to 2.0.0.0 + ([#4058](https://github.com/haskell/haskell-language-server/pull/4058)) by @wz1000 +- Promote more warnings to errors in ghcide + ([#4054](https://github.com/haskell/haskell-language-server/pull/4054)) by @jhrcek +- Add -Wunused-packages to common warnings + ([#4053](https://github.com/haskell/haskell-language-server/pull/4053)) by @jhrcek +- Bump lsp versions + ([#4052](https://github.com/haskell/haskell-language-server/pull/4052)) by @michaelpj +- Optimize semantic token extraction logic + ([#4050](https://github.com/haskell/haskell-language-server/pull/4050)) by @soulomoon +- Fix warnings in hls-graph, enable pedantic in CI + ([#4047](https://github.com/haskell/haskell-language-server/pull/4047)) by @jhrcek +- Fix -Wredundant-constraints + ([#4044](https://github.com/haskell/haskell-language-server/pull/4044)) by @jhrcek +- Disable caching job with ghc 9.2 on windows + ([#4043](https://github.com/haskell/haskell-language-server/pull/4043)) by @jhrcek +- fix token omitting problem if multiple tokens are connected. + ([#4041](https://github.com/haskell/haskell-language-server/pull/4041)) by @soulomoon +- Set test options via cabal.project + ([#4039](https://github.com/haskell/haskell-language-server/pull/4039)) by @michaelpj +- Fix document version test in hls-class-plugin + ([#4038](https://github.com/haskell/haskell-language-server/pull/4038)) by @July541 +- Fix -Wunused-imports + ([#4037](https://github.com/haskell/haskell-language-server/pull/4037)) by @jhrcek +- Use GHC2021 + ([#4033](https://github.com/haskell/haskell-language-server/pull/4033)) by @michaelpj +- Remove ghcide-test-utils as a separate package + ([#4032](https://github.com/haskell/haskell-language-server/pull/4032)) by @michaelpj +- Fix weird behavior of OPTIONS_GHC completions (fixes #3908) + ([#4031](https://github.com/haskell/haskell-language-server/pull/4031)) by @jhrcek +- semantic tokens: add infix operator + ([#4030](https://github.com/haskell/haskell-language-server/pull/4030)) by @soulomoon +- fix: a typo in docs/configuration.md + ([#4029](https://github.com/haskell/haskell-language-server/pull/4029)) by @kkweon +- Turn off tasty-rerun + ([#4028](https://github.com/haskell/haskell-language-server/pull/4028)) by @michaelpj +- Reduce the number of ad-hoc helper test functions in refactor plugin tests + ([#4027](https://github.com/haskell/haskell-language-server/pull/4027)) by @jhrcek +- Fix documentation/image links + ([#4025](https://github.com/haskell/haskell-language-server/pull/4025)) by @jhrcek +- Fix various issues + ([#4024](https://github.com/haskell/haskell-language-server/pull/4024)) by @michaelpj +- Use relative file paths for HIE files and Stan's config maps + ([#4023](https://github.com/haskell/haskell-language-server/pull/4023)) by @keithfancher +- fix isClassNodeIdentifier in hls-class-plugin + ([#4020](https://github.com/haskell/haskell-language-server/pull/4020)) by @soulomoon +- Fix -Wall and -Wunused-packages in hlint plugin + ([#4019](https://github.com/haskell/haskell-language-server/pull/4019)) by @jhrcek +- update hlint to 3.8 and prevent linting on testdata dir + ([#4018](https://github.com/haskell/haskell-language-server/pull/4018)) by @soulomoon +- refactor plugin: add reproducer and fix for #3795 + ([#4016](https://github.com/haskell/haskell-language-server/pull/4016)) by @jhrcek +- Fix -Wall and -Wunused-packages in stylish-haskell plugin + ([#4015](https://github.com/haskell/haskell-language-server/pull/4015)) by @jhrcek +- Fix -Wall and -Wunused-packages in stan plugin + ([#4014](https://github.com/haskell/haskell-language-server/pull/4014)) by @jhrcek +- fix doc for semantic token + ([#4011](https://github.com/haskell/haskell-language-server/pull/4011)) by @soulomoon +- Fix -Wall and -Wunused-packages in module name and overloaded record dot plugins + ([#4009](https://github.com/haskell/haskell-language-server/pull/4009)) by @jhrcek +- Fix -Wall and -Wunused-package in gadt plugin + ([#4008](https://github.com/haskell/haskell-language-server/pull/4008)) by @jhrcek +- Fix -Wall and -Wunused-packages in fourmolu and ormolu plugins + ([#4007](https://github.com/haskell/haskell-language-server/pull/4007)) by @jhrcek +- Fix -Wall and -Wunused-packages in plugins api and floskell + ([#4005](https://github.com/haskell/haskell-language-server/pull/4005)) by @jhrcek +- Fix -Wunused-packages in test utils + ([#4004](https://github.com/haskell/haskell-language-server/pull/4004)) by @jhrcek +- Update base lower bounds for HLS + ([#4000](https://github.com/haskell/haskell-language-server/pull/4000)) by @fendor +- Various 9.8 compat + ([#3998](https://github.com/haskell/haskell-language-server/pull/3998)) by @michaelpj +- Fix -Wall and -Wunused-packages in explicit-record-fields plugin + ([#3996](https://github.com/haskell/haskell-language-server/pull/3996)) by @jhrcek +- Fix -Wall and -Wunused-packages in explicit fixity plugin + ([#3995](https://github.com/haskell/haskell-language-server/pull/3995)) by @jhrcek +- Remove an allow-newer + ([#3989](https://github.com/haskell/haskell-language-server/pull/3989)) by @michaelpj +- chore: Fix typo s/occured/occurred + ([#3988](https://github.com/haskell/haskell-language-server/pull/3988)) by @hugo-syn +- Update support tables + ([#3987](https://github.com/haskell/haskell-language-server/pull/3987)) by @michaelpj +- Fix most -Wall in ghcide + ([#3984](https://github.com/haskell/haskell-language-server/pull/3984)) by @jhrcek +- Fix -Wall and -Wunused-packages in pragmas plugin + ([#3982](https://github.com/haskell/haskell-language-server/pull/3982)) by @jhrcek +- Fix -Wall and -Wunused-packages in eval plugin + ([#3981](https://github.com/haskell/haskell-language-server/pull/3981)) by @jhrcek +- Fix -Wall and -Wunused-packages in code-range plugin + ([#3980](https://github.com/haskell/haskell-language-server/pull/3980)) by @jhrcek +- Fix -Wall, -Wunused-packages and hlint warnings in call-hierarchy plugin + ([#3979](https://github.com/haskell/haskell-language-server/pull/3979)) by @jhrcek +- Fix -Wunused-packages in hls-cabal-plugin + ([#3977](https://github.com/haskell/haskell-language-server/pull/3977)) by @jhrcek +- Merge plugins into the HLS package + ([#3976](https://github.com/haskell/haskell-language-server/pull/3976)) by @michaelpj +- Fix most hlint warnings in ghcide + ([#3975](https://github.com/haskell/haskell-language-server/pull/3975)) by @jhrcek +- Remove allow-newer for ghc-trace-events + ([#3974](https://github.com/haskell/haskell-language-server/pull/3974)) by @jhrcek +- Exactprint plugins for 9.8 + ([#3973](https://github.com/haskell/haskell-language-server/pull/3973)) by @wz1000 +- Fix -Wall and -Wunused-packages in hls-class-plugin + ([#3972](https://github.com/haskell/haskell-language-server/pull/3972)) by @jhrcek +- Document cabal diagnostic options + ([#3971](https://github.com/haskell/haskell-language-server/pull/3971)) by @fendor +- Fix -Wall and -Wunused-packages in change-type-signature plugin + ([#3970](https://github.com/haskell/haskell-language-server/pull/3970)) by @jhrcek +- Semantic tokens: expand type synonym to checkout forall function type when possible + ([#3967](https://github.com/haskell/haskell-language-server/pull/3967)) by @soulomoon +- Fix -Wunused-packages in hls-cabal-fmt-plugin + ([#3965](https://github.com/haskell/haskell-language-server/pull/3965)) by @jhrcek +- Fix -Wall and -Wunused-packages in hls-alternate-number-format-plugin + ([#3964](https://github.com/haskell/haskell-language-server/pull/3964)) by @jhrcek +- Prepare release 2.6.0.0 + ([#3959](https://github.com/haskell/haskell-language-server/pull/3959)) by @wz1000 +- Semantic tokens: add module name support and improve performance and accuracy by traversing the hieAst along with source code + ([#3958](https://github.com/haskell/haskell-language-server/pull/3958)) by @soulomoon +- Bump cachix/cachix-action from 13 to 14 + ([#3956](https://github.com/haskell/haskell-language-server/pull/3956)) by @dependabot[bot] +- Bump cachix/install-nix-action from 24 to 25 + ([#3955](https://github.com/haskell/haskell-language-server/pull/3955)) by @dependabot[bot] +- Remove unused dependencies in hls-refactor-plugin + ([#3953](https://github.com/haskell/haskell-language-server/pull/3953)) by @jhrcek +- Cleanup conditional build logic pertaining to pre 9.2 GHCs + ([#3948](https://github.com/haskell/haskell-language-server/pull/3948)) by @jhrcek +- Fix issue: HLS HLint plugin doesn't preserve HLint's severities #3881 + ([#3902](https://github.com/haskell/haskell-language-server/pull/3902)) by @IAmPara0x +- Don't run hlint on testdata directories + ([#3901](https://github.com/haskell/haskell-language-server/pull/3901)) by @fendor +- Add option for setting manual path to Fourmolu binary + ([#3860](https://github.com/haskell/haskell-language-server/pull/3860)) by @georgefst + +## 2.6.0.0 + +- Bindists for GHC 9.6.4 +- A new semantic tokens plugin (#3892, @soulomoon). +- Improvements to multiple home unit support with GHC 9.4. When HLS is used with cabal 3.11+ it will + load proper multiple home unit sessions by default, fixing a lot of issues with + loading and reloading projects that have more than one component (#3462, @wz1000). +- Removed implicit-hie, resulting in better behaviour for projects without cradles. +- Don't produce diagnostics for disabled plugins (#3941, @fendor). +- Many other bug fixes. + +### Pull Requests + +- fix: semantic token omitting record field in `{-# LANGUAGE DuplicateRecordFields #-}` #3950 + ([#3951](https://github.com/haskell/haskell-language-server/pull/3951)) by @soulomoon +- Properties API: Remove unsafe coerce in favor of type class based method in + ([#3947](https://github.com/haskell/haskell-language-server/pull/3947)) by @soulomoon +- Bump to hiedb 0.5.0.0 to fix #3542 + ([#3943](https://github.com/haskell/haskell-language-server/pull/3943)) by @wz1000 +- Don't produce diagnostics if plugin is turned off + ([#3941](https://github.com/haskell/haskell-language-server/pull/3941)) by @fendor +- add config for semantic-tokens-plugin for mapping from hs token type to LSP default token type + ([#3940](https://github.com/haskell/haskell-language-server/pull/3940)) by @soulomoon +- add doc and ci test for semantic tokens + ([#3938](https://github.com/haskell/haskell-language-server/pull/3938)) by @soulomoon +- update Floskell to 0.11.* + ([#3933](https://github.com/haskell/haskell-language-server/pull/3933)) by @peterbecich +- Remove some people from CODEOWNERS + ([#3930](https://github.com/haskell/haskell-language-server/pull/3930)) by @michaelpj +- Adapt to minor API change for 9.6.4 compatibility + ([#3929](https://github.com/haskell/haskell-language-server/pull/3929)) by @wz1000 +- Fix multi unit session when some packages have reexported modules. + ([#3928](https://github.com/haskell/haskell-language-server/pull/3928)) by @wz1000 +- Switch to haskell-actions/setup since haskell/actions is deprecated + ([#3926](https://github.com/haskell/haskell-language-server/pull/3926)) by @fendor +- Make vscode-extension-schema honour default values + ([#3925](https://github.com/haskell/haskell-language-server/pull/3925)) by @fendor +- Add golden tests for public configs + ([#3922](https://github.com/haskell/haskell-language-server/pull/3922)) by @fendor +- Bump geekyeggo/delete-artifact from 2 to 4 + ([#3921](https://github.com/haskell/haskell-language-server/pull/3921)) by @dependabot[bot] +- Fix positionMapping in stale data + ([#3920](https://github.com/haskell/haskell-language-server/pull/3920)) by @soulomoon +- Disable stan plugin by default + ([#3917](https://github.com/haskell/haskell-language-server/pull/3917)) by @fendor +- Use stan config files for stan plugin (#3904) + ([#3914](https://github.com/haskell/haskell-language-server/pull/3914)) by @0rphee +- Bump both upload and download artifact + ([#3913](https://github.com/haskell/haskell-language-server/pull/3913)) by @michaelpj +- Update ghc-version-support.md for 2.5.0 + ([#3909](https://github.com/haskell/haskell-language-server/pull/3909)) by @lehmacdj +- Give plugins descriptions, include versions of key dependencies + ([#3903](https://github.com/haskell/haskell-language-server/pull/3903)) by @michaelpj +- Remove some buildability blockers that aren't needed + ([#3899](https://github.com/haskell/haskell-language-server/pull/3899)) by @michaelpj +- Bump actions/setup-python from 4 to 5 + ([#3895](https://github.com/haskell/haskell-language-server/pull/3895)) by @dependabot[bot] +- Update index-state to get latest stan version + ([#3894](https://github.com/haskell/haskell-language-server/pull/3894)) by @0rphee +- Generate FileTarget for all possible targetLocations + ([#3893](https://github.com/haskell/haskell-language-server/pull/3893)) by @fendor +- Implement semantic tokens plugin to support semantic highlighting(textDocument/semanticTokens/full) + ([#3892](https://github.com/haskell/haskell-language-server/pull/3892)) by @soulomoon +- session-loader: Set working directory on GHC 9.4+ + ([#3891](https://github.com/haskell/haskell-language-server/pull/3891)) by @wz1000 +- Demote home unit closure errors to warnings. + ([#3890](https://github.com/haskell/haskell-language-server/pull/3890)) by @wz1000 +- Bump cachix/install-nix-action from 23 to 24 + ([#3889](https://github.com/haskell/haskell-language-server/pull/3889)) by @dependabot[bot] +- Bump cachix/cachix-action from 12 to 13 + ([#3888](https://github.com/haskell/haskell-language-server/pull/3888)) by @dependabot[bot] +- Add more docs for implicit discovery + ([#3887](https://github.com/haskell/haskell-language-server/pull/3887)) by @fendor +- Prepare release 2.5.0.0 + ([#3879](https://github.com/haskell/haskell-language-server/pull/3879)) by @wz1000 +- Improve no plugin messages + ([#3864](https://github.com/haskell/haskell-language-server/pull/3864)) by @joyfulmantis +- Add support for multi unit argument syntax + ([#3462](https://github.com/haskell/haskell-language-server/pull/3462)) by @wz1000 +- Fix completion for qualified import + ([#2838](https://github.com/haskell/haskell-language-server/pull/2838)) by @xsebek + +## 2.5.0.0 + +- Bindists for GHC 9.4.8 +- Drop support for GHC 9.0 +- Re-add stan plugin +- Load default operator fixities in Fourmolu plugin + +### Pull Requests + +- Drop support for GHC 9.0 + ([#3875](https://github.com/haskell/haskell-language-server/pull/3875)) by @michaelpj +- Fix support tables + ([#3874](https://github.com/haskell/haskell-language-server/pull/3874)) by @michaelpj +- Prefer hls-test-utils functions over code duplication + ([#3870](https://github.com/haskell/haskell-language-server/pull/3870)) by @fendor +- Make sure running tests locally pick up the correct cradle type + ([#3869](https://github.com/haskell/haskell-language-server/pull/3869)) by @fendor +- Some versions of stylish-haskell do need the ghc-lib flag + ([#3868](https://github.com/haskell/haskell-language-server/pull/3868)) by @michaelpj +- Remove head.hackage + ([#3867](https://github.com/haskell/haskell-language-server/pull/3867)) by @wz1000 +- Load default operator fixities in Fourmolu plugin non-CLI mode + ([#3855](https://github.com/haskell/haskell-language-server/pull/3855)) by @georgefst +- Fix #3847 + ([#3854](https://github.com/haskell/haskell-language-server/pull/3854)) by @BurningLutz +- Re-add hls-stan-plugin + ([#3851](https://github.com/haskell/haskell-language-server/pull/3851)) by @0rphee +- Bump fkirc/skip-duplicate-actions from 5.3.0 to 5.3.1 + ([#3850](https://github.com/haskell/haskell-language-server/pull/3850)) by @dependabot[bot] +- Merge definitions from all plugins for Document(Type)Definition message + ([#3846](https://github.com/haskell/haskell-language-server/pull/3846)) by @JiriLojda +- Simplify cabal.project + ([#3836](https://github.com/haskell/haskell-language-server/pull/3836)) by @michaelpj +- Set the root for tests to the test directory + ([#3830](https://github.com/haskell/haskell-language-server/pull/3830)) by @fendor +- Reduce Nix support + ([#3804](https://github.com/haskell/haskell-language-server/pull/3804)) by @michaelpj + ## 2.4.0.0 * Initial support for GHC 9.8.1, without plugins dependent on `ghc-exactprint` diff --git a/RELEASING.md b/RELEASING.md index bf740d716d..a48b32cb93 100644 --- a/RELEASING.md +++ b/RELEASING.md @@ -3,10 +3,9 @@ ## Release checklist - [ ] check ghcup supports new GHC releases if any -- [ ] set the supported GHCs in workflow file `.github/workflows/release.yaml` - - There is currently a list of GHC versions for each major platform. Search for `ghc: [` to find all lists. - - Look for `TODO:` to find locations that require extra care for GHC versions. - [ ] check all plugins still work if release includes code changes +- [ ] set the supported GHCs in workflow file `.github/generate-ci/gen_ci.hs` +- [ ] regenerate the CI via `./.github/generate-ci/generate-jobs` - [ ] bump package versions in all `*.cabal` files (same version as hls) - HLS uses lockstep versioning. The core packages and all plugins use the same version number, and only support exactly this version. - Exceptions: @@ -14,18 +13,14 @@ - `shake-bench` is an internal testing tool, not exposed to the outside world. Thus, no version bump required for releases. - For updating cabal files, the following script can be used: - ```sh - # Update all `version:` fields - sed -ri "s/^version:( +)2.2.0.0/version:\12.3.0.0/" **/*.cabal - # Update all constraints expected to be in the form `== `. - # We usually don't force an exact version, so this is relatively unambiguous. - # We could introduce some more ad-hoc parsing, if there is still ambiguity. - sed -ri "s/== 2.2.0.0/== 2.3.0.0/" **/*.cabal + ./release/update_versions.sh ``` - It still requires manual verification and review - [ ] generate and update changelog - Generate a ChangeLog via `./GenChangelogs.hs ` - `` is the git tag you want to generate the ChangeLog from. - `` is a github access key: https://github.com/settings/tokens +- [ ] update https://haskell-language-server.readthedocs.io/en/latest/support/ghc-version-support.html#current-ghc-version-support-status - [ ] create release branch as `wip/` - `git switch -c wip/` - [ ] create release tag as `` @@ -45,9 +40,9 @@ - Afterwards, the artifacts are available at: `https://downloads.haskell.org/~hls/haskell-language-server-/` - Run `SIGNING_KEY=... ../../release/upload.sh purge_all` to remove CDN caches - [ ] create PR to [ghcup-metadata](https://github.com/haskell/ghcup-metadata) - - [ ] update `ghcup-0.0.7.yaml` and `ghcup-vanilla-0.0.7.yaml` + - [ ] update `ghcup-vanilla-0.0.8.yaml` and `ghcup-vanilla-0.0.7.yaml` - can use `sh scripts/release/create-yaml-snippet.sh ` to generate a snippet that can be manually inserted into the yaml files - - [ ] update `hls-metadata-0.0.1.json` + - ~~update `hls-metadata-0.0.1.json`~~ Currently unnecessary, GHCup builds its own HLS binaries and updates that file. - utilize `cabal run ghcup-gen -- generate-hls-ghcs -f ghcup-0.0.7.yaml --format json --stdout` in the root of ghcup-metadata repository - Be sure to mark the correct latest version and add the 'recommended' tag to the latest release. - [ ] get sign-off on release @@ -55,7 +50,6 @@ - [ ] publish release on github - [ ] upload hackage packages - requires credentials -- [ ] update https://haskell-language-server.readthedocs.io/en/latest/support/ghc-version-support.html#current-ghc-version-support-status - [ ] Supported tools table needs to be updated: - https://www.haskell.org/ghcup/install/#supported-platforms - https://github.com/haskell/ghcup-hs/blob/master/docs/install.md#supported-platforms diff --git a/bench/Main.hs b/bench/Main.hs index b034b10983..eec4380eb4 100644 --- a/bench/Main.hs +++ b/bench/Main.hs @@ -163,6 +163,7 @@ createBuildSystem config = do buildRules build hlsBuildRules benchRules build (MkBenchRules (askOracle $ GetSamples ()) benchHls warmupHls "haskell-language-server" (parallelism configStatic)) + addGetParentOracle csvRules build svgRules build heapProfileRules build @@ -202,8 +203,6 @@ buildHls Cabal root out = actionBracket liftIO $ writeFile projectLocal $ unlines ["package haskell-language-server" ," ghc-options: -eventlog -rtsopts" - ,"package ghcide" - ," flags: +ekg" ] return projectLocalExists) (\projectLocalExists -> do diff --git a/bench/README.md b/bench/README.md index 557fcc1420..1dc1e6a3d4 100644 --- a/bench/README.md +++ b/bench/README.md @@ -54,6 +54,9 @@ Targets: - bench-results/*/*/*/results.csv - bench-results/*/*/results.csv - bench-results/*/results.csv + - bench-results/*/*/*/resultDiff.csv + - bench-results/*/*/resultDiff.csv + - bench-results/*/resultDiff.csv - bench-results/*/*/*/*.svg - bench-results/*/*/*/*.diff.svg - bench-results/*/*/*.svg diff --git a/bench/config.yaml b/bench/config.yaml index f8a062dc3d..18211f4f24 100644 --- a/bench/config.yaml +++ b/bench/config.yaml @@ -21,18 +21,18 @@ examples: # Medium-sized project without TH - name: cabal package: Cabal - version: 3.6.3.0 + version: 3.10.2.1 modules: - src/Distribution/Simple.hs - - src/Distribution/Types/Module.hs + - src/Distribution/Types/ComponentLocalBuildInfo.hs extra-args: [] # extra HLS command line args # Small-sized project with TH - name: lsp-types package: lsp-types - version: 1.5.0.0 + version: 2.1.1.0 modules: - - src/Language/LSP/Types/WatchedFiles.hs - - src/Language/LSP/Types/CallHierarchy.hs + - src/Language/LSP/Protocol/Types/SemanticTokens.hs + - generated/Language/LSP/Protocol/Internal/Types/NotebookDocumentChangeEvent.hs - name: MultiLayerModules path: bench/MultiLayerModules.sh @@ -94,6 +94,7 @@ experiments: - "edit-header" - "edit" - "hover" + - "semanticTokens" - "hover after edit" # - "hover after cradle edit" - "getDefinition" @@ -105,6 +106,8 @@ experiments: - "code actions after cradle edit" - "documentSymbols after edit" - "hole fit suggestions" + - "eval execute single-line code lens" + - "eval execute multi-line code lens" # An ordered list of versions to analyze versions: @@ -129,7 +132,7 @@ versions: # WARNING: Currently bench versions later than e4234a3a5e347db249fccefb8e3fb36f89e8eafb # will be unable to send plugin configurations to earlier HLS versions. This causes # all plugins in those versions to always be enabled. -# In addition bench proactively disables all plugins it knows about besides the +# In addition bench proactively disables all plugins it knows about besides the # ones in the following list. However because it can only disable plugins it # knows about, any plugins that are in old versions but were removed from HLS # before the current bench will not be disabled. @@ -194,6 +197,7 @@ configurations: - qualifyImportedNames - rename - stylish-haskell + - semanticTokens # - alternateNumberFormat # - callHierarchy # - changeTypeSignature diff --git a/cabal.project b/cabal.project index 3299b5cd07..92954ec729 100644 --- a/cabal.project +++ b/cabal.project @@ -4,38 +4,11 @@ packages: ./shake-bench ./hls-graph ./ghcide - ./ghcide-bench - ./ghcide/test ./hls-plugin-api ./hls-test-utils - ./plugins/hls-cabal-plugin - ./plugins/hls-cabal-fmt-plugin - ./plugins/hls-stylish-haskell-plugin - ./plugins/hls-fourmolu-plugin - ./plugins/hls-class-plugin - ./plugins/hls-eval-plugin - ./plugins/hls-explicit-imports-plugin - ./plugins/hls-hlint-plugin - ./plugins/hls-rename-plugin - ./plugins/hls-retrie-plugin - ./plugins/hls-splice-plugin - ./plugins/hls-floskell-plugin - ./plugins/hls-pragmas-plugin - ./plugins/hls-module-name-plugin - ./plugins/hls-ormolu-plugin - ./plugins/hls-call-hierarchy-plugin - ./plugins/hls-alternate-number-format-plugin - ./plugins/hls-qualify-imported-names-plugin - ./plugins/hls-code-range-plugin - ./plugins/hls-change-type-signature-plugin - ./plugins/hls-stan-plugin - ./plugins/hls-gadt-plugin - ./plugins/hls-explicit-fixity-plugin - ./plugins/hls-explicit-record-fields-plugin - ./plugins/hls-refactor-plugin - ./plugins/hls-overloaded-record-dot-plugin -index-state: 2023-11-13T12:07:58Z + +index-state: 2025-06-16T09:44:13Z tests: True test-show-details: direct @@ -44,6 +17,11 @@ benchmarks: True write-ghc-environment-files: never +-- Many of our tests only work single-threaded, and the only way to +-- ensure tasty runs everything purely single-threaded is to pass +-- this at the top-level +test-options: -j1 + -- Make sure dependencies are build with haddock so we get -- haddock shown on hover package * @@ -55,53 +33,27 @@ constraints: text -simdutf, ghc-check -ghc-check-use-package-abis, ghc-lib-parser-ex -auto, - -- This is only present in some versions, and it's on by default since - -- 0.14.5.0, but there are some versions we allow that need this + -- This is only present in some versions, and it's on by default since + -- 0.14.5.0, but there are some versions we allow that need this -- setting stylish-haskell +ghc-lib, -- Centos 7 comes with an old gcc version that doesn't know about -- the flag '-fopen-simd', which blocked the release 2.2.0.0. -- We want to be able to benefit from the performance optimisations -- in the future, thus: TODO: remove this flag. - bitvec -simd + bitvec -simd, --- This is benign and won't affect our ability to release to Hackage, --- because we only depend on `ekg-json` when a non-default flag --- is turned on. --- DELETE MARKER FOR CI --- centos7 has an old version of git which cabal doesn't --- support. We delete these lines in gitlab ci to workaround --- this issue, as this is not necessary to build our binaries. -source-repository-package - type:git - location: https://github.com/pepeiborra/ekg-json - tag: 7a0af7a8fd38045fd15fb13445bdcc7085325460 --- END DELETE -if impl(ghc >= 9.1) - -- ekg packagess are old and unmaintained, but we - -- don't rely on them for the mainline build, so - -- this is okay - allow-newer: - ekg-json:base, - ekg-wai:time, - ekg-core:ghc-prim +-- Some of the formatters need the latest Cabal-syntax version, +-- but 'cabal-install-parsers-0.6.2' only has Cabal-syntax (>=3.12.0.0 && <3.13). +-- So, we relax the upper bounds here. +-- fourmolu-0.18.0 and ormolu-0.8 depend on Cabal-syntax == 3.14.*, while +-- cabal-add depends on cabal-install-parsers. +allow-newer: + cabal-install-parsers:Cabal-syntax, -if impl(ghc >= 9.7) +if impl(ghc >= 9.11) + benchmarks: False allow-newer: - ekg-core:text, - -- https://github.com/maoe/ghc-trace-events/issues/12 - ghc-trace-events:base, - ghc-trace-events:bytestring, - ghc-trace-events:text, - -- https://github.com/haskell-primitive/primitive-unlifted/issues/39 - primitive-unlifted:bytestring, - -- https://github.com/obsidiansystems/constraints-extras/issues/54 - constraints-extras:base, - constraints-extras:template-haskell, - -- https://github.com/obsidiansystems/commutative-semigroups/issues/13 - commutative-semigroups:base, - commutative-semigroups:template-haskell, - -- https://github.com/kcsongor/generic-lens/issues/158 - generic-lens:text, - generic-lens-core:text, + cabal-install-parsers:base, + cabal-install-parsers:time, diff --git a/docs/Makefile b/docs/Makefile index d4bb2cbb9e..bb113155fa 100644 --- a/docs/Makefile +++ b/docs/Makefile @@ -1,5 +1,4 @@ # Minimal makefile for Sphinx documentation -# # You can set these variables from the command line, and also # from the environment for the first two. @@ -8,13 +7,7 @@ SPHINXBUILD ?= sphinx-build SOURCEDIR = . BUILDDIR = _build -# Put it first so that "make" without argument is like "make help". -help: - @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) +.PHONY: Makefile -.PHONY: help Makefile - -# Catch-all target: route all unknown targets to Sphinx using the new -# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). -%: Makefile - @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) +html: Makefile + @$(SPHINXBUILD) -n -W "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) diff --git a/docs/configuration.md b/docs/configuration.md index 6da737d6b4..9da816c09e 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -41,9 +41,11 @@ This option obviously would not make sense for language servers for other langua Here is a list of the additional settings currently supported by `haskell-language-server`, along with their setting key (you may not need to know this) and default: - Formatting provider (`haskell.formattingProvider`, default `ormolu`): what formatter to use; one of `floskell`, `ormolu`, `fourmolu`, or `stylish-haskell`. +- Cabal formatting provider (`haskell.cabalFormattingProvider`, default `cabal-gild`): what formatter to use for cabal files; one of `cabal-gild` or `cabal-fmt`. - Max completions (`haskell.maxCompletions`, default 40): maximum number of completions sent to the LSP client. - Check project (`haskell.checkProject`, default true): whether to typecheck the entire project on initial load. As it is activated by default could drive to bad performance in large projects. - Check parents (`haskell.checkParents`, default `CheckOnSave`): when to typecheck reverse dependencies of a file; one of `NeverCheck`, `CheckOnSave` (means dependent/parent modules will only be checked when you save), or `AlwaysCheck` (means re-typechecking them on every change). +- Session loading preference (`haskell.sessionLoading`, default `singleComponent`): how to load sessions; one of `singleComponent` (means always loading only a single component when a new component is discovered) or `multipleComponents` (means always preferring loading multiple components in the cradle at once). `multipleComponents` might not be always possible, if the tool doesn't support multiple components loading. The cradle can decide how to handle these situations, and whether to honour the preference at all. #### Generic plugin configuration @@ -61,7 +63,7 @@ Plugins have a generic config to control their behaviour. The schema of such con - `haskell.plugin.eval.config.diff`, default true: When reloading haddock test results in changes, mark it with WAS/NOW. - `haskell.plugin.eval.config.exception`, default false: When the command results in an exception, mark it with `*** Exception:`. - `rename`: - - `haskell.plugin.rename.config.diff`, default false: Enables renaming across modules (experimental) + - `haskell.plugin.rename.config.crossModule`, default false: Enables renaming across modules (experimental) - `ghcide-completions`: - `haskell.plugin.ghcide-completions.config.snippetsOn`, default true: Inserts snippets when using code completions. - `haskell.plugin.ghcide-completions.config.autoExtendOn`, default true: Extends the import list automatically when completing a out-of-scope identifier. @@ -113,7 +115,7 @@ E.g., it still does not work, or you want to fine-tune the configuration. In that case, refer to the [hie-bios explicit configuration documentation](https://github.com/haskell/hie-bios#explicit-configuration). Keep in mind that you can start from the `hie.yaml` file generated by `implicit-hie` (see previous section) and modify it to your liking. -#### Examples of explicit `hie-yaml` configurations +#### Examples of explicit `hie.yaml` configurations ##### Basic Stack ```yaml diff --git a/docs/contributing/contributing.md b/docs/contributing/contributing.md index 096ae8b826..134a03b89c 100644 --- a/docs/contributing/contributing.md +++ b/docs/contributing/contributing.md @@ -2,12 +2,10 @@ The Haskell tooling dream is near, we need your help! -## How to contact the haskell ide team +## How to contact the Haskell Language Server (HLS) team -- Join [our IRC channel](https://web.libera.chat/?channels=#haskell-language-server) at `#haskell-language-server` on [`libera`](https://libera.chat/). -- Follow the [Haskell IDE team twitter account](https://twitter.com/IdeHaskell) for updates and help. -- Join the [#haskell-tooling channel](https://discord.com/channels/280033776820813825/505370075402862594/808027763868827659) in the Functional Programming discord server. You can join the server via [this invitation](https://discord.gg/9spEdTNGrD). -- Join the [haskell-tooling channel](https://matrix.to/#/#haskell-tooling:matrix.org) in [matrix](https://matrix.org/). +- Join the [haskell-language-server channel](https://matrix.to/#/#haskell-language-server:matrix.org) on [matrix](https://matrix.org/) (primary communication channel). +- Join [our IRC channel](https://web.libera.chat/?channels=#haskell-language-server) at `#haskell-language-server` on [`libera`](https://libera.chat/) (secondary communication channel - all messages in this IRC channel are automatically bridged to the Matrix channel). - Visit [the project GitHub repo](https://github.com/haskell/haskell-language-server) to view the source code, or open issues or pull requests. ## Building @@ -19,7 +17,7 @@ $ git clone https://github.com/haskell/haskell-language-server The project can then be built with both `cabal build` and `stack build`. -### Using Cabal +### Building with Cabal ```shell # If you have not run `cabal update` in a while @@ -28,15 +26,15 @@ $ cabal update $ cabal build ``` -### Using Stack +### Building with Stack ```shell $ stack build ``` -### Using Nix +### Building with Nix -The instructions below show how to set up a Cachix binary cache and open a nix shell for local development. +The instructions below show how to set up a Cachix binary cache and open a Nix shell for local development. ```shell $ cachix use haskell-language-server @@ -47,19 +45,19 @@ $ cabal build #### Flakes support -If you are using nix 2.4 style command (enabled by `experimental-features = nix-command`), +If you are using Nix 2.4 style commands (enabled by `experimental-features = nix-command`), you can use `nix develop` instead of `nix-shell` to enter the development shell. To enter the shell with specific GHC versions: -* `nix develop` - default GHC version -* `nix develop .#shell-ghc90` - GHC 9.0.1 (substitute GHC version as appropriate) +* `nix develop` - default GHC version, +* `nix develop .#shell-ghc90` - GHC 9.0.1 (substitute GHC version as appropriate). -If you are looking for a Nix expression to create haskell-language-server binaries, see https://github.com/haskell/haskell-language-server/issues/122 +If you are looking for a Nix expression to create `haskell-language-server` binaries, see https://github.com/haskell/haskell-language-server/issues/122 ## Testing The tests make use of the [Tasty](https://github.com/feuerbach/tasty) test framework. -There are two test suites in the main haskell-language-server package, functional tests, and wrapper tests. +There are two test suites in the main `haskell-language-server` package, functional tests, and wrapper tests. Some of the wrapper tests expect `stack` to be present on the system, or else they fail. Other project packages, like the core library or plugins, can have their own test suite. @@ -83,10 +81,18 @@ Running just the wrapper tests $ cabal test wrapper-test ``` +Running just the tests for a specific plugin + +```bash +$ cabal test hls--plugin-tests +# E.g. +$ cabal test hls-refactor-plugin-tests +``` + Running a subset of tests Tasty supports providing -[Patterns](https://github.com/feuerbach/tasty#patterns) as command +[patterns](https://github.com/feuerbach/tasty#patterns) as command line arguments, to select the specific tests to run. ```bash @@ -94,11 +100,10 @@ $ cabal test func-test --test-option "-p hlint" ``` The above recompiles everything every time you use a different test option though. - -An alternative, which only recompiles when tests (or dependencies) change: +An alternative, which only recompiles when tests (or dependencies) change is to pass the `TASTY_PATTERN` environment variable: ```bash -$ cabal run haskell-language-server:func-test -- -p "hlint enables" +$ TASTY_PATTERN='hlint' cabal test func-test ``` ## Using HLS on HLS code @@ -121,7 +126,7 @@ If you want to test HLS while hacking on it (you can even test it on HLS codebas 3. (Every time you change the HLS code) Rebuild HLS 4. (Every time you change the HLS code) Restart the LSP workspace -### Find the path to the hacked HLS you build +### Find the path to your HLS build Note that unless you change the GHC version or the HLS version between builds, the path should remain the same, this is why you need to set it only once. #### Using Cabal @@ -140,9 +145,9 @@ $ echo $(pwd)/$(stack path --dist-dir)/build/haskell-language-server/haskell-lan /haskell-language-server ``` -### Configure your editor to use it +### Configuring your editor to use your HLS build -#### VS Code +#### Configuring VS Code When using VS Code you can set up each project to use a specific HLS executable: - If it doesn't already exist in your project directory, create a directory called `.vscode`. @@ -153,7 +158,7 @@ When using VS Code you can set up each project to use a specific HLS executable: } ``` -#### Emacs +#### Configuring Emacs There are several ways to configure the HLS server path: - `M-x customize-grouplsp-haskellLsp Haskell Server Path` - Evaluate `(setq lsp-haskell-server-path "/path/to/your/hacked/haskell-language-server")` @@ -175,74 +180,56 @@ There are several ways to configure the HLS server path: The project includes a [`.editorconfig`](https://editorconfig.org) [file](https://github.com/haskell/haskell-language-server/blob/master/.editorconfig) with the editor basic settings used by the project. However, most editors will need some action to honour those settings automatically. -For example vscode needs to have installed a specific [extension](https://marketplace.visualstudio.com/items?itemName=EditorConfig.EditorConfig). +For example VS Code needs to have installed a specific [extension](https://marketplace.visualstudio.com/items?itemName=EditorConfig.EditorConfig). Please, try to follow those basic settings to keep the codebase as uniform as possible. ### Formatter pre-commit hook -We are using [pre-commit](https://pre-commit.com/) to configure git pre-commit hook for formatting. Although it is possible to run formatting manually, we recommend you to use it to set pre-commit hook as our CI checks pre-commit hook is applied or not. +We are using [pre-commit](https://pre-commit.com/) to configure the git pre-commit hook for formatting. Although it is possible to format code manually, we recommend you to use the pre-commit hook as our CI checks if the hook was applied or not. -If you are using Nix or Gitpod, pre-commit hook is automatically installed. Otherwise, follow instructions on -[https://pre-commit.com/](https://pre-commit.com/) to install the `pre-commit` tool, then run the following command: +If you are using Nix or Gitpod, the pre-commit hook is automatically installed. Otherwise, follow the instructions on +[https://pre-commit.com/](https://pre-commit.com/) to install the `pre-commit` tool. Then run the following command: ```sh pre-commit install ``` -#### Why some components are excluded from automatic formatting? +#### Why are some components excluded from automatic formatting? -- `test/testdata` and `test/data` are there as we want to test formatting plugins. -- `hie-compat` is there as we want to keep its code as close to GHC as possible. +- `test/testdata` and `test/data` are excluded because we want to test formatting plugins. +- `hie-compat` is excluded because we want to keep its code as close to GHC as possible. -## Introduction tutorial +## Plugin tutorial -See the [tutorial](./plugin-tutorial.md) on writing a plugin in HLS. +See the [tutorial on writing a plugin in HLS](./plugin-tutorial.md). ## Measuring, benchmarking and tracing -### Metrics - -When ghcide is built with the `ekg` flag, HLS opens a metrics server on port 8999 exposing GC and ghcide metrics. The ghcide metrics currently exposed are: - -- `ghcide.values_count` - count of build results in the store -- `ghcide.database_count` - count of build keys in the store (these two would be the same in the absence of GC) -- `ghcide.build_count` - build count. A key is GC'ed if it is dirty and older than 100 builds -- `ghcide.dirty_keys_count` - non transitive count of dirty build keys -- `ghcide.indexing_pending_count` - count of items in the indexing queue -- `ghcide.exports_map_count` - count of identifiers in the exports map. - ### Benchmarks -If you are touching performance sensitive code, take the time to run a differential -benchmark between HEAD and master using the benchHist script. This assumes that -"master" points to the upstream master. - -Run the benchmarks with `cabal bench`. - -It should take around 25 minutes and the results will be stored in the `bench-results` folder. To interpret the results, see the comments in the `bench/Main.hs` module. +If you are touching performance sensitive code, take the time to run a differential benchmark between `HEAD` and `origin/master` (see [bench/README](https://github.com/haskell/haskell-language-server/blob/master/bench/README.md)). -More details in [bench/README](../../bench/README.md) +Run the benchmarks with `cabal bench`. The runtime is about 25 minutes and the results will be stored in the `bench-results` folder. To interpret the results, see the comments in the [bench/Main.hs](https://github.com/haskell/haskell-language-server/blob/master/bench/Main.hs) module. ### Tracing -HLS records opentelemetry [eventlog traces](https://downloads.haskell.org/~ghc/latest/docs/html/users_guide/runtime_control.html#rts-eventlog) via [opentelemetry](https://hackage.haskell.org/package/opentelemetry). To generate the traces, build with `-eventlog` and run with `+RTS -l`. To visualize the traces, install [Tracy](https://github.com/wolfpld/tracy) and use [eventlog-to-tracy](https://hackage.haskell.org/package/opentelemetry-extra) to open the generated eventlog. +HLS records [eventlog traces](https://downloads.haskell.org/~ghc/latest/docs/html/users_guide/runtime_control.html#rts-eventlog) via [opentelemetry](https://hackage.haskell.org/package/opentelemetry). To generate the traces, build with `-eventlog` and run with `+RTS -l`. To visualize the traces, install [Tracy](https://github.com/wolfpld/tracy) and use [eventlog-to-tracy](https://hackage.haskell.org/package/opentelemetry-extra) to open the generated eventlog. ## Adding support for a new editor Adding support for new editors is fairly easy if the editor already has good support for generic LSP-based extensions. -In that case, there will likely be an editor-specific support system for this (like `lsp-mode` for Emacs). -This will typically provide instructions for how to support new languages. +In that case, there will likely be an editor-specific support system (e.g., `lsp-mode` for Emacs). +The support system will typically provide instructions for how to add support for new languages. -In some cases you may need to write a small bit of additional client support, or expose a way for the user to set the server's [configuration options](#configuring-haskell-language-server) and -for them to configure how the server is started. +In some cases you may need to write a small bit of additional client support, or expose a way for the user to set the server's [configuration options](../configuration.md#configuring-haskell-language-server) and for them to configure how the server is started. -## Building the docs +## Building the documentation -The docs are built with [Sphinx](https://www.sphinx-doc.org/en/master/) and [ReadTheDocs](https://docs.readthedocs.io/en/stable/index.html), the documentation for both is helpful. +The documentation is built with [Sphinx](https://www.sphinx-doc.org/en/master/) and [ReadTheDocs](https://docs.readthedocs.io/en/stable/index.html), the documentation of both is helpful. -To build the docs you need to install some Python prerequisites. You can either `pip install -r docs/requirements.txt`, or simply enter a `nix-shell`. +You need to install some Python prerequisites. You can either `pip install -r docs/requirements.txt`, or simply enter a `nix-shell`. -Then to build and preview the docs: +Then to build and preview the documentation: ``` cd docs @@ -250,9 +237,9 @@ make html firefox _build/html/index.html ``` -Alternatively, you can build the entire thing as a Nix derivation from the flake with `nix build .#docs`. +Alternatively, you can build the documentation as a Nix derivation from the Flake with `nix build .#docs`. -The docs are also built and previewed on every PR, so you can check them from the PR status. +The documentation is also built and previewed on every PR, so you can check them from the PR status. ## Working on code actions @@ -261,8 +248,8 @@ To make HLS easier to maintain, please follow these design guidelines when addin 1. Prefer `ghc-exactprint` to manual text parsing. 2. Prefer `ghc-exactprint` to manual code generation. 3. Code generating actions should not try to format the generated code. Assume that the user is also leveraging HLS for automated code formatting. -4. Put new code actions in their own plugin unless they are very closely aligned with an existing ghcide code action. +4. Put new code actions in their own plugin unless they are very closely aligned with an existing code action. ## Sponsorship -If you want to contribute financially you can do so via [open-collective](https://opencollective.com/haskell-language-server). In the past the funding has been used to sponsor [summer student projects](https://mpickering.github.io/ide/posts/2021-07-22-summer-of-hls.html). +If you want to contribute financially, you can do so via [open-collective](https://opencollective.com/haskell-language-server). In the past, the funding was used to sponsor [summer student projects](https://mpickering.github.io/ide/posts/2021-07-22-summer-of-hls.html). diff --git a/docs/contributing/index.rst b/docs/contributing/index.rst index 76f813bec6..c6c500c630 100644 --- a/docs/contributing/index.rst +++ b/docs/contributing/index.rst @@ -6,4 +6,3 @@ Contributing contributing plugin-tutorial - releases diff --git a/docs/contributing/plugin-tutorial.lhs b/docs/contributing/plugin-tutorial.lhs new file mode 120000 index 0000000000..e1837100c2 --- /dev/null +++ b/docs/contributing/plugin-tutorial.lhs @@ -0,0 +1 @@ +plugin-tutorial.md \ No newline at end of file diff --git a/docs/contributing/plugin-tutorial.md b/docs/contributing/plugin-tutorial.md index 63d0de1a58..d9ca59c0ad 100644 --- a/docs/contributing/plugin-tutorial.md +++ b/docs/contributing/plugin-tutorial.md @@ -1,329 +1,341 @@ # Let’s write a Haskell Language Server plugin -Haskell Language Server is an LSP server for the Haskell programming language. It builds on several previous efforts -to create a Haskell IDE, you can find many more details on the history and architecture in the [IDE 2020](https://mpickering.github.io/ide/index.html) community page. +Originally written by Pepe Iborra, maintained by the Haskell community. +Haskell Language Server (HLS) is a Language Server Protocol (LSP) server for the Haskell programming language. It builds on several previous efforts to create a Haskell IDE. +You can find many more details on the history and architecture on the [IDE 2020](https://mpickering.github.io/ide/index.html) community page. In this article we are going to cover the creation of an HLS plugin from scratch: a code lens to display explicit import lists. -Along the way we will learn about HLS, its plugin model, and the relationship with ghcide and LSP. +Along the way we will learn about HLS, its plugin model, and the relationship with [ghcide](https://github.com/haskell/haskell-language-server/tree/master/ghcide) and LSP. ## Introduction Writing plugins for HLS is a joy. Personally, I enjoy the ability to tap into the gigantic bag of goodies that is GHC, as well as the IDE integration thanks to LSP. -In the last couple of months I have written various HLS (and ghcide) plugins for things like: +In the last couple of months, I have written various HLS plugins, including: 1. Suggest imports for variables not in scope, 2. Remove redundant imports, -2. Evaluate code in comments (a la doctest), -3. Integrate the retrie refactoring library. +3. Evaluate code in comments (à la [doctest](https://docs.python.org/3/library/doctest.html)), +4. Integrate the [retrie](https://github.com/facebookincubator/retrie) refactoring library. -These plugins are small but meaningful steps towards a more polished IDE experience, and in writing them I didn't have to worry about performance, UI, distribution, or even think for the most part, since it's always another tool (usually GHC) doing all the heavy lifting. The plugins also make these tools much more accessible to all the users of HLS. +These plugins are small but meaningful steps towards a more polished IDE experience. +While writing them, I didn't have to worry about performance, UI, or distribution; another tool (usually GHC) always did the heavy lifting. -## The task +The plugins also make these tools much more accessible to all users of HLS. -Here is a visual statement of what we want to accomplish: +## Preamble - ![Imports code lens](imports.gif) +This tutorial is a literate Haskell file that can be compiled. +As such, we list the imports, extensions etc... necessary for compilation. -And here is the gist of the algorithm: +Please just skip over this `import` section, if you are only interested in the tutorial! -1. Request the type checking artefacts from the ghcide subsystem -2. Extract the actual import lists from the type checked AST, -3. Ask GHC to produce the minimal import lists for this AST, -4. For every import statement without a explicit import list, find out the minimal import list, and produce a code lens to display it together with a command to graft it on. +```haskell +{-# LANGUAGE OverloadedStrings #-} +{-# LANGUAGE DerivingStrategies #-} +{-# LANGUAGE ScopedTypeVariables #-} +{-# LANGUAGE RecordWildCards #-} +{-# LANGUAGE NamedFieldPuns #-} +{-# LANGUAGE ViewPatterns #-} +{-# LANGUAGE DeriveGeneric #-} +{-# LANGUAGE DataKinds #-} +{-# LANGUAGE DeriveAnyClass #-} + +import Ide.Types +import Ide.Logger +import Ide.Plugin.Error + +import Development.IDE.Core.RuleTypes +import Development.IDE.Core.Service hiding (Log) +import Development.IDE.Core.Shake hiding (Log) +import Development.IDE.GHC.Compat +import Development.IDE.GHC.Compat.Core +import Development.IDE.GHC.Error +import Development.IDE.Types.HscEnvEq +import Development.IDE.Core.PluginUtils + +import qualified Language.LSP.Server as LSP +import Language.LSP.Protocol.Types as JL +import Language.LSP.Protocol.Message + +import Data.Aeson as Aeson +import Data.Map (Map) +import Data.IORef +import Data.Maybe (fromMaybe, catMaybes) +import qualified Data.Map as Map +import qualified Data.HashMap.Strict as HashMap +import qualified Data.Text as T +import Control.Monad (forM) +import Control.Monad.IO.Class (liftIO) +import Control.Monad.Trans.Class +import GHC.Generics (Generic) +``` -## Setup +## Plugins in the HLS codebase -To get started, let’s fetch the HLS repo and build it. You need at least GHC 9.0 for this: +The HLS codebase includes several plugins (found in `./plugins`). For example: -``` -git clone --recursive http://github.com/haskell/haskell-language-server hls -cd hls -cabal update -cabal build -``` +- The `ormolu`, `fourmolu`, `floskell` and `stylish-haskell` plugins used to format code +- The `eval` plugin, a code lens provider to evaluate code in comments +- The `retrie` plugin, a code action provider to execute retrie commands -If you run into any issues trying to build the binaries, the #haskell-language-server IRC chat room in -[Libera Chat](https://libera.chat/) is always a good place to ask for help. +I recommend looking at the existing plugins for inspiration and reference. A few conventions shared by all plugins are: -Once cabal is done take a note of the location of the `haskell-language-server` binary and point your LSP client to it. In VSCode this is done by editing the "Haskell Server Executable Path" setting. This way you can simply test your changes by reloading your editor after rebuilding the binary. +- Plugins are in the `./plugins` folder +- Plugins implement their code under the `Ide.Plugin.*` namespace +- Folders containing the plugin follow the `hls-pluginname-plugin` naming convention +- Plugins are "linked" in `src/HlsPlugins.hs#idePlugins`. New plugin descriptors + must be added there. -![Settings](settings-vscode.png) + ```haskell ignore + -- Defined in src/HlsPlugins.**hs** -## Anatomy of a plugin + idePlugins = pluginDescToIdePlugins allPlugins + where + allPlugins = + [ GhcIde.descriptor "ghcide" + , Pragmas.descriptor "pragmas" + , Floskell.descriptor "floskell" + , Fourmolu.descriptor "fourmolu" + , Ormolu.descriptor "ormolu" + , StylishHaskell.descriptor "stylish-haskell" + , Retrie.descriptor "retrie" + , Eval.descriptor "eval" + , NewPlugin.descriptor "new-plugin" -- Add new plugins here. + ] + ``` -HLS plugins are values of the `Plugin` datatype, which is defined in `Ide.Plugin` as: -```haskell -data PluginDescriptor = - PluginDescriptor { pluginId :: !PluginId - , pluginRules :: !(Rules ()) - , pluginCommands :: ![PluginCommand] - , pluginCodeActionProvider :: !(Maybe CodeActionProvider) - , pluginCodeLensProvider :: !(Maybe CodeLensProvider) - , pluginHoverProvider :: !(Maybe HoverProvider) - , pluginSymbolsProvider :: !(Maybe SymbolsProvider) - , pluginFormattingProvider :: !(Maybe (FormattingProvider IO)) - , pluginCompletionProvider :: !(Maybe CompletionProvider) - , pluginRenameProvider :: !(Maybe RenameProvider) - } -``` -A plugin has a unique id, a set of rules, a set of command handlers, and a set of "providers": +To add a new plugin, extend the list of `allPlugins` and rebuild. -* Rules add new targets to the Shake build graph defined in ghcide. 99% of plugins need not define any new rules. -* Commands are an LSP abstraction for actions initiated by the user which are handled in the server. These actions can be long running and involve multiple modules. Many plugins define command handlers. -* Providers are a query-like abstraction where the LSP client asks the server for information. These queries must be fulfilled as quickly as possible. +## The goal of the plugin we will write -The HLS codebase includes several plugins under the namespace `Ide.Plugin.*`, the most relevant are: +Here is a visual statement of what we want to accomplish: -- The ghcide plugin, which embeds ghcide as a plugin (ghcide is also the engine under HLS). -- The example and example2 plugins, offering a dubious welcome to new contributors -- The ormolu, fourmolu, floskell and stylish-haskell plugins, a testament to the code formatting wars of our community. -- The eval plugin, a code lens provider to evaluate code in comments -- The retrie plugin, a code actions provider to execute retrie commands + ![Imports code lens](imports.gif) -I would recommend looking at the existing plugins for inspiration and reference. +And here is the gist of the algorithm: -Plugins are "linked" in the `HlsPlugins` module, so we will need to add our plugin there once we have defined it: +1. Request the type checking artifacts from the `ghcide` subsystem +2. Extract the actual import lists from the type-checked AST +3. Ask GHC to produce the minimal import lists for this AST +4. For every import statement without an explicit import list: + - Determine the minimal import list + - Produce a code lens to display it and a command to apply it -```haskell -idePlugins = pluginDescToIdePlugins allPlugins - where - allPlugins = - [ GhcIde.descriptor "ghcide" - , Pragmas.descriptor "pragmas" - , Floskell.descriptor "floskell" - , Fourmolu.descriptor "fourmolu" - , Ormolu.descriptor "ormolu" - , StylishHaskell.descriptor "stylish-haskell" - , Retrie.descriptor "retrie" - , Eval.descriptor "eval" - ] -``` -To add a new plugin, simply extend the list of `allPlugins` and rebuild. +## Setup -## Providers +To get started, fetch the HLS repository and build it by following the [installation instructions](https://haskell-language-server.readthedocs.io/en/latest/contributing/contributing.html#building). -99% of plugins will want to define at least one type of provider. But what is a provider? Let's take a look at some types: -```haskell -type CodeActionProvider = LSP.LspFuncs Config - -> IdeState - -> PluginId - -> TextDocumentIdentifier - -> Range - -> CodeActionContext - -> IO (Either ResponseError (List CAResult)) - -type CompletionProvider = LSP.LspFuncs Config - -> IdeState - -> CompletionParams - -> IO (Either ResponseError CompletionResponseResult) - -type CodeLensProvider = LSP.LspFuncs Config - -> IdeState - -> PluginId - -> CodeLensParams - -> IO (Either ResponseError (List CodeLens)) - -type RenameProvider = LSP.LspFuncs Config - -> IdeState - -> RenameParams - -> IO (Either ResponseError WorkspaceEdit) -``` +If you run into any issues trying to build the binaries, you can get in touch with the HLS team using one of the [contact channels](https://haskell-language-server.readthedocs.io/en/latest/contributing/contributing.html#how-to-contact-the-haskell-ide-team) or [open an issue](https://github.com/haskell/haskell-language-server/issues) in the HLS repository. -Providers are functions that receive some inputs and produce an IO computation that returns either an error or some result. +Once the build is done, you can find the location of the HLS binary with `cabal list-bin exe:haskell-language-server` and point your LSP client to it. +This way you can simply test your changes by reloading your editor after rebuilding the binary. -All providers receive an `LSP.LspFuncs` value, which is a record of functions to perform LSP actions. Most providers can safely ignore this argument, since the LSP interaction is automatically managed by HLS. -Some of its capabilities are: -- Querying the LSP client capabilities -- Manual progress reporting and cancellation, for plugins that provide long running commands (like the Retrie plugin), -- Custom user interactions via [message dialogs](https://microsoft.github.io/language-server-protocol/specification#window_showMessage). For instance, the Retrie plugin uses this to report skipped modules. +> **Note:** In VSCode, edit the "Haskell Server Executable Path" setting. +> +> **Note:** In Emacs, edit the `lsp-haskell-server-path` variable. -The second argument plugins receive is `IdeState`, which encapsulates all the ghcide state including the build graph. This allows to request ghcide rule results, which leverages Shake to parallelize and reuse previous results as appropriate. Rule types are instances of the `RuleResult` type family, and -most of them are defined in `Development.IDE.Core.RuleTypes`. Some relevant rule types are: -```haskell --- | The parse tree for the file using GetFileContents -type instance RuleResult GetParsedModule = ParsedModule +![Settings](settings-vscode.png) --- | The type checked version of this file -type instance RuleResult TypeCheck = TcModuleResult +[Manually test your hacked HLS](https://haskell-language-server.readthedocs.io/en/latest/contributing/contributing.html#manually-testing-your-hacked-hls) to ensure you use the HLS package you just built. --- | A GHC session that we reuse. -type instance RuleResult GhcSession = HscEnvEq +## Digression about the Language Server Protocol --- | A GHC session preloaded with all the dependencies -type instance RuleResult GhcSessionDeps = HscEnvEq +There are two main types of communication in the Language Server Protocol: --- | A ModSummary that has enough information to be used to get .hi and .hie files. -type instance RuleResult GetModSummary = ModSummary -``` +- A **request-response interaction** type where one party sends a message that requires a response from the other party. +- A **notification** is a one-way interaction where one party sends a message without expecting any response. -The `use` family of combinators allow to request rule results. For example, the following code is used in the Eval plugin to request a GHC session and a module summary (for the imports) in order to set up an interactive evaluation environment -```haskell - let nfp = toNormalizedFilePath' fp - session <- runAction "runEvalCmd.ghcSession" state $ use_ GhcSessionDeps nfp - ms <- runAction "runEvalCmd.getModSummary" state $ use_ GetModSummary nfp +> **Note**: The LSP client and server can both send requests or notifications to the other party. + +## Anatomy of a plugin + +HLS plugins are values of the `PluginDescriptor` datatype, which is defined in `hls-plugin-api/src/Ide/Types.hs` as: + +```haskell ignore +data PluginDescriptor (ideState :: Type) = + PluginDescriptor { pluginId :: !PluginId + , pluginCommands :: ![PluginCommand ideState] + , pluginHandlers :: PluginHandlers ideState + , pluginNotificationHandlers :: PluginNotificationHandlers ideState +-- , [...] -- Other fields omitted for brevity. + } ``` -There are three flavours of `use` combinators: +### Request-response interaction -1. `use*` combinators block and propagate errors, -2. `useWithStale*` combinators block and switch to stale data in case of error, -3. `useWithStaleFast*` combinators return immediately with stale data if any, or block otherwise. +The `pluginHandlers` handle LSP client requests and provide responses to the client. They must fulfill these requests as quickly as possible. -## LSP abstractions +- Example: When you want to format a file, the client sends the [`textDocument/formatting`](https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#textDocument_formatting) request to the server. The server formats the file and responds with the formatted content. -If you have used VSCode or any other LSP editor you are probably already familiar with the capabilities afforded by LSP. If not, check the [specification](https://microsoft.github.io/language-server-protocol/specification) for the full details. -Another good source of information is the [haskell-lsp-types](https://hackage.haskell.org/package/haskell-lsp-types) package, which contains a Haskell encoding of the protocol. +### Notification -The [haskell-lsp-types](https://hackage.haskell.org/package/haskell-lsp-types-0.22.0.0/docs/Language-Haskell-LSP-Types.html#t:CodeLens) package encodes code lenses in Haskell as: -```haskell -data CodeLens = - CodeLens - { _range :: Range - , _command :: Maybe Command - , _xdata :: Maybe A.Value - } deriving (Read,Show,Eq) -``` -That is, a code lens is a triple of a source range, maybe a command, and optionally some extra data. The [specification](https://microsoft.github.io/language-server-protocol/specification#textDocument_codeLens) clarifies the optionality: -``` -/** - * A code lens represents a command that should be shown along with - * source text, like the number of references, a way to run tests, etc. - * - * A code lens is _unresolved_ when no command is associated to it. For performance - * reasons the creation of a code lens and resolving should be done in two stages. - */ -``` +The `pluginNotificationHandlers` handle notifications sent by the client to the server that are not explicitly triggered by a user. + +- Example: Whenever you modify a Haskell file, the client sends a notification informing HLS about the changes to the file. -To keep things simple our plugin won't make use of the unresolved facility, embedding the command directly in the code lens. +The `pluginCommands` are special types of user-initiated notifications sent to +the server. These actions can be long-running and involve multiple modules. ## The explicit imports plugin -To provide code lenses, our plugin must define a code lens provider as well as a Command handler. -The code at `Ide.Plugin.Example` shows how the convenience `defaultPluginDescriptor` function is used -to bootstrap the plugin and how to add the desired providers: +To achieve our plugin goals, we need to define: + +- a command handler (`importLensCommand`), +- a code lens request handler (`lensProvider`). + +These will be assembled in the `descriptor` function of the plugin, which contains all the information wrapped in the `PluginDescriptor` datatype mentioned above. + +Using the convenience `defaultPluginDescriptor` function, we can bootstrap the plugin with the required parts: ```haskell -descriptor :: PluginId -> PluginDescriptor -descriptor plId = (defaultPluginDescriptor plId) { - -- This plugin provides code lenses - pluginCodeLensProvider = Just provider, - -- This plugin provides a command handler - pluginCommands = [ importLensCommand ] -} +-- plugins/hls-explicit-imports-plugin/src/Ide/Plugin/ExplicitImports.hs + +data Log + +-- | The "main" function of a plugin. +descriptor :: Recorder (WithPriority Log) -> PluginId -> PluginDescriptor IdeState +descriptor recorder plId = + (defaultPluginDescriptor plId "A plugin for generating the minimal imports") + { pluginCommands = [importLensCommand], -- The plugin provides a command handler + pluginHandlers = mconcat -- The plugin provides request handlers + [ mkPluginHandler SMethod_TextDocumentCodeLens provider + ] + } ``` +We'll start with the command, since it's the simplest of the two. + ### The command handler -Our plugin provider has two components that need to be fleshed out. Let's start with the command provider, since it's the simplest of the two. +In short, LSP commands work like this: -```haskell -importLensCommand :: PluginCommand -``` +- The LSP server (HLS) initially sends a command descriptor to the client, in this case as part of a code lens. +- When the user clicks on the code lens, the client asks HLS to execute the command with the given descriptor. The server then handles and executes the command; this latter part is implemented by the `commandFunc` field of our `PluginCommand` value. -`PluginCommand` is a type synonym defined in `LSP.Types` as: +> **Note**: Check the [LSP spec](https://microsoft.github.io/language-server-protocol/specification) for a deeper understanding of how commands work. -```haskell -data PluginCommand = forall a. (FromJSON a) => +The command handler will be called `importLensCommand` and have the `PluginCommand` type, a type defined in `Ide.Types` as: + +```haskell ignore +-- hls-plugin-api/src/Ide/Types.hs + +data PluginCommand ideState = forall a. (FromJSON a) => PluginCommand { commandId :: CommandId , commandDesc :: T.Text - , commandFunc :: CommandFunction a + , commandFunc :: CommandFunction ideState a } ``` -The meat is in the `commandFunc` field, which is of type `CommandFunction`, another type synonym from `LSP.Types`: -```haskell -type CommandFunction a = - LSP.LspFuncs Config - -> IdeState - -> a - -> IO (Either ResponseError Value, Maybe (ServerMethod, ApplyWorkspaceEditParams)) -``` - -`CommandFunction` takes in the familiar `LspFuncs` and `IdeState` arguments, together with a JSON encoded argument. -I recommend checking the LSP spec in order to understand how commands work, but briefly the LSP server (us) initially sends a command descriptor to the client, in this case as part of a code lens. When the client decides to execute the command on behalf of a user action (in this case a click on the code lens), the client sends this descriptor back to the LSP server which then proceeds to handle and execute the command. The latter part is implemented by the `commandFunc` field of our `PluginCommand` value. +Let's start by creating an unfinished command handler. We'll give it an ID and a description for now: -For our command, we are going to have a very simple handler that receives a diff (`WorkspaceEdit`) and returns it to the client. The diff will be generated by our code lens provider and sent as part -of the code lens to the LSP client, who will send it back to our command handler when the user activates -the code lens: ```haskell +-- | The command handler. +importLensCommand :: PluginCommand IdeState +importLensCommand = + PluginCommand + { commandId = importCommandId + , commandDesc = "Explicit import command" + , commandFunc = runImportCommand + } + importCommandId :: CommandId importCommandId = "ImportLensCommand" +``` -importLensCommand :: PluginCommand -importLensCommand = - PluginCommand importCommandId "Explicit import command" runImportCommand +```haskell ignore +-- | Not implemented yet. +runImportCommand = undefined +``` + +The most important (and still `undefined`) field is `commandFunc :: CommandFunction`, a type synonym from `LSP.Types`: + +```haskell ignore +-- hls-plugin-api/src/Ide/Types.hs +type CommandFunction ideState a + = ideState + -> a + -> LspM Config (Either ResponseError Value) +``` + +`CommandFunction` takes an `ideState` and a JSON-encodable argument. `LspM` is a monad transformer with access to IO, and having access to a language context environment `Config`. The action evaluates to an `Either` value. `Left` indicates failure with a `ResponseError`, `Right` indicates sucess with a `Value`. + +Our handler will ignore the state argument and only use the `WorkspaceEdit` argument. + +```haskell -- | The type of the parameters accepted by our command -data ImportCommandParams = ImportCommandParams WorkspaceEdit - deriving Generic +newtype ImportCommandParams = ImportCommandParams WorkspaceEdit + deriving (Generic) deriving anyclass (FromJSON, ToJSON) -- | The actual command handler -runImportCommand :: CommandFunction ImportCommandParams -runImportCommand _lspFuncs _state (ImportCommandParams edit) = do - return (Right Null, Just (WorkspaceApplyEdit, ApplyWorkspaceEditParams edit)) - +runImportCommand :: CommandFunction IdeState ImportCommandParams +runImportCommand _ _ (ImportCommandParams edit) = do + -- This command simply triggers a workspace edit! + _ <- lift $ pluginSendRequest SMethod_WorkspaceApplyEdit (ApplyWorkspaceEditParams Nothing edit) (\_ -> pure ()) + return $ InR JL.Null ``` +`runImportCommand` [sends a request](https://hackage.haskell.org/package/lsp/docs/Language-LSP-Server.html#v:sendRequest) to the client using the method `SWorkspaceApplyEdit` and the parameters `ApplyWorkspaceEditParams Nothing edit`, providing a response handler that does nothing. It then returns `Right Null`, which is an empty `Aeson.Value` wrapped in `Right`. + ### The code lens provider The code lens provider implements all the steps of the algorithm described earlier: -> 1. Request the type checking artefacts from the ghcide subsystem -> 2. Extract the actual import lists from the type checked AST, -> 3. Ask GHC to produce the minimal import lists for this AST, -> 4. For every import statement without a explicit import list, find out what's the minimal import list, and produce a code lens to display it together with a diff to graft the import list in. +> 1. Request the type checking artifacts. +> 2. Extract the actual import lists from the type-checked AST. +> 3. Ask GHC to produce the minimal import lists for this AST. +> 4. For each import statement lacking an explicit list, determine its minimal import list and generate a code lens displaying this list along with a command to insert it. -The provider takes the usual `LspFuncs` and `IdeState` argument, as well as a `CodeLensParams` value containing the URI -for a file, and returns an IO action producing either an error or a list of code lenses for that file. +The provider takes the usual `LspFuncs` and `IdeState` arguments, as well as a `CodeLensParams` value containing a file URI. It returns an IO action that produces either an error or a list of code lenses for that file. ```haskell -provider :: CodeLensProvider -provider _lspFuncs -- LSP functions, not used - state -- ghcide state, used to retrieve typechecking artifacts - pId -- plugin Id - CodeLensParams{_textDocument = TextDocumentIdentifier{_uri}} +provider :: PluginMethodHandler IdeState Method_TextDocumentCodeLens +provider state -- ghcide state, used to retrieve typechecking artifacts + pId -- Plugin ID + CodeLensParams{_textDocument = TextDocumentIdentifier{_uri}} = do -- VSCode uses URIs instead of file paths -- haskell-lsp provides conversion functions - | Just nfp <- uriToNormalizedFilePath $ toNormalizedUri _uri - = do - -- Get the typechecking artifacts from the module - tmr <- runAction "importLens" state $ use TypeCheck nfp - -- We also need a GHC session with all the dependencies - hsc <- runAction "importLens" state $ use GhcSessionDeps nfp - -- Use the GHC api to extract the "minimal" imports - (imports, mbMinImports) <- extractMinimalImports hsc tmr - - case mbMinImports of - Just minImports -> do - let minImportsMap = - Map.fromList [ (srcSpanStart l, i) | L l i <- minImports ] - lenses <- forM imports $ - -- for every import, maybe generate a code lens - generateLens pId _uri minImportsMap - return $ Right (List $ catMaybes lenses) - _ -> - return $ Right (List []) - | otherwise - = return $ Right (List []) + nfp <- getNormalizedFilePathE _uri + -- Get the typechecking artifacts from the module + tmr <- runActionE "importLens" state $ useE TypeCheck nfp + -- We also need a GHC session with all the dependencies + hsc <- runActionE "importLens" state $ useE GhcSessionDeps nfp + -- Use the GHC API to extract the "minimal" imports + (imports, mbMinImports) <- liftIO $ extractMinimalImports hsc tmr + + case mbMinImports of + Just minImports -> do + let minImportsMap = + Map.fromList [ (realSrcLocToPosition loc, i) + | L l i <- minImports + , let RealSrcLoc loc _ = srcSpanStart (locA l) + ] + lenses <- forM imports $ \imp -> + -- for every import, maybe generate a code lens + liftIO (generateLens pId _uri minImportsMap imp) + return $ InL (catMaybes lenses) + _ -> + return $ InL [] ``` -Note how simple it is to retrieve the type checking artifacts for the module as well as a fully setup Ghc session via the Ghcide rules. +Note the simplicity of retrieving the type checking artifacts for the module, as well as a fully set up GHC session, via the `ghcide` rules. The function `extractMinimalImports` extracts the import statements from the AST and generates the minimal import lists, implementing steps 2 and 3 of the algorithm. -The details of the GHC api are not relevant to this tutorial, but the code is terse and easy to read: + +The details of the GHC API are not relevant to this tutorial, but the code is terse and easy to read: ```haskell extractMinimalImports - :: Maybe HscEnvEq - -> Maybe TcModuleResult + :: HscEnvEq + -> TcModuleResult -> IO ([LImportDecl GhcRn], Maybe [LImportDecl GhcRn]) -extractMinimalImports (Just hsc)) (Just (tmrModule -> TypecheckedModule{..})) = do - -- extract the original imports and the typechecking environment - let (tcEnv,_) = tm_internals_ - Just (_, imports, _, _) = tm_renamed_source - ParsedModule{ pm_parsed_source = L loc _} = tm_parsed_module +extractMinimalImports hsc TcModuleResult{..} = do + -- Extract the original imports and the typechecking environment + let tcEnv = tmrTypechecked + (_, imports, _, _) = tmrRenamed + ParsedModule{ pm_parsed_source = L loc _} = tmrParsed span = fromMaybe (error "expected real") $ realSpan loc -- GHC is secretly full of mutable state @@ -334,45 +346,45 @@ extractMinimalImports (Just hsc)) (Just (tmrModule -> TypecheckedModule{..})) = -- getMinimalImports computes the minimal explicit import lists initTcWithGbl (hscEnv hsc) tcEnv span $ getMinimalImports usage return (imports, minimalImports) -extractMinimalImports _ _ = return ([], Nothing) ``` -The function `generateLens` implements the last piece of the algorithm, step 4, producing a code lens for an import statement that lacks an import list. Note how the code lens includes an `ImportCommandParams` value -that contains a workspace edit that rewrites the import statement, as expected by our command provider. +The function `generateLens` implements step 4 of the algorithm, producing a code lens for an import statement that lacks an import list. The code lens includes an `ImportCommandParams` value containing a workspace edit that rewrites the import statement, as our command provider expects. ```haskell -- | Given an import declaration, generate a code lens unless it has an explicit import list generateLens :: PluginId -> Uri - -> Map SrcLoc (ImportDecl GhcRn) + -> Map Position (ImportDecl GhcRn) -> LImportDecl GhcRn -> IO (Maybe CodeLens) generateLens pId uri minImports (L src imp) -- Explicit import list case - | ImportDecl{ideclHiding = Just (False,_)} <- imp + | ImportDecl{ideclImportList = Just _} <- imp = return Nothing -- No explicit import list - | RealSrcSpan l <- src - , Just explicit <- Map.lookup (srcSpanStart src) minImports + | RealSrcSpan l _ <- locA src + , let position = realSrcLocToPosition $ realSrcSpanStart l + , Just explicit <- Map.lookup position minImports , L _ mn <- ideclName imp - -- (almost) no one wants to see an explicit import list for Prelude + -- (Almost) no one wants to see an explicit import list for Prelude , mn /= moduleName pRELUDE = do -- The title of the command is just the minimal explicit import decl - let title = T.pack $ prettyPrint explicit - -- the range of the code lens is the span of the original import decl + let title = T.pack $ printWithoutUniques explicit + -- The range of the code lens is the span of the original import decl _range :: Range = realSrcSpanToRange l - -- the code lens has no extra data + -- The code lens has no extra data _xdata = Nothing - -- an edit that replaces the whole declaration with the explicit one - edit = WorkspaceEdit (Just editsMap) Nothing - editsMap = HashMap.fromList [(uri, List [importEdit])] + -- An edit that replaces the whole declaration with the explicit one + edit = WorkspaceEdit (Just editsMap) Nothing Nothing + editsMap = Map.fromList [(uri, [importEdit])] importEdit = TextEdit _range title - -- the command argument is simply the edit + -- The command argument is simply the edit _arguments = Just [toJSON $ ImportCommandParams edit] - -- create the command - _command <- Just <$> mkLspCommand pId importCommandId title _arguments - -- create and return the code lens + _data_ = Nothing + -- Create the command + _command = Just $ mkLspCommand pId importCommandId title _arguments + -- Create and return the code lens return $ Just CodeLens{..} | otherwise = return Nothing @@ -380,15 +392,27 @@ generateLens pId uri minImports (L src imp) ## Wrapping up -There's only one haskell code change left to do at this point: "link" the plugin in the `HlsPlugins` HLS module. -However integrating the plugin in haskell-language-server itself will need some changes in config files. The best way is looking for the id (f.e. `hls-class-plugin`) of an existing plugin: -- `./cabal*.project` and `./stack*.yaml`: add the plugin package in the `packages` field -- `./haskell-language-server.cabal`: add a conditional block with the plugin package dependency -- `./.github/workflows/test.yml`: add a block to run the test suite of the plugin -- `./.github/workflows/hackage.yml`: add the plugin to the component list to release the plugin package to hackage -- `./*.nix`: add the plugin to nix builds +There's only one Haskell code change left to do at this point: "link" the plugin in the `HlsPlugins` HLS module. + +Integrating the plugin into HLS itself requires changes to several configuration files. + +A good approach is to search for the ID of an existing plugin (e.g., `hls-class-plugin`): + +- `./haskell-language-server.cabal`: Add a conditional block with the plugin package dependency. +- `./.github/workflows/test.yml`: Add a block to run the plugin's test suite. +- `./.github/workflows/hackage.yml`: Add the plugin to the component list for releasing the plugin package to Hackage. +- `./*.nix`: Add the plugin to Nix builds. -The full code as used in this tutorial, including imports, can be found in [this Gist](https://gist.github.com/pepeiborra/49b872b2e9ad112f61a3220cdb7db967) as well as in this [branch](https://github.com/pepeiborra/ide/blob/imports-lens/src/Ide/Plugin/ImportLens.hs) +This plugin tutorial re-implements parts of the [`hls-explicit-imports-plugin`] which is part of HLS. +The plugin code additionally contains advanced concepts, such as `Rules`. -I hope this has given you a taste of how easy and joyful it is to write plugins for HLS. -If you are looking for ideas for contributing, here are some cool ones found in the HLS [issue tracker](https://github.com/haskell/haskell-language-server/issues?q=is%3Aopen+is%3Aissue+label%3A%22type%3A+possible+new+plugin%22). +I hope this has given you a taste of how easy and joyful it is to write plugins for HLS. If you are looking for contribution ideas, here are some good ones listed in the HLS [issue tracker](https://github.com/haskell/haskell-language-server/issues). + +
+ Placeholder Main, unused + +```haskell +main :: IO () +main = putStrLn "Just here to silence the error!" +``` +
diff --git a/docs/features.md b/docs/features.md index 41767d64ed..1eab0054b4 100644 --- a/docs/features.md +++ b/docs/features.md @@ -20,6 +20,7 @@ Many of these are standard LSP features, but a lot of special features are provi | [Code lenses](#code-lenses) | `textDocument/codeLens` | | [Selection range](#selection-range) | `textDocument/selectionRange` | | [Rename](#rename) | `textDocument/rename` | +| [Semantic tokens](#semantic-tokens) | `textDocument/semanticTokens/full` | The individual sections below also identify which [HLS plugin](./what-is-hls.md#hls-plugins) is responsible for providing the given functionality, which is useful if you want to raise an issue report or contribute! Additionally, not all plugins are supported on all versions of GHC, see the [plugin support page](./support/plugin-support.md) for details. @@ -80,6 +81,22 @@ Known limitations: - Only works for [local definitions](https://github.com/haskell/haskell-language-server/issues/708). +## Jump to implementation + +Provided by: `ghcide` + +Jump to the implementation instance of a type class method. + +Known limitations: + +- Only works for [local definitions](https://github.com/haskell/haskell-language-server/issues/708). + +## Jump to note definition + +Provided by: `hls-notes-plugin` + +Jump to the definition of a [GHC-style note](https://gitlab.haskell.org/ghc/ghc/-/wikis/commentary/coding-style#2-using-notes). + ## Find references Provided by: `ghcide` @@ -104,6 +121,7 @@ Completions for language pragmas. ## Formatting Format your code with various Haskell code formatters. +The default Haskell code formatter is `ormolu`, and the Haskell formatter can be configured via the `formattingProvider` option. | Formatter | Provided by | | --------------- | ---------------------------- | @@ -112,12 +130,17 @@ Format your code with various Haskell code formatters. | Ormolu | `hls-ormolu-plugin` | | Stylish Haskell | `hls-stylish-haskell-plugin` | +--- + Format your cabal files with a cabal code formatter. +The default cabal code formatter is `cabal-gild`, which needs to be available on the `$PATH`, +or the location needs to be explicitly provided. +To change the cabal formatter, edit the `cabalFormattingProvider` option. | Formatter | Provided by | |-----------------|------------------------------| | cabal-fmt | `hls-cabal-fmt-plugin` | - +| cabal-gild | `hls-cabal-gild-plugin` | ## Document symbols @@ -137,7 +160,7 @@ Provided by: `hls-call-hierarchy-plugin` Shows ingoing and outgoing calls for a function. -![Call Hierarchy in VSCode](https://github.com/haskell/haskell-language-server/raw/2857eeece0398e1cd4b2ffb6069b05c4d2308b39/plugins/hls-call-hierarchy-plugin/call-hierarchy-in-vscode.gif) +![Call Hierarchy in VSCode](../plugins/hls-call-hierarchy-plugin/call-hierarchy-in-vscode.gif) ## Highlight references @@ -198,7 +221,7 @@ Rewrites imported names to be qualified. ![Qualify Imported Names Demo](../plugins/hls-qualify-imported-names-plugin/qualify-imported-names-demo.gif) -For usage see the ![readme](../plugins/hls-qualify-imported-names-plugin/README.md). +For usage see the [readme](https://github.com/haskell/haskell-language-server/blob/master/plugins/hls-qualify-imported-names-plugin/README.md). ### Add missing class methods @@ -263,7 +286,7 @@ Known Limitations: ![Change Type Signature Demo](../plugins/hls-change-type-signature-plugin/change2.gif) -![Link to Docs](../plugins/hls-change-type-signature-plugin/README.md) +[Link to Docs](https://github.com/haskell/haskell-language-server/blob/master/plugins/hls-change-type-signature-plugin/README.md) ### Add argument to function @@ -283,7 +306,7 @@ Convert a datatype to GADT syntax. ![GADT Demo](../plugins/hls-gadt-plugin/gadt.gif) -![Link to Docs](../plugins/hls-gadt-plugin/README.md) +[Link to Docs](https://github.com/haskell/haskell-language-server/blob/master/plugins/hls-gadt-plugin/README.md) ### Expand record wildcard @@ -303,6 +326,14 @@ Code action kind: `quickfix` Correct common misspelling of SPDX Licenses such as `BSD-3-Clause`. +### Add dependency to `cabal` file + +Provided by: `hls-cabal-plugin` + +Code action kind: `quickfix` + +Add a missing package dependency to your `.cabal` file. + ## Code lenses ### Add type signature @@ -315,9 +346,9 @@ Shows the type signature for bindings without type signatures, and adds it with Provided by: `hls-eval-plugin` -Evaluates code blocks in comments with a click. [Tutorial](https://github.com/haskell/haskell-language-server/blob/master/plugins/hls-eval-plugin/README.md). +Evaluates code blocks in comments with a click. A code action is also provided. [Tutorial](https://github.com/haskell/haskell-language-server/blob/master/plugins/hls-eval-plugin/README.md). -![Eval Demo](https://raw.githubusercontent.com/haskell/haskell-language-server/master/plugins/hls-eval-plugin/demo.gif) +![Eval Demo](../plugins/hls-eval-plugin/demo.gif) Known limitations: @@ -380,7 +411,24 @@ Known limitations: - Cross-module renaming requires all components to be indexed, which sometimes causes [partial renames in multi-component projects](https://github.com/haskell/haskell-language-server/issues/2193). -### Rewrite to overloaded record syntax +To eagerly load all components, you need to + +- set `haskell.sessionLoading` to `multipleComponents`, +- set `hie.yaml` to load all components (currently only cabal supports this), + ```yaml + cradle: + cabal: + component: all + ``` +- and enable tests and benchmarks in `cabal.project` with `tests: True` and `benchmarks: True`. + +## Semantic tokens + +Provided by: `hls-semantic-tokens-plugin` + +Provides semantic tokens for each token in the source code to support semantic highlighting. + +## Rewrite to overloaded record syntax Provided by: `hls-overloaded-record-dot-plugin` @@ -389,6 +437,7 @@ Code action kind: `refactor.rewrite` Rewrites record selectors to use overloaded dot syntax ![Explicit Wildcard Demo](../plugins/hls-overloaded-record-dot-plugin/example.gif) + ## Missing features The following features are supported by the LSP specification but not implemented in HLS. @@ -399,7 +448,6 @@ Contributions welcome! | Signature help | Unimplemented | `textDocument/signatureHelp` | | Jump to declaration | Unclear if useful | `textDocument/declaration` | | Jump to implementation | Unclear if useful | `textDocument/implementation` | -| Semantic tokens | Unimplemented | `textDocument/semanticTokens` | | Linked editing | Unimplemented | `textDocument/linkedEditingRange` | | Document links | Unimplemented | `textDocument/documentLink` | | Document color | Unclear if useful | `textDocument/documentColor` | diff --git a/docs/index.rst b/docs/index.rst index 0cf743688c..e3e8fab81c 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -1,7 +1,7 @@ haskell-language-server ======================= -Official Haskell Language Server implementation. :ref:`Read more`. +Official Haskell Language Server implementation. :ref:`Read more`. .. toctree:: :maxdepth: 2 diff --git a/docs/installation.md b/docs/installation.md index 4d021e2040..4a1147ade5 100644 --- a/docs/installation.md +++ b/docs/installation.md @@ -120,14 +120,16 @@ built against the official Fedora ghc package. ## FreeBSD -HLS is available for installation from official binary packages. Use +HLS is available for installation via [devel/hs-haskell-language-server](https://www.freshports.org/devel/hs-haskell-language-server) +port or from official binary packages. Use ```bash pkg install hs-haskell-language-server ``` -to install it. At the moment, HLS installed this way only supports the same GHC -version as the ports one. +to install it. HLS installed this way targets the same GHC version that the [lang/ghc](https://www.freshports.org/lang/ghc) +port produces. Use the `pkg search haskell-language` command to list HLS packages +for other GHCs. ## Gentoo diff --git a/docs/requirements.txt b/docs/requirements.txt index bb67e0bf03..4bdb963497 100644 --- a/docs/requirements.txt +++ b/docs/requirements.txt @@ -1,4 +1,4 @@ -Sphinx~=5.3.0 -sphinx-rtd-theme~=1.1.0 -myst-parser~=1.0.0 -docutils<0.19 +Sphinx~=8.1.3 +sphinx-rtd-theme~=3.0.2 +myst-parser~=4.0.0 +docutils~=0.21.2 diff --git a/docs/support/ghc-version-support.md b/docs/support/ghc-version-support.md index cad8bf2481..df0bc23494 100644 --- a/docs/support/ghc-version-support.md +++ b/docs/support/ghc-version-support.md @@ -15,35 +15,45 @@ Support status (see the support policy below for more details): - "full support": this GHC version is currently actively supported, and most [tier 2 plugins](./plugin-support.md) work - "deprecated": this GHC version was supported in the past, but is now deprecated -| GHC version | Last supporting HLS version | Support status | -|--------------|--------------------------------------------------------------------------------------|-----------------------------------------------------------------------------| -| 9.8.1 | [latest](https://github.com/haskell/haskell-language-server/releases/latest) | initial support | -| 9.6.3 | [latest](https://github.com/haskell/haskell-language-server/releases/latest) | full support | -| 9.6.2 | [2.2.0.0](https://github.com/haskell/haskell-language-server/releases/tag/2.2.0.0) | deprecated | -| 9.6.1 | [2.0.0.0](https://github.com/haskell/haskell-language-server/releases/tag/2.0.0.0) | deprecated | -| 9.4.7 | [latest](https://github.com/haskell/haskell-language-server/releases/latest) | full support | -| 9.4.6 | [2.2.0.0](https://github.com/haskell/haskell-language-server/releases/tag/2.2.0.0) | deprecated | -| 9.4.5 | [2.2.0.0](https://github.com/haskell/haskell-language-server/releases/tag/2.2.0.0) | deprecated | -| 9.4.4 | [1.10.0.0](https://github.com/haskell/haskell-language-server/releases/tag/1.10.0.0) | deprecated | -| 9.4.3 | [1.9.1.0](https://github.com/haskell/haskell-language-server/releases/tag/1.9.1.0) | deprecated | -| 9.4.(1,2) | [1.8.0.0](https://github.com/haskell/haskell-language-server/releases/tag/1.8.0.0) | deprecated | -| 9.2.8 | [latest](https://github.com/haskell/haskell-language-server/releases/latest) | full support | -| 9.2.7 | [2.0.0.1](https://github.com/haskell/haskell-language-server/releases/tag/2.0.0.1) | deprecated | -| 9.2.(5,6) | [1.9.1.0](https://github.com/haskell/haskell-language-server/releases/tag/1.9.1.0) | deprecated | -| 9.2.(3,4) | [1.8.0.0](https://github.com/haskell/haskell-language-server/releases/tag/1.8.0.0) | deprecated | -| 9.2.(1,2) | [1.7.0.0](https://github.com/haskell/haskell-language-server/releases/tag/1.7.0.0) | deprecated | -| 9.0.2 | [2.4.0.0](https://github.com/haskell/haskell-language-server/releases/tag/2.4.0.0) | deprecated | -| 9.0.1 | [1.6.1.0](https://github.com/haskell/haskell-language-server/releases/tag/1.6.1.0) | deprecated | -| 8.10.7 | [2.2.0.0](https://github.com/haskell/haskell-language-server/releases/tag/2.2.0.0) | deprecated | -| 8.10.6 | [1.6.1.0](https://github.com/haskell/haskell-language-server/releases/tag/1.6.1.0) | deprecated | -| 8.10.5 | [1.5.1](https://github.com/haskell/haskell-language-server/releases/tag/1.5.1) | deprecated | -| 8.10.(4,3,2) | [1.4.0](https://github.com/haskell/haskell-language-server/releases/tag/1.4.0) | deprecated | -| 8.10.1 | [0.9.0](https://github.com/haskell/haskell-language-server/releases/tag/0.9.0) | deprecated | -| 8.8.4 | [1.8.0](https://github.com/haskell/haskell-language-server/releases/1.8.0) | deprecated | -| 8.8.3 | [1.5.1](https://github.com/haskell/haskell-language-server/releases/1.5.1) | deprecated | -| 8.8.2 | [1.2.0](https://github.com/haskell/haskell-language-server/releases/tag/1.2.0) | deprecated | -| 8.6.5 | [1.8.0.0](https://github.com/haskell/haskell-language-server/releases/tag/1.8.0.0) | deprecated | -| 8.6.4 | [1.4.0](https://github.com/haskell/haskell-language-server/releases/tag/1.4.0) | deprecated | +| GHC version | Last supporting HLS version | Support status | +| ------------ | ------------------------------------------------------------------------------------ | -------------- | +| 9.12.2 | [latest](https://github.com/haskell/haskell-language-server/releases/latest) | full support | +| 9.10.2 | [latest](https://github.com/haskell/haskell-language-server/releases/latest) | full support | +| 9.10.1 | [latest](https://github.com/haskell/haskell-language-server/releases/latest) | full support | +| 9.8.4 | [latest](https://github.com/haskell/haskell-language-server/releases/latest) | full support | +| 9.8.2 | [2.9.0.1](https://github.com/haskell/haskell-language-server/releases/tag/2.9.0.1) | deprecated | +| 9.8.1 | [2.6.0.0](https://github.com/haskell/haskell-language-server/releases/tag/2.6.0.0) | deprecated | +| 9.6.7 | [latest](https://github.com/haskell/haskell-language-server/releases/latest) | full support | +| 9.6.6 | [2.9.0.1](https://github.com/haskell/haskell-language-server/releases/tag/2.9.0.1) | deprecated | +| 9.6.5 | [2.9.0.1](https://github.com/haskell/haskell-language-server/releases/tag/2.9.0.1) | deprecated | +| 9.6.4 | [2.6.0.0](https://github.com/haskell/haskell-language-server/releases/tag/2.6.0.0) | deprecated | +| 9.6.3 | [2.5.0.0](https://github.com/haskell/haskell-language-server/releases/tag/2.5.0.0) | deprecated | +| 9.6.2 | [2.2.0.0](https://github.com/haskell/haskell-language-server/releases/tag/2.2.0.0) | deprecated | +| 9.6.1 | [2.0.0.0](https://github.com/haskell/haskell-language-server/releases/tag/2.0.0.0) | deprecated | +| 9.4.8 | [latest](https://github.com/haskell/haskell-language-server/releases/latest) | full support | +| 9.4.7 | [2.5.0.0](https://github.com/haskell/haskell-language-server/releases/tag/2.5.0.0) | deprecated | +| 9.4.6 | [2.2.0.0](https://github.com/haskell/haskell-language-server/releases/tag/2.2.0.0) | deprecated | +| 9.4.5 | [2.2.0.0](https://github.com/haskell/haskell-language-server/releases/tag/2.2.0.0) | deprecated | +| 9.4.4 | [1.10.0.0](https://github.com/haskell/haskell-language-server/releases/tag/1.10.0.0) | deprecated | +| 9.4.3 | [1.9.1.0](https://github.com/haskell/haskell-language-server/releases/tag/1.9.1.0) | deprecated | +| 9.4.(1,2) | [1.8.0.0](https://github.com/haskell/haskell-language-server/releases/tag/1.8.0.0) | deprecated | +| 9.2.8 | [2.9.0.0](https://github.com/haskell/haskell-language-server/releases/tag/2.9.0.0) | deprecated | +| 9.2.7 | [2.0.0.1](https://github.com/haskell/haskell-language-server/releases/tag/2.0.0.1) | deprecated | +| 9.2.(5,6) | [1.9.1.0](https://github.com/haskell/haskell-language-server/releases/tag/1.9.1.0) | deprecated | +| 9.2.(3,4) | [1.8.0.0](https://github.com/haskell/haskell-language-server/releases/tag/1.8.0.0) | deprecated | +| 9.2.(1,2) | [1.7.0.0](https://github.com/haskell/haskell-language-server/releases/tag/1.7.0.0) | deprecated | +| 9.0.2 | [2.4.0.0](https://github.com/haskell/haskell-language-server/releases/tag/2.4.0.0) | deprecated | +| 9.0.1 | [1.6.1.0](https://github.com/haskell/haskell-language-server/releases/tag/1.6.1.0) | deprecated | +| 8.10.7 | [2.2.0.0](https://github.com/haskell/haskell-language-server/releases/tag/2.2.0.0) | deprecated | +| 8.10.6 | [1.6.1.0](https://github.com/haskell/haskell-language-server/releases/tag/1.6.1.0) | deprecated | +| 8.10.5 | [1.5.1](https://github.com/haskell/haskell-language-server/releases/tag/1.5.1) | deprecated | +| 8.10.(4,3,2) | [1.4.0](https://github.com/haskell/haskell-language-server/releases/tag/1.4.0) | deprecated | +| 8.10.1 | [0.9.0](https://github.com/haskell/haskell-language-server/releases/tag/0.9.0) | deprecated | +| 8.8.4 | [1.8.0](https://github.com/haskell/haskell-language-server/releases/1.8.0) | deprecated | +| 8.8.3 | [1.5.1](https://github.com/haskell/haskell-language-server/releases/1.5.1) | deprecated | +| 8.8.2 | [1.2.0](https://github.com/haskell/haskell-language-server/releases/tag/1.2.0) | deprecated | +| 8.6.5 | [1.8.0.0](https://github.com/haskell/haskell-language-server/releases/tag/1.8.0.0) | deprecated | +| 8.6.4 | [1.4.0](https://github.com/haskell/haskell-language-server/releases/tag/1.4.0) | deprecated | GHC versions not in the list have never been supported by HLS. LTS stands for [Stackage](https://www.stackage.org/) Long Term Support. @@ -71,26 +81,62 @@ Major versions of GHC which are not supported by HLS on master are extremely unl ## GHC version deprecation policy -### Major versions +### Base policy -A major GHC version is a "legacy" version if it is 3 or more major versions behind the latest GHC version that is +This is the static part of the policy that can be checked by a machine. -1. Fully supported by HLS -2. Used in the a Stackage LTS +#### Major versions -For example, if 9.2 is the latest major version fully supported by HLS and used in a Stackage LTS, then the 8.8 major version and older will be legacy. +HLS will support major versions of GHC until they are older than _both_ -HLS will support all non-legacy major versions of GHC. +1. The major version of GHC used in the current Stackage LTS; and +2. The major version of GHC recommended by GHCup -### Minor versions +For example, if + +1. Stackage LTS uses GHC 9.2; and +2. GHCUp recommends GHC 9.4 + +then HLS will support back to GHC 9.2. + +#### Minor versions For the latest supported major GHC version we will support at least 2 minor versions. For the rest of the supported major GHC versions, we will support at least the latest minor version in Stackage LTS (so 1 minor version). -### Announcements +### Extended policy + +This is the part of the policy that needs evaluation by a human and possibly followed +by a discussion. + +#### Ecosystem factors + +To establish and apply the policy we take the following ecosystem factors into account: + +- Support status of HLS +- The most recent [stackage](https://www.stackage.org/) LTS snapshot +- The GHC version recommended by GHCup +- The GHC versions used in the most popular [linux distributions](https://repology.org/project/ghc/versions) +- The reliability of different ghc versions on the major operating systems (Linux, Windows, MacOS) +- The [Haskell Survey results](https://taylor.fausak.me/2022/11/18/haskell-survey-results/#s2q4) -We will warn users about the upcoming deprecation of a GHC version in the notes of the release *prior* to the deprecation itself. +### Supporting a GHC version beyond normal deprecation time + +In cases where the base policy demands a deprecation, but ecosystem factors +suggest that it's still widely used (e.g. last [Haskell Survey results](https://taylor.fausak.me/2022/11/18/haskell-survey-results/#s2q4)), +the deprecation should be suspended for the next release and the situation be re-evaluated for the release after that. + +When we decide to keep on an old version, we should track it as follows: + +1. open a ticket on HLS issue tracker wrt discussing to deprecate said GHC version + - explain the reason the GHC version wasn't deprecated (context) + - explain the maintenance burden it causes (reason) + - evaluate whether it impacts the next HLS release (impact) +2. discuss whether ecosystem factors changed + - e.g. if Haskell Survey results show that 25% or more of users are still on the GHC version in question, then dropping should be avoided +3. if dropping is still undesired, but maintenance burden is also high, then set out a call-for-help and contact HF for additional funding to support this GHC version +4. if no help or funding was received within 2 releases (say, e.g. 3-6 months), then drop the version regardless ### Why deprecate older versions of GHC? @@ -103,12 +149,3 @@ We will warn users about the upcoming deprecation of a GHC version in the notes So we need to limit the GHC support to save maintainers and contributors time and reduce CI resources. At same time we aim to support the right balance of GHC versions to minimize the impact on users. - -### What factors do we take into account when deprecating a version? - -To establish and apply the policy we take into account: - -- Completeness: support includes all plugins and features -- The most recent [stackage](https://www.stackage.org/) LTS snapshot -- The GHC versions used in the most popular [linux distributions](https://repology.org/project/ghc/versions) -- The reliability of different ghc versions on the major operating systems (Linux, Windows, MacOS) diff --git a/docs/support/plugin-support.md b/docs/support/plugin-support.md index 553fa7c901..4263f0d035 100644 --- a/docs/support/plugin-support.md +++ b/docs/support/plugin-support.md @@ -37,31 +37,34 @@ For example, a plugin to provide a formatter which has itself been abandoned has ## Current plugin support tiers -| Plugin | Tier | Unsupported GHC versions | -|-------------------------------------|------|--------------------------| -| ghcide core plugins | 1 | | -| `hls-call-hierarchy-plugin` | 1 | | -| `hls-code-range-plugin` | 1 | | -| `hls-explicit-imports-plugin` | 1 | | -| `hls-pragmas-plugin` | 1 | | -| `hls-refactor-plugin` | 1 | 9.8 | -| `hls-alternate-number-plugin` | 2 | | -| `hls-cabal-fmt-plugin` | 2 | | -| `hls-class-plugin` | 2 | 9.8 | -| `hls-change-type-signature-plugin` | 2 | | -| `hls-eval-plugin` | 2 | | -| `hls-explicit-fixity-plugin` | 2 | | -| `hls-explicit-record-fields-plugin` | 2 | | -| `hls-fourmolu-plugin` | 2 | 9.8 | -| `hls-gadt-plugin` | 2 | 9.8 | -| `hls-hlint-plugin` | 2 | 9.8 | -| `hls-module-name-plugin` | 2 | | -| `hls-qualify-imported-names-plugin` | 2 | | -| `hls-ormolu-plugin` | 2 | 9.8 | -| `hls-rename-plugin` | 2 | 9.8 | -| `hls-stylish-haskell-plugin` | 2 | 9.8 | -| `hls-overloaded-record-dot-plugin` | 2 | | -| `hls-floskell-plugin` | 3 | 9.6, 9.8 | -| `hls-stan-plugin` | 3 | 9.2.(4-8) | -| `hls-retrie-plugin` | 3 | 9.8 | -| `hls-splice-plugin` | 3 | 9.8 | +| Plugin | Tier | Unsupported GHC versions | +| ------------------------------------ | ---- | ------------------------ | +| ghcide core plugins | 1 | | +| `hls-call-hierarchy-plugin` | 1 | | +| `hls-code-range-plugin` | 1 | | +| `hls-explicit-imports-plugin` | 1 | | +| `hls-pragmas-plugin` | 1 | | +| `hls-refactor-plugin` | 2 | | +| `hls-alternate-number-format-plugin` | 2 | | +| `hls-cabal-fmt-plugin` | 2 | | +| `hls-cabal-gild-plugin` | 2 | | +| `hls-class-plugin` | 2 | | +| `hls-change-type-signature-plugin` | 2 | | +| `hls-eval-plugin` | 2 | | +| `hls-explicit-fixity-plugin` | 2 | | +| `hls-explicit-record-fields-plugin` | 2 | | +| `hls-fourmolu-plugin` | 2 | | +| `hls-gadt-plugin` | 2 | | +| `hls-hlint-plugin` | 2 | | +| `hls-module-name-plugin` | 2 | | +| `hls-notes-plugin` | 2 | | +| `hls-qualify-imported-names-plugin` | 2 | | +| `hls-ormolu-plugin` | 2 | | +| `hls-rename-plugin` | 2 | | +| `hls-stylish-haskell-plugin` | 2 | | +| `hls-overloaded-record-dot-plugin` | 2 | | +| `hls-semantic-tokens-plugin` | 2 | | +| `hls-floskell-plugin` | 3 | 9.10.1, 9.12.2 | +| `hls-stan-plugin` | 3 | 9.12.2 | +| `hls-retrie-plugin` | 3 | 9.10.1, 9.12.2 | +| `hls-splice-plugin` | 3 | 9.10.1, 9.12.2 | diff --git a/docs/troubleshooting.md b/docs/troubleshooting.md index 8a60854ccb..428fbe32f2 100644 --- a/docs/troubleshooting.md +++ b/docs/troubleshooting.md @@ -189,7 +189,7 @@ stack install haskell-language-server You also can leverage `ghcup compile hls`: ```bash -ghcup compile hls -v 1.9.0.0 --ghc 9.2.5 +ghcup compile hls -v 2.9.0.0 --ghc 9.6.5 ``` ### Preprocessors diff --git a/docs/what-is-hls.md b/docs/what-is-hls.md index 960eef3f1b..8b46076121 100644 --- a/docs/what-is-hls.md +++ b/docs/what-is-hls.md @@ -1,6 +1,6 @@ -# What is haskell-language-server? +# What is the Haskell Language Server? -The `haskell-language-server` (HLS) project is an implementation of a server (a "language server") for the [Language Server Protocol](https://microsoft.github.io/language-server-protocol/) (LSP). +The Haskell Language Server (HLS) is an implementation of a server (a "language server") for the [Language Server Protocol](https://microsoft.github.io/language-server-protocol/) (LSP). A language server talks to a client (typically an editor), which can ask the server to perform various operations, such as reporting errors or providing code completions. The advantage of this system is that clients and servers can interoperate more easily so long as they all speak the LSP protocol. In the case of HLS, that means that it can be used with many different editors, since editor support for the LSP protocol is now widespread. @@ -35,7 +35,7 @@ Here are a few pieces of jargon that you may come across in the HLS docs or when - *Semantic highlighting*: Special syntax highlighting performed by the server. - *Method*: A LSP method is a function in the LSP protocol that the client can invoke to perform some action, e.g. ask for completions at a point. -## haskell-language-server +## Haskell Language Server ### HLS and its wrapper @@ -51,7 +51,7 @@ Plugins can also be disabled independently to allow users to customize the behav These plugins all (currently) live in the HLS repository and are developed in tandem with the core HLS functionality. -See the [configuration page](./configuration.md#generic-plugin-configuration) for more on configuring plugins. +See the [configuration page](./configuration.md#Generic plugin configuration) for more on configuring plugins. ### hie-bios diff --git a/docutils.nix b/docutils.nix deleted file mode 100644 index 1c47e1455d..0000000000 --- a/docutils.nix +++ /dev/null @@ -1,32 +0,0 @@ -{ stdenv, lib, fetchPypi, buildPythonPackage, isPy3k, python }: - -buildPythonPackage rec { - pname = "docutils"; - version = "0.17.1"; - - src = fetchPypi { - inherit pname version; - sha256 = "686577d2e4c32380bb50cbb22f575ed742d58168cee37e99117a854bcd88f125"; - }; - - # Only Darwin needs LANG, but we could set it in general. - # It's done here conditionally to prevent mass-rebuilds. - checkPhase = lib.optionalString (isPy3k && stdenv.isDarwin) - ''LANG="en_US.UTF-8" LC_ALL="en_US.UTF-8" '' + '' - ${python.interpreter} test/alltests.py - ''; - - # Create symlinks lacking a ".py" suffix, many programs depend on these names - postFixup = '' - for f in $out/bin/*.py; do - ln -s $(basename $f) $out/bin/$(basename $f .py) - done - ''; - - meta = with lib; { - description = "Python Documentation Utilities"; - homepage = "http://docutils.sourceforge.net/"; - license = with licenses; [ publicDomain bsd2 psfl gpl3Plus ]; - maintainers = with maintainers; [ AndersonTorres ]; - }; -} diff --git a/exe/Main.hs b/exe/Main.hs index 16f99a44e0..5684c6f898 100644 --- a/exe/Main.hs +++ b/exe/Main.hs @@ -1,9 +1,7 @@ -- Copyright (c) 2019 The DAML Authors. All rights reserved. -- SPDX-License-Identifier: Apache-2.0 -{-# LANGUAGE LambdaCase #-} -{-# LANGUAGE NamedFieldPuns #-} -{-# LANGUAGE OverloadedStrings #-} -{-# LANGUAGE ScopedTypeVariables #-} +{-# LANGUAGE LambdaCase #-} +{-# LANGUAGE OverloadedStrings #-} module Main(main) where import Control.Exception (displayException) @@ -13,7 +11,11 @@ import Data.Function ((&)) import Data.Functor ((<&>)) import Data.Maybe (catMaybes) import Data.Text (Text) -import Ide.Logger (Doc, Priority (Error, Info), +import qualified HlsPlugins as Plugins +import Ide.Arguments (Arguments (..), + GhcideArguments (..), + getArguments) +import Ide.Logger (Doc, Priority (Error, Info), Recorder, WithPriority (WithPriority, priority), cfilter, cmapWithPrio, @@ -21,11 +23,7 @@ import Ide.Logger (Doc, Priority (Error, Info), layoutPretty, logWith, makeDefaultStderrRecorder, renderStrict, withFileRecorder) -import qualified Ide.Logger as Logger -import qualified HlsPlugins as Plugins -import Ide.Arguments (Arguments (..), - GhcideArguments (..), - getArguments) +import qualified Ide.Logger as Logger import Ide.Main (defaultMain) import qualified Ide.Main as IdeMain import Ide.PluginUtils (pluginDescToIdePlugins) @@ -70,7 +68,7 @@ main = do ]) -- This plugin just installs a handler for the `initialized` notification, which then -- picks up the LSP environment and feeds it to our recorders - let lspRecorderPlugin = (defaultPluginDescriptor "LSPRecorderCallback") + let lspRecorderPlugin = (defaultPluginDescriptor "LSPRecorderCallback" "Internal plugin") { pluginNotificationHandlers = mkPluginNotificationHandler LSP.SMethod_Initialized $ \_ _ _ _ -> do env <- LSP.getLspEnv liftIO $ (cb1 <> cb2) env diff --git a/exe/Wrapper.hs b/exe/Wrapper.hs index 8489c96f3d..2c2401ab6a 100644 --- a/exe/Wrapper.hs +++ b/exe/Wrapper.hs @@ -1,19 +1,14 @@ -{-# LANGUAGE CPP #-} -{-# LANGUAGE DataKinds #-} -{-# LANGUAGE ExplicitNamespaces #-} -{-# LANGUAGE GeneralizedNewtypeDeriving #-} -{-# LANGUAGE LambdaCase #-} -{-# LANGUAGE NamedFieldPuns #-} -{-# LANGUAGE OverloadedStrings #-} -{-# LANGUAGE RecordWildCards #-} -{-# LANGUAGE ScopedTypeVariables #-} +{-# LANGUAGE CPP #-} +{-# LANGUAGE DataKinds #-} +{-# LANGUAGE LambdaCase #-} +{-# LANGUAGE OverloadedStrings #-} +{-# LANGUAGE RecordWildCards #-} -- | This module is based on the hie-wrapper.sh script in -- https://github.com/alanz/vscode-hie-server module Main where import Control.Monad.Extra import Data.Default -import Data.Either.Extra (eitherToMaybe) import Data.Foldable import Data.List import Data.List.Extra (trimEnd) @@ -45,20 +40,16 @@ import qualified Data.Text as T import qualified Data.Text.IO as T import Development.IDE.LSP.LanguageServer (runLanguageServer) import qualified Development.IDE.Main as Main -import GHC.Stack.Types (emptyCallStack) -import Ide.Logger (Doc, Logger (Logger), - Pretty (pretty), - Recorder (logger_), - WithPriority (WithPriority), +import Ide.Logger (Doc, Pretty (pretty), + Recorder, WithPriority, cmapWithPrio, - makeDefaultStderrRecorder, - toCologActionWithPrio) + makeDefaultStderrRecorder) import Ide.Plugin.Config (Config) import Ide.Types (IdePlugins (IdePlugins)) import Language.LSP.Protocol.Message (Method (Method_Initialize), - ResponseError, SMethod (SMethod_Exit, SMethod_WindowShowMessageRequest), - TRequestMessage) + TRequestMessage, + TResponseError) import Language.LSP.Protocol.Types (MessageActionItem (MessageActionItem), MessageType (MessageType_Error), ShowMessageRequestParams (ShowMessageRequestParams), @@ -83,9 +74,12 @@ main = do putStrLn "Tool versions found on the $PATH" putStrLn $ showProgramVersionOfInterest programsOfInterest putStrLn "Tool versions in your project" - cradle <- findProjectCradle' False - ghcVersion <- runExceptT $ getRuntimeGhcVersion' recorder cradle - putStrLn $ showProgramVersion "ghc" $ mkVersion =<< eitherToMaybe ghcVersion + cradle <- findProjectCradle' recorder False + runExceptT (getRuntimeGhcVersion' cradle) >>= \case + Left err -> + T.hPutStrLn stderr (prettyError err NoShorten) + Right ghcVersion -> + putStrLn $ showProgramVersion "ghc" $ mkVersion ghcVersion VersionMode PrintVersion -> putStrLn hlsVer @@ -94,18 +88,20 @@ main = do putStrLn haskellLanguageServerNumericVersion BiosMode PrintCradleType -> - print =<< findProjectCradle + print =<< findProjectCradle recorder PrintLibDir -> do - cradle <- findProjectCradle' False - (CradleSuccess libdir) <- HieBios.getRuntimeGhcLibDir (toCologActionWithPrio (cmapWithPrio pretty recorder)) cradle + cradle <- findProjectCradle' recorder False + (CradleSuccess libdir) <- HieBios.getRuntimeGhcLibDir cradle putStr libdir _ -> launchHaskellLanguageServer recorder args >>= \case Right () -> pure () Left err -> do T.hPutStrLn stderr (prettyError err NoShorten) case args of - Ghcide _ -> launchErrorLSP recorder (prettyError err Shorten) - _ -> pure () + Ghcide (GhcideArguments { argsCommand = Main.LSP }) -> + launchErrorLSP recorder (prettyError err Shorten) + + _ -> exitFailure launchHaskellLanguageServer :: Recorder (WithPriority (Doc ())) -> Arguments -> IO (Either WrapperSetupError ()) launchHaskellLanguageServer recorder parsedArgs = do @@ -116,7 +112,7 @@ launchHaskellLanguageServer recorder parsedArgs = do d <- getCurrentDirectory -- search for the project cradle type - cradle <- findProjectCradle + cradle <- findProjectCradle recorder -- Get the root directory from the cradle setCurrentDirectory $ cradleRootDir cradle @@ -124,7 +120,7 @@ launchHaskellLanguageServer recorder parsedArgs = do case parsedArgs of Ghcide GhcideArguments{..} -> when argsProjectGhcVersion $ do - runExceptT (getRuntimeGhcVersion' recorder cradle) >>= \case + runExceptT (getRuntimeGhcVersion' cradle) >>= \case Right ghcVersion -> putStrLn ghcVersion >> exitSuccess Left err -> T.putStrLn (prettyError err NoShorten) >> exitFailure _ -> pure () @@ -147,7 +143,7 @@ launchHaskellLanguageServer recorder parsedArgs = do hPutStrLn stderr "Consulting the cradle to get project GHC version..." runExceptT $ do - ghcVersion <- getRuntimeGhcVersion' recorder cradle + ghcVersion <- getRuntimeGhcVersion' cradle liftIO $ hPutStrLn stderr $ "Project GHC version: " ++ ghcVersion let @@ -172,10 +168,10 @@ launchHaskellLanguageServer recorder parsedArgs = do let cradleName = actionName (cradleOptsProg cradle) -- we need to be compatible with NoImplicitPrelude - ghcBinary <- liftIO (fmap trim <$> runGhcCmd (toCologActionWithPrio (cmapWithPrio pretty recorder)) ["-v0", "-package-env=-", "-ignore-dot-ghci", "-e", "Control.Monad.join (Control.Monad.fmap System.IO.putStr System.Environment.getExecutablePath)"]) + ghcBinary <- liftIO (fmap trim <$> runGhcCmd ["-v0", "-package-env=-", "-ignore-dot-ghci", "-e", "Control.Monad.join (Control.Monad.fmap System.IO.putStr System.Environment.getExecutablePath)"]) >>= cradleResult cradleName - libdir <- liftIO (HieBios.getRuntimeGhcLibDir (toCologActionWithPrio (cmapWithPrio pretty recorder)) cradle) + libdir <- liftIO (HieBios.getRuntimeGhcLibDir cradle) >>= cradleResult cradleName env <- Map.fromList <$> liftIO getEnvironment @@ -192,8 +188,8 @@ cradleResult cradleName CradleNone = throwE $ NoneCradleGhcVersion cradleName -- | Version of 'getRuntimeGhcVersion' that dies if we can't get it, and also -- checks to see if the tool is missing if it is one of -getRuntimeGhcVersion' :: Recorder (WithPriority (Doc ())) -> Cradle Void -> ExceptT WrapperSetupError IO String -getRuntimeGhcVersion' recorder cradle = do +getRuntimeGhcVersion' :: Cradle Void -> ExceptT WrapperSetupError IO String +getRuntimeGhcVersion' cradle = do let cradleName = actionName (cradleOptsProg cradle) -- See if the tool is installed @@ -204,7 +200,7 @@ getRuntimeGhcVersion' recorder cradle = do Direct -> checkToolExists "ghc" _ -> pure () - ghcVersionRes <- liftIO $ HieBios.getRuntimeGhcVersion (toCologActionWithPrio (cmapWithPrio pretty recorder)) cradle + ghcVersionRes <- liftIO $ HieBios.getRuntimeGhcVersion cradle cradleResult cradleName ghcVersionRes where @@ -214,11 +210,11 @@ getRuntimeGhcVersion' recorder cradle = do Just _ -> pure () Nothing -> throwE $ ToolRequirementMissing exe (actionName (cradleOptsProg cradle)) -findProjectCradle :: IO (Cradle Void) -findProjectCradle = findProjectCradle' True +findProjectCradle :: Recorder (WithPriority (Doc ())) -> IO (Cradle Void) +findProjectCradle recorder = findProjectCradle' recorder True -findProjectCradle' :: Bool -> IO (Cradle Void) -findProjectCradle' log = do +findProjectCradle' :: Recorder (WithPriority (Doc ())) -> Bool -> IO (Cradle Void) +findProjectCradle' recorder log = do d <- getCurrentDirectory let initialFp = d "a" @@ -230,7 +226,7 @@ findProjectCradle' log = do Just yaml -> hPutStrLn stderr $ "Found \"" ++ yaml ++ "\" for \"" ++ initialFp ++ "\"" Nothing -> hPutStrLn stderr "No 'hie.yaml' found. Try to discover the project type!" - Session.loadCradle def hieYaml d + Session.loadCradle def (cmapWithPrio pretty recorder) hieYaml d trim :: String -> String trim s = case lines s of @@ -275,9 +271,8 @@ newtype ErrorLSPM c a = ErrorLSPM { unErrorLSPM :: (LspM c) a } -- to shut down the LSP. launchErrorLSP :: Recorder (WithPriority (Doc ())) -> T.Text -> IO () launchErrorLSP recorder errorMsg = do - let logger = Logger $ \p m -> logger_ recorder (WithPriority p emptyCallStack (pretty m)) - - let defaultArguments = Main.defaultArguments (cmapWithPrio pretty recorder) logger (IdePlugins []) + cwd <- getCurrentDirectory + let defaultArguments = Main.defaultArguments (cmapWithPrio pretty recorder) cwd (IdePlugins []) inH <- Main.argsHandleIn defaultArguments @@ -290,7 +285,7 @@ launchErrorLSP recorder errorMsg = do -- Forcefully exit let exit = void $ tryPutMVar clientMsgVar () - let doInitialize :: LSP.LanguageContextEnv Config -> TRequestMessage Method_Initialize -> IO (Either ResponseError (LSP.LanguageContextEnv Config, ())) + let doInitialize :: LSP.LanguageContextEnv Config -> TRequestMessage Method_Initialize -> IO (Either (TResponseError Method_Initialize) (LSP.LanguageContextEnv Config, ())) doInitialize env _ = do let restartTitle = "Try to restart" diff --git a/flake.lock b/flake.lock index dc8f1eb9ab..352483a773 100644 --- a/flake.lock +++ b/flake.lock @@ -3,11 +3,11 @@ "flake-compat": { "flake": false, "locked": { - "lastModified": 1673956053, - "narHash": "sha256-4gtG9iQuiKITOjNQQeQIpoIB6b16fm+504Ch3sNKLd8=", + "lastModified": 1747046372, + "narHash": "sha256-CIVLLkVgvHYbgI2UpXvIIBJ12HWgX+fjA8Xf8PUmqCY=", "owner": "edolstra", "repo": "flake-compat", - "rev": "35bb57c0c8d8b62bbfd284272c928ceb64ddbde9", + "rev": "9100a0f413b0c601e0533d1d94ffd501ce2e7885", "type": "github" }, "original": { @@ -21,11 +21,11 @@ "systems": "systems" }, "locked": { - "lastModified": 1685518550, - "narHash": "sha256-o2d0KcvaXzTrPRIo0kOLV0/QXHhDQ5DTi+OxcjO8xqY=", + "lastModified": 1731533236, + "narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=", "owner": "numtide", "repo": "flake-utils", - "rev": "a1720a10a6cfe8234c0e93907ffe81be440f4cef", + "rev": "11707dc2f618dd54ca8739b309ec4fc024de578b", "type": "github" }, "original": { @@ -36,17 +36,17 @@ }, "nixpkgs": { "locked": { - "lastModified": 1694477507, - "narHash": "sha256-RtUmM5s6vnx1W+tnrGzXArVScJ/IoGmqCLM177k5O5A=", + "lastModified": 1748437873, + "narHash": "sha256-E2640ouB7VxooUQdCiDRo/rVXnr1ykgF9A7HrwWZVSo=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "ff303118b2ec262eb342eab88ae79318fac66d52", + "rev": "c742ae7908a82c9bf23ce27bfca92a00e9bcd541", "type": "github" }, "original": { "owner": "NixOS", - "ref": "haskell-updates", "repo": "nixpkgs", + "rev": "c742ae7908a82c9bf23ce27bfca92a00e9bcd541", "type": "github" } }, diff --git a/flake.nix b/flake.nix index f0f92aa476..1002eb87b5 100644 --- a/flake.nix +++ b/flake.nix @@ -2,9 +2,11 @@ description = "haskell-language-server development flake"; inputs = { - nixpkgs.url = "github:NixOS/nixpkgs/haskell-updates"; + # Don't use nixpkgs-unstable as aarch64-darwin is currently broken there. + # Check again, when https://github.com/NixOS/nixpkgs/pull/414242 is resolved. + nixpkgs.url = "github:NixOS/nixpkgs/c742ae7908a82c9bf23ce27bfca92a00e9bcd541"; flake-utils.url = "github:numtide/flake-utils"; - # for default.nix + # For default.nix flake-compat = { url = "github:edolstra/flake-compat"; flake = false; @@ -12,8 +14,9 @@ }; outputs = - inputs@{ self, nixpkgs, flake-utils, ... }: - flake-utils.lib.eachSystem [ "x86_64-linux" "x86_64-darwin" "aarch64-linux" "aarch64-darwin" ] + { nixpkgs, flake-utils, ... }: + flake-utils.lib.eachSystem + [ "x86_64-linux" "x86_64-darwin" "aarch64-linux" "aarch64-darwin" ] (system: let pkgs = import nixpkgs { @@ -21,14 +24,23 @@ config = { allowBroken = true; }; }; - pythonWithPackages = pkgs.python3.withPackages (ps: [ps.sphinx ps.myst-parser ps.sphinx_rtd_theme ps.pip]); + pythonWithPackages = pkgs.python3.withPackages (ps: + [ ps.docutils + ps.myst-parser + ps.pip + ps.sphinx + ps.sphinx_rtd_theme + ]); docs = pkgs.stdenv.mkDerivation { name = "hls-docs"; - src = pkgs.lib.sourceFilesBySuffices ./. [ ".py" ".rst" ".md" ".png" ".gif" ".svg" ".cabal" ]; + src = pkgs.lib.sourceFilesBySuffices ./. + [ ".py" ".rst" ".md" ".png" ".gif" ".svg" ".cabal" ]; buildInputs = [ pythonWithPackages ]; - # -n gives warnings on missing link targets, -W makes warnings into errors - buildPhase = ''cd docs; sphinx-build -n -W . $out''; + buildPhase = '' + cd docs + make --makefile=${./docs/Makefile} html BUILDDIR=$out + ''; dontInstall = true; }; @@ -50,26 +62,25 @@ mkDevShell = hpkgs: with pkgs; mkShell { name = "haskell-language-server-dev-ghc${hpkgs.ghc.version}"; - # For binary Haskell tools, we use the default nixpkgs GHC - # This removes a rebuild with a different GHC version - # The drawback of this approach is that our shell may pull two GHC - # version in scope. + # For binary Haskell tools, we use the default Nixpkgs GHC version. + # This removes a rebuild with a different GHC version. The drawback of + # this approach is that our shell may pull two GHC versions in scope. buildInputs = [ - # our compiling toolchain + # Compiler toolchain hpkgs.ghc + hpkgs.haskell-language-server pkgs.haskellPackages.cabal-install - # Dependencies needed to build some parts of hackage + # Dependencies needed to build some parts of Hackage gmp zlib ncurses + # for compatibility of curl with provided gcc + curl # Changelog tooling - (gen-hls-changelogs pkgs.haskellPackages) + (gen-hls-changelogs hpkgs) # For the documentation pythonWithPackages - # @guibou: I'm not sure this is needed. - hlint (pkgs.haskell.lib.justStaticExecutables (pkgs.haskell.lib.dontCheck pkgs.haskellPackages.opentelemetry-extra)) capstone - # ormolu - # stylish-haskell + stylish-haskell pre-commit ] ++ lib.optionals (!stdenv.isDarwin) [ # tracy has a build problem on macos. @@ -92,23 +103,17 @@ ''; }; - in with pkgs; rec { + in { # Developement shell with only dev tools devShells = { default = mkDevShell pkgs.haskellPackages; - shell-ghc90 = mkDevShell pkgs.haskell.packages.ghc90; - shell-ghc92 = mkDevShell pkgs.haskell.packages.ghc92; - shell-ghc94 = mkDevShell pkgs.haskell.packages.ghc94; shell-ghc96 = mkDevShell pkgs.haskell.packages.ghc96; + shell-ghc98 = mkDevShell pkgs.haskell.packages.ghc98; + shell-ghc910 = mkDevShell pkgs.haskell.packages.ghc910; + shell-ghc912 = mkDevShell pkgs.haskell.packages.ghc912; }; - packages = { - docs = docs; - }; - - # The attributes for the default shell and package changed in recent versions of Nix, - # these are here for backwards compatibility with the old versions. - devShell = devShells.default; + packages = { inherit docs; }; }); nixConfig = { diff --git a/ghcide-bench/ghcide-bench.cabal b/ghcide-bench/ghcide-bench.cabal deleted file mode 100644 index c26665da9a..0000000000 --- a/ghcide-bench/ghcide-bench.cabal +++ /dev/null @@ -1,140 +0,0 @@ -cabal-version: 3.0 -build-type: Simple -category: Development -name: ghcide-bench -version: 2.4.0.0 -license: Apache-2.0 -license-file: LICENSE -author: The Haskell IDE team -maintainer: pepeiborra@gmail.com -copyright: The Haskell IDE team -synopsis: An LSP client for running performance experiments on HLS -description: An LSP client for running performance experiments on HLS -homepage: https://github.com/haskell/haskell-language-server/tree/master/ghcide#readme -bug-reports: https://github.com/haskell/haskell-language-server/issues -tested-with: GHC == 9.0.2 || == 9.2.5 - -source-repository head - type: git - location: https://github.com/haskell/haskell-language-server.git - -executable ghcide-bench - default-language: Haskell2010 - build-depends: - aeson, - base, - bytestring, - containers, - data-default, - directory, - extra, - filepath, - hls-plugin-api, - lens, - ghcide-bench, - lsp-test, - lsp-types, - optparse-applicative, - process, - safe-exceptions, - hls-graph, - shake, - tasty-hunit >= 0.10, - text - hs-source-dirs: exe - ghc-options: -threaded -Wall -Wno-name-shadowing -rtsopts - main-is: Main.hs - default-extensions: - BangPatterns - DeriveFunctor - DeriveGeneric - FlexibleContexts - GeneralizedNewtypeDeriving - LambdaCase - NamedFieldPuns - OverloadedStrings - RecordWildCards - ScopedTypeVariables - StandaloneDeriving - TupleSections - TypeApplications - ViewPatterns - -library - default-language: Haskell2010 - hs-source-dirs: src - ghc-options: -Wall -Wno-name-shadowing - exposed-modules: - Experiments.Types - Experiments - build-depends: - aeson, - async, - base == 4.*, - binary, - bytestring, - deepseq, - directory, - extra, - filepath, - ghcide, - ghcide-test-utils, - hashable, - lens, - lsp-test, - lsp-types, - optparse-applicative, - parser-combinators, - process, - safe-exceptions, - shake, - text, - row-types - default-extensions: - BangPatterns - DeriveFunctor - DeriveGeneric - FlexibleContexts - GeneralizedNewtypeDeriving - LambdaCase - NamedFieldPuns - RecordWildCards - ScopedTypeVariables - StandaloneDeriving - TupleSections - TypeApplications - ViewPatterns - -test-suite test - type: exitcode-stdio-1.0 - default-language: Haskell2010 - build-tool-depends: - ghcide:ghcide, - implicit-hie:gen-hie - main-is: Main.hs - hs-source-dirs: test - ghc-options: -Wunused-packages - ghc-options: -threaded -Wall - build-depends: - base, - extra, - ghcide-bench, - lsp-test ^>= 0.16, - tasty, - tasty-hunit >= 0.10, - tasty-rerun, - default-extensions: - BangPatterns - DeriveFunctor - DeriveGeneric - FlexibleContexts - GeneralizedNewtypeDeriving - LambdaCase - NamedFieldPuns - OverloadedStrings - RecordWildCards - ScopedTypeVariables - StandaloneDeriving - TupleSections - TypeApplications - ViewPatterns diff --git a/ghcide-bench/src/Experiments.hs b/ghcide-bench/src/Experiments.hs index 1a8614e1e9..c53ffd0a7c 100644 --- a/ghcide-bench/src/Experiments.hs +++ b/ghcide-bench/src/Experiments.hs @@ -1,12 +1,8 @@ -{-# LANGUAGE ConstraintKinds #-} -{-# LANGUAGE DataKinds #-} -{-# LANGUAGE ExistentialQuantification #-} -{-# LANGUAGE GADTs #-} -{-# LANGUAGE ImplicitParams #-} -{-# LANGUAGE ImpredicativeTypes #-} -{-# LANGUAGE OverloadedLabels #-} -{-# LANGUAGE OverloadedStrings #-} -{-# LANGUAGE PolyKinds #-} +{-# LANGUAGE DataKinds #-} +{-# LANGUAGE GADTs #-} +{-# LANGUAGE ImplicitParams #-} +{-# LANGUAGE ImpredicativeTypes #-} +{-# LANGUAGE OverloadedStrings #-} {-# OPTIONS_GHC -Wno-deprecations -Wno-unticked-promoted-constructors #-} module Experiments @@ -29,12 +25,12 @@ import Control.Applicative.Combinators (skipManyTill) import Control.Concurrent.Async (withAsync) import Control.Exception.Safe (IOException, handleAny, try) -import Control.Lens (_Just, (&), (.~), (^.)) +import Control.Lens (_Just, (&), (.~), (^.), + (^?)) import Control.Lens.Extras (is) import Control.Monad.Extra (allM, forM, forM_, forever, unless, void, when, whenJust, (&&^)) -import Control.Monad.Fail (MonadFail) import Control.Monad.IO.Class import Data.Aeson (Value (Null), eitherDecodeStrict', @@ -46,14 +42,12 @@ import Data.Either (fromRight) import Data.List import Data.Maybe import Data.Proxy -import Data.Row hiding (switch) import Data.Text (Text) import qualified Data.Text as T import Data.Version import Development.IDE.Plugin.Test import Development.IDE.Test.Diagnostic -import Development.Shake (CmdOption (Cwd, FileStdout), - cmd_) +import Development.Shake (CmdOption (Cwd), cmd_) import Experiments.Types import Language.LSP.Protocol.Capabilities import qualified Language.LSP.Protocol.Lens as L @@ -75,15 +69,19 @@ import Text.Printf charEdit :: Position -> TextDocumentContentChangeEvent charEdit p = - TextDocumentContentChangeEvent $ InL $ #range .== Range p p - .+ #rangeLength .== Nothing - .+ #text .== "a" + TextDocumentContentChangeEvent $ InL TextDocumentContentChangePartial + { _range = Range p p + , _rangeLength = Nothing + , _text = "a" + } headerEdit :: TextDocumentContentChangeEvent headerEdit = - TextDocumentContentChangeEvent $ InL $ #range .== Range (Position 0 0) (Position 0 0) - .+ #rangeLength .== Nothing - .+ #text .== "-- header comment \n" + TextDocumentContentChangeEvent $ InL TextDocumentContentChangePartial + { _range = Range (Position 0 0) (Position 0 0) + , _rangeLength = Nothing + , _text = "-- header comment \n" + } data DocumentPositions = DocumentPositions { -- | A position that can be used to generate non null goto-def and completion responses @@ -104,7 +102,19 @@ allWithIdentifierPos f docs = case applicableDocs of experiments :: HasConfig => [Bench] experiments = - [ --------------------------------------------------------------------------------------- + [ + bench "semanticTokens" $ \docs -> do + liftIO $ putStrLn "Starting semanticTokens" + r <- forM docs $ \DocumentPositions{..} -> do + changeDoc doc [charEdit stringLiteralP] + waitForProgressStart + waitForProgressDone + tks <- getSemanticTokens doc + case tks ^? LSP._L of + Just _ -> return True + Nothing -> return False + return $ and r, + --------------------------------------------------------------------------------------- bench "hover" $ allWithIdentifierPos $ \DocumentPositions{..} -> isJust <$> getHover doc (fromJust identifierP), --------------------------------------------------------------------------------------- @@ -119,7 +129,7 @@ experiments = (\docs -> do hieYamlUri <- getDocUri "hie.yaml" liftIO $ appendFile (fromJust $ uriToFilePath hieYamlUri) "##\n" - sendNotification SMethod_WorkspaceDidChangeWatchedFiles $ DidChangeWatchedFilesParams $ + sendNotification SMethod_WorkspaceDidChangeWatchedFiles $ DidChangeWatchedFilesParams [ FileEvent hieYamlUri FileChangeType_Changed ] flip allWithIdentifierPos docs $ \DocumentPositions{..} -> isJust <$> getHover doc (fromJust identifierP) ), @@ -201,7 +211,7 @@ experiments = ( \docs -> do hieYamlUri <- getDocUri "hie.yaml" liftIO $ appendFile (fromJust $ uriToFilePath hieYamlUri) "##\n" - sendNotification SMethod_WorkspaceDidChangeWatchedFiles $ DidChangeWatchedFilesParams $ + sendNotification SMethod_WorkspaceDidChangeWatchedFiles $ DidChangeWatchedFilesParams [ FileEvent hieYamlUri FileChangeType_Changed ] waitForProgressStart waitForProgressStart @@ -232,9 +242,11 @@ experiments = benchWithSetup "hole fit suggestions" ( mapM_ $ \DocumentPositions{..} -> do - let edit =TextDocumentContentChangeEvent $ InL $ #range .== Range bottom bottom - .+ #rangeLength .== Nothing - .+ #text .== t + let edit = TextDocumentContentChangeEvent $ InL TextDocumentContentChangePartial + { _range = Range bottom bottom + , _rangeLength = Nothing + , _text = t + } bottom = Position maxBound 0 t = T.unlines ["" @@ -254,9 +266,70 @@ experiments = flip allM docs $ \DocumentPositions{..} -> do bottom <- pred . length . T.lines <$> documentContents doc diags <- getCurrentDiagnostics doc - case requireDiagnostic diags (DiagnosticSeverity_Error, (fromIntegral bottom, 8), "Found hole", Nothing) of + case requireDiagnostic diags (DiagnosticSeverity_Error, (fromIntegral bottom, 8), "Found hole", Just "GHC-88464", Nothing) of Nothing -> pure True Just _err -> pure False + ), + --------------------------------------------------------------------------------------- + benchWithSetup + "eval execute single-line code lens" + ( mapM_ $ \DocumentPositions{..} -> do + let edit = TextDocumentContentChangeEvent $ InL TextDocumentContentChangePartial + { _range = Range bottom bottom + , _rangeLength = Nothing + , _text = t + } + bottom = Position maxBound 0 + t = T.unlines + [ "" + , "-- >>> 1 + 2" + ] + changeDoc doc [edit] + ) + ( \docs -> do + not . null <$> forM docs (\DocumentPositions{..} -> do + lenses <- getCodeLenses doc + forM_ lenses $ \case + CodeLens { _command = Just cmd } -> do + executeCommand cmd + waitForProgressStart + waitForProgressDone + _ -> return () + ) + ), + --------------------------------------------------------------------------------------- + benchWithSetup + "eval execute multi-line code lens" + ( mapM_ $ \DocumentPositions{..} -> do + let edit = TextDocumentContentChangeEvent $ InL TextDocumentContentChangePartial + { _range = Range bottom bottom + , _rangeLength = Nothing + , _text = t + } + bottom = Position maxBound 0 + t = T.unlines + [ "" + , "data T = A | B | C | D" + , " deriving (Show, Eq, Ord, Bounded, Enum)" + , "" + , "{-" + , ">>> import Data.List (nub)" + , ">>> xs = ([minBound..maxBound] ++ [minBound..maxBound] :: [T])" + , ">>> nub xs" + , "-}" + ] + changeDoc doc [edit] + ) + ( \docs -> do + not . null <$> forM docs (\DocumentPositions{..} -> do + lenses <- getCodeLenses doc + forM_ lenses $ \case + CodeLens { _command = Just cmd } -> do + executeCommand cmd + waitForProgressStart + waitForProgressDone + _ -> return () + ) ) ] where hasDefinitions (InL (Definition (InL _))) = True @@ -320,7 +393,7 @@ versionP = maybeReader $ extract . readP_to_S parseVersion extract parses = listToMaybe [ res | (res,"") <- parses] output :: (MonadIO m, HasConfig) => String -> m () -output = if quiet?config then (\_ -> pure ()) else liftIO . putStrLn +output = if quiet ?config then (\_ -> pure ()) else liftIO . putStrLn --------------------------------------------------------------------------------------- @@ -484,9 +557,9 @@ runBenchmarksFun dir allBenchmarks = do ] ++ ["--ot-memory-profiling" | Just _ <- [otMemoryProfiling ?config]] lspTestCaps = - fullCaps + fullLatestClientCaps & (L.window . _Just) .~ WindowClientCapabilities (Just True) Nothing Nothing - & (L.textDocument . _Just . L.codeAction . _Just . L.resolveSupport . _Just) .~ (#properties .== ["edit"]) + & (L.textDocument . _Just . L.codeAction . _Just . L.resolveSupport . _Just) .~ (ClientCodeActionResolveOptions ["edit"]) & (L.textDocument . _Just . L.codeAction . _Just . L.dataSupport . _Just) .~ True showMs :: Seconds -> String @@ -598,15 +671,25 @@ callCommandLogging cmd = do output cmd callCommand cmd +simpleCabalCradleContent :: String +simpleCabalCradleContent = "cradle:\n cabal:\n" + +simpleStackCradleContent :: String +simpleStackCradleContent = "cradle:\n stack:\n" + +-- | Setup the benchmark +-- we need to create a hie.yaml file for the examples +-- or the hie.yaml file would be searched in the parent directories recursively +-- implicit-hie is error prone for the example test `lsp-types-2.1.1.0` +-- we are using the simpleCabalCradleContent for the hie.yaml file instead. +-- it works if we have cabal > 3.2. setup :: HasConfig => IO SetupResult setup = do --- when alreadyExists $ removeDirectoryRecursive examplesPath benchDir <- case exampleDetails(example ?config) of ExamplePath examplePath -> do let hieYamlPath = examplePath "hie.yaml" alreadyExists <- doesFileExist hieYamlPath - unless alreadyExists $ - cmd_ (Cwd examplePath) (FileStdout hieYamlPath) ("gen-hie"::String) + unless alreadyExists $ writeFile hieYamlPath simpleCabalCradleContent return examplePath ExampleScript examplePath' scriptArgs -> do let exampleDir = examplesPath exampleName (example ?config) @@ -617,8 +700,8 @@ setup = do cmd_ (Cwd exampleDir) examplePath scriptArgs let hieYamlPath = exampleDir "hie.yaml" alreadyExists <- doesFileExist hieYamlPath - unless alreadyExists $ - cmd_ (Cwd exampleDir) (FileStdout hieYamlPath) ("gen-hie"::String) + unless alreadyExists $ writeFile hieYamlPath simpleCabalCradleContent + return exampleDir ExampleHackage ExamplePackage{..} -> do let path = examplesPath package @@ -631,7 +714,7 @@ setup = do let cabalVerbosity = "-v" ++ show (fromEnum (verbose ?config)) callCommandLogging $ "cabal get " <> cabalVerbosity <> " " <> package <> " -d " <> examplesPath let hieYamlPath = path "hie.yaml" - cmd_ (Cwd path) (FileStdout hieYamlPath) ("gen-hie"::String) + writeFile hieYamlPath simpleCabalCradleContent -- Need this in case there is a parent cabal.project somewhere writeFile (path "cabal.project") @@ -659,13 +742,12 @@ setup = do ,"compiler"] ] ) - - cmd_ (Cwd path) (FileStdout hieYamlPath) ("gen-hie"::String) ["--stack"::String] + writeFile hieYamlPath simpleStackCradleContent return path whenJust (shakeProfiling ?config) $ createDirectoryIfMissing True - let cleanUp = case exampleDetails(example ?config) of + let cleanUp = case exampleDetails (example ?config) of ExampleHackage _ -> removeDirectoryRecursive examplesPath ExampleScript _ _ -> removeDirectoryRecursive examplesPath ExamplePath _ -> return () @@ -681,10 +763,12 @@ setupDocumentContents config = -- Setup the special positions used by the experiments lastLine <- fromIntegral . length . T.lines <$> documentContents doc - changeDoc doc [TextDocumentContentChangeEvent $ InL - $ #range .== Range (Position lastLine 0) (Position lastLine 0) - .+ #rangeLength .== Nothing - .+ #text .== T.unlines [ "_hygienic = \"hygienic\"" ]] + changeDoc doc [TextDocumentContentChangeEvent $ InL TextDocumentContentChangePartial + { _range = Range (Position lastLine 0) (Position lastLine 0) + , _rangeLength = Nothing + , _text = T.unlines [ "_hygienic = \"hygienic\"" ] + } + ] let -- Points to a string in the target file, -- convenient for hygienic edits @@ -702,7 +786,7 @@ setupDocumentContents config = findEndOfImports :: [DocumentSymbol] -> Maybe Position findEndOfImports (DocumentSymbol{_kind = SymbolKind_Module, _name = "imports", _range} : _) = Just $ Position (succ $ _line $ _end _range) 4 -findEndOfImports [DocumentSymbol{_kind = SymbolKind_File, _children = Just (cc)}] = +findEndOfImports [DocumentSymbol{_kind = SymbolKind_File, _children = Just cc}] = findEndOfImports cc findEndOfImports (DocumentSymbol{_range} : _) = Just $ _range ^. L.start @@ -758,27 +842,25 @@ searchSymbol doc@TextDocumentIdentifier{_uri} fileContents pos = do not . null <$> getCompletions doc pos -getBuildKeysBuilt :: Session (Either ResponseError [T.Text]) +getBuildKeysBuilt :: Session (Either (TResponseError @ClientToServer (Method_CustomMethod "test")) [T.Text]) getBuildKeysBuilt = tryCallTestPlugin GetBuildKeysBuilt -getBuildKeysVisited :: Session (Either ResponseError [T.Text]) +getBuildKeysVisited :: Session (Either (TResponseError @ClientToServer (Method_CustomMethod "test")) [T.Text]) getBuildKeysVisited = tryCallTestPlugin GetBuildKeysVisited -getBuildKeysChanged :: Session (Either ResponseError [T.Text]) +getBuildKeysChanged :: Session (Either (TResponseError @ClientToServer (Method_CustomMethod "test")) [T.Text]) getBuildKeysChanged = tryCallTestPlugin GetBuildKeysChanged -getBuildEdgesCount :: Session (Either ResponseError Int) +getBuildEdgesCount :: Session (Either (TResponseError @ClientToServer (Method_CustomMethod "test")) Int) getBuildEdgesCount = tryCallTestPlugin GetBuildEdgesCount -getRebuildsCount :: Session (Either ResponseError Int) +getRebuildsCount :: Session (Either (TResponseError @ClientToServer (Method_CustomMethod "test")) Int) getRebuildsCount = tryCallTestPlugin GetRebuildsCount --- Copy&paste from ghcide/test/Development.IDE.Test getStoredKeys :: Session [Text] getStoredKeys = callTestPlugin GetStoredKeys --- Copy&paste from ghcide/test/Development.IDE.Test -tryCallTestPlugin :: (A.FromJSON b) => TestRequest -> Session (Either ResponseError b) +tryCallTestPlugin :: (A.FromJSON b) => TestRequest -> Session (Either (TResponseError @ClientToServer (Method_CustomMethod "test")) b) tryCallTestPlugin cmd = do let cm = SMethod_CustomMethod (Proxy @"test") waitId <- sendRequest cm (A.toJSON cmd) @@ -789,10 +871,9 @@ tryCallTestPlugin cmd = do A.Success a -> Right a A.Error e -> error e --- Copy&paste from ghcide/test/Development.IDE.Test callTestPlugin :: (A.FromJSON b) => TestRequest -> Session b callTestPlugin cmd = do res <- tryCallTestPlugin cmd case res of - Left (ResponseError t err _) -> error $ show t <> ": " <> T.unpack err - Right a -> pure a + Left (TResponseError t err _) -> error $ show t <> ": " <> T.unpack err + Right a -> pure a diff --git a/ghcide-bench/test/Main.hs b/ghcide-bench/test/Main.hs index beb5066ddb..a58016ab2b 100644 --- a/ghcide-bench/test/Main.hs +++ b/ghcide-bench/test/Main.hs @@ -7,7 +7,6 @@ {-# LANGUAGE DuplicateRecordFields #-} {-# LANGUAGE GADTs #-} {-# LANGUAGE ImplicitParams #-} -{-# LANGUAGE PolyKinds #-} {-# OPTIONS_GHC -Wno-deprecations -Wno-unticked-promoted-constructors #-} module Main (main) where @@ -42,7 +41,7 @@ benchmarkTests = ] runInDir :: FilePath -> Session a -> IO a -runInDir dir = runSessionWithConfig defaultConfig cmd fullCaps dir +runInDir dir = runSessionWithConfig defaultConfig cmd fullLatestClientCaps dir where -- TODO use HLS instead of ghcide cmd = "ghcide --lsp --test --verbose -j2 --cwd " <> dir diff --git a/ghcide/test/LICENSE b/ghcide-test/LICENSE similarity index 100% rename from ghcide/test/LICENSE rename to ghcide-test/LICENSE diff --git a/ghcide/test/data/TH/THA.hs b/ghcide-test/data/TH/THA.hs similarity index 100% rename from ghcide/test/data/TH/THA.hs rename to ghcide-test/data/TH/THA.hs diff --git a/ghcide/test/data/TH/THB.hs b/ghcide-test/data/TH/THB.hs similarity index 100% rename from ghcide/test/data/TH/THB.hs rename to ghcide-test/data/TH/THB.hs diff --git a/ghcide/test/data/TH/THC.hs b/ghcide-test/data/TH/THC.hs similarity index 100% rename from ghcide/test/data/TH/THC.hs rename to ghcide-test/data/TH/THC.hs diff --git a/ghcide/test/data/TH/hie.yaml b/ghcide-test/data/TH/hie.yaml similarity index 100% rename from ghcide/test/data/TH/hie.yaml rename to ghcide-test/data/TH/hie.yaml diff --git a/ghcide/test/data/THCoreFile/THA.hs b/ghcide-test/data/THCoreFile/THA.hs similarity index 100% rename from ghcide/test/data/THCoreFile/THA.hs rename to ghcide-test/data/THCoreFile/THA.hs diff --git a/ghcide/test/data/THCoreFile/THB.hs b/ghcide-test/data/THCoreFile/THB.hs similarity index 100% rename from ghcide/test/data/THCoreFile/THB.hs rename to ghcide-test/data/THCoreFile/THB.hs diff --git a/ghcide/test/data/THCoreFile/THC.hs b/ghcide-test/data/THCoreFile/THC.hs similarity index 100% rename from ghcide/test/data/THCoreFile/THC.hs rename to ghcide-test/data/THCoreFile/THC.hs diff --git a/ghcide/test/data/THCoreFile/hie.yaml b/ghcide-test/data/THCoreFile/hie.yaml similarity index 100% rename from ghcide/test/data/THCoreFile/hie.yaml rename to ghcide-test/data/THCoreFile/hie.yaml diff --git a/ghcide/test/data/THLoading/A.hs b/ghcide-test/data/THLoading/A.hs similarity index 100% rename from ghcide/test/data/THLoading/A.hs rename to ghcide-test/data/THLoading/A.hs diff --git a/ghcide/test/data/THLoading/B.hs b/ghcide-test/data/THLoading/B.hs similarity index 100% rename from ghcide/test/data/THLoading/B.hs rename to ghcide-test/data/THLoading/B.hs diff --git a/ghcide/test/data/THLoading/THA.hs b/ghcide-test/data/THLoading/THA.hs similarity index 100% rename from ghcide/test/data/THLoading/THA.hs rename to ghcide-test/data/THLoading/THA.hs diff --git a/ghcide/test/data/THLoading/THB.hs b/ghcide-test/data/THLoading/THB.hs similarity index 100% rename from ghcide/test/data/THLoading/THB.hs rename to ghcide-test/data/THLoading/THB.hs diff --git a/ghcide/test/data/THLoading/hie.yaml b/ghcide-test/data/THLoading/hie.yaml similarity index 100% rename from ghcide/test/data/THLoading/hie.yaml rename to ghcide-test/data/THLoading/hie.yaml diff --git a/ghcide/test/data/THNewName/A.hs b/ghcide-test/data/THNewName/A.hs similarity index 100% rename from ghcide/test/data/THNewName/A.hs rename to ghcide-test/data/THNewName/A.hs diff --git a/ghcide/test/data/THNewName/B.hs b/ghcide-test/data/THNewName/B.hs similarity index 100% rename from ghcide/test/data/THNewName/B.hs rename to ghcide-test/data/THNewName/B.hs diff --git a/ghcide/test/data/THNewName/C.hs b/ghcide-test/data/THNewName/C.hs similarity index 100% rename from ghcide/test/data/THNewName/C.hs rename to ghcide-test/data/THNewName/C.hs diff --git a/ghcide/test/data/THNewName/hie.yaml b/ghcide-test/data/THNewName/hie.yaml similarity index 100% rename from ghcide/test/data/THNewName/hie.yaml rename to ghcide-test/data/THNewName/hie.yaml diff --git a/ghcide/test/data/THUnboxed/THA.hs b/ghcide-test/data/THUnboxed/THA.hs similarity index 100% rename from ghcide/test/data/THUnboxed/THA.hs rename to ghcide-test/data/THUnboxed/THA.hs diff --git a/ghcide/test/data/THUnboxed/THB.hs b/ghcide-test/data/THUnboxed/THB.hs similarity index 100% rename from ghcide/test/data/THUnboxed/THB.hs rename to ghcide-test/data/THUnboxed/THB.hs diff --git a/ghcide/test/data/THUnboxed/THC.hs b/ghcide-test/data/THUnboxed/THC.hs similarity index 100% rename from ghcide/test/data/THUnboxed/THC.hs rename to ghcide-test/data/THUnboxed/THC.hs diff --git a/ghcide/test/data/THUnboxed/hie.yaml b/ghcide-test/data/THUnboxed/hie.yaml similarity index 100% rename from ghcide/test/data/THUnboxed/hie.yaml rename to ghcide-test/data/THUnboxed/hie.yaml diff --git a/ghcide/test/data/boot/A.hs b/ghcide-test/data/boot/A.hs similarity index 100% rename from ghcide/test/data/boot/A.hs rename to ghcide-test/data/boot/A.hs diff --git a/ghcide/test/data/boot/A.hs-boot b/ghcide-test/data/boot/A.hs-boot similarity index 100% rename from ghcide/test/data/boot/A.hs-boot rename to ghcide-test/data/boot/A.hs-boot diff --git a/ghcide/test/data/boot/B.hs b/ghcide-test/data/boot/B.hs similarity index 100% rename from ghcide/test/data/boot/B.hs rename to ghcide-test/data/boot/B.hs diff --git a/ghcide/test/data/boot/C.hs b/ghcide-test/data/boot/C.hs similarity index 100% rename from ghcide/test/data/boot/C.hs rename to ghcide-test/data/boot/C.hs diff --git a/ghcide/test/data/boot/hie.yaml b/ghcide-test/data/boot/hie.yaml similarity index 100% rename from ghcide/test/data/boot/hie.yaml rename to ghcide-test/data/boot/hie.yaml diff --git a/ghcide/test/data/boot2/A.hs b/ghcide-test/data/boot2/A.hs similarity index 100% rename from ghcide/test/data/boot2/A.hs rename to ghcide-test/data/boot2/A.hs diff --git a/ghcide/test/data/boot2/B.hs b/ghcide-test/data/boot2/B.hs similarity index 100% rename from ghcide/test/data/boot2/B.hs rename to ghcide-test/data/boot2/B.hs diff --git a/ghcide/test/data/boot2/B.hs-boot b/ghcide-test/data/boot2/B.hs-boot similarity index 100% rename from ghcide/test/data/boot2/B.hs-boot rename to ghcide-test/data/boot2/B.hs-boot diff --git a/ghcide/test/data/boot2/C.hs b/ghcide-test/data/boot2/C.hs similarity index 100% rename from ghcide/test/data/boot2/C.hs rename to ghcide-test/data/boot2/C.hs diff --git a/ghcide/test/data/boot2/D.hs b/ghcide-test/data/boot2/D.hs similarity index 100% rename from ghcide/test/data/boot2/D.hs rename to ghcide-test/data/boot2/D.hs diff --git a/ghcide/test/data/boot2/E.hs b/ghcide-test/data/boot2/E.hs similarity index 100% rename from ghcide/test/data/boot2/E.hs rename to ghcide-test/data/boot2/E.hs diff --git a/ghcide/test/data/boot2/hie.yaml b/ghcide-test/data/boot2/hie.yaml similarity index 100% rename from ghcide/test/data/boot2/hie.yaml rename to ghcide-test/data/boot2/hie.yaml diff --git a/ghcide/test/data/cabal-exe/a/a.cabal b/ghcide-test/data/cabal-exe/a/a.cabal similarity index 100% rename from ghcide/test/data/cabal-exe/a/a.cabal rename to ghcide-test/data/cabal-exe/a/a.cabal diff --git a/ghcide/test/data/cabal-exe/a/src/Main.hs b/ghcide-test/data/cabal-exe/a/src/Main.hs similarity index 100% rename from ghcide/test/data/cabal-exe/a/src/Main.hs rename to ghcide-test/data/cabal-exe/a/src/Main.hs diff --git a/ghcide/test/data/cabal-exe/cabal.project b/ghcide-test/data/cabal-exe/cabal.project similarity index 100% rename from ghcide/test/data/cabal-exe/cabal.project rename to ghcide-test/data/cabal-exe/cabal.project diff --git a/ghcide/test/data/cabal-exe/hie.yaml b/ghcide-test/data/cabal-exe/hie.yaml similarity index 100% rename from ghcide/test/data/cabal-exe/hie.yaml rename to ghcide-test/data/cabal-exe/hie.yaml diff --git a/ghcide/test/data/hover/Bar.hs b/ghcide-test/data/hover/Bar.hs similarity index 100% rename from ghcide/test/data/hover/Bar.hs rename to ghcide-test/data/hover/Bar.hs diff --git a/ghcide/test/data/hover/Foo.hs b/ghcide-test/data/hover/Foo.hs similarity index 100% rename from ghcide/test/data/hover/Foo.hs rename to ghcide-test/data/hover/Foo.hs diff --git a/ghcide/test/data/hover/GotoHover.hs b/ghcide-test/data/hover/GotoHover.hs similarity index 100% rename from ghcide/test/data/hover/GotoHover.hs rename to ghcide-test/data/hover/GotoHover.hs diff --git a/ghcide-test/data/hover/GotoImplementation.hs b/ghcide-test/data/hover/GotoImplementation.hs new file mode 100644 index 0000000000..12038857c6 --- /dev/null +++ b/ghcide-test/data/hover/GotoImplementation.hs @@ -0,0 +1,30 @@ +{-# LANGUAGE GADTs, GeneralisedNewtypeDeriving, DerivingStrategies #-} +{-# OPTIONS_GHC -Wno-missing-methods #-} +module GotoImplementation where + +data AAA = AAA +instance Num AAA where +aaa :: Num x => x +aaa = 1 +aaa1 :: AAA = aaa + +class BBB a where + bbb :: a -> a +instance BBB AAA where + bbb = const AAA +bbbb :: AAA +bbbb = bbb AAA + +ccc :: Show a => a -> String +ccc d = show d + +newtype Q k = Q k + deriving newtype (Eq, Show) +ddd :: (Show k, Eq k) => k -> String +ddd k = if Q k == Q k then show k else "" +ddd1 = ddd (Q 0) + +data GadtTest a where + GadtTest :: Int -> GadtTest Int +printUsingEvidence :: Show a => GadtTest a -> String +printUsingEvidence (GadtTest i) = show i diff --git a/ghcide/test/data/hover/RecordDotSyntax.hs b/ghcide-test/data/hover/RecordDotSyntax.hs similarity index 100% rename from ghcide/test/data/hover/RecordDotSyntax.hs rename to ghcide-test/data/hover/RecordDotSyntax.hs diff --git a/ghcide/test/data/hover/hie.yaml b/ghcide-test/data/hover/hie.yaml similarity index 64% rename from ghcide/test/data/hover/hie.yaml rename to ghcide-test/data/hover/hie.yaml index e2b3e97c5d..de7cc991cc 100644 --- a/ghcide/test/data/hover/hie.yaml +++ b/ghcide-test/data/hover/hie.yaml @@ -1 +1 @@ -cradle: {direct: {arguments: ["Foo", "Bar", "GotoHover", "RecordDotSyntax"]}} +cradle: {direct: {arguments: ["Foo", "Bar", "GotoHover", "RecordDotSyntax", "GotoImplementation"]}} diff --git a/ghcide/test/data/ignore-fatal/IgnoreFatal.hs b/ghcide-test/data/ignore-fatal/IgnoreFatal.hs similarity index 74% rename from ghcide/test/data/ignore-fatal/IgnoreFatal.hs rename to ghcide-test/data/ignore-fatal/IgnoreFatal.hs index 77b11c5bb3..b73787f166 100644 --- a/ghcide/test/data/ignore-fatal/IgnoreFatal.hs +++ b/ghcide-test/data/ignore-fatal/IgnoreFatal.hs @@ -1,7 +1,7 @@ -- "missing signature" is declared a fatal warning in the cabal file, -- but is ignored in this module. -{-# OPTIONS_GHC -fno-warn-missing-signatures #-} +{-# OPTIONS_GHC -Wno-missing-signatures #-} module IgnoreFatal where diff --git a/ghcide/test/data/ignore-fatal/cabal.project b/ghcide-test/data/ignore-fatal/cabal.project similarity index 100% rename from ghcide/test/data/ignore-fatal/cabal.project rename to ghcide-test/data/ignore-fatal/cabal.project diff --git a/ghcide/test/data/ignore-fatal/hie.yaml b/ghcide-test/data/ignore-fatal/hie.yaml similarity index 100% rename from ghcide/test/data/ignore-fatal/hie.yaml rename to ghcide-test/data/ignore-fatal/hie.yaml diff --git a/ghcide/test/data/ignore-fatal/ignore-fatal.cabal b/ghcide-test/data/ignore-fatal/ignore-fatal.cabal similarity index 100% rename from ghcide/test/data/ignore-fatal/ignore-fatal.cabal rename to ghcide-test/data/ignore-fatal/ignore-fatal.cabal diff --git a/ghcide-test/data/multi-unit-reexport/a-1.0.0-inplace b/ghcide-test/data/multi-unit-reexport/a-1.0.0-inplace new file mode 100644 index 0000000000..a54ea9bc4b --- /dev/null +++ b/ghcide-test/data/multi-unit-reexport/a-1.0.0-inplace @@ -0,0 +1,18 @@ +-this-package-name +a +-working-dir +a +-fbuilding-cabal-package +-O0 +-i. +-this-unit-id +a-1.0.0-inplace +-hide-all-packages +-Wmissing-home-modules +-no-user-package-db +-package +base +-package +text +-XHaskell98 +A diff --git a/ghcide-test/data/multi-unit-reexport/a/A.hs b/ghcide-test/data/multi-unit-reexport/a/A.hs new file mode 100644 index 0000000000..9a7d7e33c9 --- /dev/null +++ b/ghcide-test/data/multi-unit-reexport/a/A.hs @@ -0,0 +1,3 @@ +module A(foo) where +import Data.Text +foo = () diff --git a/ghcide-test/data/multi-unit-reexport/b-1.0.0-inplace b/ghcide-test/data/multi-unit-reexport/b-1.0.0-inplace new file mode 100644 index 0000000000..d656a2539b --- /dev/null +++ b/ghcide-test/data/multi-unit-reexport/b-1.0.0-inplace @@ -0,0 +1,21 @@ +-this-package-name +b +-working-dir +b +-fbuilding-cabal-package +-O0 +-i +-i. +-this-unit-id +b-1.0.0-inplace +-hide-all-packages +-Wmissing-home-modules +-no-user-package-db +-package-id +a-1.0.0-inplace +-reexported-module +A +-package +base +-XHaskell98 +B diff --git a/ghcide/test/data/multi/b/B.hs b/ghcide-test/data/multi-unit-reexport/b/B.hs similarity index 100% rename from ghcide/test/data/multi/b/B.hs rename to ghcide-test/data/multi-unit-reexport/b/B.hs diff --git a/ghcide-test/data/multi-unit-reexport/c-1.0.0-inplace b/ghcide-test/data/multi-unit-reexport/c-1.0.0-inplace new file mode 100644 index 0000000000..e60a95eda0 --- /dev/null +++ b/ghcide-test/data/multi-unit-reexport/c-1.0.0-inplace @@ -0,0 +1,19 @@ +-this-package-name +c +-working-dir +c +-fbuilding-cabal-package +-O0 +-i +-i. +-this-unit-id +c-1.0.0-inplace +-hide-all-packages +-Wmissing-home-modules +-no-user-package-db +-package-id +b-1.0.0-inplace +-package +base +-XHaskell98 +C diff --git a/ghcide-test/data/multi-unit-reexport/c/C.hs b/ghcide-test/data/multi-unit-reexport/c/C.hs new file mode 100644 index 0000000000..1b2d305296 --- /dev/null +++ b/ghcide-test/data/multi-unit-reexport/c/C.hs @@ -0,0 +1,4 @@ +module C(module C) where +import A +import B +cux = foo `seq` qux diff --git a/ghcide-test/data/multi-unit-reexport/cabal.project b/ghcide-test/data/multi-unit-reexport/cabal.project new file mode 100644 index 0000000000..96f52330c9 --- /dev/null +++ b/ghcide-test/data/multi-unit-reexport/cabal.project @@ -0,0 +1,2 @@ +packages: a b c +multi-repl: True diff --git a/ghcide-test/data/multi-unit-reexport/hie.yaml b/ghcide-test/data/multi-unit-reexport/hie.yaml new file mode 100644 index 0000000000..34858b5f64 --- /dev/null +++ b/ghcide-test/data/multi-unit-reexport/hie.yaml @@ -0,0 +1,6 @@ +cradle: + direct: + arguments: ["-unit" ,"@a-1.0.0-inplace" + ,"-unit" ,"@b-1.0.0-inplace" + ,"-unit" ,"@c-1.0.0-inplace" + ] diff --git a/ghcide-test/data/multi-unit/a-1.0.0-inplace b/ghcide-test/data/multi-unit/a-1.0.0-inplace new file mode 100644 index 0000000000..a54ea9bc4b --- /dev/null +++ b/ghcide-test/data/multi-unit/a-1.0.0-inplace @@ -0,0 +1,18 @@ +-this-package-name +a +-working-dir +a +-fbuilding-cabal-package +-O0 +-i. +-this-unit-id +a-1.0.0-inplace +-hide-all-packages +-Wmissing-home-modules +-no-user-package-db +-package +base +-package +text +-XHaskell98 +A diff --git a/ghcide-test/data/multi-unit/a/A.hs b/ghcide-test/data/multi-unit/a/A.hs new file mode 100644 index 0000000000..9a7d7e33c9 --- /dev/null +++ b/ghcide-test/data/multi-unit/a/A.hs @@ -0,0 +1,3 @@ +module A(foo) where +import Data.Text +foo = () diff --git a/ghcide-test/data/multi-unit/b-1.0.0-inplace b/ghcide-test/data/multi-unit/b-1.0.0-inplace new file mode 100644 index 0000000000..fe43e3a92d --- /dev/null +++ b/ghcide-test/data/multi-unit/b-1.0.0-inplace @@ -0,0 +1,20 @@ +-this-package-name +b +-working-dir +b +-fbuilding-cabal-package +-O0 +-i +-i. +-this-unit-id +b-1.0.0-inplace +-hide-all-packages +-Wmissing-home-modules +-no-user-package-db +-package-id +a-1.0.0-inplace +-package +base +-XHaskell98 +-XPackageImports +B diff --git a/ghcide-test/data/multi-unit/b/B.hs b/ghcide-test/data/multi-unit/b/B.hs new file mode 100644 index 0000000000..54c6b874fc --- /dev/null +++ b/ghcide-test/data/multi-unit/b/B.hs @@ -0,0 +1,3 @@ +module B(module B) where +import "a" A +qux = foo diff --git a/ghcide-test/data/multi-unit/c-1.0.0-inplace b/ghcide-test/data/multi-unit/c-1.0.0-inplace new file mode 100644 index 0000000000..7201a40de4 --- /dev/null +++ b/ghcide-test/data/multi-unit/c-1.0.0-inplace @@ -0,0 +1,19 @@ +-this-package-name +c +-working-dir +c +-fbuilding-cabal-package +-O0 +-i +-i. +-this-unit-id +c-1.0.0-inplace +-hide-all-packages +-Wmissing-home-modules +-no-user-package-db +-package-id +a-1.0.0-inplace +-package +base +-XHaskell98 +C diff --git a/ghcide/test/data/multi/c/C.hs b/ghcide-test/data/multi-unit/c/C.hs similarity index 100% rename from ghcide/test/data/multi/c/C.hs rename to ghcide-test/data/multi-unit/c/C.hs diff --git a/ghcide-test/data/multi-unit/cabal.project b/ghcide-test/data/multi-unit/cabal.project new file mode 100644 index 0000000000..96f52330c9 --- /dev/null +++ b/ghcide-test/data/multi-unit/cabal.project @@ -0,0 +1,2 @@ +packages: a b c +multi-repl: True diff --git a/ghcide-test/data/multi-unit/hie.yaml b/ghcide-test/data/multi-unit/hie.yaml new file mode 100644 index 0000000000..34858b5f64 --- /dev/null +++ b/ghcide-test/data/multi-unit/hie.yaml @@ -0,0 +1,6 @@ +cradle: + direct: + arguments: ["-unit" ,"@a-1.0.0-inplace" + ,"-unit" ,"@b-1.0.0-inplace" + ,"-unit" ,"@c-1.0.0-inplace" + ] diff --git a/ghcide/test/data/multi/a/A.hs b/ghcide-test/data/multi/a/A.hs similarity index 100% rename from ghcide/test/data/multi/a/A.hs rename to ghcide-test/data/multi/a/A.hs diff --git a/ghcide/test/data/multi/a/a.cabal b/ghcide-test/data/multi/a/a.cabal similarity index 100% rename from ghcide/test/data/multi/a/a.cabal rename to ghcide-test/data/multi/a/a.cabal diff --git a/ghcide-test/data/multi/b/B.hs b/ghcide-test/data/multi/b/B.hs new file mode 100644 index 0000000000..2c6d4b28a2 --- /dev/null +++ b/ghcide-test/data/multi/b/B.hs @@ -0,0 +1,3 @@ +module B(module B) where +import A +qux = foo diff --git a/ghcide/test/data/multi/b/b.cabal b/ghcide-test/data/multi/b/b.cabal similarity index 100% rename from ghcide/test/data/multi/b/b.cabal rename to ghcide-test/data/multi/b/b.cabal diff --git a/ghcide-test/data/multi/c/C.hs b/ghcide-test/data/multi/c/C.hs new file mode 100644 index 0000000000..b75a7fc3c7 --- /dev/null +++ b/ghcide-test/data/multi/c/C.hs @@ -0,0 +1,3 @@ +module C(module C) where +import A +cux = foo diff --git a/ghcide/test/data/multi/c/c.cabal b/ghcide-test/data/multi/c/c.cabal similarity index 100% rename from ghcide/test/data/multi/c/c.cabal rename to ghcide-test/data/multi/c/c.cabal diff --git a/ghcide/test/data/multi/cabal.project b/ghcide-test/data/multi/cabal.project similarity index 100% rename from ghcide/test/data/multi/cabal.project rename to ghcide-test/data/multi/cabal.project diff --git a/ghcide/test/data/multi/hie.yaml b/ghcide-test/data/multi/hie.yaml similarity index 100% rename from ghcide/test/data/multi/hie.yaml rename to ghcide-test/data/multi/hie.yaml diff --git a/ghcide/test/data/plugin-knownnat/KnownNat.hs b/ghcide-test/data/plugin-knownnat/KnownNat.hs similarity index 100% rename from ghcide/test/data/plugin-knownnat/KnownNat.hs rename to ghcide-test/data/plugin-knownnat/KnownNat.hs diff --git a/ghcide/test/data/plugin-knownnat/cabal.project b/ghcide-test/data/plugin-knownnat/cabal.project similarity index 100% rename from ghcide/test/data/plugin-knownnat/cabal.project rename to ghcide-test/data/plugin-knownnat/cabal.project diff --git a/ghcide/test/data/plugin-knownnat/plugin.cabal b/ghcide-test/data/plugin-knownnat/plugin.cabal similarity index 100% rename from ghcide/test/data/plugin-knownnat/plugin.cabal rename to ghcide-test/data/plugin-knownnat/plugin.cabal diff --git a/ghcide/test/data/recomp/A.hs b/ghcide-test/data/recomp/A.hs similarity index 100% rename from ghcide/test/data/recomp/A.hs rename to ghcide-test/data/recomp/A.hs diff --git a/ghcide/test/data/recomp/B.hs b/ghcide-test/data/recomp/B.hs similarity index 100% rename from ghcide/test/data/recomp/B.hs rename to ghcide-test/data/recomp/B.hs diff --git a/ghcide/test/data/recomp/P.hs b/ghcide-test/data/recomp/P.hs similarity index 100% rename from ghcide/test/data/recomp/P.hs rename to ghcide-test/data/recomp/P.hs diff --git a/ghcide/test/data/recomp/hie.yaml b/ghcide-test/data/recomp/hie.yaml similarity index 100% rename from ghcide/test/data/recomp/hie.yaml rename to ghcide-test/data/recomp/hie.yaml diff --git a/ghcide/test/data/references/Main.hs b/ghcide-test/data/references/Main.hs similarity index 100% rename from ghcide/test/data/references/Main.hs rename to ghcide-test/data/references/Main.hs diff --git a/ghcide/test/data/references/OtherModule.hs b/ghcide-test/data/references/OtherModule.hs similarity index 100% rename from ghcide/test/data/references/OtherModule.hs rename to ghcide-test/data/references/OtherModule.hs diff --git a/ghcide/test/data/references/OtherOtherModule.hs b/ghcide-test/data/references/OtherOtherModule.hs similarity index 100% rename from ghcide/test/data/references/OtherOtherModule.hs rename to ghcide-test/data/references/OtherOtherModule.hs diff --git a/ghcide/test/data/references/References.hs b/ghcide-test/data/references/References.hs similarity index 100% rename from ghcide/test/data/references/References.hs rename to ghcide-test/data/references/References.hs diff --git a/ghcide/test/data/references/hie.yaml b/ghcide-test/data/references/hie.yaml similarity index 100% rename from ghcide/test/data/references/hie.yaml rename to ghcide-test/data/references/hie.yaml diff --git a/ghcide/test/data/rootUri/dirA/Foo.hs b/ghcide-test/data/rootUri/dirA/Foo.hs similarity index 100% rename from ghcide/test/data/rootUri/dirA/Foo.hs rename to ghcide-test/data/rootUri/dirA/Foo.hs diff --git a/ghcide/test/data/rootUri/dirA/foo.cabal b/ghcide-test/data/rootUri/dirA/foo.cabal similarity index 100% rename from ghcide/test/data/rootUri/dirA/foo.cabal rename to ghcide-test/data/rootUri/dirA/foo.cabal diff --git a/ghcide/test/data/rootUri/dirB/Foo.hs b/ghcide-test/data/rootUri/dirB/Foo.hs similarity index 100% rename from ghcide/test/data/rootUri/dirB/Foo.hs rename to ghcide-test/data/rootUri/dirB/Foo.hs diff --git a/ghcide/test/data/rootUri/dirB/foo.cabal b/ghcide-test/data/rootUri/dirB/foo.cabal similarity index 100% rename from ghcide/test/data/rootUri/dirB/foo.cabal rename to ghcide-test/data/rootUri/dirB/foo.cabal diff --git a/ghcide/test/data/symlink/hie.yaml b/ghcide-test/data/symlink/hie.yaml similarity index 100% rename from ghcide/test/data/symlink/hie.yaml rename to ghcide-test/data/symlink/hie.yaml diff --git a/ghcide/test/data/symlink/other_loc/.gitkeep b/ghcide-test/data/symlink/other_loc/.gitkeep similarity index 100% rename from ghcide/test/data/symlink/other_loc/.gitkeep rename to ghcide-test/data/symlink/other_loc/.gitkeep diff --git a/ghcide/test/data/symlink/some_loc/Sym.hs b/ghcide-test/data/symlink/some_loc/Sym.hs similarity index 100% rename from ghcide/test/data/symlink/some_loc/Sym.hs rename to ghcide-test/data/symlink/some_loc/Sym.hs diff --git a/ghcide/test/data/symlink/src/Foo.hs b/ghcide-test/data/symlink/src/Foo.hs similarity index 100% rename from ghcide/test/data/symlink/src/Foo.hs rename to ghcide-test/data/symlink/src/Foo.hs diff --git a/ghcide-test/data/watched-files/reload/reload.cabal b/ghcide-test/data/watched-files/reload/reload.cabal new file mode 100644 index 0000000000..d9d5607a94 --- /dev/null +++ b/ghcide-test/data/watched-files/reload/reload.cabal @@ -0,0 +1,12 @@ +cabal-version: 2.4 +name: reload +version: 0.1.0.0 +author: Lin Jian +maintainer: me@linj.tech +build-type: Simple + +library + exposed-modules: MyLib + build-depends: base + hs-source-dirs: src + default-language: Haskell2010 diff --git a/ghcide-test/data/watched-files/reload/src/MyLib.hs b/ghcide-test/data/watched-files/reload/src/MyLib.hs new file mode 100644 index 0000000000..bbb506d001 --- /dev/null +++ b/ghcide-test/data/watched-files/reload/src/MyLib.hs @@ -0,0 +1,6 @@ +module MyLib (someFunc) where + +import Data.List.Split + +someFunc :: IO () +someFunc = putStrLn "someFunc" diff --git a/ghcide-test/data/working-dir/a/A.hs b/ghcide-test/data/working-dir/a/A.hs new file mode 100644 index 0000000000..5b4f28ba40 --- /dev/null +++ b/ghcide-test/data/working-dir/a/A.hs @@ -0,0 +1,11 @@ +{-# LANGUAGE TemplateHaskell #-} +module A(th_a) where + +import Language.Haskell.TH +import Language.Haskell.TH.Syntax +import Control.Monad.IO.Class + +th_a :: DecsQ +th_a = do + str <- makeRelativeToProject "wdtest" >>= liftIO . readFile + [d| a = $(lift str) |] diff --git a/ghcide-test/data/working-dir/a/B.hs b/ghcide-test/data/working-dir/a/B.hs new file mode 100644 index 0000000000..8563bb0875 --- /dev/null +++ b/ghcide-test/data/working-dir/a/B.hs @@ -0,0 +1,6 @@ +{-# LANGUAGE TemplateHaskell #-} +module B() where + +import A + +$th_a diff --git a/ghcide-test/data/working-dir/a/a.cabal b/ghcide-test/data/working-dir/a/a.cabal new file mode 100644 index 0000000000..1b92d21849 --- /dev/null +++ b/ghcide-test/data/working-dir/a/a.cabal @@ -0,0 +1,11 @@ +name: a +version: 1.0.0 +build-type: Simple +cabal-version: >= 1.2 +extra-source-files: wdtest + +library + build-depends: base, template-haskell + exposed-modules: A B + ghc-options: -Wmissing-signatures + hs-source-dirs: . diff --git a/ghcide-test/data/working-dir/a/wdtest b/ghcide-test/data/working-dir/a/wdtest new file mode 100644 index 0000000000..9daeafb986 --- /dev/null +++ b/ghcide-test/data/working-dir/a/wdtest @@ -0,0 +1 @@ +test diff --git a/ghcide-test/data/working-dir/cabal.project b/ghcide-test/data/working-dir/cabal.project new file mode 100644 index 0000000000..80dfe76da5 --- /dev/null +++ b/ghcide-test/data/working-dir/cabal.project @@ -0,0 +1 @@ +packages: a diff --git a/ghcide-test/data/working-dir/hie.yaml b/ghcide-test/data/working-dir/hie.yaml new file mode 100644 index 0000000000..04cd24395e --- /dev/null +++ b/ghcide-test/data/working-dir/hie.yaml @@ -0,0 +1,2 @@ +cradle: + cabal: diff --git a/ghcide/test/exe/AsyncTests.hs b/ghcide-test/exe/AsyncTests.hs similarity index 95% rename from ghcide/test/exe/AsyncTests.hs rename to ghcide-test/exe/AsyncTests.hs index 4f72a00f18..f341ab504b 100644 --- a/ghcide/test/exe/AsyncTests.hs +++ b/ghcide-test/exe/AsyncTests.hs @@ -15,17 +15,17 @@ import Language.LSP.Protocol.Types hiding mkRange) import Language.LSP.Test -- import Test.QuickCheck.Instances () +import Config import Development.IDE.Plugin.Test (TestRequest (BlockSeconds), blockCommandId) import Test.Tasty import Test.Tasty.HUnit -import TestUtils -- | Test if ghcide asynchronously handles Commands and user Requests tests :: TestTree tests = testGroup "async" [ - testSession "command" $ do + testWithDummyPluginEmpty "command" $ do -- Execute a command that will block forever let req = ExecuteCommandParams Nothing blockCommandId Nothing void $ sendRequest SMethod_WorkspaceExecuteCommand req @@ -38,7 +38,7 @@ tests = testGroup "async" codeLenses <- getAndResolveCodeLenses doc liftIO $ [ _title | CodeLens{_command = Just Command{_title}} <- codeLenses] @=? [ "foo :: a -> a" ] - , testSession "request" $ do + , testWithDummyPluginEmpty "request" $ do -- Execute a custom request that will block for 1000 seconds void $ sendRequest (SMethod_CustomMethod (Proxy @"test")) $ toJSON $ BlockSeconds 1000 -- Load a file and check for code actions. Will only work if the request is run asynchronously diff --git a/ghcide/test/exe/BootTests.hs b/ghcide-test/exe/BootTests.hs similarity index 92% rename from ghcide/test/exe/BootTests.hs rename to ghcide-test/exe/BootTests.hs index 3e4d87c550..06c05ba9b6 100644 --- a/ghcide/test/exe/BootTests.hs +++ b/ghcide-test/exe/BootTests.hs @@ -1,5 +1,7 @@ module BootTests (tests) where +import Config (checkDefs, mkR, runInDir, + runWithExtraFiles) import Control.Applicative.Combinators import Control.Monad import Control.Monad.IO.Class (liftIO) @@ -14,10 +16,9 @@ import Language.LSP.Protocol.Types hiding SemanticTokensEdit (..), mkRange) import Language.LSP.Test -import System.FilePath +import System.FilePath (()) import Test.Tasty import Test.Tasty.HUnit -import TestUtils tests :: TestTree diff --git a/ghcide/test/exe/CPPTests.hs b/ghcide-test/exe/CPPTests.hs similarity index 87% rename from ghcide/test/exe/CPPTests.hs rename to ghcide-test/exe/CPPTests.hs index da9f564fe4..762e6632f1 100644 --- a/ghcide/test/exe/CPPTests.hs +++ b/ghcide-test/exe/CPPTests.hs @@ -9,14 +9,14 @@ import Language.LSP.Protocol.Types hiding (SemanticTokenAbsolute (..), SemanticTokensEdit (..), mkRange) import Language.LSP.Test -- import Test.QuickCheck.Instances () +import Config import Test.Tasty import Test.Tasty.HUnit -import TestUtils tests :: TestTree tests = testGroup "cpp" - [ ignoreInWindowsBecause "Throw a lsp session time out in windows for ghc-8.8 and is broken for other versions" $ testCase "cpp-error" $ do + [ testCase "cpp-error" $ do let content = T.unlines [ "{-# LANGUAGE CPP #-}", @@ -32,7 +32,7 @@ tests = let _ = e :: HUnitFailure run $ expectError content (2, 1) ) - , testSessionWait "cpp-ghcide" $ do + , testWithDummyPluginEmpty "cpp-ghcide" $ do _ <- createDoc "A.hs" "haskell" $ T.unlines ["{-# LANGUAGE CPP #-}" ,"main =" @@ -42,7 +42,7 @@ tests = ," failed" ,"#endif" ] - expectDiagnostics [("A.hs", [(DiagnosticSeverity_Error, (3, 2), "Variable not in scope: worked")])] + expectDiagnostics [("A.hs", [(DiagnosticSeverity_Error, (3, 2), "Variable not in scope: worked", Just "GHC-88464")])] ] where expectError :: T.Text -> Cursor -> Session () @@ -50,7 +50,7 @@ tests = _ <- createDoc "Testing.hs" "haskell" content expectDiagnostics [ ( "Testing.hs", - [(DiagnosticSeverity_Error, cursor, "error: unterminated")] + [(DiagnosticSeverity_Error, cursor, "error: unterminated", Nothing)] ) ] expectNoMoreDiagnostics 0.5 diff --git a/ghcide/test/exe/ClientSettingsTests.hs b/ghcide-test/exe/ClientSettingsTests.hs similarity index 85% rename from ghcide/test/exe/ClientSettingsTests.hs rename to ghcide-test/exe/ClientSettingsTests.hs index 23bc752f82..7c3c3b27f1 100644 --- a/ghcide/test/exe/ClientSettingsTests.hs +++ b/ghcide-test/exe/ClientSettingsTests.hs @@ -1,6 +1,7 @@ {-# LANGUAGE GADTs #-} module ClientSettingsTests (tests) where +import Config (testWithDummyPluginEmpty) import Control.Applicative.Combinators import Control.Monad import Data.Aeson (toJSON) @@ -14,14 +15,13 @@ import Language.LSP.Protocol.Types hiding SemanticTokensEdit (..), mkRange) import Language.LSP.Test +import Test.Hls (waitForProgressDone) import Test.Tasty -import TestUtils tests :: TestTree tests = testGroup "client settings handling" - [ testSession "ghcide restarts shake session on config changes" $ do + [ testWithDummyPluginEmpty "ghcide restarts shake session on config changes" $ do setIgnoringLogNotifications False - void $ skipManyTill anyMessage $ message SMethod_ClientRegisterCapability void $ createDoc "A.hs" "haskell" "module A where" waitForProgressDone setConfigSection "haskell" $ toJSON (def :: Config) diff --git a/ghcide-test/exe/CodeLensTests.hs b/ghcide-test/exe/CodeLensTests.hs new file mode 100644 index 0000000000..fd821e37b6 --- /dev/null +++ b/ghcide-test/exe/CodeLensTests.hs @@ -0,0 +1,140 @@ +{-# LANGUAGE GADTs #-} + +module CodeLensTests (tests) where + +import Config +import Control.Applicative.Combinators +import Control.Lens ((^.)) +import Control.Monad (void) +import Control.Monad.IO.Class (liftIO) +import qualified Data.Aeson as A +import Data.Maybe +import qualified Data.Text as T +import Development.IDE.GHC.Compat (GhcVersion (..), ghcVersion) +import qualified Language.LSP.Protocol.Lens as L +import Language.LSP.Protocol.Types hiding + (SemanticTokenAbsolute (..), + SemanticTokenRelative (..), + SemanticTokensEdit (..), + mkRange) +import Language.LSP.Test +import Test.Hls (mkRange, waitForProgressDone) +import Test.Tasty +import Test.Tasty.HUnit + +tests :: TestTree +tests = testGroup "code lenses" + [ addSigLensesTests + ] + +data TestSpec = + TestSpec + { mName :: Maybe TestName -- ^ Optional Test Name + , input :: T.Text -- ^ Input + , expected :: Maybe T.Text -- ^ Expected Type Sig + } + +mkT :: T.Text -> T.Text -> TestSpec +mkT i e = TestSpec Nothing i (Just e) +mkT' :: TestName -> T.Text -> T.Text -> TestSpec +mkT' name i e = TestSpec (Just name) i (Just e) + +noExpected :: TestSpec -> TestSpec +noExpected t = t { expected = Nothing } + +mkTestName :: TestSpec -> String +mkTestName t = case mName t of + Nothing -> T.unpack $ T.replace "\n" "\\n" (input t) + Just name -> name + +addSigLensesTests :: TestTree +addSigLensesTests = + let pragmas = "{-# OPTIONS_GHC -Wmissing-signatures -Wmissing-pattern-synonym-signatures #-}" + moduleH exported = + T.unlines + [ "{-# LANGUAGE PatternSynonyms,TypeApplications,DataKinds,RankNTypes,ScopedTypeVariables,TypeOperators,GADTs,BangPatterns #-}" + , "module Sigs(" <> exported <> ") where" + , "import qualified Data.Complex as C" + , "import Data.Data (Proxy (..), type (:~:) (..), mkCharType)" + , "data T1 a where" + , " MkT1 :: (Show b) => a -> b -> T1 a" + ] + before enableGHCWarnings exported spec others = + T.unlines $ [pragmas | enableGHCWarnings] <> [moduleH exported, input spec] <> others + after' enableGHCWarnings exported spec others = + T.unlines $ [pragmas | enableGHCWarnings] <> [moduleH exported] <> maybe [] pure (expected spec) <> [input spec] <> others + createConfig mode = A.object ["plugin" A..= A.object ["ghcide-type-lenses" A..= A.object ["config" A..= A.object ["mode" A..= A.String mode]]]] + sigSession testName enableGHCWarnings waitForDiags mode exported spec others = testWithDummyPluginEmpty testName $ do + let originalCode = before enableGHCWarnings exported spec others + let expectedCode = after' enableGHCWarnings exported spec others + setConfigSection "haskell" (createConfig mode) + doc <- createDoc "Sigs.hs" "haskell" originalCode + -- Because the diagnostics mode is really relying only on diagnostics now + -- to generate the code lens we need to make sure we wait till the file + -- is parsed before asking for codelenses, otherwise we will get nothing. + if waitForDiags + then void waitForDiagnostics + else waitForProgressDone + codeLenses <- getAndResolveCodeLenses doc + if isJust $ expected spec + then do + liftIO $ length codeLenses == 1 @? "Expected 1 code lens, but got: " <> show codeLenses + executeCommand $ fromJust $ head codeLenses ^. L.command + modifiedCode <- skipManyTill anyMessage (getDocumentEdit doc) + liftIO $ expectedCode @=? modifiedCode + else liftIO $ null codeLenses @? "Expected no code lens, but got: " <> show codeLenses + cases = + [ mkT "abc = True" "abc :: Bool" + , mkT "foo a b = a + b" "foo :: Num a => a -> a -> a" + , mkT "bar a b = show $ a + b" "bar :: (Show a, Num a) => a -> a -> String" + , mkT "(!!!) a b = a > b" "(!!!) :: Ord a => a -> a -> Bool" + , mkT "a >>>> b = a + b" "(>>>>) :: Num a => a -> a -> a" + , mkT "a `haha` b = a b" "haha :: (t1 -> t2) -> t1 -> t2" + , mkT "pattern Some a = Just a" "pattern Some :: a -> Maybe a" + , mkT "pattern Some a <- Just a" "pattern Some :: a -> Maybe a" + , mkT "pattern Some a <- Just a\n where Some a = Just a" "pattern Some :: a -> Maybe a" + , mkT "pattern Some a <- Just !a\n where Some !a = Just a" "pattern Some :: a -> Maybe a" + , mkT "pattern Point{x, y} = (x, y)" "pattern Point :: a -> b -> (a, b)" + , mkT "pattern Point{x, y} <- (x, y)" "pattern Point :: a -> b -> (a, b)" + , mkT "pattern Point{x, y} <- (x, y)\n where Point x y = (x, y)" "pattern Point :: a -> b -> (a, b)" + , mkT "pattern MkT1' b = MkT1 42 b" "pattern MkT1' :: (Eq a, Num a) => Show b => b -> T1 a" + , mkT "pattern MkT1' b <- MkT1 42 b" "pattern MkT1' :: (Eq a, Num a) => Show b => b -> T1 a" + , mkT "pattern MkT1' b <- MkT1 42 b\n where MkT1' b = MkT1 42 b" "pattern MkT1' :: (Eq a, Num a) => Show b => b -> T1 a" + , mkT "qualifiedSigTest= C.realPart" "qualifiedSigTest :: C.Complex a -> a" + , mkT "head = 233" "head :: Integer" + , mkT "rank2Test (k :: forall a . a -> a) = (k 233 :: Int, k \"QAQ\")" "rank2Test :: (forall a. a -> a) -> (Int, String)" + , mkT "symbolKindTest = Proxy @\"qwq\"" "symbolKindTest :: Proxy \"qwq\"" + , mkT "promotedKindTest = Proxy @Nothing" (if ghcVersion >= GHC96 then "promotedKindTest :: Proxy Nothing" else "promotedKindTest :: Proxy 'Nothing") + , mkT "typeOperatorTest = Refl" "typeOperatorTest :: forall {k} {a :: k}. a :~: a" + , mkT "notInScopeTest = mkCharType" + (if ghcVersion < GHC910 + then "notInScopeTest :: String -> Data.Data.DataType" + else "notInScopeTest :: String -> GHC.Internal.Data.Data.DataType" + ) + + , mkT' "aVeryLongSignature" + "aVeryLongSignature a b c d e f g h i j k l m n = a && b && c && d && e && f && g && h && i && j && k && l && m && n" + "aVeryLongSignature :: Bool -> Bool -> Bool -> Bool -> Bool -> Bool -> Bool -> Bool -> Bool -> Bool -> Bool -> Bool -> Bool -> Bool -> Bool" + ] + in testGroup + "add signature" + [ testGroup "signatures are correct" [sigSession (mkTestName spec) False False "always" "" spec [] | spec <- cases] + , sigSession "exported mode works" False False "exported" "xyz" (mkT "xyz = True" "xyz :: Bool") (input <$> take 3 cases) + , testGroup + "diagnostics mode works" + [ sigSession "with GHC warnings" True True "diagnostics" "" (head cases) [] + , sigSession "without GHC warnings" False False "diagnostics" "" (noExpected $ head cases) [] + ] + , testWithDummyPluginEmpty "keep stale lens" $ do + let content = T.unlines + [ "module Stale where" + , "f = _" + ] + doc <- createDoc "Stale.hs" "haskell" content + oldLens <- getCodeLenses doc + liftIO $ length oldLens @?= 1 + let edit = TextEdit (mkRange 0 4 0 5) "" -- Remove the `_` + _ <- applyEdit doc edit + newLens <- getCodeLenses doc + liftIO $ newLens @?= oldLens + ] diff --git a/ghcide/test/exe/CompletionTests.hs b/ghcide-test/exe/CompletionTests.hs similarity index 80% rename from ghcide/test/exe/CompletionTests.hs rename to ghcide-test/exe/CompletionTests.hs index 4508197bcc..8c44173bd6 100644 --- a/ghcide/test/exe/CompletionTests.hs +++ b/ghcide-test/exe/CompletionTests.hs @@ -1,9 +1,13 @@ -{-# LANGUAGE GADTs #-} -{-# LANGUAGE OverloadedLabels #-} +{-# LANGUAGE GADTs #-} +{-# LANGUAGE LambdaCase #-} +{-# LANGUAGE OverloadedLabels #-} +{-# LANGUAGE OverloadedStrings #-} +{-# LANGUAGE RecordWildCards #-} module CompletionTests (tests) where +import Config import Control.Lens ((^.)) import qualified Control.Lens as Lens import Control.Monad @@ -11,10 +15,7 @@ import Control.Monad.IO.Class (liftIO) import Data.Default import Data.List.Extra import Data.Maybe -import Data.Row import qualified Data.Text as T -import Development.IDE.GHC.Compat (GhcVersion (..), ghcVersion) -import Development.IDE.Test (waitForTypecheck) import Development.IDE.Types.Location import Ide.Plugin.Config import qualified Language.LSP.Protocol.Lens as L @@ -25,11 +26,12 @@ import Language.LSP.Protocol.Types hiding SemanticTokensEdit (..), mkRange) import Language.LSP.Test -import System.FilePath +import Test.Hls (waitForTypecheck) +import qualified Test.Hls.FileSystem as FS +import Test.Hls.FileSystem (file, text) +import Test.Hls.Util import Test.Tasty import Test.Tasty.HUnit -import TestUtils - tests :: TestTree tests @@ -44,10 +46,21 @@ tests , testGroup "doc" completionDocTests ] +testSessionEmpty :: TestName -> Session () -> TestTree +testSessionEmpty name = testWithDummyPlugin name (mkIdeTestFs [FS.directCradle ["A.hs"]]) + +testSessionEmptyWithCradle :: TestName -> T.Text -> Session () -> TestTree +testSessionEmptyWithCradle name cradle = testWithDummyPlugin name (mkIdeTestFs [file "hie.yaml" (text cradle)]) + +testSessionSingleFile :: TestName -> FilePath -> T.Text -> Session () -> TestTree +testSessionSingleFile testName fp txt session = + testWithDummyPlugin testName (mkIdeTestFs [FS.directCradle [T.pack fp] , file fp (text txt)]) session + completionTest :: HasCallStack => String -> [T.Text] -> Position -> [(T.Text, CompletionItemKind, T.Text, Bool, Bool, Maybe [TextEdit])] -> TestTree -completionTest name src pos expected = testSessionWait name $ do - docId <- createDoc "A.hs" "haskell" (T.unlines src) +completionTest name src pos expected = testSessionSingleFile name "A.hs" (T.unlines src) $ do + docId <- openDoc "A.hs" "haskell" _ <- waitForDiagnostics + compls <- getAndResolveCompletions docId pos let compls' = [ (_label, _kind, _insertText, _additionalTextEdits) | CompletionItem{..} <- compls] let emptyToMaybe x = if T.null x then Nothing else Just x @@ -185,12 +198,12 @@ localCompletionTests = [ [("abcd", CompletionItemKind_Function, "abcd", True, False, Nothing) ,("abcde", CompletionItemKind_Function, "abcde", True, False, Nothing) ], - testSessionWait "incomplete entries" $ do + testSessionEmpty "incomplete entries" $ do let src a = "data Data = " <> a doc <- createDoc "A.hs" "haskell" $ src "AAA" void $ waitForTypecheck doc let editA rhs = - changeDoc doc [TextDocumentContentChangeEvent . InR . (.==) #text $ src rhs] + changeDoc doc [TextDocumentContentChangeEvent . InR . TextDocumentContentChangeWholeDocument $ src rhs] editA "AAAA" void $ waitForTypecheck doc editA "AAAAA" @@ -198,12 +211,43 @@ localCompletionTests = [ compls <- getCompletions doc (Position 0 15) liftIO $ filter ("AAA" `T.isPrefixOf`) (mapMaybe _insertText compls) @?= ["AAAAA"] - pure () + pure (), + completionTest + "polymorphic record dot completion" + [ "{-# LANGUAGE OverloadedRecordDot #-}" + , "module A () where" + , "data Record = Record" + , " { field1 :: Int" + , " , field2 :: Int" + , " }" + , -- Without the following, this file doesn't trigger any diagnostics, so completionTest waits forever + "triggerDiag :: UnknownType" + , "foo record = record.f" + ] + (Position 7 21) + [("field1", CompletionItemKind_Function, "field1", True, False, Nothing) + ,("field2", CompletionItemKind_Function, "field2", True, False, Nothing) + ], + completionTest + "qualified polymorphic record dot completion" + [ "{-# LANGUAGE OverloadedRecordDot #-}" + , "module A () where" + , "data Record = Record" + , " { field1 :: Int" + , " , field2 :: Int" + , " }" + , "someValue = undefined" + , "foo = A.someValue.f" + ] + (Position 7 19) + [("field1", CompletionItemKind_Function, "field1", True, False, Nothing) + ,("field2", CompletionItemKind_Function, "field2", True, False, Nothing) + ] ] nonLocalCompletionTests :: [TestTree] nonLocalCompletionTests = - [ brokenForWinGhc $ completionTest + [ brokenForWinOldGhc $ completionTest "variable" ["module A where", "f = hea"] (Position 1 7) @@ -261,7 +305,10 @@ nonLocalCompletionTests = [] ] where - brokenForWinGhc = knownBrokenFor (BrokenSpecific Windows [GHC90, GHC92, GHC94, GHC96, GHC98]) "Windows has strange things in scope for some reason" + brokenForWinGhc = knownBrokenOnWindows "Windows has strange things in scope for some reason" + brokenForWinOldGhc = + knownBrokenInSpecificEnv [HostOS Windows, GhcVer GHC96] "Windows (GHC == 9.6) has strange things in scope for some reason" + . knownBrokenInSpecificEnv [HostOS Windows, GhcVer GHC98] "Windows (GHC == 9.8) has strange things in scope for some reason" otherCompletionTests :: [TestTree] otherCompletionTests = [ @@ -283,7 +330,7 @@ otherCompletionTests = [ (Position 3 11) [("Integer", CompletionItemKind_Struct, "Integer", True, True, Nothing)], - testSession "duplicate record fields" $ do + testSessionEmpty "duplicate record fields" $ do void $ createDoc "B.hs" "haskell" $ T.unlines @@ -304,22 +351,21 @@ otherCompletionTests = [ let compls' = [txt | CompletionItem {_insertText = Just txt, ..} <- compls, _label == "member"] liftIO $ take 1 compls' @?= ["member"], - testSessionWait "maxCompletions" $ do + testSessionEmpty "maxCompletions" $ do doc <- createDoc "A.hs" "haskell" $ T.unlines [ "{-# OPTIONS_GHC -Wunused-binds #-}", "module A () where", "a = Prelude." ] _ <- waitForDiagnostics - compls <- getCompletions doc (Position 3 13) + compls <- getCompletions doc (Position 3 13) liftIO $ length compls @?= maxCompletions def ] packageCompletionTests :: [TestTree] packageCompletionTests = - [ testSession' "fromList" $ \dir -> do - liftIO $ writeFile (dir "hie.yaml") - "cradle: {direct: {arguments: [-hide-all-packages, -package, base, A]}}" + [ testSessionEmptyWithCradle "fromList" "cradle: {direct: {arguments: [-hide-all-packages, -package, base, A]}}" $ do + doc <- createDoc "A.hs" "haskell" $ T.unlines [ "{-# OPTIONS_GHC -Wunused-binds #-}", "module A () where", @@ -334,12 +380,13 @@ packageCompletionTests = , _label == "fromList" ] liftIO $ take 3 (sort compls') @?= - map ("Defined in "<>) ( - [ "'Data.List.NonEmpty" + map ("Defined in "<>) [ + "'Data.List.NonEmpty" , "'GHC.Exts" - ] ++ if ghcVersion >= GHC94 then [ "'GHC.IsList" ] else []) + , "'GHC.IsList" + ] - , testSessionWait "Map" $ do + , testSessionEmptyWithCradle "Map" "cradle: {direct: {arguments: [-hide-all-packages, -package, base, -package, containers, A]}}" $ do doc <- createDoc "A.hs" "haskell" $ T.unlines [ "{-# OPTIONS_GHC -Wunused-binds #-}", "module A () where", @@ -359,7 +406,7 @@ packageCompletionTests = , "'Data.Map.Lazy" , "'Data.Map.Strict" ] - , testSessionWait "no duplicates" $ do + , testSessionEmpty "no duplicates" $ do doc <- createDoc "A.hs" "haskell" $ T.unlines [ "{-# OPTIONS_GHC -Wunused-binds #-}", "module A () where", @@ -381,7 +428,7 @@ packageCompletionTests = ) compls liftIO $ length duplicate @?= 1 - , testSessionWait "non-local before global" $ do + , testSessionEmpty "non-local before global" $ do -- non local completions are more specific doc <- createDoc "A.hs" "haskell" $ T.unlines [ "{-# OPTIONS_GHC -Wunused-binds #-}", @@ -402,9 +449,7 @@ packageCompletionTests = projectCompletionTests :: [TestTree] projectCompletionTests = - [ testSession' "from hiedb" $ \dir-> do - liftIO $ writeFile (dir "hie.yaml") - "cradle: {direct: {arguments: [\"-Wmissing-signatures\", \"A\", \"B\"]}}" + [ testSessionEmptyWithCradle "from hiedb" "cradle: {direct: {arguments: [\"-Wmissing-signatures\", \"A\", \"B\"]}}" $ do _ <- createDoc "A.hs" "haskell" $ T.unlines [ "module A (anidentifier) where", "anidentifier = ()" @@ -423,9 +468,7 @@ projectCompletionTests = , _label == "anidentifier" ] liftIO $ compls' @?= ["Defined in 'A"], - testSession' "auto complete project imports" $ \dir-> do - liftIO $ writeFile (dir "hie.yaml") - "cradle: {direct: {arguments: [\"-Wmissing-signatures\", \"ALocalModule\", \"B\"]}}" + testSessionEmptyWithCradle "auto complete project imports" "cradle: {direct: {arguments: [\"-Wmissing-signatures\", \"ALocalModule\", \"B\"]}}" $ do _ <- createDoc "ALocalModule.hs" "haskell" $ T.unlines [ "module ALocalModule (anidentifier) where", "anidentifier = ()" @@ -440,9 +483,7 @@ projectCompletionTests = let item = head $ filter ((== "ALocalModule") . (^. L.label)) compls liftIO $ do item ^. L.label @?= "ALocalModule", - testSession' "auto complete functions from qualified imports without alias" $ \dir-> do - liftIO $ writeFile (dir "hie.yaml") - "cradle: {direct: {arguments: [\"-Wmissing-signatures\", \"A\", \"B\"]}}" + testSessionEmptyWithCradle "auto complete functions from qualified imports without alias" "cradle: {direct: {arguments: [\"-Wmissing-signatures\", \"A\", \"B\"]}}" $ do _ <- createDoc "A.hs" "haskell" $ T.unlines [ "module A (anidentifier) where", "anidentifier = ()" @@ -457,9 +498,8 @@ projectCompletionTests = let item = head compls liftIO $ do item ^. L.label @?= "anidentifier", - testSession' "auto complete functions from qualified imports with alias" $ \dir-> do - liftIO $ writeFile (dir "hie.yaml") - "cradle: {direct: {arguments: [\"-Wmissing-signatures\", \"A\", \"B\"]}}" + testSessionEmptyWithCradle "auto complete functions from qualified imports with alias" + "cradle: {direct: {arguments: [\"-Wmissing-signatures\", \"A\", \"B\"]}}" $ do _ <- createDoc "A.hs" "haskell" $ T.unlines [ "module A (anidentifier) where", "anidentifier = ()" @@ -478,7 +518,7 @@ projectCompletionTests = completionDocTests :: [TestTree] completionDocTests = - [ testSession "local define" $ do + [ testSessionEmpty "local define" $ do doc <- createDoc "A.hs" "haskell" $ T.unlines [ "module A where" , "foo = ()" @@ -486,14 +526,14 @@ completionDocTests = ] let expected = "*Defined at line 2, column 1 in this module*\n" test doc (Position 2 8) "foo" Nothing [expected] - , testSession "local empty doc" $ do + , testSessionEmpty "local empty doc" $ do doc <- createDoc "A.hs" "haskell" $ T.unlines [ "module A where" , "foo = ()" , "bar = fo" ] test doc (Position 2 8) "foo" Nothing ["*Defined at line 2, column 1 in this module*\n"] - , testSession "local single line doc without newline" $ do + , testSessionEmpty "local single line doc without newline" $ do doc <- createDoc "A.hs" "haskell" $ T.unlines [ "module A where" , "-- |docdoc" @@ -501,7 +541,7 @@ completionDocTests = , "bar = fo" ] test doc (Position 3 8) "foo" Nothing ["*Defined at line 3, column 1 in this module*\n* * *\n\n\ndocdoc\n"] - , testSession "local multi line doc with newline" $ do + , testSessionEmpty "local multi line doc with newline" $ do doc <- createDoc "A.hs" "haskell" $ T.unlines [ "module A where" , "-- | abcabc" @@ -510,7 +550,7 @@ completionDocTests = , "bar = fo" ] test doc (Position 4 8) "foo" Nothing ["*Defined at line 4, column 1 in this module*\n* * *\n\n\nabcabc\n"] - , testSession "local multi line doc without newline" $ do + , testSessionEmpty "local multi line doc without newline" $ do doc <- createDoc "A.hs" "haskell" $ T.unlines [ "module A where" , "-- | abcabc" @@ -520,28 +560,28 @@ completionDocTests = , "bar = fo" ] test doc (Position 5 8) "foo" Nothing ["*Defined at line 5, column 1 in this module*\n* * *\n\n\nabcabc \n\ndef\n"] - , testSession "extern empty doc" $ do + , testSessionEmpty "extern empty doc" $ do doc <- createDoc "A.hs" "haskell" $ T.unlines [ "module A where" , "foo = od" ] let expected = "*Imported from 'Prelude'*\n" test doc (Position 1 8) "odd" (Just $ T.length expected) [expected] - , brokenForMacGhc9 $ brokenForWinGhc90 $ testSession "extern single line doc without '\\n'" $ do + , testSessionEmpty "extern single line doc without '\\n'" $ do doc <- createDoc "A.hs" "haskell" $ T.unlines [ "module A where" , "foo = no" ] let expected = "*Imported from 'Prelude'*\n* * *\n\n\nBoolean \"not\"\n" test doc (Position 1 8) "not" (Just $ T.length expected) [expected] - , brokenForMacGhc9 $ brokenForWinGhc90 $ testSession "extern mulit line doc" $ do + , testSessionEmpty "extern mulit line doc" $ do doc <- createDoc "A.hs" "haskell" $ T.unlines [ "module A where" , "foo = i" ] let expected = "*Imported from 'Prelude'*\n* * *\n\n\nIdentity function. \n```haskell\nid x = x\n```\n" test doc (Position 1 7) "id" (Just $ T.length expected) [expected] - , testSession "extern defined doc" $ do + , testSessionEmpty "extern defined doc" $ do doc <- createDoc "A.hs" "haskell" $ T.unlines [ "module A where" , "foo = i" @@ -550,21 +590,14 @@ completionDocTests = test doc (Position 1 7) "id" (Just $ T.length expected) [expected] ] where - brokenForGhc9 = knownBrokenFor (BrokenForGHC [GHC90, GHC92, GHC94, GHC96]) "Completion doc doesn't support ghc9" - brokenForWinGhc90 = knownBrokenFor (BrokenSpecific Windows [GHC90]) "Extern doc doesn't support Windows for ghc9.2" - -- https://gitlab.haskell.org/ghc/ghc/-/issues/20903 - brokenForMacGhc9 = knownBrokenFor (BrokenSpecific MacOS [GHC90, GHC92, GHC94, GHC96]) "Extern doc doesn't support MacOS for ghc9" test doc pos label mn expected = do _ <- waitForDiagnostics compls <- getCompletions doc pos rcompls <- forM compls $ \item -> do - if isJust (item ^. L.data_) - then do - rsp <- request SMethod_CompletionItemResolve item - case rsp ^. L.result of - Left err -> liftIO $ assertFailure ("completionItem/resolve failed with: " <> show err) - Right x -> pure x - else pure item + rsp <- request SMethod_CompletionItemResolve item + case rsp ^. L.result of + Left err -> liftIO $ assertFailure ("completionItem/resolve failed with: " <> show err) + Right x -> pure x let compls' = [ -- We ignore doc uris since it points to the local path which determined by specific machines case mn of diff --git a/ghcide-test/exe/Config.hs b/ghcide-test/exe/Config.hs new file mode 100644 index 0000000000..c98023e90e --- /dev/null +++ b/ghcide-test/exe/Config.hs @@ -0,0 +1,187 @@ +{-# LANGUAGE PatternSynonyms #-} + +module Config( + -- * basic config for ghcIde testing + mkIdeTestFs + , dummyPlugin + + -- * runners for testing specific plugins + , testSessionWithPlugin + -- * runners for testing with dummy plugin + , runWithDummyPlugin + , testWithDummyPlugin + , testWithDummyPluginEmpty + , testWithDummyPlugin' + , testWithDummyPluginEmpty' + , testWithConfig + , testWithExtraFiles + , runWithExtraFiles + , runInDir + , run + + -- * utilities for testing + , Expect(..) + , pattern R + , mkR + , checkDefs + , mkL + , withLongTimeout + , lspTestCaps + , lspTestCapsNoFileWatches + , testDataDir + ) where + +import Control.Exception (bracket_) +import Control.Lens.Setter ((.~)) +import Control.Monad (unless) +import Data.Foldable (traverse_) +import Data.Function ((&)) +import qualified Data.Text as T +import Development.IDE (Pretty) +import Development.IDE.Test (canonicalizeUri) +import Ide.Types (defaultPluginDescriptor) +import qualified Language.LSP.Protocol.Lens as L +import Language.LSP.Protocol.Types (Null (..)) +import System.Environment.Blank (setEnv, unsetEnv) +import System.FilePath (()) +import Test.Hls +import qualified Test.Hls.FileSystem as FS + +testDataDir :: FilePath +testDataDir = "ghcide-test" "data" + +mkIdeTestFs :: [FS.FileTree] -> FS.VirtualFileTree +mkIdeTestFs = FS.mkVirtualFileTree testDataDir + +-- * Run with some injected plugin +-- testSessionWithPlugin :: FS.VirtualFileTree -> (FilePath -> Session a) -> IO a +testSessionWithPlugin :: Pretty b => FS.VirtualFileTree -> PluginTestDescriptor b -> (FilePath -> Session a) -> IO a +testSessionWithPlugin fs plugin = runSessionWithTestConfig def + { testPluginDescriptor = plugin + , testDirLocation = Right fs + , testConfigCaps = lspTestCaps + , testShiftRoot = True + } + +-- * A dummy plugin for testing ghcIde +dummyPlugin :: PluginTestDescriptor () +dummyPlugin = mkPluginTestDescriptor (\_ pid -> defaultPluginDescriptor pid "dummyTestPlugin") "core" + +runWithDummyPlugin :: FS.VirtualFileTree -> Session a -> IO a +runWithDummyPlugin = runSessionWithServerInTmpDir def dummyPlugin + +testWithConfig :: String -> TestConfig () -> Session () -> TestTree +testWithConfig name conf s = testCase name $ runSessionWithTestConfig conf $ const s + +runWithDummyPlugin' :: FS.VirtualFileTree -> (FilePath -> Session a) -> IO a +runWithDummyPlugin' fs = runSessionWithTestConfig def + { testPluginDescriptor = dummyPlugin + , testDirLocation = Right fs + , testConfigCaps = lspTestCaps + , testShiftRoot = True + } + +testWithDummyPlugin :: String -> FS.VirtualFileTree -> Session () -> TestTree +testWithDummyPlugin caseName vfs = testWithDummyPlugin' caseName vfs . const + +testWithDummyPlugin' :: String -> FS.VirtualFileTree -> (FilePath -> Session ()) -> TestTree +testWithDummyPlugin' caseName vfs = testCase caseName . runWithDummyPlugin' vfs + +testWithDummyPluginEmpty :: String -> Session () -> TestTree +testWithDummyPluginEmpty caseName = testWithDummyPlugin caseName $ mkIdeTestFs [] + +testWithDummyPluginEmpty' :: String -> (FilePath -> Session ()) -> TestTree +testWithDummyPluginEmpty' caseName = testWithDummyPlugin' caseName $ mkIdeTestFs [] + +runWithExtraFiles :: String -> (FilePath -> Session a) -> IO a +runWithExtraFiles dirName action = do + let vfs = mkIdeTestFs [FS.copyDir dirName] + runWithDummyPlugin' vfs action + +testWithExtraFiles :: String -> String -> (FilePath -> Session ()) -> TestTree +testWithExtraFiles testName dirName action = testCase testName $ runWithExtraFiles dirName action + +runInDir :: FilePath -> Session a -> IO a +runInDir fs = runSessionWithServer def dummyPlugin fs + +run :: Session a -> IO a +run = runSessionWithTestConfig def + { testDirLocation = Right (mkIdeTestFs []) + , testPluginDescriptor = dummyPlugin } + . const + +pattern R :: UInt -> UInt -> UInt -> UInt -> Range +pattern R x y x' y' = Range (Position x y) (Position x' y') + +data Expect + = ExpectRange Range -- Both gotoDef and hover should report this range + | ExpectRanges [Range] -- definition lookup with multiple results + | ExpectLocation Location +-- | ExpectDefRange Range -- Only gotoDef should report this range + | ExpectHoverRange Range -- Only hover should report this range + | ExpectHoverText [T.Text] -- the hover message must contain these snippets + | ExpectHoverExcludeText [T.Text] -- the hover message must _not_ contain these snippets + | ExpectHoverTextRegex T.Text -- the hover message must match this pattern + | ExpectExternFail -- definition lookup in other file expected to fail + | ExpectNoDefinitions + | ExpectNoImplementations + | ExpectNoHover +-- | ExpectExtern -- TODO: as above, but expected to succeed: need some more info in here, once we have some working examples + deriving Eq + +mkR :: UInt -> UInt -> UInt -> UInt -> Expect +mkR startLine startColumn endLine endColumn = ExpectRange $ mkRange startLine startColumn endLine endColumn + +mkL :: Uri -> UInt -> UInt -> UInt -> UInt -> Expect +mkL uri startLine startColumn endLine endColumn = ExpectLocation $ Location uri $ mkRange startLine startColumn endLine endColumn + + +checkDefs :: Definition |? ([DefinitionLink] |? Null) -> Session [Expect] -> Session () +checkDefs (defToLocation -> defs) mkExpectations = traverse_ check =<< mkExpectations where + check (ExpectRange expectedRange) = do + def <- assertOneDefinitionFound defs + assertRangeCorrect def expectedRange + check (ExpectRanges ranges) = + traverse_ (assertHasRange defs) ranges + check (ExpectLocation expectedLocation) = do + def <- assertOneDefinitionFound defs + liftIO $ do + canonActualLoc <- canonicalizeLocation def + canonExpectedLoc <- canonicalizeLocation expectedLocation + canonActualLoc @?= canonExpectedLoc + check ExpectNoImplementations = do + liftIO $ assertBool "Expecting no implementations" $ null defs + check ExpectNoDefinitions = do + liftIO $ assertBool "Expecting no definitions" $ null defs + check ExpectExternFail = liftIO $ assertFailure "Expecting to fail to find in external file" + check _ = pure () -- all other expectations not relevant to getDefinition + + assertOneDefinitionFound :: [Location] -> Session Location + assertOneDefinitionFound [def] = pure def + assertOneDefinitionFound xs = liftIO . assertFailure $ "Expecting exactly one definition, got " <> show (length xs) + + assertRangeCorrect Location{_range = foundRange} expectedRange = + liftIO $ expectedRange @=? foundRange + + assertHasRange actualRanges expectedRange = do + let hasRange = any (\Location{_range=foundRange} -> foundRange == expectedRange) actualRanges + unless hasRange $ liftIO $ assertFailure $ + "expected range: " <> show expectedRange <> "\nbut got ranges: " <> show defs + +canonicalizeLocation :: Location -> IO Location +canonicalizeLocation (Location uri range) = Location <$> canonicalizeUri uri <*> pure range + +defToLocation :: Definition |? ([DefinitionLink] |? Null) -> [Location] +defToLocation (InL (Definition (InL l))) = [l] +defToLocation (InL (Definition (InR ls))) = ls +defToLocation (InR (InL defLink)) = (\(DefinitionLink LocationLink{_targetUri,_targetRange}) -> Location _targetUri _targetRange) <$> defLink +defToLocation (InR (InR Null)) = [] + +lspTestCaps :: ClientCapabilities +lspTestCaps = fullLatestClientCaps { _window = Just $ WindowClientCapabilities (Just True) Nothing Nothing } + +lspTestCapsNoFileWatches :: ClientCapabilities +lspTestCapsNoFileWatches = lspTestCaps & L.workspace . traverse . L.didChangeWatchedFiles .~ Nothing + +withLongTimeout :: IO a -> IO a +withLongTimeout = bracket_ (setEnv "LSP_TIMEOUT" "120" True) (unsetEnv "LSP_TIMEOUT") diff --git a/ghcide/test/exe/CradleTests.hs b/ghcide-test/exe/CradleTests.hs similarity index 73% rename from ghcide/test/exe/CradleTests.hs rename to ghcide-test/exe/CradleTests.hs index 167860833b..046b8bbf2f 100644 --- a/ghcide/test/exe/CradleTests.hs +++ b/ghcide-test/exe/CradleTests.hs @@ -1,21 +1,24 @@ -{-# LANGUAGE GADTs #-} -{-# LANGUAGE OverloadedLabels #-} +{-# LANGUAGE GADTs #-} module CradleTests (tests) where +import Config (checkDefs, mkL, runInDir, + runWithExtraFiles, + testWithDummyPluginEmpty') import Control.Applicative.Combinators +import Control.Lens ((^.)) import Control.Monad.IO.Class (liftIO) -import Data.Row import qualified Data.Text as T -import Development.IDE.GHC.Compat (GhcVersion (..), ghcVersion) import Development.IDE.GHC.Util +import Development.IDE.Plugin.Test (WaitForIdeRuleResult (..)) import Development.IDE.Test (expectDiagnostics, expectDiagnosticsWithTags, expectNoMoreDiagnostics, isReferenceReady, waitForAction) import Development.IDE.Types.Location +import GHC.TypeLits (symbolVal) import qualified Language.LSP.Protocol.Lens as L import Language.LSP.Protocol.Message import Language.LSP.Protocol.Types hiding @@ -26,13 +29,10 @@ import Language.LSP.Protocol.Types hiding import Language.LSP.Test import System.FilePath import System.IO.Extra hiding (withTempDir) --- import Test.QuickCheck.Instances () -import Control.Lens ((^.)) -import Development.IDE.Plugin.Test (WaitForIdeRuleResult (..)) -import GHC.TypeLits (symbolVal) +import Test.Hls.Util (EnvSpec (..), OS (..), + ignoreInEnv) import Test.Tasty import Test.Tasty.HUnit -import TestUtils tests :: TestTree @@ -40,14 +40,16 @@ tests = testGroup "cradle" [testGroup "dependencies" [sessionDepsArePickedUp] ,testGroup "ignore-fatal" [ignoreFatalWarning] ,testGroup "loading" [loadCradleOnlyonce, retryFailedCradle] - ,testGroup "multi" [simpleMultiTest, simpleMultiTest2, simpleMultiTest3, simpleMultiDefTest] + ,testGroup "multi" (multiTests "multi") + ,testGroup "multi-unit" (multiTests "multi-unit") ,testGroup "sub-directory" [simpleSubDirectoryTest] + ,testGroup "multi-unit-rexport" [multiRexportTest] ] loadCradleOnlyonce :: TestTree loadCradleOnlyonce = testGroup "load cradle only once" - [ testSession' "implicit" implicit - , testSession' "direct" direct + [ testWithDummyPluginEmpty' "implicit" implicit + , testWithDummyPluginEmpty' "direct" direct ] where direct dir = do @@ -59,7 +61,7 @@ loadCradleOnlyonce = testGroup "load cradle only once" doc <- createDoc "B.hs" "haskell" "module B where\nimport Data.Foo" msgs <- someTill (skipManyTill anyMessage cradleLoadedMessage) (skipManyTill anyMessage (message SMethod_TextDocumentPublishDiagnostics)) liftIO $ length msgs @?= 1 - changeDoc doc [TextDocumentContentChangeEvent . InR . (.==) #text $ "module B where\nimport Data.Maybe"] + changeDoc doc [TextDocumentContentChangeEvent . InR . TextDocumentContentChangeWholeDocument $ "module B where\nimport Data.Maybe"] msgs <- manyTill (skipManyTill anyMessage cradleLoadedMessage) (skipManyTill anyMessage (message SMethod_TextDocumentPublishDiagnostics)) liftIO $ length msgs @?= 0 _ <- createDoc "A.hs" "haskell" "module A where\nimport LoadCradleBar" @@ -67,7 +69,7 @@ loadCradleOnlyonce = testGroup "load cradle only once" liftIO $ length msgs @?= 0 retryFailedCradle :: TestTree -retryFailedCradle = testSession' "retry failed" $ \dir -> do +retryFailedCradle = testWithDummyPluginEmpty' "retry failed" $ \dir -> do -- The false cradle always fails let hieContents = "cradle: {bios: {shell: \"false\"}}" hiePath = dir "hie.yaml" @@ -80,7 +82,7 @@ retryFailedCradle = testSession' "retry failed" $ \dir -> do -- Fix the cradle and typecheck again let validCradle = "cradle: {bios: {shell: \"echo A.hs\"}}" liftIO $ writeFileUTF8 hiePath $ T.unpack validCradle - sendNotification SMethod_WorkspaceDidChangeWatchedFiles $ DidChangeWatchedFilesParams $ + sendNotification SMethod_WorkspaceDidChangeWatchedFiles $ DidChangeWatchedFilesParams [FileEvent (filePathToUri $ dir "hie.yaml") FileChangeType_Changed ] WaitForIdeRuleResult {..} <- waitForAction "TypeCheck" doc @@ -109,12 +111,19 @@ simpleSubDirectoryTest = mainSource <- liftIO $ readFileUtf8 mainPath _mdoc <- createDoc mainPath "haskell" mainSource expectDiagnosticsWithTags - [("a/src/Main.hs", [(DiagnosticSeverity_Warning,(2,0), "Top-level binding", Nothing)]) -- So that we know P has been loaded + [("a/src/Main.hs", [(DiagnosticSeverity_Warning,(2,0), "Top-level binding", Just "GHC-38417", Nothing)]) -- So that we know P has been loaded ] expectNoMoreDiagnostics 0.5 -simpleMultiTest :: TestTree -simpleMultiTest = testCase "simple-multi-test" $ withLongTimeout $ runWithExtraFiles "multi" $ \dir -> do +multiTests :: FilePath -> [TestTree] +multiTests dir = + [simpleMultiTest dir, simpleMultiTest2 dir, simpleMultiTest3 dir, simpleMultiDefTest dir] + +multiTestName :: FilePath -> String -> String +multiTestName dir name = "simple-" ++ dir ++ "-" ++ name + +simpleMultiTest :: FilePath -> TestTree +simpleMultiTest variant = testCase (multiTestName variant "test") $ runWithExtraFiles variant $ \dir -> do let aPath = dir "a/A.hs" bPath = dir "b/B.hs" adoc <- openDoc aPath "haskell" @@ -129,8 +138,8 @@ simpleMultiTest = testCase "simple-multi-test" $ withLongTimeout $ runWithExtraF expectNoMoreDiagnostics 0.5 -- Like simpleMultiTest but open the files in the other order -simpleMultiTest2 :: TestTree -simpleMultiTest2 = testCase "simple-multi-test2" $ runWithExtraFiles "multi" $ \dir -> do +simpleMultiTest2 :: FilePath -> TestTree +simpleMultiTest2 variant = testCase (multiTestName variant "test2") $ runWithExtraFiles variant $ \dir -> do let aPath = dir "a/A.hs" bPath = dir "b/B.hs" bdoc <- openDoc bPath "haskell" @@ -143,9 +152,9 @@ simpleMultiTest2 = testCase "simple-multi-test2" $ runWithExtraFiles "multi" $ \ expectNoMoreDiagnostics 0.5 -- Now with 3 components -simpleMultiTest3 :: TestTree -simpleMultiTest3 = - testCase "simple-multi-test3" $ runWithExtraFiles "multi" $ \dir -> do +simpleMultiTest3 :: FilePath -> TestTree +simpleMultiTest3 variant = + testCase (multiTestName variant "test3") $ runWithExtraFiles variant $ \dir -> do let aPath = dir "a/A.hs" bPath = dir "b/B.hs" cPath = dir "c/C.hs" @@ -161,8 +170,9 @@ simpleMultiTest3 = expectNoMoreDiagnostics 0.5 -- Like simpleMultiTest but open the files in component 'a' in a separate session -simpleMultiDefTest :: TestTree -simpleMultiDefTest = testCase "simple-multi-def-test" $ runWithExtraFiles "multi" $ \dir -> do +simpleMultiDefTest :: FilePath -> TestTree +simpleMultiDefTest variant = ignoreForWindows $ testCase testName $ + runWithExtraFiles variant $ \dir -> do let aPath = dir "a/A.hs" bPath = dir "b/B.hs" adoc <- liftIO $ runInDir dir $ do @@ -177,10 +187,26 @@ simpleMultiDefTest = testCase "simple-multi-def-test" $ runWithExtraFiles "multi let fooL = mkL (adoc ^. L.uri) 2 0 2 3 checkDefs locs (pure [fooL]) expectNoMoreDiagnostics 0.5 - + where + testName = multiTestName variant "def-test" + ignoreForWindows + | testName == "simple-multi-def-test" = ignoreInEnv [HostOS Windows] "Test is flaky on Windows, see #4270" + | otherwise = id + +multiRexportTest :: TestTree +multiRexportTest = + testCase "multi-unit-reexport-test" $ runWithExtraFiles "multi-unit-reexport" $ \dir -> do + let cPath = dir "c/C.hs" + cdoc <- openDoc cPath "haskell" + WaitForIdeRuleResult {} <- waitForAction "TypeCheck" cdoc + locs <- getDefinitions cdoc (Position 3 7) + let aPath = dir "a/A.hs" + let fooL = mkL (filePathToUri aPath) 2 0 2 3 + checkDefs locs (pure [fooL]) + expectNoMoreDiagnostics 0.5 sessionDepsArePickedUp :: TestTree -sessionDepsArePickedUp = testSession' +sessionDepsArePickedUp = testWithDummyPluginEmpty' "session-deps-are-picked-up" $ \dir -> do liftIO $ @@ -189,23 +215,22 @@ sessionDepsArePickedUp = testSession' "cradle: {direct: {arguments: []}}" -- Open without OverloadedStrings and expect an error. doc <- createDoc "Foo.hs" "haskell" fooContent - expectDiagnostics $ - if ghcVersion >= GHC90 - -- String vs [Char] causes this change in error message - then [("Foo.hs", [(DiagnosticSeverity_Error, (3, 6), "Couldn't match type")])] - else [("Foo.hs", [(DiagnosticSeverity_Error, (3, 6), "Couldn't match expected type")])] + expectDiagnostics [("Foo.hs", [(DiagnosticSeverity_Error, (3, 6), "Couldn't match type", Just "GHC-83865")])] + -- Update hie.yaml to enable OverloadedStrings. liftIO $ writeFileUTF8 (dir "hie.yaml") "cradle: {direct: {arguments: [-XOverloadedStrings]}}" - sendNotification SMethod_WorkspaceDidChangeWatchedFiles $ DidChangeWatchedFilesParams $ + sendNotification SMethod_WorkspaceDidChangeWatchedFiles $ DidChangeWatchedFilesParams [FileEvent (filePathToUri $ dir "hie.yaml") FileChangeType_Changed ] -- Send change event. let change = - TextDocumentContentChangeEvent $ InL $ #range .== Range (Position 4 0) (Position 4 0) - .+ #rangeLength .== Nothing - .+ #text .== "\n" + TextDocumentContentChangeEvent $ InL TextDocumentContentChangePartial + { _range = Range (Position 4 0) (Position 4 0) + , _rangeLength = Nothing + , _text = "\n" + } changeDoc doc [change] -- Now no errors. expectDiagnostics [("Foo.hs", [])] diff --git a/ghcide/test/exe/DependentFileTest.hs b/ghcide-test/exe/DependentFileTest.hs similarity index 53% rename from ghcide/test/exe/DependentFileTest.hs rename to ghcide-test/exe/DependentFileTest.hs index d78ad49a8a..1f243819e3 100644 --- a/ghcide/test/exe/DependentFileTest.hs +++ b/ghcide-test/exe/DependentFileTest.hs @@ -1,13 +1,10 @@ -{-# LANGUAGE GADTs #-} -{-# LANGUAGE OverloadedLabels #-} +{-# LANGUAGE GADTs #-} module DependentFileTest (tests) where -import Control.Monad.IO.Class (liftIO) -import Data.Row +import Config import qualified Data.Text as T -import Development.IDE.GHC.Compat (GhcVersion (..), ghcVersion) import Development.IDE.Test (expectDiagnostics) import Development.IDE.Types.Location import Language.LSP.Protocol.Message @@ -17,19 +14,23 @@ import Language.LSP.Protocol.Types hiding SemanticTokensEdit (..), mkRange) import Language.LSP.Test -import System.FilePath -import Test.Tasty -import TestUtils +import Test.Hls + tests :: TestTree tests = testGroup "addDependentFile" - [testGroup "file-changed" [testSession' "test" test] + [testGroup "file-changed" [testCase "test" $ runSessionWithTestConfig def + { testShiftRoot = True + , testDirLocation = Right (mkIdeTestFs []) + , testPluginDescriptor = dummyPlugin + } test] ] where - test dir = do + test :: FilePath -> Session () + test _ = do -- If the file contains B then no type error -- otherwise type error - let depFilePath = dir "dep-file.txt" + let depFilePath = "dep-file.txt" liftIO $ writeFile depFilePath "A" let fooContent = T.unlines [ "{-# LANGUAGE TemplateHaskell #-}" @@ -37,26 +38,25 @@ tests = testGroup "addDependentFile" , "import Language.Haskell.TH.Syntax" , "foo :: Int" , "foo = 1 + $(do" - , " qAddDependentFile \"dep-file.txt\"" - , " f <- qRunIO (readFile \"dep-file.txt\")" + , " qAddDependentFile \"" <> T.pack depFilePath <> "\"" + , " f <- qRunIO (readFile \"" <> T.pack depFilePath <> "\")" , " if f == \"B\" then [| 1 |] else lift f)" ] let bazContent = T.unlines ["module Baz where", "import Foo ()"] - _ <- createDoc "Foo.hs" "haskell" fooContent + _fooDoc <- createDoc "Foo.hs" "haskell" fooContent doc <- createDoc "Baz.hs" "haskell" bazContent - expectDiagnostics $ - if ghcVersion >= GHC90 - -- String vs [Char] causes this change in error message - then [("Foo.hs", [(DiagnosticSeverity_Error, if ghcVersion >= GHC92 then (4,11) else (4, 6), "Couldn't match type")])] - else [("Foo.hs", [(DiagnosticSeverity_Error, (4, 6), "Couldn't match expected type")])] + expectDiagnostics + [("Foo.hs", [(DiagnosticSeverity_Error, (4,11), "Couldn't match type", Just "GHC-83865")])] -- Now modify the dependent file liftIO $ writeFile depFilePath "B" - sendNotification SMethod_WorkspaceDidChangeWatchedFiles $ DidChangeWatchedFilesParams $ - [FileEvent (filePathToUri "dep-file.txt") FileChangeType_Changed ] + sendNotification SMethod_WorkspaceDidChangeWatchedFiles $ DidChangeWatchedFilesParams + [FileEvent (filePathToUri depFilePath) FileChangeType_Changed ] -- Modifying Baz will now trigger Foo to be rebuilt as well - let change = TextDocumentContentChangeEvent $ InL $ #range .== Range (Position 2 0) (Position 2 6) - .+ #rangeLength .== Nothing - .+ #text .== "f = ()" + let change = TextDocumentContentChangeEvent $ InL TextDocumentContentChangePartial + { _range = Range (Position 2 0) (Position 2 6) + , _rangeLength = Nothing + , _text = "f = ()" + } changeDoc doc [change] expectDiagnostics [("Foo.hs", [])] diff --git a/ghcide/test/exe/DiagnosticTests.hs b/ghcide-test/exe/DiagnosticTests.hs similarity index 70% rename from ghcide/test/exe/DiagnosticTests.hs rename to ghcide-test/exe/DiagnosticTests.hs index 5a219f6c50..52aba0b9b7 100644 --- a/ghcide/test/exe/DiagnosticTests.hs +++ b/ghcide-test/exe/DiagnosticTests.hs @@ -1,6 +1,5 @@ -{-# LANGUAGE GADTs #-} -{-# LANGUAGE OverloadedLabels #-} +{-# LANGUAGE GADTs #-} module DiagnosticTests (tests) where @@ -9,7 +8,6 @@ import qualified Control.Lens as Lens import Control.Monad import Control.Monad.IO.Class (liftIO) import Data.List.Extra -import Data.Row import qualified Data.Text as T import Development.IDE.GHC.Compat (GhcVersion (..), ghcVersion) import Development.IDE.GHC.Util @@ -31,46 +29,57 @@ import Language.LSP.Test import System.Directory import System.FilePath import System.IO.Extra hiding (withTempDir) --- import Test.QuickCheck.Instances () + +import Config import Control.Lens ((^.)) import Control.Monad.Extra (whenJust) +import Data.Default (def) import Development.IDE.Plugin.Test (WaitForIdeRuleResult (..)) import System.Time.Extra +import Test.Hls (TestConfig (testConfigCaps, testDirLocation, testDisableKick, testPluginDescriptor), + runSessionWithTestConfig, + waitForProgressBegin) +import Test.Hls.FileSystem (directCradle, file, text) import Test.Tasty import Test.Tasty.HUnit -import TestUtils tests :: TestTree tests = testGroup "diagnostics" - [ testSessionWait "fix syntax error" $ do + [ testWithDummyPluginEmpty "fix syntax error" $ do let content = T.unlines [ "module Testing wher" ] doc <- createDoc "Testing.hs" "haskell" content - expectDiagnostics [("Testing.hs", [(DiagnosticSeverity_Error, (0, 15), "parse error")])] - let change = TextDocumentContentChangeEvent $ InL $ #range .== Range (Position 0 15) (Position 0 19) - .+ #rangeLength .== Nothing - .+ #text .== "where" + expectDiagnostics [("Testing.hs", [(DiagnosticSeverity_Error, (0, 15), "parse error", Just "GHC-58481")])] + let change = TextDocumentContentChangeEvent $ InL TextDocumentContentChangePartial + { _range = Range (Position 0 15) (Position 0 19) + , _rangeLength = Nothing + , _text = "where" + } changeDoc doc [change] expectDiagnostics [("Testing.hs", [])] - , testSessionWait "introduce syntax error" $ do + , testWithDummyPluginEmpty "introduce syntax error" $ do let content = T.unlines [ "module Testing where" ] doc <- createDoc "Testing.hs" "haskell" content void $ skipManyTill anyMessage (message SMethod_WindowWorkDoneProgressCreate) waitForProgressBegin - let change = TextDocumentContentChangeEvent$ InL $ #range .== Range (Position 0 15) (Position 0 18) - .+ #rangeLength .== Nothing - .+ #text .== "wher" + let change = TextDocumentContentChangeEvent$ InL TextDocumentContentChangePartial + { _range = Range (Position 0 15) (Position 0 18) + , _rangeLength = Nothing + , _text = "wher" + } changeDoc doc [change] - expectDiagnostics [("Testing.hs", [(DiagnosticSeverity_Error, (0, 15), "parse error")])] - , testSessionWait "update syntax error" $ do + expectDiagnostics [("Testing.hs", [(DiagnosticSeverity_Error, (0, 15), "parse error", Just "GHC-58481")])] + , testWithDummyPluginEmpty "update syntax error" $ do let content = T.unlines [ "module Testing(missing) where" ] doc <- createDoc "Testing.hs" "haskell" content - expectDiagnostics [("Testing.hs", [(DiagnosticSeverity_Error, (0, 15), "Not in scope: 'missing'")])] - let change = TextDocumentContentChangeEvent $ InL $ #range .== Range (Position 0 15) (Position 0 16) - .+ #rangeLength .== Nothing - .+ #text .== "l" + expectDiagnostics [("Testing.hs", [(DiagnosticSeverity_Error, (0, 15), "Not in scope: 'missing'", Just "GHC-76037")])] + let change = TextDocumentContentChangeEvent $ InL TextDocumentContentChangePartial + { _range = Range (Position 0 15) (Position 0 16) + , _rangeLength = Nothing + , _text = "l" + } changeDoc doc [change] - expectDiagnostics [("Testing.hs", [(DiagnosticSeverity_Error, (0, 15), "Not in scope: 'lissing'")])] - , testSessionWait "variable not in scope" $ do + expectDiagnostics [("Testing.hs", [(DiagnosticSeverity_Error, (0, 15), "Not in scope: 'lissing'", Just "GHC-76037")])] + , testWithDummyPluginEmpty "variable not in scope" $ do let content = T.unlines [ "module Testing where" , "foo :: Int -> Int -> Int" @@ -81,12 +90,12 @@ tests = testGroup "diagnostics" _ <- createDoc "Testing.hs" "haskell" content expectDiagnostics [ ( "Testing.hs" - , [ (DiagnosticSeverity_Error, (2, 15), "Variable not in scope: ab") - , (DiagnosticSeverity_Error, (4, 11), "Variable not in scope: cd") + , [ (DiagnosticSeverity_Error, (2, 15), "Variable not in scope: ab", Just "GHC-88464") + , (DiagnosticSeverity_Error, (4, 11), "Variable not in scope: cd", Just "GHC-88464") ] ) ] - , testSessionWait "type error" $ do + , testWithDummyPluginEmpty "type error" $ do let content = T.unlines [ "module Testing where" , "foo :: Int -> String -> Int" @@ -95,10 +104,10 @@ tests = testGroup "diagnostics" _ <- createDoc "Testing.hs" "haskell" content expectDiagnostics [ ( "Testing.hs" - , [(DiagnosticSeverity_Error, (2, 14), "Couldn't match type '[Char]' with 'Int'")] + , [(DiagnosticSeverity_Error, (2, 14), "Couldn't match type '[Char]' with 'Int'", Just "GHC-83865")] ) ] - , testSessionWait "typed hole" $ do + , testWithDummyPluginEmpty "typed hole" $ do let content = T.unlines [ "module Testing where" , "foo :: Int -> String" @@ -107,7 +116,7 @@ tests = testGroup "diagnostics" _ <- createDoc "Testing.hs" "haskell" content expectDiagnostics [ ( "Testing.hs" - , [(DiagnosticSeverity_Error, (2, 8), "Found hole: _ :: Int -> String")] + , [(DiagnosticSeverity_Error, (2, 8), "Found hole: _ :: Int -> String", Just "GHC-88464")] ) ] @@ -122,20 +131,20 @@ tests = testGroup "diagnostics" , "b :: Float" , "b = True"] bMessage = "Couldn't match expected type 'Float' with actual type 'Bool'" - expectedDs aMessage = - [ ("A.hs", [(DiagnosticSeverity_Error, (2,4), aMessage)]) - , ("B.hs", [(DiagnosticSeverity_Error, (3,4), bMessage)])] - deferralTest title binding msg = testSessionWait title $ do + expectedDs aMessage aCode = + [ ("A.hs", [(DiagnosticSeverity_Error, (2,4), aMessage, aCode)]) + , ("B.hs", [(DiagnosticSeverity_Error, (3,4), bMessage, Just "GHC-83865")])] + deferralTest title binding msg code = testWithDummyPluginEmpty title $ do _ <- createDoc "A.hs" "haskell" $ sourceA binding _ <- createDoc "B.hs" "haskell" sourceB - expectDiagnostics $ expectedDs msg + expectDiagnostics $ expectedDs msg code in - [ deferralTest "type error" "True" "Couldn't match expected type" - , deferralTest "typed hole" "_" "Found hole" - , deferralTest "out of scope var" "unbound" "Variable not in scope" + [ deferralTest "type error" "True" "Couldn't match expected type" (Just "GHC-83865") + , deferralTest "typed hole" "_" "Found hole" (Just "GHC-88464") + , deferralTest "out of scope var" "unbound" "Variable not in scope" (Just "GHC-88464") ] - , testSessionWait "remove required module" $ do + , testWithDummyPluginEmpty "remove required module" $ do let contentA = T.unlines [ "module ModuleA where" ] docA <- createDoc "ModuleA.hs" "haskell" contentA let contentB = T.unlines @@ -143,37 +152,44 @@ tests = testGroup "diagnostics" , "import ModuleA" ] _ <- createDoc "ModuleB.hs" "haskell" contentB - let change = TextDocumentContentChangeEvent $ InL $ #range .== Range (Position 0 0) (Position 0 20) - .+ #rangeLength .== Nothing - .+ #text .== "" + let change = TextDocumentContentChangeEvent $ InL TextDocumentContentChangePartial + { _range = Range (Position 0 0) (Position 0 20) + , _rangeLength = Nothing + , _text = "" + } changeDoc docA [change] - expectDiagnostics [("ModuleB.hs", [(DiagnosticSeverity_Error, (1, 0), "Could not find module")])] - , testSessionWait "add missing module" $ do + expectDiagnostics [("ModuleB.hs", [(DiagnosticSeverity_Error, (1, 0), "Could not find module", Nothing)])] + , testWithDummyPluginEmpty "add missing module" $ do let contentB = T.unlines [ "module ModuleB where" , "import ModuleA ()" ] _ <- createDoc "ModuleB.hs" "haskell" contentB - expectDiagnostics [("ModuleB.hs", [(DiagnosticSeverity_Error, (1, 7), "Could not find module")])] + expectDiagnostics [("ModuleB.hs", [(DiagnosticSeverity_Error, (1, 7), "Could not find module", Nothing)])] let contentA = T.unlines [ "module ModuleA where" ] _ <- createDoc "ModuleA.hs" "haskell" contentA expectDiagnostics [("ModuleB.hs", [])] , testCase "add missing module (non workspace)" $ + runSessionWithTestConfig def + { testPluginDescriptor = dummyPlugin + , testConfigCaps = lspTestCapsNoFileWatches + , testDirLocation = Right (mkIdeTestFs []) + } + $ \tmpDir -> do -- By default lsp-test sends FileWatched notifications for all files, which we don't want -- as non workspace modules will not be watched by the LSP server. -- To work around this, we tell lsp-test that our client doesn't have the -- FileWatched capability, which is enough to disable the notifications - withTempDir $ \tmpDir -> runInDir'' lspTestCapsNoFileWatches tmpDir "." "." [] $ do let contentB = T.unlines [ "module ModuleB where" , "import ModuleA ()" ] _ <- createDoc (tmpDir "ModuleB.hs") "haskell" contentB - expectDiagnostics [(tmpDir "ModuleB.hs", [(DiagnosticSeverity_Error, (1, 7), "Could not find module")])] + expectDiagnostics [(tmpDir "ModuleB.hs", [(DiagnosticSeverity_Error, (1, 7), "Could not find module", Nothing)])] let contentA = T.unlines [ "module ModuleA where" ] _ <- createDoc (tmpDir "ModuleA.hs") "haskell" contentA expectDiagnostics [(tmpDir "ModuleB.hs", [])] - , testSessionWait "cyclic module dependency" $ do + , testWithDummyPluginEmpty "cyclic module dependency" $ do let contentA = T.unlines [ "module ModuleA where" , "import ModuleB" @@ -186,34 +202,30 @@ tests = testGroup "diagnostics" _ <- createDoc "ModuleB.hs" "haskell" contentB expectDiagnostics [ ( "ModuleA.hs" - , [(DiagnosticSeverity_Error, (1, 7), "Cyclic module dependency between ModuleA, ModuleB")] + , [(DiagnosticSeverity_Error, (1, 7), "Cyclic module dependency between ModuleA, ModuleB", Nothing)] ) , ( "ModuleB.hs" - , [(DiagnosticSeverity_Error, (1, 7), "Cyclic module dependency between ModuleA, ModuleB")] + , [(DiagnosticSeverity_Error, (1, 7), "Cyclic module dependency between ModuleA, ModuleB", Nothing)] ) ] - , testSession' "deeply nested cyclic module dependency" $ \path -> do - let contentA = unlines - [ "module ModuleA where" , "import ModuleB" ] - let contentB = unlines - [ "module ModuleB where" , "import ModuleA" ] - let contentC = unlines - [ "module ModuleC where" , "import ModuleB" ] - let contentD = T.unlines - [ "module ModuleD where" , "import ModuleC" ] - cradle = - "cradle: {direct: {arguments: [ModuleA, ModuleB, ModuleC, ModuleD]}}" - liftIO $ writeFile (path "ModuleA.hs") contentA - liftIO $ writeFile (path "ModuleB.hs") contentB - liftIO $ writeFile (path "ModuleC.hs") contentC - liftIO $ writeFile (path "hie.yaml") cradle + , let contentA = T.unlines [ "module ModuleA where" , "import ModuleB" ] + contentB = T.unlines [ "module ModuleB where" , "import ModuleA" ] + contentC = T.unlines [ "module ModuleC where" , "import ModuleB" ] + contentD = T.unlines [ "module ModuleD where" , "import ModuleC" ] + cradle = directCradle ["ModuleA", "ModuleB", "ModuleC", "ModuleD"] + in testWithDummyPlugin "deeply nested cyclic module dependency" + (mkIdeTestFs [ + file "ModuleA.hs" (text contentA) + ,file "ModuleB.hs" (text contentB) + ,file "ModuleC.hs" (text contentC) + ,cradle + ]) $ do _ <- createDoc "ModuleD.hs" "haskell" contentD expectDiagnostics - [ ( "ModuleB.hs" - , [(DiagnosticSeverity_Error, (1, 7), "Cyclic module dependency between ModuleA, ModuleB")] - ) + [ ( "ModuleB.hs" , [(DiagnosticSeverity_Error, (1, 7), "Cyclic module dependency between ModuleA, ModuleB", Nothing)]) + , ( "ModuleA.hs" , [(DiagnosticSeverity_Error, (1, 7), "Cyclic module dependency between ModuleA, ModuleB", Nothing)]) ] - , testSessionWait "cyclic module dependency with hs-boot" $ do + , testWithDummyPluginEmpty "cyclic module dependency with hs-boot" $ do let contentA = T.unlines [ "module ModuleA where" , "import {-# SOURCE #-} ModuleB" @@ -231,8 +243,33 @@ tests = testGroup "diagnostics" _ <- createDoc "ModuleA.hs" "haskell" contentA _ <- createDoc "ModuleB.hs" "haskell" contentB _ <- createDoc "ModuleB.hs-boot" "haskell" contentBboot - expectDiagnostics [("ModuleB.hs", [(DiagnosticSeverity_Warning, (3,0), "Top-level binding")])] - , testSessionWait "correct reference used with hs-boot" $ do + expectDiagnostics [("ModuleB.hs", [(DiagnosticSeverity_Warning, (3,0), "Top-level binding", Just "GHC-38417")])] + , testWithDummyPlugin "bidirectional module dependency with hs-boot" + (mkIdeTestFs [directCradle ["ModuleA", "ModuleB"]]) + $ do + let contentA = T.unlines + [ "module ModuleA where" + , "import {-# SOURCE #-} ModuleB" + ] + let contentB = T.unlines + [ "{-# OPTIONS -Wmissing-signatures#-}" + , "module ModuleB where" + , "import {-# SOURCE #-} ModuleA" + -- introduce an artificial diagnostic + , "foo = ()" + ] + let contentBboot = T.unlines + [ "module ModuleB where" + ] + let contentAboot = T.unlines + [ "module ModuleA where" + ] + _ <- createDoc "ModuleA.hs" "haskell" contentA + _ <- createDoc "ModuleA.hs-boot" "haskell" contentAboot + _ <- createDoc "ModuleB.hs" "haskell" contentB + _ <- createDoc "ModuleB.hs-boot" "haskell" contentBboot + expectDiagnostics [("ModuleB.hs", [(DiagnosticSeverity_Warning, (3,0), "Top-level binding", Just "GHC-38417")])] + , testWithDummyPluginEmpty "correct reference used with hs-boot" $ do let contentB = T.unlines [ "module ModuleB where" , "import {-# SOURCE #-} ModuleA()" @@ -257,8 +294,8 @@ tests = testGroup "diagnostics" _ <- createDoc "ModuleA.hs" "haskell" contentA _ <- createDoc "ModuleA.hs-boot" "haskell" contentAboot _ <- createDoc "ModuleC.hs" "haskell" contentC - expectDiagnostics [("ModuleC.hs", [(DiagnosticSeverity_Warning, (3,0), "Top-level binding")])] - , testSessionWait "redundant import" $ do + expectDiagnostics [("ModuleC.hs", [(DiagnosticSeverity_Warning, (3,0), "Top-level binding", Just "GHC-38417")])] + , testWithDummyPluginEmpty "redundant import" $ do let contentA = T.unlines ["module ModuleA where"] let contentB = T.unlines [ "{-# OPTIONS_GHC -Wunused-imports #-}" @@ -269,10 +306,10 @@ tests = testGroup "diagnostics" _ <- createDoc "ModuleB.hs" "haskell" contentB expectDiagnosticsWithTags [ ( "ModuleB.hs" - , [(DiagnosticSeverity_Warning, (2, 0), "The import of 'ModuleA' is redundant", Just DiagnosticTag_Unnecessary)] + , [(DiagnosticSeverity_Warning, (2, 0), "The import of 'ModuleA' is redundant", Nothing, Just DiagnosticTag_Unnecessary)] ) ] - , testSessionWait "redundant import even without warning" $ do + , testWithDummyPluginEmpty "redundant import even without warning" $ do let contentA = T.unlines ["module ModuleA where"] let contentB = T.unlines [ "{-# OPTIONS_GHC -Wno-unused-imports -Wmissing-signatures #-}" @@ -283,8 +320,8 @@ tests = testGroup "diagnostics" ] _ <- createDoc "ModuleA.hs" "haskell" contentA _ <- createDoc "ModuleB.hs" "haskell" contentB - expectDiagnostics [("ModuleB.hs", [(DiagnosticSeverity_Warning, (3,0), "Top-level binding")])] - , testSessionWait "package imports" $ do + expectDiagnostics [("ModuleB.hs", [(DiagnosticSeverity_Warning, (3,0), "Top-level binding", Just "GHC-38417")])] + , testWithDummyPluginEmpty "package imports" $ do let thisDataListContent = T.unlines [ "module Data.List where" , "x :: Integer" @@ -306,23 +343,13 @@ tests = testGroup "diagnostics" expectDiagnostics [ ( "Main.hs" , [(DiagnosticSeverity_Error, (6, 9), - if ghcVersion >= GHC96 then - "Variable not in scope: ThisList.map" - else if ghcVersion >= GHC94 then - "Variable not in scope: map" -- See https://gitlab.haskell.org/ghc/ghc/-/issues/22130 - else - "Not in scope: \8216ThisList.map\8217") + "Variable not in scope: ThisList.map", Just "GHC-88464") ,(DiagnosticSeverity_Error, (7, 9), - if ghcVersion >= GHC96 then - "Variable not in scope: BaseList.x" - else if ghcVersion >= GHC94 then - "Variable not in scope: x" -- See https://gitlab.haskell.org/ghc/ghc/-/issues/22130 - else - "Not in scope: \8216BaseList.x\8217") + "Variable not in scope: BaseList.x", Just "GHC-88464") ] ) ] - , testSessionWait "unqualified warnings" $ do + , testWithDummyPluginEmpty "unqualified warnings" $ do let fooContent = T.unlines [ "{-# OPTIONS_GHC -Wredundant-constraints #-}" , "module Foo where" @@ -336,11 +363,11 @@ tests = testGroup "diagnostics" -- where appropriate. The warning should use an unqualified name 'Ord', not -- something like 'GHC.Classes.Ord'. The choice of redundant-constraints to -- test this is fairly arbitrary. - , [(DiagnosticSeverity_Warning, (2, if ghcVersion >= GHC94 then 7 else 0), "Redundant constraint: Ord a") + , [(DiagnosticSeverity_Warning, (2, 7), "Redundant constraint: Ord a", Just "GHC-30606") ] ) ] - , testSessionWait "lower-case drive" $ do + , testWithDummyPluginEmpty "lower-case drive" $ do let aContent = T.unlines [ "module A.A where" , "import A.B ()" @@ -369,27 +396,11 @@ tests = testGroup "diagnostics" -- Check that if we put a lower-case drive in for A.A -- the diagnostics for A.B will also be lower-case. liftIO $ fileUri @?= uriB - let msg :: T.Text = (head diags) ^. L.message + let msg :: T.Text = head diags ^. L.message liftIO $ unless ("redundant" `T.isInfixOf` msg) $ assertFailure ("Expected redundant import but got " <> T.unpack msg) closeDoc a - , testSessionWait "haddock parse error" $ do - let fooContent = T.unlines - [ "module Foo where" - , "foo :: Int" - , "foo = 1 {-|-}" - ] - _ <- createDoc "Foo.hs" "haskell" fooContent - if ghcVersion >= GHC90 then - -- Haddock parse errors are ignored on ghc-9.0 - pure () - else - expectDiagnostics - [ ( "Foo.hs" - , [(DiagnosticSeverity_Warning, (2, 8), "Haddock parse error on input")] - ) - ] - , testSessionWait "strip file path" $ do + , testWithDummyPluginEmpty "strip file path" $ do let name = "Testing" content = T.unlines @@ -408,9 +419,9 @@ tests = testGroup "diagnostics" Lens.filtered (T.isInfixOf ("/" <> name <> ".hs:")) failure msg = liftIO $ assertFailure $ "Expected file path to be stripped but got " <> T.unpack msg Lens.mapMOf_ offenders failure notification - , testSession' "-Werror in cradle is ignored" $ \sessionDir -> do - liftIO $ writeFile (sessionDir "hie.yaml") - "cradle: {direct: {arguments: [\"-Wall\", \"-Werror\"]}}" + , testWithDummyPlugin "-Werror in cradle is ignored" + (mkIdeTestFs [directCradle ["-Wall", "-Werror"]]) + $ do let fooContent = T.unlines [ "module Foo where" , "foo = ()" @@ -418,11 +429,11 @@ tests = testGroup "diagnostics" _ <- createDoc "Foo.hs" "haskell" fooContent expectDiagnostics [ ( "Foo.hs" - , [(DiagnosticSeverity_Warning, (1, 0), "Top-level binding with no type signature:") + , [(DiagnosticSeverity_Warning, (1, 0), "Top-level binding with no type signature:", Nothing) ] ) ] - , testSessionWait "-Werror in pragma is ignored" $ do + , testWithDummyPluginEmpty "-Werror in pragma is ignored" $ do let fooContent = T.unlines [ "{-# OPTIONS_GHC -Wall -Werror #-}" , "module Foo() where" @@ -432,7 +443,7 @@ tests = testGroup "diagnostics" _ <- createDoc "Foo.hs" "haskell" fooContent expectDiagnostics [ ( "Foo.hs" - , [(DiagnosticSeverity_Warning, (3, 0), "Defined but not used:") + , [(DiagnosticSeverity_Warning, (3, 0), "Defined but not used:", Nothing) ] ) ] @@ -448,41 +459,41 @@ tests = testGroup "diagnostics" bdoc <- createDoc bPath "haskell" bSource _pdoc <- createDoc pPath "haskell" pSource expectDiagnostics - [("P.hs", [(DiagnosticSeverity_Warning,(4,0), "Top-level binding")])] -- So that we know P has been loaded + [("P.hs", [(DiagnosticSeverity_Warning,(4,0), "Top-level binding", Just "GHC-38417")])] -- So that we know P has been loaded -- Change y from Int to B which introduces a type error in A (imported from P) - changeDoc bdoc [TextDocumentContentChangeEvent . InR . (.==) #text $ + changeDoc bdoc [TextDocumentContentChangeEvent . InR . TextDocumentContentChangeWholeDocument $ T.unlines ["module B where", "y :: Bool", "y = undefined"]] expectDiagnostics - [("A.hs", [(DiagnosticSeverity_Error, (5, 4), "Couldn't match expected type 'Int' with actual type 'Bool'")]) + [("A.hs", [(DiagnosticSeverity_Error, (5, 4), "Couldn't match expected type 'Int' with actual type 'Bool'", Just "GHC-83865")]) ] -- Open A and edit to fix the type error adoc <- createDoc aPath "haskell" aSource - changeDoc adoc [TextDocumentContentChangeEvent . InR . (.==) #text $ + changeDoc adoc [TextDocumentContentChangeEvent . InR . TextDocumentContentChangeWholeDocument $ T.unlines ["module A where", "import B", "x :: Bool", "x = y"]] expectDiagnostics [ ( "P.hs", - [ (DiagnosticSeverity_Error, (4, 6), "Couldn't match expected type 'Int' with actual type 'Bool'"), - (DiagnosticSeverity_Warning, (4, 0), "Top-level binding") + [ (DiagnosticSeverity_Error, (4, 6), "Couldn't match expected type 'Int' with actual type 'Bool'", Just "GHC-83865"), + (DiagnosticSeverity_Warning, (4, 0), "Top-level binding", Just "GHC-38417") ] ), ("A.hs", []) ] expectNoMoreDiagnostics 1 - , testSessionWait "deduplicate missing module diagnostics" $ do + , testWithDummyPluginEmpty "deduplicate missing module diagnostics" $ do let fooContent = T.unlines [ "module Foo() where" , "import MissingModule" ] doc <- createDoc "Foo.hs" "haskell" fooContent - expectDiagnostics [("Foo.hs", [(DiagnosticSeverity_Error, (1,7), "Could not find module 'MissingModule'")])] + expectDiagnostics [("Foo.hs", [(DiagnosticSeverity_Error, (1,7), "Could not find module 'MissingModule'", Nothing)])] - changeDoc doc [TextDocumentContentChangeEvent . InR . (.==) #text $ "module Foo() where" ] + changeDoc doc [TextDocumentContentChangeEvent . InR . TextDocumentContentChangeWholeDocument $ "module Foo() where" ] expectDiagnostics [] - changeDoc doc [TextDocumentContentChangeEvent . InR . (.==) #text $ T.unlines + changeDoc doc [TextDocumentContentChangeEvent . InR . TextDocumentContentChangeWholeDocument $ T.unlines [ "module Foo() where" , "import MissingModule" ] ] - expectDiagnostics [("Foo.hs", [(DiagnosticSeverity_Error, (1,7), "Could not find module 'MissingModule'")])] + expectDiagnostics [("Foo.hs", [(DiagnosticSeverity_Error, (1,7), "Could not find module 'MissingModule'", Nothing)])] , testGroup "Cancellation" [ cancellationTestGroup "edit header" editHeader yesSession noParse noTc @@ -492,12 +503,18 @@ tests = testGroup "diagnostics" ] where editPair x y = let p = Position x y ; p' = Position x (y+2) in - (TextDocumentContentChangeEvent $ InL $ #range .== Range p p - .+ #rangeLength .== Nothing - .+ #text .== "fd" - ,TextDocumentContentChangeEvent $ InL $ #range .== Range p p' - .+ #rangeLength .== Nothing - .+ #text .== "") + (TextDocumentContentChangeEvent $ InL TextDocumentContentChangePartial + { _range = Range p p + , _rangeLength = Nothing + , _text = "fd" + } + + ,TextDocumentContentChangeEvent $ InL TextDocumentContentChangePartial + { _range = Range p p' + , _rangeLength = Nothing + , _text = "" + } + ) editHeader = editPair 0 0 editImport = editPair 2 10 editBody = editPair 3 10 @@ -537,7 +554,7 @@ cancellationTemplate (edit, undoEdit) mbKey = testCase (maybe "-" fst mbKey) $ r ] -- for the example above we expect one warning - let missingSigDiags = [(DiagnosticSeverity_Warning, (3, 0), "Top-level binding") ] + let missingSigDiags = [(DiagnosticSeverity_Warning, (3, 0), "Top-level binding", Just "GHC-38417") ] typeCheck doc >> expectCurrentDiagnostics doc missingSigDiags -- Now we edit the document and wait for the given key (if any) @@ -553,8 +570,12 @@ cancellationTemplate (edit, undoEdit) mbKey = testCase (maybe "-" fst mbKey) $ r expectNoMoreDiagnostics 0.5 where - -- similar to run except it disables kick - runTestNoKick s = withTempDir $ \dir -> runInDir' dir "." "." ["--test-no-kick"] s + runTestNoKick s = + runSessionWithTestConfig def + { testPluginDescriptor = dummyPlugin + , testDirLocation = Right (mkIdeTestFs []) + , testDisableKick = True + } $ const s typeCheck doc = do WaitForIdeRuleResult {..} <- waitForAction "TypeCheck" doc diff --git a/ghcide/test/exe/ExceptionTests.hs b/ghcide-test/exe/ExceptionTests.hs similarity index 59% rename from ghcide/test/exe/ExceptionTests.hs rename to ghcide-test/exe/ExceptionTests.hs index a528cb29ad..a95f91e97c 100644 --- a/ghcide/test/exe/ExceptionTests.hs +++ b/ghcide-test/exe/ExceptionTests.hs @@ -7,19 +7,17 @@ import Control.Lens import Control.Monad.Error.Class (MonadError (throwError)) import Control.Monad.IO.Class (liftIO) import qualified Data.Aeson as A -import Data.Text as T +import Data.Default (Default (..)) +import qualified Data.Text as T import Development.IDE.Core.Shake (IdeState (..)) import qualified Development.IDE.LSP.Notifications as Notifications -import qualified Development.IDE.Main as IDE import Development.IDE.Plugin.HLS (toResponseError) -import Development.IDE.Plugin.Test as Test -import Development.IDE.Types.Options import GHC.Base (coerce) -import Ide.Logger (Logger, Recorder, - WithPriority, cmapWithPrio) +import Ide.Logger (Recorder, WithPriority, + cmapWithPrio) import Ide.Plugin.Error -import Ide.PluginUtils (idePluginsToPluginDesc, - pluginDescToIdePlugins) +import Ide.Plugin.HandleRequestTypes (RejectionReason (DisabledGlobally)) +import Ide.PluginUtils (pluginDescToIdePlugins) import Ide.Types import qualified Language.LSP.Protocol.Lens as L import Language.LSP.Protocol.Message @@ -30,61 +28,67 @@ import Language.LSP.Protocol.Types hiding mkRange) import Language.LSP.Test import LogType (Log (..)) +import Test.Hls (TestConfig (testDisableDefaultPlugin, testPluginDescriptor), + runSessionWithTestConfig, + testCheckProject, + waitForProgressDone) import Test.Tasty import Test.Tasty.HUnit -import TestUtils -tests :: Recorder (WithPriority Log) -> Logger -> TestTree -tests recorder logger = do +tests :: TestTree +tests = do testGroup "Exceptions and PluginError" [ testGroup "Testing that IO Exceptions are caught in..." [ testCase "PluginHandlers" $ do let pluginId = "plugin-handler-exception" - plugins = pluginDescToIdePlugins $ - [ (defaultPluginDescriptor pluginId) + plugins :: Recorder (WithPriority Log) -> IdePlugins IdeState + plugins r = pluginDescToIdePlugins $ + [ (defaultPluginDescriptor pluginId "") { pluginHandlers = mconcat [ mkPluginHandler SMethod_TextDocumentCodeLens $ \_ _ _-> do _ <- liftIO $ throwIO DivideByZero pure (InL []) ] - }] - testIde recorder (testingLite recorder logger plugins) $ do + }] ++ [Notifications.descriptor (cmapWithPrio LogNotifications r) "ghcide-core"] + runSessionWithTestConfig def {testPluginDescriptor = plugins, testDisableDefaultPlugin=True, testCheckProject=False + } $ const $ do doc <- createDoc "A.hs" "haskell" "module A where" - waitForProgressDone (view L.result -> lens) <- request SMethod_TextDocumentCodeLens (CodeLensParams Nothing Nothing doc) case lens of - Left (ResponseError {_code = InR ErrorCodes_InternalError, _message}) -> - liftIO $ assertBool "We caught an error, but it wasn't ours!" + Left (TResponseError {_code = InR ErrorCodes_InternalError, _message}) -> + liftIO $ assertBool "We caught an error, but it wasn't ours!" (T.isInfixOf "divide by zero" _message && T.isInfixOf (coerce pluginId) _message) _ -> liftIO $ assertFailure $ show lens , testCase "Commands" $ do let pluginId = "command-exception" commandId = CommandId "exception" - plugins = pluginDescToIdePlugins $ - [ (defaultPluginDescriptor pluginId) + plugins :: Recorder (WithPriority Log) -> IdePlugins IdeState + plugins r = pluginDescToIdePlugins $ + [ (defaultPluginDescriptor pluginId "") { pluginCommands = - [ PluginCommand commandId "Causes an exception" $ \_ (_::Int) -> do + [ PluginCommand commandId "Causes an exception" $ \_ _ (_::Int) -> do _ <- liftIO $ throwIO DivideByZero pure (InR Null) ] - }] - testIde recorder (testingLite recorder logger plugins) $ do + }] ++ [Notifications.descriptor (cmapWithPrio LogNotifications r) "ghcide-core"] + runSessionWithTestConfig def {testPluginDescriptor = plugins, testDisableDefaultPlugin=True, testCheckProject=False} $ const $ do _ <- createDoc "A.hs" "haskell" "module A where" waitForProgressDone let cmd = mkLspCommand (coerce pluginId) commandId "" (Just [A.toJSON (1::Int)]) execParams = ExecuteCommandParams Nothing (cmd ^. L.command) (cmd ^. L.arguments) (view L.result -> res) <- request SMethod_WorkspaceExecuteCommand execParams case res of - Left (ResponseError {_code = InR ErrorCodes_InternalError, _message}) -> + Left (TResponseError {_code = InR ErrorCodes_InternalError, _message}) -> liftIO $ assertBool "We caught an error, but it wasn't ours!" (T.isInfixOf "divide by zero" _message && T.isInfixOf (coerce pluginId) _message) _ -> liftIO $ assertFailure $ show res , testCase "Notification Handlers" $ do let pluginId = "notification-exception" - plugins = pluginDescToIdePlugins $ - [ (defaultPluginDescriptor pluginId) + plugins :: Recorder (WithPriority Log) -> IdePlugins IdeState + plugins r = pluginDescToIdePlugins $ + [ (defaultPluginDescriptor pluginId "") { pluginNotificationHandlers = mconcat [ mkPluginNotificationHandler SMethod_TextDocumentDidOpen $ \_ _ _ _ -> liftIO $ throwIO DivideByZero @@ -93,8 +97,8 @@ tests recorder logger = do [ mkPluginHandler SMethod_TextDocumentCodeLens $ \_ _ _-> do pure (InL []) ] - }] - testIde recorder (testingLite recorder logger plugins) $ do + }] ++ [Notifications.descriptor (cmapWithPrio LogNotifications r) "ghcide-core"] + runSessionWithTestConfig def {testPluginDescriptor = plugins, testDisableDefaultPlugin=True, testCheckProject=False} $ const $ do doc <- createDoc "A.hs" "haskell" "module A where" waitForProgressDone (view L.result -> lens) <- request SMethod_TextDocumentCodeLens (CodeLensParams Nothing Nothing doc) @@ -106,50 +110,31 @@ tests recorder logger = do _ -> liftIO $ assertFailure $ "We should have had an empty list" <> show lens] , testGroup "Testing PluginError order..." - [ pluginOrderTestCase recorder logger "InternalError over InvalidParams" PluginInternalError PluginInvalidParams - , pluginOrderTestCase recorder logger "InvalidParams over InvalidUserState" PluginInvalidParams PluginInvalidUserState - , pluginOrderTestCase recorder logger "InvalidUserState over RequestRefused" PluginInvalidUserState PluginRequestRefused + [ pluginOrderTestCase "InternalError over InvalidParams" (PluginInternalError "error test") (PluginInvalidParams "error test") + , pluginOrderTestCase "InvalidParams over InvalidUserState" (PluginInvalidParams "error test") (PluginInvalidUserState "error test") + , pluginOrderTestCase "InvalidUserState over RequestRefused" (PluginInvalidUserState "error test") (PluginRequestRefused DisabledGlobally) ] ] -testingLite :: Recorder (WithPriority Log) -> Logger -> IdePlugins IdeState -> IDE.Arguments -testingLite recorder logger plugins = - let - arguments@IDE.Arguments{ argsIdeOptions } = - IDE.defaultArguments (cmapWithPrio LogIDEMain recorder) logger plugins - hlsPlugins = pluginDescToIdePlugins $ - idePluginsToPluginDesc plugins - ++ [Notifications.descriptor (cmapWithPrio LogNotifications recorder) "ghcide-core"] - ++ [Test.blockCommandDescriptor "block-command", Test.plugin] - ideOptions config sessionLoader = - let - defOptions = argsIdeOptions config sessionLoader - in - defOptions{ optTesting = IdeTesting True } - in - arguments - { IDE.argsHlsPlugins = hlsPlugins - , IDE.argsIdeOptions = ideOptions - } - -pluginOrderTestCase :: Recorder (WithPriority Log) -> Logger -> TestName -> (T.Text -> PluginError) -> (T.Text -> PluginError) -> TestTree -pluginOrderTestCase recorder logger msg err1 err2 = +pluginOrderTestCase :: TestName -> PluginError -> PluginError -> TestTree +pluginOrderTestCase msg err1 err2 = testCase msg $ do let pluginId = "error-order-test" - plugins = pluginDescToIdePlugins $ - [ (defaultPluginDescriptor pluginId) + plugins :: Recorder (WithPriority Log) -> IdePlugins IdeState + plugins r = pluginDescToIdePlugins $ + [ (defaultPluginDescriptor pluginId "") { pluginHandlers = mconcat [ mkPluginHandler SMethod_TextDocumentCodeLens $ \_ _ _-> do - throwError $ err1 "error test" + throwError err1 ,mkPluginHandler SMethod_TextDocumentCodeLens $ \_ _ _-> do - throwError $ err2 "error test" + throwError err2 ] - }] - testIde recorder (testingLite recorder logger plugins) $ do + }] ++ [Notifications.descriptor (cmapWithPrio LogNotifications r) "ghcide-core"] + runSessionWithTestConfig def {testPluginDescriptor = plugins, testDisableDefaultPlugin=True, testCheckProject=False} $ const $ do doc <- createDoc "A.hs" "haskell" "module A where" waitForProgressDone (view L.result -> lens) <- request SMethod_TextDocumentCodeLens (CodeLensParams Nothing Nothing doc) case lens of - Left re | toResponseError (pluginId, err1 "error test") == re -> pure () + Left re | toResponseError (pluginId, err1) == re -> pure () | otherwise -> liftIO $ assertFailure "We caught an error, but it wasn't ours!" _ -> liftIO $ assertFailure $ show lens diff --git a/ghcide-test/exe/FindDefinitionAndHoverTests.hs b/ghcide-test/exe/FindDefinitionAndHoverTests.hs new file mode 100644 index 0000000000..7920ff4949 --- /dev/null +++ b/ghcide-test/exe/FindDefinitionAndHoverTests.hs @@ -0,0 +1,257 @@ +{-# LANGUAGE ExplicitNamespaces #-} +{-# LANGUAGE OverloadedStrings #-} +{-# LANGUAGE ViewPatterns #-} + +module FindDefinitionAndHoverTests (tests) where + +import Control.Monad +import Data.Foldable +import Data.Maybe +import qualified Data.Text as T +import qualified Language.LSP.Protocol.Lens as L +import Language.LSP.Test +import System.Info.Extra (isWindows) + +import Config +import Control.Category ((>>>)) +import Control.Lens ((^.)) +import Development.IDE.Test (expectDiagnostics, + standardizeQuotes) +import Test.Hls +import Test.Hls.FileSystem (copyDir) +import Text.Regex.TDFA ((=~)) + +tests :: TestTree +tests = let + tst :: (TextDocumentIdentifier -> Position -> Session a, a -> Session [Expect] -> Session ()) -> Position -> String -> Session [Expect] -> String -> TestTree + tst (get, check) pos sfp targetRange title = + testWithDummyPlugin title (mkIdeTestFs [copyDir "hover"]) $ do + doc <- openDoc sfp "haskell" + waitForProgressDone + _x <- waitForTypecheck doc + found <- get doc pos + check found targetRange + + + + checkHover :: (HasCallStack) => Maybe Hover -> Session [Expect] -> Session () + checkHover hover expectations = traverse_ check =<< expectations where + + check :: (HasCallStack) => Expect -> Session () + check expected = + case hover of + Nothing -> unless (expected == ExpectNoHover) $ liftIO $ assertFailure "no hover found" + Just Hover{_contents = (InL MarkupContent{_value = standardizeQuotes -> msg}) + ,_range = rangeInHover } -> + case expected of + ExpectRange expectedRange -> checkHoverRange expectedRange rangeInHover msg + ExpectHoverRange expectedRange -> checkHoverRange expectedRange rangeInHover msg + ExpectHoverText snippets -> liftIO $ traverse_ (`assertFoundIn` msg) snippets + ExpectHoverExcludeText snippets -> liftIO $ traverse_ (`assertNotFoundIn` msg) snippets + ExpectHoverTextRegex re -> liftIO $ assertBool ("Regex not found in " <> T.unpack msg) (msg =~ re :: Bool) + ExpectNoHover -> liftIO $ assertFailure $ "Expected no hover but got " <> show hover + _ -> pure () -- all other expectations not relevant to hover + _ -> liftIO $ assertFailure $ "test not expecting this kind of hover info" <> show hover + + extractLineColFromHoverMsg :: T.Text -> [T.Text] + extractLineColFromHoverMsg = + -- Hover messages contain multiple lines, and we are looking for the definition + -- site + T.lines + -- The line we are looking for looks like: "*Defined at /tmp/GotoHover.hs:22:3*" + -- So filter by the start of the line + >>> mapMaybe (T.stripPrefix "*Defined at") + -- There can be multiple definitions per hover message! + -- See the test "field in record definition" for example. + -- The tests check against the last line that contains the above line. + >>> last + -- [" /tmp/", "22:3*"] + >>> T.splitOn (sourceFileName <> ":") + -- "22:3*" + >>> last + -- ["22:3", ""] + >>> T.splitOn "*" + -- "22:3" + >>> head + -- ["22", "3"] + >>> T.splitOn ":" + + checkHoverRange :: Range -> Maybe Range -> T.Text -> Session () + checkHoverRange expectedRange rangeInHover msg = + let + lineCol = extractLineColFromHoverMsg msg + -- looks like hovers use 1-based numbering while definitions use 0-based + -- turns out that they are stored 1-based in RealSrcLoc by GHC itself. + adjust Position{_line = l, _character = c} = + Position{_line = l + 1, _character = c + 1} + in + case map (read . T.unpack) lineCol of + [l,c] -> liftIO $ adjust (expectedRange ^. L.start) @=? Position l c + _ -> liftIO $ assertFailure $ + "expected: " <> show ("[...]" <> sourceFileName <> "::**[...]", Just expectedRange) <> + "\n but got: " <> show (msg, rangeInHover) + + assertFoundIn :: T.Text -> T.Text -> Assertion + assertFoundIn part whole = assertBool + (T.unpack $ "failed to find: `" <> part <> "` in hover message:\n" <> whole) + (part `T.isInfixOf` whole) + + assertNotFoundIn :: T.Text -> T.Text -> Assertion + assertNotFoundIn part whole = assertBool + (T.unpack $ "found unexpected: `" <> part <> "` in hover message:\n" <> whole) + (not . T.isInfixOf part $ whole) + + sourceFilePath = T.unpack sourceFileName + sourceFileName = "GotoHover.hs" + + mkFindTests tests = testGroup "get" + [ testGroup "definition" $ mapMaybe fst tests + , testGroup "hover" $ mapMaybe snd tests + , testGroup "hover compile" [checkFileCompiles sourceFilePath $ + expectDiagnostics + [ ( "GotoHover.hs", [(DiagnosticSeverity_Error, (62, 7), "Found hole: _", Just "GHC-88464")]) + , ( "GotoHover.hs", [(DiagnosticSeverity_Error, (65, 8), "Found hole: _", Just "GHC-88464")]) + ]] + , testGroup "type-definition" typeDefinitionTests + , testGroup "hover-record-dot-syntax" recordDotSyntaxTests ] + + typeDefinitionTests = [ tst (getTypeDefinitions, checkDefs) aaaL14 sourceFilePath (pure tcData) "Saturated data con" + , tst (getTypeDefinitions, checkDefs) aL20 sourceFilePath (pure [ExpectNoDefinitions]) "Polymorphic variable"] + + recordDotSyntaxTests = + [ tst (getHover, checkHover) (Position 17 24) (T.unpack "RecordDotSyntax.hs") (pure [ExpectHoverText ["x :: MyRecord"]]) "hover over parent" + , tst (getHover, checkHover) (Position 17 25) (T.unpack "RecordDotSyntax.hs") (pure [ExpectHoverText ["_ :: MyChild"]]) "hover over dot shows child" + , tst (getHover, checkHover) (Position 17 26) (T.unpack "RecordDotSyntax.hs") (pure [ExpectHoverText ["_ :: MyChild"]]) "hover over child" + ] + + test :: (HasCallStack) => (TestTree -> a) -> (TestTree -> b) -> Position -> [Expect] -> String -> (a, b) + test runDef runHover look expect = testM runDef runHover look (return expect) + + testM :: (HasCallStack) => (TestTree -> a) + -> (TestTree -> b) + -> Position + -> Session [Expect] + -> String + -> (a, b) + testM runDef runHover look expect title = + ( runDef $ tst def look sourceFilePath expect title + , runHover $ tst hover look sourceFilePath expect title ) where + def = (getDefinitions, checkDefs) + hover = (getHover , checkHover) + + -- search locations expectations on results + -- TODO: Lookup of record field should return exactly one result + fffL4 = fffR ^. L.start; fffR = mkRange 8 4 8 7; fff = [ExpectRanges [fffR, mkRange 7 23 9 16]] + fffL8 = Position 12 4 ; fff' = [ExpectRange fffR] + fffL14 = Position 18 7 ; + aL20 = Position 19 15 + aaaL14 = Position 18 20 ; aaa = [mkR 11 0 11 3] + dcL7 = Position 11 11 ; tcDC = [mkR 7 23 9 16] + dcL12 = Position 16 11 ; + xtcL5 = Position 9 11 ; xtc = [ExpectHoverText ["Int", "Defined in ", "GHC.Types", "ghc-prim"]] + tcL6 = Position 10 11 ; tcData = [mkR 7 0 9 16, ExpectHoverText ["TypeConstructor", "GotoHover.hs:8:1"]] + vvL16 = Position 20 12 ; vv = [mkR 20 4 20 6] + opL16 = Position 20 15 ; op = [mkR 21 2 21 4] + opL18 = Position 22 22 ; opp = [mkR 22 13 22 17] + aL18 = Position 22 20 ; apmp = [mkR 22 10 22 11] + b'L19 = Position 23 13 ; bp = [mkR 23 6 23 7] + xvL20 = Position 24 8 ; xvMsg = [ExpectHoverText ["pack", ":: String -> Text", "Data.Text", "text"]] + clL23 = Position 27 11 ; cls = [mkR 25 0 26 20, ExpectHoverText ["MyClass", "GotoHover.hs:26:1"]] + clL25 = Position 29 9 + eclL15 = Position 19 8 ; ecls = [ExpectHoverText ["Num", "Defined in ", if ghcVersion < GHC910 then "GHC.Num" else "GHC.Internal.Num", "base"]] + dnbL29 = Position 33 18 ; dnb = [ExpectHoverText [":: ()"], mkR 33 12 33 21] + dnbL30 = Position 34 23 + lcbL33 = Position 37 26 ; lcb = [ExpectHoverText [":: Char"], mkR 37 26 37 27] + lclL33 = Position 37 22 + mclL36 = Position 40 1 ; mcl = [mkR 40 0 40 14] + mclL37 = Position 41 1 + spaceL37 = Position 41 24 ; space = [ExpectNoDefinitions, ExpectHoverText [":: Char"]] + docL41 = Position 45 1 ; doc = [ExpectHoverText ["Recognizable docs: kpqz"]] + ; constr = [ExpectHoverText ["Monad m"]] + eitL40 = Position 44 28 ; kindE = [ExpectHoverText [":: Type -> Type -> Type\n"]] + intL40 = Position 44 34 ; kindI = [ExpectHoverText [":: Type\n"]] + -- TODO: Kind signature of type variables should be `Type -> Type` + tvrL40 = Position 44 37 ; kindV = [ExpectHoverText ["m"]]; kindV' = [ExpectHoverText [":: * -> *\n"]] + -- TODO: Hover of integer literal should be `7518` + intL41 = Position 45 20 ; litI = [ExpectHoverText ["_ :: Int"]]; litI' = [ExpectHoverText ["7518"]] + -- TODO: Hover info of char literal should be `'f'` + chrL36 = Position 41 24 ; litC = [ExpectHoverText ["_ :: Char"]]; litC' = [ExpectHoverText ["'f'"]] + -- TODO: Hover info of Text literal should be `"dfgy"` + txtL8 = Position 12 14 ; litT = [ExpectHoverText ["_ :: Text"]]; litT' = [ExpectHoverText ["\"dfgy\""]] + -- TODO: Hover info of List literal should be `[8391 :: Int, 6268]` + lstL43 = Position 47 12 ; litL = [ExpectHoverText ["[Int]"]]; litL' = [ExpectHoverText ["[8391 :: Int, 6268]"]] + outL45 = Position 49 3 ; outSig = [ExpectHoverText ["outer", "Bool"], mkR 50 0 50 5] + -- TODO: Hover info of local function signature should be `inner :: Bool` + innL48 = Position 52 5 ; innSig = [ExpectHoverText ["inner"], mkR 53 2 53 7]; innSig' = [ExpectHoverText ["inner", "Char"], mkR 49 2 49 7] + holeL60 = Position 62 7 ; hleInfo = [ExpectHoverText ["_ ::"]] + holeL65 = Position 65 8 ; hleInfo2 = [ExpectHoverText ["_ :: a -> Maybe a"]] + cccL17 = Position 17 16 ; docLink = [ExpectHoverTextRegex "\\*Defined in 'GHC.Types'\\* \\*\\(ghc-prim-[0-9.]+\\)\\*\n\n"] + imported = Position 56 13 ; importedSig = getDocUri "Foo.hs" >>= \foo -> return [ExpectHoverText ["foo", "Foo", "Haddock"], mkL foo 5 0 5 3] + reexported = Position 55 14 ; reexportedSig = getDocUri "Bar.hs" >>= \bar -> return [ExpectHoverText ["Bar", "Bar", "Haddock"], if ghcVersion < GHC910 then mkL bar 3 5 3 8 else mkL bar 3 0 3 14] + thLocL57 = Position 59 10 ; thLoc = [ExpectHoverText ["Identity"]] + cmtL68 = Position 67 0 ; lackOfdEq = [ExpectHoverExcludeText ["$dEq"]] + import310 = Position 3 10; pkgTxt = [ExpectHoverText ["Data.Text\n\ntext-"]] + in + mkFindTests + -- def hover look expect + [ -- It suggests either going to the constructor or to the field + test (broken fff') yes fffL4 fff "field in record definition" + , test yes yes fffL8 fff' "field in record construction #1102" + , test yes yes fffL14 fff' "field name used as accessor" -- https://github.com/haskell/ghcide/pull/120 in Calculate.hs + , test yes yes aaaL14 aaa "top-level name" -- https://github.com/haskell/ghcide/pull/120 + , test yes yes dcL7 tcDC "data constructor record #1029" + , test yes yes dcL12 tcDC "data constructor plain" -- https://github.com/haskell/ghcide/pull/121 + , test yes yes tcL6 tcData "type constructor #1028" -- https://github.com/haskell/ghcide/pull/147 + , test yes yes xtcL5 xtc "type constructor external #717,1028" + , test yes yes xvL20 xvMsg "value external package #717" -- https://github.com/haskell/ghcide/pull/120 + , test yes yes vvL16 vv "plain parameter" -- https://github.com/haskell/ghcide/pull/120 + , test yes yes aL18 apmp "pattern match name" -- https://github.com/haskell/ghcide/pull/120 + , test yes yes opL16 op "top-level operator #713" -- https://github.com/haskell/ghcide/pull/120 + , test yes yes opL18 opp "parameter operator" -- https://github.com/haskell/ghcide/pull/120 + , test yes yes b'L19 bp "name in backticks" -- https://github.com/haskell/ghcide/pull/120 + , test yes yes clL23 cls "class in instance declaration #1027" + , test yes yes clL25 cls "class in signature #1027" -- https://github.com/haskell/ghcide/pull/147 + , test yes yes eclL15 ecls "external class in signature #717,1027" + , test yes yes dnbL29 dnb "do-notation bind #1073" + , test yes yes dnbL30 dnb "do-notation lookup" + , test yes yes lcbL33 lcb "listcomp bind #1073" + , test yes yes lclL33 lcb "listcomp lookup" + , test yes yes mclL36 mcl "top-level fn 1st clause" + , test yes yes mclL37 mcl "top-level fn 2nd clause #1030" + , test yes yes spaceL37 space "top-level fn on space #1002" + , test no yes docL41 doc "documentation #1129" + , test no yes eitL40 kindE "kind of Either #1017" + , test no yes intL40 kindI "kind of Int #1017" + , test no (broken kindV') tvrL40 kindV "kind of (* -> *) type variable #1017" + , test no (broken litI') intL41 litI "literal Int in hover info #1016" + , test no (broken litC') chrL36 litC "literal Char in hover info #1016" + , test no (broken litT') txtL8 litT "literal Text in hover info #1016" + , test no (broken litL') lstL43 litL "literal List in hover info #1016" + , test yes yes cmtL68 lackOfdEq "no Core symbols #3280" + , test no yes docL41 constr "type constraint in hover info #1012" + , test no yes outL45 outSig "top-level signature #767" + , test yes (broken innSig') innL48 innSig "inner signature #767" + , test no yes holeL60 hleInfo "hole without internal name #831" + , test no yes holeL65 hleInfo2 "hole with variable" + , test no yes cccL17 docLink "Haddock html links" + , testM yes yes imported importedSig "Imported symbol" + , if isWindows then + -- Flaky on Windows: https://github.com/haskell/haskell-language-server/issues/2997 + testM no yes reexported reexportedSig "Imported symbol (reexported)" + else + testM yes yes reexported reexportedSig "Imported symbol (reexported)" + , test no yes thLocL57 thLoc "TH Splice Hover" + , test yes yes import310 pkgTxt "show package name and its version" + ] + where yes :: (TestTree -> Maybe TestTree) + yes = Just -- test should run and pass + no = const Nothing -- don't run this test at all + --skip = const Nothing -- unreliable, don't run + broken :: [Expect] -> TestTree -> Maybe TestTree + broken _ = yes + +checkFileCompiles :: FilePath -> Session () -> TestTree +checkFileCompiles fp diag = + testWithDummyPlugin ("hover: Does " ++ fp ++ " compile") (mkIdeTestFs [copyDir "hover"]) $ do + _ <- openDoc fp "haskell" + diag diff --git a/ghcide-test/exe/FindImplementationAndHoverTests.hs b/ghcide-test/exe/FindImplementationAndHoverTests.hs new file mode 100644 index 0000000000..221be90dd2 --- /dev/null +++ b/ghcide-test/exe/FindImplementationAndHoverTests.hs @@ -0,0 +1,228 @@ +{-# LANGUAGE ExplicitNamespaces #-} +{-# LANGUAGE OverloadedStrings #-} +{-# LANGUAGE ViewPatterns #-} + +module FindImplementationAndHoverTests (tests) where + +import Control.Monad +import Data.Foldable +import Data.Maybe +import Data.Text (Text) +import qualified Data.Text as T +import Language.LSP.Test +import Text.Regex.TDFA ((=~)) + +import Config +import Development.IDE.Test (standardizeQuotes) +import Test.Hls +import Test.Hls.FileSystem (copyDir) + +tests :: TestTree +tests = let + tst :: (TextDocumentIdentifier -> Position -> Session a, a -> Session [Expect] -> Session ()) -> Position -> String -> Session [Expect] -> String -> TestTree + tst (get, check) pos sfp targetRange title = + testWithDummyPlugin title (mkIdeTestFs [copyDir "hover"]) $ do + doc <- openDoc sfp "haskell" + waitForProgressDone + _x <- waitForTypecheck doc + found <- get doc pos + check found targetRange + + checkHover :: (HasCallStack) => Maybe Hover -> Session [Expect] -> Session () + checkHover hover expectations = traverse_ check =<< expectations where + + check :: (HasCallStack) => Expect -> Session () + check expected = + case hover of + Nothing -> unless (expected == ExpectNoHover) $ liftIO $ assertFailure "no hover found" + Just Hover{_contents = (InL MarkupContent{_value = standardizeQuotes -> msg}) + ,_range = _rangeInHover } -> + case expected of + ExpectRange _expectedRange -> liftIO $ assertFailure $ "ExpectRange assertion not implemented, yet." + ExpectHoverRange _expectedRange -> liftIO $ assertFailure $ "ExpectHoverRange assertion not implemented, yet." + ExpectHoverText snippets -> liftIO $ traverse_ (`assertFoundIn` msg) snippets + ExpectHoverExcludeText snippets -> liftIO $ traverse_ (`assertNotFoundIn` msg) snippets + ExpectHoverTextRegex re -> liftIO $ assertBool ("Regex not found in " <> T.unpack msg) (msg =~ re :: Bool) + ExpectNoHover -> liftIO $ assertFailure $ "Expected no hover but got " <> show hover + _ -> pure () -- all other expectations not relevant to hover + _ -> liftIO $ assertFailure $ "test not expecting this kind of hover info" <> show hover + + assertFoundIn :: T.Text -> T.Text -> Assertion + assertFoundIn part whole = assertBool + (T.unpack $ "failed to find: `" <> part <> "` in hover message:\n" <> whole) + (part `T.isInfixOf` whole) + + assertNotFoundIn :: T.Text -> T.Text -> Assertion + assertNotFoundIn part whole = assertBool + (T.unpack $ "found unexpected: `" <> part <> "` in hover message:\n" <> whole) + (not . T.isInfixOf part $ whole) + + sourceFilePath = T.unpack sourceFileName + sourceFileName = "GotoImplementation.hs" + + mkFindTests tests = testGroup "goto implementation" + [ testGroup "implementation" $ mapMaybe fst allTests + , testGroup "hover" $ mapMaybe snd allTests + ] + where + allTests = tests ++ recordDotSyntaxTests + + recordDotSyntaxTests = + -- We get neither new hover information nor 'Goto Implementation' locations for record-dot-syntax + [ test' "RecordDotSyntax.hs" yes yes (Position 17 6) [ExpectNoImplementations, ExpectHoverText ["_ :: [Char]"]] "hover over parent" + , test' "RecordDotSyntax.hs" yes yes (Position 17 18) [ExpectNoImplementations, ExpectHoverText ["_ :: Integer"]] "hover over dot shows child" + , test' "RecordDotSyntax.hs" yes yes (Position 17 25) [ExpectNoImplementations, ExpectHoverText ["_ :: MyChild"]] "hover over child" + , test' "RecordDotSyntax.hs" yes yes (Position 17 27) [ExpectNoImplementations, ExpectHoverText ["_ :: [Char]"]] "hover over grandchild" + ] + + test :: (HasCallStack) => (TestTree -> a) -> (TestTree -> b) -> Position -> [Expect] -> String -> (a, b) + test runImpl runHover look expect = testM runImpl runHover look (return expect) + + testM :: (HasCallStack) => (TestTree -> a) + -> (TestTree -> b) + -> Position + -> Session [Expect] + -> String + -> (a, b) + testM = testM' sourceFilePath + + test' :: (HasCallStack) => FilePath -> (TestTree -> a) -> (TestTree -> b) -> Position -> [Expect] -> String -> (a, b) + test' sourceFile runImpl runHover look expect = testM' sourceFile runImpl runHover look (return expect) + + testM' :: (HasCallStack) + => FilePath + -> (TestTree -> a) + -> (TestTree -> b) + -> Position + -> Session [Expect] + -> String + -> (a, b) + testM' sourceFile runImpl runHover look expect title = + ( runImpl $ tst impl look sourceFile expect title + , runHover $ tst hover look sourceFile expect title ) where + impl = (getImplementations, checkDefs) + hover = (getHover , checkHover) + + aaaL = Position 8 15; aaaR = mkRange 5 9 5 16; + aaa = + [ ExpectRanges [aaaR] + , ExpectHoverText (evidenceBoundByConstraint "Num" "AAA") + ] + + bbbL = Position 15 8; bbbR = mkRange 12 9 12 16; + bbb = + [ ExpectRanges [bbbR] + , ExpectHoverText (evidenceBoundByConstraint "BBB" "AAA") + ] + cccL = Position 18 11; + ccc = + [ ExpectNoImplementations + , ExpectHoverText (evidenceBySignatureOrPattern "Show" "a") + ] + dddShowR = mkRange 21 26 21 30; dddEqR = mkRange 21 22 21 24 + dddL1 = Position 23 16; + ddd1 = + [ ExpectRanges [dddEqR] + , ExpectHoverText + [ constraintEvidence "Eq" "(Q k)" + , evidenceGoal' "'forall k. Eq k => Eq (Q k)'" + , boundByInstanceOf "Eq" + , evidenceGoal "Eq" "k" + , boundByTypeSigOrPattern + ] + ] + dddL2 = Position 23 29; + ddd2 = + [ ExpectNoImplementations + , ExpectHoverText (evidenceBySignatureOrPattern "Show" "k") + ] + dddL3 = Position 24 8; + ddd3 = + [ ExpectRanges [dddEqR, dddShowR] + , ExpectHoverText + [ constraintEvidence "Show" "(Q Integer)" + , evidenceGoal' "'forall k. Show k => Show (Q k)'" + , boundByInstance + , evidenceGoal "Show" "Integer" + , usingExternalInstance + , constraintEvidence "Eq" "(Q Integer)" + , evidenceGoal' "'forall k. Eq k => Eq (Q k)'" + , boundByInstance + , evidenceGoal "Eq" "Integer" + , usingExternalInstance + ] + ] + gadtL = Position 29 35; + gadt = + [ ExpectNoImplementations + , ExpectHoverText + [ constraintEvidence "Show" "Int" + , evidenceGoal "Show" "a" + , boundByTypeSigOrPattern + , evidenceGoal' "'a ~ Int'" + , boundByPattern + ] + ] + in + mkFindTests + -- impl hover look expect + [ + test yes yes aaaL aaa "locally defined class instance" + , test yes yes bbbL bbb "locally defined class and instance" + , test yes yes cccL ccc "bound by type signature" + , test yes yes dddL1 ddd1 "newtype Eq evidence" + , test yes yes dddL2 ddd2 "Show evidence" + , test yes yes dddL3 ddd3 "evidence construction" + , test yes yes gadtL gadt "GADT evidence" + ] + where yes :: (TestTree -> Maybe TestTree) + yes = Just -- test should run and pass + no = const Nothing -- don't run this test at all + +-- ---------------------------------------------------------------------------- +-- Helper functions for creating hover message verification +-- ---------------------------------------------------------------------------- + +evidenceBySignatureOrPattern :: Text -> Text -> [Text] +evidenceBySignatureOrPattern tyclass varname = + [ constraintEvidence tyclass varname + , boundByTypeSigOrPattern + ] + +evidenceBoundByConstraint :: Text -> Text -> [Text] +evidenceBoundByConstraint tyclass varname = + [ constraintEvidence tyclass varname + , boundByInstanceOf tyclass + ] + +boundByTypeSigOrPattern :: Text +boundByTypeSigOrPattern = "bound by type signature or pattern" + +boundByInstance :: Text +boundByInstance = + "bound by an instance of" + +boundByInstanceOf :: Text -> Text +boundByInstanceOf tyvar = + "bound by an instance of class " <> tyvar + +boundByPattern :: Text +boundByPattern = + "bound by a pattern" + +usingExternalInstance :: Text +usingExternalInstance = + "using an external instance" + +constraintEvidence :: Text -> Text -> Text +constraintEvidence tyclass varname = "Evidence of constraint " <> quotedName tyclass varname + +-- | A goal in the evidence tree. +evidenceGoal :: Text -> Text -> Text +evidenceGoal tyclass varname = "- " <> quotedName tyclass varname + +evidenceGoal' :: Text -> Text +evidenceGoal' t = "- " <> t + +quotedName :: Text -> Text -> Text +quotedName tyclass varname = "'" <> tyclass <> " " <> varname <> "'" diff --git a/ghcide-test/exe/FuzzySearch.hs b/ghcide-test/exe/FuzzySearch.hs new file mode 100644 index 0000000000..1d2a5ac181 --- /dev/null +++ b/ghcide-test/exe/FuzzySearch.hs @@ -0,0 +1,52 @@ +module FuzzySearch (tests) where + +import Data.Maybe (isJust, mapMaybe) +import Data.Text (Text) +import qualified Data.Text as Text +import Prelude hiding (filter) +import Test.Tasty +import Test.Tasty.HUnit +import Test.Tasty.QuickCheck +import Text.Fuzzy.Parallel + +tests :: TestTree +tests = + testGroup + "Fuzzy search" + [ testGroup "match" + [ testCase "empty" $ + match "" "" @?= Just 0 + , testCase "camel case" $ + match "myImportantField" "myImportantField" @?= Just 262124 + , testCase "a" $ + mapMaybe (matchInput "a") ["", "a", "aa", "aaa", "A", "AA", "aA", "Aa"] + @?= [("a",3),("aa",3),("aaa",3),("aA",3),("Aa",1)] + , testCase "lowercase words" $ + mapMaybe (matchInput "abc") ["abc", "abcd", "axbc", "axbxc", "def"] + @?= [("abc", 25), ("abcd", 25), ("axbc", 7), ("axbxc", 5)] + , testCase "lower upper mix" $ + mapMaybe (matchInput "abc") ["abc", "aBc", "axbC", "axBxC", "def"] + @?= [("abc", 25), ("aBc", 25), ("axbC", 7), ("axBxC", 5)] + , testCase "prefixes" $ + mapMaybe (matchInput "alpha") (Text.inits "alphabet") + @?= [("alpha", 119), ("alphab", 119), ("alphabe", 119), ("alphabet", 119)] + , testProperty "x `isSubsequenceOf` y => match x y returns Just" + prop_matchIfSubsequence + ] + ] + where + matchInput :: Text -> Text -> Maybe (Text, Int) + matchInput needle candidate = (candidate,) <$> match needle candidate + +prop_matchIfSubsequence :: Property +prop_matchIfSubsequence = + forAll genNonEmptyText $ \haystack -> + forAll (genSubsequence haystack) $ \needle -> + isJust (match needle haystack) + where + genNonEmptyText = + Text.pack <$> listOf1 (elements $ ['a'..'z'] ++ ['A'..'Z'] ++ ['0'..'9']) + + genSubsequence :: Text -> Gen Text + genSubsequence = + fmap Text.pack . sublistOf . Text.unpack diff --git a/ghcide/test/exe/GarbageCollectionTests.hs b/ghcide-test/exe/GarbageCollectionTests.hs similarity index 87% rename from ghcide/test/exe/GarbageCollectionTests.hs rename to ghcide-test/exe/GarbageCollectionTests.hs index d7033a8439..5cc9935352 100644 --- a/ghcide/test/exe/GarbageCollectionTests.hs +++ b/ghcide-test/exe/GarbageCollectionTests.hs @@ -1,10 +1,7 @@ - -{-# LANGUAGE OverloadedLabels #-} - module GarbageCollectionTests (tests) where +import Config (testWithDummyPluginEmpty') import Control.Monad.IO.Class (liftIO) -import Data.Row import qualified Data.Set as Set import qualified Data.Text as T import Development.IDE.Test (expectCurrentDiagnostics, @@ -15,23 +12,21 @@ import Language.LSP.Protocol.Types hiding (SemanticTokenAbsolute (..), SemanticTokensEdit (..), mkRange) import Language.LSP.Test import System.FilePath --- import Test.QuickCheck.Instances () import Test.Tasty import Test.Tasty.HUnit -import TestUtils import Text.Printf (printf) tests :: TestTree tests = testGroup "garbage collection" [ testGroup "dirty keys" - [ testSession' "are collected" $ \dir -> do + [ testWithDummyPluginEmpty' "are collected" $ \dir -> do liftIO $ writeFile (dir "hie.yaml") "cradle: {direct: {arguments: [A]}}" doc <- generateGarbage "A" dir closeDoc doc garbage <- waitForGC liftIO $ assertBool "no garbage was found" $ not $ null garbage - , testSession' "are deleted from the state" $ \dir -> do + , testWithDummyPluginEmpty' "are deleted from the state" $ \dir -> do liftIO $ writeFile (dir "hie.yaml") "cradle: {direct: {arguments: [A]}}" docA <- generateGarbage "A" dir keys0 <- getStoredKeys @@ -41,7 +36,7 @@ tests = testGroup "garbage collection" keys1 <- getStoredKeys liftIO $ assertBool "keys were not deleted from the state" (length keys1 < length keys0) - , testSession' "are not regenerated unless needed" $ \dir -> do + , testWithDummyPluginEmpty' "are not regenerated unless needed" $ \dir -> do liftIO $ writeFile (dir "hie.yaml") "cradle: {direct: {arguments: [A.hs, B.hs]}}" docA <- generateGarbage "A" dir _docB <- generateGarbage "B" dir @@ -62,7 +57,7 @@ tests = testGroup "garbage collection" Set.intersection (Set.fromList garbage) (Set.fromList keysB) liftIO $ regeneratedKeys @?= mempty - , testSession' "regenerate successfully" $ \dir -> do + , testWithDummyPluginEmpty' "regenerate successfully" $ \dir -> do liftIO $ writeFile (dir "hie.yaml") "cradle: {direct: {arguments: [A]}}" docA <- generateGarbage "A" dir closeDoc docA @@ -74,14 +69,14 @@ tests = testGroup "garbage collection" , "a = ()" ] doc <- generateGarbage "A" dir - changeDoc doc [TextDocumentContentChangeEvent . InR . (.==) #text $ edit] + changeDoc doc [TextDocumentContentChangeEvent . InR $ TextDocumentContentChangeWholeDocument edit] builds <- waitForTypecheck doc liftIO $ assertBool "it still builds" builds - expectCurrentDiagnostics doc [(DiagnosticSeverity_Error, (2,4), "Couldn't match expected type")] + expectCurrentDiagnostics doc [(DiagnosticSeverity_Error, (2,4), "Couldn't match expected type", Just "GHC-83865")] ] ] where - isExpected k = any (`T.isPrefixOf` k) ["GhcSessionIO"] + isExpected k = "GhcSessionIO" `T.isPrefixOf` k generateGarbage :: String -> FilePath -> Session TextDocumentIdentifier generateGarbage modName dir = do diff --git a/ghcide/test/exe/HaddockTests.hs b/ghcide-test/exe/HaddockTests.hs similarity index 100% rename from ghcide/test/exe/HaddockTests.hs rename to ghcide-test/exe/HaddockTests.hs diff --git a/ghcide/test/exe/HieDbRetry.hs b/ghcide-test/exe/HieDbRetry.hs similarity index 99% rename from ghcide/test/exe/HieDbRetry.hs rename to ghcide-test/exe/HieDbRetry.hs index b84715c1b8..3e0c41c2f9 100644 --- a/ghcide/test/exe/HieDbRetry.hs +++ b/ghcide-test/exe/HieDbRetry.hs @@ -44,7 +44,6 @@ errorBusy = SQLite.SQLError{ sqlError = SQLite.ErrorBusy, sqlErrorDetails = "", isErrorCall :: ErrorCall -> Maybe ErrorCall isErrorCall e | ErrorCall _ <- e = Just e - | otherwise = Nothing tests :: TestTree tests = testGroup "RetryHieDb" diff --git a/ghcide/test/exe/HighlightTests.hs b/ghcide-test/exe/HighlightTests.hs similarity index 89% rename from ghcide/test/exe/HighlightTests.hs rename to ghcide-test/exe/HighlightTests.hs index e01377615d..3450404679 100644 --- a/ghcide/test/exe/HighlightTests.hs +++ b/ghcide-test/exe/HighlightTests.hs @@ -1,9 +1,9 @@ module HighlightTests (tests) where +import Config import Control.Monad.IO.Class (liftIO) import qualified Data.Text as T -import Development.IDE.GHC.Compat (GhcVersion (..), ghcVersion) import Development.IDE.Types.Location import Language.LSP.Protocol.Types hiding (SemanticTokenAbsolute (..), @@ -13,11 +13,10 @@ import Language.LSP.Protocol.Types hiding import Language.LSP.Test import Test.Tasty import Test.Tasty.HUnit -import TestUtils tests :: TestTree tests = testGroup "highlight" - [ testSessionWait "value" $ do + [ testWithDummyPluginEmpty "value" $ do doc <- createDoc "A.hs" "haskell" source _ <- waitForDiagnostics highlights <- getHighlights doc (Position 3 2) @@ -27,7 +26,7 @@ tests = testGroup "highlight" , DocumentHighlight (R 4 6 4 9) (Just DocumentHighlightKind_Read) , DocumentHighlight (R 5 22 5 25) (Just DocumentHighlightKind_Read) ] - , testSessionWait "type" $ do + , testWithDummyPluginEmpty "type" $ do doc <- createDoc "A.hs" "haskell" source _ <- waitForDiagnostics highlights <- getHighlights doc (Position 2 8) @@ -35,7 +34,7 @@ tests = testGroup "highlight" [ DocumentHighlight (R 2 7 2 10) (Just DocumentHighlightKind_Read) , DocumentHighlight (R 3 11 3 14) (Just DocumentHighlightKind_Read) ] - , testSessionWait "local" $ do + , testWithDummyPluginEmpty "local" $ do doc <- createDoc "A.hs" "haskell" source _ <- waitForDiagnostics highlights <- getHighlights doc (Position 6 5) @@ -44,8 +43,8 @@ tests = testGroup "highlight" , DocumentHighlight (R 6 10 6 13) (Just DocumentHighlightKind_Read) , DocumentHighlight (R 7 12 7 15) (Just DocumentHighlightKind_Read) ] - , knownBrokenForGhcVersions [GHC90, GHC92, GHC94, GHC96, GHC98] "Ghc9 highlights the constructor and not just this field" $ - testSessionWait "record" $ do + , + testWithDummyPluginEmpty "record" $ do doc <- createDoc "A.hs" "haskell" recsource _ <- waitForDiagnostics highlights <- getHighlights doc (Position 4 15) diff --git a/ghcide/test/exe/IfaceTests.hs b/ghcide-test/exe/IfaceTests.hs similarity index 73% rename from ghcide/test/exe/IfaceTests.hs rename to ghcide-test/exe/IfaceTests.hs index 7aad572564..d7dc533550 100644 --- a/ghcide/test/exe/IfaceTests.hs +++ b/ghcide-test/exe/IfaceTests.hs @@ -1,10 +1,7 @@ - -{-# LANGUAGE OverloadedLabels #-} - module IfaceTests (tests) where +import Config import Control.Monad.IO.Class (liftIO) -import Data.Row import qualified Data.Text as T import Development.IDE.GHC.Util import Development.IDE.Test (configureCheckProject, @@ -23,7 +20,6 @@ import System.FilePath import System.IO.Extra hiding (withTempDir) import Test.Tasty import Test.Tasty.HUnit -import TestUtils tests :: TestTree tests = testGroup "Interface loading tests" @@ -37,7 +33,7 @@ tests = testGroup "Interface loading tests" -- | test that TH reevaluates across interfaces ifaceTHTest :: TestTree -ifaceTHTest = testCase "iface-th-test" $ runWithExtraFiles "TH" $ \dir -> do +ifaceTHTest = testWithExtraFiles "iface-th-test" "TH" $ \dir -> do let aPath = dir "THA.hs" bPath = dir "THB.hs" cPath = dir "THC.hs" @@ -52,14 +48,14 @@ ifaceTHTest = testCase "iface-th-test" $ runWithExtraFiles "TH" $ \dir -> do liftIO $ writeFileUTF8 aPath (unlines $ init (lines $ T.unpack aSource) ++ ["th_a = [d| a = False|]"]) -- Check that the change propagates to C - changeDoc cdoc [TextDocumentContentChangeEvent . InR . (.==) #text $ cSource] + changeDoc cdoc [TextDocumentContentChangeEvent . InR $ TextDocumentContentChangeWholeDocument cSource] expectDiagnostics - [("THC.hs", [(DiagnosticSeverity_Error, (4, 4), "Couldn't match expected type '()' with actual type 'Bool'")]) - ,("THB.hs", [(DiagnosticSeverity_Warning, (4,thDollarIdx), "Top-level binding")])] + [("THC.hs", [(DiagnosticSeverity_Error, (4, 4), "Couldn't match expected type '()' with actual type 'Bool'", Just "GHC-83865")]) + ,("THB.hs", [(DiagnosticSeverity_Warning, (4,1), "Top-level binding", Just "GHC-38417")])] closeDoc cdoc ifaceErrorTest :: TestTree -ifaceErrorTest = testCase "iface-error-test-1" $ runWithExtraFiles "recomp" $ \dir -> do +ifaceErrorTest = testWithExtraFiles "iface-error-test-1" "recomp" $ \dir -> do configureCheckProject True let bPath = dir "B.hs" pPath = dir "P.hs" @@ -69,17 +65,19 @@ ifaceErrorTest = testCase "iface-error-test-1" $ runWithExtraFiles "recomp" $ \d bdoc <- createDoc bPath "haskell" bSource expectDiagnostics - [("P.hs", [(DiagnosticSeverity_Warning,(4,0), "Top-level binding")])] -- So what we know P has been loaded + [("P.hs", [(DiagnosticSeverity_Warning,(4,0), "Top-level binding", Just "GHC-38417")])] -- So what we know P has been loaded -- Change y from Int to B - changeDoc bdoc [TextDocumentContentChangeEvent . InR . (.==) #text $ T.unlines ["module B where", "y :: Bool", "y = undefined"]] + changeDoc bdoc [ TextDocumentContentChangeEvent . InR . TextDocumentContentChangeWholeDocument $ + T.unlines [ "module B where", "y :: Bool", "y = undefined"] + ] -- save so that we can that the error propagates to A sendNotification SMethod_TextDocumentDidSave (DidSaveTextDocumentParams bdoc Nothing) -- Check that the error propagates to A expectDiagnostics - [("A.hs", [(DiagnosticSeverity_Error, (5, 4), "Couldn't match expected type 'Int' with actual type 'Bool'")])] + [("A.hs", [(DiagnosticSeverity_Error, (5, 4), "Couldn't match expected type 'Int' with actual type 'Bool'", Just "GHC-83865")])] -- Check that we wrote the interfaces for B when we saved hidir <- getInterfaceFilesDir bdoc @@ -88,9 +86,9 @@ ifaceErrorTest = testCase "iface-error-test-1" $ runWithExtraFiles "recomp" $ \d pdoc <- openDoc pPath "haskell" expectDiagnostics - [("P.hs", [(DiagnosticSeverity_Warning,(4,0), "Top-level binding")]) + [("P.hs", [(DiagnosticSeverity_Warning,(4,0), "Top-level binding", Just "GHC-38417")]) ] - changeDoc pdoc [TextDocumentContentChangeEvent . InR . (.==) #text $ pSource <> "\nfoo = y :: Bool" ] + changeDoc pdoc [TextDocumentContentChangeEvent . InR . TextDocumentContentChangeWholeDocument $ pSource <> "\nfoo = y :: Bool" ] -- Now in P we have -- bar = x :: Int -- foo = y :: Bool @@ -100,13 +98,13 @@ ifaceErrorTest = testCase "iface-error-test-1" $ runWithExtraFiles "recomp" $ \d -- - The diagnostic for A has already been received. Ghcide does not repeat diagnostics -- - P is being typechecked with the last successful artifacts for A. expectDiagnostics - [("P.hs", [(DiagnosticSeverity_Warning,(4,0), "Top-level binding")]) - ,("P.hs", [(DiagnosticSeverity_Warning,(6,0), "Top-level binding")]) + [("P.hs", [(DiagnosticSeverity_Warning,(4,0), "Top-level binding", Just "GHC-38417")]) + ,("P.hs", [(DiagnosticSeverity_Warning,(6,0), "Top-level binding", Just "GHC-38417")]) ] expectNoMoreDiagnostics 2 ifaceErrorTest2 :: TestTree -ifaceErrorTest2 = testCase "iface-error-test-2" $ runWithExtraFiles "recomp" $ \dir -> do +ifaceErrorTest2 = testWithExtraFiles "iface-error-test-2" "recomp" $ \dir -> do let bPath = dir "B.hs" pPath = dir "P.hs" @@ -116,13 +114,14 @@ ifaceErrorTest2 = testCase "iface-error-test-2" $ runWithExtraFiles "recomp" $ \ bdoc <- createDoc bPath "haskell" bSource pdoc <- createDoc pPath "haskell" pSource expectDiagnostics - [("P.hs", [(DiagnosticSeverity_Warning,(4,0), "Top-level binding")])] -- So that we know P has been loaded + [("P.hs", [(DiagnosticSeverity_Warning,(4,0), "Top-level binding", Just "GHC-38417")])] -- So that we know P has been loaded -- Change y from Int to B - changeDoc bdoc [TextDocumentContentChangeEvent . InR . (.==) #text $ T.unlines ["module B where", "y :: Bool", "y = undefined"]] + changeDoc bdoc [TextDocumentContentChangeEvent . InR . TextDocumentContentChangeWholeDocument $ + T.unlines ["module B where", "y :: Bool", "y = undefined"]] -- Add a new definition to P - changeDoc pdoc [TextDocumentContentChangeEvent . InR . (.==) #text $ pSource <> "\nfoo = y :: Bool" ] + changeDoc pdoc [TextDocumentContentChangeEvent . InR . TextDocumentContentChangeWholeDocument $ pSource <> "\nfoo = y :: Bool" ] -- Now in P we have -- bar = x :: Int -- foo = y :: Bool @@ -131,15 +130,15 @@ ifaceErrorTest2 = testCase "iface-error-test-2" $ runWithExtraFiles "recomp" $ \ expectDiagnostics -- As in the other test, P is being typechecked with the last successful artifacts for A -- (ot thanks to -fdeferred-type-errors) - [("A.hs", [(DiagnosticSeverity_Error, (5, 4), "Couldn't match expected type 'Int' with actual type 'Bool'")]) - ,("P.hs", [(DiagnosticSeverity_Warning, (4, 0), "Top-level binding")]) - ,("P.hs", [(DiagnosticSeverity_Warning, (6, 0), "Top-level binding")]) + [("A.hs", [(DiagnosticSeverity_Error, (5, 4), "Couldn't match expected type 'Int' with actual type 'Bool'", Just "GHC-83865")]) + ,("P.hs", [(DiagnosticSeverity_Warning, (4, 0), "Top-level binding", Just "GHC-38417")]) + ,("P.hs", [(DiagnosticSeverity_Warning, (6, 0), "Top-level binding", Just "GHC-38417")]) ] expectNoMoreDiagnostics 2 ifaceErrorTest3 :: TestTree -ifaceErrorTest3 = testCase "iface-error-test-3" $ runWithExtraFiles "recomp" $ \dir -> do +ifaceErrorTest3 = testWithExtraFiles "iface-error-test-3" "recomp" $ \dir -> do let bPath = dir "B.hs" pPath = dir "P.hs" @@ -149,7 +148,7 @@ ifaceErrorTest3 = testCase "iface-error-test-3" $ runWithExtraFiles "recomp" $ \ bdoc <- createDoc bPath "haskell" bSource -- Change y from Int to B - changeDoc bdoc [TextDocumentContentChangeEvent . InR . (.==) #text $ T.unlines ["module B where", "y :: Bool", "y = undefined"]] + changeDoc bdoc [TextDocumentContentChangeEvent . InR . TextDocumentContentChangeWholeDocument $ T.unlines ["module B where", "y :: Bool", "y = undefined"]] -- P should not typecheck, as there are no last valid artifacts for A _pdoc <- createDoc pPath "haskell" pSource @@ -157,7 +156,7 @@ ifaceErrorTest3 = testCase "iface-error-test-3" $ runWithExtraFiles "recomp" $ \ -- In this example the interface file for A should not exist (modulo the cache folder) -- Despite that P still type checks, as we can generate an interface file for A thanks to -fdeferred-type-errors expectDiagnostics - [("A.hs", [(DiagnosticSeverity_Error, (5, 4), "Couldn't match expected type 'Int' with actual type 'Bool'")]) - ,("P.hs", [(DiagnosticSeverity_Warning,(4,0), "Top-level binding")]) + [("A.hs", [(DiagnosticSeverity_Error, (5, 4), "Couldn't match expected type 'Int' with actual type 'Bool'", Just "GHC-83865")]) + ,("P.hs", [(DiagnosticSeverity_Warning,(4,0), "Top-level binding", Just "GHC-38417")]) ] expectNoMoreDiagnostics 2 diff --git a/ghcide/test/exe/InitializeResponseTests.hs b/ghcide-test/exe/InitializeResponseTests.hs similarity index 62% rename from ghcide/test/exe/InitializeResponseTests.hs rename to ghcide-test/exe/InitializeResponseTests.hs index 84e673ef8e..f13344e368 100644 --- a/ghcide/test/exe/InitializeResponseTests.hs +++ b/ghcide-test/exe/InitializeResponseTests.hs @@ -1,28 +1,20 @@ -{-# LANGUAGE DataKinds #-} -{-# LANGUAGE OverloadedLabels #-} +{-# LANGUAGE DataKinds #-} module InitializeResponseTests (tests) where import Control.Monad import Data.List.Extra -import Data.Row import qualified Data.Text as T import Development.IDE.Plugin.TypeLenses (typeLensCommandId) import qualified Language.LSP.Protocol.Lens as L import Language.LSP.Protocol.Message -import Language.LSP.Protocol.Types hiding - (SemanticTokenAbsolute (..), - SemanticTokenRelative (..), - SemanticTokensEdit (..), - mkRange) import Language.LSP.Test --- import Test.QuickCheck.Instances () + +import Config import Control.Lens ((^.)) import Development.IDE.Plugin.Test (blockCommandId) -import Test.Tasty -import Test.Tasty.HUnit -import TestUtils +import Test.Hls tests :: TestTree tests = withResource acquire release tests where @@ -36,32 +28,35 @@ tests = withResource acquire release tests where tests getInitializeResponse = testGroup "initialize response capabilities" [ chk " text doc sync" _textDocumentSync tds - , chk " hover" _hoverProvider (Just $ InL True) - , chk " completion" _completionProvider (Just $ CompletionOptions Nothing (Just ["."]) Nothing (Just True) Nothing) + , chk " hover" _hoverProvider (Just $ InR (HoverOptions (Just False))) + , chk " completion" _completionProvider (Just $ CompletionOptions (Just False) (Just ["."]) Nothing (Just True) Nothing) , chk "NO signature help" _signatureHelpProvider Nothing - , chk " goto definition" _definitionProvider (Just $ InL True) - , chk " goto type definition" _typeDefinitionProvider (Just $ InL True) - -- BUG in lsp-test, this test fails, just change the accepted response - -- for now - , chk "NO goto implementation" _implementationProvider (Just $ InL False) - , chk " find references" _referencesProvider (Just $ InL True) - , chk " doc highlight" _documentHighlightProvider (Just $ InL True) - , chk " doc symbol" _documentSymbolProvider (Just $ InL True) - , chk " workspace symbol" _workspaceSymbolProvider (Just $ InL True) - , chk " code action" _codeActionProvider (Just $ InL False) + , chk " goto definition" _definitionProvider (Just $ InR (DefinitionOptions (Just False))) + , chk " goto type definition" _typeDefinitionProvider (Just $ InR (InL (TypeDefinitionOptions (Just False)))) + , chk " goto implementation" _implementationProvider (Just $ InR (InL (ImplementationOptions (Just False)))) + , chk " find references" _referencesProvider (Just $ InR (ReferenceOptions (Just False))) + , chk " doc highlight" _documentHighlightProvider (Just $ InR (DocumentHighlightOptions (Just False))) + , chk " doc symbol" _documentSymbolProvider (Just $ InR (DocumentSymbolOptions (Just False) Nothing)) + , chk " workspace symbol" _workspaceSymbolProvider (Just $ InR (WorkspaceSymbolOptions (Just False) (Just False))) + , chk "NO code action" _codeActionProvider Nothing , chk " code lens" _codeLensProvider (Just $ CodeLensOptions (Just False) (Just True)) - , chk "NO doc formatting" _documentFormattingProvider (Just $ InL False) + , chk "NO doc formatting" _documentFormattingProvider Nothing , chk "NO doc range formatting" - _documentRangeFormattingProvider (Just $ InL False) + _documentRangeFormattingProvider Nothing , chk "NO doc formatting on typing" _documentOnTypeFormattingProvider Nothing - , chk "NO renaming" _renameProvider (Just $ InL False) + , chk "NO renaming" _renameProvider Nothing , chk "NO doc link" _documentLinkProvider Nothing - , chk "NO color" (^. L.colorProvider) (Just $ InL False) - , chk "NO folding range" _foldingRangeProvider (Just $ InL False) + , chk "NO color" (^. L.colorProvider) Nothing + , chk "NO folding range" _foldingRangeProvider Nothing , che " execute command" _executeCommandProvider [typeLensCommandId, blockCommandId] - , chk " workspace" (^. L.workspace) (Just $ #workspaceFolders .== Just WorkspaceFoldersServerCapabilities{_supported = Just True, _changeNotifications = Just ( InR True )} - .+ #fileOperations .== Nothing) + , chk " workspace" (^. L.workspace) (Just $ WorkspaceOptions + { _workspaceFolders = Just WorkspaceFoldersServerCapabilities + { _supported = Just True + , _changeNotifications = Just (InR True) + } + , _fileOperations = Nothing + }) , chk "NO experimental" (^. L.experimental) Nothing ] where @@ -77,13 +72,13 @@ tests = withResource acquire release tests where testCase title $ getInitializeResponse >>= \ir -> expected @=? (getActual . innerCaps) ir che :: TestName -> (ServerCapabilities -> Maybe ExecuteCommandOptions) -> [T.Text] -> TestTree - che title getActual expected = testCase title doTest - where - doTest = do - ir <- getInitializeResponse - let Just ExecuteCommandOptions {_commands = commands} = getActual $ innerCaps ir - commandNames = (!! 2) . T.splitOn ":" <$> commands - zipWithM_ (\e o -> T.isSuffixOf e o @? show (e,o)) (sort expected) (sort commandNames) + che title getActual expected = testCase title $ do + ir <- getInitializeResponse + ExecuteCommandOptions {_commands = commands} <- case getActual $ innerCaps ir of + Just eco -> pure eco + Nothing -> assertFailure "Was expecting Just ExecuteCommandOptions, got Nothing" + let commandNames = (!! 2) . T.splitOn ":" <$> commands + zipWithM_ (\e o -> T.isSuffixOf e o @? show (e,o)) (sort expected) (sort commandNames) innerCaps :: TResponseMessage Method_Initialize -> ServerCapabilities innerCaps (TResponseMessage _ _ (Right (InitializeResult c _))) = c @@ -93,5 +88,5 @@ tests = withResource acquire release tests where acquire = run initializeResponse release :: TResponseMessage Method_Initialize -> IO () - release = const $ pure () + release = mempty diff --git a/ghcide/test/exe/LogType.hs b/ghcide-test/exe/LogType.hs similarity index 100% rename from ghcide/test/exe/LogType.hs rename to ghcide-test/exe/LogType.hs diff --git a/ghcide-test/exe/Main.hs b/ghcide-test/exe/Main.hs new file mode 100644 index 0000000000..c8d927072c --- /dev/null +++ b/ghcide-test/exe/Main.hs @@ -0,0 +1,106 @@ +-- Copyright (c) 2019 The DAML Authors. All rights reserved. +-- SPDX-License-Identifier: Apache-2.0 + +{- + NOTE On enforcing determinism + + The tests below use two mechanisms to enforce deterministic LSP sequences: + + 1. Progress reporting: waitForProgress(Begin|Done) + 2. Diagnostics: expectDiagnostics + + Either is fine, but diagnostics are generally more reliable. + + Mixing them both in the same test is NOT FINE as it will introduce race + conditions since multiple interleavings are possible. In other words, + the sequence of diagnostics and progress reports is not deterministic. + For example: + + < do something > + waitForProgressDone + expectDiagnostics [...] + + - When the diagnostics arrive after the progress done message, as they usually do, the test will pass + - When the diagnostics arrive before the progress done msg, when on a slow machine occasionally, the test will timeout + + Therefore, avoid mixing both progress reports and diagnostics in the same test + -} + + + +module Main (main) where + +import qualified HieDbRetry +import Test.Tasty +import Test.Tasty.Ingredients.Rerun + +import AsyncTests +import BootTests +import ClientSettingsTests +import CodeLensTests +import CompletionTests +import CPPTests +import CradleTests +import DependentFileTest +import DiagnosticTests +import ExceptionTests +import FindDefinitionAndHoverTests +import FindImplementationAndHoverTests +import GarbageCollectionTests +import HaddockTests +import HighlightTests +import IfaceTests +import InitializeResponseTests +import LogType () +import NonLspCommandLine +import OpenCloseTest +import OutlineTests +import PluginSimpleTests +import PositionMappingTests +import PreprocessorTests +import ReferenceTests +import ResolveTests +import RootUriTests +import SafeTests +import SymlinkTests +import THTests +import UnitTests +import WatchedFileTests + +main :: IO () +main = do + -- We mess with env vars so run single-threaded. + defaultMainWithRerun $ testGroup "ghcide" + [ OpenCloseTest.tests + , InitializeResponseTests.tests + , CompletionTests.tests + , CPPTests.tests + , DiagnosticTests.tests + , CodeLensTests.tests + , OutlineTests.tests + , HighlightTests.tests + , FindDefinitionAndHoverTests.tests + , FindImplementationAndHoverTests.tests + , PluginSimpleTests.tests + , PreprocessorTests.tests + , THTests.tests + , SymlinkTests.tests + , SafeTests.tests + , UnitTests.tests + , HaddockTests.tests + , PositionMappingTests.tests + , WatchedFileTests.tests + , CradleTests.tests + , DependentFileTest.tests + , NonLspCommandLine.tests + , IfaceTests.tests + , BootTests.tests + , RootUriTests.tests + , AsyncTests.tests + , ClientSettingsTests.tests + , ReferenceTests.tests + , ResolveTests.tests + , GarbageCollectionTests.tests + , HieDbRetry.tests + , ExceptionTests.tests + ] diff --git a/ghcide-test/exe/NonLspCommandLine.hs b/ghcide-test/exe/NonLspCommandLine.hs new file mode 100644 index 0000000000..b2b41071d4 --- /dev/null +++ b/ghcide-test/exe/NonLspCommandLine.hs @@ -0,0 +1,51 @@ + +module NonLspCommandLine (tests) where + +import Control.Monad ((>=>)) +import Data.Foldable (for_) +import Development.Shake (getDirectoryFilesIO) +import System.Directory (copyFile, createDirectoryIfMissing) +import System.Directory.Extra (canonicalizePath) +import System.Environment.Blank (setEnv) +import System.Exit (ExitCode (ExitSuccess)) +import System.FilePath (takeDirectory, ()) +import qualified System.IO.Extra +import System.Process.Extra (CreateProcess (cwd), proc, + readCreateProcessWithExitCode) +import Test.Tasty +import Test.Tasty.HUnit +import Config (testDataDir) + + +-- A test to ensure that the command line ghcide workflow stays working +tests :: TestTree +tests = testGroup "ghcide command line" + [ testCase "works" $ withTempDir $ \dir -> do + ghcide <- locateGhcideExecutable + copyTestDataFiles dir "multi" + let cmd = (proc ghcide ["a/A.hs"]){cwd = Just dir} + + setEnv "HOME" "/homeless-shelter" False + + (ec, _, _) <- readCreateProcessWithExitCode cmd "" + + ec @?= ExitSuccess + ] + +locateGhcideExecutable :: IO FilePath +locateGhcideExecutable = pure "ghcide" + +-- | Version of 'System.IO.Extra.withTempDir' that canonicalizes the path +-- Which we need to do on macOS since the $TMPDIR can be in @/private/var@ or +-- @/var@ +withTempDir :: (FilePath -> IO a) -> IO a +withTempDir f = System.IO.Extra.withTempDir $ canonicalizePath >=> f + + +copyTestDataFiles :: FilePath -> FilePath -> IO () +copyTestDataFiles dir prefix = do + -- Copy all the test data files to the temporary workspace + testDataFiles <- getDirectoryFilesIO (testDataDir prefix) ["//*"] + for_ testDataFiles $ \f -> do + createDirectoryIfMissing True $ dir takeDirectory f + copyFile (testDataDir prefix f) (dir f) diff --git a/ghcide/test/exe/OpenCloseTest.hs b/ghcide-test/exe/OpenCloseTest.hs similarity index 64% rename from ghcide/test/exe/OpenCloseTest.hs rename to ghcide-test/exe/OpenCloseTest.hs index 2c7237fc28..83a85520f2 100644 --- a/ghcide/test/exe/OpenCloseTest.hs +++ b/ghcide-test/exe/OpenCloseTest.hs @@ -6,11 +6,13 @@ import Control.Monad import Language.LSP.Protocol.Message import Language.LSP.Test -- import Test.QuickCheck.Instances () +import Config (testWithDummyPluginEmpty) +import Test.Hls (waitForProgressBegin, + waitForProgressDone) import Test.Tasty -import TestUtils tests :: TestTree -tests = testSession "open close" $ do +tests = testWithDummyPluginEmpty "open close" $ do doc <- createDoc "Testing.hs" "haskell" "" void (skipManyTill anyMessage $ message SMethod_WindowWorkDoneProgressCreate) waitForProgressBegin diff --git a/ghcide-test/exe/OutlineTests.hs b/ghcide-test/exe/OutlineTests.hs new file mode 100644 index 0000000000..0d336a6bd0 --- /dev/null +++ b/ghcide-test/exe/OutlineTests.hs @@ -0,0 +1,153 @@ +{-# LANGUAGE OverloadedStrings #-} +{-# LANGUAGE TypeFamilies #-} + +module OutlineTests (tests) where + +import Config +import Control.Monad.IO.Class (liftIO) +import Data.Text (Text) +import qualified Data.Text as T +import Development.IDE.GHC.Compat (GhcVersion (..), ghcVersion) +import Language.LSP.Protocol.Types hiding (SemanticTokenAbsolute (..), + SemanticTokenRelative (..), + SemanticTokensEdit (..), mkRange) +import Language.LSP.Test +import Test.Hls.FileSystem (file, text) +import Test.Tasty +import Test.Tasty.HUnit + +testSymbols :: (HasCallStack) => TestName -> FilePath -> [Text] -> [DocumentSymbol] -> TestTree +testSymbols testName path content expectedSymbols = + testCase testName $ runWithDummyPlugin (mkIdeTestFs [file path (text $ T.unlines content)]) $ do + docId <- openDoc path "haskell" + symbols <- getDocumentSymbols docId + liftIO $ symbols @?= Right expectedSymbols + +testSymbolsA :: (HasCallStack) => TestName -> [Text] -> [DocumentSymbol] -> TestTree +testSymbolsA testName content expectedSymbols = + testSymbols testName "A.hs" content expectedSymbols + +tests :: TestTree +tests = + testGroup + "outline" + [ testSymbolsA + "type class:" + ["module A where", "class A a where a :: a -> Bool"] + [ moduleSymbol + "A" + (R 0 7 0 8) + [ classSymbol + "A a" + (R 1 0 1 30) + [docSymbol' "a" SymbolKind_Method (R 1 16 1 30) (R 1 16 1 17)] + ] + ], + testSymbolsA + "type class instance " + ["class A a where", "instance A () where"] + [ classSymbol "A a" (R 0 0 0 15) [], + docSymbol "A ()" SymbolKind_Interface (R 1 0 1 19) + ], + testSymbolsA "type family" ["{-# language TypeFamilies #-}", "type family A"] [docSymbolD "A" "type family" SymbolKind_Function (R 1 0 1 13)], + testSymbolsA + "type family instance " + ["{-# language TypeFamilies #-}", "type family A a", "type instance A () = ()"] + [ docSymbolD "A a" "type family" SymbolKind_Function (R 1 0 1 15), + docSymbol "A ()" SymbolKind_Interface (R 2 0 2 23) + ], + testSymbolsA "data family" ["{-# language TypeFamilies #-}", "data family A"] [docSymbolD "A" "data family" SymbolKind_Function (R 1 0 1 (if ghcVersion >= GHC910 then 13 else 11))], + testSymbolsA + "data family instance " + ["{-# language TypeFamilies #-}", "data family A a", "data instance A () = A ()"] + [ docSymbolD "A a" "data family" SymbolKind_Function (R 1 0 1 (if ghcVersion >= GHC910 then 15 else 11)), + docSymbol "A ()" SymbolKind_Interface (R 2 0 2 25) + ], + testSymbolsA "constant" ["a = ()"] [docSymbol "a" SymbolKind_Function (R 0 0 0 6)], + testSymbolsA "pattern" ["Just foo = Just 21"] [docSymbol "Just foo" SymbolKind_Function (R 0 0 0 18)], + testSymbolsA "pattern with type signature" ["{-# language ScopedTypeVariables #-}", "a :: () = ()"] [docSymbol "a :: ()" SymbolKind_Function (R 1 0 1 12)], + testSymbolsA "function" ["a _x = ()"] [docSymbol "a" SymbolKind_Function (R 0 0 0 9)], + testSymbolsA "type synonym" ["type A = Bool"] [docSymbol' "A" SymbolKind_TypeParameter (R 0 0 0 13) (R 0 5 0 6)], + testSymbolsA "datatype" ["data A = C"] [docSymbolWithChildren "A" SymbolKind_Struct (R 0 0 0 10) [docSymbol "C" SymbolKind_Constructor (R 0 9 0 10)]], + testSymbolsA + "record fields" + ["data A = B {", " x :: Int", " , y :: Int}"] + [ docSymbolWithChildren + "A" + SymbolKind_Struct + (R 0 0 2 13) + [ docSymbolWithChildren' + "B" + SymbolKind_Constructor + (R 0 9 2 13) + (R 0 9 0 10) + [ docSymbol "x" SymbolKind_Field (R 1 2 1 3), + docSymbol "y" SymbolKind_Field (R 2 4 2 5) + ] + ] + ], + testSymbolsA + "import" + ["import Data.Maybe ()"] + [ docSymbolWithChildren + "imports" + SymbolKind_Module + (R 0 0 0 20) + [ docSymbol "import Data.Maybe" SymbolKind_Module (R 0 0 0 20) + ] + ], + testSymbolsA + "multiple import" + ["", "import Data.Maybe ()", "", "import Control.Exception ()", ""] + [ docSymbolWithChildren + "imports" + SymbolKind_Module + (R 1 0 3 27) + [ docSymbol "import Data.Maybe" SymbolKind_Module (R 1 0 1 20), + docSymbol "import Control.Exception" SymbolKind_Module (R 3 0 3 27) + ] + ], + testSymbolsA + "foreign import" + [ "{-# language ForeignFunctionInterface #-}", + "foreign import ccall \"a\" a :: Int" + ] + [docSymbolD "a" "import" SymbolKind_Object (R 1 0 1 33)], + testSymbolsA + "foreign export" + [ "{-# language ForeignFunctionInterface #-}", + "foreign export ccall odd :: Int -> Bool" + ] + [docSymbolD "odd" "export" SymbolKind_Object (R 1 0 1 39)] + ] + where + docSymbol name kind loc = + DocumentSymbol name Nothing kind Nothing Nothing loc loc Nothing + docSymbol' name kind loc selectionLoc = + DocumentSymbol name Nothing kind Nothing Nothing loc selectionLoc Nothing + docSymbolD name detail kind loc = + DocumentSymbol name (Just detail) kind Nothing Nothing loc loc Nothing + docSymbolWithChildren name kind loc cc = + DocumentSymbol name Nothing kind Nothing Nothing loc loc (Just cc) + docSymbolWithChildren' name kind loc selectionLoc cc = + DocumentSymbol name Nothing kind Nothing Nothing loc selectionLoc (Just cc) + moduleSymbol name loc cc = + DocumentSymbol + name + Nothing + SymbolKind_File + Nothing + Nothing + (R 0 0 maxBound 0) + loc + (Just cc) + classSymbol name loc cc = + DocumentSymbol + name + (Just "class") + SymbolKind_Interface + Nothing + Nothing + loc + loc + (Just cc) diff --git a/ghcide/test/exe/PluginSimpleTests.hs b/ghcide-test/exe/PluginSimpleTests.hs similarity index 84% rename from ghcide/test/exe/PluginSimpleTests.hs rename to ghcide-test/exe/PluginSimpleTests.hs index cc5b5eba6c..c160d2461c 100644 --- a/ghcide/test/exe/PluginSimpleTests.hs +++ b/ghcide-test/exe/PluginSimpleTests.hs @@ -1,17 +1,15 @@ module PluginSimpleTests (tests) where +import Config import Control.Monad.IO.Class (liftIO) -import Development.IDE.GHC.Compat (GhcVersion (..)) import Development.IDE.Test (expectDiagnostics) import Language.LSP.Protocol.Types hiding (SemanticTokenAbsolute (..), SemanticTokenRelative (..), SemanticTokensEdit (..), mkRange) import Language.LSP.Test import System.FilePath --- import Test.QuickCheck.Instances () import Test.Tasty -import TestUtils tests :: TestTree tests = @@ -36,15 +34,13 @@ tests = -- Error: cabal: Failed to build ghc-typelits-natnormalise-0.7.7 (which is -- required by plugin-1.0.0). See the build log above for details. - ignoreFor (BrokenForGHC [GHC96, GHC98]) "fragile, frequently times out" $ - ignoreFor (BrokenSpecific Windows [GHC94]) "ghc-typelist-natnormalise fails to build on GHC 9.4.2 for windows only" $ - testSessionWithExtraFiles "plugin-knownnat" "simple plugin" $ \dir -> do + testWithExtraFiles "simple plugin" "plugin-knownnat" $ \dir -> do _ <- openDoc (dir "KnownNat.hs") "haskell" liftIO $ writeFile (dir"hie.yaml") "cradle: {cabal: [{path: '.', component: 'lib:plugin'}]}" expectDiagnostics [ ( "KnownNat.hs", - [(DiagnosticSeverity_Error, (9, 15), "Variable not in scope: c")] + [(DiagnosticSeverity_Error, (9, 15), "Variable not in scope: c", Just "GHC-88464")] ) ] diff --git a/ghcide/test/exe/PositionMappingTests.hs b/ghcide-test/exe/PositionMappingTests.hs similarity index 85% rename from ghcide/test/exe/PositionMappingTests.hs rename to ghcide-test/exe/PositionMappingTests.hs index 083e765db0..dfd9b0374b 100644 --- a/ghcide/test/exe/PositionMappingTests.hs +++ b/ghcide-test/exe/PositionMappingTests.hs @@ -1,16 +1,16 @@ {-# LANGUAGE AllowAmbiguousTypes #-} -{-# LANGUAGE OverloadedLabels #-} module PositionMappingTests (tests) where -import Data.Row +import qualified Data.EnumMap.Strict as EM import qualified Data.Text as T -import Data.Text.Utf16.Rope (Rope) -import qualified Data.Text.Utf16.Rope as Rope +import Data.Text.Utf16.Rope.Mixed (Rope) +import qualified Data.Text.Utf16.Rope.Mixed as Rope import Development.IDE.Core.PositionMapping (PositionResult (..), fromCurrent, positionResultToMaybe, - toCurrent) + toCurrent, + toCurrentPosition) import Development.IDE.Types.Location import Language.LSP.Protocol.Types hiding (SemanticTokenAbsolute (..), @@ -20,15 +20,38 @@ import Language.LSP.Protocol.Types hiding import Language.LSP.VFS (applyChange) import Test.QuickCheck -- import Test.QuickCheck.Instances () +import Control.Arrow (second) import Data.Functor.Identity (runIdentity) +import Data.Text (Text) +import Development.IDE.Core.Shake (updatePositionMappingHelper) import Test.Tasty import Test.Tasty.HUnit import Test.Tasty.QuickCheck +enumMapMappingTest :: TestTree +enumMapMappingTest = testCase "enumMapMappingTest" $ do + let + mkCE :: UInt -> UInt -> UInt -> UInt -> Text -> TextDocumentContentChangeEvent + mkCE l1 c1 l2 c2 = mkChangeEvent (Range (Position l1 c1) (Position l2 c2)) + events :: [(Int32, [TextDocumentContentChangeEvent])] + events = map (second return) [(0, mkCE 0 0 0 0 ""), (1, mkCE 0 1 0 1 " "), (2, mkCE 0 2 0 2 " "), (3, mkCE 0 3 0 3 " "), (4, mkCE 0 4 0 4 " "), (5, mkCE 0 5 0 5 " ")] + finalMap = Prelude.foldl (\m (i, e) -> updatePositionMappingHelper i e m) mempty events + let updatePose fromPos = do + mapping <- snd <$> EM.lookup 0 finalMap + toCurrentPosition mapping fromPos + updatePose (Position 0 4) @?= Just (Position 0 9) + updatePose (Position 0 5) @?= Just (Position 0 10) + +mkChangeEvent :: Range -> Text -> TextDocumentContentChangeEvent +mkChangeEvent r t = TextDocumentContentChangeEvent $ InL + TextDocumentContentChangePartial {_range = r, _rangeLength = Nothing, _text = t} + tests :: TestTree tests = testGroup "position mapping" - [ testGroup "toCurrent" + [ + enumMapMappingTest + , testGroup "toCurrent" [ testCase "before" $ toCurrent (Range (Position 0 1) (Position 0 3)) @@ -144,10 +167,7 @@ tests = rope <- genRope range <- genRange rope PrintableText replacement <- arbitrary - let newRope = runIdentity $ applyChange mempty rope - (TextDocumentContentChangeEvent $ InL $ #range .== range - .+ #rangeLength .== Nothing - .+ #text .== replacement) + let newRope = runIdentity $ applyChange mempty rope $ mkChangeEvent range replacement newPos <- genPosition newRope pure (range, replacement, newPos) forAll diff --git a/ghcide/test/exe/PreprocessorTests.hs b/ghcide-test/exe/PreprocessorTests.hs similarity index 85% rename from ghcide/test/exe/PreprocessorTests.hs rename to ghcide-test/exe/PreprocessorTests.hs index 315ffd1ccb..24e2e80a10 100644 --- a/ghcide/test/exe/PreprocessorTests.hs +++ b/ghcide-test/exe/PreprocessorTests.hs @@ -8,11 +8,11 @@ import Language.LSP.Protocol.Types hiding (SemanticTokenAbsolute (..), SemanticTokensEdit (..), mkRange) import Language.LSP.Test -- import Test.QuickCheck.Instances () +import Config import Test.Tasty -import TestUtils tests :: TestTree -tests = testSessionWait "preprocessor" $ do +tests = testWithDummyPluginEmpty "preprocessor" $ do let content = T.unlines [ "{-# OPTIONS_GHC -F -pgmF=ghcide-test-preprocessor #-}" @@ -22,6 +22,6 @@ tests = testSessionWait "preprocessor" $ do _ <- createDoc "Testing.hs" "haskell" content expectDiagnostics [ ( "Testing.hs", - [(DiagnosticSeverity_Error, (2, 8), "Variable not in scope: z")] + [(DiagnosticSeverity_Error, (2, 8), "Variable not in scope: z", Nothing)] -- TODO: Why doesn't this work with expected code "GHC-88464"? ) ] diff --git a/ghcide/test/exe/Progress.hs b/ghcide-test/exe/Progress.hs similarity index 98% rename from ghcide/test/exe/Progress.hs rename to ghcide-test/exe/Progress.hs index a92fea9bc4..08ad03c78b 100644 --- a/ghcide/test/exe/Progress.hs +++ b/ghcide-test/exe/Progress.hs @@ -38,7 +38,7 @@ reportProgressTests = testGroup "recordProgress" model state $ \st -> recordProgress st key change model stateModelIO k = do state <- fromModel =<< stateModelIO - k state + _ <- k state toModel state test name p = testCase name $ do InProgressModel{..} <- p diff --git a/ghcide/test/exe/ReferenceTests.hs b/ghcide-test/exe/ReferenceTests.hs similarity index 58% rename from ghcide/test/exe/ReferenceTests.hs rename to ghcide-test/exe/ReferenceTests.hs index 5abb18bfe8..cdbf8e472d 100644 --- a/ghcide/test/exe/ReferenceTests.hs +++ b/ghcide-test/exe/ReferenceTests.hs @@ -1,3 +1,7 @@ +{-# LANGUAGE DataKinds #-} +{-# LANGUAGE GADTs #-} +{-# LANGUAGE OverloadedStrings #-} + module ReferenceTests (tests) where @@ -7,8 +11,6 @@ import Control.Monad import Control.Monad.IO.Class (liftIO) import Data.List.Extra import qualified Data.Set as Set -import Development.IDE.Test (configureCheckProject, - referenceReady) import Development.IDE.Types.Location import qualified Language.LSP.Protocol.Lens as L import Language.LSP.Protocol.Types hiding @@ -18,14 +20,27 @@ import Language.LSP.Protocol.Types hiding mkRange) import Language.LSP.Test import System.Directory -import System.FilePath -- import Test.QuickCheck.Instances () +import Config import Control.Lens ((^.)) +import qualified Data.Aeson as A +import Data.Default (def) import Data.Tuple.Extra +import GHC.TypeLits (symbolVal) +import Ide.PluginUtils (toAbsolute) +import Ide.Types +import System.FilePath (addTrailingPathSeparator, + ()) +import Test.Hls (BrokenBehavior (..), + ExpectBroken (..), + FromServerMessage' (..), + SMethod (..), + TCustomMessage (..), + TNotificationMessage (..), + unCurrent) +import Test.Hls.FileSystem (copyDir) import Test.Tasty -import Test.Tasty.ExpectedFailure import Test.Tasty.HUnit -import TestUtils tests :: TestTree @@ -78,29 +93,29 @@ tests = testGroup "references" , ("Main.hs", 10, 0) ] - , expectFailBecause "references provider does not respect includeDeclaration parameter" $ - referenceTest "works when we ask to exclude declarations" - ("References.hs", 4, 7) - NoExcludeDeclaration - [ ("References.hs", 6, 0) - , ("References.hs", 6, 14) - , ("References.hs", 9, 7) - , ("References.hs", 10, 11) - ] - - , referenceTest "INCORRECTLY returns declarations when we ask to exclude them" + -- TODO: references provider does not respect includeDeclaration parameter + , referenceTestExpectFail "works when we ask to exclude declarations" ("References.hs", 4, 7) NoExcludeDeclaration - [ ("References.hs", 4, 6) - , ("References.hs", 6, 0) - , ("References.hs", 6, 14) - , ("References.hs", 9, 7) - , ("References.hs", 10, 11) - ] + (BrokenIdeal + [ ("References.hs", 6, 0) + , ("References.hs", 6, 14) + , ("References.hs", 9, 7) + , ("References.hs", 10, 11) + ] + ) + (BrokenCurrent + [ ("References.hs", 4, 6) + , ("References.hs", 6, 0) + , ("References.hs", 6, 14) + , ("References.hs", 9, 7) + , ("References.hs", 10, 11) + ] + ) ] , testGroup "can get references to non FOIs" - [ referenceTest "can get references to symbol defined in a module we import" + [ referenceTest "references to symbol defined in a module we import" ("References.hs", 22, 4) YesIncludeDeclaration [ ("References.hs", 22, 4) @@ -108,7 +123,7 @@ tests = testGroup "references" , ("OtherModule.hs", 4, 0) ] - , referenceTest "can get references in modules that import us to symbols we define" + , referenceTest "references in modules that import us to symbols we define" ("OtherModule.hs", 4, 0) YesIncludeDeclaration [ ("References.hs", 22, 4) @@ -116,7 +131,7 @@ tests = testGroup "references" , ("OtherModule.hs", 4, 0) ] - , referenceTest "can get references to symbol defined in a module we import transitively" + , referenceTest "references to symbol defined in a module we import transitively" ("References.hs", 24, 4) YesIncludeDeclaration [ ("References.hs", 24, 4) @@ -124,7 +139,7 @@ tests = testGroup "references" , ("OtherOtherModule.hs", 2, 0) ] - , referenceTest "can get references in modules that import us transitively to symbols we define" + , referenceTest "references in modules that transitively use symbols we define" ("OtherOtherModule.hs", 2, 0) YesIncludeDeclaration [ ("References.hs", 24, 4) @@ -132,7 +147,7 @@ tests = testGroup "references" , ("OtherOtherModule.hs", 2, 0) ] - , referenceTest "can get type references to other modules" + , referenceTest "type references to other modules" ("Main.hs", 12, 10) YesIncludeDeclaration [ ("Main.hs", 12, 7) @@ -149,44 +164,64 @@ data IncludeDeclaration = YesIncludeDeclaration | NoExcludeDeclaration -getReferences' :: SymbolLocation -> IncludeDeclaration -> Session ([Location]) +getReferences' :: SymbolLocation -> IncludeDeclaration -> Session [Location] getReferences' (file, l, c) includeDeclaration = do doc <- openDoc file "haskell" getReferences doc (Position l c) $ toBool includeDeclaration where toBool YesIncludeDeclaration = True toBool NoExcludeDeclaration = False + + referenceTestSession :: String -> FilePath -> [FilePath] -> (FilePath -> Session ()) -> TestTree -referenceTestSession name thisDoc docs' f = testSessionWithExtraFiles "references" name $ \dir -> do - -- needed to build whole project indexing - configureCheckProject True - let docs = map (dir ) $ delete thisDoc $ nubOrd docs' - -- Initial Index - docid <- openDoc thisDoc "haskell" - let - loop :: [FilePath] -> Session () - loop [] = pure () - loop docs = do - doc <- skipManyTill anyMessage $ referenceReady (`elem` docs) - loop (delete doc docs) - loop docs - f dir - closeDoc docid +referenceTestSession name thisDoc docs' f = do + testWithDummyPlugin' name (mkIdeTestFs [copyDir "references"]) $ \fs -> do + let rootDir = addTrailingPathSeparator fs + -- needed to build whole project indexing + configureCheckProject True + -- need to get the real paths through links + docs <- mapM (liftIO . canonicalizePath . (fs )) $ delete thisDoc $ nubOrd docs' + -- Initial Index + docid <- openDoc thisDoc "haskell" + + liftIO $ putStrLn $ "docs:" <> show docs + let + -- todo wait for docs + loop :: [FilePath] -> Session () + loop [] = pure () + loop docs = do + + doc <- skipManyTill anyMessage $ referenceReady (`elem` docs) + loop (delete doc docs) + loop docs + f rootDir + closeDoc docid -- | Given a location, lookup the symbol and all references to it. Make sure -- they are the ones we expect. -referenceTest :: String -> SymbolLocation -> IncludeDeclaration -> [SymbolLocation] -> TestTree +referenceTest :: (HasCallStack) => String -> SymbolLocation -> IncludeDeclaration -> [SymbolLocation] -> TestTree referenceTest name loc includeDeclaration expected = - referenceTestSession name (fst3 loc) docs $ \dir -> do + referenceTestSession name (fst3 loc) docs $ \rootDir -> do actual <- getReferences' loc includeDeclaration - liftIO $ actual `expectSameLocations` map (first3 (dir )) expected + liftIO $ expectSameLocations rootDir actual expected where docs = map fst3 expected +referenceTestExpectFail + :: (HasCallStack) + => String + -> SymbolLocation + -> IncludeDeclaration + -> ExpectBroken 'Ideal [SymbolLocation] + -> ExpectBroken 'Current [SymbolLocation] + -> TestTree +referenceTestExpectFail name loc includeDeclaration _ = + referenceTest name loc includeDeclaration . unCurrent + type SymbolLocation = (FilePath, UInt, UInt) -expectSameLocations :: [Location] -> [SymbolLocation] -> Assertion -expectSameLocations actual expected = do +expectSameLocations :: (HasCallStack) => FilePath -> [Location] -> [SymbolLocation] -> Assertion +expectSameLocations rootDir actual expected = do let actual' = Set.map (\location -> (location ^. L.uri , location ^. L.range . L.start . L.line . Lens.to fromIntegral @@ -194,6 +229,19 @@ expectSameLocations actual expected = do $ Set.fromList actual expected' <- Set.fromList <$> (forM expected $ \(file, l, c) -> do - fp <- canonicalizePath file + fp <- canonicalizePath $ toAbsolute rootDir file return (filePathToUri fp, l, c)) actual' @?= expected' + + +-- todo find where to put this in hls +configureCheckProject :: Bool -> Session () +configureCheckProject overrideCheckProject = setConfigSection "haskell" (A.toJSON $ def{checkProject = overrideCheckProject}) +referenceReady :: (FilePath -> Bool) -> Session FilePath +referenceReady pred = satisfyMaybe $ \case + FromServerMess (SMethod_CustomMethod p) (NotMess TNotificationMessage{_params}) + | A.Success fp <- A.fromJSON _params + , pred fp + , symbolVal p == "ghcide/reference/ready" + -> Just fp + _ -> Nothing diff --git a/ghcide-test/exe/ResolveTests.hs b/ghcide-test/exe/ResolveTests.hs new file mode 100644 index 0000000000..4fc917c56b --- /dev/null +++ b/ghcide-test/exe/ResolveTests.hs @@ -0,0 +1,199 @@ +{-# LANGUAGE DeriveAnyClass #-} +{-# LANGUAGE DerivingStrategies #-} +{-# LANGUAGE OverloadedStrings #-} +module ResolveTests (tests) where + +import Config +import Control.Lens +import Data.Aeson +import qualified Data.Maybe as Maybe +import Data.Text (Text) +import qualified Data.Text as T +import GHC.Generics +import Ide.Logger +import Ide.Types (PluginDescriptor (..), PluginId, + defaultPluginDescriptor, + mkPluginHandler, + mkResolveHandler) +import qualified Language.LSP.Protocol.Lens as J +import qualified Language.LSP.Protocol.Lens as JL +import Language.LSP.Protocol.Message (SomeMethod (..)) +import qualified Language.LSP.Protocol.Message as LSP +import Language.LSP.Protocol.Types +import Language.LSP.Test hiding (resolveCompletion) +import Test.Hls (IdeState, SMethod (..), liftIO, + mkPluginTestDescriptor, + someMethodToMethodString, + waitForAllProgressDone) +import qualified Test.Hls.FileSystem as FS +import Test.Tasty +import Test.Tasty.HUnit + +tests :: TestTree +tests = testGroup "resolve" + [ testGroup "with and without data" resolveRequests + ] + +removeData :: JL.HasData_ s (Maybe a) => s -> s +removeData param = param & JL.data_ .~ Nothing + +simpleTestSession :: TestName -> Session () -> TestTree +simpleTestSession name act = + testCase name $ runWithResolvePlugin (mkIdeTestFs [FS.directCradle ["A.hs"]]) (const act) + +runWithResolvePlugin :: FS.VirtualFileTree -> (FilePath -> Session a) -> IO a +runWithResolvePlugin fs = + testSessionWithPlugin fs + (mkPluginTestDescriptor resolvePluginDescriptor "resolve-plugin") + +data CompletionItemResolveData = CompletionItemResolveData + { completionItemResolve_number :: Int + } + deriving stock (Show, Eq, Ord, Generic) + deriving anyclass (ToJSON, FromJSON) + +data CodeActionResolve = CodeActionResolve + { codeActionResolve_number :: Int + } + deriving stock (Show, Eq, Ord, Generic) + deriving anyclass (ToJSON, FromJSON) + +data CodeLensResolve = CodeLensResolve + { codeLensResolve_number :: Int + } + deriving stock (Show, Eq, Ord, Generic) + deriving anyclass (ToJSON, FromJSON) + +resolvePluginDescriptor :: Recorder (WithPriority Text) -> PluginId -> PluginDescriptor IdeState +resolvePluginDescriptor recorder pid = (defaultPluginDescriptor pid "Test Plugin for Resolve Requests") + { pluginHandlers = mconcat + [ mkResolveHandler LSP.SMethod_CompletionItemResolve $ \_ _ param _ CompletionItemResolveData{} -> pure param + , mkPluginHandler LSP.SMethod_TextDocumentCompletion $ \_ _ _ -> do + pure $ InL + [ defCompletionItem "test item without data" + , defCompletionItem "test item with data" + & J.data_ .~ Just (toJSON $ CompletionItemResolveData 100) + ] + , mkResolveHandler LSP.SMethod_CodeActionResolve $ \_ _ param _ CodeActionResolve{} -> pure param + , mkPluginHandler LSP.SMethod_TextDocumentCodeAction $ \_ _ _ -> do + logWith recorder Debug "Why is the handler not called?" + pure $ InL + [ InR $ defCodeAction "test item without data" + , InR $ defCodeAction "test item with data" + & J.data_ .~ Just (toJSON $ CodeActionResolve 70) + ] + , mkResolveHandler LSP.SMethod_CodeLensResolve $ \_ _ param _ CodeLensResolve{} -> pure param + , mkPluginHandler LSP.SMethod_TextDocumentCodeLens $ \_ _ _ -> do + pure $ InL + [ defCodeLens "test item without data" + , defCodeLens "test item with data" + & J.data_ .~ Just (toJSON $ CodeLensResolve 50) + ] + ] + } + +resolveRequests :: [TestTree] +resolveRequests = + [ simpleTestSession "completion resolve" $ do + doc <- createDoc "A.hs" "haskell" $ T.unlines + [ "module A where" + , "data Foo = Foo { foo :: Int }" + , "bar = Foo 4" + ] + waitForAllProgressDone + items <- getCompletions doc (Position 2 7) + let resolveCompItems = filter (\i -> "test item" `T.isPrefixOf` (i ^. J.label)) items + liftIO $ assertEqual "There must be exactly two results" 2 (length resolveCompItems) + -- This must not throw an error. + _ <- traverse (resolveCompletion . removeData) resolveCompItems + pure () + , simpleTestSession "codeAction resolve" $ do + doc <- createDoc "A.hs" "haskell" $ T.unlines + [ "module A where" + , "data Foo = Foo { foo :: Int }" + , "bar = Foo 4" + ] + waitForAllProgressDone + -- Cant use 'getAllCodeActions', as this lsp-test function queries the diagnostic + -- locations and we don't have diagnostics in these tests. + cas <- Maybe.mapMaybe (preview _R) <$> getCodeActions doc (Range (Position 0 0) (Position 1 0)) + let resolveCas = filter (\i -> "test item" `T.isPrefixOf` (i ^. J.title)) cas + liftIO $ assertEqual "There must be exactly two results" 2 (length resolveCas) + -- This must not throw an error. + _ <- traverse (resolveCodeAction . removeData) resolveCas + pure () + , simpleTestSession "codelens resolve" $ do + doc <- createDoc "A.hs" "haskell" $ T.unlines + [ "module A where" + , "data Foo = Foo { foo :: Int }" + , "bar = Foo 4" + ] + waitForAllProgressDone + cd <- getCodeLenses doc + let resolveCodeLenses = filter (\i -> case i ^. J.command of + Just cmd -> "test item" `T.isPrefixOf` (cmd ^. J.title) + Nothing -> False + ) cd + liftIO $ assertEqual "There must be exactly two results" 2 (length resolveCodeLenses) + -- This must not throw an error. + _ <- traverse (resolveCodeLens . removeData) resolveCodeLenses + pure () + ] + +defCompletionItem :: T.Text -> CompletionItem +defCompletionItem lbl = CompletionItem + { _label = lbl + , _labelDetails = Nothing + , _kind = Nothing + , _tags = Nothing + , _detail = Nothing + , _documentation = Nothing + , _deprecated = Nothing + , _preselect = Nothing + , _sortText = Nothing + , _filterText = Nothing + , _insertText = Just "insertion" + , _insertTextFormat = Nothing + , _insertTextMode = Nothing + , _textEdit = Nothing + , _textEditText = Nothing + , _additionalTextEdits = Nothing + , _commitCharacters = Nothing + , _command = Nothing + , _data_ = Nothing + } + +defCodeAction :: T.Text -> CodeAction +defCodeAction lbl = CodeAction + { _title = lbl + , _kind = Just CodeActionKind_Refactor + , _diagnostics = Nothing + , _isPreferred = Nothing + , _disabled = Nothing + , _edit = Nothing + , _command = Just $ Command + { _title = lbl + , _command = lbl + , _arguments = Nothing + } + , _data_ = Nothing + } + +defCodeLens :: T.Text -> CodeLens +defCodeLens lbl = CodeLens + { _range = mkRange 0 0 1 0 + , _command = Just $ Command + { _title = lbl + , _command = lbl + , _arguments = Nothing + } + , _data_ = Nothing + } + +-- TODO: expose this from lsp-test +resolveCompletion :: CompletionItem -> Session CompletionItem +resolveCompletion item = do + rsp <- request SMethod_CompletionItemResolve item + case rsp ^. JL.result of + Left err -> liftIO $ assertFailure (someMethodToMethodString (SomeMethod SMethod_CompletionItemResolve) <> " failed with: " <> show err) + Right x -> pure x diff --git a/ghcide/test/exe/RootUriTests.hs b/ghcide-test/exe/RootUriTests.hs similarity index 59% rename from ghcide/test/exe/RootUriTests.hs rename to ghcide-test/exe/RootUriTests.hs index 2237150508..2a9cb19ab1 100644 --- a/ghcide/test/exe/RootUriTests.hs +++ b/ghcide-test/exe/RootUriTests.hs @@ -7,20 +7,33 @@ import Development.IDE.Test (expectNoMoreDiagnostics) import Language.LSP.Test import System.FilePath -- import Test.QuickCheck.Instances () +import Config +import Data.Default (def) +import Test.Hls (TestConfig (..), + runSessionWithTestConfig) +import Test.Hls.FileSystem (copyDir) import Test.Tasty import Test.Tasty.HUnit -import TestUtils -- | checks if we use InitializeParams.rootUri for loading session tests :: TestTree tests = testCase "use rootUri" . runTest "dirA" "dirB" $ \dir -> do let bPath = dir "dirB/Foo.hs" - liftIO $ copyTestDataFiles dir "rootUri" bSource <- liftIO $ readFileUtf8 bPath _ <- createDoc "Foo.hs" "haskell" bSource expectNoMoreDiagnostics 0.5 where -- similar to run' except we can configure where to start ghcide and session runTest :: FilePath -> FilePath -> (FilePath -> Session ()) -> IO () - runTest dir1 dir2 s = withTempDir $ \dir -> runInDir' dir dir1 dir2 [] (s dir) + runTest dir1 dir2 = runSessionWithTestConfig + def + { + testPluginDescriptor = dummyPlugin + , testDirLocation = Right $ mkIdeTestFs [copyDir "rootUri"] + , testServerRoot = Just dir1 + , testClientRoot = Just dir2 + , testShiftRoot = True + } + + diff --git a/ghcide/test/exe/SafeTests.hs b/ghcide-test/exe/SafeTests.hs similarity index 94% rename from ghcide/test/exe/SafeTests.hs rename to ghcide-test/exe/SafeTests.hs index 4bdef3b7c1..85964ba07a 100644 --- a/ghcide/test/exe/SafeTests.hs +++ b/ghcide-test/exe/SafeTests.hs @@ -5,15 +5,15 @@ import qualified Data.Text as T import Development.IDE.Test (expectNoMoreDiagnostics) import Language.LSP.Test +import Config import Test.Tasty -import TestUtils tests :: TestTree tests = testGroup "SafeHaskell" [ -- Test for https://github.com/haskell/ghcide/issues/424 - testSessionWait "load" $ do + testWithDummyPluginEmpty "load" $ do let sourceA = T.unlines ["{-# LANGUAGE Trustworthy #-}" diff --git a/ghcide/test/exe/SymlinkTests.hs b/ghcide-test/exe/SymlinkTests.hs similarity index 94% rename from ghcide/test/exe/SymlinkTests.hs rename to ghcide-test/exe/SymlinkTests.hs index 19c86a5264..dda41922f0 100644 --- a/ghcide/test/exe/SymlinkTests.hs +++ b/ghcide-test/exe/SymlinkTests.hs @@ -10,9 +10,9 @@ import Language.LSP.Test import System.Directory import System.FilePath +import Config import Test.Tasty import Test.Tasty.HUnit -import TestUtils -- | Tests for projects that use symbolic links one way or another tests :: TestTree @@ -22,6 +22,6 @@ tests = liftIO $ createFileLink (dir "some_loc" "Sym.hs") (dir "other_loc" "Sym.hs") let fooPath = dir "src" "Foo.hs" _ <- openDoc fooPath "haskell" - expectDiagnosticsWithTags [("src" "Foo.hs", [(DiagnosticSeverity_Warning, (2, 0), "The import of 'Sym' is redundant", Just DiagnosticTag_Unnecessary)])] + expectDiagnosticsWithTags [("src" "Foo.hs", [(DiagnosticSeverity_Warning, (2, 0), "The import of 'Sym' is redundant", Nothing, Just DiagnosticTag_Unnecessary)])] pure () ] diff --git a/ghcide/test/exe/THTests.hs b/ghcide-test/exe/THTests.hs similarity index 84% rename from ghcide/test/exe/THTests.hs rename to ghcide-test/exe/THTests.hs index 8b1d5a19c8..59b06431f5 100644 --- a/ghcide/test/exe/THTests.hs +++ b/ghcide-test/exe/THTests.hs @@ -1,10 +1,8 @@ -{-# LANGUAGE OverloadedLabels #-} - module THTests (tests) where +import Config import Control.Monad.IO.Class (liftIO) -import Data.Row import qualified Data.Text as T import Development.IDE.GHC.Util import Development.IDE.Test (expectCurrentDiagnostics, @@ -17,14 +15,13 @@ import Language.LSP.Test import System.FilePath import Test.Tasty import Test.Tasty.HUnit -import TestUtils tests :: TestTree tests = testGroup "TemplateHaskell" [ -- Test for https://github.com/haskell/ghcide/pull/212 - testSessionWait "load" $ do + testWithDummyPluginEmpty "load" $ do let sourceA = T.unlines [ "{-# LANGUAGE PackageImports #-}", @@ -46,8 +43,8 @@ tests = ] _ <- createDoc "A.hs" "haskell" sourceA _ <- createDoc "B.hs" "haskell" sourceB - expectDiagnostics [ ( "B.hs", [(DiagnosticSeverity_Error, (6, 29), "Variable not in scope: n")] ) ] - , testSessionWait "newtype-closure" $ do + expectDiagnostics [ ( "B.hs", [(DiagnosticSeverity_Error, (6, 29), "Variable not in scope: n", Just "GHC-88464")] ) ] + , testWithDummyPluginEmpty "newtype-closure" $ do let sourceA = T.unlines [ "{-# LANGUAGE DeriveDataTypeable #-}" @@ -71,11 +68,11 @@ tests = , thReloadingTest False , thLoadingTest , thCoreTest - , ignoreInWindowsBecause "Broken in windows" $ thReloadingTest True + , thReloadingTest True -- Regression test for https://github.com/haskell/haskell-language-server/issues/891 , thLinkingTest False - , ignoreInWindowsBecause "Broken in windows" $ thLinkingTest True - , testSessionWait "findsTHIdentifiers" $ do + , thLinkingTest True + , testWithDummyPluginEmpty "findsTHIdentifiers" $ do let sourceA = T.unlines [ "{-# LANGUAGE TemplateHaskell #-}" @@ -94,7 +91,7 @@ tests = , "main = $a (putStrLn \"success!\")"] _ <- createDoc "A.hs" "haskell" sourceA _ <- createDoc "B.hs" "haskell" sourceB - expectDiagnostics [ ( "B.hs", [(DiagnosticSeverity_Warning, (4, 0), "Top-level binding with no type signature: main :: IO ()")] ) ] + expectDiagnostics [ ( "B.hs", [(DiagnosticSeverity_Warning, (4, 0), "Top-level binding with no type signature: main :: IO ()", Just "GHC-38417")] ) ] , testCase "findsTHnewNameConstructor" $ runWithExtraFiles "THNewName" $ \dir -> do -- This test defines a TH value with the meaning "data A = A" in A.hs @@ -105,7 +102,7 @@ tests = let cPath = dir "C.hs" _ <- openDoc cPath "haskell" - expectDiagnostics [ ( cPath, [(DiagnosticSeverity_Warning, (3, 0), "Top-level binding with no type signature: a :: A")] ) ] + expectDiagnostics [ ( cPath, [(DiagnosticSeverity_Warning, (3, 0), "Top-level binding with no type signature: a :: A", Just "GHC-38417")] ) ] ] @@ -138,19 +135,19 @@ thReloadingTest unboxed = testCase name $ runWithExtraFiles dir $ \dir -> do bdoc <- createDoc bPath "haskell" bSource cdoc <- createDoc cPath "haskell" cSource - expectDiagnostics [("THB.hs", [(DiagnosticSeverity_Warning, (4,thDollarIdx), "Top-level binding")])] + expectDiagnostics [("THB.hs", [(DiagnosticSeverity_Warning, (4,1), "Top-level binding", Just "GHC-38417")])] -- Change th from () to Bool let aSource' = T.unlines $ init (T.lines aSource) ++ ["th_a = [d| a = False|]"] - changeDoc adoc [TextDocumentContentChangeEvent . InR . (.==) #text $ aSource'] + changeDoc adoc [TextDocumentContentChangeEvent . InR $ TextDocumentContentChangeWholeDocument aSource'] -- generate an artificial warning to avoid timing out if the TH change does not propagate - changeDoc cdoc [TextDocumentContentChangeEvent . InR . (.==) #text $ cSource <> "\nfoo=()"] + changeDoc cdoc [TextDocumentContentChangeEvent . InR . TextDocumentContentChangeWholeDocument $ cSource <> "\nfoo=()"] -- Check that the change propagates to C expectDiagnostics - [("THC.hs", [(DiagnosticSeverity_Error, (4, 4), "Couldn't match expected type '()' with actual type 'Bool'")]) - ,("THC.hs", [(DiagnosticSeverity_Warning, (6,0), "Top-level binding")]) - ,("THB.hs", [(DiagnosticSeverity_Warning, (4,thDollarIdx), "Top-level bindin")]) + [("THC.hs", [(DiagnosticSeverity_Error, (4, 4), "Couldn't match expected type '()' with actual type 'Bool'", Just "GHC-83865")]) + ,("THC.hs", [(DiagnosticSeverity_Warning, (6,0), "Top-level binding", Just "GHC-38417")]) + ,("THB.hs", [(DiagnosticSeverity_Warning, (4,1), "Top-level bindin", Just "GHC-38417")]) ] closeDoc adoc @@ -173,18 +170,17 @@ thLinkingTest unboxed = testCase name $ runWithExtraFiles dir $ \dir -> do adoc <- createDoc aPath "haskell" aSource bdoc <- createDoc bPath "haskell" bSource - expectDiagnostics [("THB.hs", [(DiagnosticSeverity_Warning, (4,thDollarIdx), "Top-level binding")])] + expectDiagnostics [("THB.hs", [(DiagnosticSeverity_Warning, (4,1), "Top-level binding", Just "GHC-38417")])] let aSource' = T.unlines $ init (init (T.lines aSource)) ++ ["th :: DecsQ", "th = [d| a = False|]"] - changeDoc adoc [TextDocumentContentChangeEvent . InR . (.==) #text $ aSource'] + changeDoc adoc [TextDocumentContentChangeEvent . InR $ TextDocumentContentChangeWholeDocument aSource'] -- modify b too let bSource' = T.unlines $ init (T.lines bSource) ++ ["$th"] - changeDoc bdoc [TextDocumentContentChangeEvent . InR . (.==) #text $ bSource'] - waitForProgressBegin - waitForAllProgressDone + changeDoc bdoc [TextDocumentContentChangeEvent . InR $ TextDocumentContentChangeWholeDocument bSource'] + _ <- waitForDiagnostics - expectCurrentDiagnostics bdoc [(DiagnosticSeverity_Warning, (4,thDollarIdx), "Top-level binding")] + expectCurrentDiagnostics bdoc [(DiagnosticSeverity_Warning, (4,1), "Top-level binding", Just "GHC-38417")] closeDoc adoc closeDoc bdoc diff --git a/ghcide/test/exe/UnitTests.hs b/ghcide-test/exe/UnitTests.hs similarity index 84% rename from ghcide/test/exe/UnitTests.hs rename to ghcide-test/exe/UnitTests.hs index d76e24372e..b2940ab27f 100644 --- a/ghcide/test/exe/UnitTests.hs +++ b/ghcide-test/exe/UnitTests.hs @@ -1,6 +1,7 @@ module UnitTests (tests) where +import Config (mkIdeTestFs) import Control.Concurrent import Control.Monad.IO.Class (liftIO) import Data.IORef @@ -9,13 +10,11 @@ import Data.List.Extra import Data.String (IsString (fromString)) import qualified Data.Text as T import Development.IDE.Core.FileStore (getModTime) -import qualified Development.IDE.Main as IDE import qualified Development.IDE.Plugin.HLS.GhcIde as Ghcide import qualified Development.IDE.Types.Diagnostics as Diagnostics import Development.IDE.Types.Location import qualified FuzzySearch -import Ide.Logger (Logger, Recorder, - WithPriority, cmapWithPrio) +import Ide.Logger (Recorder, WithPriority) import Ide.PluginUtils (pluginDescToIdePlugins) import Ide.Types import Language.LSP.Protocol.Message @@ -25,19 +24,20 @@ import Language.LSP.Protocol.Types hiding SemanticTokensEdit (..), mkRange) import Language.LSP.Test -import LogType (Log (..)) import Network.URI import qualified Progress import System.IO.Extra hiding (withTempDir) import System.Mem (performGC) +import Test.Hls (IdeState, def, + runSessionWithServerInTmpDir, + waitForProgressDone) import Test.Tasty import Test.Tasty.ExpectedFailure import Test.Tasty.HUnit -import TestUtils import Text.Printf (printf) -tests :: Recorder (WithPriority Log) -> Logger -> TestTree -tests recorder logger = do +tests :: TestTree +tests = do testGroup "Unit" [ testCase "empty file path does NOT work with the empty String literal" $ uriToFilePath' (fromNormalizedUri $ filePathToUri' "") @?= Just "." @@ -51,7 +51,7 @@ tests recorder logger = do let uri = Uri "file://" uriToFilePath' uri @?= Just "" , testCase "showDiagnostics prints ranges 1-based (like vscode)" $ do - let diag = ("", Diagnostics.ShowDiag, Diagnostic + let diag = Diagnostics.FileDiagnostic "" Diagnostics.ShowDiag Diagnostic { _codeDescription = Nothing , _data_ = Nothing , _range = Range @@ -64,14 +64,16 @@ tests recorder logger = do , _message = "" , _relatedInformation = Nothing , _tags = Nothing - }) + } Diagnostics.NoStructuredMessage let shown = T.unpack (Diagnostics.showDiagnostics [diag]) let expected = "1:2-3:4" assertBool (unwords ["expected to find range", expected, "in diagnostic", shown]) $ expected `isInfixOf` shown , testCase "notification handlers run in priority order" $ do orderRef <- newIORef [] - let plugins = pluginDescToIdePlugins $ + let + plugins ::Recorder (WithPriority Ghcide.Log) -> IdePlugins IdeState + plugins recorder = pluginDescToIdePlugins $ [ (priorityPluginDescriptor i) { pluginNotificationHandlers = mconcat [ mkPluginNotificationHandler SMethod_TextDocumentDidOpen $ \_ _ _ _ -> @@ -79,10 +81,10 @@ tests recorder logger = do ] } | i <- [1..20] - ] ++ Ghcide.descriptors (cmapWithPrio LogGhcIde recorder) - priorityPluginDescriptor i = (defaultPluginDescriptor $ fromString $ show i){pluginPriority = i} + ] ++ Ghcide.descriptors recorder + priorityPluginDescriptor i = (defaultPluginDescriptor (fromString $ show i) ""){pluginPriority = i} - testIde recorder (IDE.testing (cmapWithPrio LogIDEMain recorder) logger plugins) $ do + runSessionWithServerInTmpDir def plugins (mkIdeTestFs []) $ do _ <- createDoc "A.hs" "haskell" "module A where" waitForProgressDone actualOrder <- liftIO $ reverse <$> readIORef orderRef diff --git a/ghcide/test/exe/WatchedFileTests.hs b/ghcide-test/exe/WatchedFileTests.hs similarity index 70% rename from ghcide/test/exe/WatchedFileTests.hs rename to ghcide-test/exe/WatchedFileTests.hs index 8d33f4f5cc..1c2ded9109 100644 --- a/ghcide/test/exe/WatchedFileTests.hs +++ b/ghcide-test/exe/WatchedFileTests.hs @@ -1,13 +1,16 @@ -{-# LANGUAGE GADTs #-} -{-# LANGUAGE PolyKinds #-} +{-# LANGUAGE GADTs #-} module WatchedFileTests (tests) where +import Config (mkIdeTestFs, + testWithDummyPlugin', + testWithDummyPluginEmpty') import Control.Applicative.Combinators import Control.Monad.IO.Class (liftIO) import qualified Data.Aeson as A import qualified Data.Text as T +import qualified Data.Text.IO as T import Development.IDE.Test (expectDiagnostics) import Language.LSP.Protocol.Message import Language.LSP.Protocol.Types hiding @@ -18,27 +21,28 @@ import Language.LSP.Protocol.Types hiding import Language.LSP.Test import System.Directory import System.FilePath --- import Test.QuickCheck.Instances () +import Test.Hls.FileSystem import Test.Tasty import Test.Tasty.HUnit -import TestUtils tests :: TestTree tests = testGroup "watched files" [ testGroup "Subscriptions" - [ testSession' "workspace files" $ \sessionDir -> do + [ testWithDummyPluginEmpty' "workspace files" $ \sessionDir -> do liftIO $ writeFile (sessionDir "hie.yaml") "cradle: {direct: {arguments: [\"-isrc\", \"A\", \"WatchedFilesMissingModule\"]}}" _doc <- createDoc "A.hs" "haskell" "{-#LANGUAGE NoImplicitPrelude #-}\nmodule A where\nimport WatchedFilesMissingModule" + setIgnoringRegistrationRequests False watchedFileRegs <- getWatchedFilesSubscriptionsUntil SMethod_TextDocumentPublishDiagnostics -- Expect 2 subscriptions: one for all .hs files and one for the hie.yaml cradle liftIO $ length watchedFileRegs @?= 2 - , testSession' "non workspace file" $ \sessionDir -> do + , testWithDummyPluginEmpty' "non workspace file" $ \sessionDir -> do tmpDir <- liftIO getTemporaryDirectory let yaml = "cradle: {direct: {arguments: [\"-i" <> tail(init(show tmpDir)) <> "\", \"A\", \"WatchedFilesMissingModule\"]}}" liftIO $ writeFile (sessionDir "hie.yaml") yaml _doc <- createDoc "A.hs" "haskell" "{-# LANGUAGE NoImplicitPrelude#-}\nmodule A where\nimport WatchedFilesMissingModule" + setIgnoringRegistrationRequests False watchedFileRegs <- getWatchedFilesSubscriptionsUntil SMethod_TextDocumentPublishDiagnostics -- Expect 2 subscriptions: one for all .hs files and one for the hie.yaml cradle @@ -48,7 +52,7 @@ tests = testGroup "watched files" ] , testGroup "Changes" [ - testSession' "workspace files" $ \sessionDir -> do + testWithDummyPluginEmpty' "workspace files" $ \sessionDir -> do liftIO $ writeFile (sessionDir "hie.yaml") "cradle: {direct: {arguments: [\"-isrc\", \"A\", \"B\"]}}" liftIO $ writeFile (sessionDir "B.hs") $ unlines ["module B where" @@ -60,15 +64,26 @@ tests = testGroup "watched files" ,"a :: ()" ,"a = b" ] - expectDiagnostics [("A.hs", [(DiagnosticSeverity_Error, (3, 4), "Couldn't match expected type '()' with actual type 'Bool'")])] + expectDiagnostics [("A.hs", [(DiagnosticSeverity_Error, (3, 4), "Couldn't match expected type '()' with actual type 'Bool'", Just "GHC-83865")])] -- modify B off editor liftIO $ writeFile (sessionDir "B.hs") $ unlines ["module B where" ,"b :: Int" ,"b = 0"] - sendNotification SMethod_WorkspaceDidChangeWatchedFiles $ DidChangeWatchedFilesParams $ + sendNotification SMethod_WorkspaceDidChangeWatchedFiles $ DidChangeWatchedFilesParams [FileEvent (filePathToUri $ sessionDir "B.hs") FileChangeType_Changed ] - expectDiagnostics [("A.hs", [(DiagnosticSeverity_Error, (3, 4), "Couldn't match expected type '()' with actual type 'Int'")])] + expectDiagnostics [("A.hs", [(DiagnosticSeverity_Error, (3, 4), "Couldn't match expected type '()' with actual type 'Int'", Just "GHC-83865")])] + , testWithDummyPlugin' "reload HLS after .cabal file changes" (mkIdeTestFs [copyDir ("watched-files" "reload")]) $ \sessionDir -> do + let hsFile = "src" "MyLib.hs" + _ <- openDoc hsFile "haskell" + expectDiagnostics [(hsFile, [(DiagnosticSeverity_Error, (2, 7), "Could not load module \8216Data.List.Split\8217", Nothing)])] + let cabalFile = "reload.cabal" + cabalContent <- liftIO $ T.readFile cabalFile + let fix = T.replace "build-depends: base" "build-depends: base, split" + liftIO $ T.writeFile cabalFile (fix cabalContent) + sendNotification SMethod_WorkspaceDidChangeWatchedFiles $ DidChangeWatchedFilesParams + [ FileEvent (filePathToUri $ sessionDir cabalFile) FileChangeType_Changed ] + expectDiagnostics [(hsFile, [])] ] ] diff --git a/ghcide/test/manual/lhs/Bird.lhs b/ghcide-test/manual/lhs/Bird.lhs similarity index 100% rename from ghcide/test/manual/lhs/Bird.lhs rename to ghcide-test/manual/lhs/Bird.lhs diff --git a/ghcide/test/manual/lhs/Main.hs b/ghcide-test/manual/lhs/Main.hs similarity index 100% rename from ghcide/test/manual/lhs/Main.hs rename to ghcide-test/manual/lhs/Main.hs diff --git a/ghcide/test/manual/lhs/Test.lhs b/ghcide-test/manual/lhs/Test.lhs similarity index 100% rename from ghcide/test/manual/lhs/Test.lhs rename to ghcide-test/manual/lhs/Test.lhs diff --git a/ghcide/test/preprocessor/Main.hs b/ghcide-test/preprocessor/Main.hs similarity index 100% rename from ghcide/test/preprocessor/Main.hs rename to ghcide-test/preprocessor/Main.hs diff --git a/ghcide/exe/Arguments.hs b/ghcide/exe/Arguments.hs index f1be07dbcb..627c041970 100644 --- a/ghcide/exe/Arguments.hs +++ b/ghcide/exe/Arguments.hs @@ -20,7 +20,6 @@ data Arguments = Arguments ,argsVerbose :: Bool ,argsCommand :: Command ,argsConservativeChangeTracking :: Bool - ,argsMonitoringPort :: Int } getArguments :: IdePlugins IdeState -> IO Arguments @@ -43,7 +42,6 @@ arguments plugins = Arguments <*> switch (short 'd' <> long "verbose" <> help "Include internal events in logging output") <*> (commandP plugins <|> lspCommand <|> checkCommand) <*> switch (long "conservative-change-tracking" <> help "disable reactive change tracking (for testing/debugging)") - <*> option auto (long "monitoring-port" <> metavar "PORT" <> value 8999 <> showDefault <> help "Port to use for EKG monitoring (if the binary is built with EKG)") where checkCommand = Check <$> many (argument str (metavar "FILES/DIRS...")) lspCommand = LSP <$ flag' True (long "lsp" <> help "Start talking to an LSP client") diff --git a/ghcide/exe/Main.hs b/ghcide/exe/Main.hs index 0c6b1dd0f9..80913da190 100644 --- a/ghcide/exe/Main.hs +++ b/ghcide/exe/Main.hs @@ -1,31 +1,24 @@ -- Copyright (c) 2019 The DAML Authors. All rights reserved. -- SPDX-License-Identifier: Apache-2.0 -{-# OPTIONS_GHC -Wno-dodgy-imports #-} -- GHC no longer exports def in GHC 8.6 and above {-# LANGUAGE TemplateHaskell #-} module Main(main) where import Arguments (Arguments (..), getArguments) -import Control.Monad.Extra (unless) import Control.Monad.IO.Class (liftIO) import Data.Default (def) import Data.Function ((&)) import Data.Version (showVersion) import Development.GitRev (gitHash) -import Development.IDE (action) -import Development.IDE.Core.OfInterest (kick) import Development.IDE.Core.Rules (mainRule) import qualified Development.IDE.Core.Rules as Rules -import Development.IDE.Core.Tracing (withTelemetryLogger) +import Development.IDE.Core.Tracing (withTelemetryRecorder) import qualified Development.IDE.Main as IDEMain -import qualified Development.IDE.Monitoring.EKG as EKG import qualified Development.IDE.Monitoring.OpenTelemetry as OpenTelemetry import qualified Development.IDE.Plugin.HLS.GhcIde as GhcIde import Development.IDE.Types.Options -import GHC.Stack (emptyCallStack) -import Ide.Logger (Logger (Logger), - LoggingColumn (DataColumn, PriorityColumn), +import Ide.Logger (LoggingColumn (..), Pretty (pretty), Priority (Debug, Error, Info), WithPriority (WithPriority, priority), @@ -73,11 +66,11 @@ ghcideVersion = do <> gitHashSection main :: IO () -main = withTelemetryLogger $ \telemetryLogger -> do +main = withTelemetryRecorder $ \telemetryRecorder -> do -- stderr recorder just for plugin cli commands pluginCliRecorder <- cmapWithPrio pretty - <$> makeDefaultStderrRecorder (Just [PriorityColumn, DataColumn]) + <$> makeDefaultStderrRecorder (Just [ThreadIdColumn, PriorityColumn, DataColumn]) let hlsPlugins = pluginDescToIdePlugins (GhcIde.descriptors (cmapWithPrio LogGhcIde pluginCliRecorder)) -- WARNING: If you write to stdout before runLanguageServer @@ -100,7 +93,7 @@ main = withTelemetryLogger $ \telemetryLogger -> do (lspMessageRecorder, cb2) <- Logger.withBacklog Logger.lspClientMessageRecorder -- This plugin just installs a handler for the `initialized` notification, which then -- picks up the LSP environment and feeds it to our recorders - let lspRecorderPlugin = (defaultPluginDescriptor "LSPRecorderCallback") + let lspRecorderPlugin = (defaultPluginDescriptor "LSPRecorderCallback" "Internal plugin") { pluginNotificationHandlers = mkPluginNotificationHandler LSP.SMethod_Initialized $ \_ _ _ _ -> do env <- LSP.getLspEnv liftIO $ (cb1 <> cb2) env @@ -111,32 +104,24 @@ main = withTelemetryLogger $ \telemetryLogger -> do (lspLogRecorder & cmapWithPrio (renderStrict . layoutPretty defaultLayoutOptions) & cfilter (\WithPriority{ priority } -> priority >= minPriority)) <> (lspMessageRecorder & cmapWithPrio (renderStrict . layoutPretty defaultLayoutOptions) - & cfilter (\WithPriority{ priority } -> priority >= Error)) - - -- exists so old-style logging works. intended to be phased out - let logger = Logger $ \p m -> Logger.logger_ docWithFilteredPriorityRecorder (WithPriority p emptyCallStack (pretty m)) + & cfilter (\WithPriority{ priority } -> priority >= Error)) <> + telemetryRecorder let recorder = docWithFilteredPriorityRecorder & cmapWithPrio pretty let arguments = if argsTesting - then IDEMain.testing (cmapWithPrio LogIDEMain recorder) logger hlsPlugins - else IDEMain.defaultArguments (cmapWithPrio LogIDEMain recorder) logger hlsPlugins + then IDEMain.testing (cmapWithPrio LogIDEMain recorder) argsCwd hlsPlugins + else IDEMain.defaultArguments (cmapWithPrio LogIDEMain recorder) argsCwd hlsPlugins IDEMain.defaultMain (cmapWithPrio LogIDEMain recorder) arguments - { IDEMain.argsProjectRoot = Just argsCwd + { IDEMain.argsProjectRoot = argsCwd , IDEMain.argCommand = argsCommand - , IDEMain.argsLogger = IDEMain.argsLogger arguments <> pure telemetryLogger , IDEMain.argsHlsPlugins = IDEMain.argsHlsPlugins arguments <> pluginDescToIdePlugins [lspRecorderPlugin] , IDEMain.argsRules = do - -- install the main and ghcide-plugin rules mainRule (cmapWithPrio LogRules recorder) def - -- install the kick action, which triggers a typecheck on every - -- Shake database restart, i.e. on every user edit. - unless argsDisableKick $ - action kick , IDEMain.argsThreads = case argsThreads of 0 -> Nothing ; i -> Just (fromIntegral i) @@ -149,5 +134,5 @@ main = withTelemetryLogger $ \telemetryLogger -> do , optRunSubset = not argsConservativeChangeTracking , optVerifyCoreFile = argsVerifyCoreFile } - , IDEMain.argsMonitoring = OpenTelemetry.monitoring <> EKG.monitoring logger argsMonitoringPort + , IDEMain.argsMonitoring = OpenTelemetry.monitoring } diff --git a/ghcide/ghcide.cabal b/ghcide/ghcide.cabal index 03cc575c78..416e389f2f 100644 --- a/ghcide/ghcide.cabal +++ b/ghcide/ghcide.cabal @@ -1,8 +1,8 @@ -cabal-version: 3.0 +cabal-version: 3.4 build-type: Simple category: Development name: ghcide -version: 2.4.0.0 +version: 2.11.0.0 license: Apache-2.0 license-file: LICENSE author: Digital Asset and Ghcide contributors @@ -14,39 +14,38 @@ homepage: https://github.com/haskell/haskell-language-server/tree/master/ghcide#readme bug-reports: https://github.com/haskell/haskell-language-server/issues -tested-with: GHC ==9.0.2 || ==9.2.5 +tested-with: GHC == {9.12.2, 9.10.1, 9.8.4, 9.6.7} extra-source-files: CHANGELOG.md README.md - test/data/**/*.cabal - test/data/**/*.hs - test/data/**/*.hs-boot - test/data/**/*.project - test/data/**/*.yaml source-repository head type: git location: https://github.com/haskell/haskell-language-server.git -flag ekg - description: - Enable EKG monitoring of the build graph and other metrics on port 8999 - - default: False - manual: True - flag pedantic description: Enable -Werror default: False manual: True +common warnings + ghc-options: + -Werror=incomplete-patterns + -Wall + -Wincomplete-uni-patterns + -Wunused-packages + -Wno-name-shadowing + -Wno-unticked-promoted-constructors + -fno-ignore-asserts + library - default-language: Haskell2010 + import: warnings + default-language: GHC2021 build-depends: , aeson , array , async - , base >=4 && <5 + , base >=4.16 && <5 , base16-bytestring >=0.1.1 && <1.1 , binary , bytestring @@ -58,7 +57,7 @@ library , deepseq , dependent-map , dependent-sum - , Diff ^>=0.4.0 + , Diff ^>=0.5 || ^>=1.0.0 , directory , dlist , enummapset @@ -67,35 +66,33 @@ library , filepath , fingertree , focus >=1.0.3.2 - , ghc >=9.0 + , ghc >=9.2 , ghc-boot , ghc-boot-th - , ghc-check >=0.5.0.8 - , ghc-paths , ghc-trace-events , Glob , haddock-library >=1.8 && <1.12 , hashable - , hie-bios ==0.12.1 + , hie-bios ^>=0.15.0 , hie-compat ^>=0.3.0.0 - , hiedb >=0.4.4 && <0.4.5 - , hls-graph ==2.4.0.0 - , hls-plugin-api ==2.4.0.0 - , implicit-hie <0.1.3 - , implicit-hie-cradle ^>=0.3.0.5 || ^>=0.5 + , hiedb ^>= 0.7.0.0 + , hls-graph == 2.11.0.0 + , hls-plugin-api == 2.11.0.0 + , implicit-hie >= 0.1.4.0 && < 0.1.5 , lens + , lens-aeson , list-t - , lsp ^>=2.3.0.0 - , lsp-types ^>=2.1.0.0 + , lsp ^>=2.7 + , lsp-types ^>=2.3 , mtl , opentelemetry >=0.6.1 , optparse-applicative + , os-string , parallel , prettyprinter >=1.7 , prettyprinter-ansi-terminal , random , regex-tdfa >=1.3.1.0 - , row-types , safe-exceptions , sorted-list , sqlite-simple @@ -111,10 +108,6 @@ library , unordered-containers >=0.2.10.0 , vector - -- implicit-hie 0.1.3.0 introduced an unexpected behavioral change. - -- https://github.com/Avi-D-coder/implicit-hie/issues/50 - -- to make sure ghcide behaves in a desirable way, we put implicit-hie - -- fake dependency here. if os(windows) build-depends: Win32 @@ -122,24 +115,11 @@ library build-depends: unix default-extensions: - BangPatterns DataKinds - DeriveFoldable - DeriveFunctor - DeriveGeneric - DeriveTraversable - FlexibleContexts - GeneralizedNewtypeDeriving - KindSignatures + ExplicitNamespaces LambdaCase - NamedFieldPuns OverloadedStrings RecordWildCards - ScopedTypeVariables - StandaloneDeriving - TupleSections - TypeApplications - TypeOperators ViewPatterns hs-source-dirs: src session-loader @@ -163,9 +143,13 @@ library Development.IDE.Core.Shake Development.IDE.Core.Tracing Development.IDE.Core.UseStale + Development.IDE.Core.WorkerThread Development.IDE.GHC.Compat Development.IDE.GHC.Compat.Core + Development.IDE.GHC.Compat.CmdLine + Development.IDE.GHC.Compat.Driver Development.IDE.GHC.Compat.Env + Development.IDE.GHC.Compat.Error Development.IDE.GHC.Compat.Iface Development.IDE.GHC.Compat.Logger Development.IDE.GHC.Compat.Outputable @@ -186,17 +170,18 @@ library Development.IDE.LSP.Server Development.IDE.Main Development.IDE.Main.HeapStats - Development.IDE.Monitoring.EKG Development.IDE.Monitoring.OpenTelemetry Development.IDE.Plugin Development.IDE.Plugin.Completions Development.IDE.Plugin.Completions.Types + Development.IDE.Plugin.Completions.Logic Development.IDE.Plugin.HLS Development.IDE.Plugin.HLS.GhcIde Development.IDE.Plugin.Test Development.IDE.Plugin.TypeLenses Development.IDE.Session Development.IDE.Session.Diagnostics + Development.IDE.Session.Implicit Development.IDE.Spans.AtPoint Development.IDE.Spans.Common Development.IDE.Spans.Documentation @@ -217,62 +202,22 @@ library Development.IDE.Core.FileExists Development.IDE.GHC.CPP Development.IDE.GHC.Warnings - Development.IDE.Plugin.Completions.Logic - Development.IDE.Session.VersionCheck Development.IDE.Types.Action - ghc-options: - -Wall -Wincomplete-uni-patterns -Wno-unticked-promoted-constructors - -Wunused-packages -fno-ignore-asserts - if flag(pedantic) - -- We eventually want to build with Werror fully, but we haven't - -- finished purging the warnings, so some are set to not be errors - -- for now ghc-options: - -Werror -Wwarn=unused-packages -Wwarn=unrecognised-pragmas - -Wwarn=dodgy-imports -Wwarn=missing-signatures - -Wwarn=duplicate-exports -Wwarn=dodgy-exports - -Wwarn=incomplete-patterns -Wwarn=overlapping-patterns - -Wwarn=incomplete-record-updates - - -- ambiguous-fields is only understood by GHC >= 9.2, so we only disable it - -- then. The above comment goes for here too -- this should be understood to - -- be temporary until we can remove these warnings. - if (impl(ghc >=9.2) && flag(pedantic)) - ghc-options: -Wwarn=ambiguous-fields - - if flag(ekg) - build-depends: - , ekg-core - , ekg-wai - - cpp-options: -DMONITORING_EKG - -flag test-exe - description: Build the ghcide-test-preprocessor executable - default: True - -executable ghcide-test-preprocessor - default-language: Haskell2010 - hs-source-dirs: test/preprocessor - ghc-options: -Wall -Wno-name-shadowing - main-is: Main.hs - build-depends: base >=4 && <5 - - if !flag(test-exe) - buildable: False + -Werror flag executable description: Build the ghcide executable default: True executable ghcide - default-language: Haskell2010 + import: warnings + default-language: GHC2021 hs-source-dirs: exe - ghc-options: - -threaded -Wall -Wincomplete-uni-patterns -Wno-name-shadowing - -rtsopts "-with-rtsopts=-I0 -A128M -T" + ghc-options: -threaded -rtsopts "-with-rtsopts=-I0 -A128M -T" + -- allow user RTS overrides -- disable idle GC @@ -280,7 +225,7 @@ executable ghcide -- Enable collection of heap statistics main-is: Main.hs build-depends: - , base >=4 && <5 + , base >=4.16 && <5 , data-default , extra , ghcide @@ -296,154 +241,10 @@ executable ghcide autogen-modules: Paths_ghcide default-extensions: - BangPatterns - DeriveFunctor - DeriveGeneric - FlexibleContexts - GeneralizedNewtypeDeriving LambdaCase - NamedFieldPuns OverloadedStrings RecordWildCards - ScopedTypeVariables - StandaloneDeriving - TupleSections - TypeApplications ViewPatterns if !flag(executable) buildable: False - - if flag(ekg) - build-depends: - , ekg-core - , ekg-wai - - cpp-options: -DMONITORING_EKG - - if impl(ghc >=9) - ghc-options: -Wunused-packages - -test-suite ghcide-tests - type: exitcode-stdio-1.0 - default-language: Haskell2010 - build-tool-depends: - , ghcide:ghcide - , ghcide:ghcide-test-preprocessor - , implicit-hie:gen-hie - - build-depends: - , aeson - , async - , base - , containers - , data-default - , directory - , extra - , filepath - , fuzzy - , ghc - , ghcide - , hls-plugin-api - , lens - , list-t - , lsp - , lsp-test ^>=0.16.0.0 - , lsp-types - , monoid-subclasses - , mtl - , network-uri - , QuickCheck - , random - , regex-tdfa ^>=1.3.1 - , row-types - , shake - , sqlite-simple - , stm - , stm-containers - , tasty - , tasty-expected-failure - , tasty-hunit >=0.10 - , tasty-quickcheck - , tasty-rerun - , text - , text-rope - , unordered-containers - - -------------------------------------------------------------- - -- The MIN_VERSION_ghc macro relies on MIN_VERSION pragmas - -- which require depending on ghc. So the tests need to depend - -- on ghc if they need to use MIN_VERSION_ghc. Maybe a - -- better solution can be found, but this is a quick solution - -- which works for now. - -------------------------------------------------------------- - if impl(ghc <9.2) - build-depends: - , record-dot-preprocessor - , record-hasfield - - if impl(ghc <9.3) - build-depends: ghc-typelits-knownnat - - hs-source-dirs: test/cabal test/exe test/src - ghc-options: - -threaded -Wall -Wno-name-shadowing -O0 - -Wno-unticked-promoted-constructors -Wunused-packages - - main-is: Main.hs - other-modules: - AsyncTests - BootTests - ClientSettingsTests - CodeLensTests - CompletionTests - CPPTests - CradleTests - DependentFileTest - Development.IDE.Test - Development.IDE.Test.Diagnostic - Development.IDE.Test.Runfiles - DiagnosticTests - ExceptionTests - FindDefinitionAndHoverTests - FuzzySearch - GarbageCollectionTests - HaddockTests - HieDbRetry - HighlightTests - IfaceTests - InitializeResponseTests - LogType - NonLspCommandLine - OpenCloseTest - OutlineTests - PluginParsedResultTests - PluginSimpleTests - PositionMappingTests - PreprocessorTests - Progress - ReferenceTests - RootUriTests - SafeTests - SymlinkTests - TestUtils - THTests - UnitTests - WatchedFileTests - - -- Tests that have been pulled out of the main file - default-extensions: - BangPatterns - DeriveFunctor - DeriveGeneric - FlexibleContexts - GeneralizedNewtypeDeriving - LambdaCase - NamedFieldPuns - OverloadedStrings - RecordWildCards - ScopedTypeVariables - StandaloneDeriving - TupleSections - TypeApplications - ViewPatterns diff --git a/ghcide/session-loader/Development/IDE/Session.hs b/ghcide/session-loader/Development/IDE/Session.hs index 9ae787a30e..78bfb798af 100644 --- a/ghcide/session-loader/Development/IDE/Session.hs +++ b/ghcide/session-loader/Development/IDE/Session.hs @@ -1,7 +1,5 @@ -{-# LANGUAGE CPP #-} -{-# LANGUAGE ExistentialQuantification #-} -{-# LANGUAGE RankNTypes #-} -{-# LANGUAGE TypeFamilies #-} +{-# LANGUAGE CPP #-} +{-# LANGUAGE TypeFamilies #-} {-| The logic for setting up a ghcide session by tapping into hie-bios. @@ -9,111 +7,125 @@ The logic for setting up a ghcide session by tapping into hie-bios. module Development.IDE.Session (SessionLoadingOptions(..) ,CacheDirs(..) - ,loadSession ,loadSessionWithOptions - ,setInitialDynFlags + ,getInitialGhcLibDirDefault ,getHieDbLoc - ,runWithDb ,retryOnSqliteBusy ,retryOnException ,Log(..) + ,runWithDb ) where -- Unfortunately, we cannot use loadSession with ghc-lib since hie-bios uses -- the real GHC library and the types are incompatible. Furthermore, when -- building with ghc-lib we need to make this Haskell agnostic, so no hie-bios! -import Control.Concurrent.Async import Control.Concurrent.Strict -import Control.Exception.Safe as Safe +import Control.Exception.Safe as Safe import Control.Monad -import Control.Monad.Extra +import Control.Monad.Extra as Extra import Control.Monad.IO.Class -import qualified Crypto.Hash.SHA1 as H -import Data.Aeson hiding (Error) +import qualified Crypto.Hash.SHA1 as H +import Data.Aeson hiding (Error) import Data.Bifunctor -import qualified Data.ByteString.Base16 as B16 -import qualified Data.ByteString.Char8 as B +import qualified Data.ByteString.Base16 as B16 +import qualified Data.ByteString.Char8 as B import Data.Default import Data.Either.Extra import Data.Function -import Data.Hashable hiding (hash) -import qualified Data.HashMap.Strict as HM +import Data.Hashable hiding (hash) +import qualified Data.HashMap.Strict as HM +import Data.IORef import Data.List -import Data.List.NonEmpty (NonEmpty (..)) -import qualified Data.List.NonEmpty as NE -import qualified Data.Map.Strict as Map +import Data.List.Extra as L +import Data.List.NonEmpty (NonEmpty (..)) +import qualified Data.List.NonEmpty as NE +import qualified Data.Map.Strict as Map import Data.Maybe import Data.Proxy -import qualified Data.Text as T +import qualified Data.Text as T import Data.Time.Clock import Data.Version import Development.IDE.Core.RuleTypes -import Development.IDE.Core.Shake hiding (Log, Priority, - knownTargets, withHieDb) -import qualified Development.IDE.GHC.Compat as Compat -import Development.IDE.GHC.Compat.Core hiding (Target, - TargetFile, TargetModule, - Var, Warning, getOptions) -import qualified Development.IDE.GHC.Compat.Core as GHC -import Development.IDE.GHC.Compat.Env hiding (Logger) -import Development.IDE.GHC.Compat.Units (UnitId) +import Development.IDE.Core.Shake hiding (Log, knownTargets, + withHieDb) +import qualified Development.IDE.GHC.Compat as Compat +import Development.IDE.GHC.Compat.CmdLine +import Development.IDE.GHC.Compat.Core hiding (Target, TargetFile, + TargetModule, Var, + Warning, getOptions) +import qualified Development.IDE.GHC.Compat.Core as GHC +import Development.IDE.GHC.Compat.Env hiding (Logger) +import Development.IDE.GHC.Compat.Units (UnitId) import Development.IDE.GHC.Util -import Development.IDE.Graph (Action) -import Development.IDE.Session.VersionCheck +import Development.IDE.Graph (Action) +import qualified Development.IDE.Session.Implicit as GhcIde import Development.IDE.Types.Diagnostics import Development.IDE.Types.Exports -import Development.IDE.Types.HscEnvEq (HscEnvEq, newHscEnvEq, - newHscEnvEqPreserveImportPaths) +import Development.IDE.Types.HscEnvEq (HscEnvEq, newHscEnvEq) import Development.IDE.Types.Location import Development.IDE.Types.Options -import GHC.Check -import qualified HIE.Bios as HieBios -import HIE.Bios.Environment hiding (getCacheDir) -import HIE.Bios.Types hiding (Log) -import qualified HIE.Bios.Types as HieBios -import Hie.Implicit.Cradle (loadImplicitHieCradle) -import Ide.Logger (Pretty (pretty), - Priority (Debug, Error, Info, Warning), - Recorder, WithPriority, - cmapWithPrio, logWith, - nest, - toCologActionWithPrio, - vcat, viaShow, (<+>)) +import GHC.ResponseFile +import qualified HIE.Bios as HieBios +import HIE.Bios.Environment hiding (getCacheDir) +import HIE.Bios.Types hiding (Log) +import qualified HIE.Bios.Types as HieBios +import Ide.Logger (Pretty (pretty), + Priority (Debug, Error, Info, Warning), + Recorder, WithPriority, + cmapWithPrio, logWith, + nest, + toCologActionWithPrio, + vcat, viaShow, (<+>)) +import Ide.Types (SessionLoadingPreferenceConfig (..), + sessionLoading) import Language.LSP.Protocol.Message import Language.LSP.Server import System.Directory -import qualified System.Directory.Extra as IO +import qualified System.Directory.Extra as IO import System.FilePath import System.Info -import Control.Applicative (Alternative ((<|>))) +import Control.Applicative (Alternative ((<|>))) import Data.Void -import Control.Concurrent.STM.Stats (atomically, modifyTVar', - readTVar, writeTVar) +import Control.Concurrent.STM.Stats (atomically, modifyTVar', + readTVar, writeTVar) import Control.Concurrent.STM.TQueue import Control.DeepSeq -import Control.Exception (evaluate) -import Control.Monad.IO.Unlift (MonadUnliftIO) -import Data.Foldable (for_) -import Data.HashMap.Strict (HashMap) -import Data.HashSet (HashSet) -import qualified Data.HashSet as Set +import Control.Exception (evaluate) +import Control.Monad.IO.Unlift (MonadUnliftIO) +import Control.Monad.Trans.Cont (ContT (ContT, runContT)) +import Data.Foldable (for_) +import Data.HashMap.Strict (HashMap) +import Data.HashSet (HashSet) +import qualified Data.HashSet as Set +import qualified Data.Set as OS import Database.SQLite.Simple -import Development.IDE.Core.Tracing (withTrace) -import Development.IDE.Session.Diagnostics (renderCradleError) -import Development.IDE.Types.Shake (WithHieDb) +import Development.IDE.Core.Tracing (withTrace) +import Development.IDE.Core.WorkerThread (awaitRunInThread, + withWorkerQueue) +import qualified Development.IDE.GHC.Compat.Util as Compat +import Development.IDE.Session.Diagnostics (renderCradleError) +import Development.IDE.Types.Shake (WithHieDb, + WithHieDbShield (..), + toNoFileKey) +import GHC.Data.Graph.Directed import HieDb.Create import HieDb.Types -import HieDb.Utils -import qualified System.Random as Random -import System.Random (RandomGen) - --- See Note [Guidelines For Using CPP In GHCIDE Import Statements] - -#if !MIN_VERSION_ghc(9,4,0) -import Data.IORef +import Ide.PluginUtils (toAbsolute) +import qualified System.Random as Random +import System.Random (RandomGen) +import Text.ParserCombinators.ReadP (readP_to_S) + +import GHC.Driver.Env (hsc_all_home_unit_ids) +import GHC.Driver.Errors.Types +import GHC.Types.Error (errMsgDiagnostic, + singleMessage) +import GHC.Unit.State + +#if MIN_VERSION_ghc(9,13,0) +import GHC.Driver.Make (checkHomeUnitsClosed) #endif data Log @@ -130,11 +142,12 @@ data Log | LogDLLLoadError !String | LogCradlePath !FilePath | LogCradleNotFound !FilePath - | LogSessionLoadingResult !(Either [CradleError] (ComponentOptions, FilePath)) + | LogSessionLoadingResult !(Either [CradleError] (ComponentOptions, FilePath, String)) | LogCradle !(Cradle Void) | LogNoneCradleFound FilePath | LogNewComponentCache !(([FileDiagnostic], Maybe HscEnvEq), DependencyInfo) | LogHieBios HieBios.Log + | LogSessionLoadingChanged deriving instance Show Log instance Pretty Log where @@ -205,6 +218,8 @@ instance Pretty Log where LogNewComponentCache componentCache -> "New component cache HscEnvEq:" <+> viaShow componentCache LogHieBios msg -> pretty msg + LogSessionLoadingChanged -> + "Session Loading config changed, reloading the full session." -- | Bump this version number when making changes to the format of the data stored in hiedb hiedbDataVersion :: String @@ -218,20 +233,13 @@ data SessionLoadingOptions = SessionLoadingOptions -- | Load the cradle with an optional 'hie.yaml' location. -- If a 'hie.yaml' is given, use it to load the cradle. -- Otherwise, use the provided project root directory to determine the cradle type. - , loadCradle :: Maybe FilePath -> FilePath -> IO (HieBios.Cradle Void) + , loadCradle :: Recorder (WithPriority Log) -> Maybe FilePath -> FilePath -> IO (HieBios.Cradle Void) -- | Given the project name and a set of command line flags, -- return the path for storing generated GHC artifacts, -- or 'Nothing' to respect the cradle setting , getCacheDirs :: String -> [String] -> IO CacheDirs -- | Return the GHC lib dir to use for the 'unsafeGlobalDynFlags' , getInitialGhcLibDir :: Recorder (WithPriority Log) -> FilePath -> IO (Maybe LibDir) -#if !MIN_VERSION_ghc(9,3,0) - , fakeUid :: UnitId - -- ^ unit id used to tag the internal component built by ghcide - -- To reuse external interface files the unit ids must match, - -- thus make sure to build them with `--this-unit-id` set to the - -- same value as the ghcide fake uid -#endif } instance Default SessionLoadingOptions where @@ -240,9 +248,6 @@ instance Default SessionLoadingOptions where ,loadCradle = loadWithImplicitCradle ,getCacheDirs = getCacheDirsDefault ,getInitialGhcLibDir = getInitialGhcLibDirDefault -#if !MIN_VERSION_ghc(9,3,0) - ,fakeUid = Compat.toUnitId (Compat.stringToUnit "main") -#endif } -- | Find the cradle for a given 'hie.yaml' configuration. @@ -255,22 +260,25 @@ instance Default SessionLoadingOptions where -- using the provided root directory for discovering the project. -- The implicit config uses different heuristics to determine the type -- of the project that may or may not be accurate. -loadWithImplicitCradle :: Maybe FilePath - -- ^ Optional 'hie.yaml' location. Will be used if given. - -> FilePath - -- ^ Root directory of the project. Required as a fallback - -- if no 'hie.yaml' location is given. - -> IO (HieBios.Cradle Void) -loadWithImplicitCradle mHieYaml rootDir = do +loadWithImplicitCradle + :: Recorder (WithPriority Log) + -> Maybe FilePath + -- ^ Optional 'hie.yaml' location. Will be used if given. + -> FilePath + -- ^ Root directory of the project. Required as a fallback + -- if no 'hie.yaml' location is given. + -> IO (HieBios.Cradle Void) +loadWithImplicitCradle recorder mHieYaml rootDir = do + let logger = toCologActionWithPrio (cmapWithPrio LogHieBios recorder) case mHieYaml of - Just yaml -> HieBios.loadCradle yaml - Nothing -> loadImplicitHieCradle $ addTrailingPathSeparator rootDir + Just yaml -> HieBios.loadCradle logger yaml + Nothing -> GhcIde.loadImplicitCradle logger rootDir getInitialGhcLibDirDefault :: Recorder (WithPriority Log) -> FilePath -> IO (Maybe LibDir) getInitialGhcLibDirDefault recorder rootDir = do hieYaml <- findCradle def (rootDir "a") - cradle <- loadCradle def hieYaml rootDir - libDirRes <- getRuntimeGhcLibDir (toCologActionWithPrio (cmapWithPrio LogHieBios recorder)) cradle + cradle <- loadCradle def recorder hieYaml rootDir + libDirRes <- getRuntimeGhcLibDir cradle case libDirRes of CradleSuccess libdir -> pure $ Just $ LibDir libdir CradleFail err -> do @@ -280,15 +288,6 @@ getInitialGhcLibDirDefault recorder rootDir = do logWith recorder Warning LogGetInitialGhcLibDirDefaultCradleNone pure Nothing --- | Sets `unsafeGlobalDynFlags` on using the hie-bios cradle and returns the GHC libdir -setInitialDynFlags :: Recorder (WithPriority Log) -> FilePath -> SessionLoadingOptions -> IO (Maybe LibDir) -setInitialDynFlags recorder rootDir SessionLoadingOptions{..} = do - libdir <- getInitialGhcLibDir recorder rootDir - dynFlags <- mapM dynFlagsForPrinting libdir - logWith recorder Debug LogSettingInitialDynFlags - mapM_ setUnsafeGlobalDynFlags dynFlags - pure libdir - -- | If the action throws exception that satisfies predicate then we sleep for -- a duration determined by the random exponential backoff formula, -- `uniformRandom(0, min (maxDelay, (baseDelay * 2) ^ retryAttempt))`, and try @@ -355,8 +354,10 @@ makeWithHieDbRetryable recorder rng hieDb f = -- writing. Actions are picked off one by one from the `HieWriterChan` and executed in serial -- by a worker thread using a dedicated database connection. -- This is done in order to serialize writes to the database, or else SQLite becomes unhappy -runWithDb :: Recorder (WithPriority Log) -> FilePath -> (WithHieDb -> IndexQueue -> IO ()) -> IO () -runWithDb recorder fp k = do +-- +-- Also see Note [Serializing runs in separate thread] +runWithDb :: Recorder (WithPriority Log) -> FilePath -> ContT () IO (WithHieDbShield, IndexQueue) +runWithDb recorder fp = ContT $ \k -> do -- use non-deterministic seed because maybe multiple HLS start at same time -- and send bursts of requests rng <- Random.newStdGen @@ -374,18 +375,15 @@ runWithDb recorder fp k = do withWriteDbRetryable = makeWithHieDbRetryable recorder rng writedb withWriteDbRetryable initConn - chan <- newTQueueIO - withAsync (writerThread withWriteDbRetryable chan) $ \_ -> do - withHieDb fp (\readDb -> k (makeWithHieDbRetryable recorder rng readDb) chan) + -- Clear the index of any files that might have been deleted since the last run + _ <- withWriteDbRetryable deleteMissingRealFiles + _ <- withWriteDbRetryable garbageCollectTypeNames + + runContT (withWorkerQueue (writer withWriteDbRetryable)) $ \chan -> + withHieDb fp (\readDb -> k (WithHieDbShield $ makeWithHieDbRetryable recorder rng readDb, chan)) where - writerThread :: WithHieDb -> IndexQueue -> IO () - writerThread withHieDbRetryable chan = do - -- Clear the index of any files that might have been deleted since the last run - _ <- withHieDbRetryable deleteMissingRealFiles - _ <- withHieDbRetryable garbageCollectTypeNames - forever $ do - l <- atomically $ readTQueue chan + writer withHieDbRetryable l = do -- TODO: probably should let exceptions be caught/logged/handled by top level handler l withHieDbRetryable `Safe.catch` \e@SQLError{} -> do @@ -415,11 +413,11 @@ getHieDbLoc dir = do -- This is the key function which implements multi-component support. All -- components mapping to the same hie.yaml file are mapped to the same -- HscEnv which is updated as new components are discovered. -loadSession :: Recorder (WithPriority Log) -> FilePath -> IO (Action IdeGhcSession) -loadSession recorder = loadSessionWithOptions recorder def -loadSessionWithOptions :: Recorder (WithPriority Log) -> SessionLoadingOptions -> FilePath -> IO (Action IdeGhcSession) -loadSessionWithOptions recorder SessionLoadingOptions{..} dir = do +loadSessionWithOptions :: Recorder (WithPriority Log) -> SessionLoadingOptions -> FilePath -> TQueue (IO ()) -> IO (Action IdeGhcSession) +loadSessionWithOptions recorder SessionLoadingOptions{..} rootDir que = do + let toAbsolutePath = toAbsolute rootDir -- see Note [Root Directory] + cradle_files <- newIORef [] -- Mapping from hie.yaml file to HscEnv, one per hie.yaml file hscEnvs <- newVar Map.empty :: IO (Var HieMap) -- Mapping from a Filepath to HscEnv @@ -431,6 +429,7 @@ loadSessionWithOptions recorder SessionLoadingOptions{..} dir = do filesMap <- newVar HM.empty :: IO (Var FilesMap) -- Version of the mappings above version <- newVar 0 + biosSessionLoadingVar <- newVar Nothing :: IO (Var (Maybe SessionLoadingPreferenceConfig)) let returnWithVersion fun = IdeGhcSession fun <$> liftIO (readVar version) -- This caches the mapping from Mod.hs -> hie.yaml cradleLoc <- liftIO $ memoIO $ \v -> do @@ -438,19 +437,16 @@ loadSessionWithOptions recorder SessionLoadingOptions{..} dir = do -- Sometimes we get C:, sometimes we get c:, and sometimes we get a relative path -- try and normalise that -- e.g. see https://github.com/haskell/ghcide/issues/126 - res' <- traverse makeAbsolute res + let res' = toAbsolutePath <$> res return $ normalise <$> res' - dummyAs <- async $ return (error "Uninitialised") - runningCradle <- newVar dummyAs :: IO (Var (Async (IdeResult HscEnvEq,[FilePath]))) - return $ do + clientConfig <- getClientConfigAction extras@ShakeExtras{restartShakeSession, ideNc, knownTargetsVar, lspEnv } <- getShakeExtras - let invalidateShakeCache :: IO () - invalidateShakeCache = do + let invalidateShakeCache = do void $ modifyVar' version succ - join $ atomically $ recordDirtyKeys extras GhcSessionIO [emptyFilePath] + return $ toNoFileKey GhcSessionIO IdeOptions{ optTesting = IdeTesting optTesting , optCheckProject = getCheckProject @@ -461,51 +457,45 @@ loadSessionWithOptions recorder SessionLoadingOptions{..} dir = do -- files in the project so that `knownFiles` can learn about them and -- we can generate a complete module graph let extendKnownTargets newTargets = do - knownTargets <- forM newTargets $ \TargetDetails{..} -> + knownTargets <- concatForM newTargets $ \TargetDetails{..} -> case targetTarget of - TargetFile f -> pure (targetTarget, [f]) + TargetFile f -> do + -- If a target file has multiple possible locations, then we + -- assume they are all separate file targets. + -- This happens with '.hs-boot' files if they are in the root directory of the project. + -- GHC reports options such as '-i. A' as 'TargetFile A.hs' instead of 'TargetModule A'. + -- In 'fromTargetId', we dutifully look for '.hs-boot' files and add them to the + -- targetLocations of the TargetDetails. Then we add everything to the 'knownTargetsVar'. + -- However, when we look for a 'Foo.hs-boot' file in 'FindImports.hs', we look for either + -- + -- * TargetFile Foo.hs-boot + -- * TargetModule Foo + -- + -- If we don't generate a TargetFile for each potential location, we will only have + -- 'TargetFile Foo.hs' in the 'knownTargetsVar', thus not find 'TargetFile Foo.hs-boot' + -- and also not find 'TargetModule Foo'. + fs <- filterM (IO.doesFileExist . fromNormalizedFilePath) targetLocations + pure $ map (\fp -> (TargetFile fp, Set.singleton fp)) (nubOrd (f:fs)) TargetModule _ -> do found <- filterM (IO.doesFileExist . fromNormalizedFilePath) targetLocations - return (targetTarget, found) - hasUpdate <- join $ atomically $ do + return [(targetTarget, Set.fromList found)] + hasUpdate <- atomically $ do known <- readTVar knownTargetsVar - let known' = flip mapHashed known $ \k -> - HM.unionWith (<>) k $ HM.fromList $ map (second Set.fromList) knownTargets + let known' = flip mapHashed known $ \k -> unionKnownTargets k (mkKnownTargets knownTargets) hasUpdate = if known /= known' then Just (unhashed known') else Nothing writeTVar knownTargetsVar known' - logDirtyKeys <- recordDirtyKeys extras GetKnownTargets [emptyFilePath] - return (logDirtyKeys >> pure hasUpdate) + pure hasUpdate for_ hasUpdate $ \x -> - logWith recorder Debug $ LogKnownFilesUpdated x + logWith recorder Debug $ LogKnownFilesUpdated (targetMap x) + return $ toNoFileKey GetKnownTargets -- Create a new HscEnv from a hieYaml root and a set of options - -- If the hieYaml file already has an HscEnv, the new component is - -- combined with the components in the old HscEnv into a new HscEnv - -- which contains the union. let packageSetup :: (Maybe FilePath, NormalizedFilePath, ComponentOptions, FilePath) - -> IO (HscEnv, ComponentInfo, [ComponentInfo]) + -> IO ([ComponentInfo], [ComponentInfo]) packageSetup (hieYaml, cfp, opts, libDir) = do -- Parse DynFlags for the newly discovered component hscEnv <- emptyHscEnv ideNc libDir - (df', targets) <- evalGhcEnv hscEnv $ setOptions opts (hsc_dflags hscEnv) - let df = -#if MIN_VERSION_ghc(9,3,0) - case unitIdString (homeUnitId_ df') of - -- cabal uses main for the unit id of all executable packages - -- This makes multi-component sessions confused about what - -- options to use for that component. - -- Solution: hash the options and use that as part of the unit id - -- This works because there won't be any dependencies on the - -- executable unit. - "main" -> - let hash = B.unpack $ B16.encode $ H.finalize $ H.updates H.init (map B.pack $ componentOptions opts) - hashed_uid = Compat.toUnitId (Compat.stringToUnit ("main-"++hash)) - in setHomeUnitId_ hashed_uid df' - _ -> df' -#else - df' -#endif - + newTargetDfs <- evalGhcEnv hscEnv $ setOptions cfp opts (hsc_dflags hscEnv) rootDir let deps = componentDependencies opts ++ maybeToList hieYaml dep_info <- getDependencyInfo deps -- Now lookup to see whether we are combining with an existing HscEnv @@ -520,118 +510,81 @@ loadSessionWithOptions recorder SessionLoadingOptions{..} dir = do -- We will modify the unitId and DynFlags used for -- compilation but these are the true source of -- information. - - new_deps = RawComponentInfo (homeUnitId_ df) df targets cfp opts dep_info - :| maybe [] snd oldDeps + new_deps = fmap (\(df, targets) -> RawComponentInfo (homeUnitId_ df) df targets cfp opts dep_info) newTargetDfs + all_deps = new_deps `NE.appendList` fromMaybe [] oldDeps -- Get all the unit-ids for things in this component - inplace = map rawComponentUnitId $ NE.toList new_deps + _inplace = map rawComponentUnitId $ NE.toList all_deps - new_deps' <- forM new_deps $ \RawComponentInfo{..} -> do - -- Remove all inplace dependencies from package flags for - -- components in this HscEnv -#if MIN_VERSION_ghc(9,3,0) - let (df2, uids) = (rawComponentDynFlags, []) -#else - let (df2, uids) = _removeInplacePackages fakeUid inplace rawComponentDynFlags -#endif + all_deps' <- forM all_deps $ \RawComponentInfo{..} -> do let prefix = show rawComponentUnitId -- See Note [Avoiding bad interface files] - let hscComponents = sort $ map show uids - cacheDirOpts = hscComponents ++ componentOptions opts + let cacheDirOpts = componentOptions opts cacheDirs <- liftIO $ getCacheDirs prefix cacheDirOpts - processed_df <- setCacheDirs recorder cacheDirs df2 + processed_df <- setCacheDirs recorder cacheDirs rawComponentDynFlags -- The final component information, mostly the same but the DynFlags don't -- contain any packages which are also loaded -- into the same component. - pure $ ComponentInfo rawComponentUnitId - processed_df - uids - rawComponentTargets - rawComponentFP - rawComponentCOptions - rawComponentDependencyInfo - -- Make a new HscEnv, we have to recompile everything from - -- scratch again (for now) - -- It's important to keep the same NameCache though for reasons - -- that I do not fully understand - logWith recorder Info $ LogMakingNewHscEnv inplace - hscEnvB <- emptyHscEnv ideNc libDir - !newHscEnv <- - -- Add the options for the current component to the HscEnv - evalGhcEnv hscEnvB $ do - _ <- setSessionDynFlags -#if !MIN_VERSION_ghc(9,3,0) - $ setHomeUnitId_ fakeUid -#endif - df - getSession - - -- Modify the map so the hieYaml now maps to the newly created - -- HscEnv + pure $ ComponentInfo + { componentUnitId = rawComponentUnitId + , componentDynFlags = processed_df + , componentTargets = rawComponentTargets + , componentFP = rawComponentFP + , componentCOptions = rawComponentCOptions + , componentDependencyInfo = rawComponentDependencyInfo + } + -- Modify the map so the hieYaml now maps to the newly updated + -- ComponentInfos -- Returns - -- . the new HscEnv so it can be used to modify the - -- FilePath -> HscEnv map (fileToFlags) -- . The information for the new component which caused this cache miss -- . The modified information (without -inplace flags) for -- existing packages - pure (Map.insert hieYaml (newHscEnv, NE.toList new_deps) m, (newHscEnv, NE.head new_deps', NE.tail new_deps')) + let (new,old) = NE.splitAt (NE.length new_deps) all_deps' + pure (Map.insert hieYaml (NE.toList all_deps) m, (new,old)) let session :: (Maybe FilePath, NormalizedFilePath, ComponentOptions, FilePath) -> IO (IdeResult HscEnvEq,[FilePath]) session args@(hieYaml, _cfp, _opts, _libDir) = do - (hscEnv, new, old_deps) <- packageSetup args - - -- Whenever we spin up a session on Linux, dynamically load libm.so.6 - -- in. We need this in case the binary is statically linked, in which - -- case the interactive session will fail when trying to load - -- ghc-prim, which happens whenever Template Haskell is being - -- evaluated or haskell-language-server's eval plugin tries to run - -- some code. If the binary is dynamically linked, then this will have - -- no effect. - -- See https://github.com/haskell/haskell-language-server/issues/221 - when (os == "linux") $ do - initObjLinker hscEnv - res <- loadDLL hscEnv "libm.so.6" - case res of - Nothing -> pure () - Just err -> logWith recorder Error $ LogDLLLoadError err - - - -- Make a map from unit-id to DynFlags, this is used when trying to - -- resolve imports. (especially PackageImports) - let uids = map (\ci -> (componentUnitId ci, componentDynFlags ci)) (new : old_deps) + (new_deps, old_deps) <- packageSetup args -- For each component, now make a new HscEnvEq which contains the -- HscEnv for the hie.yaml file but the DynFlags for that component - - -- New HscEnv for the component in question, returns the new HscEnvEq and - -- a mapping from FilePath to the newly created HscEnvEq. - let new_cache = newComponentCache recorder optExtensions hieYaml _cfp hscEnv uids - (cs, res) <- new_cache new - -- Modified cache targets for everything else in the hie.yaml file - -- which now uses the same EPS and so on - cached_targets <- concatMapM (fmap fst . new_cache) old_deps - - let all_targets = cs ++ cached_targets - - void $ modifyVar' fileToFlags $ - Map.insert hieYaml (HM.fromList (concatMap toFlagsMap all_targets)) - void $ modifyVar' filesMap $ - flip HM.union (HM.fromList (zip (map fst $ concatMap toFlagsMap all_targets) (repeat hieYaml))) - - void $ extendKnownTargets all_targets - - -- Invalidate all the existing GhcSession build nodes by restarting the Shake session - invalidateShakeCache - + -- For GHC's supporting multi component sessions, we create a shared + -- HscEnv but set the active component accordingly + hscEnv <- emptyHscEnv ideNc _libDir + let new_cache = newComponentCache recorder optExtensions _cfp hscEnv + all_target_details <- new_cache old_deps new_deps + + this_dep_info <- getDependencyInfo $ maybeToList hieYaml + let (all_targets, this_flags_map, this_options) + = case HM.lookup _cfp flags_map' of + Just this -> (all_targets', flags_map', this) + Nothing -> (this_target_details : all_targets', HM.insert _cfp this_flags flags_map', this_flags) + where all_targets' = concat all_target_details + flags_map' = HM.fromList (concatMap toFlagsMap all_targets') + this_target_details = TargetDetails (TargetFile _cfp) this_error_env this_dep_info [_cfp] + this_flags = (this_error_env, this_dep_info) + this_error_env = ([this_error], Nothing) + this_error = ideErrorWithSource (Just "cradle") (Just DiagnosticSeverity_Error) _cfp + (T.unlines + [ "No cradle target found. Is this file listed in the targets of your cradle?" + , "If you are using a .cabal file, please ensure that this module is listed in either the exposed-modules or other-modules section" + ]) + Nothing + + void $ modifyVar' fileToFlags $ Map.insert hieYaml this_flags_map + void $ modifyVar' filesMap $ flip HM.union (HM.fromList (map ((,hieYaml) . fst) $ concatMap toFlagsMap all_targets)) -- The VFS doesn't change on cradle edits, re-use the old one. - restartShakeSession VFSUnmodified "new component" [] + -- Invalidate all the existing GhcSession build nodes by restarting the Shake session + keys2 <- invalidateShakeCache + restartShakeSession VFSUnmodified "new component" [] $ do + keys1 <- extendKnownTargets all_targets + return [keys1, keys2] -- Typecheck all files in the project on startup checkProject <- getCheckProject - unless (null cs || not checkProject) $ do - cfps' <- liftIO $ filterM (IO.doesFileExist . fromNormalizedFilePath) (concatMap targetLocations cs) + unless (null new_deps || not checkProject) $ do + cfps' <- liftIO $ filterM (IO.doesFileExist . fromNormalizedFilePath) (concatMap targetLocations all_targets) void $ shakeEnqueue extras $ mkDelayedAction "InitialLoad" Debug $ void $ do mmt <- uses GetModificationTime cfps' let cs_exist = catMaybes (zipWith (<$) cfps' mmt) @@ -641,30 +594,26 @@ loadSessionWithOptions recorder SessionLoadingOptions{..} dir = do let !exportsMap' = createExportsMap $ mapMaybe (fmap hirModIface) modIfaces liftIO $ atomically $ modifyTVar' (exportsMap shakeExtras) (exportsMap' <>) - return (second Map.keys res) + return $ second Map.keys this_options let consultCradle :: Maybe FilePath -> FilePath -> IO (IdeResult HscEnvEq, [FilePath]) consultCradle hieYaml cfp = do - lfpLog <- flip makeRelative cfp <$> getCurrentDirectory + let lfpLog = makeRelative rootDir cfp logWith recorder Info $ LogCradlePath lfpLog - when (isNothing hieYaml) $ logWith recorder Warning $ LogCradleNotFound lfpLog - - cradle <- loadCradle hieYaml dir - -- TODO: Why are we repeating the same command we have on line 646? - lfp <- flip makeRelative cfp <$> getCurrentDirectory - + cradle <- loadCradle recorder hieYaml rootDir when optTesting $ mRunLspT lspEnv $ sendNotification (SMethod_CustomMethod (Proxy @"ghcide/cradle/loaded")) (toJSON cfp) -- Display a user friendly progress message here: They probably don't know what a cradle is let progMsg = "Setting up " <> T.pack (takeBaseName (cradleRootDir cradle)) - <> " (for " <> T.pack lfp <> ")" - eopts <- mRunLspTCallback lspEnv (withIndefiniteProgress progMsg NotCancellable) $ + <> " (for " <> T.pack lfpLog <> ")" + eopts <- mRunLspTCallback lspEnv (\act -> withIndefiniteProgress progMsg Nothing NotCancellable (const act)) $ withTrace "Load cradle" $ \addTag -> do - addTag "file" lfp - res <- cradleToOptsAndLibDir recorder cradle cfp + addTag "file" lfpLog + old_files <- readIORef cradle_files + res <- cradleToOptsAndLibDir recorder (sessionLoading clientConfig) cradle cfp old_files addTag "result" (show res) return res @@ -672,15 +621,15 @@ loadSessionWithOptions recorder SessionLoadingOptions{..} dir = do case eopts of -- The cradle gave us some options so get to work turning them -- into and HscEnv. - Right (opts, libDir) -> do - installationCheck <- ghcVersionChecker libDir - case installationCheck of - InstallationNotFound{..} -> - error $ "GHC installation not found in libdir: " <> libdir - InstallationMismatch{..} -> - return (([renderPackageSetupException cfp GhcVersionMismatch{..}], Nothing),[]) - InstallationChecked _compileTime _ghcLibCheck -> - session (hieYaml, toNormalizedFilePath' cfp, opts, libDir) + Right (opts, libDir, version) -> do + let compileTime = fullCompilerVersion + case reverse $ readP_to_S parseVersion version of + [] -> error $ "GHC version could not be parsed: " <> version + ((runTime, _):_) + | compileTime == runTime -> do + atomicModifyIORef' cradle_files (\xs -> (cfp:xs,())) + session (hieYaml, toNormalizedFilePath' cfp, opts, libDir) + | otherwise -> return (([renderPackageSetupException cfp GhcVersionMismatch{..}], Nothing),[]) -- Failure case, either a cradle error or the none cradle Left err -> do dep_info <- getDependencyInfo (maybeToList hieYaml) @@ -691,13 +640,40 @@ loadSessionWithOptions recorder SessionLoadingOptions{..} dir = do void $ modifyVar' filesMap $ HM.insert ncfp hieYaml return (res, maybe [] pure hieYaml ++ concatMap cradleErrorDependencies err) + let + -- | We allow users to specify a loading strategy. + -- Check whether this config was changed since the last time we have loaded + -- a session. + -- + -- If the loading configuration changed, we likely should restart the session + -- in its entirety. + didSessionLoadingPreferenceConfigChange :: IO Bool + didSessionLoadingPreferenceConfigChange = do + mLoadingConfig <- readVar biosSessionLoadingVar + case mLoadingConfig of + Nothing -> do + writeVar biosSessionLoadingVar (Just (sessionLoading clientConfig)) + pure False + Just loadingConfig -> do + writeVar biosSessionLoadingVar (Just (sessionLoading clientConfig)) + pure (loadingConfig /= sessionLoading clientConfig) + -- This caches the mapping from hie.yaml + Mod.hs -> [String] -- Returns the Ghc session and the cradle dependencies let sessionOpts :: (Maybe FilePath, FilePath) -> IO (IdeResult HscEnvEq, [FilePath]) sessionOpts (hieYaml, file) = do + Extra.whenM didSessionLoadingPreferenceConfigChange $ do + logWith recorder Info LogSessionLoadingChanged + -- If the dependencies are out of date then clear both caches and start + -- again. + modifyVar_ fileToFlags (const (return Map.empty)) + modifyVar_ filesMap (const (return HM.empty)) + -- Don't even keep the name cache, we start from scratch here! + modifyVar_ hscEnvs (const (return Map.empty)) + v <- Map.findWithDefault HM.empty hieYaml <$> readVar fileToFlags - cfp <- makeAbsolute file + let cfp = toAbsolutePath file case HM.lookup (toNormalizedFilePath' cfp) v of Just (opts, old_di) -> do deps_ok <- checkDependencyInfo old_di @@ -706,8 +682,9 @@ loadSessionWithOptions recorder SessionLoadingOptions{..} dir = do -- If the dependencies are out of date then clear both caches and start -- again. modifyVar_ fileToFlags (const (return Map.empty)) + modifyVar_ filesMap (const (return HM.empty)) -- Keep the same name cache - modifyVar_ hscEnvs (return . Map.adjust (\(h, _) -> (h, [])) hieYaml ) + modifyVar_ hscEnvs (return . Map.adjust (const []) hieYaml ) consultCradle hieYaml cfp else return (opts, Map.keys old_di) Nothing -> consultCradle hieYaml cfp @@ -718,39 +695,43 @@ loadSessionWithOptions recorder SessionLoadingOptions{..} dir = do -- before attempting to do so. let getOptions :: FilePath -> IO (IdeResult HscEnvEq, [FilePath]) getOptions file = do - ncfp <- toNormalizedFilePath' <$> makeAbsolute file + let ncfp = toNormalizedFilePath' (toAbsolutePath file) cachedHieYamlLocation <- HM.lookup ncfp <$> readVar filesMap hieYaml <- cradleLoc file - sessionOpts (join cachedHieYamlLocation <|> hieYaml, file) `Safe.catch` \e -> + let + -- Each one of deps will be registered as a FileSystemWatcher in the GhcSession action + -- so that we can get a workspace/didChangeWatchedFiles notification when a dep changes. + -- The GlobPattern of a FileSystemWatcher can be absolute or relative. + -- We use the absolute one because it is supported by more LSP clients. + -- Here we make sure deps are absolute and later we use those absolute deps as GlobPattern. + absolutePathsCradleDeps (eq, deps) + = (eq, fmap toAbsolutePath deps) + (absolutePathsCradleDeps <$> sessionOpts (join cachedHieYamlLocation <|> hieYaml, file)) `Safe.catch` \e -> return (([renderPackageSetupException file e], Nothing), maybe [] pure hieYaml) returnWithVersion $ \file -> do - opts <- liftIO $ join $ mask_ $ modifyVar runningCradle $ \as -> do - -- If the cradle is not finished, then wait for it to finish. - void $ wait as - asyncRes <- async $ getOptions file - return (asyncRes, wait asyncRes) - pure opts + -- see Note [Serializing runs in separate thread] + awaitRunInThread que $ getOptions file -- | Run the specific cradle on a specific FilePath via hie-bios. -- This then builds dependencies or whatever based on the cradle, gets the -- GHC options/dynflags needed for the session and the GHC library directory -cradleToOptsAndLibDir :: Recorder (WithPriority Log) -> Cradle Void -> FilePath - -> IO (Either [CradleError] (ComponentOptions, FilePath)) -cradleToOptsAndLibDir recorder cradle file = do +cradleToOptsAndLibDir :: Recorder (WithPriority Log) -> SessionLoadingPreferenceConfig -> Cradle Void -> FilePath -> [FilePath] + -> IO (Either [CradleError] (ComponentOptions, FilePath, String)) +cradleToOptsAndLibDir recorder loadConfig cradle file old_fps = do -- let noneCradleFoundMessage :: FilePath -> T.Text -- noneCradleFoundMessage f = T.pack $ "none cradle found for " <> f <> ", ignoring the file" -- Start off by getting the session options logWith recorder Debug $ LogCradle cradle - let logger = toCologActionWithPrio $ cmapWithPrio LogHieBios recorder - cradleRes <- HieBios.getCompilerOptions logger file cradle + cradleRes <- HieBios.getCompilerOptions file loadStyle cradle case cradleRes of CradleSuccess r -> do -- Now get the GHC lib dir - libDirRes <- getRuntimeGhcLibDir logger cradle - case libDirRes of + libDirRes <- getRuntimeGhcLibDir cradle + versionRes <- getRuntimeGhcVersion cradle + case liftA2 (,) libDirRes versionRes of -- This is the successful path - CradleSuccess libDir -> pure (Right (r, libDir)) + (CradleSuccess (libDir, version)) -> pure (Right (r, libDir, version)) CradleFail err -> return (Left [err]) CradleNone -> do logWith recorder Info $ LogNoneCradleFound file @@ -761,13 +742,17 @@ cradleToOptsAndLibDir recorder cradle file = do logWith recorder Info $ LogNoneCradleFound file return (Left []) -#if MIN_VERSION_ghc(9,3,0) + where + loadStyle = case loadConfig of + PreferSingleComponentLoading -> LoadFile + PreferMultiComponentLoading -> LoadWithContext old_fps + emptyHscEnv :: NameCache -> FilePath -> IO HscEnv -#else -emptyHscEnv :: IORef NameCache -> FilePath -> IO HscEnv -#endif emptyHscEnv nc libDir = do - env <- runGhc (Just libDir) getSession + -- We call setSessionDynFlags so that the loader is initialised + -- We need to do this before we call initUnits. + env <- runGhc (Just libDir) $ + getSessionDynFlags >>= setSessionDynFlags >> getSession pure $ setNameCache nc (hscSetFlags ((hsc_dflags env){useUnicode = True }) env) data TargetDetails = TargetDetails @@ -791,72 +776,159 @@ fromTargetId is exts (GHC.TargetModule modName) env dep = do , i <- is , boot <- ["", "-boot"] ] - locs <- mapM (fmap toNormalizedFilePath' . makeAbsolute) fps + let locs = fmap toNormalizedFilePath' fps return [TargetDetails (TargetModule modName) env dep locs] -- For a 'TargetFile' we consider all the possible module names fromTargetId _ _ (GHC.TargetFile f _) env deps = do - nf <- toNormalizedFilePath' <$> makeAbsolute f - return [TargetDetails (TargetFile nf) env deps [nf]] + let nf = toNormalizedFilePath' f + let other + | "-boot" `isSuffixOf` f = toNormalizedFilePath' (L.dropEnd 5 $ fromNormalizedFilePath nf) + | otherwise = toNormalizedFilePath' (fromNormalizedFilePath nf ++ "-boot") + return [TargetDetails (TargetFile nf) env deps [nf, other]] toFlagsMap :: TargetDetails -> [(NormalizedFilePath, (IdeResult HscEnvEq, DependencyInfo))] toFlagsMap TargetDetails{..} = [ (l, (targetEnv, targetDepends)) | l <- targetLocations] -#if MIN_VERSION_ghc(9,3,0) setNameCache :: NameCache -> HscEnv -> HscEnv +setNameCache nc hsc = hsc { hsc_NC = nc } + +#if MIN_VERSION_ghc(9,13,0) +-- Moved back to implementation in GHC. +checkHomeUnitsClosed' :: UnitEnv -> OS.Set UnitId -> [DriverMessages] +checkHomeUnitsClosed' ue _ = checkHomeUnitsClosed ue #else -setNameCache :: IORef NameCache -> HscEnv -> HscEnv +-- This function checks the important property that if both p and q are home units +-- then any dependency of p, which transitively depends on q is also a home unit. +-- GHC had an implementation of this function, but it was horribly inefficient +-- We should move back to the GHC implementation on compilers where +-- https://gitlab.haskell.org/ghc/ghc/-/merge_requests/12162 is included +checkHomeUnitsClosed' :: UnitEnv -> OS.Set UnitId -> Maybe (Compat.MsgEnvelope DriverMessage) +checkHomeUnitsClosed' ue home_id_set + | OS.null bad_unit_ids = Nothing + | otherwise = Just (GHC.mkPlainErrorMsgEnvelope rootLoc $ DriverHomePackagesNotClosed (OS.toList bad_unit_ids)) + where + bad_unit_ids = upwards_closure OS.\\ home_id_set + rootLoc = mkGeneralSrcSpan (Compat.fsLit "") + + graph :: Graph (Node UnitId UnitId) + graph = graphFromEdgedVerticesUniq graphNodes + + -- downwards closure of graph + downwards_closure + = graphFromEdgedVerticesUniq [ DigraphNode uid uid (OS.toList deps) + | (uid, deps) <- Map.toList (allReachable graph node_key)] + + inverse_closure = transposeG downwards_closure + + upwards_closure = OS.fromList $ map node_key $ reachablesG inverse_closure [DigraphNode uid uid [] | uid <- OS.toList home_id_set] + + all_unit_direct_deps :: UniqMap UnitId (OS.Set UnitId) + all_unit_direct_deps + = unitEnv_foldWithKey go emptyUniqMap $ ue_home_unit_graph ue + where + go rest this this_uis = + plusUniqMap_C OS.union + (addToUniqMap_C OS.union external_depends this (OS.fromList this_deps)) + rest + where + external_depends = mapUniqMap (OS.fromList . unitDepends) +#if !MIN_VERSION_ghc(9,7,0) + $ listToUniqMap $ Map.toList +#endif + + $ unitInfoMap this_units + this_units = homeUnitEnv_units this_uis + this_deps = [ Compat.toUnitId unit | (unit,Just _) <- explicitUnits this_units] + + graphNodes :: [Node UnitId UnitId] + graphNodes = go OS.empty home_id_set + where + go done todo + = case OS.minView todo of + Nothing -> [] + Just (uid, todo') + | OS.member uid done -> go done todo' + | otherwise -> case lookupUniqMap all_unit_direct_deps uid of + Nothing -> pprPanic "uid not found" (Compat.ppr (uid, all_unit_direct_deps)) + Just depends -> + let todo'' = (depends OS.\\ done) `OS.union` todo' + in DigraphNode uid uid (OS.toList depends) : go (OS.insert uid done) todo'' #endif -setNameCache nc hsc = hsc { hsc_NC = nc } -- | Create a mapping from FilePaths to HscEnvEqs +-- This combines all the components we know about into +-- an appropriate session, which is a multi component +-- session on GHC 9.4+ newComponentCache :: Recorder (WithPriority Log) - -> [String] -- File extensions to consider - -> Maybe FilePath -- Path to cradle - -> NormalizedFilePath -- Path to file that caused the creation of this component - -> HscEnv - -> [(UnitId, DynFlags)] - -> ComponentInfo - -> IO ( [TargetDetails], (IdeResult HscEnvEq, DependencyInfo)) -newComponentCache recorder exts cradlePath cfp hsc_env uids ci = do - let df = componentDynFlags ci - hscEnv' <- -#if MIN_VERSION_ghc(9,3,0) - -- Set up a multi component session with the other units on GHC 9.4 - Compat.initUnits (map snd uids) (hscSetFlags df hsc_env) -#else - -- This initializes the units for GHC 9.2 - -- Add the options for the current component to the HscEnv - -- We want to call `setSessionDynFlags` instead of `hscSetFlags` - -- because `setSessionDynFlags` also initializes the package database, - -- which we need for any changes to the package flags in the dynflags - -- to be visible. - -- See #2693 - evalGhcEnv hsc_env $ do - _ <- setSessionDynFlags $ df - getSession -#endif - - let newFunc = maybe newHscEnvEqPreserveImportPaths newHscEnvEq cradlePath - henv <- newFunc hscEnv' uids - let targetEnv = ([], Just henv) - targetDepends = componentDependencyInfo ci - res = (targetEnv, targetDepends) - logWith recorder Debug $ LogNewComponentCache res - evaluate $ liftRnf rwhnf $ componentTargets ci - - let mk t = fromTargetId (importPaths df) exts (targetId t) targetEnv targetDepends - ctargets <- concatMapM mk (componentTargets ci) - - -- A special target for the file which caused this wonderful - -- component to be created. In case the cradle doesn't list all the targets for - -- the component, in which case things will be horribly broken anyway. - -- Otherwise, we will immediately attempt to reload this module which - -- causes an infinite loop and high CPU usage. - let special_target = TargetDetails (TargetFile cfp) targetEnv targetDepends [componentFP ci] - return (special_target:ctargets, res) + -> [String] -- ^ File extensions to consider + -> NormalizedFilePath -- ^ Path to file that caused the creation of this component + -> HscEnv -- ^ An empty HscEnv + -> [ComponentInfo] -- ^ New components to be loaded + -> [ComponentInfo] -- ^ old, already existing components + -> IO [ [TargetDetails] ] +newComponentCache recorder exts _cfp hsc_env old_cis new_cis = do + let cis = Map.unionWith unionCIs (mkMap new_cis) (mkMap old_cis) + -- When we have multiple components with the same uid, + -- prefer the new one over the old. + -- However, we might have added some targets to the old unit + -- (see special target), so preserve those + unionCIs new_ci old_ci = new_ci { componentTargets = componentTargets new_ci ++ componentTargets old_ci } + mkMap = Map.fromListWith unionCIs . map (\ci -> (componentUnitId ci, ci)) + let dfs = map componentDynFlags $ Map.elems cis + uids = Map.keys cis + logWith recorder Info $ LogMakingNewHscEnv uids + hscEnv' <- -- Set up a multi component session with the other units on GHC 9.4 + Compat.initUnits dfs hsc_env + + let closure_errs = maybeToList $ checkHomeUnitsClosed' (hsc_unit_env hscEnv') (hsc_all_home_unit_ids hscEnv') + closure_err_to_multi_err err = + ideErrorWithSource + (Just "cradle") (Just DiagnosticSeverity_Warning) _cfp + (T.pack (Compat.printWithoutUniques (singleMessage err))) + (Just (fmap GhcDriverMessage err)) + multi_errs = map closure_err_to_multi_err closure_errs + bad_units = OS.fromList $ concat $ do + x <- map errMsgDiagnostic closure_errs + DriverHomePackagesNotClosed us <- pure x + pure us + isBad ci = (homeUnitId_ (componentDynFlags ci)) `OS.member` bad_units + -- Whenever we spin up a session on Linux, dynamically load libm.so.6 + -- in. We need this in case the binary is statically linked, in which + -- case the interactive session will fail when trying to load + -- ghc-prim, which happens whenever Template Haskell is being + -- evaluated or haskell-language-server's eval plugin tries to run + -- some code. If the binary is dynamically linked, then this will have + -- no effect. + -- See https://github.com/haskell/haskell-language-server/issues/221 + -- We need to do this after the call to setSessionDynFlags initialises + -- the loader + when (os == "linux") $ do + initObjLinker hscEnv' + res <- loadDLL hscEnv' "libm.so.6" + case res of + Nothing -> pure () + Just err -> logWith recorder Error $ LogDLLLoadError err + + forM (Map.elems cis) $ \ci -> do + let df = componentDynFlags ci + thisEnv <- do + -- In GHC 9.4 we have multi component support, and we have initialised all the units + -- above. + -- We just need to set the current unit here + pure $ hscSetActiveUnitId (homeUnitId_ df) hscEnv' + henv <- newHscEnvEq thisEnv + let targetEnv = (if isBad ci then multi_errs else [], Just henv) + targetDepends = componentDependencyInfo ci + logWith recorder Debug $ LogNewComponentCache (targetEnv, targetDepends) + evaluate $ liftRnf rwhnf $ componentTargets ci + + let mk t = fromTargetId (importPaths df) exts (targetId t) targetEnv targetDepends + ctargets <- concatMapM mk (componentTargets ci) + + return (L.nubOrdOn targetTarget ctargets) {- Note [Avoiding bad interface files] ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -923,7 +995,7 @@ setCacheDirs recorder CacheDirs{..} dflags = do -- See Note [Multi Cradle Dependency Info] type DependencyInfo = Map.Map FilePath (Maybe UTCTime) -type HieMap = Map.Map (Maybe FilePath) (HscEnv, [RawComponentInfo]) +type HieMap = Map.Map (Maybe FilePath) [RawComponentInfo] -- | Maps a "hie.yaml" location to all its Target Filepaths and options. type FlagsMap = Map.Map (Maybe FilePath) (HM.HashMap NormalizedFilePath (IdeResult HscEnvEq, DependencyInfo)) -- | Maps a Filepath to its respective "hie.yaml" location. @@ -953,16 +1025,12 @@ data ComponentInfo = ComponentInfo -- | Processed DynFlags. Does not contain inplace packages such as local -- libraries. Can be used to actually load this Component. , componentDynFlags :: DynFlags - -- | Internal units, such as local libraries, that this component - -- is loaded with. These have been extracted from the original - -- ComponentOptions. - , _componentInternalUnits :: [UnitId] -- | All targets of this components. , componentTargets :: [GHC.Target] -- | Filepath which caused the creation of this component , componentFP :: NormalizedFilePath -- | Component Options used to load the component. - , _componentCOptions :: ComponentOptions + , componentCOptions :: ComponentOptions -- | Maps cradle dependencies, such as `stack.yaml`, or `.cabal` file -- to last modification time. See Note [Multi Cradle Dependency Info] , componentDependencyInfo :: DependencyInfo @@ -1029,31 +1097,87 @@ memoIO op = do return (Map.insert k res mp, res) Just res -> return (mp, res) +unit_flags :: [Flag (CmdLineP [String])] +unit_flags = [defFlag "unit" (SepArg addUnit)] + +addUnit :: String -> EwM (CmdLineP [String]) () +addUnit unit_str = liftEwM $ do + units <- getCmdLineState + putCmdLineState (unit_str : units) + -- | Throws if package flags are unsatisfiable -setOptions :: GhcMonad m => ComponentOptions -> DynFlags -> m (DynFlags, [GHC.Target]) -setOptions (ComponentOptions theOpts compRoot _) dflags = do - (dflags', targets') <- addCmdOpts theOpts dflags - let targets = makeTargetsAbsolute compRoot targets' - let dflags'' = - disableWarningsAsErrors $ - -- disabled, generated directly by ghcide instead - flip gopt_unset Opt_WriteInterface $ - -- disabled, generated directly by ghcide instead - -- also, it can confuse the interface stale check - dontWriteHieFiles $ - setIgnoreInterfacePragmas $ - setBytecodeLinkerOptions $ - disableOptimisation $ - Compat.setUpTypedHoles $ - makeDynFlagsAbsolute compRoot dflags' - -- initPackages parses the -package flags and - -- sets up the visibility for each component. - -- Throws if a -package flag cannot be satisfied. - -- This only works for GHC <9.2 - -- For GHC >= 9.2, we need to modify the unit env in the hsc_dflags, which - -- is done later in newComponentCache - final_flags <- liftIO $ wrapPackageSetupException $ Compat.oldInitUnits dflags'' - return (final_flags, targets) +setOptions :: GhcMonad m + => NormalizedFilePath + -> ComponentOptions + -> DynFlags + -> FilePath -- ^ root dir, see Note [Root Directory] + -> m (NonEmpty (DynFlags, [GHC.Target])) +setOptions cfp (ComponentOptions theOpts compRoot _) dflags rootDir = do + ((theOpts',_errs,_warns),units) <- processCmdLineP unit_flags [] (map noLoc theOpts) + case NE.nonEmpty units of + Just us -> initMulti us + Nothing -> do + (df, targets) <- initOne (map unLoc theOpts') + -- A special target for the file which caused this wonderful + -- component to be created. In case the cradle doesn't list all the targets for + -- the component, in which case things will be horribly broken anyway. + -- + -- When we have a singleComponent that is caused to be loaded due to a + -- file, we assume the file is part of that component. This is useful + -- for bare GHC sessions, such as many of the ones used in the testsuite + -- + -- We don't do this when we have multiple components, because each + -- component better list all targets or there will be anarchy. + -- It is difficult to know which component to add our file to in + -- that case. + -- Multi unit arguments are likely to come from cabal, which + -- does list all targets. + -- + -- If we don't end up with a target for the current file in the end, then + -- we will report it as an error for that file + let abs_fp = toAbsolute rootDir (fromNormalizedFilePath cfp) + let special_target = Compat.mkSimpleTarget df abs_fp + pure $ (df, special_target : targets) :| [] + where + initMulti unitArgFiles = + forM unitArgFiles $ \f -> do + args <- liftIO $ expandResponse [f] + initOne args + initOne this_opts = do + (dflags', targets') <- addCmdOpts this_opts dflags + let dflags'' = + case unitIdString (homeUnitId_ dflags') of + -- cabal uses main for the unit id of all executable packages + -- This makes multi-component sessions confused about what + -- options to use for that component. + -- Solution: hash the options and use that as part of the unit id + -- This works because there won't be any dependencies on the + -- executable unit. + "main" -> + let hash = B.unpack $ B16.encode $ H.finalize $ H.updates H.init (map B.pack this_opts) + hashed_uid = Compat.toUnitId (Compat.stringToUnit ("main-"++hash)) + in setHomeUnitId_ hashed_uid dflags' + _ -> dflags' + + let targets = makeTargetsAbsolute root targets' + root = case workingDirectory dflags'' of + Nothing -> compRoot + Just wdir -> compRoot wdir + let dflags''' = + setWorkingDirectory root $ + disableWarningsAsErrors $ + -- disabled, generated directly by ghcide instead + flip gopt_unset Opt_WriteInterface $ + -- disabled, generated directly by ghcide instead + -- also, it can confuse the interface stale check + dontWriteHieFiles $ + setIgnoreInterfacePragmas $ + setBytecodeLinkerOptions $ + disableOptimisation $ + Compat.setUpTypedHoles $ + makeDynFlagsAbsolute compRoot -- makeDynFlagsAbsolute already accounts for workingDirectory + dflags'' + return (dflags''', targets) setIgnoreInterfacePragmas :: DynFlags -> DynFlags setIgnoreInterfacePragmas df = @@ -1095,17 +1219,10 @@ data PackageSetupException { compileTime :: !Version , runTime :: !Version } - | PackageCheckFailed !NotCompatibleReason deriving (Eq, Show, Typeable) instance Exception PackageSetupException --- | Wrap any exception as a 'PackageSetupException' -wrapPackageSetupException :: IO a -> IO a -wrapPackageSetupException = handleAny $ \case - e | Just (pkgE :: PackageSetupException) <- fromException e -> throwIO pkgE - e -> (throwIO . PackageSetupException . show) e - showPackageSetupException :: PackageSetupException -> String showPackageSetupException GhcVersionMismatch{..} = unwords ["ghcide compiled against GHC" @@ -1115,22 +1232,10 @@ showPackageSetupException GhcVersionMismatch{..} = unwords ,"\nThis is unsupported, ghcide must be compiled with the same GHC version as the project." ] showPackageSetupException PackageSetupException{..} = unwords - [ "ghcide compiled by GHC", showVersion compilerVersion + [ "ghcide compiled by GHC", showVersion fullCompilerVersion , "failed to load packages:", message <> "." , "\nPlease ensure that ghcide is compiled with the same GHC installation as the project."] -showPackageSetupException (PackageCheckFailed PackageVersionMismatch{..}) = unwords - ["ghcide compiled with package " - , packageName <> "-" <> showVersion compileTime - ,"but project uses package" - , packageName <> "-" <> showVersion runTime - ,"\nThis is unsupported, ghcide must be compiled with the same GHC installation as the project." - ] -showPackageSetupException (PackageCheckFailed BasePackageAbiMismatch{..}) = unwords - ["ghcide compiled with base-" <> showVersion compileTime <> "-" <> compileTimeAbi - ,"but project uses base-" <> showVersion compileTime <> "-" <> runTimeAbi - ,"\nThis is unsupported, ghcide must be compiled with the same GHC installation as the project." - ] -renderPackageSetupException :: FilePath -> PackageSetupException -> (NormalizedFilePath, ShowDiagnostic, Diagnostic) +renderPackageSetupException :: FilePath -> PackageSetupException -> FileDiagnostic renderPackageSetupException fp e = - ideErrorWithSource (Just "cradle") (Just DiagnosticSeverity_Error) (toNormalizedFilePath' fp) (T.pack $ showPackageSetupException e) + ideErrorWithSource (Just "cradle") (Just DiagnosticSeverity_Error) (toNormalizedFilePath' fp) (T.pack $ showPackageSetupException e) Nothing diff --git a/ghcide/session-loader/Development/IDE/Session/Diagnostics.hs b/ghcide/session-loader/Development/IDE/Session/Diagnostics.hs index 5c46e2f2ae..2890c87966 100644 --- a/ghcide/session-loader/Development/IDE/Session/Diagnostics.hs +++ b/ghcide/session-loader/Development/IDE/Session/Diagnostics.hs @@ -1,8 +1,8 @@ {-# LANGUAGE DeriveAnyClass #-} -{-# LANGUAGE DeriveGeneric #-} module Development.IDE.Session.Diagnostics where import Control.Applicative +import Control.Lens import Control.Monad import qualified Data.Aeson as Aeson import Data.List @@ -28,13 +28,17 @@ data CradleErrorDetails = Depicts the cradle error in a user-friendly way. -} renderCradleError :: CradleError -> Cradle a -> NormalizedFilePath -> FileDiagnostic -renderCradleError (CradleError deps _ec ms) cradle nfp - | HieBios.isCabalCradle cradle = - let (fp, showDiag, diag) = ideErrorWithSource (Just "cradle") (Just DiagnosticSeverity_Error) nfp $ T.unlines $ map T.pack userFriendlyMessage in - (fp, showDiag, diag{_data_ = Just $ Aeson.toJSON CradleErrorDetails{cabalProjectFiles=absDeps}}) - | otherwise = ideErrorWithSource (Just "cradle") (Just DiagnosticSeverity_Error) nfp $ T.unlines $ map T.pack userFriendlyMessage +renderCradleError cradleError cradle nfp = + let noDetails = + ideErrorWithSource (Just "cradle") (Just DiagnosticSeverity_Error) nfp (T.unlines $ map T.pack userFriendlyMessage) Nothing + in + if HieBios.isCabalCradle cradle + then noDetails & fdLspDiagnosticL %~ \diag -> diag{_data_ = Just $ Aeson.toJSON CradleErrorDetails{cabalProjectFiles=absDeps}} + else noDetails where - absDeps = fmap (cradleRootDir cradle ) deps + ms = cradleErrorStderr cradleError + + absDeps = fmap (cradleRootDir cradle ) (cradleErrorDependencies cradleError) userFriendlyMessage :: [String] userFriendlyMessage | HieBios.isCabalCradle cradle = fromMaybe ms $ fileMissingMessage <|> mkUnknownModuleMessage diff --git a/ghcide/session-loader/Development/IDE/Session/Implicit.hs b/ghcide/session-loader/Development/IDE/Session/Implicit.hs new file mode 100644 index 0000000000..c7a6402a9f --- /dev/null +++ b/ghcide/session-loader/Development/IDE/Session/Implicit.hs @@ -0,0 +1,155 @@ +module Development.IDE.Session.Implicit + ( loadImplicitCradle + ) where + + +import Control.Applicative ((<|>)) +import Control.Exception (handleJust) +import Control.Monad +import Control.Monad.IO.Class +import Control.Monad.Trans.Maybe +import Data.Bifunctor +import Data.Functor ((<&>)) +import Data.Maybe +import Data.Void +import System.Directory hiding (findFile) +import System.FilePath +import System.IO.Error + +import Colog.Core (LogAction (..), WithSeverity (..)) +import HIE.Bios.Config +import HIE.Bios.Cradle (defaultCradle, getCradle) +import HIE.Bios.Types hiding (ActionName (..)) + +import Hie.Cabal.Parser +import Hie.Locate +import qualified Hie.Yaml as Implicit + +loadImplicitCradle :: Show a => LogAction IO (WithSeverity Log) -> FilePath -> IO (Cradle a) +loadImplicitCradle l wfile = do + is_dir <- doesDirectoryExist wfile + let wdir | is_dir = wfile + | otherwise = takeDirectory wfile + cfg <- runMaybeT (implicitConfig wdir) + case cfg of + Just bc -> getCradle l absurd bc + Nothing -> return $ defaultCradle l wdir + +-- | Wraps up the cradle inferred by @inferCradleTree@ as a @CradleConfig@ with no dependencies +implicitConfig :: FilePath -> MaybeT IO (CradleConfig a, FilePath) +implicitConfig = (fmap . first) (CradleConfig noDeps) . inferCradleTree + where + noDeps :: [FilePath] + noDeps = [] + + +inferCradleTree :: FilePath -> MaybeT IO (CradleTree a, FilePath) +inferCradleTree start_dir = + maybeItsBios + -- If we have both a config file (cabal.project/stack.yaml) and a work dir + -- (dist-newstyle/.stack-work), prefer that + <|> (cabalExecutable >> cabalConfigDir start_dir >>= \dir -> cabalWorkDir dir >> pure (simpleCabalCradle dir)) + <|> (stackExecutable >> stackConfigDir start_dir >>= \dir -> stackWorkDir dir >> stackCradle dir) + -- If we have a cabal.project OR we have a .cabal and dist-newstyle, prefer cabal + <|> (cabalExecutable >> (cabalConfigDir start_dir <|> cabalFileAndWorkDir) <&> simpleCabalCradle) + -- If we have a stack.yaml, use stack + <|> (stackExecutable >> stackConfigDir start_dir >>= stackCradle) + -- If we have a cabal file, use cabal + <|> (cabalExecutable >> cabalFileDir start_dir <&> simpleCabalCradle) + + where + maybeItsBios = (\wdir -> (Bios (Program $ wdir ".hie-bios") Nothing Nothing, wdir)) <$> biosWorkDir start_dir + + cabalFileAndWorkDir = cabalFileDir start_dir >>= (\dir -> cabalWorkDir dir >> pure dir) + +-- | Generate a stack cradle given a filepath. +-- +-- Since we assume there was proof that this file belongs to a stack cradle +-- we look immediately for the relevant @*.cabal@ and @stack.yaml@ files. +-- We do not look for package.yaml, as we assume the corresponding .cabal has +-- been generated already. +-- +-- We parse the @stack.yaml@ to find relevant @*.cabal@ file locations, then +-- we parse the @*.cabal@ files to generate a mapping from @hs-source-dirs@ to +-- component names. +stackCradle :: FilePath -> MaybeT IO (CradleTree a, FilePath) +stackCradle fp = do + pkgs <- stackYamlPkgs fp + pkgsWithComps <- liftIO $ catMaybes <$> mapM (nestedPkg fp) pkgs + let yaml = fp "stack.yaml" + pure $ (,fp) $ case pkgsWithComps of + [] -> Stack (StackType Nothing (Just yaml)) + ps -> StackMulti mempty $ do + Package n cs <- ps + c <- cs + let (prefix, comp) = Implicit.stackComponent n c + pure (prefix, StackType (Just comp) (Just yaml)) + +-- | By default, we generate a simple cabal cradle which is equivalent to the +-- following hie.yaml: +-- +-- @ +-- cradle: +-- cabal: +-- @ +-- +-- Note, this only works reliable for reasonably modern cabal versions >= 3.2. +simpleCabalCradle :: FilePath -> (CradleTree a, FilePath) +simpleCabalCradle fp = (Cabal $ CabalType Nothing Nothing, fp) + +cabalExecutable :: MaybeT IO FilePath +cabalExecutable = MaybeT $ findExecutable "cabal" + +stackExecutable :: MaybeT IO FilePath +stackExecutable = MaybeT $ findExecutable "stack" + +biosWorkDir :: FilePath -> MaybeT IO FilePath +biosWorkDir = findFileUpwards (".hie-bios" ==) + +cabalWorkDir :: FilePath -> MaybeT IO () +cabalWorkDir wdir = do + check <- liftIO $ doesDirectoryExist (wdir "dist-newstyle") + unless check $ fail "No dist-newstyle" + +stackWorkDir :: FilePath -> MaybeT IO () +stackWorkDir wdir = do + check <- liftIO $ doesDirectoryExist (wdir ".stack-work") + unless check $ fail "No .stack-work" + +cabalConfigDir :: FilePath -> MaybeT IO FilePath +cabalConfigDir = findFileUpwards (\fp -> fp == "cabal.project" || fp == "cabal.project.local") + +cabalFileDir :: FilePath -> MaybeT IO FilePath +cabalFileDir = findFileUpwards (\fp -> takeExtension fp == ".cabal") + +stackConfigDir :: FilePath -> MaybeT IO FilePath +stackConfigDir = findFileUpwards isStack + where + isStack name = name == "stack.yaml" + +-- | Searches upwards for the first directory containing a file to match +-- the predicate. +findFileUpwards :: (FilePath -> Bool) -> FilePath -> MaybeT IO FilePath +findFileUpwards p dir = do + cnts <- + liftIO + $ handleJust + -- Catch permission errors + (\(e :: IOError) -> if isPermissionError e then Just [] else Nothing) + pure + (findFile p dir) + + case cnts of + [] | dir' == dir -> fail "No cabal files" + | otherwise -> findFileUpwards p dir' + _ : _ -> return dir + where dir' = takeDirectory dir + +-- | Sees if any file in the directory matches the predicate +findFile :: (FilePath -> Bool) -> FilePath -> IO [FilePath] +findFile p dir = do + b <- doesDirectoryExist dir + if b then getFiles >>= filterM doesPredFileExist else return [] + where + getFiles = filter p <$> getDirectoryContents dir + doesPredFileExist file = doesFileExist $ dir file diff --git a/ghcide/session-loader/Development/IDE/Session/VersionCheck.hs b/ghcide/session-loader/Development/IDE/Session/VersionCheck.hs deleted file mode 100644 index 80399846c3..0000000000 --- a/ghcide/session-loader/Development/IDE/Session/VersionCheck.hs +++ /dev/null @@ -1,15 +0,0 @@ -{-# LANGUAGE TemplateHaskell #-} - --- | This module exists to circumvent a compile time exception on Windows with --- Stack and GHC 8.10.1. It's just been pulled out from Development.IDE.Session. --- See https://github.com/haskell/ghcide/pull/697 -module Development.IDE.Session.VersionCheck (ghcVersionChecker) where - -import GHC.Check --- Only use this for checking against the compile time GHC libDir! --- Use getRuntimeGhcLibDir from hie-bios instead for everything else --- otherwise binaries will not be distributable since paths will be baked into them -import qualified GHC.Paths - -ghcVersionChecker :: GhcVersionChecker -ghcVersionChecker = $$(makeGhcVersionChecker (return GHC.Paths.libdir)) diff --git a/ghcide/src/Development/IDE.hs b/ghcide/src/Development/IDE.hs index 15cee28f04..8741c98c37 100644 --- a/ghcide/src/Development/IDE.hs +++ b/ghcide/src/Development/IDE.hs @@ -10,7 +10,9 @@ import Development.IDE.Core.Actions as X (getAtPoint, getDefinition, getTypeDefinition) import Development.IDE.Core.FileExists as X (getFileExists) -import Development.IDE.Core.FileStore as X (getFileContents) +import Development.IDE.Core.FileStore as X (getFileContents, + getFileModTimeContents, + getUriContents) import Development.IDE.Core.IdeConfiguration as X (IdeConfiguration (..), isWorkspaceFile) import Development.IDE.Core.OfInterest as X (getFilesOfInterestUntracked) @@ -31,7 +33,7 @@ import Development.IDE.Core.Shake as X (FastResult (..), defineNoDiagnostics, getClientConfig, getPluginConfigAction, - ideLogger, + ideLogger, rootDir, runIdeAction, shakeExtras, use, useNoFile, @@ -50,7 +52,6 @@ import Development.IDE.Graph as X (Action, RuleResult, import Development.IDE.Plugin as X import Development.IDE.Types.Diagnostics as X import Development.IDE.Types.HscEnvEq as X (HscEnvEq (..), - hscEnv, - hscEnvWithImportPaths) + hscEnv) import Development.IDE.Types.Location as X import Ide.Logger as X diff --git a/ghcide/src/Development/IDE/Core/Actions.hs b/ghcide/src/Development/IDE/Core/Actions.hs index 6b9004b0d5..0d55a73120 100644 --- a/ghcide/src/Development/IDE/Core/Actions.hs +++ b/ghcide/src/Development/IDE/Core/Actions.hs @@ -1,9 +1,9 @@ -{-# LANGUAGE RankNTypes #-} {-# LANGUAGE TypeFamilies #-} module Development.IDE.Core.Actions ( getAtPoint , getDefinition , getTypeDefinition +, getImplementationDefinition , highlightAtPoint , refsAtPoint , workspaceSymbols @@ -67,56 +67,68 @@ getAtPoint file pos = runMaybeT $ do !pos' <- MaybeT (return $ fromCurrentPosition mapping pos) MaybeT $ liftIO $ fmap (first (toCurrentRange mapping =<<)) <$> AtPoint.atPoint opts hf dkMap env pos' --- | For each Location, determine if we have the PositionMapping --- for the correct file. If not, get the correct position mapping --- and then apply the position mapping to the location. -toCurrentLocations +-- | Converts locations in the source code to their current positions, +-- taking into account changes that may have occurred due to edits. +toCurrentLocation :: PositionMapping -> NormalizedFilePath - -> [Location] - -> IdeAction [Location] -toCurrentLocations mapping file = mapMaybeM go + -> Location + -> IdeAction (Maybe Location) +toCurrentLocation mapping file (Location uri range) = + -- The Location we are going to might be in a different + -- file than the one we are calling gotoDefinition from. + -- So we check that the location file matches the file + -- we are in. + if nUri == normalizedFilePathToUri file + -- The Location matches the file, so use the PositionMapping + -- we have. + then pure $ Location uri <$> toCurrentRange mapping range + -- The Location does not match the file, so get the correct + -- PositionMapping and use that instead. + else do + otherLocationMapping <- fmap (fmap snd) $ runMaybeT $ do + otherLocationFile <- MaybeT $ pure $ uriToNormalizedFilePath nUri + useWithStaleFastMT GetHieAst otherLocationFile + pure $ Location uri <$> (flip toCurrentRange range =<< otherLocationMapping) where - go :: Location -> IdeAction (Maybe Location) - go (Location uri range) = - -- The Location we are going to might be in a different - -- file than the one we are calling gotoDefinition from. - -- So we check that the location file matches the file - -- we are in. - if nUri == normalizedFilePathToUri file - -- The Location matches the file, so use the PositionMapping - -- we have. - then pure $ Location uri <$> toCurrentRange mapping range - -- The Location does not match the file, so get the correct - -- PositionMapping and use that instead. - else do - otherLocationMapping <- fmap (fmap snd) $ runMaybeT $ do - otherLocationFile <- MaybeT $ pure $ uriToNormalizedFilePath nUri - useWithStaleFastMT GetHieAst otherLocationFile - pure $ Location uri <$> (flip toCurrentRange range =<< otherLocationMapping) - where - nUri :: NormalizedUri - nUri = toNormalizedUri uri + nUri :: NormalizedUri + nUri = toNormalizedUri uri -- | Goto Definition. -getDefinition :: NormalizedFilePath -> Position -> IdeAction (Maybe [Location]) +getDefinition :: NormalizedFilePath -> Position -> IdeAction (Maybe [(Location, Identifier)]) getDefinition file pos = runMaybeT $ do ide@ShakeExtras{ withHieDb, hiedbWriter } <- ask opts <- liftIO $ getIdeOptionsIO ide - (HAR _ hf _ _ _, mapping) <- useWithStaleFastMT GetHieAst file + (hf, mapping) <- useWithStaleFastMT GetHieAst file (ImportMap imports, _) <- useWithStaleFastMT GetImportMap file !pos' <- MaybeT (pure $ fromCurrentPosition mapping pos) - locations <- AtPoint.gotoDefinition withHieDb (lookupMod hiedbWriter) opts imports hf pos' - MaybeT $ Just <$> toCurrentLocations mapping file locations + locationsWithIdentifier <- AtPoint.gotoDefinition withHieDb (lookupMod hiedbWriter) opts imports hf pos' + mapMaybeM (\(location, identifier) -> do + fixedLocation <- MaybeT $ toCurrentLocation mapping file location + pure $ Just (fixedLocation, identifier) + ) locationsWithIdentifier + -getTypeDefinition :: NormalizedFilePath -> Position -> IdeAction (Maybe [Location]) +getTypeDefinition :: NormalizedFilePath -> Position -> IdeAction (Maybe [(Location, Identifier)]) getTypeDefinition file pos = runMaybeT $ do ide@ShakeExtras{ withHieDb, hiedbWriter } <- ask opts <- liftIO $ getIdeOptionsIO ide (hf, mapping) <- useWithStaleFastMT GetHieAst file !pos' <- MaybeT (return $ fromCurrentPosition mapping pos) - locations <- AtPoint.gotoTypeDefinition withHieDb (lookupMod hiedbWriter) opts hf pos' - MaybeT $ Just <$> toCurrentLocations mapping file locations + locationsWithIdentifier <- AtPoint.gotoTypeDefinition withHieDb (lookupMod hiedbWriter) opts hf pos' + mapMaybeM (\(location, identifier) -> do + fixedLocation <- MaybeT $ toCurrentLocation mapping file location + pure $ Just (fixedLocation, identifier) + ) locationsWithIdentifier + +getImplementationDefinition :: NormalizedFilePath -> Position -> IdeAction (Maybe [Location]) +getImplementationDefinition file pos = runMaybeT $ do + ide@ShakeExtras{ withHieDb, hiedbWriter } <- ask + opts <- liftIO $ getIdeOptionsIO ide + (hf, mapping) <- useWithStaleFastMT GetHieAst file + !pos' <- MaybeT (pure $ fromCurrentPosition mapping pos) + locs <- AtPoint.gotoImplementation withHieDb (lookupMod hiedbWriter) opts hf pos' + traverse (MaybeT . toCurrentLocation mapping file) locs highlightAtPoint :: NormalizedFilePath -> Position -> IdeAction (Maybe [DocumentHighlight]) highlightAtPoint file pos = runMaybeT $ do diff --git a/ghcide/src/Development/IDE/Core/Compile.hs b/ghcide/src/Development/IDE/Core/Compile.hs index eba9cd6ec1..552409fbba 100644 --- a/ghcide/src/Development/IDE/Core/Compile.hs +++ b/ghcide/src/Development/IDE/Core/Compile.hs @@ -1,9 +1,8 @@ -- Copyright (c) 2019 The DAML Authors. All rights reserved. -- SPDX-License-Identifier: Apache-2.0 -{-# LANGUAGE CPP #-} -{-# LANGUAGE GADTs #-} -{-# LANGUAGE RankNTypes #-} +{-# LANGUAGE CPP #-} +{-# LANGUAGE GADTs #-} -- | Based on https://ghc.haskell.org/trac/ghc/wiki/Commentary/Compiler/API. -- Given a list of paths to find libraries, and a file to compile, produce a list of 'CoreModule' values. @@ -37,111 +36,119 @@ module Development.IDE.Core.Compile , sourceTypecheck , sourceParser , shareUsages + , setNonHomeFCHook ) where -import Prelude hiding (mod) -import Control.Monad.IO.Class -import Control.Concurrent.Extra -import Control.Concurrent.STM.Stats hiding (orElse) -import Control.DeepSeq (NFData (..), force, - rnf) -import Control.Exception (evaluate) +import Control.Concurrent.STM.Stats hiding (orElse) +import Control.DeepSeq (NFData (..), + force, rnf) +import Control.Exception (evaluate) import Control.Exception.Safe -import Control.Lens hiding (List, (<.>), pre) -import Control.Monad.Except +import Control.Lens hiding (List, pre, + (<.>)) import Control.Monad.Extra +import Control.Monad.IO.Class import Control.Monad.Trans.Except -import qualified Control.Monad.Trans.State.Strict as S -import Data.Aeson (toJSON) -import Data.Bifunctor (first, second) +import qualified Control.Monad.Trans.State.Strict as S +import Data.Aeson (toJSON) +import Data.Bifunctor (first, second) import Data.Binary -import qualified Data.ByteString as BS +import qualified Data.ByteString as BS import Data.Coerce -import qualified Data.DList as DL +import qualified Data.DList as DL import Data.Functor import Data.Generics.Aliases import Data.Generics.Schemes -import qualified Data.HashMap.Strict as HashMap -import Data.IntMap (IntMap) +import qualified Data.HashMap.Strict as HashMap +import Data.IntMap (IntMap) import Data.IORef import Data.List.Extra -import qualified Data.Map.Strict as Map -import Data.Proxy (Proxy(Proxy)) +import qualified Data.Map.Strict as Map import Data.Maybe -import qualified Data.Text as T -import Data.Time (UTCTime (..)) -import Data.Tuple.Extra (dupe) -import Data.Unique as Unique +import Data.Proxy (Proxy (Proxy)) +import qualified Data.Text as T +import Data.Time (UTCTime (..)) +import Data.Tuple.Extra (dupe) import Debug.Trace -import Development.IDE.Core.FileStore (resetInterfaceStore) +import Development.IDE.Core.FileStore (resetInterfaceStore) import Development.IDE.Core.Preprocessor +import Development.IDE.Core.ProgressReporting (progressUpdate) import Development.IDE.Core.RuleTypes import Development.IDE.Core.Shake -import Development.IDE.Core.Tracing (withTrace) -import Development.IDE.GHC.Compat hiding (loadInterface, - parseHeader, parseModule, - tcRnModule, writeHieFile) -import qualified Development.IDE.GHC.Compat as Compat -import qualified Development.IDE.GHC.Compat as GHC -import qualified Development.IDE.GHC.Compat.Util as Util +import Development.IDE.Core.Tracing (withTrace) +import qualified Development.IDE.GHC.Compat as Compat +import qualified Development.IDE.GHC.Compat as GHC +import Development.IDE.GHC.Compat.Driver (hscTypecheckRenameWithDiagnostics) +import qualified Development.IDE.GHC.Compat.Util as Util import Development.IDE.GHC.CoreFile import Development.IDE.GHC.Error -import Development.IDE.GHC.Orphans () +import Development.IDE.GHC.Orphans () import Development.IDE.GHC.Util import Development.IDE.GHC.Warnings +import Development.IDE.Import.DependencyInformation import Development.IDE.Types.Diagnostics import Development.IDE.Types.Location import Development.IDE.Types.Options -import GHC (ForeignHValue, - GetDocsFailure (..), - parsedSource) -import qualified GHC.LanguageExtensions as LangExt +import GHC (ForeignHValue, + GetDocsFailure (..), + ModLocation (..), + parsedSource) +import qualified GHC.LanguageExtensions as LangExt import GHC.Serialized -import HieDb hiding (withHieDb) -import qualified Language.LSP.Server as LSP -import Language.LSP.Protocol.Types (DiagnosticTag (..)) -import qualified Language.LSP.Protocol.Types as LSP -import qualified Language.LSP.Protocol.Message as LSP +import HieDb hiding (withHieDb) +import qualified Language.LSP.Protocol.Message as LSP +import Language.LSP.Protocol.Types (DiagnosticTag (..)) +import qualified Language.LSP.Server as LSP +import Prelude hiding (mod) import System.Directory import System.FilePath -import System.IO.Extra (fixIO, newTempFileWithin) - --- See Note [Guidelines For Using CPP In GHCIDE Import Statements] +import System.IO.Extra (fixIO, + newTempFileWithin) +import qualified Data.Set as Set +import qualified GHC as G +import GHC.Core.Lint.Interactive +import GHC.Driver.Config.CoreToStg.Prep +import qualified GHC.Runtime.Loader as Loader import GHC.Tc.Gen.Splice - - - -import qualified GHC as G - -#if !MIN_VERSION_ghc(9,3,0) -import GHC (ModuleGraph) -#endif - +import GHC.Types.Error import GHC.Types.ForeignStubs import GHC.Types.HpcInfo import GHC.Types.TypeEnv -#if !MIN_VERSION_ghc(9,3,0) -import Data.Map (Map) -import GHC (GhcException (..)) -import Unsafe.Coerce +-- See Note [Guidelines For Using CPP In GHCIDE Import Statements] + +#if MIN_VERSION_ghc(9,7,0) +import Data.Foldable (toList) +import GHC.Unit.Module.Warnings +#else +import Development.IDE.Core.FileStore (shareFilePath) #endif -#if MIN_VERSION_ghc(9,3,0) -import qualified Data.Set as Set +#if MIN_VERSION_ghc(9,10,0) +import Development.IDE.GHC.Compat hiding (assert, + loadInterface, + parseHeader, + parseModule, + tcRnModule, + writeHieFile) +#else +import Development.IDE.GHC.Compat hiding + (loadInterface, + parseHeader, + parseModule, + tcRnModule, + writeHieFile) #endif -#if MIN_VERSION_ghc(9,5,0) -import GHC.Driver.Config.CoreToStg.Prep -import GHC.Core.Lint.Interactive +#if MIN_VERSION_ghc(9,11,0) +import qualified Data.List.NonEmpty as NE +import Data.Time (getCurrentTime) +import GHC.Driver.Env (hsc_all_home_unit_ids) #endif -#if MIN_VERSION_ghc(9,7,0) -import Data.Foldable (toList) -import GHC.Unit.Module.Warnings -#else -import Development.IDE.Core.FileStore (shareFilePath) +#if MIN_VERSION_ghc(9,12,0) +import Development.IDE.Import.FindImports #endif --Simple constants to make sure the source is consistently named @@ -171,13 +178,18 @@ computePackageDeps -> IO (Either [FileDiagnostic] [UnitId]) computePackageDeps env pkg = do case lookupUnit env pkg of - Nothing -> return $ Left [ideErrorText (toNormalizedFilePath' noFilePath) $ - T.pack $ "unknown package: " ++ show pkg] + Nothing -> + return $ Left + [ ideErrorText + (toNormalizedFilePath' noFilePath) + (T.pack $ "unknown package: " ++ show pkg) + ] Just pkgInfo -> return $ Right $ unitDepends pkgInfo -newtype TypecheckHelpers +data TypecheckHelpers = TypecheckHelpers - { getLinkables :: [NormalizedFilePath] -> IO [LinkableResult] -- ^ hls-graph action to get linkables for files + { getLinkables :: [NormalizedFilePath] -> IO [LinkableResult] -- ^ hls-graph action to get linkables for files + , getModuleGraph :: IO DependencyInformation } typecheckModule :: IdeDefer @@ -189,24 +201,28 @@ typecheckModule (IdeDefer defer) hsc tc_helpers pm = do let modSummary = pm_mod_summary pm dflags = ms_hspp_opts modSummary initialized <- catchSrcErrors (hsc_dflags hsc) "typecheck (initialize plugins)" - (initPlugins hsc modSummary) + (Loader.initializePlugins (hscSetFlags (ms_hspp_opts modSummary) hsc)) case initialized of Left errs -> return (errs, Nothing) - Right (modSummary', hscEnv) -> do - (warnings, etcm) <- withWarnings sourceTypecheck $ \tweak -> + Right hscEnv -> do + etcm <- let - session = tweak (hscSetFlags dflags hscEnv) - -- TODO: maybe settings ms_hspp_opts is unnecessary? - mod_summary'' = modSummary' { ms_hspp_opts = hsc_dflags session} + -- TODO: maybe setting ms_hspp_opts is unnecessary? + mod_summary' = modSummary { ms_hspp_opts = hsc_dflags hscEnv} in catchSrcErrors (hsc_dflags hscEnv) sourceTypecheck $ do - tcRnModule session tc_helpers $ demoteIfDefer pm{pm_mod_summary = mod_summary''} - let errorPipeline = unDefer . hideDiag dflags . tagDiag - diags = map errorPipeline warnings - deferredError = any fst diags + tcRnModule hscEnv tc_helpers $ demoteIfDefer pm{pm_mod_summary = mod_summary'} case etcm of - Left errs -> return (map snd diags ++ errs, Nothing) - Right tcm -> return (map snd diags, Just $ tcm{tmrDeferredError = deferredError}) + Left errs -> return (errs, Nothing) + Right tcm -> + let addReason diag = + map (Just (diagnosticReason (errMsgDiagnostic diag)),) $ + diagFromErrMsg sourceTypecheck (hsc_dflags hscEnv) diag + errorPipeline = map (unDefer . hideDiag dflags . tagDiag) . addReason + diags = concatMap errorPipeline $ Compat.getMessages $ tmrWarnings tcm + deferredError = any fst diags + in + return (map snd diags, Just $ tcm{tmrDeferredError = deferredError}) where demoteIfDefer = if defer then demoteTypeErrorsToWarnings else id @@ -234,11 +250,7 @@ captureSplicesAndDeps TypecheckHelpers{..} env k = do -- come from in the IORef,, as these are the modules on whose implementation -- we depend. compile_bco_hook :: IORef (ModuleEnv BS.ByteString) -> HscEnv -> SrcSpan -> CoreExpr -#if MIN_VERSION_ghc(9,3,0) -> IO (ForeignHValue, [Linkable], PkgsLoaded) -#else - -> IO ForeignHValue -#endif compile_bco_hook var hsc_env srcspan ds_expr = do { let dflags = hsc_dflags hsc_env @@ -258,10 +270,8 @@ captureSplicesAndDeps TypecheckHelpers{..} env k = do ; let iNTERACTIVELoc = G.ModLocation{ ml_hs_file = Nothing, ml_hi_file = panic "hscCompileCoreExpr':ml_hi_file", ml_obj_file = panic "hscCompileCoreExpr':ml_obj_file", -#if MIN_VERSION_ghc(9,3,0) ml_dyn_obj_file = panic "hscCompileCoreExpr':ml_dyn_obj_file", ml_dyn_hi_file = panic "hscCompileCoreExpr':ml_dyn_hi_file", -#endif ml_hie_file = panic "hscCompileCoreExpr':ml_hie_file" } ; let ictxt = hsc_IC hsc_env @@ -270,9 +280,7 @@ captureSplicesAndDeps TypecheckHelpers{..} env k = do myCoreToStgExpr (hsc_logger hsc_env) (hsc_dflags hsc_env) ictxt -#if MIN_VERSION_ghc(9,3,0) True -- for bytecode -#endif (icInteractiveModule ictxt) iNTERACTIVELoc prepd_expr @@ -282,6 +290,9 @@ captureSplicesAndDeps TypecheckHelpers{..} env k = do (icInteractiveModule ictxt) stg_expr [] Nothing +#if MIN_VERSION_ghc(9,11,0) + [] -- spt_entries +#endif -- Exclude wired-in names because we may not have read -- their interface files, so getLinkDeps will fail @@ -290,63 +301,41 @@ captureSplicesAndDeps TypecheckHelpers{..} env k = do -- Find the linkables for the modules we need ; let needed_mods = mkUniqSet [ -#if MIN_VERSION_ghc(9,3,0) mod -- We need the whole module for 9.4 because of multiple home units modules may have different unit ids -#else - moduleName mod -- On <= 9.2, just the name is enough because all unit ids will be the same -#endif | n <- concatMap (uniqDSetToList . bcoFreeNames) $ bc_bcos bcos - , Just mod <- [nameModule_maybe n] -- Names from other modules , not (isWiredInName n) -- Exclude wired-in names + , Just mod <- [nameModule_maybe n] -- Names from other modules , moduleUnitId mod `elem` home_unit_ids -- Only care about stuff from the home package set ] home_unit_ids = -#if MIN_VERSION_ghc(9,3,0) map fst (hugElts $ hsc_HUG hsc_env) -#else - [homeUnitId_ dflags] -#endif mods_transitive = getTransitiveMods hsc_env needed_mods -- If we don't support multiple home units, ModuleNames are sufficient because all the units will be the same mods_transitive_list = -#if MIN_VERSION_ghc(9,3,0) mapMaybe nodeKeyToInstalledModule $ Set.toList mods_transitive -#else - -- Non det OK as we will put it into maps later anyway - map (Compat.installedModule (homeUnitId_ dflags)) $ nonDetEltsUniqSet mods_transitive -#endif -#if MIN_VERSION_ghc(9,3,0) - ; moduleLocs <- readIORef (fcModuleCache $ hsc_FC hsc_env) -#else - ; moduleLocs <- readIORef (hsc_FC hsc_env) -#endif - ; lbs <- getLinkables [toNormalizedFilePath' file + ; moduleLocs <- getModuleGraph + ; lbs <- getLinkables [file | installedMod <- mods_transitive_list - , let ifr = fromJust $ lookupInstalledModuleEnv moduleLocs installedMod - file = case ifr of - InstalledFound loc _ -> - fromJust $ ml_hs_file loc - _ -> panic "hscCompileCoreExprHook: module not found" + , let file = fromJust $ lookupModuleFile (installedMod { moduleUnit = RealUnit (Definite $ moduleUnit installedMod) }) moduleLocs ] ; let hsc_env' = loadModulesHome (map linkableHomeMod lbs) hsc_env -#if MIN_VERSION_ghc(9,3,0) {- load it -} - ; (fv_hvs, lbss, pkgs) <- loadDecls (hscInterp hsc_env') hsc_env' srcspan bcos - ; let hval = ((expectJust "hscCompileCoreExpr'" $ lookup (idName binding_id) fv_hvs), lbss, pkgs) +#if MIN_VERSION_ghc(9,11,0) + ; bco_time <- getCurrentTime + ; (fv_hvs, lbss, pkgs) <- loadDecls (hscInterp hsc_env') hsc_env' srcspan $ + Linkable bco_time (icInteractiveModule ictxt) $ NE.singleton $ BCOs bcos #else - {- load it -} - ; fv_hvs <- loadDecls (hscInterp hsc_env') hsc_env' srcspan bcos - ; let hval = (expectJust "hscCompileCoreExpr'" $ lookup (idName binding_id) fv_hvs) + ; (fv_hvs, lbss, pkgs) <- loadDecls (hscInterp hsc_env') hsc_env' srcspan bcos #endif + ; let hval = (expectJust "hscCompileCoreExpr'" $ lookup (idName binding_id) fv_hvs, lbss, pkgs) ; modifyIORef' var (flip extendModuleEnvList [(mi_module $ hm_iface hm, linkableHash lb) | lb <- lbs, let hm = linkableHomeMod lb]) ; return hval } -#if MIN_VERSION_ghc(9,3,0) -- TODO: support backpack nodeKeyToInstalledModule :: NodeKey -> Maybe InstalledModule -- We shouldn't get boot files here, but to be safe, never map them to an installed module @@ -357,28 +346,13 @@ captureSplicesAndDeps TypecheckHelpers{..} env k = do nodeKeyToInstalledModule _ = Nothing moduleToNodeKey :: Module -> NodeKey moduleToNodeKey mod = NodeKey_Module $ ModNodeKeyWithUid (GWIB (moduleName mod) NotBoot) (moduleUnitId mod) -#endif -- Compute the transitive set of linkables required getTransitiveMods hsc_env needed_mods -#if MIN_VERSION_ghc(9,3,0) = Set.unions (Set.fromList (map moduleToNodeKey mods) : [ dep | m <- mods , Just dep <- [Map.lookup (moduleToNodeKey m) (mgTransDeps (hsc_mod_graph hsc_env))] ]) where mods = nonDetEltsUniqSet needed_mods -- OK because we put them into a set immediately after -#else - = go emptyUniqSet needed_mods - where - hpt = hsc_HPT hsc_env - go seen new - | isEmptyUniqSet new = seen - | otherwise = go seen' new' - where - seen' = seen `unionUniqSets` new - new' = new_deps `minusUniqSet` seen' - new_deps = unionManyUniqSets [ mkUniqSet $ getDependentMods $ hm_iface mod_info - | mod_info <- eltsUDFM $ udfmIntersectUFM hpt (getUniqSet new)] -#endif -- | Add a Hook to the DynFlags which captures and returns the -- typechecked splices before they are run. This information @@ -419,12 +393,12 @@ tcRnModule hsc_env tc_helpers pmod = do let ms = pm_mod_summary pmod hsc_env_tmp = hscSetFlags (ms_hspp_opts ms) hsc_env - ((tc_gbl_env', mrn_info), splices, mod_env) + (((tc_gbl_env', mrn_info), warning_messages), splices, mod_env) <- captureSplicesAndDeps tc_helpers hsc_env_tmp $ \hscEnvTmp -> - do hscTypecheckRename hscEnvTmp ms $ - HsParsedModule { hpm_module = parsedSource pmod, - hpm_src_files = pm_extra_src_files pmod, - hpm_annotations = pm_annotations pmod } + do hscTypecheckRenameWithDiagnostics hscEnvTmp ms $ + HsParsedModule { hpm_module = parsedSource pmod + , hpm_src_files = pm_extra_src_files pmod + } let rn_info = case mrn_info of Just x -> x Nothing -> error "no renamed info tcRnModule" @@ -433,10 +407,11 @@ tcRnModule hsc_env tc_helpers pmod = do mod_env_anns = map (\(mod, hash) -> Annotation (ModuleTarget mod) $ toSerialized BS.unpack hash) (moduleEnvToList mod_env) tc_gbl_env = tc_gbl_env' { tcg_ann_env = extendAnnEnvList (tcg_ann_env tc_gbl_env') mod_env_anns } - pure (TcModuleResult pmod rn_info tc_gbl_env splices False mod_env) + pure (TcModuleResult pmod rn_info tc_gbl_env splices False mod_env warning_messages) -- Note [Clearing mi_globals after generating an iface] +-- ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -- GHC populates the mi_global field in interfaces for GHCi if we are using the bytecode -- interpreter. -- However, this field is expensive in terms of heap usage, and we don't use it in HLS @@ -452,12 +427,8 @@ tcRnModule hsc_env tc_helpers pmod = do -- HLS. To avoid GHC from pessimising HLS, we filter out certain dependency information -- that we track ourselves. See also Note [Recompilation avoidance in the presence of TH] filterUsages :: [Usage] -> [Usage] -#if MIN_VERSION_ghc(9,3,0) filterUsages = filter $ \case UsageHomeModuleInterface{} -> False _ -> True -#else -filterUsages = id -#endif -- | Mitigation for https://gitlab.haskell.org/ghc/ghc/-/issues/22744 -- Important to do this immediately after reading the unit before @@ -482,12 +453,15 @@ mkHiFileResultNoCompile session tcm = do tcGblEnv = tmrTypechecked tcm details <- makeSimpleDetails hsc_env_tmp tcGblEnv sf <- finalSafeMode (ms_hspp_opts ms) tcGblEnv - iface' <- mkIfaceTc hsc_env_tmp sf details ms -#if MIN_VERSION_ghc(9,5,0) - Nothing + iface' <- mkIfaceTc hsc_env_tmp sf details ms Nothing tcGblEnv + -- See Note [Clearing mi_globals after generating an iface] + let iface = iface' +#if MIN_VERSION_ghc(9,11,0) + & set_mi_top_env Nothing + & set_mi_usages (filterUsages (mi_usages iface')) +#else + { mi_globals = Nothing, mi_usages = filterUsages (mi_usages iface') } #endif - tcGblEnv - let iface = iface' { mi_globals = Nothing, mi_usages = filterUsages (mi_usages iface') } -- See Note [Clearing mi_globals after generating an iface] pure $! mkHiFileResult ms iface details (tmrRuntimeModules tcm) Nothing mkHiFileResultCompile @@ -508,20 +482,27 @@ mkHiFileResultCompile se session' tcm simplified_guts = catchErrs $ do pure (details, guts) let !partial_iface = force $ mkPartialIface session -#if MIN_VERSION_ghc(9,5,0) (cg_binds guts) -#endif details -#if MIN_VERSION_ghc(9,3,0) ms +#if MIN_VERSION_ghc(9,11,0) + (tcg_import_decls (tmrTypechecked tcm)) #endif simplified_guts final_iface' <- mkFullIface session partial_iface Nothing -#if MIN_VERSION_ghc(9,4,2) Nothing +#if MIN_VERSION_ghc(9,11,0) + NoStubs [] +#endif + -- See Note [Clearing mi_globals after generating an iface] + let final_iface = final_iface' +#if MIN_VERSION_ghc(9,11,0) + & set_mi_top_env Nothing + & set_mi_usages (filterUsages (mi_usages final_iface')) +#else + {mi_globals = Nothing, mi_usages = filterUsages (mi_usages final_iface')} #endif - let final_iface = final_iface' {mi_globals = Nothing, mi_usages = filterUsages (mi_usages final_iface')} -- See Note [Clearing mi_globals after generating an iface] -- Write the core file now core_file <- do @@ -529,7 +510,7 @@ mkHiFileResultCompile se session' tcm simplified_guts = catchErrs $ do core_file = codeGutsToCoreFile iface_hash guts iface_hash = getModuleHash final_iface core_hash1 <- atomicFileWrite se core_fp $ \fp -> - writeBinCoreFile fp core_file + writeBinCoreFile (hsc_dflags session) fp core_file -- We want to drop references to guts and read in a serialized, compact version -- of the core file from disk (as it is deserialised lazily) -- This is because we don't want to keep the guts in memory for every file in @@ -551,32 +532,16 @@ mkHiFileResultCompile se session' tcm simplified_guts = catchErrs $ do mod = ms_mod ms data_tycons = filter isDataTyCon tycons CgGuts{cg_binds = unprep_binds'} <- coreFileToCgGuts session final_iface details core - -#if MIN_VERSION_ghc(9,5,0) cp_cfg <- initCorePrepConfig session -#endif - let corePrep = corePrepPgm -#if MIN_VERSION_ghc(9,5,0) (hsc_logger session) cp_cfg (initCorePrepPgmConfig (hsc_dflags session) (interactiveInScope $ hsc_IC session)) -#else - session -#endif mod (ms_location ms) -- Run corePrep first as we want to test the final version of the program that will -- get translated to STG/Bytecode -#if MIN_VERSION_ghc(9,3,0) prepd_binds -#else - (prepd_binds , _) -#endif <- corePrep unprep_binds data_tycons -#if MIN_VERSION_ghc(9,3,0) prepd_binds' -#else - (prepd_binds', _) -#endif <- corePrep unprep_binds' data_tycons let binds = noUnfoldings $ (map flattenBinds . (:[])) prepd_binds binds' = noUnfoldings $ (map flattenBinds . (:[])) prepd_binds' @@ -595,7 +560,7 @@ mkHiFileResultCompile se session' tcm simplified_guts = catchErrs $ do -- SYB is slow but fine given that this is only used for testing noUnfoldings = everywhere $ mkT $ \v -> if isId v then - let v' = if isOtherUnfolding (realIdUnfolding v) then (setIdUnfolding v noUnfolding) else v + let v' = if isOtherUnfolding (realIdUnfolding v) then setIdUnfolding v noUnfolding else v in setIdOccInfo v' noOccInfo else v isOtherUnfolding (OtherCon _) = True @@ -613,8 +578,14 @@ mkHiFileResultCompile se session' tcm simplified_guts = catchErrs $ do source = "compile" catchErrs x = x `catches` [ Handler $ return . (,Nothing) . diagFromGhcException source dflags - , Handler $ return . (,Nothing) . diagFromString source DiagnosticSeverity_Error (noSpan "") - . (("Error during " ++ T.unpack source) ++) . show @SomeException + , Handler $ \diag -> + return + ( diagFromString + source DiagnosticSeverity_Error (noSpan "") + ("Error during " ++ T.unpack source ++ show @SomeException diag) + Nothing + , Nothing + ) ] -- | Whether we should run the -O0 simplifier when generating core. @@ -673,33 +644,23 @@ generateObjectCode session summary guts = do let env' = tweak (hscSetFlags (ms_hspp_opts summary) session) target = platformDefaultBackend (hsc_dflags env') newFlags = setBackend target $ updOptLevel 0 $ setOutputFile -#if MIN_VERSION_ghc(9,3,0) (Just dot_o) -#else - dot_o -#endif $ hsc_dflags env' session' = hscSetFlags newFlags session -#if MIN_VERSION_ghc(9,4,2) (outputFilename, _mStub, _foreign_files, _cinfos, _stgcinfos) <- hscGenHardCode session' guts -#else - (outputFilename, _mStub, _foreign_files, _cinfos) <- hscGenHardCode session' guts -#endif (ms_location summary) fp obj <- compileFile session' driverNoStop (outputFilename, Just (As False)) -#if MIN_VERSION_ghc(9,3,0) case obj of Nothing -> throwGhcExceptionIO $ Panic "compileFile didn't generate object code" Just x -> pure x -#else - return obj -#endif - let unlinked = DotO dot_o_fp -- Need time to be the modification time for recompilation checking t <- liftIO $ getModificationTime dot_o_fp - let linkable = LM t mod [unlinked] - +#if MIN_VERSION_ghc(9,11,0) + let linkable = Linkable t mod (pure $ DotO dot_o_fp ModuleObject) +#else + let linkable = LM t mod [DotO dot_o_fp] +#endif pure (map snd warnings, linkable) newtype CoreFileTime = CoreFileTime UTCTime @@ -708,6 +669,16 @@ generateByteCode :: CoreFileTime -> HscEnv -> ModSummary -> CgGuts -> IO (IdeRes generateByteCode (CoreFileTime time) hscEnv summary guts = do fmap (either (, Nothing) (second Just)) $ catchSrcErrors (hsc_dflags hscEnv) "bytecode" $ do + +#if MIN_VERSION_ghc(9,11,0) + (warnings, (_, bytecode)) <- + withWarnings "bytecode" $ \_tweak -> do + let session = _tweak (hscSetFlags (ms_hspp_opts summary) hscEnv) + -- TODO: maybe settings ms_hspp_opts is unnecessary? + summary' = summary { ms_hspp_opts = hsc_dflags session } + hscInteractive session (mkCgInteractiveGuts guts) + (ms_location summary') +#else (warnings, (_, bytecode, sptEntries)) <- withWarnings "bytecode" $ \_tweak -> do let session = _tweak (hscSetFlags (ms_hspp_opts summary) hscEnv) @@ -715,8 +686,14 @@ generateByteCode (CoreFileTime time) hscEnv summary guts = do summary' = summary { ms_hspp_opts = hsc_dflags session } hscInteractive session (mkCgInteractiveGuts guts) (ms_location summary') - let unlinked = BCOs bytecode sptEntries - let linkable = LM time (ms_mod summary) [unlinked] +#endif + +#if MIN_VERSION_ghc(9,11,0) + let linkable = Linkable time (ms_mod summary) (pure $ BCOs bytecode) +#else + let linkable = LM time (ms_mod summary) [BCOs bytecode sptEntries] +#endif + pure (map snd warnings, linkable) demoteTypeErrorsToWarnings :: ParsedModule -> ParsedModule @@ -739,34 +716,23 @@ update_pm_mod_summary :: (ModSummary -> ModSummary) -> ParsedModule -> ParsedMod update_pm_mod_summary up pm = pm{pm_mod_summary = up $ pm_mod_summary pm} -#if MIN_VERSION_ghc(9,3,0) unDefer :: (Maybe DiagnosticReason, FileDiagnostic) -> (Bool, FileDiagnostic) unDefer (Just (WarningWithFlag Opt_WarnDeferredTypeErrors) , fd) = (True, upgradeWarningToError fd) unDefer (Just (WarningWithFlag Opt_WarnTypedHoles) , fd) = (True, upgradeWarningToError fd) unDefer (Just (WarningWithFlag Opt_WarnDeferredOutOfScopeVariables), fd) = (True, upgradeWarningToError fd) -#else -unDefer :: (WarnReason, FileDiagnostic) -> (Bool, FileDiagnostic) -unDefer (Reason Opt_WarnDeferredTypeErrors , fd) = (True, upgradeWarningToError fd) -unDefer (Reason Opt_WarnTypedHoles , fd) = (True, upgradeWarningToError fd) -unDefer (Reason Opt_WarnDeferredOutOfScopeVariables, fd) = (True, upgradeWarningToError fd) -#endif unDefer ( _ , fd) = (False, fd) upgradeWarningToError :: FileDiagnostic -> FileDiagnostic -upgradeWarningToError (nfp, sh, fd) = - (nfp, sh, fd{_severity = Just DiagnosticSeverity_Error, _message = warn2err $ _message fd}) where +upgradeWarningToError = + fdLspDiagnosticL %~ \diag -> diag {_severity = Just DiagnosticSeverity_Error, _message = warn2err $ _message diag} + where warn2err :: T.Text -> T.Text warn2err = T.intercalate ": error:" . T.splitOn ": warning:" -#if MIN_VERSION_ghc(9,3,0) hideDiag :: DynFlags -> (Maybe DiagnosticReason, FileDiagnostic) -> (Maybe DiagnosticReason, FileDiagnostic) -hideDiag originalFlags (w@(Just (WarningWithFlag warning)), (nfp, _sh, fd)) -#else -hideDiag :: DynFlags -> (WarnReason, FileDiagnostic) -> (WarnReason, FileDiagnostic) -hideDiag originalFlags (w@(Reason warning), (nfp, _sh, fd)) -#endif +hideDiag originalFlags (w@(Just (WarningWithFlag warning)), fd) | not (wopt warning originalFlags) - = (w, (nfp, HideDiag, fd)) + = (w, fd { fdShouldShowDiagnostic = HideDiag }) hideDiag _originalFlags t = t -- | Warnings which lead to a diagnostic tag @@ -787,29 +753,21 @@ unnecessaryDeprecationWarningFlags ] -- | Add a unnecessary/deprecated tag to the required diagnostics. -#if MIN_VERSION_ghc(9,3,0) tagDiag :: (Maybe DiagnosticReason, FileDiagnostic) -> (Maybe DiagnosticReason, FileDiagnostic) -#else -tagDiag :: (WarnReason, FileDiagnostic) -> (WarnReason, FileDiagnostic) -#endif #if MIN_VERSION_ghc(9,7,0) -tagDiag (w@(Just (WarningWithCategory cat)), (nfp, sh, fd)) +tagDiag (w@(Just (WarningWithCategory cat)), fd) | cat == defaultWarningCategory -- default warning category is for deprecations - = (w, (nfp, sh, fd { _tags = Just $ DiagnosticTag_Deprecated : concat (_tags fd) })) -tagDiag (w@(Just (WarningWithFlags warnings)), (nfp, sh, fd)) + = (w, fd & fdLspDiagnosticL %~ \diag -> diag { _tags = Just $ DiagnosticTag_Deprecated : concat (_tags diag) }) +tagDiag (w@(Just (WarningWithFlags warnings)), fd) | tags <- mapMaybe requiresTag (toList warnings) - = (w, (nfp, sh, fd { _tags = Just $ tags ++ concat (_tags fd) })) -#elif MIN_VERSION_ghc(9,3,0) -tagDiag (w@(Just (WarningWithFlag warning)), (nfp, sh, fd)) - | Just tag <- requiresTag warning - = (w, (nfp, sh, fd { _tags = Just $ tag : concat (_tags fd) })) + = (w, fd & fdLspDiagnosticL %~ \diag -> diag { _tags = Just $ tags ++ concat (_tags diag) }) #else -tagDiag (w@(Reason warning), (nfp, sh, fd)) +tagDiag (w@(Just (WarningWithFlag warning)), fd) | Just tag <- requiresTag warning - = (w, (nfp, sh, fd { _tags = Just $ tag : concat (_tags fd) })) + = (w, fd & fdLspDiagnosticL %~ \diag -> diag { _tags = Just $ tag : concat (_tags diag) }) #endif - where + where requiresTag :: WarningFlag -> Maybe DiagnosticTag #if !MIN_VERSION_ghc(9,7,0) -- doesn't exist on 9.8, we use WarningWithCategory instead @@ -836,33 +794,43 @@ atomicFileWrite se targetPath write = do (write tempFilePath >>= \x -> renameFile tempFilePath targetPath >> atomically (resetInterfaceStore se (toNormalizedFilePath' targetPath)) >> pure x) `onException` cleanUp -generateHieAsts :: HscEnv -> TcModuleResult -> IO ([FileDiagnostic], Maybe (HieASTs Type)) +generateHieAsts :: HscEnv -> TcModuleResult +#if MIN_VERSION_ghc(9,11,0) + -> IO ([FileDiagnostic], Maybe (HieASTs Type, NameEntityInfo)) +#else + -> IO ([FileDiagnostic], Maybe (HieASTs Type)) +#endif generateHieAsts hscEnv tcm = handleGenerationErrors' dflags "extended interface generation" $ runHsc hscEnv $ do -- These varBinds use unitDataConId but it could be anything as the id name is not used -- during the hie file generation process. It's a workaround for the fact that the hie modules -- don't export an interface which allows for additional information to be added to hie files. - let fake_splice_binds = Util.listToBag (map (mkVarBind unitDataConId) (spliceExpressions $ tmrTopLevelSplices tcm)) + let fake_splice_binds = +#if !MIN_VERSION_ghc(9,11,0) + Util.listToBag $ +#endif + map (mkVarBind unitDataConId) (spliceExpressions $ tmrTopLevelSplices tcm) real_binds = tcg_binds $ tmrTypechecked tcm + all_binds = +#if MIN_VERSION_ghc(9,11,0) + fake_splice_binds ++ real_binds +#else + fake_splice_binds `Util.unionBags` real_binds +#endif ts = tmrTypechecked tcm :: TcGblEnv top_ev_binds = tcg_ev_binds ts :: Util.Bag EvBind insts = tcg_insts ts :: [ClsInst] tcs = tcg_tcs ts :: [TyCon] - run ts $ -#if MIN_VERSION_ghc(9,3,0) - pure $ Just $ + hie_asts = GHC.enrichHie all_binds (tmrRenamed tcm) top_ev_binds insts tcs + + pure $ Just $ +#if MIN_VERSION_ghc(9,11,0) + hie_asts (tcg_type_env ts) #else - Just <$> + hie_asts #endif - GHC.enrichHie (fake_splice_binds `Util.unionBags` real_binds) (tmrRenamed tcm) top_ev_binds insts tcs where dflags = hsc_dflags hscEnv - run _ts = -- ts is only used in GHC 9.2 -#if !MIN_VERSION_ghc(9,3,0) - fmap (join . snd) . liftIO . initDs hscEnv _ts -#else - id -#endif spliceExpressions :: Splices -> [LHsExpr GhcTc] spliceExpressions Splices{..} = @@ -904,7 +872,6 @@ spliceExpressions Splices{..} = -- indexHieFile :: ShakeExtras -> ModSummary -> NormalizedFilePath -> Util.Fingerprint -> Compat.HieFile -> IO () indexHieFile se mod_summary srcPath !hash hf = do - IdeOptions{optProgressStyle} <- getIdeOptionsIO se atomically $ do pending <- readTVar indexPending case HashMap.lookup srcPath pending of @@ -925,69 +892,14 @@ indexHieFile se mod_summary srcPath !hash hf = do unless newerScheduled $ do -- Using bracket, so even if an exception happen during withHieDb call, -- the `post` (which clean the progress indicator) will still be called. - bracket_ (pre optProgressStyle) post $ + bracket_ pre post $ withHieDb (\db -> HieDb.addRefsFromLoaded db targetPath (HieDb.RealFile $ fromNormalizedFilePath srcPath) hash hf') where mod_location = ms_location mod_summary targetPath = Compat.ml_hie_file mod_location HieDbWriter{..} = hiedbWriter se - -- Get a progress token to report progress and update it for the current file - pre style = do - tok <- modifyVar indexProgressToken $ fmap dupe . \case - x@(Just _) -> pure x - -- Create a token if we don't already have one - Nothing -> do - case lspEnv se of - Nothing -> pure Nothing - Just env -> LSP.runLspT env $ do - u <- LSP.ProgressToken . LSP.InR . T.pack . show . hashUnique <$> liftIO Unique.newUnique - -- TODO: Wait for the progress create response to use the token - _ <- LSP.sendRequest LSP.SMethod_WindowWorkDoneProgressCreate (LSP.WorkDoneProgressCreateParams u) (const $ pure ()) - LSP.sendNotification LSP.SMethod_Progress $ LSP.ProgressParams u $ - toJSON $ LSP.WorkDoneProgressBegin - { _kind = LSP.AString @"begin" - , _title = "Indexing" - , _cancellable = Nothing - , _message = Nothing - , _percentage = Nothing - } - pure (Just u) - - (!done, !remaining) <- atomically $ do - done <- readTVar indexCompleted - remaining <- HashMap.size <$> readTVar indexPending - pure (done, remaining) - let - progressFrac :: Double - progressFrac = fromIntegral done / fromIntegral (done + remaining) - progressPct :: LSP.UInt - progressPct = floor $ 100 * progressFrac - - whenJust (lspEnv se) $ \env -> whenJust tok $ \token -> LSP.runLspT env $ - LSP.sendNotification LSP.SMethod_Progress $ LSP.ProgressParams token $ - toJSON $ - case style of - Percentage -> LSP.WorkDoneProgressReport - { _kind = LSP.AString @"report" - , _cancellable = Nothing - , _message = Nothing - , _percentage = Just progressPct - } - Explicit -> LSP.WorkDoneProgressReport - { _kind = LSP.AString @"report" - , _cancellable = Nothing - , _message = Just $ - T.pack " (" <> T.pack (show done) <> "/" <> T.pack (show $ done + remaining) <> ")..." - , _percentage = Nothing - } - NoProgress -> LSP.WorkDoneProgressReport - { _kind = LSP.AString @"report" - , _cancellable = Nothing - , _message = Nothing - , _percentage = Nothing - } - + pre = progressUpdate indexProgressReporting ProgressStarted -- Report the progress once we are done indexing this file post = do mdone <- atomically $ do @@ -1002,20 +914,16 @@ indexHieFile se mod_summary srcPath !hash hf = do when (coerce $ ideTesting se) $ LSP.sendNotification (LSP.SMethod_CustomMethod (Proxy @"ghcide/reference/ready")) $ toJSON $ fromNormalizedFilePath srcPath - whenJust mdone $ \done -> - modifyVar_ indexProgressToken $ \tok -> do - whenJust (lspEnv se) $ \env -> LSP.runLspT env $ - whenJust tok $ \token -> - LSP.sendNotification LSP.SMethod_Progress $ LSP.ProgressParams token $ - toJSON $ - LSP.WorkDoneProgressEnd - { _kind = LSP.AString @"end" - , _message = Just $ "Finished indexing " <> T.pack (show done) <> " files" - } - -- We are done with the current indexing cycle, so destroy the token - pure Nothing - -writeAndIndexHieFile :: HscEnv -> ShakeExtras -> ModSummary -> NormalizedFilePath -> [GHC.AvailInfo] -> HieASTs Type -> BS.ByteString -> IO [FileDiagnostic] + whenJust mdone $ \_ -> progressUpdate indexProgressReporting ProgressCompleted + +writeAndIndexHieFile + :: HscEnv -> ShakeExtras -> ModSummary -> NormalizedFilePath -> [GHC.AvailInfo] +#if MIN_VERSION_ghc(9,11,0) + -> (HieASTs Type, NameEntityInfo) +#else + -> HieASTs Type +#endif + -> BS.ByteString -> IO [FileDiagnostic] writeAndIndexHieFile hscEnv se mod_summary srcPath exports ast source = handleGenerationErrors dflags "extended interface write/compression" $ do hf <- runHsc hscEnv $ @@ -1042,27 +950,80 @@ handleGenerationErrors :: DynFlags -> T.Text -> IO () -> IO [FileDiagnostic] handleGenerationErrors dflags source action = action >> return [] `catches` [ Handler $ return . diagFromGhcException source dflags - , Handler $ return . diagFromString source DiagnosticSeverity_Error (noSpan "") - . (("Error during " ++ T.unpack source) ++) . show @SomeException + , Handler $ \(exception :: SomeException) -> return $ + diagFromString + source DiagnosticSeverity_Error (noSpan "") + ("Error during " ++ T.unpack source ++ show exception) + Nothing ] handleGenerationErrors' :: DynFlags -> T.Text -> IO (Maybe a) -> IO ([FileDiagnostic], Maybe a) handleGenerationErrors' dflags source action = fmap ([],) action `catches` [ Handler $ return . (,Nothing) . diagFromGhcException source dflags - , Handler $ return . (,Nothing) . diagFromString source DiagnosticSeverity_Error (noSpan "") - . (("Error during " ++ T.unpack source) ++) . show @SomeException + , Handler $ \(exception :: SomeException) -> + return + ( diagFromString + source DiagnosticSeverity_Error (noSpan "") + ("Error during " ++ T.unpack source ++ show exception) + Nothing + , Nothing + ) ] - -- Merge the HPTs, module graphs and FinderCaches -- See Note [GhcSessionDeps] in Development.IDE.Core.Rules -- Add the current ModSummary to the graph, along with the -- HomeModInfo's of all direct dependencies (by induction hypothesis all -- transitive dependencies will be contained in envs) -mergeEnvs :: HscEnv -> ModuleGraph -> ModSummary -> [HomeModInfo] -> [HscEnv] -> IO HscEnv -mergeEnvs env mg ms extraMods envs = do -#if MIN_VERSION_ghc(9,3,0) +#if MIN_VERSION_ghc(9,11,0) +mergeEnvs :: HscEnv + -> ModuleGraph + -> DependencyInformation + -> ModSummary + -> [HomeModInfo] + -> [HscEnv] + -> IO HscEnv +mergeEnvs env mg dep_info ms extraMods envs = do + return $! loadModulesHome extraMods $ + let newHug = foldl' mergeHUG (hsc_HUG env) (map hsc_HUG envs) in + (hscUpdateHUG (const newHug) env){ + hsc_mod_graph = mg, + hsc_FC = (hsc_FC env) + { addToFinderCache = \gwib@(GWIB im _) val -> + if moduleUnit im `elem` hsc_all_home_unit_ids env + then pure () + else addToFinderCache (hsc_FC env) gwib val + , lookupFinderCache = \gwib@(GWIB im _) -> + if moduleUnit im `elem` hsc_all_home_unit_ids env + then case lookupModuleFile (im { moduleUnit = RealUnit (Definite $ moduleUnit im) }) dep_info of + Nothing -> pure Nothing + Just fs -> let ml = fromJust $ do + id <- lookupPathToId (depPathIdMap dep_info) fs + artifactModLocation (idToModLocation (depPathIdMap dep_info) id) + in pure $ Just $ InstalledFound ml im + else lookupFinderCache (hsc_FC env) gwib + } + } + + where + mergeHUG (UnitEnvGraph a) (UnitEnvGraph b) = UnitEnvGraph $ Map.unionWith mergeHUE a b + mergeHUE a b = a { homeUnitEnv_hpt = mergeUDFM (homeUnitEnv_hpt a) (homeUnitEnv_hpt b) } + mergeUDFM = plusUDFM_C combineModules + + combineModules a b + | HsSrcFile <- mi_hsc_src (hm_iface a) = a + | otherwise = b + +#else +mergeEnvs :: HscEnv + -> ModuleGraph + -> DependencyInformation + -> ModSummary + -> [HomeModInfo] + -> [HscEnv] + -> IO HscEnv +mergeEnvs env mg _dep_info ms extraMods envs = do let im = Compat.installedModule (toUnitId $ moduleUnit $ ms_mod ms) (moduleName (ms_mod ms)) ifr = InstalledFound (ms_location ms) im curFinderCache = Compat.extendInstalledModuleEnv Compat.emptyInstalledModuleEnv im ifr @@ -1096,33 +1057,9 @@ mergeEnvs env mg ms extraMods envs = do fcModules' <- newIORef $! foldl' (plusInstalledModuleEnv combineModuleLocations) cur fcModules fcFiles' <- newIORef $! Map.unions fcFiles pure $ FinderCache fcModules' fcFiles' - -#else - prevFinderCache <- concatFC <$> mapM (readIORef . hsc_FC) envs - let im = Compat.installedModule (toUnitId $ moduleUnit $ ms_mod ms) (moduleName (ms_mod ms)) - ifr = InstalledFound (ms_location ms) im - newFinderCache <- newIORef $! Compat.extendInstalledModuleEnv prevFinderCache im ifr - return $! loadModulesHome extraMods $ - env{ - hsc_HPT = foldMapBy mergeUDFM emptyUDFM hsc_HPT envs, - hsc_FC = newFinderCache, - hsc_mod_graph = mg - } - - where - mergeUDFM = plusUDFM_C combineModules - combineModules a b - | HsSrcFile <- mi_hsc_src (hm_iface a) = a - | otherwise = b - -- required because 'FinderCache': - -- 1) doesn't have a 'Monoid' instance, - -- 2) is abstract and doesn't export constructors - -- To work around this, we coerce to the underlying type - -- To remove this, I plan to upstream the missing Monoid instance - concatFC :: [FinderCache] -> FinderCache - concatFC = unsafeCoerce (mconcat @(Map InstalledModule InstalledFindResult)) #endif + withBootSuffix :: HscSource -> ModLocation -> ModLocation withBootSuffix HsBootFile = addBootSuffixLocnOut withBootSuffix _ = id @@ -1166,24 +1103,16 @@ getModSummaryFromImports env fp _modTime mContents = do convImport (L _ i) = ( -#if !MIN_VERSION_ghc(9,3,0) - fmap sl_fs -#endif (ideclPkgQual i) , reLoc $ ideclName i) msrImports = implicit_imports ++ imps -#if MIN_VERSION_ghc(9,3,0) rn_pkg_qual = renameRawPkgQual (hsc_unit_env ppEnv) rn_imps = fmap (\(pk, lmn@(L _ mn)) -> (rn_pkg_qual mn pk, lmn)) srcImports = rn_imps $ map convImport src_idecls textualImports = rn_imps $ map convImport (implicit_imports ++ ordinary_imps) ghc_prim_import = not (null _ghc_prim_imports) -#else - srcImports = map convImport src_idecls - textualImports = map convImport (implicit_imports ++ ordinary_imps) -#endif -- Force bits that might keep the string buffer and DynFlags alive unnecessarily @@ -1199,18 +1128,14 @@ getModSummaryFromImports env fp _modTime mContents = do let modl = mkHomeModule (hscHomeUnit ppEnv) mod sourceType = if "-boot" `isSuffixOf` takeExtension fp then HsBootFile else HsSrcFile - msrModSummary2 = + msrModSummary = ModSummary { ms_mod = modl , ms_hie_date = Nothing -#if MIN_VERSION_ghc(9,3,0) , ms_dyn_obj_date = Nothing , ms_ghc_prim_import = ghc_prim_import , ms_hs_hash = _src_hash -#else - , ms_hs_date = _modTime -#endif , ms_hsc_src = sourceType -- The contents are used by the GetModSummary rule , ms_hspp_buf = Just contents @@ -1224,8 +1149,8 @@ getModSummaryFromImports env fp _modTime mContents = do , ms_textual_imps = textualImports } - msrFingerprint <- liftIO $ computeFingerprint opts msrModSummary2 - (msrModSummary, msrHscEnv) <- liftIO $ initPlugins ppEnv msrModSummary2 + msrFingerprint <- liftIO $ computeFingerprint opts msrModSummary + msrHscEnv <- liftIO $ Loader.initializePlugins (hscSetFlags (ms_hspp_opts msrModSummary) ppEnv) return ModSummaryResult{..} where -- Compute a fingerprint from the contents of `ModSummary`, @@ -1235,19 +1160,24 @@ getModSummaryFromImports env fp _modTime mContents = do put $ Util.uniq $ moduleNameFS $ moduleName ms_mod forM_ (ms_srcimps ++ ms_textual_imps) $ \(mb_p, m) -> do put $ Util.uniq $ moduleNameFS $ unLoc m -#if MIN_VERSION_ghc(9,3,0) case mb_p of - G.NoPkgQual -> pure () + G.NoPkgQual -> pure () G.ThisPkg uid -> put $ getKey $ getUnique uid G.OtherPkg uid -> put $ getKey $ getUnique uid -#else - whenJust mb_p $ put . Util.uniq -#endif return $! Util.fingerprintFingerprints $ [ Util.fingerprintString fp , fingerPrintImports + , modLocationFingerprint ms_location ] ++ map Util.fingerprintString opts + modLocationFingerprint :: ModLocation -> Util.Fingerprint + modLocationFingerprint ModLocation{..} = Util.fingerprintFingerprints $ + Util.fingerprintString <$> [ fromMaybe "" ml_hs_file + , ml_hi_file + , ml_dyn_hi_file + , ml_obj_file + , ml_dyn_obj_file + , ml_hie_file] -- | Parse only the module header parseHeader @@ -1255,18 +1185,14 @@ parseHeader => DynFlags -- ^ flags to use -> FilePath -- ^ the filename (for source locations) -> Util.StringBuffer -- ^ Haskell module source text (full Unicode is supported) -#if MIN_VERSION_ghc(9,5,0) - -> ExceptT [FileDiagnostic] m ([FileDiagnostic], Located(HsModule GhcPs)) -#else - -> ExceptT [FileDiagnostic] m ([FileDiagnostic], Located(HsModule)) -#endif + -> ExceptT [FileDiagnostic] m ([FileDiagnostic], Located (HsModule GhcPs)) parseHeader dflags filename contents = do let loc = mkRealSrcLoc (Util.mkFastString filename) 1 1 case unP Compat.parseHeader (initParserState (initParserOpts dflags) contents loc) of PFailedWithErrorMessages msgs -> - throwE $ diagFromErrMsgs sourceParser dflags $ msgs dflags + throwE $ diagFromGhcErrorMessages sourceParser dflags $ msgs dflags POk pst rdr_module -> do - let (warns, errs) = renderMessages $ getPsMessages pst dflags + let (warns, errs) = renderMessages $ getPsMessages pst -- Just because we got a `POk`, it doesn't mean there -- weren't errors! To clarify, the GHC parser @@ -1278,9 +1204,9 @@ parseHeader dflags filename contents = do -- errors are those from which a parse tree just can't -- be produced. unless (null errs) $ - throwE $ diagFromErrMsgs sourceParser dflags errs + throwE $ diagFromGhcErrorMessages sourceParser dflags errs - let warnings = diagFromErrMsgs sourceParser dflags warns + let warnings = diagFromGhcErrorMessages sourceParser dflags warns return (warnings, rdr_module) -- | Given a buffer, flags, and file path, produce a @@ -1297,20 +1223,29 @@ parseFileContents env customPreprocessor filename ms = do dflags = ms_hspp_opts ms contents = fromJust $ ms_hspp_buf ms case unP Compat.parseModule (initParserState (initParserOpts dflags) contents loc) of - PFailedWithErrorMessages msgs -> throwE $ diagFromErrMsgs sourceParser dflags $ msgs dflags + PFailedWithErrorMessages msgs -> + throwE $ diagFromGhcErrorMessages sourceParser dflags $ msgs dflags POk pst rdr_module -> let - hpm_annotations = mkApiAnns pst - psMessages = getPsMessages pst dflags + psMessages = getPsMessages pst in do - let IdePreprocessedSource preproc_warns errs parsed = customPreprocessor rdr_module - - unless (null errs) $ - throwE $ diagFromStrings sourceParser DiagnosticSeverity_Error errs - - let preproc_warnings = diagFromStrings sourceParser DiagnosticSeverity_Warning preproc_warns - (parsed', msgs) <- liftIO $ applyPluginsParsedResultAction env dflags ms hpm_annotations parsed psMessages + let IdePreprocessedSource preproc_warns preproc_errs parsed = customPreprocessor rdr_module + let attachNoStructuredError (span, msg) = (span, msg, Nothing) + + unless (null preproc_errs) $ + throwE $ + diagFromStrings + sourceParser + DiagnosticSeverity_Error + (fmap attachNoStructuredError preproc_errs) + + let preproc_warning_file_diagnostics = + diagFromStrings + sourceParser + DiagnosticSeverity_Warning + (fmap attachNoStructuredError preproc_warns) + (parsed', msgs) <- liftIO $ applyPluginsParsedResultAction env ms parsed psMessages let (warns, errors) = renderMessages msgs -- Just because we got a `POk`, it doesn't mean there @@ -1323,8 +1258,7 @@ parseFileContents env customPreprocessor filename ms = do -- errors are those from which a parse tree just can't -- be produced. unless (null errors) $ - throwE $ diagFromErrMsgs sourceParser dflags errors - + throwE $ diagFromGhcErrorMessages sourceParser dflags errors -- To get the list of extra source files, we take the list -- that the parser gave us, @@ -1337,11 +1271,7 @@ parseFileContents env customPreprocessor filename ms = do -- - filter out the .hs/.lhs source filename if we have one -- let n_hspp = normalise filename -#if MIN_VERSION_ghc(9,3,0) TempDir tmp_dir = tmpDir dflags -#else - tmp_dir = tmpDir dflags -#endif srcs0 = nubOrd $ filter (not . (tmp_dir `isPrefixOf`)) $ filter (/= n_hspp) $ map normalise @@ -1357,9 +1287,9 @@ parseFileContents env customPreprocessor filename ms = do -- filter them out: srcs2 <- liftIO $ filterM doesFileExist srcs1 - let pm = ParsedModule ms parsed' srcs2 hpm_annotations - warnings = diagFromErrMsgs sourceParser dflags warns - pure (warnings ++ preproc_warnings, pm) + let pm = ParsedModule ms parsed' srcs2 + warnings = diagFromGhcErrorMessages sourceParser dflags warns + pure (warnings ++ preproc_warning_file_diagnostics, pm) loadHieFile :: Compat.NameCacheUpdater -> FilePath -> IO GHC.HieFile loadHieFile ncu f = do @@ -1367,7 +1297,7 @@ loadHieFile ncu f = do {- Note [Recompilation avoidance in the presence of TH] - + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Most versions of GHC we currently support don't have a working implementation of code unloading for object code, and no version of GHC supports this on certain platforms like Windows. This makes it completely infeasible for interactive use, @@ -1434,6 +1364,7 @@ data RecompilationInfo m , old_value :: Maybe (HiFileResult, FileVersion) , get_file_version :: NormalizedFilePath -> m (Maybe FileVersion) , get_linkable_hashes :: [NormalizedFilePath] -> m [BS.ByteString] + , get_module_graph :: m DependencyInformation , regenerate :: Maybe LinkableType -> m ([FileDiagnostic], Maybe HiFileResult) -- ^ Action to regenerate an interface } @@ -1488,14 +1419,9 @@ loadInterface session ms linkableNeeded RecompilationInfo{..} = do -- ncu and read_dflags are only used in GHC >= 9.4 let _ncu = hsc_NC sessionWithMsDynFlags _read_dflags = hsc_dflags sessionWithMsDynFlags -#if MIN_VERSION_ghc(9,3,0) read_result <- liftIO $ readIface _read_dflags _ncu mod iface_file -#else - read_result <- liftIO $ initIfaceCheck (text "readIface") sessionWithMsDynFlags - $ readIface mod iface_file -#endif case read_result of - Util.Failed{} -> return Nothing + Util.Failed{} -> return Nothing -- important to call `shareUsages` here before checkOldIface -- consults `mi_usages` Util.Succeeded iface -> return $ Just (shareUsages iface) @@ -1503,13 +1429,9 @@ loadInterface session ms linkableNeeded RecompilationInfo{..} = do -- If mb_old_iface is nothing then checkOldIface will load it for us -- given that the source is unmodified (recomp_iface_reqd, mb_checked_iface) -#if MIN_VERSION_ghc(9,3,0) <- liftIO $ checkOldIface sessionWithMsDynFlags ms _old_iface >>= \case UpToDateItem x -> pure (UpToDate, Just x) OutOfDateItem reason x -> pure (NeedsRecompile reason, x) -#else - <- liftIO $ checkOldIface sessionWithMsDynFlags ms _sourceMod mb_old_iface -#endif let do_regenerate _reason = withTrace "regenerate interface" $ \setTag -> do setTag "Module" $ moduleNameString $ moduleName mod @@ -1525,7 +1447,7 @@ loadInterface session ms linkableNeeded RecompilationInfo{..} = do | not (mi_used_th iface) = emptyModuleEnv | otherwise = parseRuntimeDeps (md_anns details) -- Peform the fine grained recompilation check for TH - maybe_recomp <- checkLinkableDependencies session get_linkable_hashes runtime_deps + maybe_recomp <- checkLinkableDependencies get_linkable_hashes get_module_graph runtime_deps case maybe_recomp of Just msg -> do_regenerate msg Nothing @@ -1562,20 +1484,10 @@ parseRuntimeDeps anns = mkModuleEnv $ mapMaybe go anns -- the runtime dependencies of the module, to check if any of them are out of date -- Hopefully 'runtime_deps' will be empty if the module didn't actually use TH -- See Note [Recompilation avoidance in the presence of TH] -checkLinkableDependencies :: MonadIO m => HscEnv -> ([NormalizedFilePath] -> m [BS.ByteString]) -> ModuleEnv BS.ByteString -> m (Maybe RecompileRequired) -checkLinkableDependencies hsc_env get_linkable_hashes runtime_deps = do -#if MIN_VERSION_ghc(9,3,0) - moduleLocs <- liftIO $ readIORef (fcModuleCache $ hsc_FC hsc_env) -#else - moduleLocs <- liftIO $ readIORef (hsc_FC hsc_env) -#endif - let go (mod, hash) = do - ifr <- lookupInstalledModuleEnv moduleLocs $ Compat.installedModule (toUnitId $ moduleUnit mod) (moduleName mod) - case ifr of - InstalledFound loc _ -> do - hs <- ml_hs_file loc - pure (toNormalizedFilePath' hs,hash) - _ -> Nothing +checkLinkableDependencies :: MonadIO m => ([NormalizedFilePath] -> m [BS.ByteString]) -> m DependencyInformation -> ModuleEnv BS.ByteString -> m (Maybe RecompileRequired) +checkLinkableDependencies get_linkable_hashes get_module_graph runtime_deps = do + graph <- get_module_graph + let go (mod, hash) = (,hash) <$> lookupModuleFile mod graph hs_files = mapM go (moduleEnvToList runtime_deps) case hs_files of Nothing -> error "invalid module graph" @@ -1589,27 +1501,16 @@ checkLinkableDependencies hsc_env get_linkable_hashes runtime_deps = do recompBecause :: String -> RecompileRequired recompBecause = -#if MIN_VERSION_ghc(9,3,0) NeedsRecompile . -#endif RecompBecause -#if MIN_VERSION_ghc(9,3,0) . CustomReason -#endif -#if MIN_VERSION_ghc(9,3,0) data SourceModified = SourceModified | SourceUnmodified deriving (Eq, Ord, Show) -#endif showReason :: RecompileRequired -> String -showReason UpToDate = "UpToDate" -#if MIN_VERSION_ghc(9,3,0) -showReason (NeedsRecompile MustCompile) = "MustCompile" -showReason (NeedsRecompile s) = printWithoutUniques s -#else -showReason MustCompile = "MustCompile" -showReason (RecompBecause s) = s -#endif +showReason UpToDate = "UpToDate" +showReason (NeedsRecompile MustCompile) = "MustCompile" +showReason (NeedsRecompile s) = printWithoutUniques s mkDetailsFromIface :: HscEnv -> ModIface -> IO ModDetails mkDetailsFromIface session iface = do @@ -1624,24 +1525,18 @@ coreFileToCgGuts session iface details core_file = do this_mod = mi_module iface types_var <- newIORef (md_types details) let hsc_env' = hscUpdateHPT act (session { -#if MIN_VERSION_ghc(9,3,0) hsc_type_env_vars = knotVarsFromModuleEnv (mkModuleEnv [(this_mod, types_var)]) -#else - hsc_type_env_var = Just (this_mod, types_var) -#endif }) core_binds <- initIfaceCheck (text "l") hsc_env' $ typecheckCoreFile this_mod types_var core_file -- Implicit binds aren't saved, so we need to regenerate them ourselves. let _implicit_binds = concatMap getImplicitBinds tyCons -- only used if GHC < 9.6 tyCons = typeEnvTyCons (md_types details) -#if MIN_VERSION_ghc(9,5,0) -- In GHC 9.6, the implicit binds are tidied and part of core_binds - pure $ CgGuts this_mod tyCons core_binds [] NoStubs [] mempty (emptyHpcInfo False) Nothing [] -#elif MIN_VERSION_ghc(9,3,0) - pure $ CgGuts this_mod tyCons (_implicit_binds ++ core_binds) [] NoStubs [] mempty (emptyHpcInfo False) Nothing [] -#else - pure $ CgGuts this_mod tyCons (_implicit_binds ++ core_binds) NoStubs [] [] (emptyHpcInfo False) Nothing [] + pure $ CgGuts this_mod tyCons core_binds [] NoStubs [] mempty +#if !MIN_VERSION_ghc(9,11,0) + (emptyHpcInfo False) #endif + Nothing [] coreFileToLinkable :: LinkableType -> HscEnv -> ModSummary -> ModIface -> ModDetails -> CoreFile -> UTCTime -> IO ([FileDiagnostic], Maybe HomeModInfo) coreFileToLinkable linkableType session ms iface details core_file t = do @@ -1657,45 +1552,23 @@ coreFileToLinkable linkableType session ms iface details core_file t = do getDocsBatch :: HscEnv -> [Name] -#if MIN_VERSION_ghc(9,3,0) -> IO [Either String (Maybe [HsDoc GhcRn], IntMap (HsDoc GhcRn))] -#else - -> IO [Either String (Maybe HsDocString, IntMap HsDocString)] -#endif getDocsBatch hsc_env _names = do res <- initIfaceLoad hsc_env $ forM _names $ \name -> case nameModule_maybe name of Nothing -> return (Left $ NameHasNoModule name) Just mod -> do ModIface { -#if MIN_VERSION_ghc(9,3,0) mi_docs = Just Docs{ docs_mod_hdr = mb_doc_hdr , docs_decls = dmap , docs_args = amap } -#else - mi_doc_hdr = mb_doc_hdr - , mi_decl_docs = DeclDocMap dmap - , mi_arg_docs = ArgDocMap amap -#endif } <- loadSysInterface (text "getModuleInterface") mod -#if MIN_VERSION_ghc(9,3,0) if isNothing mb_doc_hdr && isNullUniqMap dmap && isNullUniqMap amap -#else - if isNothing mb_doc_hdr && Map.null dmap && null amap -#endif then pure (Left (NoDocsInIface mod $ compiled name)) else pure (Right ( -#if MIN_VERSION_ghc(9,3,0) lookupUniqMap dmap name, -#else - Map.lookup name dmap , -#endif -#if MIN_VERSION_ghc(9,3,0) lookupWithDefaultUniqMap amap mempty name)) -#else - Map.findWithDefault mempty name amap)) -#endif return $ map (first $ T.unpack . printOutputable) res where compiled n = @@ -1725,7 +1598,7 @@ lookupName hsc_env name = exceptionHandle $ do res <- initIfaceLoad hsc_env $ importDecl name case res of Util.Succeeded x -> return (Just x) - _ -> return Nothing + _ -> return Nothing where exceptionHandle x = x `catch` \(_ :: IOEnvFailure) -> pure Nothing @@ -1736,7 +1609,24 @@ pathToModuleName = mkModuleName . map rep rep ':' = '_' rep c = c +-- | Initialising plugins looks in the finder cache, but we know that the plugin doesn't come from a home module, so don't +-- error out when we don't find it +setNonHomeFCHook :: HscEnv -> HscEnv +setNonHomeFCHook hsc_env = +#if MIN_VERSION_ghc(9,11,0) + hsc_env { hsc_FC = (hsc_FC hsc_env) + { lookupFinderCache = \m@(GWIB im _) -> + if moduleUnit im `elem` hsc_all_home_unit_ids hsc_env + then pure (Just $ InstalledNotFound [] Nothing) + else lookupFinderCache (hsc_FC hsc_env) m + } + } +#else + hsc_env +#endif + {- Note [Guidelines For Using CPP In GHCIDE Import Statements] + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ GHCIDE's interface with GHC is extensive, and unfortunately, because we have to work with multiple versions of GHC, we have several files that need to use a lot of CPP. In order to simplify the CPP in the import section of every file @@ -1748,19 +1638,19 @@ pathToModuleName = mkModuleName . map rep - CPP clauses should be placed at the end of the imports section. The clauses should be ordered by the GHC version they target from earlier to later versions, - with negative if clauses coming before positive if clauses of the same - version. (If you think about which GHC version a clause activates for this + with negative if clauses coming before positive if clauses of the same + version. (If you think about which GHC version a clause activates for this should make sense `!MIN_VERSION_GHC(9,0,0)` refers to 8.10 and lower which is - a earlier version than `MIN_VERSION_GHC(9,0,0)` which refers to versions 9.0 + an earlier version than `MIN_VERSION_GHC(9,0,0)` which refers to versions 9.0 and later). In addition there should be a space before and after each CPP clause. - - In if clauses that use `&&` and depend on more than one statement, the + - In if clauses that use `&&` and depend on more than one statement, the positive statement should come before the negative statement. In addition the clause should come after the single positive clause for that GHC version. - - There shouldn't be multiple identical CPP statements. The use of odd or even + - There shouldn't be multiple identical CPP statements. The use of odd or even GHC numbers is identical, with the only preference being to use what is - already there. (i.e. (`MIN_VERSION_GHC(9,2,0)` and `MIN_VERSION_GHC(9,1,0)` + already there. (i.e. (`MIN_VERSION_GHC(9,2,0)` and `MIN_VERSION_GHC(9,1,0)` are functionally equivalent) -} diff --git a/ghcide/src/Development/IDE/Core/FileExists.hs b/ghcide/src/Development/IDE/Core/FileExists.hs index 7a3d9cdd60..280cd14028 100644 --- a/ghcide/src/Development/IDE/Core/FileExists.hs +++ b/ghcide/src/Development/IDE/Core/FileExists.hs @@ -28,6 +28,7 @@ import qualified Development.IDE.Core.Shake as Shake import Development.IDE.Graph import Development.IDE.Types.Location import Development.IDE.Types.Options +import Development.IDE.Types.Shake (toKey) import qualified Focus import Ide.Logger (Pretty (pretty), Recorder, WithPriority, @@ -40,6 +41,7 @@ import qualified System.Directory as Dir import qualified System.FilePath.Glob as Glob {- Note [File existence cache and LSP file watchers] + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Some LSP servers provide the ability to register file watches with the client, which will then notify us of file changes. Some clients can do this more efficiently than us, or generally it's a tricky problem @@ -104,12 +106,12 @@ getFileExistsMapUntracked = do FileExistsMapVar v <- getIdeGlobalAction return v --- | Modify the global store of file exists. -modifyFileExists :: IdeState -> [(NormalizedFilePath, FileChangeType)] -> IO () +-- | Modify the global store of file exists and return the keys that need to be marked as dirty +modifyFileExists :: IdeState -> [(NormalizedFilePath, FileChangeType)] -> IO [Key] modifyFileExists state changes = do FileExistsMapVar var <- getIdeGlobalState state -- Masked to ensure that the previous values are flushed together with the map update - join $ mask_ $ atomicallyNamed "modifyFileExists" $ do + mask_ $ atomicallyNamed "modifyFileExists" $ do forM_ changes $ \(f,c) -> case fromChange c of Just c' -> STM.focus (Focus.insert c') f var @@ -118,10 +120,10 @@ modifyFileExists state changes = do -- flush previous values let (fileModifChanges, fileExistChanges) = partition ((== FileChangeType_Changed) . snd) changes - mapM_ (deleteValue (shakeExtras state) GetFileExists . fst) fileExistChanges - io1 <- recordDirtyKeys (shakeExtras state) GetFileExists $ map fst fileExistChanges - io2 <- recordDirtyKeys (shakeExtras state) GetModificationTime $ map fst fileModifChanges - return (io1 <> io2) + keys0 <- concat <$> mapM (deleteValue (shakeExtras state) GetFileExists . fst) fileExistChanges + let keys1 = map (toKey GetFileExists . fst) fileExistChanges + let keys2 = map (toKey GetModificationTime . fst) fileModifChanges + return (keys0 <> keys1 <> keys2) fromChange :: FileChangeType -> Maybe Bool fromChange FileChangeType_Created = Just True @@ -135,6 +137,7 @@ getFileExists :: NormalizedFilePath -> Action Bool getFileExists fp = use_ GetFileExists fp {- Note [Which files should we watch?] + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ The watcher system gives us a lot of flexibility: we can set multiple watchers, and they can all watch on glob patterns. @@ -201,6 +204,7 @@ fileExistsRulesFast recorder isWatched = else fileExistsSlow file {- Note [Invalidating file existence results] + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ We have two mechanisms for getting file existence information: - The file existence cache - The VFS lookup diff --git a/ghcide/src/Development/IDE/Core/FileStore.hs b/ghcide/src/Development/IDE/Core/FileStore.hs index 315a078282..7dad386ece 100644 --- a/ghcide/src/Development/IDE/Core/FileStore.hs +++ b/ghcide/src/Development/IDE/Core/FileStore.hs @@ -3,7 +3,10 @@ {-# LANGUAGE TypeFamilies #-} module Development.IDE.Core.FileStore( + getFileModTimeContents, getFileContents, + getUriContents, + getVersionedTextDoc, setFileModified, setSomethingModified, fileStoreRules, @@ -18,13 +21,13 @@ module Development.IDE.Core.FileStore( isWatchSupported, registerFileWatches, shareFilePath, - Log(..) + Log(..), ) where -import Control.Concurrent.STM.Stats (STM, atomically, - modifyTVar') +import Control.Concurrent.STM.Stats (STM, atomically) import Control.Concurrent.STM.TQueue (writeTQueue) import Control.Exception +import Control.Lens ((^.)) import Control.Monad.Extra import Control.Monad.IO.Class import qualified Data.Binary as B @@ -32,10 +35,9 @@ import qualified Data.ByteString as BS import qualified Data.ByteString.Lazy as LBS import qualified Data.HashMap.Strict as HashMap import Data.IORef -import Data.List (foldl') import qualified Data.Text as T import qualified Data.Text as Text -import qualified Data.Text.Utf16.Rope as Rope +import Data.Text.Utf16.Rope.Mixed (Rope) import Data.Time import Data.Time.Clock.POSIX import Development.IDE.Core.FileUtils @@ -49,6 +51,7 @@ import Development.IDE.Import.DependencyInformation import Development.IDE.Types.Diagnostics import Development.IDE.Types.Location import Development.IDE.Types.Options +import Development.IDE.Types.Shake (toKey) import HieDb.Create (deleteMissingRealFiles) import Ide.Logger (Pretty (pretty), Priority (Info), @@ -58,13 +61,16 @@ import Ide.Logger (Pretty (pretty), logWith, viaShow, (<+>)) import qualified Ide.Logger as L -import Ide.Plugin.Config (CheckParents (..), - Config) +import Ide.Types +import qualified Language.LSP.Protocol.Lens as L import Language.LSP.Protocol.Message (toUntypedRegistration) import qualified Language.LSP.Protocol.Message as LSP import Language.LSP.Protocol.Types (DidChangeWatchedFilesRegistrationOptions (DidChangeWatchedFilesRegistrationOptions), FileSystemWatcher (..), - _watchers) + TextDocumentIdentifier (..), + VersionedTextDocumentIdentifier (..), + _watchers, + uriToNormalizedFilePath) import qualified Language.LSP.Protocol.Types as LSP import qualified Language.LSP.Server as LSP import Language.LSP.VFS @@ -148,24 +154,24 @@ isInterface :: NormalizedFilePath -> Bool isInterface f = takeExtension (fromNormalizedFilePath f) `elem` [".hi", ".hi-boot", ".hie", ".hie-boot", ".core"] -- | Reset the GetModificationTime state of interface files -resetInterfaceStore :: ShakeExtras -> NormalizedFilePath -> STM () +resetInterfaceStore :: ShakeExtras -> NormalizedFilePath -> STM [Key] resetInterfaceStore state f = do deleteValue state GetModificationTime f -- | Reset the GetModificationTime state of watched files -- Assumes the list does not include any FOIs -resetFileStore :: IdeState -> [(NormalizedFilePath, LSP.FileChangeType)] -> IO () +resetFileStore :: IdeState -> [(NormalizedFilePath, LSP.FileChangeType)] -> IO [Key] resetFileStore ideState changes = mask $ \_ -> do -- we record FOIs document versions in all the stored values -- so NEVER reset FOIs to avoid losing their versions -- FOI filtering is done by the caller (LSP Notification handler) - forM_ changes $ \(nfp, c) -> do - case c of - LSP.FileChangeType_Changed - -- already checked elsewhere | not $ HM.member nfp fois - -> atomically $ - deleteValue (shakeExtras ideState) GetModificationTime nfp - _ -> pure () + fmap concat <$> + forM changes $ \(nfp, c) -> do + case c of + LSP.FileChangeType_Changed + -- already checked elsewhere | not $ HM.member nfp fois + -> atomically $ deleteValue (shakeExtras ideState) GetModificationTime nfp + _ -> pure [] modificationTime :: FileVersion -> Maybe UTCTime @@ -177,20 +183,20 @@ getFileContentsRule recorder = define (cmapWithPrio LogShake recorder) $ \GetFil getFileContentsImpl :: NormalizedFilePath - -> Action ([FileDiagnostic], Maybe (FileVersion, Maybe T.Text)) + -> Action ([FileDiagnostic], Maybe (FileVersion, Maybe Rope)) getFileContentsImpl file = do -- need to depend on modification time to introduce a dependency with Cutoff time <- use_ GetModificationTime file res <- do mbVirtual <- getVirtualFile file - pure $ Rope.toText . _file_text <$> mbVirtual + pure $ _file_text <$> mbVirtual pure ([], Just (time, res)) -- | Returns the modification time and the contents. -- For VFS paths, the modification time is the current time. -getFileContents :: NormalizedFilePath -> Action (UTCTime, Maybe T.Text) -getFileContents f = do - (fv, txt) <- use_ GetFileContents f +getFileModTimeContents :: NormalizedFilePath -> Action (UTCTime, Maybe Rope) +getFileModTimeContents f = do + (fv, contents) <- use_ GetFileContents f modTime <- case modificationTime fv of Just t -> pure t Nothing -> do @@ -200,7 +206,29 @@ getFileContents f = do _ -> do posix <- getModTime $ fromNormalizedFilePath f pure $ posixSecondsToUTCTime posix - return (modTime, txt) + return (modTime, contents) + +getFileContents :: NormalizedFilePath -> Action (Maybe Rope) +getFileContents f = snd <$> use_ GetFileContents f + +getUriContents :: NormalizedUri -> Action (Maybe Rope) +getUriContents uri = + join <$> traverse getFileContents (uriToNormalizedFilePath uri) + +-- | Given a text document identifier, annotate it with the latest version. +-- +-- Like Language.LSP.Server.Core.getVersionedTextDoc, but gets the virtual file +-- from the Shake VFS rather than the LSP VFS. +getVersionedTextDoc :: TextDocumentIdentifier -> Action VersionedTextDocumentIdentifier +getVersionedTextDoc doc = do + let uri = doc ^. L.uri + mvf <- + maybe (pure Nothing) getVirtualFile $ + uriToNormalizedFilePath $ toNormalizedUri uri + let ver = case mvf of + Just (VirtualFile lspver _ _) -> lspver + Nothing -> 0 + return (VersionedTextDocumentIdentifier uri ver) fileStoreRules :: Recorder (WithPriority Log) -> (NormalizedFilePath -> Action Bool) -> Rules () fileStoreRules recorder isWatched = do @@ -215,16 +243,18 @@ setFileModified :: Recorder (WithPriority Log) -> IdeState -> Bool -- ^ Was the file saved? -> NormalizedFilePath + -> IO [Key] -> IO () -setFileModified recorder vfs state saved nfp = do +setFileModified recorder vfs state saved nfp actionBefore = do ideOptions <- getIdeOptionsIO $ shakeExtras state doCheckParents <- optCheckParents ideOptions let checkParents = case doCheckParents of AlwaysCheck -> True CheckOnSave -> saved _ -> False - join $ atomically $ recordDirtyKeys (shakeExtras state) GetModificationTime [nfp] - restartShakeSession (shakeExtras state) vfs (fromNormalizedFilePath nfp ++ " (modified)") [] + restartShakeSession (shakeExtras state) vfs (fromNormalizedFilePath nfp ++ " (modified)") [] $ do + keys<-actionBefore + return (toKey GetModificationTime nfp:keys) when checkParents $ typecheckParents recorder state nfp @@ -234,7 +264,7 @@ typecheckParents recorder state nfp = void $ shakeEnqueue (shakeExtras state) pa typecheckParentsAction :: Recorder (WithPriority Log) -> NormalizedFilePath -> Action () typecheckParentsAction recorder nfp = do - revs <- transitiveReverseDependencies nfp <$> useNoFile_ GetModuleGraph + revs <- transitiveReverseDependencies nfp <$> useWithSeparateFingerprintRule_ GetModuleGraphTransReverseDepsFingerprints GetModuleGraph nfp case revs of Nothing -> logWith recorder Info $ LogCouldNotIdentifyReverseDeps nfp Just rs -> do @@ -244,14 +274,11 @@ typecheckParentsAction recorder nfp = do -- | Note that some keys have been modified and restart the session -- Only valid if the virtual file system was initialised by LSP, as that -- independently tracks which files are modified. -setSomethingModified :: VFSModified -> IdeState -> [Key] -> String -> IO () -setSomethingModified vfs state keys reason = do +setSomethingModified :: VFSModified -> IdeState -> String -> IO [Key] -> IO () +setSomethingModified vfs state reason actionBetweenSession = do -- Update database to remove any files that might have been renamed/deleted - atomically $ do - writeTQueue (indexQueue $ hiedbWriter $ shakeExtras state) (\withHieDb -> withHieDb deleteMissingRealFiles) - modifyTVar' (dirtyKeys $ shakeExtras state) $ \x -> - foldl' (flip insertKeySet) x keys - void $ restartShakeSession (shakeExtras state) vfs reason [] + atomically $ writeTQueue (indexQueue $ hiedbWriter $ shakeExtras state) (\withHieDb -> withHieDb deleteMissingRealFiles) + void $ restartShakeSession (shakeExtras state) vfs reason [] actionBetweenSession registerFileWatches :: [String] -> LSP.LspT Config IO Bool registerFileWatches globs = do @@ -265,7 +292,7 @@ registerFileWatches globs = do -- our purposes. registration = LSP.TRegistration { _id ="globalFileWatches" , _method = LSP.SMethod_WorkspaceDidChangeWatchedFiles - , _registerOptions = Just $ regOptions} + , _registerOptions = Just regOptions} regOptions = DidChangeWatchedFilesRegistrationOptions { _watchers = watchers } -- See Note [File existence cache and LSP file watchers] for why this exists, and the choice of watch kind @@ -306,4 +333,3 @@ shareFilePath k = unsafePerformIO $ do Just v -> (km, v) Nothing -> (HashMap.insert k k km, k) {-# NOINLINE shareFilePath #-} - diff --git a/ghcide/src/Development/IDE/Core/OfInterest.hs b/ghcide/src/Development/IDE/Core/OfInterest.hs index 599947659b..19e0f40e24 100644 --- a/ghcide/src/Development/IDE/Core/OfInterest.hs +++ b/ghcide/src/Development/IDE/Core/OfInterest.hs @@ -1,8 +1,7 @@ -- Copyright (c) 2019 The DAML Authors. All rights reserved. -- SPDX-License-Identifier: Apache-2.0 -{-# LANGUAGE FlexibleInstances #-} -{-# LANGUAGE TypeFamilies #-} +{-# LANGUAGE TypeFamilies #-} -- | Utilities and state for the files of interest - those which are currently -- open in the editor. The rule is 'IsFileOfInterest' @@ -25,7 +24,6 @@ import Control.Monad.IO.Class import Data.HashMap.Strict (HashMap) import qualified Data.HashMap.Strict as HashMap import Data.Proxy -import qualified Data.Text as T import Development.IDE.Graph import Control.Concurrent.STM.Stats (atomically, @@ -41,12 +39,14 @@ import Development.IDE.Plugin.Completions.Types import Development.IDE.Types.Exports import Development.IDE.Types.Location import Development.IDE.Types.Options (IdeTesting (..)) +import Development.IDE.Types.Shake (toKey) import GHC.TypeLits (KnownSymbol) import Ide.Logger (Pretty (pretty), + Priority (..), Recorder, WithPriority, cmapWithPrio, - logDebug) + logWith) import qualified Language.LSP.Protocol.Message as LSP import qualified Language.LSP.Server as LSP @@ -103,24 +103,26 @@ getFilesOfInterestUntracked = do OfInterestVar var <- getIdeGlobalAction liftIO $ readVar var -addFileOfInterest :: IdeState -> NormalizedFilePath -> FileOfInterestStatus -> IO () +addFileOfInterest :: IdeState -> NormalizedFilePath -> FileOfInterestStatus -> IO [Key] addFileOfInterest state f v = do OfInterestVar var <- getIdeGlobalState state (prev, files) <- modifyVar var $ \dict -> do let (prev, new) = HashMap.alterF (, Just v) f dict pure (new, (prev, new)) - when (prev /= Just v) $ do - join $ atomically $ recordDirtyKeys (shakeExtras state) IsFileOfInterest [f] - logDebug (ideLogger state) $ - "Set files of interest to: " <> T.pack (show files) - -deleteFileOfInterest :: IdeState -> NormalizedFilePath -> IO () + if prev /= Just v + then do + logWith (ideLogger state) Debug $ + LogSetFilesOfInterest (HashMap.toList files) + return [toKey IsFileOfInterest f] + else return [] + +deleteFileOfInterest :: IdeState -> NormalizedFilePath -> IO [Key] deleteFileOfInterest state f = do OfInterestVar var <- getIdeGlobalState state files <- modifyVar' var $ HashMap.delete f - join $ atomically $ recordDirtyKeys (shakeExtras state) IsFileOfInterest [f] - logDebug (ideLogger state) $ "Set files of interest to: " <> T.pack (show files) - + logWith (ideLogger state) Debug $ + LogSetFilesOfInterest (HashMap.toList files) + return [toKey IsFileOfInterest f] scheduleGarbageCollection :: IdeState -> IO () scheduleGarbageCollection state = do GarbageCollectVar var <- getIdeGlobalState state @@ -139,7 +141,7 @@ kick = do toJSON $ map fromNormalizedFilePath files signal (Proxy @"kick/start") - liftIO $ progressUpdate progress KickStarted + liftIO $ progressUpdate progress ProgressNewStarted -- Update the exports map results <- uses GenerateCore files @@ -150,7 +152,7 @@ kick = do let mguts = catMaybes results void $ liftIO $ atomically $ modifyTVar' exportsMap (updateExportsMapMg mguts) - liftIO $ progressUpdate progress KickCompleted + liftIO $ progressUpdate progress ProgressCompleted GarbageCollectVar var <- getIdeGlobalAction garbageCollectionScheduled <- liftIO $ readVar var diff --git a/ghcide/src/Development/IDE/Core/PluginUtils.hs b/ghcide/src/Development/IDE/Core/PluginUtils.hs index 76c88421c9..6ba633df26 100644 --- a/ghcide/src/Development/IDE/Core/PluginUtils.hs +++ b/ghcide/src/Development/IDE/Core/PluginUtils.hs @@ -1,6 +1,6 @@ {-# LANGUAGE GADTs #-} module Development.IDE.Core.PluginUtils -(-- Wrapped Action functions +(-- * Wrapped Action functions runActionE , runActionMT , useE @@ -9,13 +9,13 @@ module Development.IDE.Core.PluginUtils , usesMT , useWithStaleE , useWithStaleMT --- Wrapped IdeAction functions +-- * Wrapped IdeAction functions , runIdeActionE , runIdeActionMT , useWithStaleFastE , useWithStaleFastMT , uriToFilePathE --- Wrapped PositionMapping functions +-- * Wrapped PositionMapping functions , toCurrentPositionE , toCurrentPositionMT , fromCurrentPositionE @@ -23,8 +23,16 @@ module Development.IDE.Core.PluginUtils , toCurrentRangeE , toCurrentRangeMT , fromCurrentRangeE -, fromCurrentRangeMT) where - +, fromCurrentRangeMT +-- * Diagnostics +, activeDiagnosticsInRange +, activeDiagnosticsInRangeMT +-- * Formatting handlers +, mkFormattingHandlers) where + +import Control.Concurrent.STM +import Control.Lens ((^.)) +import Control.Monad.Error.Class (MonadError (throwError)) import Control.Monad.Extra import Control.Monad.IO.Class import Control.Monad.Reader (runReaderT) @@ -32,7 +40,10 @@ import Control.Monad.Trans.Except import Control.Monad.Trans.Maybe import Data.Functor.Identity import qualified Data.Text as T +import qualified Data.Text.Utf16.Rope.Mixed as Rope +import Development.IDE.Core.FileStore import Development.IDE.Core.PositionMapping +import Development.IDE.Core.Service (runAction) import Development.IDE.Core.Shake (IdeAction, IdeRule, IdeState (shakeExtras), mkDelayedAction, @@ -40,11 +51,17 @@ import Development.IDE.Core.Shake (IdeAction, IdeRule, import qualified Development.IDE.Core.Shake as Shake import Development.IDE.GHC.Orphans () import Development.IDE.Graph hiding (ShakeValue) +import Development.IDE.Types.Diagnostics import Development.IDE.Types.Location (NormalizedFilePath) import qualified Development.IDE.Types.Location as Location import qualified Ide.Logger as Logger import Ide.Plugin.Error +import Ide.PluginUtils (rangesOverlap) +import Ide.Types +import qualified Language.LSP.Protocol.Lens as LSP +import Language.LSP.Protocol.Message (SMethod (..)) import qualified Language.LSP.Protocol.Types as LSP +import qualified StmContainers.Map as STM -- ---------------------------------------------------------------------------- -- Action wrappers @@ -162,3 +179,77 @@ fromCurrentRangeE mapping = maybeToExceptT (PluginInvalidUserState "fromCurrentR -- |MaybeT version of `fromCurrentRange` fromCurrentRangeMT :: Monad m => PositionMapping -> LSP.Range -> MaybeT m LSP.Range fromCurrentRangeMT mapping = MaybeT . pure . fromCurrentRange mapping + +-- ---------------------------------------------------------------------------- +-- Diagnostics +-- ---------------------------------------------------------------------------- + +-- | @'activeDiagnosticsInRangeMT' shakeExtras nfp range@ computes the +-- 'FileDiagnostic' 's that HLS produced and overlap with the given @range@. +-- +-- This function is to be used whenever we need an authoritative source of truth +-- for which diagnostics are shown to the user. +-- These diagnostics can be used to provide various IDE features, for example +-- CodeActions, CodeLenses, or refactorings. +-- +-- However, why do we need this when computing 'CodeAction's? A 'CodeActionParam' +-- has the 'CodeActionContext' which already contains the diagnostics! +-- But according to the LSP docs, the server shouldn't rely that these Diagnostic +-- are actually up-to-date and accurately reflect the state of the document. +-- +-- From the LSP docs: +-- > An array of diagnostics known on the client side overlapping the range +-- > provided to the `textDocument/codeAction` request. They are provided so +-- > that the server knows which errors are currently presented to the user +-- > for the given range. There is no guarantee that these accurately reflect +-- > the error state of the resource. The primary parameter +-- > to compute code actions is the provided range. +-- +-- Thus, even when the client sends us the context, we should compute the +-- diagnostics on the server side. +activeDiagnosticsInRangeMT :: MonadIO m => Shake.ShakeExtras -> NormalizedFilePath -> LSP.Range -> MaybeT m [FileDiagnostic] +activeDiagnosticsInRangeMT ide nfp range = do + MaybeT $ liftIO $ atomically $ do + mDiags <- STM.lookup (LSP.normalizedFilePathToUri nfp) (Shake.publishedDiagnostics ide) + case mDiags of + Nothing -> pure Nothing + Just fileDiags -> do + pure $ Just $ filter diagRangeOverlaps fileDiags + where + diagRangeOverlaps = \fileDiag -> + rangesOverlap range (fileDiag ^. fdLspDiagnosticL . LSP.range) + +-- | Just like 'activeDiagnosticsInRangeMT'. See the docs of 'activeDiagnosticsInRangeMT' for details. +activeDiagnosticsInRange :: MonadIO m => Shake.ShakeExtras -> NormalizedFilePath -> LSP.Range -> m (Maybe [FileDiagnostic]) +activeDiagnosticsInRange ide nfp range = runMaybeT (activeDiagnosticsInRangeMT ide nfp range) + +-- ---------------------------------------------------------------------------- +-- Formatting handlers +-- ---------------------------------------------------------------------------- + +-- `mkFormattingHandlers` was moved here from hls-plugin-api package so that +-- `mkFormattingHandlers` can refer to `IdeState`. `IdeState` is defined in the +-- ghcide package, but hls-plugin-api does not depend on ghcide, so `IdeState` +-- is not in scope there. + +mkFormattingHandlers :: FormattingHandler IdeState -> PluginHandlers IdeState +mkFormattingHandlers f = mkPluginHandler SMethod_TextDocumentFormatting ( provider SMethod_TextDocumentFormatting) + <> mkPluginHandler SMethod_TextDocumentRangeFormatting (provider SMethod_TextDocumentRangeFormatting) + where + provider :: forall m. FormattingMethod m => SMethod m -> PluginMethodHandler IdeState m + provider m ide _pid params + | Just nfp <- LSP.uriToNormalizedFilePath $ LSP.toNormalizedUri uri = do + contentsMaybe <- liftIO $ runAction "mkFormattingHandlers" ide $ getFileContents nfp + case contentsMaybe of + Just contents -> do + let (typ, mtoken) = case m of + SMethod_TextDocumentFormatting -> (FormatText, params ^. LSP.workDoneToken) + SMethod_TextDocumentRangeFormatting -> (FormatRange (params ^. LSP.range), params ^. LSP.workDoneToken) + _ -> Prelude.error "mkFormattingHandlers: impossible" + f ide mtoken typ (Rope.toText contents) nfp opts + Nothing -> throwError $ PluginInvalidParams $ T.pack $ "Formatter plugin: could not get file contents for " ++ show uri + + | otherwise = throwError $ PluginInvalidParams $ T.pack $ "Formatter plugin: uriToFilePath failed for: " ++ show uri + where + uri = params ^. LSP.textDocument . LSP.uri + opts = params ^. LSP.options diff --git a/ghcide/src/Development/IDE/Core/PositionMapping.hs b/ghcide/src/Development/IDE/Core/PositionMapping.hs index 82d8334c87..de02f5b1f7 100644 --- a/ghcide/src/Development/IDE/Core/PositionMapping.hs +++ b/ghcide/src/Development/IDE/Core/PositionMapping.hs @@ -1,4 +1,3 @@ -{-# LANGUAGE OverloadedLabels #-} -- Copyright (c) 2019 The DAML Authors. All rights reserved. -- SPDX-License-Identifier: Apache-2.0 module Development.IDE.Core.PositionMapping @@ -10,7 +9,7 @@ module Development.IDE.Core.PositionMapping , fromCurrentPosition , toCurrentPosition , PositionDelta(..) - , addDelta + , addOldDelta , idDelta , composeDelta , mkDelta @@ -25,13 +24,14 @@ module Development.IDE.Core.PositionMapping ) where import Control.DeepSeq +import Control.Lens ((^.)) import Control.Monad import Data.Algorithm.Diff import Data.Bifunctor import Data.List -import Data.Row import qualified Data.Text as T import qualified Data.Vector.Unboxed as V +import qualified Language.LSP.Protocol.Lens as L import Language.LSP.Protocol.Types (Position (Position), Range (Range), TextDocumentContentChangeEvent (TextDocumentContentChangeEvent), @@ -119,16 +119,20 @@ idDelta = PositionDelta pure pure mkDelta :: [TextDocumentContentChangeEvent] -> PositionDelta mkDelta cs = foldl' applyChange idDelta cs --- | Add a new delta onto a Mapping k n to make a Mapping (k - 1) n -addDelta :: PositionDelta -> PositionMapping -> PositionMapping -addDelta delta (PositionMapping pm) = PositionMapping (composeDelta delta pm) +-- | addOldDelta +-- Add a old delta onto a Mapping k n to make a Mapping (k - 1) n +addOldDelta :: + PositionDelta -- ^ delta from version k - 1 to version k + -> PositionMapping -- ^ The input mapping is from version k to version n + -> PositionMapping -- ^ The output mapping is from version k - 1 to version n +addOldDelta delta (PositionMapping pm) = PositionMapping (composeDelta pm delta) -- TODO: We currently ignore the right hand side (if there is only text), as -- that was what was done with lsp* 1.6 packages applyChange :: PositionDelta -> TextDocumentContentChangeEvent -> PositionDelta applyChange PositionDelta{..} (TextDocumentContentChangeEvent (InL x)) = PositionDelta - { toDelta = toCurrent (x .! #range) (x .! #text) <=< toDelta - , fromDelta = fromDelta <=< fromCurrent (x .! #range) (x .! #text) + { toDelta = toCurrent (x ^. L.range) (x ^. L.text) <=< toDelta + , fromDelta = fromDelta <=< fromCurrent (x ^. L.range) (x ^. L.text) } applyChange posMapping _ = posMapping diff --git a/ghcide/src/Development/IDE/Core/Preprocessor.hs b/ghcide/src/Development/IDE/Core/Preprocessor.hs index 24a754870d..b3614d89ad 100644 --- a/ghcide/src/Development/IDE/Core/Preprocessor.hs +++ b/ghcide/src/Development/IDE/Core/Preprocessor.hs @@ -28,15 +28,11 @@ import Development.IDE.GHC.Error import Development.IDE.Types.Diagnostics import Development.IDE.Types.Location import qualified GHC.LanguageExtensions as LangExt +import qualified GHC.Runtime.Loader as Loader +import GHC.Utils.Logger (LogFlags (..)) import System.FilePath import System.IO.Extra --- See Note [Guidelines For Using CPP In GHCIDE Import Statements] - -#if MIN_VERSION_ghc(9,3,0) -import GHC.Utils.Logger (LogFlags (..)) -#endif - -- | Given a file and some contents, apply any necessary preprocessors, -- e.g. unlit/cpp. Return the resulting buffer and the DynFlags it implies. preprocessor :: HscEnv -> FilePath -> Maybe Util.StringBuffer -> ExceptT [FileDiagnostic] IO (Util.StringBuffer, [String], HscEnv, Util.Fingerprint) @@ -88,11 +84,7 @@ preprocessor env filename mbContents = do where logAction :: IORef [CPPLog] -> LogActionCompat logAction cppLogs dflags _reason severity srcSpan _style msg = do -#if MIN_VERSION_ghc(9,3,0) let cppLog = CPPLog (fromMaybe SevWarning severity) srcSpan $ T.pack $ renderWithContext (log_default_user_context dflags) msg -#else - let cppLog = CPPLog severity srcSpan $ T.pack $ showSDoc dflags msg -#endif modifyIORef cppLogs (cppLog :) @@ -112,7 +104,7 @@ data CPPDiag diagsFromCPPLogs :: FilePath -> [CPPLog] -> [FileDiagnostic] diagsFromCPPLogs filename logs = - map (\d -> (toNormalizedFilePath' filename, ShowDiag, cppDiagToDiagnostic d)) $ + map (\d -> ideErrorFromLspDiag (cppDiagToDiagnostic d) (toNormalizedFilePath' filename) Nothing) $ go [] logs where -- On errors, CPP calls logAction with a real span for the initial log and @@ -152,17 +144,13 @@ parsePragmasIntoHscEnv -> Util.StringBuffer -> IO (Either [FileDiagnostic] ([String], HscEnv)) parsePragmasIntoHscEnv env fp contents = catchSrcErrors dflags0 "pragmas" $ do -#if MIN_VERSION_ghc(9,3,0) let (_warns,opts) = getOptions (initParserOpts dflags0) contents fp -#else - let opts = getOptions dflags0 contents fp -#endif -- Force bits that might keep the dflags and stringBuffer alive unnecessarily evaluate $ rnf opts (dflags, _, _) <- parseDynamicFilePragma dflags0 opts - hsc_env' <- initializePlugins (hscSetFlags dflags env) + hsc_env' <- Loader.initializePlugins (hscSetFlags dflags env) return (map unLoc opts, hscSetFlags (disableWarningsAsErrors $ hsc_dflags hsc_env') hsc_env') where dflags0 = hsc_dflags env diff --git a/ghcide/src/Development/IDE/Core/ProgressReporting.hs b/ghcide/src/Development/IDE/Core/ProgressReporting.hs index 83d4670782..3d8a2bf989 100644 --- a/ghcide/src/Development/IDE/Core/ProgressReporting.hs +++ b/ghcide/src/Development/IDE/Core/ProgressReporting.hs @@ -1,208 +1,236 @@ -{-# LANGUAGE RankNTypes #-} +{-# LANGUAGE ScopedTypeVariables #-} +{-# LANGUAGE TypeFamilies #-} + module Development.IDE.Core.ProgressReporting - ( ProgressEvent(..) - , ProgressReporting(..) - , noProgressReporting - , delayedProgressReporting - -- utilities, reexported for use in Core.Shake - , mRunLspT - , mRunLspTCallback - -- for tests - , recordProgress - , InProgressState(..) + ( ProgressEvent (..), + PerFileProgressReporting (..), + ProgressReporting, + noPerFileProgressReporting, + progressReporting, + progressReportingNoTrace, + -- utilities, reexported for use in Core.Shake + mRunLspT, + mRunLspTCallback, + -- for tests + recordProgress, + InProgressState (..), + progressStop, + progressUpdate ) - where - -import Control.Concurrent.Async -import Control.Concurrent.STM.Stats (TVar, atomicallyNamed, - modifyTVar', newTVarIO, - readTVarIO) -import Control.Concurrent.Strict +where + +import Control.Concurrent.STM (STM) +import Control.Concurrent.STM.Stats (TVar, atomically, + atomicallyNamed, modifyTVar', + newTVarIO, readTVar, retry) +import Control.Concurrent.Strict (modifyVar_, newVar, + threadDelay) import Control.Monad.Extra hiding (loop) import Control.Monad.IO.Class import Control.Monad.Trans.Class (lift) -import Data.Aeson (ToJSON (toJSON)) -import Data.Foldable (for_) import Data.Functor (($>)) import qualified Data.Text as T -import Data.Unique import Development.IDE.GHC.Orphans () -import Development.IDE.Graph hiding (ShakeValue) import Development.IDE.Types.Location import Development.IDE.Types.Options import qualified Focus -import Language.LSP.Protocol.Message import Language.LSP.Protocol.Types -import qualified Language.LSP.Protocol.Types as LSP +import Language.LSP.Server (ProgressAmount (..), + ProgressCancellable (..), + withProgress) import qualified Language.LSP.Server as LSP import qualified StmContainers.Map as STM -import System.Time.Extra -import UnliftIO.Exception (bracket_) +import UnliftIO (Async, async, bracket, cancel) data ProgressEvent - = KickStarted - | KickCompleted - -data ProgressReporting = ProgressReporting - { progressUpdate :: ProgressEvent -> IO () - , inProgress :: forall a. NormalizedFilePath -> Action a -> Action a - , progressStop :: IO () + = ProgressNewStarted + | ProgressCompleted + | ProgressStarted + +data ProgressReporting = ProgressReporting + { _progressUpdate :: ProgressEvent -> IO (), + _progressStop :: IO () + -- ^ we are using IO here because creating and stopping the `ProgressReporting` + -- is different from how we use it. } -noProgressReporting :: IO ProgressReporting -noProgressReporting = return $ ProgressReporting - { progressUpdate = const $ pure () - , inProgress = const id - , progressStop = pure () +data PerFileProgressReporting = PerFileProgressReporting + { + inProgress :: forall a. NormalizedFilePath -> IO a -> IO a, + -- ^ see Note [ProgressReporting API and InProgressState] + progressReportingInner :: ProgressReporting } +class ProgressReporter a where + progressUpdate :: a -> ProgressEvent -> IO () + progressStop :: a -> IO () + +instance ProgressReporter ProgressReporting where + progressUpdate = _progressUpdate + progressStop = _progressStop + +instance ProgressReporter PerFileProgressReporting where + progressUpdate = _progressUpdate . progressReportingInner + progressStop = _progressStop . progressReportingInner + +{- Note [ProgressReporting API and InProgressState] +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +The progress of tasks can be tracked in two ways: + +1. `ProgressReporting`: we have an internal state that actively tracks the progress. + Changes to the progress are made directly to this state. + +2. `ProgressReporting`: there is an external state that tracks the progress. + The external state is converted into an STM Int for the purpose of reporting progress. + +The `inProgress` function is only useful when we are using `ProgressReporting`. +-} + +noProgressReporting :: ProgressReporting +noProgressReporting = ProgressReporting + { _progressUpdate = const $ pure (), + _progressStop = pure () + } +noPerFileProgressReporting :: IO PerFileProgressReporting +noPerFileProgressReporting = + return $ + PerFileProgressReporting + { inProgress = const id, + progressReportingInner = noProgressReporting + } + -- | State used in 'delayedProgressReporting' data State - = NotStarted - | Stopped - | Running (Async ()) + = NotStarted + | Stopped + | Running (Async ()) -- | State transitions used in 'delayedProgressReporting' data Transition = Event ProgressEvent | StopProgress -updateState :: IO (Async ()) -> Transition -> State -> IO State -updateState _ _ Stopped = pure Stopped -updateState start (Event KickStarted) NotStarted = Running <$> start -updateState start (Event KickStarted) (Running a) = cancel a >> Running <$> start -updateState _ (Event KickCompleted) (Running a) = cancel a $> NotStarted -updateState _ (Event KickCompleted) st = pure st -updateState _ StopProgress (Running a) = cancel a $> Stopped -updateState _ StopProgress st = pure st +updateState :: IO () -> Transition -> State -> IO State +updateState _ _ Stopped = pure Stopped +updateState start (Event ProgressNewStarted) NotStarted = Running <$> async start +updateState start (Event ProgressNewStarted) (Running job) = cancel job >> Running <$> async start +updateState start (Event ProgressStarted) NotStarted = Running <$> async start +updateState _ (Event ProgressStarted) (Running job) = return (Running job) +updateState _ (Event ProgressCompleted) (Running job) = cancel job $> NotStarted +updateState _ (Event ProgressCompleted) st = pure st +updateState _ StopProgress (Running job) = cancel job $> Stopped +updateState _ StopProgress st = pure st -- | Data structure to track progress across the project -data InProgressState = InProgressState - { todoVar :: TVar Int -- ^ Number of files to do - , doneVar :: TVar Int -- ^ Number of files done - , currentVar :: STM.Map NormalizedFilePath Int - } +-- see Note [ProgressReporting API and InProgressState] +data InProgressState + = InProgressState + { -- | Number of files to do + todoVar :: TVar Int, + -- | Number of files done + doneVar :: TVar Int, + currentVar :: STM.Map NormalizedFilePath Int + } newInProgress :: IO InProgressState newInProgress = InProgressState <$> newTVarIO 0 <*> newTVarIO 0 <*> STM.newIO recordProgress :: InProgressState -> NormalizedFilePath -> (Int -> Int) -> IO () -recordProgress InProgressState{..} file shift = do - (prev, new) <- atomicallyNamed "recordProgress" $ STM.focus alterPrevAndNew file currentVar - atomicallyNamed "recordProgress2" $ do - case (prev,new) of - (Nothing,0) -> modifyTVar' doneVar (+1) >> modifyTVar' todoVar (+1) - (Nothing,_) -> modifyTVar' todoVar (+1) - (Just 0, 0) -> pure () - (Just 0, _) -> modifyTVar' doneVar pred - (Just _, 0) -> modifyTVar' doneVar (+1) - (Just _, _) -> pure() +recordProgress InProgressState {..} file shift = do + (prev, new) <- atomicallyNamed "recordProgress" $ STM.focus alterPrevAndNew file currentVar + atomicallyNamed "recordProgress2" $ case (prev, new) of + (Nothing, 0) -> modifyTVar' doneVar (+ 1) >> modifyTVar' todoVar (+ 1) + (Nothing, _) -> modifyTVar' todoVar (+ 1) + (Just 0, 0) -> pure () + (Just 0, _) -> modifyTVar' doneVar pred + (Just _, 0) -> modifyTVar' doneVar (+ 1) + (Just _, _) -> pure () where alterPrevAndNew = do - prev <- Focus.lookup - Focus.alter alter - new <- Focus.lookupWithDefault 0 - return (prev, new) + prev <- Focus.lookup + Focus.alter alter + new <- Focus.lookupWithDefault 0 + return (prev, new) alter x = let x' = maybe (shift 0) shift x in Just x' --- | A 'ProgressReporting' that enqueues Begin and End notifications in a new --- thread, with a grace period (nothing will be sent if 'KickCompleted' arrives --- before the end of the grace period). -delayedProgressReporting - :: Seconds -- ^ Grace period before starting - -> Seconds -- ^ sampling delay - -> Maybe (LSP.LanguageContextEnv c) - -> ProgressReportingStyle - -> IO ProgressReporting -delayedProgressReporting _before _after Nothing _optProgressStyle = noProgressReporting -delayedProgressReporting before after (Just lspEnv) optProgressStyle = do - inProgressState <- newInProgress - progressState <- newVar NotStarted - let progressUpdate event = updateStateVar $ Event event - progressStop = updateStateVar StopProgress - updateStateVar = modifyVar_ progressState . updateState (lspShakeProgress inProgressState) - - inProgress = updateStateForFile inProgressState - return ProgressReporting{..} - where - lspShakeProgress InProgressState{..} = do - -- first sleep a bit, so we only show progress messages if it's going to take - -- a "noticable amount of time" (we often expect a thread kill to arrive before the sleep finishes) - liftIO $ sleep before - u <- ProgressToken . InR . T.pack . show . hashUnique <$> liftIO newUnique - - b <- liftIO newBarrier - void $ LSP.runLspT lspEnv $ LSP.sendRequest SMethod_WindowWorkDoneProgressCreate - LSP.WorkDoneProgressCreateParams { _token = u } $ liftIO . signalBarrier b - liftIO $ async $ do - ready <- waitBarrier b - LSP.runLspT lspEnv $ for_ ready $ const $ bracket_ (start u) (stop u) (loop u 0) - where - start token = LSP.sendNotification SMethod_Progress $ - LSP.ProgressParams - { _token = token - , _value = toJSON $ WorkDoneProgressBegin - { _kind = AString @"begin" - , _title = "Processing" - , _cancellable = Nothing - , _message = Nothing - , _percentage = Nothing - } - } - stop token = LSP.sendNotification SMethod_Progress - LSP.ProgressParams - { _token = token - , _value = toJSON $ WorkDoneProgressEnd - { _kind = AString @"end" - , _message = Nothing - } - } - loop _ _ | optProgressStyle == NoProgress = - forever $ liftIO $ threadDelay maxBound - loop token prevPct = do - done <- liftIO $ readTVarIO doneVar - todo <- liftIO $ readTVarIO todoVar - liftIO $ sleep after - if todo == 0 then loop token 0 else do - let - nextFrac :: Double - nextFrac = fromIntegral done / fromIntegral todo - nextPct :: UInt - nextPct = floor $ 100 * nextFrac - when (nextPct /= prevPct) $ - LSP.sendNotification SMethod_Progress $ - LSP.ProgressParams - { _token = token - , _value = case optProgressStyle of - Explicit -> toJSON $ WorkDoneProgressReport - { _kind = AString @"report" - , _cancellable = Nothing - , _message = Just $ T.pack $ show done <> "/" <> show todo - , _percentage = Nothing - } - Percentage -> toJSON $ WorkDoneProgressReport - { _kind = AString @"report" - , _cancellable = Nothing - , _message = Nothing - , _percentage = Just nextPct - } - NoProgress -> error "unreachable" - } - loop token nextPct - - updateStateForFile inProgress file = actionBracket (f succ) (const $ f pred) . const - -- This functions are deliberately eta-expanded to avoid space leaks. - -- Do not remove the eta-expansion without profiling a session with at - -- least 1000 modifications. - where - f shift = recordProgress inProgress file shift - -mRunLspT :: Applicative m => Maybe (LSP.LanguageContextEnv c ) -> LSP.LspT c m () -> m () + +-- | `progressReportingNoTrace` initiates a new progress reporting session. +-- It functions similarly to `progressReporting`, but it utilizes an external state for progress tracking. +-- Refer to Note [ProgressReporting API and InProgressState] for more details. +progressReportingNoTrace :: + STM Int -> + STM Int -> + Maybe (LSP.LanguageContextEnv c) -> + T.Text -> + ProgressReportingStyle -> + IO ProgressReporting +progressReportingNoTrace _ _ Nothing _title _optProgressStyle = return noProgressReporting +progressReportingNoTrace todo done (Just lspEnv) title optProgressStyle = do + progressState <- newVar NotStarted + let _progressUpdate event = liftIO $ updateStateVar $ Event event + _progressStop = updateStateVar StopProgress + updateStateVar = modifyVar_ progressState . updateState (progressCounter lspEnv title optProgressStyle todo done) + return ProgressReporting {..} + +-- | `progressReporting` initiates a new progress reporting session. +-- It necessitates the active tracking of progress using the `inProgress` function. +-- Refer to Note [ProgressReporting API and InProgressState] for more details. +progressReporting :: + Maybe (LSP.LanguageContextEnv c) -> + T.Text -> + ProgressReportingStyle -> + IO PerFileProgressReporting +progressReporting Nothing _title _optProgressStyle = noPerFileProgressReporting +progressReporting (Just lspEnv) title optProgressStyle = do + inProgressState <- newInProgress + progressReportingInner <- progressReportingNoTrace (readTVar $ todoVar inProgressState) + (readTVar $ doneVar inProgressState) (Just lspEnv) title optProgressStyle + let + inProgress :: NormalizedFilePath -> IO a -> IO a + inProgress = updateStateForFile inProgressState + return PerFileProgressReporting {..} + where + updateStateForFile inProgress file = UnliftIO.bracket (liftIO $ f succ) (const $ liftIO $ f pred) . const + where + -- This functions are deliberately eta-expanded to avoid space leaks. + -- Do not remove the eta-expansion without profiling a session with at + -- least 1000 modifications. + + f = recordProgress inProgress file + +-- Kill this to complete the progress session +progressCounter :: + LSP.LanguageContextEnv c -> + T.Text -> + ProgressReportingStyle -> + STM Int -> + STM Int -> + IO () +progressCounter lspEnv title optProgressStyle getTodo getDone = + LSP.runLspT lspEnv $ withProgress title Nothing NotCancellable $ \update -> loop update 0 + where + loop _ _ | optProgressStyle == NoProgress = forever $ liftIO $ threadDelay maxBound + loop update prevPct = do + (todo, done, nextPct) <- liftIO $ atomically $ do + todo <- getTodo + done <- getDone + let nextFrac :: Double + nextFrac = if todo == 0 then 0 else fromIntegral done / fromIntegral todo + nextPct :: UInt + nextPct = floor $ 100 * nextFrac + when (nextPct == prevPct) retry + pure (todo, done, nextPct) + + _ <- update (ProgressAmount (Just nextPct) (Just $ T.pack $ show done <> "/" <> show todo)) + loop update nextPct + +mRunLspT :: (Applicative m) => Maybe (LSP.LanguageContextEnv c) -> LSP.LspT c m () -> m () mRunLspT (Just lspEnv) f = LSP.runLspT lspEnv f mRunLspT Nothing _ = pure () -mRunLspTCallback :: Monad m - => Maybe (LSP.LanguageContextEnv c) - -> (LSP.LspT c m a -> LSP.LspT c m a) - -> m a - -> m a +mRunLspTCallback :: + (Monad m) => + Maybe (LSP.LanguageContextEnv c) -> + (LSP.LspT c m a -> LSP.LspT c m a) -> + m a -> + m a mRunLspTCallback (Just lspEnv) f g = LSP.runLspT lspEnv $ f (lift g) mRunLspTCallback Nothing _ g = g diff --git a/ghcide/src/Development/IDE/Core/RuleTypes.hs b/ghcide/src/Development/IDE/Core/RuleTypes.hs index 30251ee8d3..43b80be119 100644 --- a/ghcide/src/Development/IDE/Core/RuleTypes.hs +++ b/ghcide/src/Development/IDE/Core/RuleTypes.hs @@ -2,7 +2,6 @@ -- SPDX-License-Identifier: Apache-2.0 {-# LANGUAGE DerivingStrategies #-} -{-# LANGUAGE FlexibleInstances #-} {-# LANGUAGE GADTs #-} {-# LANGUAGE PatternSynonyms #-} {-# LANGUAGE TemplateHaskell #-} @@ -17,7 +16,7 @@ module Development.IDE.Core.RuleTypes( ) where import Control.DeepSeq -import Control.Exception (assert) +import qualified Control.Exception as E import Control.Lens import Data.Aeson.Types (Value) import Data.Hashable @@ -25,7 +24,8 @@ import qualified Data.Map as M import Data.Time.Clock.POSIX import Data.Typeable import Development.IDE.GHC.Compat hiding - (HieFileResult) + (HieFileResult, + assert) import Development.IDE.GHC.Compat.Util import Development.IDE.GHC.CoreFile import Development.IDE.GHC.Util @@ -36,12 +36,15 @@ import Development.IDE.Types.KnownTargets import GHC.Generics (Generic) import Data.ByteString (ByteString) -import Data.Text (Text) +import Data.Text.Utf16.Rope.Mixed (Rope) import Development.IDE.Import.FindImports (ArtifactsLocation) import Development.IDE.Spans.Common import Development.IDE.Spans.LocalBindings import Development.IDE.Types.Diagnostics +import GHC.Driver.Errors.Types (WarningMessages) import GHC.Serialized (Serialized) +import Ide.Logger (Pretty (..), + viaShow) import Language.LSP.Protocol.Types (Int32, NormalizedFilePath) @@ -71,6 +74,12 @@ type instance RuleResult GetParsedModuleWithComments = ParsedModule type instance RuleResult GetModuleGraph = DependencyInformation +-- | it only compute the fingerprint of the module graph for a file and its dependencies +-- we need this to trigger recompilation when the sub module graph for a file changes +type instance RuleResult GetModuleGraphTransDepsFingerprints = Fingerprint +type instance RuleResult GetModuleGraphTransReverseDepsFingerprints = Fingerprint +type instance RuleResult GetModuleGraphImmediateReverseDepsFingerprints = Fingerprint + data GetKnownTargets = GetKnownTargets deriving (Show, Generic, Eq, Ord) instance Hashable GetKnownTargets @@ -81,7 +90,7 @@ type instance RuleResult GetKnownTargets = KnownTargets type instance RuleResult GenerateCore = ModGuts data GenerateCore = GenerateCore - deriving (Eq, Show, Typeable, Generic) + deriving (Eq, Show, Generic) instance Hashable GenerateCore instance NFData GenerateCore @@ -101,12 +110,12 @@ instance NFData LinkableResult where rnf = rwhnf data GetLinkable = GetLinkable - deriving (Eq, Show, Typeable, Generic) + deriving (Eq, Show, Generic) instance Hashable GetLinkable instance NFData GetLinkable data GetImportMap = GetImportMap - deriving (Eq, Show, Typeable, Generic) + deriving (Eq, Show, Generic) instance Hashable GetImportMap instance NFData GetImportMap @@ -156,6 +165,8 @@ data TcModuleResult = TcModuleResult -- ^ Which modules did we need at runtime while compiling this file? -- Used for recompilation checking in the presence of TH -- Stores the hash of their core file + , tmrWarnings :: WarningMessages + -- ^ Structured warnings for this module. } instance Show TcModuleResult where show = show . pm_mod_summary . tmrParsed @@ -187,9 +198,9 @@ hiFileFingerPrint HiFileResult{..} = hirIfaceFp <> maybe "" snd hirCoreFp mkHiFileResult :: ModSummary -> ModIface -> ModDetails -> ModuleEnv ByteString -> Maybe (CoreFile, ByteString) -> HiFileResult mkHiFileResult hirModSummary hirModIface hirModDetails hirRuntimeModules hirCoreFp = - assert (case hirCoreFp of Just (CoreFile{cf_iface_hash}, _) - -> getModuleHash hirModIface == cf_iface_hash - _ -> True) + E.assert (case hirCoreFp of + Just (CoreFile{cf_iface_hash}, _) -> getModuleHash hirModIface == cf_iface_hash + _ -> True) HiFileResult{..} where hirIfaceFp = fingerprintToBS . getModuleHash $ hirModIface -- will always be two bytes @@ -238,14 +249,14 @@ type instance RuleResult GetHieAst = HieAstResult -- | A IntervalMap telling us what is in scope at each point type instance RuleResult GetBindings = Bindings -data DocAndKindMap = DKMap {getDocMap :: !DocMap, getKindMap :: !KindMap} -instance NFData DocAndKindMap where +data DocAndTyThingMap = DKMap {getDocMap :: !DocMap, getTyThingMap :: !TyThingMap} +instance NFData DocAndTyThingMap where rnf (DKMap a b) = rwhnf a `seq` rwhnf b -instance Show DocAndKindMap where +instance Show DocAndTyThingMap where show = const "docmap" -type instance RuleResult GetDocMap = DocAndKindMap +type instance RuleResult GetDocMap = DocAndTyThingMap -- | A GHC session that we reuse. type instance RuleResult GhcSession = HscEnvEq @@ -274,10 +285,12 @@ type instance RuleResult GetModIfaceFromDiskAndIndex = HiFileResult type instance RuleResult GetModIface = HiFileResult -- | Get the contents of a file, either dirty (if the buffer is modified) or Nothing to mean use from disk. -type instance RuleResult GetFileContents = (FileVersion, Maybe Text) +type instance RuleResult GetFileContents = (FileVersion, Maybe Rope) type instance RuleResult GetFileExists = Bool +type instance RuleResult GetFileHash = Fingerprint + type instance RuleResult AddWatchedFile = Bool @@ -328,21 +341,30 @@ instance Hashable GetFileContents instance NFData GetFileContents data GetFileExists = GetFileExists - deriving (Eq, Show, Typeable, Generic) + deriving (Eq, Show, Generic) instance NFData GetFileExists instance Hashable GetFileExists +data GetFileHash = GetFileHash + deriving (Eq, Show, Generic) + +instance NFData GetFileHash +instance Hashable GetFileHash + data FileOfInterestStatus = OnDisk | Modified { firstOpen :: !Bool -- ^ was this file just opened } - deriving (Eq, Show, Typeable, Generic) + deriving (Eq, Show, Generic) instance Hashable FileOfInterestStatus instance NFData FileOfInterestStatus +instance Pretty FileOfInterestStatus where + pretty = viaShow + data IsFileOfInterestResult = NotFOI | IsFOI FileOfInterestStatus - deriving (Eq, Show, Typeable, Generic) + deriving (Eq, Show, Generic) instance Hashable IsFileOfInterestResult instance NFData IsFileOfInterestResult @@ -374,17 +396,17 @@ type instance RuleResult GetModSummary = ModSummaryResult type instance RuleResult GetModSummaryWithoutTimestamps = ModSummaryResult data GetParsedModule = GetParsedModule - deriving (Eq, Show, Typeable, Generic) + deriving (Eq, Show, Generic) instance Hashable GetParsedModule instance NFData GetParsedModule data GetParsedModuleWithComments = GetParsedModuleWithComments - deriving (Eq, Show, Typeable, Generic) + deriving (Eq, Show, Generic) instance Hashable GetParsedModuleWithComments instance NFData GetParsedModuleWithComments data GetLocatedImports = GetLocatedImports - deriving (Eq, Show, Typeable, Generic) + deriving (Eq, Show, Generic) instance Hashable GetLocatedImports instance NFData GetLocatedImports @@ -392,42 +414,57 @@ instance NFData GetLocatedImports type instance RuleResult NeedsCompilation = Maybe LinkableType data NeedsCompilation = NeedsCompilation - deriving (Eq, Show, Typeable, Generic) + deriving (Eq, Show, Generic) instance Hashable NeedsCompilation instance NFData NeedsCompilation data GetModuleGraph = GetModuleGraph - deriving (Eq, Show, Typeable, Generic) + deriving (Eq, Show, Generic) instance Hashable GetModuleGraph instance NFData GetModuleGraph +data GetModuleGraphTransDepsFingerprints = GetModuleGraphTransDepsFingerprints + deriving (Eq, Show, Generic) +instance Hashable GetModuleGraphTransDepsFingerprints +instance NFData GetModuleGraphTransDepsFingerprints + +data GetModuleGraphTransReverseDepsFingerprints = GetModuleGraphTransReverseDepsFingerprints + deriving (Eq, Show, Generic) +instance Hashable GetModuleGraphTransReverseDepsFingerprints +instance NFData GetModuleGraphTransReverseDepsFingerprints + +data GetModuleGraphImmediateReverseDepsFingerprints = GetModuleGraphImmediateReverseDepsFingerprints + deriving (Eq, Show, Generic) +instance Hashable GetModuleGraphImmediateReverseDepsFingerprints +instance NFData GetModuleGraphImmediateReverseDepsFingerprints + data ReportImportCycles = ReportImportCycles - deriving (Eq, Show, Typeable, Generic) + deriving (Eq, Show, Generic) instance Hashable ReportImportCycles instance NFData ReportImportCycles data TypeCheck = TypeCheck - deriving (Eq, Show, Typeable, Generic) + deriving (Eq, Show, Generic) instance Hashable TypeCheck instance NFData TypeCheck data GetDocMap = GetDocMap - deriving (Eq, Show, Typeable, Generic) + deriving (Eq, Show, Generic) instance Hashable GetDocMap instance NFData GetDocMap data GetHieAst = GetHieAst - deriving (Eq, Show, Typeable, Generic) + deriving (Eq, Show, Generic) instance Hashable GetHieAst instance NFData GetHieAst data GetBindings = GetBindings - deriving (Eq, Show, Typeable, Generic) + deriving (Eq, Show, Generic) instance Hashable GetBindings instance NFData GetBindings data GhcSession = GhcSession - deriving (Eq, Show, Typeable, Generic) + deriving (Eq, Show, Generic) instance Hashable GhcSession instance NFData GhcSession @@ -436,7 +473,7 @@ newtype GhcSessionDeps = GhcSessionDeps_ -- Required for interactive evaluation, but leads to more cache invalidations fullModSummary :: Bool } - deriving newtype (Eq, Typeable, Hashable, NFData) + deriving newtype (Eq, Hashable, NFData) instance Show GhcSessionDeps where show (GhcSessionDeps_ False) = "GhcSessionDeps" @@ -446,45 +483,45 @@ pattern GhcSessionDeps :: GhcSessionDeps pattern GhcSessionDeps = GhcSessionDeps_ False data GetModIfaceFromDisk = GetModIfaceFromDisk - deriving (Eq, Show, Typeable, Generic) + deriving (Eq, Show, Generic) instance Hashable GetModIfaceFromDisk instance NFData GetModIfaceFromDisk data GetModIfaceFromDiskAndIndex = GetModIfaceFromDiskAndIndex - deriving (Eq, Show, Typeable, Generic) + deriving (Eq, Show, Generic) instance Hashable GetModIfaceFromDiskAndIndex instance NFData GetModIfaceFromDiskAndIndex data GetModIface = GetModIface - deriving (Eq, Show, Typeable, Generic) + deriving (Eq, Show, Generic) instance Hashable GetModIface instance NFData GetModIface data IsFileOfInterest = IsFileOfInterest - deriving (Eq, Show, Typeable, Generic) + deriving (Eq, Show, Generic) instance Hashable IsFileOfInterest instance NFData IsFileOfInterest data GetModSummaryWithoutTimestamps = GetModSummaryWithoutTimestamps - deriving (Eq, Show, Typeable, Generic) + deriving (Eq, Show, Generic) instance Hashable GetModSummaryWithoutTimestamps instance NFData GetModSummaryWithoutTimestamps data GetModSummary = GetModSummary - deriving (Eq, Show, Typeable, Generic) + deriving (Eq, Show, Generic) instance Hashable GetModSummary instance NFData GetModSummary -- See Note [Client configuration in Rules] -- | Get the client config stored in the ide state data GetClientSettings = GetClientSettings - deriving (Eq, Show, Typeable, Generic) + deriving (Eq, Show, Generic) instance Hashable GetClientSettings instance NFData GetClientSettings type instance RuleResult GetClientSettings = Hashed (Maybe Value) -data AddWatchedFile = AddWatchedFile deriving (Eq, Show, Typeable, Generic) +data AddWatchedFile = AddWatchedFile deriving (Eq, Show, Generic) instance Hashable AddWatchedFile instance NFData AddWatchedFile @@ -504,7 +541,7 @@ data IdeGhcSession = IdeGhcSession instance Show IdeGhcSession where show _ = "IdeGhcSession" instance NFData IdeGhcSession where rnf !_ = () -data GhcSessionIO = GhcSessionIO deriving (Eq, Show, Typeable, Generic) +data GhcSessionIO = GhcSessionIO deriving (Eq, Show, Generic) instance Hashable GhcSessionIO instance NFData GhcSessionIO @@ -513,6 +550,7 @@ makeLensesWith ''Splices {- Note [Client configuration in Rules] + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ The LSP client configuration is stored by `lsp` for us, and is accesible in handlers through the LspT monad. diff --git a/ghcide/src/Development/IDE/Core/Rules.hs b/ghcide/src/Development/IDE/Core/Rules.hs index 7cc89ce170..f1b11d971b 100644 --- a/ghcide/src/Development/IDE/Core/Rules.hs +++ b/ghcide/src/Development/IDE/Core/Rules.hs @@ -3,7 +3,6 @@ {-# LANGUAGE CPP #-} {-# LANGUAGE DuplicateRecordFields #-} -{-# LANGUAGE FlexibleInstances #-} {-# LANGUAGE TypeFamilies #-} -- | A Shake implementation of the compiler service, built @@ -12,11 +11,8 @@ module Development.IDE.Core.Rules( -- * Types IdeState, GetParsedModule(..), TransitiveDependencies(..), - Priority(..), GhcSessionIO(..), GetClientSettings(..), + GhcSessionIO(..), GetClientSettings(..), -- * Functions - priorityTypeCheck, - priorityGenerateCore, - priorityFilesOfInterest, runAction, toIdeResult, defineNoFile, @@ -27,6 +23,7 @@ module Development.IDE.Core.Rules( getParsedModuleWithComments, getClientConfigAction, usePropertyAction, + usePropertyByPathAction, getHieFile, -- * Rules CompiledLinkables(..), @@ -46,7 +43,6 @@ module Development.IDE.Core.Rules( getHieAstsRule, getBindingsRule, needsCompilationRule, - computeLinkableTypeForDynFlags, generateCoreRule, getImportMapRule, regenerateHiFile, @@ -61,16 +57,18 @@ module Development.IDE.Core.Rules( DisplayTHWarning(..), ) where -import Prelude hiding (mod) import Control.Applicative -import Control.Concurrent.Async (concurrently) +import Control.Concurrent.STM.Stats (atomically) +import Control.Concurrent.STM.TVar import Control.Concurrent.Strict import Control.DeepSeq -import Control.Exception.Safe import Control.Exception (evaluate) -import Control.Monad.Extra hiding (msum) -import Control.Monad.Reader hiding (msum) -import Control.Monad.State hiding (msum) +import Control.Exception.Safe +import Control.Lens ((%~), (&), (.~)) +import Control.Monad.Extra +import Control.Monad.IO.Unlift +import Control.Monad.Reader +import Control.Monad.State import Control.Monad.Trans.Except (ExceptT, except, runExceptT) import Control.Monad.Trans.Maybe @@ -79,44 +77,52 @@ import qualified Data.Binary as B import qualified Data.ByteString as BS import qualified Data.ByteString.Lazy as LBS import Data.Coerce -import Data.Foldable hiding (msum) +import Data.Default (Default, def) +import Data.Foldable +import Data.Hashable import qualified Data.HashMap.Strict as HM import qualified Data.HashSet as HashSet -import Data.Hashable -import Data.IORef -import Control.Concurrent.STM.TVar import Data.IntMap.Strict (IntMap) import qualified Data.IntMap.Strict as IntMap +import Data.IORef import Data.List import Data.List.Extra (nubOrdOn) import qualified Data.Map as M import Data.Maybe import Data.Proxy -import qualified Data.Text.Utf16.Rope as Rope -import qualified Data.Set as Set import qualified Data.Text as T import qualified Data.Text.Encoding as T +import qualified Data.Text.Utf16.Rope.Mixed as Rope import Data.Time (UTCTime (..)) +import Data.Time.Clock.POSIX (posixSecondsToUTCTime) import Data.Tuple.Extra import Data.Typeable (cast) import Development.IDE.Core.Compile -import Development.IDE.Core.FileExists hiding (LogShake, Log) +import Development.IDE.Core.FileExists hiding (Log, + LogShake) import Development.IDE.Core.FileStore (getFileContents, + getFileModTimeContents, getModTime) import Development.IDE.Core.IdeConfiguration -import Development.IDE.Core.OfInterest hiding (LogShake, Log) +import Development.IDE.Core.OfInterest hiding (Log, + LogShake) import Development.IDE.Core.PositionMapping import Development.IDE.Core.RuleTypes -import Development.IDE.Core.Service hiding (LogShake, Log) -import Development.IDE.Core.Shake hiding (Log) -import Development.IDE.GHC.Compat.Env +import Development.IDE.Core.Service hiding (Log, + LogShake) +import Development.IDE.Core.Shake hiding (Log) +import qualified Development.IDE.Core.Shake as Shake import Development.IDE.GHC.Compat hiding - (vcat, nest, parseModule, - TargetId(..), - loadInterface, + (TargetId (..), Var, - (<+>), settings) -import qualified Development.IDE.GHC.Compat as Compat hiding (vcat, nest) + loadInterface, + nest, + parseModule, + settings, vcat, + (<+>)) +import qualified Development.IDE.GHC.Compat as Compat hiding + (nest, + vcat) import qualified Development.IDE.GHC.Compat.Util as Util import Development.IDE.GHC.Error import Development.IDE.GHC.Util hiding @@ -131,47 +137,42 @@ import Development.IDE.Types.Diagnostics as Diag import Development.IDE.Types.HscEnvEq import Development.IDE.Types.Location import Development.IDE.Types.Options +import qualified Development.IDE.Types.Shake as Shake import qualified GHC.LanguageExtensions as LangExt +import HIE.Bios.Ghc.Gap (hostIsDynamic) import qualified HieDb +import Ide.Logger (Pretty (pretty), + Recorder, + WithPriority, + cmapWithPrio, + logWith, nest, + vcat, (<+>)) +import qualified Ide.Logger as Logger import Ide.Plugin.Config -import qualified Language.LSP.Server as LSP -import Language.LSP.Protocol.Types (ShowMessageParams (ShowMessageParams), MessageType (MessageType_Info)) -import Language.LSP.Protocol.Message (SMethod (SMethod_CustomMethod, SMethod_WindowShowMessage)) -import Language.LSP.VFS -import System.Directory (makeAbsolute, doesFileExist) -import Data.Default (def, Default) import Ide.Plugin.Properties (HasProperty, + HasPropertyByPath, + KeyNamePath, KeyNameProxy, Properties, ToHsType, - useProperty) + useProperty, + usePropertyByPath) import Ide.Types (DynFlagsModifications (dynFlagsModifyGlobal, dynFlagsModifyParser), PluginId) -import Control.Concurrent.STM.Stats (atomically) -import Language.LSP.Server (LspT) -import System.Info.Extra (isWindows) -import HIE.Bios.Ghc.Gap (hostIsDynamic) -import Ide.Logger (Recorder, logWith, cmapWithPrio, WithPriority, Pretty (pretty), (<+>), nest, vcat) -import qualified Development.IDE.Core.Shake as Shake -import qualified Ide.Logger as Logger -import qualified Development.IDE.Types.Shake as Shake -import Data.Time.Clock.POSIX (posixSecondsToUTCTime) -import Control.Monad.IO.Unlift - - -import GHC.Fingerprint - --- See Note [Guidelines For Using CPP In GHCIDE Import Statements] - -#if !MIN_VERSION_ghc(9,3,0) -import GHC (mgModSummaries) -#endif - -#if MIN_VERSION_ghc(9,3,0) -import qualified Data.IntMap as IM -#endif +import qualified Language.LSP.Protocol.Lens as JL +import Language.LSP.Protocol.Message (SMethod (SMethod_CustomMethod, SMethod_WindowShowMessage)) +import Language.LSP.Protocol.Types (MessageType (MessageType_Info), + ShowMessageParams (ShowMessageParams)) +import Language.LSP.Server (LspT) +import qualified Language.LSP.Server as LSP +import Language.LSP.VFS +import Prelude hiding (mod) +import System.Directory (doesFileExist) +import System.Info.Extra (isWindows) +import qualified Data.IntMap as IM +import GHC.Fingerprint data Log = LogShake Shake.Log @@ -217,12 +218,15 @@ toIdeResult = either (, Nothing) (([],) . Just) ------------------------------------------------------------ -- Exposed API ------------------------------------------------------------ + +-- TODO: rename +-- TODO: return text --> return rope getSourceFileSource :: NormalizedFilePath -> Action BS.ByteString getSourceFileSource nfp = do - (_, msource) <- getFileContents nfp + msource <- getFileContents nfp case msource of Nothing -> liftIO $ BS.readFile (fromNormalizedFilePath nfp) - Just source -> pure $ T.encodeUtf8 source + Just source -> pure $ T.encodeUtf8 $ Rope.toText source -- | Parse the contents of a haskell file. getParsedModule :: NormalizedFilePath -> Action (Maybe ParsedModule) @@ -237,15 +241,6 @@ getParsedModuleWithComments = use GetParsedModuleWithComments -- Rules -- These typically go from key to value and are oracles. -priorityTypeCheck :: Priority -priorityTypeCheck = Priority 0 - -priorityGenerateCore :: Priority -priorityGenerateCore = Priority (-1) - -priorityFilesOfInterest :: Priority -priorityFilesOfInterest = Priority (-2) - -- | WARNING: -- We currently parse the module both with and without Opt_Haddock, and -- return the one with Haddocks if it -- succeeds. However, this may not work @@ -267,40 +262,7 @@ getParsedModuleRule recorder = -- We still parse with Haddocks whether Opt_Haddock is True or False to collect information -- but we no longer need to parse with and without Haddocks separately for above GHC90. - res@(_,pmod) <- if Compat.ghcVersion >= Compat.GHC90 then - liftIO $ (fmap.fmap.fmap) reset_ms $ getParsedModuleDefinition hsc opt file (withOptHaddock ms) - else do - let dflags = ms_hspp_opts ms - mainParse = getParsedModuleDefinition hsc opt file ms - - -- Parse again (if necessary) to capture Haddock parse errors - if gopt Opt_Haddock dflags - then - liftIO $ (fmap.fmap.fmap) reset_ms mainParse - else do - let haddockParse = getParsedModuleDefinition hsc opt file (withOptHaddock ms) - - -- parse twice, with and without Haddocks, concurrently - -- we cannot ignore Haddock parse errors because files of - -- non-interest are always parsed with Haddocks - -- If we can parse Haddocks, might as well use them - ((diags,res),(diagsh,resh)) <- liftIO $ (fmap.fmap.fmap.fmap) reset_ms $ concurrently mainParse haddockParse - - -- Merge haddock and regular diagnostics so we can always report haddock - -- parse errors - let diagsM = mergeParseErrorsHaddock diags diagsh - case resh of - Just _ - | HaddockParse <- optHaddockParse opt - -> pure (diagsM, resh) - -- If we fail to parse haddocks, report the haddock diagnostics as well and - -- return the non-haddock parse. - -- This seems to be the correct behaviour because the Haddock flag is added - -- by us and not the user, so our IDE shouldn't stop working because of it. - _ -> pure (diagsM, res) - -- Add dependencies on included files - _ <- uses GetModificationTime $ map toNormalizedFilePath' (maybe [] pm_extra_src_files pmod) - pure res + liftIO $ (fmap.fmap.fmap) reset_ms $ getParsedModuleDefinition hsc opt file (withOptHaddock ms) withOptHaddock :: ModSummary -> ModSummary withOptHaddock = withOption Opt_Haddock @@ -311,18 +273,6 @@ withOption opt ms = ms{ms_hspp_opts= gopt_set (ms_hspp_opts ms) opt} withoutOption :: GeneralFlag -> ModSummary -> ModSummary withoutOption opt ms = ms{ms_hspp_opts= gopt_unset (ms_hspp_opts ms) opt} --- | Given some normal parse errors (first) and some from Haddock (second), merge them. --- Ignore Haddock errors that are in both. Demote Haddock-only errors to warnings. -mergeParseErrorsHaddock :: [FileDiagnostic] -> [FileDiagnostic] -> [FileDiagnostic] -mergeParseErrorsHaddock normal haddock = normal ++ - [ (a,b,c{_severity = Just DiagnosticSeverity_Warning, _message = fixMessage $ _message c}) - | (a,b,c) <- haddock, Diag._range c `Set.notMember` locations] - where - locations = Set.fromList $ map (Diag._range . thd3) normal - - fixMessage x | "parse error " `T.isPrefixOf` x = "Haddock " <> x - | otherwise = "Haddock: " <> x - -- | This rule provides a ParsedModule preserving all annotations, -- including keywords, punctuation and comments. -- So it is suitable for use cases where you need a perfect edit. @@ -364,22 +314,14 @@ getLocatedImportsRule :: Recorder (WithPriority Log) -> Rules () getLocatedImportsRule recorder = define (cmapWithPrio LogShake recorder) $ \GetLocatedImports file -> do ModSummaryResult{msrModSummary = ms} <- use_ GetModSummaryWithoutTimestamps file - targets <- useNoFile_ GetKnownTargets - let targetsMap = HM.mapWithKey const targets + (KnownTargets targets targetsMap) <- useNoFile_ GetKnownTargets let imports = [(False, imp) | imp <- ms_textual_imps ms] ++ [(True, imp) | imp <- ms_srcimps ms] env_eq <- use_ GhcSession file - let env = hscEnvWithImportPaths env_eq - let import_dirs = deps env_eq + let env = hscEnv env_eq + let import_dirs = map (second homeUnitEnv_dflags) $ hugElts $ hsc_HUG env let dflags = hsc_dflags env - isImplicitCradle = isNothing $ envImportPaths env_eq - dflags' <- return $ if isImplicitCradle - then addRelativeImport file (moduleName $ ms_mod ms) dflags - else dflags opt <- getIdeOptions let getTargetFor modName nfp - | isImplicitCradle = do - itExists <- getFileExists nfp - return $ if itExists then Just nfp else Nothing | Just (TargetFile nfp') <- HM.lookup (TargetFile nfp) targetsMap = do -- reuse the existing NormalizedFilePath in order to maximize sharing itExists <- getFileExists nfp' @@ -390,10 +332,11 @@ getLocatedImportsRule recorder = nfp' = HM.lookupDefault nfp nfp ttmap itExists <- getFileExists nfp' return $ if itExists then Just nfp' else Nothing - | otherwise - = return Nothing + | otherwise = do + itExists <- getFileExists nfp + return $ if itExists then Just nfp else Nothing (diags, imports') <- fmap unzip $ forM imports $ \(isSource, (mbPkgName, modName)) -> do - diagOrImp <- locateModule (hscSetFlags dflags' env) import_dirs (optExtensions opt) getTargetFor modName mbPkgName isSource + diagOrImp <- locateModule (hscSetFlags dflags env) import_dirs (optExtensions opt) getTargetFor modName mbPkgName isSource case diagOrImp of Left diags -> pure (diags, Just (modName, Nothing)) Right (FileImport path) -> pure ([], Just (modName, Just path)) @@ -440,16 +383,16 @@ rawDependencyInformation fs = do go :: NormalizedFilePath -- ^ Current module being processed -> Maybe ModSummary -- ^ ModSummary of the module -> RawDepM FilePathId - go f msum = do + go f mbModSum = do -- First check to see if we have already processed the FilePath -- If we have, just return its Id but don't update any of the state. -- Otherwise, we need to process its imports. checkAlreadyProcessed f $ do - let al = modSummaryToArtifactsLocation f msum + let al = modSummaryToArtifactsLocation f mbModSum -- Get a fresh FilePathId for the new file fId <- getFreshFid al -- Record this module and its location - whenJust msum $ \ms -> + whenJust mbModSum $ \ms -> modifyRawDepInfo (\rd -> rd { rawModuleMap = IntMap.insert (getFilePathId fId) (ShowableModule $ ms_mod ms) (rawModuleMap rd)}) @@ -527,7 +470,7 @@ rawDependencyInformation fs = do reportImportCyclesRule :: Recorder (WithPriority Log) -> Rules () reportImportCyclesRule recorder = defineEarlyCutoff (cmapWithPrio LogShake recorder) $ Rule $ \ReportImportCycles file -> fmap (\errs -> if null errs then (Just "1",([], Just ())) else (Nothing, (errs, Nothing))) $ do - DependencyInformation{..} <- useNoFile_ GetModuleGraph + DependencyInformation{..} <- useWithSeparateFingerprintRule_ GetModuleGraphTransDepsFingerprints GetModuleGraph file case pathToId depPathIdMap file of -- The header of the file does not parse, so it can't be part of any import cycles. Nothing -> pure [] @@ -538,23 +481,15 @@ reportImportCyclesRule recorder = let cycles = mapMaybe (cycleErrorInFile fileId) (toList errs) -- Convert cycles of files into cycles of module names forM cycles $ \(imp, files) -> do - modNames <- forM files $ + modNames <- forM files $ getModuleName . idToPath depPathIdMap pure $ toDiag imp $ sort modNames where cycleErrorInFile f (PartOfCycle imp fs) | f `elem` fs = Just (imp, fs) cycleErrorInFile _ _ = Nothing - toDiag imp mods = (fp , ShowDiag , ) $ Diagnostic - { _range = rng - , _severity = Just DiagnosticSeverity_Error - , _source = Just "Import cycle detection" - , _message = "Cyclic module dependency between " <> showCycle mods - , _code = Nothing - , _relatedInformation = Nothing - , _tags = Nothing - , _codeDescription = Nothing - , _data_ = Nothing - } + toDiag imp mods = + ideErrorWithSource (Just "Import cycle detection") (Just DiagnosticSeverity_Error) fp ("Cyclic module dependency between " <> showCycle mods) Nothing + & fdLspDiagnosticL %~ JL.range .~ rng where rng = fromMaybe noRange $ srcSpanToRange (getLoc imp) fp = toNormalizedFilePath' $ fromMaybe noFilePath $ srcSpanToFilename (getLoc imp) getModuleName file = do @@ -576,14 +511,19 @@ persistentHieFileRule recorder = addPersistentRule GetHieAst $ \file -> runMaybe vfsData <- liftIO $ _vfsMap <$> readTVarIO vfsRef (currentSource, ver) <- liftIO $ case M.lookup (filePathToUri' file) vfsData of Nothing -> (,Nothing) . T.decodeUtf8 <$> BS.readFile (fromNormalizedFilePath file) - Just vf -> pure (Rope.toText $ _file_text vf, Just $ _lsp_version vf) + Just vf -> pure (virtualFileText vf, Just $ virtualFileVersion vf) let refmap = Compat.generateReferencesMap . Compat.getAsts . Compat.hie_asts $ res del = deltaFromDiff (T.decodeUtf8 $ Compat.hie_hs_src res) currentSource pure (HAR (Compat.hie_module res) (Compat.hie_asts res) refmap mempty (HieFromDisk res),del,ver) getHieAstRuleDefinition :: NormalizedFilePath -> HscEnv -> TcModuleResult -> Action (IdeResult HieAstResult) getHieAstRuleDefinition f hsc tmr = do - (diags, masts) <- liftIO $ generateHieAsts hsc tmr + (diags, masts') <- liftIO $ generateHieAsts hsc tmr +#if MIN_VERSION_ghc(9,11,0) + let masts = fst <$> masts' +#else + let masts = masts' +#endif se <- getShakeExtras isFoi <- use_ IsFileOfInterest f @@ -593,11 +533,11 @@ getHieAstRuleDefinition f hsc tmr = do LSP.sendNotification (SMethod_CustomMethod (Proxy @"ghcide/reference/ready")) $ toJSON $ fromNormalizedFilePath f pure [] - _ | Just asts <- masts -> do + _ | Just asts <- masts' -> do source <- getSourceFileSource f let exports = tcg_exports $ tmrTypechecked tmr - msum = tmrModSummary tmr - liftIO $ writeAndIndexHieFile hsc se msum f exports asts source + modSummary = tmrModSummary tmr + liftIO $ writeAndIndexHieFile hsc se modSummary f exports asts source _ -> pure [] let refmap = Compat.generateReferencesMap . Compat.getAsts <$> masts @@ -666,7 +606,7 @@ typeCheckRule recorder = define (cmapWithPrio LogShake recorder) $ \TypeCheck fi -- very expensive. when (foi == NotFOI) $ logWith recorder Logger.Warning $ LogTypecheckedFOI file - typeCheckRuleDefinition hsc pm + typeCheckRuleDefinition hsc pm file knownFilesRule :: Recorder (WithPriority Log) -> Rules () knownFilesRule recorder = defineEarlyCutOffNoFile (cmapWithPrio LogShake recorder) $ \GetKnownTargets -> do @@ -674,6 +614,13 @@ knownFilesRule recorder = defineEarlyCutOffNoFile (cmapWithPrio LogShake recorde fs <- knownTargets pure (LBS.toStrict $ B.encode $ hash fs, unhashed fs) +getFileHashRule :: Recorder (WithPriority Log) -> Rules () +getFileHashRule recorder = + defineEarlyCutoff (cmapWithPrio LogShake recorder) $ Rule $ \GetFileHash file -> do + void $ use_ GetModificationTime file + fileHash <- liftIO $ Util.getFileHash (fromNormalizedFilePath file) + return (Just (fingerprintToBS fileHash), ([], Just fileHash)) + getModuleGraphRule :: Recorder (WithPriority Log) -> Rules () getModuleGraphRule recorder = defineEarlyCutOffNoFile (cmapWithPrio LogShake recorder) $ \GetModuleGraph -> do fs <- toKnownFiles <$> useNoFile_ GetKnownTargets @@ -685,7 +632,6 @@ dependencyInfoForFiles fs = do let (all_fs, _all_ids) = unzip $ HM.toList $ pathToIdMap $ rawPathIdMap rawDepInfo msrs <- uses GetModSummaryWithoutTimestamps all_fs let mss = map (fmap msrModSummary) msrs -#if MIN_VERSION_ghc(9,3,0) let deps = map (\i -> IM.lookup (getFilePathId i) (rawImports rawDepInfo)) _all_ids nodeKeys = IM.fromList $ catMaybes $ zipWith (\fi mms -> (getFilePathId fi,) . NodeKey_Module . msKey <$> mms) _all_ids mss mns = catMaybes $ zipWith go mss deps @@ -695,15 +641,10 @@ dependencyInfoForFiles fs = do go (Just ms) _ = Just $ ModuleNode [] ms go _ _ = Nothing mg = mkModuleGraph mns -#else - let mg = mkModuleGraph $ - -- We don't do any instantiation for backpack at this point of time, so it is OK to use - -- 'extendModSummaryNoDeps'. - -- This may have to change in the future. - map extendModSummaryNoDeps $ - (catMaybes mss) -#endif - pure (fingerprintToBS $ Util.fingerprintFingerprints $ map (maybe fingerprint0 msrFingerprint) msrs, processDependencyInformation rawDepInfo bm mg) + let shallowFingers = IntMap.fromList $ foldr' (\(i, m) acc -> case m of + Just x -> (getFilePathId i,msrFingerprint x):acc + Nothing -> acc) [] $ zip _all_ids msrs + pure (fingerprintToBS $ Util.fingerprintFingerprints $ map (maybe fingerprint0 msrFingerprint) msrs, processDependencyInformation rawDepInfo bm mg shallowFingers) -- This is factored out so it can be directly called from the GetModIface -- rule. Directly calling this rule means that on the initial load we can @@ -712,14 +653,15 @@ dependencyInfoForFiles fs = do typeCheckRuleDefinition :: HscEnv -> ParsedModule + -> NormalizedFilePath -> Action (IdeResult TcModuleResult) -typeCheckRuleDefinition hsc pm = do - setPriority priorityTypeCheck +typeCheckRuleDefinition hsc pm fp = do IdeOptions { optDefer = defer } <- getIdeOptions unlift <- askUnliftIO let dets = TypecheckHelpers { getLinkables = unliftIO unlift . uses_ GetLinkable + , getModuleGraph = unliftIO unlift $ useWithSeparateFingerprintRule_ GetModuleGraphTransDepsFingerprints GetModuleGraph fp } addUsageDependencies $ liftIO $ typecheckModule defer hsc dets pm @@ -746,20 +688,31 @@ loadGhcSession recorder ghcSessionDepsConfig = do defineEarlyCutOffNoFile (cmapWithPrio LogShake recorder) $ \GhcSessionIO -> do alwaysRerun opts <- getIdeOptions + config <- getClientConfigAction res <- optGhcSession opts - let fingerprint = LBS.toStrict $ B.encode $ hash (sessionVersion res) + let fingerprint = LBS.toStrict $ LBS.concat + [ B.encode (hash (sessionVersion res)) + -- When the session version changes, reload all session + -- hsc env sessions + , B.encode (show (sessionLoading config)) + -- The loading config affects session loading. + -- Invalidate all build nodes. + -- Changing the session loading config will increment + -- the 'sessionVersion', thus we don't generate the same fingerprint + -- twice by accident. + ] return (fingerprint, res) defineEarlyCutoff (cmapWithPrio LogShake recorder) $ Rule $ \GhcSession file -> do IdeGhcSession{loadSessionFun} <- useNoFile_ GhcSessionIO + -- loading is always returning a absolute path now (val,deps) <- liftIO $ loadSessionFun $ fromNormalizedFilePath file -- add the deps to the Shake graph let addDependency fp = do -- VSCode uses absolute paths in its filewatch notifications - afp <- liftIO $ makeAbsolute fp - let nfp = toNormalizedFilePath' afp + let nfp = toNormalizedFilePath' fp itExists <- getFileExists nfp when itExists $ void $ do use_ GetModificationTime nfp @@ -781,6 +734,7 @@ instance Default GhcSessionDepsConfig where } -- | Note [GhcSessionDeps] +-- ~~~~~~~~~~~~~~~~~~~~~ -- For a file 'Foo', GhcSessionDeps "Foo.hs" results in an HscEnv which includes -- 1. HomeModInfo's (in the HUG/HPT) for all modules in the transitive closure of "Foo", **NOT** including "Foo" itself. -- 2. ModSummary's (in the ModuleGraph) for all modules in the transitive closure of "Foo", including "Foo" itself. @@ -804,12 +758,12 @@ ghcSessionDepsDefinition fullModSummary GhcSessionDepsConfig{..} env file = do depSessions <- map hscEnv <$> uses_ (GhcSessionDeps_ fullModSummary) deps ifaces <- uses_ GetModIface deps let inLoadOrder = map (\HiFileResult{..} -> HomeModInfo hirModIface hirModDetails emptyHomeModInfoLinkable) ifaces + de <- useWithSeparateFingerprintRule_ GetModuleGraphTransDepsFingerprints GetModuleGraph file mg <- do if fullModuleGraph - then depModuleGraph <$> useNoFile_ GetModuleGraph + then return $ depModuleGraph de else do let mgs = map hsc_mod_graph depSessions -#if MIN_VERSION_ghc(9,3,0) -- On GHC 9.4+, the module graph contains not only ModSummary's but each `ModuleNode` in the graph -- also points to all the direct descendants of the current module. To get the keys for the descendants -- we must get their `ModSummary`s @@ -818,17 +772,9 @@ ghcSessionDepsDefinition fullModSummary GhcSessionDepsConfig{..} env file = do return $!! map (NodeKey_Module . msKey) dep_mss let module_graph_nodes = nubOrdOn mkNodeKey (ModuleNode final_deps ms : concatMap mgModSummaries' mgs) -#else - let module_graph_nodes = - -- We don't do any instantiation for backpack at this point of time, so it is OK to use - -- 'extendModSummaryNoDeps'. - -- This may have to change in the future. - map extendModSummaryNoDeps $ - nubOrdOn ms_mod (ms : concatMap mgModSummaries mgs) -#endif liftIO $ evaluate $ liftRnf rwhnf module_graph_nodes return $ mkModuleGraph module_graph_nodes - session' <- liftIO $ mergeEnvs hsc mg ms inLoadOrder depSessions + session' <- liftIO $ mergeEnvs hsc mg de ms inLoadOrder depSessions -- Here we avoid a call to to `newHscEnvEqWithImportPaths`, which creates a new -- ExportsMap when it is called. We only need to create the ExportsMap once per @@ -851,15 +797,17 @@ getModIfaceFromDiskRule recorder = defineEarlyCutoff (cmapWithPrio LogShake reco let m_old = case old of Shake.Succeeded (Just old_version) v -> Just (v, old_version) Shake.Stale _ (Just old_version) v -> Just (v, old_version) - _ -> Nothing + _ -> Nothing recompInfo = RecompilationInfo { source_version = ver , old_value = m_old , get_file_version = use GetModificationTime_{missingFileDiagnostics = False} , get_linkable_hashes = \fs -> map (snd . fromJust . hirCoreFp) <$> uses_ GetModIface fs + , get_module_graph = useWithSeparateFingerprintRule_ GetModuleGraphTransDepsFingerprints GetModuleGraph f , regenerate = regenerateHiFile session f ms } - r <- loadInterface (hscEnv session) ms linkableType recompInfo + hsc_env' <- setFileCacheHook (hscEnv session) + r <- loadInterface hsc_env' ms linkableType recompInfo case r of (diags, Nothing) -> return (Nothing, (diags, Nothing)) (diags, Just x) -> do @@ -886,7 +834,7 @@ getModIfaceFromDiskAndIndexRule recorder = hie_loc = Compat.ml_hie_file $ ms_location ms fileHash <- liftIO $ Util.getFileHash hie_loc mrow <- liftIO $ withHieDb (\hieDb -> HieDb.lookupHieFileFromSource hieDb (fromNormalizedFilePath f)) - hie_loc' <- liftIO $ traverse (makeAbsolute . HieDb.hieModuleHieFile) mrow + let hie_loc' = HieDb.hieModuleHieFile <$> mrow case mrow of Just row | fileHash == HieDb.modInfoHash (HieDb.hieModInfo row) @@ -927,23 +875,18 @@ getModSummaryRule displayTHWarning recorder = do defineEarlyCutoff (cmapWithPrio LogShake recorder) $ Rule $ \GetModSummary f -> do session' <- hscEnv <$> use_ GhcSession f modify_dflags <- getModifyDynFlags dynFlagsModifyGlobal - let session = hscSetFlags (modify_dflags $ hsc_dflags session') session' - (modTime, mFileContent) <- getFileContents f + let session = setNonHomeFCHook $ hscSetFlags (modify_dflags $ hsc_dflags session') session' -- TODO wz1000 + (modTime, mFileContent) <- getFileModTimeContents f let fp = fromNormalizedFilePath f modS <- liftIO $ runExceptT $ - getModSummaryFromImports session fp modTime (textToStringBuffer <$> mFileContent) + getModSummaryFromImports session fp modTime (textToStringBuffer . Rope.toText <$> mFileContent) case modS of Right res -> do -- Check for Template Haskell when (uses_th_qq $ msrModSummary res) $ do DisplayTHWarning act <- getIdeGlobalAction liftIO act -#if MIN_VERSION_ghc(9,3,0) let bufFingerPrint = ms_hs_hash (msrModSummary res) -#else - bufFingerPrint <- liftIO $ - fingerprintFromStringBuffer $ fromJust $ ms_hspp_buf $ msrModSummary res -#endif let fingerPrint = Util.fingerprintFingerprints [ msrFingerprint res, bufFingerPrint ] return ( Just (fingerprintToBS fingerPrint) , ([], Just res)) @@ -954,9 +897,6 @@ getModSummaryRule displayTHWarning recorder = do case mbMs of Just res@ModSummaryResult{..} -> do let ms = msrModSummary { -#if !MIN_VERSION_ghc(9,3,0) - ms_hs_date = error "use GetModSummary instead of GetModSummaryWithoutTimestamps", -#endif ms_hspp_buf = error "use GetModSummary instead of GetModSummaryWithoutTimestamps" } fp = fingerprintToBS msrFingerprint @@ -966,9 +906,9 @@ getModSummaryRule displayTHWarning recorder = do generateCore :: RunSimplifier -> NormalizedFilePath -> Action (IdeResult ModGuts) generateCore runSimplifier file = do packageState <- hscEnv <$> use_ GhcSessionDeps file + hsc' <- setFileCacheHook packageState tm <- use_ TypeCheck file - setPriority priorityGenerateCore - liftIO $ compileModule runSimplifier packageState (tmrModSummary tm) (tmrTypechecked tm) + liftIO $ compileModule runSimplifier hsc' (tmrModSummary tm) (tmrTypechecked tm) generateCoreRule :: Recorder (WithPriority Log) -> Rules () generateCoreRule recorder = @@ -983,14 +923,15 @@ getModIfaceRule recorder = defineEarlyCutoff (cmapWithPrio LogShake recorder) $ tmr <- use_ TypeCheck f linkableType <- getLinkableType f hsc <- hscEnv <$> use_ GhcSessionDeps f + hsc' <- setFileCacheHook hsc let compile = fmap ([],) $ use GenerateCore f se <- getShakeExtras - (diags, !mbHiFile) <- writeCoreFileIfNeeded se hsc linkableType compile tmr + (diags, !mbHiFile) <- writeCoreFileIfNeeded se hsc' linkableType compile tmr let fp = hiFileFingerPrint <$> mbHiFile hiDiags <- case mbHiFile of Just hiFile | OnDisk <- status - , not (tmrDeferredError tmr) -> liftIO $ writeHiFile se hsc hiFile + , not (tmrDeferredError tmr) -> liftIO $ writeHiFile se hsc' hiFile _ -> pure [] return (fp, (diags++hiDiags, mbHiFile)) NotFOI -> do @@ -1014,32 +955,33 @@ incrementRebuildCount = do count <- getRebuildCountVar <$> getIdeGlobalAction liftIO $ atomically $ modifyTVar' count (+1) +setFileCacheHook :: HscEnv -> Action HscEnv +setFileCacheHook old_hsc_env = do +#if MIN_VERSION_ghc(9,11,0) + unlift <- askUnliftIO + return $ old_hsc_env { hsc_FC = (hsc_FC old_hsc_env) { lookupFileCache = unliftIO unlift . use_ GetFileHash . toNormalizedFilePath' } } +#else + return old_hsc_env +#endif + -- | Also generates and indexes the `.hie` file, along with the `.o` file if needed -- Invariant maintained is that if the `.hi` file was successfully written, then the -- `.hie` and `.o` file (if needed) were also successfully written regenerateHiFile :: HscEnvEq -> NormalizedFilePath -> ModSummary -> Maybe LinkableType -> Action ([FileDiagnostic], Maybe HiFileResult) regenerateHiFile sess f ms compNeeded = do - let hsc = hscEnv sess + hsc <- setFileCacheHook (hscEnv sess) opt <- getIdeOptions -- Embed haddocks in the interface file (diags, mb_pm) <- liftIO $ getParsedModuleDefinition hsc opt f (withOptHaddock ms) - (diags', mb_pm') <- - -- We no longer need to parse again if GHC version is above 9.0. https://github.com/haskell/haskell-language-server/issues/1892 - if Compat.ghcVersion >= Compat.GHC90 || isJust mb_pm then do - return (diags, mb_pm) - else do - -- if parsing fails, try parsing again with Haddock turned off - (diagsNoHaddock, mb_pm') <- liftIO $ getParsedModuleDefinition hsc opt f ms - return (mergeParseErrorsHaddock diagsNoHaddock diags, mb_pm') - case mb_pm' of - Nothing -> return (diags', Nothing) + case mb_pm of + Nothing -> return (diags, Nothing) Just pm -> do -- Invoke typechecking directly to update it without incurring a dependency -- on the parsed module and the typecheck rules - (diags'', mtmr) <- typeCheckRuleDefinition hsc pm + (diags', mtmr) <- typeCheckRuleDefinition hsc pm f case mtmr of - Nothing -> pure (diags'', Nothing) + Nothing -> pure (diags', Nothing) Just tmr -> do let compile = liftIO $ compileModule (RunSimplifier True) hsc (pm_mod_summary pm) $ tmrTypechecked tmr @@ -1047,7 +989,7 @@ regenerateHiFile sess f ms compNeeded = do se <- getShakeExtras -- Bang pattern is important to avoid leaking 'tmr' - (diags''', !res) <- writeCoreFileIfNeeded se hsc compNeeded compile tmr + (diags'', !res) <- writeCoreFileIfNeeded se hsc compNeeded compile tmr -- Write hi file hiDiags <- case res of @@ -1071,7 +1013,7 @@ regenerateHiFile sess f ms compNeeded = do pure (hiDiags <> gDiags <> concat wDiags) Nothing -> pure [] - return (diags' <> diags'' <> diags''' <> hiDiags, res) + return (diags <> diags' <> diags'' <> hiDiags, res) -- | HscEnv should have deps included already @@ -1108,28 +1050,44 @@ usePropertyAction kn plId p = do pluginConfig <- getPluginConfigAction plId pure $ useProperty kn p $ plcConfig pluginConfig +usePropertyByPathAction :: + (HasPropertyByPath props path t) => + KeyNamePath path -> + PluginId -> + Properties props -> + Action (ToHsType t) +usePropertyByPathAction path plId p = do + pluginConfig <- getPluginConfigAction plId + pure $ usePropertyByPath path p $ plcConfig pluginConfig + -- --------------------------------------------------------------------- getLinkableRule :: Recorder (WithPriority Log) -> Rules () getLinkableRule recorder = defineEarlyCutoff (cmapWithPrio LogShake recorder) $ Rule $ \GetLinkable f -> do - ModSummaryResult{msrModSummary = ms} <- use_ GetModSummary f - HiFileResult{hirModIface, hirModDetails, hirCoreFp} <- use_ GetModIface f - let obj_file = ml_obj_file (ms_location ms) - core_file = ml_core_file (ms_location ms) - -- Can't use `GetModificationTime` rule because the core file was possibly written in this - -- very session, so the results aren't reliable - core_t <- liftIO $ getModTime core_file + HiFileResult{hirModSummary, hirModIface, hirModDetails, hirCoreFp} <- use_ GetModIface f + let obj_file = ml_obj_file (ms_location hirModSummary) + core_file = ml_core_file (ms_location hirModSummary) +#if MIN_VERSION_ghc(9,11,0) + mkLinkable t mod l = Linkable t mod (pure l) + dotO o = DotO o ModuleObject +#else + mkLinkable t mod l = LM t mod [l] + dotO = DotO +#endif case hirCoreFp of - Nothing -> error "called GetLinkable for a file without a linkable" + Nothing -> error $ "called GetLinkable for a file without a linkable: " ++ show f Just (bin_core, fileHash) -> do session <- use_ GhcSessionDeps f linkableType <- getLinkableType f >>= \case - Nothing -> error "called GetLinkable for a file which doesn't need compilation" + Nothing -> error $ "called GetLinkable for a file which doesn't need compilation: " ++ show f Just t -> pure t + -- Can't use `GetModificationTime` rule because the core file was possibly written in this + -- very session, so the results aren't reliable + core_t <- liftIO $ getModTime core_file (warns, hmi) <- case linkableType of -- Bytecode needs to be regenerated from the core file - BCOLinkable -> liftIO $ coreFileToLinkable linkableType (hscEnv session) ms hirModIface hirModDetails bin_core (posixSecondsToUTCTime core_t) + BCOLinkable -> liftIO $ coreFileToLinkable linkableType (hscEnv session) hirModSummary hirModIface hirModDetails bin_core (posixSecondsToUTCTime core_t) -- Object code can be read from the disk ObjectLinkable -> do -- object file is up to date if it is newer than the core file @@ -1142,10 +1100,15 @@ getLinkableRule recorder = else pure Nothing case mobj_time of Just obj_t - | obj_t >= core_t -> pure ([], Just $ HomeModInfo hirModIface hirModDetails (justObjects $ LM (posixSecondsToUTCTime obj_t) (ms_mod ms) [DotO obj_file])) - _ -> liftIO $ coreFileToLinkable linkableType (hscEnv session) ms hirModIface hirModDetails bin_core (error "object doesn't have time") + | obj_t >= core_t -> pure ([], Just $ HomeModInfo hirModIface hirModDetails (justObjects $ mkLinkable (posixSecondsToUTCTime obj_t) (ms_mod hirModSummary) (dotO obj_file))) + _ -> liftIO $ coreFileToLinkable linkableType (hscEnv session) hirModSummary hirModIface hirModDetails bin_core (error "object doesn't have time") -- Record the linkable so we know not to unload it, and unload old versions - whenJust ((homeModInfoByteCode =<< hmi) <|> (homeModInfoObject =<< hmi)) $ \(LM time mod _) -> do + whenJust ((homeModInfoByteCode =<< hmi) <|> (homeModInfoObject =<< hmi)) +#if MIN_VERSION_ghc(9,11,0) + $ \(Linkable time mod _) -> do +#else + $ \(LM time mod _) -> do +#endif compiledLinkables <- getCompiledLinkables <$> getIdeGlobalAction liftIO $ modifyVar compiledLinkables $ \old -> do let !to_keep = extendModuleEnv old mod time @@ -1159,7 +1122,9 @@ getLinkableRule recorder = --just before returning it to be loaded. This has a substantial effect on recompile --times as the number of loaded modules and splices increases. -- - unload (hscEnv session) (map (\(mod', time') -> LM time' mod' []) $ moduleEnvToList to_keep) + --We use a dummy DotA linkable part to fake a NativeCode linkable. + --The unload function doesn't care about the exact linkable parts. + unload (hscEnv session) (map (\(mod', time') -> mkLinkable time' mod' (DotA "dummy")) $ moduleEnvToList to_keep) return (to_keep, ()) return (fileHash <$ hmi, (warns, LinkableResult <$> hmi <*> pure fileHash)) @@ -1167,13 +1132,12 @@ getLinkableRule recorder = getLinkableType :: NormalizedFilePath -> Action (Maybe LinkableType) getLinkableType f = use_ NeedsCompilation f --- needsCompilationRule :: Rules () needsCompilationRule :: NormalizedFilePath -> Action (IdeResultNoDiagnosticsEarlyCutoff (Maybe LinkableType)) needsCompilationRule file - | "boot" `isSuffixOf` (fromNormalizedFilePath file) = + | "boot" `isSuffixOf` fromNormalizedFilePath file = pure (Just $ encodeLinkableType Nothing, Just Nothing) needsCompilationRule file = do - graph <- useNoFile GetModuleGraph + graph <- useWithSeparateFingerprintRule GetModuleGraphImmediateReverseDepsFingerprints GetModuleGraph file res <- case graph of -- Treat as False if some reverse dependency header fails to parse Nothing -> pure Nothing @@ -1190,36 +1154,23 @@ needsCompilationRule file = do -- that we just threw away, and thus have to recompile all dependencies once -- again, this time keeping the object code. -- A file needs to be compiled if any file that depends on it uses TemplateHaskell or needs to be compiled - ms <- msrModSummary . fst <$> useWithStale_ GetModSummaryWithoutTimestamps file (modsums,needsComps) <- liftA2 (,) (map (fmap (msrModSummary . fst)) <$> usesWithStale GetModSummaryWithoutTimestamps revdeps) (uses NeedsCompilation revdeps) - pure $ computeLinkableType ms modsums (map join needsComps) + pure $ computeLinkableType modsums (map join needsComps) pure (Just $ encodeLinkableType res, Just res) where - computeLinkableType :: ModSummary -> [Maybe ModSummary] -> [Maybe LinkableType] -> Maybe LinkableType - computeLinkableType this deps xs + computeLinkableType :: [Maybe ModSummary] -> [Maybe LinkableType] -> Maybe LinkableType + computeLinkableType deps xs | Just ObjectLinkable `elem` xs = Just ObjectLinkable -- If any dependent needs object code, so do we - | Just BCOLinkable `elem` xs = Just this_type -- If any dependent needs bytecode, then we need to be compiled - | any (maybe False uses_th_qq) deps = Just this_type -- If any dependent needs TH, then we need to be compiled + | Just BCOLinkable `elem` xs = Just BCOLinkable -- If any dependent needs bytecode, then we need to be compiled + | any (maybe False uses_th_qq) deps = Just BCOLinkable -- If any dependent needs TH, then we need to be compiled | otherwise = Nothing -- If none of these conditions are satisfied, we don't need to compile - where - this_type = computeLinkableTypeForDynFlags (ms_hspp_opts this) uses_th_qq :: ModSummary -> Bool uses_th_qq (ms_hspp_opts -> dflags) = xopt LangExt.TemplateHaskell dflags || xopt LangExt.QuasiQuotes dflags --- | How should we compile this module? --- (assuming we do in fact need to compile it). --- Depends on whether it uses unboxed tuples or sums -computeLinkableTypeForDynFlags :: DynFlags -> LinkableType -computeLinkableTypeForDynFlags d - = BCOLinkable - where -- unboxed_tuples_or_sums is only used in GHC < 9.2 - _unboxed_tuples_or_sums = - xopt LangExt.UnboxedTuples d || xopt LangExt.UnboxedSums d - -- | Tracks which linkables are current, so we don't need to unload them newtype CompiledLinkables = CompiledLinkables { getCompiledLinkables :: Var (ModuleEnv UTCTime) } instance IsIdeGlobal CompiledLinkables @@ -1234,9 +1185,9 @@ data RulesConfig = RulesConfig -- Disabling this drastically decreases sharing and is likely to -- increase memory usage if you have multiple files open -- Disabling this also disables checking for import cycles - fullModuleGraph :: Bool + fullModuleGraph :: Bool -- | Disable TH for improved performance in large codebases - , enableTemplateHaskell :: Bool + , enableTemplateHaskell :: Bool -- | Warning to show when TH is not supported by the current HLS binary , templateHaskellWarning :: LspT Config IO () } @@ -1277,6 +1228,7 @@ mainRule recorder RulesConfig{..} = do getModIfaceRule recorder getModSummaryRule templateHaskellWarning recorder getModuleGraphRule recorder + getFileHashRule recorder knownFilesRule recorder getClientSettingsRule recorder getHieAstsRule recorder @@ -1297,6 +1249,19 @@ mainRule recorder RulesConfig{..} = do persistentDocMapRule persistentImportMapRule getLinkableRule recorder + defineEarlyCutoff (cmapWithPrio LogShake recorder) $ Rule $ \GetModuleGraphTransDepsFingerprints file -> do + di <- useNoFile_ GetModuleGraph + let finger = lookupFingerprint file di (depTransDepsFingerprints di) + return (fingerprintToBS <$> finger, ([], finger)) + defineEarlyCutoff (cmapWithPrio LogShake recorder) $ Rule $ \GetModuleGraphTransReverseDepsFingerprints file -> do + di <- useNoFile_ GetModuleGraph + let finger = lookupFingerprint file di (depTransReverseDepsFingerprints di) + return (fingerprintToBS <$> finger, ([], finger)) + defineEarlyCutoff (cmapWithPrio LogShake recorder) $ Rule $ \GetModuleGraphImmediateReverseDepsFingerprints file -> do + di <- useNoFile_ GetModuleGraph + let finger = lookupFingerprint file di (depImmediateReverseDepsFingerprints di) + return (fingerprintToBS <$> finger, ([], finger)) + -- | Get HieFile for haskell file on NormalizedFilePath getHieFile :: NormalizedFilePath -> Action (Maybe HieFile) diff --git a/ghcide/src/Development/IDE/Core/Service.hs b/ghcide/src/Development/IDE/Core/Service.hs index 3efbd7e2d5..52639aeb22 100644 --- a/ghcide/src/Development/IDE/Core/Service.hs +++ b/ghcide/src/Development/IDE/Core/Service.hs @@ -1,9 +1,7 @@ -- Copyright (c) 2019 The DAML Authors. All rights reserved. -- SPDX-License-Identifier: Apache-2.0 -{-# LANGUAGE FlexibleInstances #-} -{-# LANGUAGE RankNTypes #-} -{-# LANGUAGE TypeFamilies #-} +{-# LANGUAGE TypeFamilies #-} -- | A Shake implementation of the compiler service, built -- using the "Shaker" abstraction layer for in-memory use. @@ -24,8 +22,7 @@ import Development.IDE.Core.FileExists (fileExistsRules) import Development.IDE.Core.OfInterest hiding (Log, LogShake) import Development.IDE.Graph import Development.IDE.Types.Options (IdeOptions (..)) -import Ide.Logger as Logger (Logger, - Pretty (pretty), +import Ide.Logger as Logger (Pretty (pretty), Priority (Debug), Recorder, WithPriority, @@ -56,6 +53,7 @@ instance Pretty Log where LogOfInterest msg -> pretty msg LogFileExists msg -> pretty msg + ------------------------------------------------------------ -- Exposed API @@ -65,14 +63,14 @@ initialise :: Recorder (WithPriority Log) -> IdePlugins IdeState -> Rules () -> Maybe (LSP.LanguageContextEnv Config) - -> Logger -> Debouncer LSP.NormalizedUri -> IdeOptions -> WithHieDb - -> IndexQueue + -> ThreadQueue -> Monitoring + -> FilePath -- ^ Root directory see Note [Root Directory] -> IO IdeState -initialise recorder defaultConfig plugins mainRule lspEnv logger debouncer options withHieDb hiedbChan metrics = do +initialise recorder defaultConfig plugins mainRule lspEnv debouncer options withHieDb hiedbChan metrics rootDir = do shakeProfiling <- do let fromConf = optShakeProfiling options fromEnv <- lookupEnv "GHCIDE_BUILD_PROFILING" @@ -82,7 +80,6 @@ initialise recorder defaultConfig plugins mainRule lspEnv logger debouncer optio lspEnv defaultConfig plugins - logger debouncer shakeProfiling (optReportProgress options) @@ -91,11 +88,12 @@ initialise recorder defaultConfig plugins mainRule lspEnv logger debouncer optio hiedbChan (optShakeOptions options) metrics - $ do + (do addIdeGlobal $ GlobalIdeOptions options ofInterestRules (cmapWithPrio LogOfInterest recorder) fileExistsRules (cmapWithPrio LogFileExists recorder) lspEnv - mainRule + mainRule) + rootDir -- | Shutdown the Compiler Service. shutdown :: IdeState -> IO () diff --git a/ghcide/src/Development/IDE/Core/Shake.hs b/ghcide/src/Development/IDE/Core/Shake.hs index 80837a6668..6fc9a4d00e 100644 --- a/ghcide/src/Development/IDE/Core/Shake.hs +++ b/ghcide/src/Development/IDE/Core/Shake.hs @@ -1,16 +1,12 @@ -- Copyright (c) 2019 The DAML Authors. All rights reserved. -- SPDX-License-Identifier: Apache-2.0 -{-# LANGUAGE CPP #-} -{-# LANGUAGE ConstraintKinds #-} -{-# LANGUAGE DerivingStrategies #-} -{-# LANGUAGE DuplicateRecordFields #-} -{-# LANGUAGE ExistentialQuantification #-} -{-# LANGUAGE PackageImports #-} -{-# LANGUAGE PolyKinds #-} -{-# LANGUAGE RankNTypes #-} -{-# LANGUAGE RecursiveDo #-} -{-# LANGUAGE TypeFamilies #-} +{-# LANGUAGE CPP #-} +{-# LANGUAGE DerivingStrategies #-} +{-# LANGUAGE DuplicateRecordFields #-} +{-# LANGUAGE PackageImports #-} +{-# LANGUAGE RecursiveDo #-} +{-# LANGUAGE TypeFamilies #-} -- | A Shake implementation of the compiler service. -- @@ -26,15 +22,17 @@ -- always stored as real Haskell values, whereas Shake serialises all 'A' values -- between runs. To deserialise a Shake value, we just consult Values. module Development.IDE.Core.Shake( - IdeState, shakeSessionInit, shakeExtras, shakeDb, + IdeState, shakeSessionInit, shakeExtras, shakeDb, rootDir, ShakeExtras(..), getShakeExtras, getShakeExtrasRules, - KnownTargets, Target(..), toKnownFiles, + KnownTargets(..), Target(..), toKnownFiles, unionKnownTargets, mkKnownTargets, IdeRule, IdeResult, GetModificationTime(GetModificationTime, GetModificationTime_, missingFileDiagnostics), shakeOpen, shakeShut, shakeEnqueue, newSession, use, useNoFile, uses, useWithStaleFast, useWithStaleFast', delayedAction, + useWithSeparateFingerprintRule, + useWithSeparateFingerprintRule_, FastResult(..), use_, useNoFile_, uses_, useWithStale, usesWithStale, @@ -55,14 +53,13 @@ module Development.IDE.Core.Shake( HLS.getClientConfig, getPluginConfigAction, knownTargets, - setPriority, ideLogger, actionLogger, getVirtualFile, FileVersion(..), - Priority(..), updatePositionMapping, - deleteValue, recordDirtyKeys, + updatePositionMappingHelper, + deleteValue, WithProgressFunc, WithIndefiniteProgressFunc, ProgressEvent(..), DelayedAction, mkDelayedAction, @@ -78,6 +75,8 @@ module Development.IDE.Core.Shake( garbageCollectDirtyKeysOlderThan, Log(..), VFSModified(..), getClientConfigAction, + ThreadQueue(..), + runWithSignal ) where import Control.Concurrent.Async @@ -86,7 +85,7 @@ import Control.Concurrent.STM.Stats (atomicallyNamed) import Control.Concurrent.Strict import Control.DeepSeq import Control.Exception.Extra hiding (bracket_) -import Control.Lens ((&), (?~)) +import Control.Lens ((%~), (&), (?~)) import Control.Monad.Extra import Control.Monad.IO.Class import Control.Monad.Reader @@ -127,9 +126,14 @@ import Development.IDE.Core.FileUtils (getModTime) import Development.IDE.Core.PositionMapping import Development.IDE.Core.ProgressReporting import Development.IDE.Core.RuleTypes +import Development.IDE.Types.Options as Options +import qualified Language.LSP.Protocol.Message as LSP +import qualified Language.LSP.Server as LSP + import Development.IDE.Core.Tracing +import Development.IDE.Core.WorkerThread import Development.IDE.GHC.Compat (NameCache, - NameCacheUpdater (..), + NameCacheUpdater, initNameCache, knownKeyNames) import Development.IDE.GHC.Orphans () @@ -150,51 +154,48 @@ import qualified Development.IDE.Types.Exports as ExportsMap import Development.IDE.Types.KnownTargets import Development.IDE.Types.Location import Development.IDE.Types.Monitoring (Monitoring (..)) -import Development.IDE.Types.Options import Development.IDE.Types.Shake import qualified Focus import GHC.Fingerprint import GHC.Stack (HasCallStack) +import GHC.TypeLits (KnownSymbol) import HieDb.Types import Ide.Logger hiding (Priority) import qualified Ide.Logger as Logger import Ide.Plugin.Config import qualified Ide.PluginUtils as HLS -import Ide.Types (IdePlugins (IdePlugins), - PluginDescriptor (pluginId), - PluginId) -import Language.LSP.Diagnostics +import Ide.Types import qualified Language.LSP.Protocol.Lens as L import Language.LSP.Protocol.Message import Language.LSP.Protocol.Types import qualified Language.LSP.Protocol.Types as LSP -import qualified Language.LSP.Server as LSP import Language.LSP.VFS hiding (start) import qualified "list-t" ListT import OpenTelemetry.Eventlog hiding (addEvent) +import qualified Prettyprinter as Pretty import qualified StmContainers.Map as STM import System.FilePath hiding (makeRelative) import System.IO.Unsafe (unsafePerformIO) import System.Time.Extra +import UnliftIO (MonadUnliftIO (withRunInIO)) --- See Note [Guidelines For Using CPP In GHCIDE Import Statements] - -#if !MIN_VERSION_ghc(9,3,0) -import Data.IORef -import Development.IDE.GHC.Compat (mkSplitUniqSupply, - upNameCache) -#endif data Log = LogCreateHieDbExportsMapStart | LogCreateHieDbExportsMapFinish !Int - | LogBuildSessionRestart !String ![DelayedActionInternal] !(KeySet) !Seconds !(Maybe FilePath) + | LogBuildSessionRestart !String ![DelayedActionInternal] !KeySet !Seconds !(Maybe FilePath) | LogBuildSessionRestartTakingTooLong !Seconds | LogDelayedAction !(DelayedAction ()) !Seconds | LogBuildSessionFinish !(Maybe SomeException) | LogDiagsDiffButNoLspEnv ![FileDiagnostic] | LogDefineEarlyCutoffRuleNoDiagHasDiag !FileDiagnostic | LogDefineEarlyCutoffRuleCustomNewnessHasDiag !FileDiagnostic + | LogCancelledAction !T.Text + | LogSessionInitialised + | LogLookupPersistentKey !T.Text + | LogShakeGarbageCollection !T.Text !Int !Seconds + -- * OfInterest Log messages + | LogSetFilesOfInterest ![(NormalizedFilePath, FileOfInterestStatus)] deriving Show instance Pretty Log where @@ -228,17 +229,26 @@ instance Pretty Log where LogDefineEarlyCutoffRuleCustomNewnessHasDiag fileDiagnostic -> "defineEarlyCutoff RuleWithCustomNewnessCheck - file diagnostic:" <+> pretty (showDiagnosticsColored [fileDiagnostic]) + LogCancelledAction action -> + pretty action <+> "was cancelled" + LogSessionInitialised -> "Shake session initialized" + LogLookupPersistentKey key -> + "LOOKUP PERSISTENT FOR:" <+> pretty key + LogShakeGarbageCollection label number duration -> + pretty label <+> "of" <+> pretty number <+> "keys (took " <+> pretty (showDuration duration) <> ")" + LogSetFilesOfInterest ofInterest -> + "Set files of interst to" <> Pretty.line + <> indent 4 (pretty $ fmap (first fromNormalizedFilePath) ofInterest) -- | We need to serialize writes to the database, so we send any function that -- needs to write to the database over the channel, where it will be picked up by -- a worker thread. data HieDbWriter = HieDbWriter - { indexQueue :: IndexQueue - , indexPending :: TVar (HMap.HashMap NormalizedFilePath Fingerprint) -- ^ Avoid unnecessary/out of date indexing - , indexCompleted :: TVar Int -- ^ to report progress - , indexProgressToken :: Var (Maybe LSP.ProgressToken) - -- ^ This is a Var instead of a TVar since we need to do IO to initialise/update, so we need a lock + { indexQueue :: IndexQueue + , indexPending :: TVar (HMap.HashMap NormalizedFilePath Fingerprint) -- ^ Avoid unnecessary/out of date indexing + , indexCompleted :: TVar Int -- ^ to report progress + , indexProgressReporting :: ProgressReporting } -- | Actions to queue up on the index worker thread @@ -246,12 +256,25 @@ data HieDbWriter -- with (currently) retry functionality type IndexQueue = TQueue (((HieDb -> IO ()) -> IO ()) -> IO ()) +data ThreadQueue = ThreadQueue { + tIndexQueue :: IndexQueue + , tRestartQueue :: TQueue (IO ()) + , tLoaderQueue :: TQueue (IO ()) +} + +-- Note [Semantic Tokens Cache Location] +-- ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +-- storing semantic tokens cache for each file in shakeExtras might +-- not be ideal, since it most used in LSP request handlers +-- instead of rules. We should consider moving it to a more +-- appropriate place in the future if we find one, store it for now. + -- information we stash inside the shakeExtra field data ShakeExtras = ShakeExtras { --eventer :: LSP.FromServerMessage -> IO () lspEnv :: Maybe (LSP.LanguageContextEnv Config) ,debouncer :: Debouncer NormalizedUri - ,logger :: Logger + ,shakeRecorder :: Recorder (WithPriority Log) ,idePlugins :: IdePlugins IdeState ,globals :: TVar (HMap.HashMap TypeRep Dynamic) -- ^ Registry of global state used by rules. @@ -259,27 +282,32 @@ data ShakeExtras = ShakeExtras ,state :: Values ,diagnostics :: STMDiagnosticStore ,hiddenDiagnostics :: STMDiagnosticStore - ,publishedDiagnostics :: STM.Map NormalizedUri [Diagnostic] + ,publishedDiagnostics :: STM.Map NormalizedUri [FileDiagnostic] -- ^ This represents the set of diagnostics that we have published. -- Due to debouncing not every change might get published. + + ,semanticTokensCache:: STM.Map NormalizedFilePath SemanticTokens + -- ^ Cache of last response of semantic tokens for each file, + -- so we can compute deltas for semantic tokens(SMethod_TextDocumentSemanticTokensFullDelta). + -- putting semantic tokens cache and id in shakeExtras might not be ideal + -- see Note [Semantic Tokens Cache Location] + ,semanticTokensId :: TVar Int + -- ^ semanticTokensId is used to generate unique ids for each lsp response of semantic tokens. ,positionMapping :: STM.Map NormalizedUri (EnumMap Int32 (PositionDelta, PositionMapping)) -- ^ Map from a text document version to a PositionMapping that describes how to map -- positions in a version of that document to positions in the latest version -- First mapping is delta from previous version and second one is an - -- accumulation of all previous mappings. - ,progress :: ProgressReporting + -- accumulation to the current version. + ,progress :: PerFileProgressReporting ,ideTesting :: IdeTesting -- ^ Whether to enable additional lsp messages used by the test suite for checking invariants ,restartShakeSession :: VFSModified -> String -> [DelayedAction ()] + -> IO [Key] -> IO () -#if MIN_VERSION_ghc(9,3,0) ,ideNc :: NameCache -#else - ,ideNc :: IORef NameCache -#endif -- | A mapping of module name to known target (or candidate targets, if missing) ,knownTargetsVar :: TVar (Hashed KnownTargets) -- | A mapping of exported identifiers for local modules. Updated on kick @@ -302,6 +330,10 @@ data ShakeExtras = ShakeExtras -- ^ Default HLS config, only relevant if the client does not provide any Config , dirtyKeys :: TVar KeySet -- ^ Set of dirty rule keys since the last Shake run + , restartQueue :: TQueue (IO ()) + -- ^ Queue of restart actions to be run. + , loaderQueue :: TQueue (IO ()) + -- ^ Queue of loader actions to be run. } type WithProgressFunc = forall a. @@ -428,7 +460,7 @@ lastValueIO s@ShakeExtras{positionMapping,persistentKeys,state} k file = do | otherwise = do pmap <- readTVarIO persistentKeys mv <- runMaybeT $ do - liftIO $ Logger.logDebug (logger s) $ T.pack $ "LOOKUP PERSISTENT FOR: " ++ show k + liftIO $ logWith (shakeRecorder s) Debug $ LogLookupPersistentKey (T.pack $ show k) f <- MaybeT $ pure $ lookupKeyMap (newKey k) pmap (dv,del,ver) <- MaybeT $ runIdeAction "lastValueIO" s $ f file MaybeT $ pure $ (,del,ver) <$> fromDynamic dv @@ -443,7 +475,7 @@ lastValueIO s@ShakeExtras{positionMapping,persistentKeys,state} k file = do `catch` (\(_ :: IOException) -> pure Nothing) atomicallyNamed "lastValueIO 2" $ do STM.focus (Focus.alter (alterValue $ Stale (Just del) actual_version (toDyn v))) (toKey k file) state - Just . (v,) . addDelta del <$> mappingForVersion positionMapping file actual_version + Just . (v,) . addOldDelta del <$> mappingForVersion positionMapping file actual_version -- We got a new stale value from the persistent rule, insert it in the map without affecting diagnostics alterValue new Nothing = Just (ValueWithDiagnostics new mempty) -- If it wasn't in the map, give it empty diagnostics @@ -459,7 +491,7 @@ lastValueIO s@ShakeExtras{positionMapping,persistentKeys,state} k file = do Succeeded ver (fromDynamic -> Just v) -> atomicallyNamed "lastValueIO 5" $ Just . (v,) <$> mappingForVersion positionMapping file ver Stale del ver (fromDynamic -> Just v) -> - atomicallyNamed "lastValueIO 6" $ Just . (v,) . maybe id addDelta del <$> mappingForVersion positionMapping file ver + atomicallyNamed "lastValueIO 6" $ Just . (v,) . maybe id addOldDelta del <$> mappingForVersion positionMapping file ver Failed p | not p -> readPersistent _ -> pure Nothing @@ -495,6 +527,33 @@ newtype ShakeSession = ShakeSession -- ^ Closes the Shake session } +-- Note [Root Directory] +-- ~~~~~~~~~~~~~~~~~~~~~ +-- We keep track of the root directory explicitly, which is the directory of the project root. +-- We might be setting it via these options with decreasing priority: +-- +-- 1. from LSP workspace root, `resRootPath` in `LanguageContextEnv`. +-- 2. command line (--cwd) +-- 3. default to the current directory. +-- +-- Using `getCurrentDirectory` makes it more difficult to run the tests, as we spawn one thread of HLS per test case. +-- If we modify the global Variable CWD, via `setCurrentDirectory`, all other test threads are suddenly affected, +-- forcing us to run all integration tests sequentially. +-- +-- Also, there might be a race condition if we depend on the current directory, as some plugin might change it. +-- e.g. stylish's `loadConfig`. https://github.com/haskell/haskell-language-server/issues/4234 +-- +-- But according to https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#workspace_workspaceFolders +-- The root dir is deprecated, that means we should cleanup dependency on the project root(Or $CWD) thing gradually, +-- so multi-workspaces can actually be supported when we use absolute path everywhere(might also need some high level design). +-- That might not be possible unless we have everything adapted to it, like 'hlint' and 'evaluation of template haskell'. +-- But we should still be working towards the goal. +-- +-- We can drop it in the future once: +-- 1. We can get rid all the usages of root directory in the codebase. +-- 2. LSP version we support actually removes the root directory from the protocol. +-- + -- | A Shake database plus persistent store. Can be thought of as storing -- mappings from @(FilePath, k)@ to @RuleResult k@. data IdeState = IdeState @@ -503,6 +562,8 @@ data IdeState = IdeState ,shakeExtras :: ShakeExtras ,shakeDatabaseProfile :: ShakeDatabase -> IO (Maybe FilePath) ,stopMonitoring :: IO () + -- | See Note [Root Directory] + ,rootDir :: FilePath } @@ -531,26 +592,17 @@ setValues state key file val diags = -- | Delete the value stored for a given ide build key +-- and return the key that was deleted. deleteValue :: Shake.ShakeValue k => ShakeExtras -> k -> NormalizedFilePath - -> STM () -deleteValue ShakeExtras{dirtyKeys, state} key file = do + -> STM [Key] +deleteValue ShakeExtras{state} key file = do STM.delete (toKey key file) state - modifyTVar' dirtyKeys $ insertKeySet (toKey key file) + return [toKey key file] -recordDirtyKeys - :: Shake.ShakeValue k - => ShakeExtras - -> k - -> [NormalizedFilePath] - -> STM (IO ()) -recordDirtyKeys ShakeExtras{dirtyKeys} key file = do - modifyTVar' dirtyKeys $ \x -> foldl' (flip insertKeySet) x (toKey key <$> file) - return $ withEventTrace "recordDirtyKeys" $ \addEvent -> do - addEvent (fromString $ unlines $ "dirty " <> show key : map fromNormalizedFilePath file) -- | We return Nothing if the rule has not run and Just Failed if it has failed to produce a value. getValues :: @@ -591,41 +643,47 @@ shakeOpen :: Recorder (WithPriority Log) -> Maybe (LSP.LanguageContextEnv Config) -> Config -> IdePlugins IdeState - -> Logger -> Debouncer NormalizedUri -> Maybe FilePath -> IdeReportProgress -> IdeTesting -> WithHieDb - -> IndexQueue + -> ThreadQueue -> ShakeOptions -> Monitoring -> Rules () + -> FilePath + -- ^ Root directory, this one might be picking up from `LanguageContextEnv`'s `resRootPath` + -- , see Note [Root Directory] -> IO IdeState -shakeOpen recorder lspEnv defaultConfig idePlugins logger debouncer +shakeOpen recorder lspEnv defaultConfig idePlugins debouncer shakeProfileDir (IdeReportProgress reportProgress) - ideTesting@(IdeTesting testing) - withHieDb indexQueue opts monitoring rules = mdo + ideTesting + withHieDb threadQueue opts monitoring rules rootDir = mdo + -- see Note [Serializing runs in separate thread] + let indexQueue = tIndexQueue threadQueue + restartQueue = tRestartQueue threadQueue + loaderQueue = tLoaderQueue threadQueue -#if MIN_VERSION_ghc(9,3,0) ideNc <- initNameCache 'r' knownKeyNames -#else - us <- mkSplitUniqSupply 'r' - ideNc <- newIORef (initNameCache us knownKeyNames) -#endif shakeExtras <- do globals <- newTVarIO HMap.empty state <- STM.newIO diagnostics <- STM.newIO hiddenDiagnostics <- STM.newIO publishedDiagnostics <- STM.newIO + semanticTokensCache <- STM.newIO positionMapping <- STM.newIO - knownTargetsVar <- newTVarIO $ hashed HMap.empty + knownTargetsVar <- newTVarIO $ hashed emptyKnownTargets let restartShakeSession = shakeRestart recorder ideState persistentKeys <- newTVarIO mempty indexPending <- newTVarIO HMap.empty indexCompleted <- newTVarIO 0 - indexProgressToken <- newVar Nothing + semanticTokensId <- newTVarIO 0 + indexProgressReporting <- progressReportingNoTrace + (liftM2 (+) (length <$> readTVar indexPending) (readTVar indexCompleted)) + (readTVar indexCompleted) + lspEnv "Indexing" optProgressStyle let hiedbWriter = HieDbWriter{..} exportsMap <- newTVarIO mempty -- lazily initialize the exports map with the contents of the hiedb @@ -636,18 +694,17 @@ shakeOpen recorder lspEnv defaultConfig idePlugins logger debouncer atomically $ modifyTVar' exportsMap (<> em) logWith recorder Debug $ LogCreateHieDbExportsMapFinish (ExportsMap.size em) - progress <- do - let (before, after) = if testing then (0,0.1) else (0.1,0.1) + progress <- if reportProgress - then delayedProgressReporting before after lspEnv optProgressStyle - else noProgressReporting + then progressReporting lspEnv "Processing" optProgressStyle + else noPerFileProgressReporting actionQueue <- newQueue let clientCapabilities = maybe def LSP.resClientCapabilities lspEnv dirtyKeys <- newTVarIO mempty -- Take one VFS snapshot at the start vfsVar <- newTVarIO =<< vfsSnapshot lspEnv - pure ShakeExtras{..} + pure ShakeExtras{shakeRecorder = recorder, ..} shakeDb <- shakeNewDatabase opts { shakeExtra = newShakeExtra shakeExtras } @@ -688,13 +745,13 @@ getStateKeys = (fmap.fmap) fst . atomically . ListT.toList . STM.listT . state -- | Must be called in the 'Initialized' handler and only once shakeSessionInit :: Recorder (WithPriority Log) -> IdeState -> IO () -shakeSessionInit recorder ide@IdeState{..} = do +shakeSessionInit recorder IdeState{..} = do -- Take a snapshot of the VFS - it should be empty as we've received no notifications -- till now, but it can't hurt to be in sync with the `lsp` library. vfs <- vfsSnapshot (lspEnv shakeExtras) initSession <- newSession recorder shakeExtras (VFSModified vfs) shakeDb [] "shakeSessionInit" putMVar shakeSession initSession - logDebug (ideLogger ide) "Shake session initialized" + logWith recorder Debug LogSessionInitialised shakeShut :: IdeState -> IO () shakeShut IdeState{..} = do @@ -704,6 +761,7 @@ shakeShut IdeState{..} = do for_ runner cancelShakeSession void $ shakeDatabaseProfile shakeDb progressStop $ progress shakeExtras + progressStop $ indexProgressReporting $ hiedbWriter shakeExtras stopMonitoring @@ -729,27 +787,33 @@ delayedAction a = do extras <- ask liftIO $ shakeEnqueue extras a + -- | Restart the current 'ShakeSession' with the given system actions. -- Any actions running in the current session will be aborted, -- but actions added via 'shakeEnqueue' will be requeued. -shakeRestart :: Recorder (WithPriority Log) -> IdeState -> VFSModified -> String -> [DelayedAction ()] -> IO () -shakeRestart recorder IdeState{..} vfs reason acts = - withMVar' - shakeSession - (\runner -> do - (stopTime,()) <- duration $ logErrorAfter 10 $ cancelShakeSession runner - res <- shakeDatabaseProfile shakeDb - backlog <- readTVarIO $ dirtyKeys shakeExtras - queue <- atomicallyNamed "actionQueue - peek" $ peekInProgress $ actionQueue shakeExtras - - -- this log is required by tests - logWith recorder Debug $ LogBuildSessionRestart reason queue backlog stopTime res - ) - -- It is crucial to be masked here, otherwise we can get killed - -- between spawning the new thread and updating shakeSession. - -- See https://github.com/haskell/ghcide/issues/79 - (\() -> do - (,()) <$> newSession recorder shakeExtras vfs shakeDb acts reason) +shakeRestart :: Recorder (WithPriority Log) -> IdeState -> VFSModified -> String -> [DelayedAction ()] -> IO [Key] -> IO () +shakeRestart recorder IdeState{..} vfs reason acts ioActionBetweenShakeSession = + void $ awaitRunInThread (restartQueue shakeExtras) $ do + withMVar' + shakeSession + (\runner -> do + (stopTime,()) <- duration $ logErrorAfter 10 $ cancelShakeSession runner + keys <- ioActionBetweenShakeSession + -- it is every important to update the dirty keys after we enter the critical section + -- see Note [Housekeeping rule cache and dirty key outside of hls-graph] + atomically $ modifyTVar' (dirtyKeys shakeExtras) $ \x -> foldl' (flip insertKeySet) x keys + res <- shakeDatabaseProfile shakeDb + backlog <- readTVarIO $ dirtyKeys shakeExtras + queue <- atomicallyNamed "actionQueue - peek" $ peekInProgress $ actionQueue shakeExtras + + -- this log is required by tests + logWith recorder Debug $ LogBuildSessionRestart reason queue backlog stopTime res + ) + -- It is crucial to be masked here, otherwise we can get killed + -- between spawning the new thread and updating shakeSession. + -- See https://github.com/haskell/ghcide/issues/79 + (\() -> do + (,()) <$> newSession recorder shakeExtras vfs shakeDb acts reason) where logErrorAfter :: Seconds -> IO () -> IO () logErrorAfter seconds action = flip withAsync (const action) $ do @@ -762,7 +826,7 @@ shakeRestart recorder IdeState{..} vfs reason acts = -- -- Appropriate for user actions other than edits. shakeEnqueue :: ShakeExtras -> DelayedAction a -> IO (IO a) -shakeEnqueue ShakeExtras{actionQueue, logger} act = do +shakeEnqueue ShakeExtras{actionQueue, shakeRecorder} act = do (b, dai) <- instantiateDelayedAction act atomicallyNamed "actionQueue - push" $ pushQueue dai actionQueue let wait' barrier = @@ -771,7 +835,7 @@ shakeEnqueue ShakeExtras{actionQueue, logger} act = do fail $ "internal bug: forever blocked on MVar for " <> actionName act) , Handler (\e@AsyncCancelled -> do - logPriority logger Debug $ T.pack $ actionName act <> " was cancelled" + logWith shakeRecorder Debug $ LogCancelledAction (T.pack $ actionName act) atomicallyNamed "actionQueue - abort" $ abortQueue dai actionQueue throw e) @@ -895,13 +959,12 @@ garbageCollectDirtyKeysOlderThan maxAge checkParents = otTracedGarbageCollection garbageCollectKeys :: String -> Int -> CheckParents -> [(Key, Int)] -> Action [Key] garbageCollectKeys label maxAge checkParents agedKeys = do start <- liftIO offsetTime - ShakeExtras{state, dirtyKeys, lspEnv, logger, ideTesting} <- getShakeExtras + ShakeExtras{state, dirtyKeys, lspEnv, shakeRecorder, ideTesting} <- getShakeExtras (n::Int, garbage) <- liftIO $ foldM (removeDirtyKey dirtyKeys state) (0,[]) agedKeys t <- liftIO start when (n>0) $ liftIO $ do - logDebug logger $ T.pack $ - label <> " of " <> show n <> " keys (took " <> showDuration t <> ")" + logWith shakeRecorder Debug $ LogShakeGarbageCollection (T.pack label) n t when (coerce ideTesting) $ liftIO $ mRunLspT lspEnv $ LSP.sendNotification (SMethod_CustomMethod (Proxy @"ghcide/GC")) (toJSON $ mapMaybe (fmap showKey . fromKeyType) garbage) @@ -1004,13 +1067,8 @@ askShake :: IdeAction ShakeExtras askShake = ask -#if MIN_VERSION_ghc(9,3,0) mkUpdater :: NameCache -> NameCacheUpdater mkUpdater = id -#else -mkUpdater :: IORef NameCache -> NameCacheUpdater -mkUpdater ref = NCU (upNameCache ref) -#endif -- | A (maybe) stale result now, and an up to date one later data FastResult a = FastResult { stale :: Maybe (a,PositionMapping), uptoDate :: IO (Maybe a) } @@ -1092,6 +1150,23 @@ usesWithStale key files = do -- whether the rule succeeded or not. traverse (lastValue key) files +-- we use separate fingerprint rules to trigger the rebuild of the rule +useWithSeparateFingerprintRule + :: (IdeRule k v, IdeRule k1 Fingerprint) + => k1 -> k -> NormalizedFilePath -> Action (Maybe v) +useWithSeparateFingerprintRule fingerKey key file = do + _ <- use fingerKey file + useWithoutDependency key emptyFilePath + +-- we use separate fingerprint rules to trigger the rebuild of the rule +useWithSeparateFingerprintRule_ + :: (IdeRule k v, IdeRule k1 Fingerprint) + => k1 -> k -> NormalizedFilePath -> Action v +useWithSeparateFingerprintRule_ fingerKey key file = do + useWithSeparateFingerprintRule fingerKey key file >>= \case + Just v -> return v + Nothing -> liftIO $ throwIO $ BadDependency (show key) + useWithoutDependency :: IdeRule k v => k -> NormalizedFilePath -> Action (Maybe v) useWithoutDependency key file = @@ -1116,7 +1191,7 @@ defineEarlyCutoff recorder (Rule op) = addRule $ \(Q (key, file)) (old :: Maybe extras <- getShakeExtras let diagnostics ver diags = do traceDiagnostics diags - updateFileDiagnostics recorder file ver (newKey key) extras . map (\(_,y,z) -> (y,z)) $ diags + updateFileDiagnostics recorder file ver (newKey key) extras diags defineEarlyCutoff' diagnostics (==) key file old mode $ const $ op key file defineEarlyCutoff recorder (RuleNoDiagnostics op) = addRule $ \(Q (key, file)) (old :: Maybe BS.ByteString) mode -> otTracedAction key file mode traceA $ \traceDiagnostics -> do let diagnostics _ver diags = do @@ -1135,7 +1210,7 @@ defineEarlyCutoff recorder (RuleWithOldValue op) = addRule $ \(Q (key, file)) (o extras <- getShakeExtras let diagnostics ver diags = do traceDiagnostics diags - updateFileDiagnostics recorder file ver (newKey key) extras . map (\(_,y,z) -> (y,z)) $ diags + updateFileDiagnostics recorder file ver (newKey key) extras diags defineEarlyCutoff' diagnostics (==) key file old mode $ op key file defineNoFile :: IdeRule k v => Recorder (WithPriority Log) -> (k -> Action v) -> Rules () @@ -1162,7 +1237,8 @@ defineEarlyCutoff' defineEarlyCutoff' doDiagnostics cmp key file mbOld mode action = do ShakeExtras{state, progress, dirtyKeys} <- getShakeExtras options <- getIdeOptions - (if optSkipProgress options key then id else inProgress progress file) $ do + let trans g x = withRunInIO $ \run -> g (run x) + (if optSkipProgress options key then id else trans (inProgress progress file)) $ do val <- case mbOld of Just old | mode == RunDependenciesSame -> do mbValue <- liftIO $ atomicallyNamed "define - read 1" $ getValues state key file @@ -1172,7 +1248,7 @@ defineEarlyCutoff' doDiagnostics cmp key file mbOld mode action = do Just (v@(Succeeded _ x), diags) -> do ver <- estimateFileVersionUnsafely key (Just x) file doDiagnostics (vfsVersion =<< ver) $ Vector.toList diags - return $ Just $ RunResult ChangedNothing old $ A v + return $ Just $ RunResult ChangedNothing old (A v) $ return () _ -> return Nothing _ -> -- assert that a "clean" rule is never a cache miss @@ -1189,14 +1265,13 @@ defineEarlyCutoff' doDiagnostics cmp key file mbOld mode action = do (mbBs, (diags, mbRes)) <- actionCatch (do v <- action staleV; liftIO $ evaluate $ force v) $ \(e :: SomeException) -> do - pure (Nothing, ([ideErrorText file $ T.pack $ show e | not $ isBadDependency e],Nothing)) + pure (Nothing, ([ideErrorText file (T.pack $ show (key, file) ++ show e) | not $ isBadDependency e],Nothing)) ver <- estimateFileVersionUnsafely key mbRes file (bs, res) <- case mbRes of Nothing -> do pure (toShakeValue ShakeStale mbBs, staleV) Just v -> pure (maybe ShakeNoCutoff ShakeResult mbBs, Succeeded ver v) - liftIO $ atomicallyNamed "define - write" $ setValues state key file res (Vector.fromList diags) doDiagnostics (vfsVersion =<< ver) diags let eq = case (bs, fmap decodeShakeValue mbOld) of (ShakeResult a, Just (ShakeResult b)) -> cmp a b @@ -1206,9 +1281,12 @@ defineEarlyCutoff' doDiagnostics cmp key file mbOld mode action = do _ -> False return $ RunResult (if eq then ChangedRecomputeSame else ChangedRecomputeDiff) - (encodeShakeValue bs) $ - A res - liftIO $ atomicallyNamed "define - dirtyKeys" $ modifyTVar' dirtyKeys (deleteKeySet $ toKey key file) + (encodeShakeValue bs) + (A res) $ do + -- this hook needs to be run in the same transaction as the key is marked clean + -- see Note [Housekeeping rule cache and dirty key outside of hls-graph] + setValues state key file res (Vector.fromList diags) + modifyTVar' dirtyKeys (deleteKeySet $ toKey key file) return res where -- Highly unsafe helper to compute the version of a file @@ -1232,6 +1310,32 @@ defineEarlyCutoff' doDiagnostics cmp key file mbOld mode action = do -- * creating bogus "file does not exists" diagnostics | otherwise = useWithoutDependency (GetModificationTime_ False) fp +-- Note [Housekeeping rule cache and dirty key outside of hls-graph] +-- ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +-- Hls-graph contains its own internal running state for each key in the shakeDatabase. +-- ShakeExtras contains `state` field (rule result cache) and `dirtyKeys` (keys that became +-- dirty in between build sessions) that is not visible to the hls-graph +-- Essentially, we need to keep the rule cache and dirty key and hls-graph's internal state +-- in sync. + +-- 1. A dirty key collected in a session should not be removed from dirty keys in the same session. +-- Since if we clean out the dirty key in the same session, +-- 1.1. we will lose the chance to dirty its reverse dependencies. Since it only happens during session restart. +-- 1.2. a key might be marked as dirty in ShakeExtras while it's being recomputed by hls-graph which could lead to it's premature removal from dirtyKeys. +-- See issue https://github.com/haskell/haskell-language-server/issues/4093 for more details. + +-- 2. When a key is marked clean in the hls-graph's internal running +-- state, the rule cache and dirty keys are updated in the same transaction. +-- otherwise, some situations like the following can happen: +-- thread 1: hls-graph session run a key +-- thread 1: defineEarlyCutoff' run the action for the key +-- thread 1: the action is done, rule cache and dirty key are updated +-- thread 2: we restart the hls-graph session, thread 1 is killed, the +-- hls-graph's internal state is not updated. +-- This is problematic with early cut off because we are having a new rule cache matching the +-- old hls-graph's internal state, which might case it's reverse dependency to skip the recomputation. +-- See https://github.com/haskell/haskell-language-server/issues/4194 for more details. + traceA :: A v -> String traceA (A Failed{}) = "Failed" traceA (A Stale{}) = "Stale" @@ -1243,89 +1347,83 @@ updateFileDiagnostics :: MonadIO m -> Maybe Int32 -> Key -> ShakeExtras - -> [(ShowDiagnostic,Diagnostic)] -- ^ current results + -> [FileDiagnostic] -- ^ current results -> m () -updateFileDiagnostics recorder fp ver k ShakeExtras{diagnostics, hiddenDiagnostics, publishedDiagnostics, debouncer, lspEnv, ideTesting} current0 = +updateFileDiagnostics recorder fp ver k ShakeExtras{diagnostics, hiddenDiagnostics, publishedDiagnostics, debouncer, lspEnv, ideTesting} current0 = do liftIO $ withTrace ("update diagnostics " <> fromString(fromNormalizedFilePath fp)) $ \ addTag -> do addTag "key" (show k) - let (currentShown, currentHidden) = partition ((== ShowDiag) . fst) current + let (currentShown, currentHidden) = partition ((== ShowDiag) . fdShouldShowDiagnostic) current uri = filePathToUri' fp addTagUnsafe :: String -> String -> String -> a -> a addTagUnsafe msg t x v = unsafePerformIO(addTag (msg <> t) x) `seq` v - update :: (forall a. String -> String -> a -> a) -> [Diagnostic] -> STMDiagnosticStore -> STM [Diagnostic] + update :: (forall a. String -> String -> a -> a) -> [FileDiagnostic] -> STMDiagnosticStore -> STM [FileDiagnostic] update addTagUnsafeMethod new store = addTagUnsafeMethod "count" (show $ Prelude.length new) $ setStageDiagnostics addTagUnsafeMethod uri ver (renderKey k) new store - current = second diagsFromRule <$> current0 + current = map (fdLspDiagnosticL %~ diagsFromRule) current0 addTag "version" (show ver) mask_ $ do -- Mask async exceptions to ensure that updated diagnostics are always -- published. Otherwise, we might never publish certain diagnostics if -- an exception strikes between modifyVar but before -- publishDiagnosticsNotification. - newDiags <- liftIO $ atomicallyNamed "diagnostics - update" $ update (addTagUnsafe "shown ") (map snd currentShown) diagnostics - _ <- liftIO $ atomicallyNamed "diagnostics - hidden" $ update (addTagUnsafe "hidden ") (map snd currentHidden) hiddenDiagnostics + newDiags <- liftIO $ atomicallyNamed "diagnostics - update" $ update (addTagUnsafe "shown ") currentShown diagnostics + _ <- liftIO $ atomicallyNamed "diagnostics - hidden" $ update (addTagUnsafe "hidden ") currentHidden hiddenDiagnostics let uri' = filePathToUri' fp let delay = if null newDiags then 0.1 else 0 registerEvent debouncer delay uri' $ withTrace ("report diagnostics " <> fromString (fromNormalizedFilePath fp)) $ \tag -> do - join $ mask_ $ do - lastPublish <- atomicallyNamed "diagnostics - publish" $ STM.focus (Focus.lookupWithDefault [] <* Focus.insert newDiags) uri' publishedDiagnostics - let action = when (lastPublish /= newDiags) $ case lspEnv of + join $ mask_ $ do + lastPublish <- atomicallyNamed "diagnostics - publish" $ STM.focus (Focus.lookupWithDefault [] <* Focus.insert newDiags) uri' publishedDiagnostics + let action = when (lastPublish /= newDiags) $ case lspEnv of Nothing -> -- Print an LSP event. - logWith recorder Info $ LogDiagsDiffButNoLspEnv (map (fp, ShowDiag,) newDiags) + logWith recorder Info $ LogDiagsDiffButNoLspEnv newDiags Just env -> LSP.runLspT env $ do liftIO $ tag "count" (show $ Prelude.length newDiags) liftIO $ tag "key" (show k) LSP.sendNotification SMethod_TextDocumentPublishDiagnostics $ - LSP.PublishDiagnosticsParams (fromNormalizedUri uri') (fmap fromIntegral ver) ( newDiags) - return action + LSP.PublishDiagnosticsParams (fromNormalizedUri uri') (fmap fromIntegral ver) (map fdLspDiagnostic newDiags) + return action where diagsFromRule :: Diagnostic -> Diagnostic diagsFromRule c@Diagnostic{_range} | coerce ideTesting = c & L.relatedInformation ?~ - [ - DiagnosticRelatedInformation + [ DiagnosticRelatedInformation (Location (filePathToUri $ fromNormalizedFilePath fp) _range ) (T.pack $ show k) - ] + ] | otherwise = c -newtype Priority = Priority Double - -setPriority :: Priority -> Action () -setPriority (Priority p) = reschedule p - -ideLogger :: IdeState -> Logger -ideLogger IdeState{shakeExtras=ShakeExtras{logger}} = logger +ideLogger :: IdeState -> Recorder (WithPriority Log) +ideLogger IdeState{shakeExtras=ShakeExtras{shakeRecorder}} = shakeRecorder -actionLogger :: Action Logger -actionLogger = do - ShakeExtras{logger} <- getShakeExtras - return logger +actionLogger :: Action (Recorder (WithPriority Log)) +actionLogger = shakeRecorder <$> getShakeExtras -------------------------------------------------------------------------------- -type STMDiagnosticStore = STM.Map NormalizedUri StoreItem +type STMDiagnosticStore = STM.Map NormalizedUri StoreItem' +data StoreItem' = StoreItem' (Maybe Int32) FileDiagnosticsBySource +type FileDiagnosticsBySource = Map.Map (Maybe T.Text) (SL.SortedList FileDiagnostic) -getDiagnosticsFromStore :: StoreItem -> [Diagnostic] -getDiagnosticsFromStore (StoreItem _ diags) = concatMap SL.fromSortedList $ Map.elems diags +getDiagnosticsFromStore :: StoreItem' -> [FileDiagnostic] +getDiagnosticsFromStore (StoreItem' _ diags) = concatMap SL.fromSortedList $ Map.elems diags updateSTMDiagnostics :: (forall a. String -> String -> a -> a) -> STMDiagnosticStore -> NormalizedUri -> Maybe Int32 -> - DiagnosticsBySource -> - STM [LSP.Diagnostic] + FileDiagnosticsBySource -> + STM [FileDiagnostic] updateSTMDiagnostics addTag store uri mv newDiagsBySource = getDiagnosticsFromStore . fromJust <$> STM.focus (Focus.alter update *> Focus.lookup) uri store where - update (Just(StoreItem mvs dbs)) + update (Just(StoreItem' mvs dbs)) | addTag "previous version" (show mvs) $ addTag "previous count" (show $ Prelude.length $ filter (not.null) $ Map.elems dbs) False = undefined - | mvs == mv = Just (StoreItem mv (newDiagsBySource <> dbs)) - update _ = Just (StoreItem mv newDiagsBySource) + | mvs == mv = Just (StoreItem' mv (newDiagsBySource <> dbs)) + update _ = Just (StoreItem' mv newDiagsBySource) -- | Sets the diagnostics for a file and compilation step -- if you want to clear the diagnostics call this with an empty list @@ -1334,9 +1432,9 @@ setStageDiagnostics -> NormalizedUri -> Maybe Int32 -- ^ the time that the file these diagnostics originate from was last edited -> T.Text - -> [LSP.Diagnostic] + -> [FileDiagnostic] -> STMDiagnosticStore - -> STM [LSP.Diagnostic] + -> STM [FileDiagnostic] setStageDiagnostics addTag uri ver stage diags ds = updateSTMDiagnostics addTag ds uri ver updatedDiags where !updatedDiags = Map.singleton (Just stage) $! SL.toSortedList diags @@ -1345,19 +1443,41 @@ getAllDiagnostics :: STMDiagnosticStore -> STM [FileDiagnostic] getAllDiagnostics = - fmap (concatMap (\(k,v) -> map (fromUri k,ShowDiag,) $ getDiagnosticsFromStore v)) . ListT.toList . STM.listT + fmap (concatMap (\(_,v) -> getDiagnosticsFromStore v)) . ListT.toList . STM.listT updatePositionMapping :: IdeState -> VersionedTextDocumentIdentifier -> [TextDocumentContentChangeEvent] -> STM () updatePositionMapping IdeState{shakeExtras = ShakeExtras{positionMapping}} VersionedTextDocumentIdentifier{..} changes = STM.focus (Focus.alter f) uri positionMapping where uri = toNormalizedUri _uri - f = Just . f' . fromMaybe mempty - f' mappingForUri = snd $ - -- Very important to use mapAccum here so that the tails of - -- each mapping can be shared, otherwise quadratic space is - -- used which is evident in long running sessions. - EM.mapAccumRWithKey (\acc _k (delta, _) -> let new = addDelta delta acc in (new, (delta, acc))) - zeroMapping - (EM.insert _version (shared_change, zeroMapping) mappingForUri) - shared_change = mkDelta changes + f = Just . updatePositionMappingHelper _version changes . fromMaybe mempty + + +updatePositionMappingHelper :: + Int32 + -> [TextDocumentContentChangeEvent] + -> EnumMap Int32 (PositionDelta, PositionMapping) + -> EnumMap Int32 (PositionDelta, PositionMapping) +updatePositionMappingHelper ver changes mappingForUri = snd $ + -- Very important to use mapAccum here so that the tails of + -- each mapping can be shared, otherwise quadratic space is + -- used which is evident in long running sessions. + EM.mapAccumRWithKey (\acc _k (delta, _) -> let new = addOldDelta delta acc in (new, (delta, acc))) + zeroMapping + (EM.insert ver (mkDelta changes, zeroMapping) mappingForUri) + +-- | sends a signal whenever shake session is run/restarted +-- being used in cabal and hlint plugin tests to know when its time +-- to look for file diagnostics +kickSignal :: KnownSymbol s => Bool -> Maybe (LSP.LanguageContextEnv c) -> [NormalizedFilePath] -> Proxy s -> Action () +kickSignal testing lspEnv files msg = when testing $ liftIO $ mRunLspT lspEnv $ + LSP.sendNotification (LSP.SMethod_CustomMethod msg) $ + toJSON $ map fromNormalizedFilePath files + +-- | Add kick start/done signal to rule +runWithSignal :: (KnownSymbol s0, KnownSymbol s1, IdeRule k v) => Proxy s0 -> Proxy s1 -> [NormalizedFilePath] -> k -> Action () +runWithSignal msgStart msgEnd files rule = do + ShakeExtras{ideTesting = Options.IdeTesting testing, lspEnv} <- getShakeExtras + kickSignal testing lspEnv files msgStart + void $ uses rule files + kickSignal testing lspEnv files msgEnd diff --git a/ghcide/src/Development/IDE/Core/Tracing.hs b/ghcide/src/Development/IDE/Core/Tracing.hs index ed30a174af..34839faaee 100644 --- a/ghcide/src/Development/IDE/Core/Tracing.hs +++ b/ghcide/src/Development/IDE/Core/Tracing.hs @@ -7,7 +7,7 @@ module Development.IDE.Core.Tracing , otTracedGarbageCollection , withTrace , withEventTrace - , withTelemetryLogger + , withTelemetryRecorder ) where @@ -26,7 +26,7 @@ import Development.IDE.Graph.Rule import Development.IDE.Types.Diagnostics (FileDiagnostic, showDiagnostics) import Development.IDE.Types.Location (Uri (..)) -import Ide.Logger (Logger (Logger)) +import Ide.Logger import Ide.Types (PluginId (..)) import Language.LSP.Protocol.Types (NormalizedFilePath, fromNormalizedFilePath) @@ -51,16 +51,20 @@ withEventTrace name act | otherwise = act (\_ -> pure ()) -- | Returns a logger that produces telemetry events in a single span -withTelemetryLogger :: (MonadIO m, MonadMask m) => (Logger -> m a) -> m a -withTelemetryLogger k = withSpan "Logger" $ \sp -> +withTelemetryRecorder :: (MonadIO m, MonadMask m) => (Recorder (WithPriority (Doc a)) -> m c) -> m c +withTelemetryRecorder k = withSpan "Logger" $ \sp -> -- Tracy doesn't like when we create a new span for every log line. -- To workaround that, we create a single span for all log events. -- This is fine since we don't care about the span itself, only about the events - k $ Logger $ \p m -> - addEvent sp (fromString $ show p) (encodeUtf8 $ trim m) - where - -- eventlog message size is limited by EVENT_PAYLOAD_SIZE_MAX = STG_WORD16_MAX - trim = T.take (fromIntegral(maxBound :: Word16) - 10) + k $ telemetryLogRecorder sp + +-- | Returns a logger that produces telemetry events in a single span. +telemetryLogRecorder :: SpanInFlight -> Recorder (WithPriority (Doc a)) +telemetryLogRecorder sp = Recorder $ \WithPriority {..} -> + liftIO $ addEvent sp (fromString $ show priority) (encodeUtf8 $ trim $ renderStrict $ layoutCompact payload) + where + -- eventlog message size is limited by EVENT_PAYLOAD_SIZE_MAX = STG_WORD16_MAX + trim = T.take (fromIntegral(maxBound :: Word16) - 10) -- | Trace a handler using OpenTelemetry. Adds various useful info into tags in the OpenTelemetry span. otTracedHandler @@ -108,7 +112,7 @@ otTracedAction key file mode result act ExitCaseSuccess res -> do setTag sp "result" (pack $ result $ runValue res) setTag sp "changed" $ case res of - RunResult x _ _ -> fromString $ show x + RunResult x _ _ _ -> fromString $ show x endSpan sp) (\sp -> act (liftIO . setTag sp "diagnostics" . encodeUtf8 . showDiagnostics )) | otherwise = act (\_ -> return ()) diff --git a/ghcide/src/Development/IDE/Core/UseStale.hs b/ghcide/src/Development/IDE/Core/UseStale.hs index ab6a0afa48..498ea44bee 100644 --- a/ghcide/src/Development/IDE/Core/UseStale.hs +++ b/ghcide/src/Development/IDE/Core/UseStale.hs @@ -1,7 +1,5 @@ -{-# LANGUAGE DerivingVia #-} -{-# LANGUAGE GADTs #-} -{-# LANGUAGE KindSignatures #-} -{-# LANGUAGE RankNTypes #-} +{-# LANGUAGE DerivingVia #-} +{-# LANGUAGE GADTs #-} module Development.IDE.Core.UseStale ( Age(..) diff --git a/ghcide/src/Development/IDE/Core/WorkerThread.hs b/ghcide/src/Development/IDE/Core/WorkerThread.hs new file mode 100644 index 0000000000..6d141c7ef3 --- /dev/null +++ b/ghcide/src/Development/IDE/Core/WorkerThread.hs @@ -0,0 +1,59 @@ +{- +Module : Development.IDE.Core.WorkerThread +Author : @soulomoon +SPDX-License-Identifier: Apache-2.0 + +Description : This module provides an API for managing worker threads in the IDE. +see Note [Serializing runs in separate thread] +-} +module Development.IDE.Core.WorkerThread + (withWorkerQueue, awaitRunInThread) + where + +import Control.Concurrent.Async (AsyncCancelled (AsyncCancelled), + withAsync) +import Control.Concurrent.STM +import Control.Concurrent.Strict (newBarrier, signalBarrier, + waitBarrier) +import Control.Exception.Safe (Exception (fromException), + SomeException, throwIO, try) +import Control.Monad (forever) +import Control.Monad.Cont (ContT (ContT)) + +{- +Note [Serializing runs in separate thread] +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +We often want to take long-running actions using some resource that cannot be shared. +In this instance it is useful to have a queue of jobs to run using the resource. +Like the db writes, session loading in session loader, shake session restarts. + +Originally we used various ways to implement this, but it was hard to maintain and error prone. +Moreover, we can not stop these threads uniformly when we are shutting down the server. +-} + +-- | 'withWorkerQueue' creates a new 'TQueue', and launches a worker +-- thread which polls the queue for requests and runs the given worker +-- function on them. +withWorkerQueue :: (t -> IO a) -> ContT () IO (TQueue t) +withWorkerQueue workerAction = ContT $ \mainAction -> do + q <- newTQueueIO + withAsync (writerThread q) $ \_ -> mainAction q + where + writerThread q = + forever $ do + l <- atomically $ readTQueue q + workerAction l + +-- | 'awaitRunInThread' queues up an 'IO' action to be run by a worker thread, +-- and then blocks until the result is computed. If the action throws an +-- non-async exception, it is rethrown in the calling thread. +awaitRunInThread :: TQueue (IO ()) -> IO result -> IO result +awaitRunInThread q act = do + -- Take an action from TQueue, run it and + -- use barrier to wait for the result + barrier <- newBarrier + atomically $ writeTQueue q $ try act >>= signalBarrier barrier + resultOrException <- waitBarrier barrier + case resultOrException of + Left e -> throwIO (e :: SomeException) + Right r -> return r diff --git a/ghcide/src/Development/IDE/GHC/CPP.hs b/ghcide/src/Development/IDE/GHC/CPP.hs index b65fa8e89a..c97afd90e7 100644 --- a/ghcide/src/Development/IDE/GHC/CPP.hs +++ b/ghcide/src/Development/IDE/GHC/CPP.hs @@ -18,21 +18,18 @@ where import Development.IDE.GHC.Compat as Compat import Development.IDE.GHC.Compat.Util import GHC +import GHC.Settings +import qualified GHC.SysTools.Cpp as Pipeline -- See Note [Guidelines For Using CPP In GHCIDE Import Statements] -import GHC.Settings -#if !MIN_VERSION_ghc(9,3,0) -import qualified GHC.Driver.Pipeline as Pipeline +#if MIN_VERSION_ghc(9,10,2) +import qualified GHC.SysTools.Tasks as Pipeline #endif -#if MIN_VERSION_ghc(9,3,0) && !MIN_VERSION_ghc(9,5,0) -import qualified GHC.Driver.Pipeline.Execute as Pipeline -#endif - -#if MIN_VERSION_ghc(9,5,0) -import qualified GHC.SysTools.Cpp as Pipeline +#if MIN_VERSION_ghc(9,11,0) +import qualified GHC.SysTools.Tasks as Pipeline #endif addOptP :: String -> DynFlags -> DynFlags @@ -46,22 +43,21 @@ addOptP f = alterToolSettings $ \s -> s doCpp :: HscEnv -> FilePath -> FilePath -> IO () doCpp env input_fn output_fn = - -- See GHC commit a2f53ac8d968723417baadfab5be36a020ea6850 - -- this function/Pipeline.doCpp previously had a raw parameter - -- always set to True that corresponded to these settings - -#if MIN_VERSION_ghc(9,5,0) + -- See GHC commit a2f53ac8d968723417baadfab5be36a020ea6850 + -- this function/Pipeline.doCpp previously had a raw parameter + -- always set to True that corresponded to these settings let cpp_opts = Pipeline.CppOpts { cppLinePragmas = True -# if MIN_VERSION_ghc(9,9,0) + +#if MIN_VERSION_ghc(9,10,2) + , sourceCodePreprocessor = Pipeline.SCPHsCpp +#elif MIN_VERSION_ghc(9,10,0) , useHsCpp = True -# else - , cppUseCc = False -# endif - } in #else - let cpp_opts = True in + , cppUseCc = False #endif + } in + Pipeline.doCpp (hsc_logger env) (hsc_tmpfs env) (hsc_dflags env) (hsc_unit_env env) cpp_opts input_fn output_fn diff --git a/ghcide/src/Development/IDE/GHC/Compat.hs b/ghcide/src/Development/IDE/GHC/Compat.hs index fd5e0c01d5..ddf01c61c5 100644 --- a/ghcide/src/Development/IDE/GHC/Compat.hs +++ b/ghcide/src/Development/IDE/GHC/Compat.hs @@ -1,51 +1,29 @@ -- Copyright (c) 2019 The DAML Authors. All rights reserved. -- SPDX-License-Identifier: Apache-2.0 -{-# LANGUAGE CPP #-} -{-# LANGUAGE ConstraintKinds #-} -{-# LANGUAGE FlexibleInstances #-} -{-# LANGUAGE PatternSynonyms #-} -{-# OPTIONS -Wno-incomplete-uni-patterns -Wno-dodgy-imports #-} +{-# LANGUAGE CPP #-} +{-# LANGUAGE PatternSynonyms #-} -- | Attempt at hiding the GHC version differences we can. module Development.IDE.GHC.Compat( - mkHomeModLocation, hPutStringBuffer, addIncludePathsQuote, getModuleHash, setUpTypedHoles, - NameCacheUpdater(..), -#if MIN_VERSION_ghc(9,3,0) - getMessages, - renderDiagnosticMessageWithHints, - nameEnvElts, -#else - upNameCache, -#endif lookupNameCache, disableWarningsAsErrors, reLoc, reLocA, - getPsMessages, renderMessages, pattern PFailedWithErrorMessages, - isObjectLinkable, - -#if !MIN_VERSION_ghc(9,3,0) - extendModSummaryNoDeps, - emsModSummary, -#endif myCoreToStgExpr, - Usage(..), - - liftZonkM, - FastStringCompat, bytesFS, mkFastStringByteString, nodeInfo', getNodeIds, + getSourceNodeIds, sourceNodeInfo, generatedNodeInfo, simpleNodeInfoCompat, @@ -53,10 +31,6 @@ module Development.IDE.GHC.Compat( nodeAnnotations, mkAstNode, combineRealSrcSpans, - - nonDetOccEnvElts, - nonDetFoldOccEnv, - isQualifiedImport, GhcVersion(..), ghcVersion, @@ -94,7 +68,6 @@ module Development.IDE.GHC.Compat( simplifyExpr, tidyExpr, emptyTidyEnv, - tcInitTidyEnv, corePrepExpr, corePrepPgm, lintInteractiveExpr, @@ -102,11 +75,6 @@ module Development.IDE.GHC.Compat( HomePackageTable, lookupHpt, loadModulesHome, -#if MIN_VERSION_ghc(9,3,0) - Dependencies(dep_direct_mods), -#else - Dependencies(dep_mods), -#endif bcoFreeNames, ModIfaceAnnotation, pattern Annotation, @@ -129,13 +97,37 @@ module Development.IDE.GHC.Compat( expectJust, extract_cons, recDotDot, -#if MIN_VERSION_ghc(9,5,0) + + + Dependencies(dep_direct_mods), + NameCacheUpdater, + XModulePs(..), + +#if !MIN_VERSION_ghc(9,7,0) + liftZonkM, + nonDetFoldOccEnv, +#endif + +#if MIN_VERSION_ghc(9,7,0) + tcInitTidyEnv, #endif ) where -import Prelude hiding (mod) -import Development.IDE.GHC.Compat.Core hiding (moduleUnitId) +import Compat.HieAst (enrichHie) +import Compat.HieBin +import Compat.HieTypes hiding + (nodeAnnotations) +import qualified Compat.HieTypes as GHC (nodeAnnotations) +import Compat.HieUtils +import Control.Applicative ((<|>)) +import qualified Data.ByteString as BS +import Data.Coerce (coerce) +import Data.List (foldl') +import qualified Data.Map as Map +import qualified Data.Set as S +import Data.String (IsString (fromString)) +import Development.IDE.GHC.Compat.Core import Development.IDE.GHC.Compat.Env import Development.IDE.GHC.Compat.Iface import Development.IDE.GHC.Compat.Logger @@ -144,287 +136,178 @@ import Development.IDE.GHC.Compat.Parser import Development.IDE.GHC.Compat.Plugins import Development.IDE.GHC.Compat.Units import Development.IDE.GHC.Compat.Util -import GHC hiding (HasSrcSpan, - ModLocation, - RealSrcSpan, exprType, - getLoc, lookupName) -import Data.Coerce (coerce) -import Data.String (IsString (fromString)) -import Compat.HieAst (enrichHie) -import Compat.HieBin -import Compat.HieTypes hiding (nodeAnnotations) -import qualified Compat.HieTypes as GHC (nodeAnnotations) -import Compat.HieUtils -import qualified Data.ByteString as BS -import Data.List (foldl') -import qualified Data.Map as Map -import qualified Data.Set as S - --- See Note [Guidelines For Using CPP In GHCIDE Import Statements] - -#if MIN_VERSION_ghc(9,7,0) -import GHC.Tc.Zonk.TcType (tcInitTidyEnv) -#endif -import qualified GHC.Core.Opt.Pipeline as GHC -import GHC.Core.Tidy (tidyExpr) -import GHC.CoreToStg.Prep (corePrepPgm) -import qualified GHC.CoreToStg.Prep as GHC -import GHC.Driver.Hooks (hscCompileCoreExprHook) - -import GHC.ByteCode.Asm (bcoFreeNames) -import GHC.Types.Annotations (AnnTarget (ModuleTarget), - Annotation (..), - extendAnnEnvList) -import GHC.Types.Unique.DFM as UniqDFM -import GHC.Types.Unique.DSet as UniqDSet -import GHC.Types.Unique.Set as UniqSet -import GHC.Data.FastString +import GHC hiding (ModLocation, + RealSrcSpan, exprType, + getLoc, lookupName) +import Prelude hiding (mod) + +import qualified GHC.Core.Opt.Pipeline as GHC +import GHC.Core.Tidy (tidyExpr) +import GHC.CoreToStg.Prep (corePrepPgm) +import qualified GHC.CoreToStg.Prep as GHC +import GHC.Driver.Hooks (hscCompileCoreExprHook) + +import GHC.ByteCode.Asm (bcoFreeNames) import GHC.Core +import GHC.Data.FastString import GHC.Data.StringBuffer -import GHC.Driver.Session hiding (ExposePackage) +import GHC.Driver.Session hiding (ExposePackage) +import GHC.Iface.Make (mkIfaceExports) +import GHC.SysTools.Tasks (runPp, runUnlit) +import GHC.Types.Annotations (AnnTarget (ModuleTarget), + Annotation (..), + extendAnnEnvList) +import qualified GHC.Types.Avail as Avail +import GHC.Types.Unique.DFM as UniqDFM +import GHC.Types.Unique.DSet as UniqDSet +import GHC.Types.Unique.Set as UniqSet import GHC.Types.Var.Env -import GHC.Iface.Make (mkIfaceExports) -import GHC.SysTools.Tasks (runUnlit, runPp) -import qualified GHC.Types.Avail as Avail - - -#if !MIN_VERSION_ghc(9,5,0) -import GHC.Core.Lint (lintInteractiveExpr) -#endif - -import GHC.Iface.Env -import GHC.Types.SrcLoc (combineRealSrcSpans) -import GHC.Linker.Loader (loadExpr) -import GHC.Runtime.Context (icInteractiveModule) -import GHC.Unit.Home.ModInfo (HomePackageTable, - lookupHpt) -import GHC.Driver.Env as Env -import GHC.Unit.Module.ModIface import GHC.Builtin.Uniques import GHC.ByteCode.Types +import GHC.Core.Lint.Interactive (interactiveInScope) import GHC.CoreToStg import GHC.Data.Maybe -import GHC.Linker.Loader (loadDecls) +import GHC.Driver.Config.Core.Lint.Interactive (lintInteractiveExpr) +import GHC.Driver.Config.Core.Opt.Simplify (initSimplifyExprOpts) +import GHC.Driver.Config.CoreToStg (initCoreToStgOpts) +import GHC.Driver.Config.CoreToStg.Prep (initCorePrepConfig) +import GHC.Driver.Config.Stg.Pipeline +import GHC.Driver.Env as Env +import GHC.Iface.Env +import GHC.Linker.Loader (loadDecls, loadExpr) +import GHC.Runtime.Context (icInteractiveModule) import GHC.Stg.Pipeline import GHC.Stg.Syntax import GHC.StgToByteCode import GHC.Types.CostCentre import GHC.Types.IPE +import GHC.Types.SrcLoc (combineRealSrcSpans) +import GHC.Unit.Home.ModInfo (HomePackageTable, + lookupHpt) +import GHC.Unit.Module.Deps (Dependencies (dep_direct_mods), + Usage (..)) +import GHC.Unit.Module.ModIface -#if !MIN_VERSION_ghc(9,3,0) -import GHC.Unit.Module.Deps (Dependencies(dep_mods), Usage(..)) -import GHC.Linker.Types (isObjectLinkable) -import GHC.Unit.Module.ModSummary -import GHC.Runtime.Interpreter -#endif - -#if !MIN_VERSION_ghc(9,3,0) -import Data.IORef -#endif - -#if MIN_VERSION_ghc(9,3,0) -import GHC.Unit.Module.Deps (Dependencies(dep_direct_mods), Usage(..)) -import GHC.Driver.Config.Stg.Pipeline -#endif +-- See Note [Guidelines For Using CPP In GHCIDE Import Statements] -#if MIN_VERSION_ghc(9,5,0) -import GHC.Core.Lint.Interactive (interactiveInScope) -import GHC.Driver.Config.Core.Lint.Interactive (lintInteractiveExpr) -import GHC.Driver.Config.Core.Opt.Simplify (initSimplifyExprOpts) -import GHC.Driver.Config.CoreToStg (initCoreToStgOpts) -import GHC.Driver.Config.CoreToStg.Prep (initCorePrepConfig) +#if MIN_VERSION_ghc(9,7,0) +import GHC.Tc.Zonk.TcType (tcInitTidyEnv) #endif #if !MIN_VERSION_ghc(9,7,0) liftZonkM :: a -> a liftZonkM = id -#endif -#if !MIN_VERSION_ghc(9,7,0) nonDetFoldOccEnv :: (a -> b -> b) -> b -> OccEnv a -> b nonDetFoldOccEnv = foldOccEnv #endif -#if !MIN_VERSION_ghc(9,3,0) -nonDetOccEnvElts :: OccEnv a -> [a] -nonDetOccEnvElts = occEnvElts -#endif type ModIfaceAnnotation = Annotation -#if MIN_VERSION_ghc(9,3,0) -nameEnvElts :: NameEnv a -> [a] -nameEnvElts = nonDetNameEnvElts -#endif myCoreToStgExpr :: Logger -> DynFlags -> InteractiveContext -#if MIN_VERSION_ghc(9,3,0) -> Bool -#endif -> Module -> ModLocation -> CoreExpr -> IO ( Id -#if MIN_VERSION_ghc(9,3,0) ,[CgStgTopBinding] -- output program -#else - ,[StgTopBinding] -- output program -#endif , InfoTableProvMap , CollectedCCs ) myCoreToStgExpr logger dflags ictxt -#if MIN_VERSION_ghc(9,3,0) for_bytecode -#endif this_mod ml prepd_expr = do {- Create a temporary binding (just because myCoreToStg needs a binding for the stg2stg step) -} let bco_tmp_id = mkSysLocal (fsLit "BCO_toplevel") (mkPseudoUniqueE 0) -#if MIN_VERSION_ghc(9,5,0) ManyTy -#else - Many -#endif (exprType prepd_expr) (stg_binds, prov_map, collected_ccs) <- myCoreToStg logger dflags ictxt -#if MIN_VERSION_ghc(9,3,0) for_bytecode -#endif this_mod ml [NonRec bco_tmp_id prepd_expr] return (bco_tmp_id, stg_binds, prov_map, collected_ccs) myCoreToStg :: Logger -> DynFlags -> InteractiveContext -#if MIN_VERSION_ghc(9,3,0) -> Bool -#endif -> Module -> ModLocation -> CoreProgram -#if MIN_VERSION_ghc(9,3,0) -> IO ( [CgStgTopBinding] -- output program -#else - -> IO ( [StgTopBinding] -- output program -#endif , InfoTableProvMap , CollectedCCs ) -- CAF cost centre info (declared and used) myCoreToStg logger dflags ictxt -#if MIN_VERSION_ghc(9,3,0) for_bytecode -#endif this_mod ml prepd_binds = do let (stg_binds, denv, cost_centre_info) = {-# SCC "Core2Stg" #-} coreToStg -#if MIN_VERSION_ghc(9,5,0) (initCoreToStgOpts dflags) -#else - dflags -#endif this_mod ml prepd_binds #if MIN_VERSION_ghc(9,8,0) (unzip -> (stg_binds2,_),_) -#elif MIN_VERSION_ghc(9,4,2) - (stg_binds2,_) #else - stg_binds2 + (stg_binds2,_) #endif <- {-# SCC "Stg2Stg" #-} -#if MIN_VERSION_ghc(9,3,0) stg2stg logger -#if MIN_VERSION_ghc(9,5,0) (interactiveInScope ictxt) -#else - ictxt -#endif (initStgPipelineOpts dflags for_bytecode) this_mod stg_binds -#else - stg2stg logger dflags ictxt this_mod stg_binds -#endif return (stg_binds2, denv, cost_centre_info) - +#if MIN_VERSION_ghc(9,9,0) +reLocA :: (HasLoc (GenLocated a e), HasAnnotation b) + => GenLocated a e -> GenLocated b e +reLocA = reLoc +#endif getDependentMods :: ModIface -> [ModuleName] -#if MIN_VERSION_ghc(9,3,0) getDependentMods = map (gwib_mod . snd) . S.toList . dep_direct_mods . mi_deps -#else -getDependentMods = map gwib_mod . dep_mods . mi_deps -#endif simplifyExpr :: DynFlags -> HscEnv -> CoreExpr -> IO CoreExpr -#if MIN_VERSION_ghc(9,5,0) simplifyExpr _ env = GHC.simplifyExpr (Development.IDE.GHC.Compat.Env.hsc_logger env) (ue_eps (Development.IDE.GHC.Compat.Env.hsc_unit_env env)) (initSimplifyExprOpts (hsc_dflags env) (hsc_IC env)) -#else -simplifyExpr _ = GHC.simplifyExpr -#endif corePrepExpr :: DynFlags -> HscEnv -> CoreExpr -> IO CoreExpr -#if MIN_VERSION_ghc(9,5,0) corePrepExpr _ env expr = do cfg <- initCorePrepConfig env GHC.corePrepExpr (Development.IDE.GHC.Compat.Env.hsc_logger env) cfg expr -#else -corePrepExpr _ = GHC.corePrepExpr -#endif renderMessages :: PsMessages -> (Bag WarnMsg, Bag ErrMsg) renderMessages msgs = -#if MIN_VERSION_ghc(9,3,0) - let renderMsgs extractor = (fmap . fmap) renderDiagnosticMessageWithHints . getMessages $ extractor msgs + let renderMsgs extractor = (fmap . fmap) GhcPsMessage . getMessages $ extractor msgs in (renderMsgs psWarnings, renderMsgs psErrors) -#else - msgs -#endif -pattern PFailedWithErrorMessages :: forall a b. (b -> Bag (MsgEnvelope DecoratedSDoc)) -> ParseResult a +pattern PFailedWithErrorMessages :: forall a b. (b -> Bag (MsgEnvelope GhcMessage)) -> ParseResult a pattern PFailedWithErrorMessages msgs -#if MIN_VERSION_ghc(9,3,0) - <- PFailed (const . fmap (fmap renderDiagnosticMessageWithHints) . getMessages . getPsErrorMessages -> msgs) -#else - <- PFailed (const . fmap pprError . getErrorMessages -> msgs) -#endif + <- PFailed (const . fmap (fmap GhcPsMessage) . getMessages . getPsErrorMessages -> msgs) {-# COMPLETE POk, PFailedWithErrorMessages #-} hieExportNames :: HieFile -> [(SrcSpan, Name)] hieExportNames = nameListFromAvails . hie_exports -#if MIN_VERSION_ghc(9,3,0) type NameCacheUpdater = NameCache -#else - -lookupNameCache :: Module -> OccName -> NameCache -> (NameCache, Name) --- Lookup up the (Module,OccName) in the NameCache --- If you find it, return it; if not, allocate a fresh original name and extend --- the NameCache. --- Reason: this may the first occurrence of (say) Foo.bar we have encountered. --- If we need to explore its value we will load Foo.hi; but meanwhile all we --- need is a Name for it. -lookupNameCache mod occ name_cache = - case lookupOrigNameCache (nsNames name_cache) mod occ of { - Just name -> (name_cache, name); - Nothing -> - case takeUniqFromSupply (nsUniqs name_cache) of { - (uniq, us) -> - let - name = mkExternalName uniq mod occ noSrcSpan - new_cache = extendNameCache (nsNames name_cache) mod occ name - in (name_cache{ nsUniqs = us, nsNames = new_cache }, name) }} - -upNameCache :: IORef NameCache -> (NameCache -> (NameCache, c)) -> IO c -upNameCache = updNameCache -#endif mkHieFile' :: ModSummary -> [Avail.AvailInfo] +#if MIN_VERSION_ghc(9,11,0) + -> (HieASTs Type, NameEntityInfo) +#else -> HieASTs Type +#endif -> BS.ByteString -> Hsc HieFile -mkHieFile' ms exports asts src = do +mkHieFile' ms exports +#if MIN_VERSION_ghc(9,11,0) + (asts, entityInfo) +#else + asts +#endif + src = do let Just src_file = ml_hs_file $ ms_location ms (asts',arr) = compressTypes asts return $ HieFile @@ -432,6 +315,9 @@ mkHieFile' ms exports asts src = do , hie_module = ms_mod ms , hie_types = arr , hie_asts = asts' +#if MIN_VERSION_ghc(9,11,0) + , hie_entity_infos = entityInfo +#endif -- mkIfaceExports sorts the AvailInfos for stability , hie_exports = mkIfaceExports exports , hie_hs_src = src @@ -447,7 +333,7 @@ setHieDir _f d = d { hieDir = Just _f} dontWriteHieFiles :: DynFlags -> DynFlags dontWriteHieFiles d = gopt_unset d Opt_WriteHie -setUpTypedHoles ::DynFlags -> DynFlags +setUpTypedHoles :: DynFlags -> DynFlags setUpTypedHoles df = flip gopt_unset Opt_AbstractRefHoleFits -- too spammy $ flip gopt_unset Opt_ShowDocsOfHoleFits -- not used @@ -460,9 +346,13 @@ setUpTypedHoles df $ flip gopt_unset Opt_SortValidHoleFits $ flip gopt_unset Opt_UnclutterValidHoleFits $ df - { refLevelHoleFits = Just 1 -- becomes slow at higher levels - , maxRefHoleFits = Just 10 -- quantity does not impact speed - , maxValidHoleFits = Nothing -- quantity does not impact speed + { refLevelHoleFits = refLevelHoleFits df <|> Just 1 -- becomes slow at higher levels + + -- Sometimes GHC can emit a lot of hole fits, this causes editors to be slow + -- or just crash, we limit the hole fits to 10. The number was chosen + -- arbirtarily by the author. + , maxRefHoleFits = maxRefHoleFits df <|> Just 10 + , maxValidHoleFits = maxValidHoleFits df <|> Just 10 } @@ -484,7 +374,9 @@ isQualifiedImport ImportDecl{ideclQualified = NotQualified} = False isQualifiedImport ImportDecl{} = True isQualifiedImport _ = False - +-- | Like getNodeIds but with generated node removed +getSourceNodeIds :: HieAST a -> Map.Map Identifier (IdentifierDetails a) +getSourceNodeIds = Map.foldl' combineNodeIds Map.empty . Map.filterWithKey (\k _ -> k == SourceInfo) . getSourcedNodeInfo . sourcedNodeInfo getNodeIds :: HieAST a -> Map.Map Identifier (IdentifierDetails a) getNodeIds = Map.foldl' combineNodeIds Map.empty . getSourcedNodeInfo . sourcedNodeInfo @@ -517,26 +409,24 @@ generatedNodeInfo :: HieAST a -> Maybe (NodeInfo a) generatedNodeInfo = Map.lookup GeneratedInfo . getSourcedNodeInfo . sourcedNodeInfo data GhcVersion - = GHC810 - | GHC90 - | GHC92 - | GHC94 - | GHC96 + = GHC96 | GHC98 - deriving (Eq, Ord, Show) + | GHC910 + | GHC912 + deriving (Eq, Ord, Show, Enum) ghcVersionStr :: String ghcVersionStr = VERSION_ghc ghcVersion :: GhcVersion -#if MIN_VERSION_GLASGOW_HASKELL(9,8,0,0) +#if MIN_VERSION_GLASGOW_HASKELL(9,12,0,0) +ghcVersion = GHC912 +#elif MIN_VERSION_GLASGOW_HASKELL(9,10,0,0) +ghcVersion = GHC910 +#elif MIN_VERSION_GLASGOW_HASKELL(9,8,0,0) ghcVersion = GHC98 -#elif MIN_VERSION_GLASGOW_HASKELL(9,6,0,0) +#else ghcVersion = GHC96 -#elif MIN_VERSION_GLASGOW_HASKELL(9,4,0,0) -ghcVersion = GHC94 -#elif MIN_VERSION_GLASGOW_HASKELL(9,2,0,0) -ghcVersion = GHC92 #endif simpleNodeInfoCompat :: FastStringCompat -> FastStringCompat -> NodeInfo a @@ -569,27 +459,12 @@ loadModulesHome -> HscEnv -> HscEnv loadModulesHome mod_infos e = -#if MIN_VERSION_ghc(9,3,0) hscUpdateHUG (\hug -> foldl' (flip addHomeModInfoToHug) hug mod_infos) (e { hsc_type_env_vars = emptyKnotVars }) -#else - let !new_modules = addListToHpt (hsc_HPT e) [(mod_name x, x) | x <- mod_infos] - in e { hsc_HPT = new_modules - , hsc_type_env_var = Nothing - } - where - mod_name = moduleName . mi_module . hm_iface -#endif recDotDot :: HsRecFields (GhcPass p) arg -> Maybe Int recDotDot x = -#if MIN_VERSION_ghc(9,5,0) unRecFieldsDotDot <$> -#endif unLoc <$> rec_dotdot x -#if MIN_VERSION_ghc(9,5,0) -extract_cons (NewTypeCon x) = [x] +extract_cons (NewTypeCon x) = [x] extract_cons (DataTypeCons _ xs) = xs -#else -extract_cons = id -#endif diff --git a/ghcide/src/Development/IDE/GHC/Compat/CmdLine.hs b/ghcide/src/Development/IDE/GHC/Compat/CmdLine.hs new file mode 100644 index 0000000000..7c9efb37e8 --- /dev/null +++ b/ghcide/src/Development/IDE/GHC/Compat/CmdLine.hs @@ -0,0 +1,19 @@ +{-# LANGUAGE CPP #-} + +-- | Compat module Interface file relevant code. +module Development.IDE.GHC.Compat.CmdLine ( + processCmdLineP + , CmdLineP (..) + , getCmdLineState + , putCmdLineState + , Flag(..) + , OptKind(..) + , EwM + , defFlag + , liftEwM + ) where + +import GHC.Driver.CmdLine +import GHC.Driver.Session (CmdLineP (..), getCmdLineState, + processCmdLineP, putCmdLineState) + diff --git a/ghcide/src/Development/IDE/GHC/Compat/Core.hs b/ghcide/src/Development/IDE/GHC/Compat/Core.hs index 767d23ef35..42f654b609 100644 --- a/ghcide/src/Development/IDE/GHC/Compat/Core.hs +++ b/ghcide/src/Development/IDE/GHC/Compat/Core.hs @@ -1,8 +1,5 @@ -{-# LANGUAGE CPP #-} -{-# LANGUAGE ConstraintKinds #-} -{-# LANGUAGE FlexibleInstances #-} -{-# LANGUAGE PatternSynonyms #-} -{-# LANGUAGE ViewPatterns #-} +{-# LANGUAGE CPP #-} +{-# LANGUAGE PatternSynonyms #-} -- | Compat Core module that handles the GHC module hierarchy re-organization -- by re-exporting everything we care about. @@ -61,9 +58,6 @@ module Development.IDE.GHC.Compat.Core ( pattern ExposePackage, parseDynamicFlagsCmdLine, parseDynamicFilePragma, -#if !MIN_VERSION_ghc(9,3,0) - WarnReason(..), -#endif wWarningFlags, updOptLevel, -- slightly unsafe @@ -75,17 +69,18 @@ module Development.IDE.GHC.Compat.Core ( IfaceTyCon(..), ModIface, ModIface_(..), +#if MIN_VERSION_ghc(9,11,0) + pattern ModIface, + set_mi_top_env, + set_mi_usages, +#endif HscSource(..), WhereFrom(..), loadInterface, -#if !MIN_VERSION_ghc(9,3,0) - SourceModified(..), -#endif loadModuleInterface, RecompileRequired(..), mkPartialIface, mkFullIface, - checkOldIface, IsBootInterface(..), -- * Fixity LexicalFixity(..), @@ -120,14 +115,11 @@ module Development.IDE.GHC.Compat.Core ( pattern ConPatIn, conPatDetails, mapConPatDetail, - mkVisFunTys, -- * Specs ImpDeclSpec(..), ImportSpec(..), -- * SourceText SourceText(..), - -- * Name - tyThingParent_maybe, -- * Ways Way, wayGeneralFlags, @@ -168,6 +160,7 @@ module Development.IDE.GHC.Compat.Core ( hscInteractive, hscSimplify, hscTypecheckRename, + hscUpdateHPT, Development.IDE.GHC.Compat.Core.makeSimpleDetails, -- * Typecheck utils tcSplitForAllTyVars, @@ -176,7 +169,6 @@ module Development.IDE.GHC.Compat.Core ( Development.IDE.GHC.Compat.Core.mkIfaceTc, Development.IDE.GHC.Compat.Core.mkBootModDetailsTc, Development.IDE.GHC.Compat.Core.initTidyOpts, - hscUpdateHPT, driverNoStop, tidyProgram, ImportedModsVal(..), @@ -204,8 +196,9 @@ module Development.IDE.GHC.Compat.Core ( pattern RealSrcLoc, SrcLoc.SrcLoc(SrcLoc.UnhelpfulLoc), BufSpan, - SrcSpanAnn', +#if !MIN_VERSION_ghc(9,9,0) GHC.SrcAnn, +#endif SrcLoc.leftmost_smallest, SrcLoc.containsSpan, SrcLoc.mkGeneralSrcSpan, @@ -232,18 +225,22 @@ module Development.IDE.GHC.Compat.Core ( SrcLoc.noSrcSpan, SrcLoc.noSrcLoc, SrcLoc.noLoc, + SrcLoc.srcSpanToRealSrcSpan, mapLoc, -- * Finder FindResult(..), mkHomeModLocation, - addBootSuffixLocnOut, findObjectLinkableMaybe, InstalledFindResult(..), -- * Module and Package ModuleOrigin(..), PackageName(..), -- * Linker +#if MIN_VERSION_ghc(9,11,0) + LinkablePart(..), +#else Unlinked(..), +#endif Linkable(..), unload, -- * Hooks @@ -262,7 +259,7 @@ module Development.IDE.GHC.Compat.Core ( -- * Driver-Make Target(..), TargetId(..), - mkModuleGraph, + mkSimpleTarget, -- * GHCi initObjLinker, loadDLL, @@ -284,8 +281,6 @@ module Development.IDE.GHC.Compat.Core ( Role(..), -- * Panic Plain.PlainGhcException, - panic, - panicDoc, -- * Other GHC.CoreModule(..), GHC.SafeHaskellMode(..), @@ -320,6 +315,7 @@ module Development.IDE.GHC.Compat.Core ( module GHC.HsToCore.Monad, module GHC.Iface.Syntax, + module GHC.Iface.Recomp, module GHC.Hs.Decls, module GHC.Hs.Expr, @@ -343,9 +339,8 @@ module Development.IDE.GHC.Compat.Core ( module GHC.Types.Basic, module GHC.Types.Id, - module GHC.Types.Name , + module GHC.Types.Name, module GHC.Types.Name.Set, - module GHC.Types.Name.Cache, module GHC.Types.Name.Env, module GHC.Types.Name.Reader, @@ -360,52 +355,38 @@ module Development.IDE.GHC.Compat.Core ( module GHC.Types.Unique.Supply, module GHC.Types.Var, module GHC.Unit.Module, + module GHC.Unit.Module.Graph, -- * Syntax re-exports module GHC.Hs, module GHC.Hs.Binds, module GHC.Parser, module GHC.Parser.Header, module GHC.Parser.Lexer, -#if MIN_VERSION_ghc(9,3,0) + module GHC.Utils.Panic, CompileReason(..), hsc_type_env_vars, - hscUpdateHUG, hscUpdateHPT, hsc_HUG, + hscUpdateHUG, hsc_HUG, GhcMessage(..), getKey, module GHC.Driver.Env.KnotVars, - module GHC.Iface.Recomp, module GHC.Linker.Types, - module GHC.Unit.Module.Graph, module GHC.Types.Unique.Map, module GHC.Utils.TmpFs, - module GHC.Utils.Panic, module GHC.Unit.Finder.Types, module GHC.Unit.Env, module GHC.Driver.Phases, -#endif -# if !MIN_VERSION_ghc(9,4,0) - pattern HsFieldBind, - hfbAnn, - hfbLHS, - hfbRHS, - hfbPun, -#endif -#if !MIN_VERSION_ghc_boot_th(9,4,1) - Extension(.., NamedFieldPuns), -#else Extension(..), -#endif - UniqFM, mkCgInteractiveGuts, justBytecode, justObjects, emptyHomeModInfoLinkable, homeModInfoByteCode, homeModInfoObject, -# if !MIN_VERSION_ghc(9,5,0) - field_label, -#endif groupOrigin, + isVisibleFunArg, +#if MIN_VERSION_ghc(9,8,0) + lookupGlobalRdrEnv +#endif ) where import qualified GHC @@ -413,182 +394,187 @@ import qualified GHC -- NOTE(ozkutuk): Cpp clashes Phase.Cpp, so we hide it. -- Not the greatest solution, but gets the job done -- (until the CPP extension is actually needed). -import GHC.LanguageExtensions.Type hiding (Cpp) +import GHC.LanguageExtensions.Type hiding (Cpp) -import GHC.Hs.Binds - --- See Note [Guidelines For Using CPP In GHCIDE Import Statements] - -import GHC.Builtin.Names hiding (Unique, printName) +import GHC.Builtin.Names hiding (Unique, printName) import GHC.Builtin.Types import GHC.Builtin.Types.Prim import GHC.Builtin.Utils +import GHC.Core (CoreProgram) import GHC.Core.Class import GHC.Core.Coercion import GHC.Core.ConLike -import GHC.Core.DataCon hiding (dataConExTyCoVars) -import qualified GHC.Core.DataCon as DataCon -import GHC.Core.FamInstEnv hiding (pprFamInst) +import GHC.Core.DataCon hiding (dataConExTyCoVars) +import qualified GHC.Core.DataCon as DataCon +import GHC.Core.FamInstEnv hiding (pprFamInst) import GHC.Core.InstEnv -import GHC.Types.Unique.FM import GHC.Core.PatSyn import GHC.Core.Predicate import GHC.Core.TyCo.Ppr -import qualified GHC.Core.TyCo.Rep as TyCoRep +import qualified GHC.Core.TyCo.Rep as TyCoRep import GHC.Core.TyCon -import GHC.Core.Type +import GHC.Core.Type import GHC.Core.Unify import GHC.Core.Utils -import GHC.Driver.CmdLine (Warn (..)) +import GHC.Driver.CmdLine (Warn (..)) import GHC.Driver.Hooks -import GHC.Driver.Main as GHC +import GHC.Driver.Main as GHC import GHC.Driver.Monad import GHC.Driver.Phases import GHC.Driver.Pipeline import GHC.Driver.Plugins -import GHC.Driver.Session hiding (ExposePackage) -import qualified GHC.Driver.Session as DynFlags +import GHC.Driver.Session hiding (ExposePackage) +import qualified GHC.Driver.Session as DynFlags +import GHC.Hs.Binds import GHC.HsToCore.Docs import GHC.HsToCore.Expr import GHC.HsToCore.Monad import GHC.Iface.Load -import GHC.Iface.Make as GHC +import GHC.Iface.Make as GHC import GHC.Iface.Recomp import GHC.Iface.Syntax -import GHC.Iface.Tidy as GHC +import GHC.Iface.Tidy as GHC import GHC.IfaceToCore import GHC.Parser -import GHC.Parser.Header hiding (getImports) -import GHC.Rename.Fixity (lookupFixityRn) +import GHC.Parser.Header hiding (getImports) +import GHC.Rename.Fixity (lookupFixityRn) import GHC.Rename.Names import GHC.Rename.Splice -import qualified GHC.Runtime.Interpreter as GHCi +import qualified GHC.Runtime.Interpreter as GHCi import GHC.Tc.Instance.Family import GHC.Tc.Module import GHC.Tc.Types -import GHC.Tc.Types.Evidence hiding ((<.>)) +import GHC.Tc.Types.Evidence hiding ((<.>)) import GHC.Tc.Utils.Env -import GHC.Tc.Utils.Monad hiding (Applicative (..), IORef, - MonadFix (..), MonadIO (..), - allM, anyM, concatMapM, - mapMaybeM, (<$>)) -import GHC.Tc.Utils.TcType as TcType -import qualified GHC.Types.Avail as Avail +import GHC.Tc.Utils.Monad hiding (Applicative (..), IORef, + MonadFix (..), MonadIO (..), allM, + anyM, concatMapM, mapMaybeM, foldMapM, + (<$>)) +import GHC.Tc.Utils.TcType as TcType +import qualified GHC.Types.Avail as Avail import GHC.Types.Basic import GHC.Types.Id -import GHC.Types.Name hiding (varName) +import GHC.Types.Name hiding (varName) import GHC.Types.Name.Cache import GHC.Types.Name.Env -import GHC.Types.Name.Reader hiding (GRE, gre_name, gre_imp, gre_lcl, gre_par) -import qualified GHC.Types.Name.Reader as RdrName -import GHC.Types.SrcLoc (BufPos, BufSpan, - SrcLoc (UnhelpfulLoc), - SrcSpan (UnhelpfulSpan)) -import qualified GHC.Types.SrcLoc as SrcLoc +import GHC.Types.Name.Reader hiding (GRE, gre_imp, gre_lcl, + gre_name, gre_par) +import qualified GHC.Types.Name.Reader as RdrName +import GHC.Types.SrcLoc (BufPos, BufSpan, + SrcLoc (UnhelpfulLoc), + SrcSpan (UnhelpfulSpan)) +import qualified GHC.Types.SrcLoc as SrcLoc +import GHC.Types.Unique.FM import GHC.Types.Unique.Supply -import GHC.Types.Var (Var (varName), setTyVarUnique, - setVarUnique) -import GHC.Unit.Info (PackageName (..)) -import GHC.Unit.Module hiding (ModLocation (..), UnitId, - moduleUnit, - toUnitId) -import qualified GHC.Unit.Module as Module -import GHC.Unit.State (ModuleOrigin (..)) -import GHC.Utils.Error (Severity (..), emptyMessages) -import GHC.Utils.Panic hiding (try) -import qualified GHC.Utils.Panic.Plain as Plain - - -import Data.Foldable (toList) +import GHC.Types.Var (Var (varName), setTyVarUnique, + setVarUnique) + +import qualified GHC.Types.Var as TypesVar +import GHC.Unit.Info (PackageName (..)) +import GHC.Unit.Module hiding (ModLocation (..), UnitId, + moduleUnit, toUnitId) +import qualified GHC.Unit.Module as Module +import GHC.Unit.State (ModuleOrigin (..)) +import GHC.Utils.Error (Severity (..), emptyMessages) +import GHC.Utils.Panic hiding (try) +import qualified GHC.Utils.Panic.Plain as Plain + + +import Data.Foldable (toList) +import GHC.Core.Multiplicity (scaledThing) import GHC.Data.Bag -import GHC.Core.Multiplicity (scaledThing) +import qualified GHC.Data.Strict as Strict +import qualified GHC.Driver.Config.Finder as GHC +import qualified GHC.Driver.Config.Tidy as GHC import GHC.Driver.Env -import GHC.Hs (HsModule (..), SrcSpanAnn') -import GHC.Hs.Decls hiding (FunDep) +import GHC.Driver.Env as GHCi +import GHC.Driver.Env.KnotVars +import GHC.Driver.Errors.Types +import GHC.Hs (HsModule (..)) +import GHC.Hs.Decls hiding (FunDep) import GHC.Hs.Doc import GHC.Hs.Expr import GHC.Hs.Extension import GHC.Hs.ImpExp import GHC.Hs.Pat import GHC.Hs.Type -import GHC.Hs.Utils hiding (collectHsBindsBinders) -import qualified GHC.Linker.Loader as Linker +import GHC.Hs.Utils hiding (collectHsBindsBinders) +import qualified GHC.Linker.Loader as Linker import GHC.Linker.Types -import GHC.Parser.Lexer hiding (initParserState, getPsMessages) -import GHC.Parser.Annotation (EpAnn (..)) +import GHC.Parser.Annotation (EpAnn (..)) +import GHC.Parser.Lexer hiding (getPsMessages, + initParserState) import GHC.Platform.Ways -import GHC.Runtime.Context (InteractiveImport (..)) -#if !MIN_VERSION_ghc(9,7,0) -import GHC.Types.Avail (greNamePrintableName) -#endif -import GHC.Types.Fixity (LexicalFixity (..), Fixity (..), defaultFixity) +import GHC.Runtime.Context (InteractiveImport (..)) +import GHC.Types.Fixity (Fixity (..), LexicalFixity (..), + defaultFixity) import GHC.Types.Meta import GHC.Types.Name.Set -import GHC.Types.SourceFile (HscSource (..)) +import GHC.Types.SourceFile (HscSource (..)) import GHC.Types.SourceText -import GHC.Types.Target (Target (..), TargetId (..)) +import GHC.Types.Target (Target (..), TargetId (..)) import GHC.Types.TyThing import GHC.Types.TyThing.Ppr -import GHC.Unit.Finder hiding (mkHomeModLocation) +import GHC.Types.Unique +import GHC.Types.Unique.Map +import GHC.Unit.Env +import GHC.Unit.Finder hiding (mkHomeModLocation) +import qualified GHC.Unit.Finder as GHC +import GHC.Unit.Finder.Types import GHC.Unit.Home.ModInfo +import GHC.Unit.Module.Graph import GHC.Unit.Module.Imported import GHC.Unit.Module.ModDetails import GHC.Unit.Module.ModGuts -import GHC.Unit.Module.ModIface (IfaceExport, ModIface (..), - ModIface_ (..), mi_fix) -import GHC.Unit.Module.ModSummary (ModSummary (..)) -import Language.Haskell.Syntax hiding (FunDep) - -#if !MIN_VERSION_ghc(9,3,0) -import GHC.Types.SourceFile (SourceModified(..)) -import GHC.Unit.Module.Graph (mkModuleGraph) -import qualified GHC.Unit.Finder as GHC +#if !MIN_VERSION_ghc(9,9,0) +import GHC.Hs (SrcSpanAnn') #endif +import GHC.Unit.Module.ModIface (IfaceExport, ModIface, + ModIface_ (..), mi_fix +#if MIN_VERSION_ghc(9,11,0) + , pattern ModIface + , set_mi_top_env + , set_mi_usages +#endif + ) +import GHC.Unit.Module.ModSummary (ModSummary (..)) +import GHC.Utils.Error (mkPlainErrorMsgEnvelope) +import GHC.Utils.Panic +import GHC.Utils.TmpFs +import Language.Haskell.Syntax hiding (FunDep) + -#if MIN_VERSION_ghc(9,3,0) -import GHC.Driver.Env.KnotVars -import GHC.Unit.Module.Graph -import GHC.Driver.Errors.Types -import GHC.Types.Unique.Map -import GHC.Types.Unique -import GHC.Utils.TmpFs -import GHC.Utils.Panic -import GHC.Unit.Finder.Types -import GHC.Unit.Env -import qualified GHC.Driver.Config.Tidy as GHC -import qualified GHC.Data.Strict as Strict -import GHC.Driver.Env as GHCi -import qualified GHC.Unit.Finder as GHC -import qualified GHC.Driver.Config.Finder as GHC +-- See Note [Guidelines For Using CPP In GHCIDE Import Statements] + +#if MIN_VERSION_ghc(9,11,0) +import System.OsPath +#endif + +#if !MIN_VERSION_ghc(9,7,0) +import GHC.Types.Avail (greNamePrintableName) +#endif + +#if !MIN_VERSION_ghc(9,9,0) +import GHC.Hs (SrcSpanAnn') #endif mkHomeModLocation :: DynFlags -> ModuleName -> FilePath -> IO Module.ModLocation -#if MIN_VERSION_ghc(9,3,0) -mkHomeModLocation df mn f = pure $ GHC.mkHomeModLocation (GHC.initFinderOpts df) mn f +#if MIN_VERSION_ghc(9,11,0) +mkHomeModLocation df mn f = + let osf = unsafeEncodeUtf f + in pure $ GHC.mkHomeModLocation (GHC.initFinderOpts df) mn osf #else -mkHomeModLocation = GHC.mkHomeModLocation +mkHomeModLocation df mn f = pure $ GHC.mkHomeModLocation (GHC.initFinderOpts df) mn f #endif -#if MIN_VERSION_ghc(9,3,0) -pattern RealSrcSpan :: SrcLoc.RealSrcSpan -> Maybe BufSpan -> SrcLoc.SrcSpan -#else pattern RealSrcSpan :: SrcLoc.RealSrcSpan -> Maybe BufSpan -> SrcLoc.SrcSpan -#endif -#if MIN_VERSION_ghc(9,3,0) pattern RealSrcSpan x y <- SrcLoc.RealSrcSpan x ((\case Strict.Nothing -> Nothing; Strict.Just a -> Just a) -> y) where RealSrcSpan x y = SrcLoc.RealSrcSpan x (case y of Nothing -> Strict.Nothing; Just a -> Strict.Just a) -#else -pattern RealSrcSpan x y = SrcLoc.RealSrcSpan x y -#endif {-# COMPLETE RealSrcSpan, UnhelpfulSpan #-} -#if MIN_VERSION_ghc(9,3,0) pattern RealSrcLoc :: SrcLoc.RealSrcLoc -> Strict.Maybe BufPos-> SrcLoc.SrcLoc -#else -pattern RealSrcLoc :: SrcLoc.RealSrcLoc -> Maybe BufPos-> SrcLoc.SrcLoc -#endif pattern RealSrcLoc x y = SrcLoc.RealSrcLoc x y {-# COMPLETE RealSrcLoc, UnhelpfulLoc #-} @@ -596,7 +582,7 @@ pattern RealSrcLoc x y = SrcLoc.RealSrcLoc x y pattern AvailTC :: Name -> [Name] -> [FieldLabel] -> Avail.AvailInfo #if __GLASGOW_HASKELL__ >= 907 pattern AvailTC n names pieces <- Avail.AvailTC n ((,[]) -> (names,pieces)) -#else +#else pattern AvailTC n names pieces <- Avail.AvailTC n ((\gres -> foldr (\gre (names, pieces) -> case gre of Avail.NormalGreName name -> (name: names, pieces) Avail.FieldGreName label -> (names, label:pieces)) ([], []) gres) -> (names, pieces)) @@ -605,14 +591,14 @@ pattern AvailTC n names pieces <- Avail.AvailTC n ((\gres -> foldr (\gre (names, pattern AvailName :: Name -> Avail.AvailInfo #if __GLASGOW_HASKELL__ >= 907 pattern AvailName n <- Avail.Avail n -#else +#else pattern AvailName n <- Avail.Avail (Avail.NormalGreName n) #endif pattern AvailFL :: FieldLabel -> Avail.AvailInfo #if __GLASGOW_HASKELL__ >= 907 pattern AvailFL fl <- (const Nothing -> Just fl) -- this pattern always fails as this field was removed in 9.7 -#else +#else pattern AvailFL fl <- Avail.Avail (Avail.FieldGreName fl) #endif @@ -629,11 +615,11 @@ pattern ExposePackage s a mr <- DynFlags.ExposePackage s a _ mr pattern ExposePackage s a mr = DynFlags.ExposePackage s a mr #endif -pattern FunTy :: Type -> Type -> Type -pattern FunTy arg res <- TyCoRep.FunTy {ft_arg = arg, ft_res = res} - --- type HasSrcSpan x a = (GenLocated SrcSpan a ~ x) --- type HasSrcSpan x = () :: Constraint +isVisibleFunArg :: Development.IDE.GHC.Compat.Core.FunTyFlag -> Bool +isVisibleFunArg = TypesVar.isVisibleFunArg +type FunTyFlag = TypesVar.FunTyFlag +pattern FunTy :: Development.IDE.GHC.Compat.Core.FunTyFlag -> Type -> Type -> Type +pattern FunTy af arg res <- TyCoRep.FunTy {ft_af = af, ft_arg = arg, ft_res = res} class HasSrcSpan a where getLoc :: a -> SrcSpan @@ -644,10 +630,22 @@ instance HasSrcSpan SrcSpan where instance HasSrcSpan (SrcLoc.GenLocated SrcSpan a) where getLoc = GHC.getLoc +#if MIN_VERSION_ghc(9,9,0) +instance HasSrcSpan (EpAnn a) where + getLoc = GHC.getHasLoc +#endif + +#if MIN_VERSION_ghc(9,9,0) +instance HasSrcSpan (SrcLoc.GenLocated (EpAnn ann) a) where + getLoc (L l _) = getLoc l +instance HasSrcSpan (SrcLoc.GenLocated (GHC.EpaLocation) a) where + getLoc = GHC.getHasLoc +#else instance HasSrcSpan (SrcSpanAnn' ann) where getLoc = GHC.locA instance HasSrcSpan (SrcLoc.GenLocated (SrcSpanAnn' ann) a) where getLoc (L l _) = l +#endif pattern L :: HasSrcSpan a => SrcSpan -> e -> SrcLoc.GenLocated a e pattern L l a <- GHC.L (getLoc -> l) a @@ -655,9 +653,15 @@ pattern L l a <- GHC.L (getLoc -> l) a -- This is from the old api, but it still simplifies pattern ConPatIn :: SrcLoc.Located (ConLikeP GhcPs) -> HsConPatDetails GhcPs -> Pat GhcPs +#if MIN_VERSION_ghc(9,9,0) +pattern ConPatIn con args <- ConPat _ (L _ (SrcLoc.noLoc -> con)) args + where + ConPatIn con args = ConPat GHC.noAnn (GHC.noLocA $ SrcLoc.unLoc con) args +#else pattern ConPatIn con args <- ConPat EpAnnNotUsed (L _ (SrcLoc.noLoc -> con)) args where ConPatIn con args = ConPat EpAnnNotUsed (GHC.noLocA $ SrcLoc.unLoc con) args +#endif conPatDetails :: Pat p -> Maybe (HsConPatDetails p) conPatDetails (ConPat _ _ args) = Just args @@ -673,8 +677,16 @@ initObjLinker env = GHCi.initObjLinker (GHCi.hscInterp env) loadDLL :: HscEnv -> String -> IO (Maybe String) -loadDLL env = - GHCi.loadDLL (GHCi.hscInterp env) +loadDLL env str = do + res <- GHCi.loadDLL (GHCi.hscInterp env) str +#if MIN_VERSION_ghc(9,11,0) || (MIN_VERSION_ghc(9, 8, 3) && !MIN_VERSION_ghc(9, 9, 0)) || (MIN_VERSION_ghc(9, 10, 2) && !MIN_VERSION_ghc(9, 11, 0)) + pure $ + case res of + Left err_msg -> Just err_msg + Right _ -> Nothing +#else + pure res +#endif unload :: HscEnv -> [Linkable] -> IO () unload hsc_env linkables = @@ -682,12 +694,6 @@ unload hsc_env linkables = (GHCi.hscInterp hsc_env) hsc_env linkables -#if !MIN_VERSION_ghc(9,3,0) -setOutputFile :: FilePath -> DynFlags -> DynFlags -setOutputFile f d = d { - outputFile_ = Just f - } -#endif isSubspanOfA :: LocatedAn la a -> LocatedAn lb b -> Bool isSubspanOfA a b = SrcLoc.isSubspanOf (GHC.getLocA a) (GHC.getLocA b) @@ -708,7 +714,7 @@ pattern GRE{gre_name, gre_par, gre_lcl, gre_imp} <- RdrName.GRE #endif ,gre_par, gre_lcl, gre_imp = (toList -> gre_imp)} -collectHsBindsBinders :: CollectPass p => Bag (XRec p (HsBindLR p idR)) -> [IdP p] +collectHsBindsBinders :: CollectPass p => LHsBindsLR p idR -> [IdP p] collectHsBindsBinders x = GHC.collectHsBindsBinders CollNoDictBinders x @@ -716,92 +722,32 @@ collectHsBindsBinders x = GHC.collectHsBindsBinders CollNoDictBinders x makeSimpleDetails :: HscEnv -> TcGblEnv -> IO ModDetails makeSimpleDetails hsc_env = GHC.makeSimpleDetails -#if MIN_VERSION_ghc(9,3,0) (hsc_logger hsc_env) -#else - hsc_env -#endif -mkIfaceTc hsc_env sf details _ms tcGblEnv = -- ms is only used in GHC >= 9.4 - GHC.mkIfaceTc hsc_env sf details -#if MIN_VERSION_ghc(9,3,0) - _ms -#endif - tcGblEnv +mkIfaceTc :: HscEnv -> GHC.SafeHaskellMode -> ModDetails -> ModSummary -> Maybe CoreProgram -> TcGblEnv -> IO ModIface +mkIfaceTc hscEnv shm md _ms _mcp = + GHC.mkIfaceTc hscEnv shm md _ms _mcp -- mcp::Maybe CoreProgram is only used in GHC >= 9.6 mkBootModDetailsTc :: HscEnv -> TcGblEnv -> IO ModDetails mkBootModDetailsTc session = GHC.mkBootModDetailsTc -#if MIN_VERSION_ghc(9,3,0) (hsc_logger session) -#else - session -#endif -#if !MIN_VERSION_ghc(9,3,0) -type TidyOpts = HscEnv -#endif initTidyOpts :: HscEnv -> IO TidyOpts initTidyOpts = -#if MIN_VERSION_ghc(9,3,0) GHC.initTidyOpts -#else - pure -#endif -driverNoStop = -#if MIN_VERSION_ghc(9,3,0) - NoStop -#else - StopLn -#endif +driverNoStop :: StopPhase +driverNoStop = NoStop -#if !MIN_VERSION_ghc(9,3,0) -hscUpdateHPT :: (HomePackageTable -> HomePackageTable) -> HscEnv -> HscEnv -hscUpdateHPT k session = session { hsc_HPT = k (hsc_HPT session) } -#endif - -#if !MIN_VERSION_ghc(9,4,0) -pattern HsFieldBind :: XHsRecField id -> id -> arg -> Bool -> HsRecField' id arg -pattern HsFieldBind {hfbAnn, hfbLHS, hfbRHS, hfbPun} <- HsRecField hfbAnn (SrcLoc.unLoc -> hfbLHS) hfbRHS hfbPun where - HsFieldBind ann lhs rhs pun = HsRecField ann (SrcLoc.noLoc lhs) rhs pun -#endif - -#if !MIN_VERSION_ghc_boot_th(9,4,1) -pattern NamedFieldPuns :: Extension -pattern NamedFieldPuns = RecordPuns -#endif - -#if MIN_VERSION_ghc(9,5,0) -mkVisFunTys = mkScaledFunctionTys +groupOrigin :: MatchGroup GhcRn body -> Origin mapLoc :: (a -> b) -> SrcLoc.GenLocated l a -> SrcLoc.GenLocated l b mapLoc = fmap groupOrigin = mg_ext -#else -mapLoc :: (a -> b) -> SrcLoc.GenLocated l a -> SrcLoc.GenLocated l b -mapLoc = SrcLoc.mapLoc -groupOrigin :: MatchGroup p body -> Origin -groupOrigin = mg_origin -#endif - -#if !MIN_VERSION_ghc(9,5,0) -mkCgInteractiveGuts :: CgGuts -> CgGuts -mkCgInteractiveGuts = id +mkSimpleTarget :: DynFlags -> FilePath -> Target +mkSimpleTarget df fp = Target (TargetFile fp Nothing) True (homeUnitId_ df) Nothing -emptyHomeModInfoLinkable :: Maybe Linkable -emptyHomeModInfoLinkable = Nothing - -justBytecode :: Linkable -> Maybe Linkable -justBytecode = Just - -justObjects :: Linkable -> Maybe Linkable -justObjects = Just - -homeModInfoByteCode, homeModInfoObject :: HomeModInfo -> Maybe Linkable -homeModInfoByteCode = hm_linkable -homeModInfoObject = hm_linkable - -field_label :: a -> a -field_label = id +#if MIN_VERSION_ghc(9,7,0) +lookupGlobalRdrEnv gre_env occ = lookupGRE gre_env (LookupOccName occ AllRelevantGREs) #endif diff --git a/ghcide/src/Development/IDE/GHC/Compat/Driver.hs b/ghcide/src/Development/IDE/GHC/Compat/Driver.hs new file mode 100644 index 0000000000..6ab1d26df2 --- /dev/null +++ b/ghcide/src/Development/IDE/GHC/Compat/Driver.hs @@ -0,0 +1,142 @@ +-- ============================================================================ +-- DO NOT EDIT +-- This module copies parts of the driver code in GHC.Driver.Main to provide +-- `hscTypecheckRenameWithDiagnostics`. +-- Issue to add this function: https://gitlab.haskell.org/ghc/ghc/-/issues/24996 +-- MR to add this function: https://gitlab.haskell.org/ghc/ghc/-/merge_requests/12891 +-- ============================================================================ + +{-# LANGUAGE CPP #-} + +module Development.IDE.GHC.Compat.Driver + ( hscTypecheckRenameWithDiagnostics + ) where + +#if MIN_VERSION_ghc(9,11,0) + +import GHC.Driver.Main (hscTypecheckRenameWithDiagnostics) + +#else + +import Control.Monad +import GHC.Core +import GHC.Data.FastString +import GHC.Data.Maybe +import GHC.Driver.Env +import GHC.Driver.Errors.Types +import GHC.Driver.Main +import GHC.Driver.Session +import GHC.Hs +import GHC.Hs.Dump +import GHC.Iface.Ext.Ast (mkHieFile) +import GHC.Iface.Ext.Binary (hie_file_result, readHieFile, + writeHieFile) +import GHC.Iface.Ext.Debug (diffFile, validateScopes) +import GHC.Iface.Ext.Types (getAsts, hie_asts, hie_module) +import GHC.Tc.Module +import GHC.Tc.Utils.Monad +import GHC.Types.SourceFile +import GHC.Types.SrcLoc +import GHC.Unit +import GHC.Unit.Module.ModDetails +import GHC.Unit.Module.ModIface +import GHC.Unit.Module.ModSummary +import GHC.Utils.Error +import GHC.Utils.Logger +import GHC.Utils.Outputable +import GHC.Utils.Panic.Plain + +hscTypecheckRenameWithDiagnostics :: HscEnv -> ModSummary -> HsParsedModule + -> IO ((TcGblEnv, RenamedStuff), Messages GhcMessage) +hscTypecheckRenameWithDiagnostics hsc_env mod_summary rdr_module = + runHsc' hsc_env $ hsc_typecheck True mod_summary (Just rdr_module) + +-- ============================================================================ +-- DO NOT EDIT - Refer to top of file +-- ============================================================================ +hsc_typecheck :: Bool -- ^ Keep renamed source? + -> ModSummary -> Maybe HsParsedModule + -> Hsc (TcGblEnv, RenamedStuff) +hsc_typecheck keep_rn mod_summary mb_rdr_module = do + hsc_env <- getHscEnv + let hsc_src = ms_hsc_src mod_summary + dflags = hsc_dflags hsc_env + home_unit = hsc_home_unit hsc_env + outer_mod = ms_mod mod_summary + mod_name = moduleName outer_mod + outer_mod' = mkHomeModule home_unit mod_name + inner_mod = homeModuleNameInstantiation home_unit mod_name + src_filename = ms_hspp_file mod_summary + real_loc = realSrcLocSpan $ mkRealSrcLoc (mkFastString src_filename) 1 1 + keep_rn' = gopt Opt_WriteHie dflags || keep_rn + massert (isHomeModule home_unit outer_mod) + tc_result <- if hsc_src == HsigFile && not (isHoleModule inner_mod) + then ioMsgMaybe $ hoistTcRnMessage $ tcRnInstantiateSignature hsc_env outer_mod' real_loc + else + do hpm <- case mb_rdr_module of + Just hpm -> return hpm + Nothing -> hscParse' mod_summary + tc_result0 <- tcRnModule' mod_summary keep_rn' hpm + if hsc_src == HsigFile + then + do (iface, _) <- liftIO $ hscSimpleIface hsc_env Nothing tc_result0 mod_summary + ioMsgMaybe $ hoistTcRnMessage $ + tcRnMergeSignatures hsc_env hpm tc_result0 iface + else return tc_result0 + rn_info <- extract_renamed_stuff mod_summary tc_result + return (tc_result, rn_info) + +-- ============================================================================ +-- DO NOT EDIT - Refer to top of file +-- ============================================================================ +extract_renamed_stuff :: ModSummary -> TcGblEnv -> Hsc RenamedStuff +extract_renamed_stuff mod_summary tc_result = do + let rn_info = getRenamedStuff tc_result + + dflags <- getDynFlags + logger <- getLogger + liftIO $ putDumpFileMaybe logger Opt_D_dump_rn_ast "Renamer" + FormatHaskell (showAstData NoBlankSrcSpan NoBlankEpAnnotations rn_info) + + -- Create HIE files + when (gopt Opt_WriteHie dflags) $ do + -- I assume this fromJust is safe because `-fwrite-hie-file` + -- enables the option which keeps the renamed source. + hieFile <- mkHieFile mod_summary tc_result (fromJust rn_info) + let out_file = ml_hie_file $ ms_location mod_summary + liftIO $ writeHieFile out_file hieFile + liftIO $ putDumpFileMaybe logger Opt_D_dump_hie "HIE AST" FormatHaskell (ppr $ hie_asts hieFile) + + -- Validate HIE files + when (gopt Opt_ValidateHie dflags) $ do + hs_env <- Hsc $ \e w -> return (e, w) + liftIO $ do + -- Validate Scopes + case validateScopes (hie_module hieFile) $ getAsts $ hie_asts hieFile of + [] -> putMsg logger $ text "Got valid scopes" + xs -> do + putMsg logger $ text "Got invalid scopes" + mapM_ (putMsg logger) xs + -- Roundtrip testing + file' <- readHieFile (hsc_NC hs_env) out_file + case diffFile hieFile (hie_file_result file') of + [] -> + putMsg logger $ text "Got no roundtrip errors" + xs -> do + putMsg logger $ text "Got roundtrip errors" + let logger' = updateLogFlags logger (log_set_dopt Opt_D_ppr_debug) + mapM_ (putMsg logger') xs + return rn_info + +-- ============================================================================ +-- DO NOT EDIT - Refer to top of file +-- ============================================================================ +hscSimpleIface :: HscEnv + -> Maybe CoreProgram + -> TcGblEnv + -> ModSummary + -> IO (ModIface, ModDetails) +hscSimpleIface hsc_env mb_core_program tc_result summary + = runHsc hsc_env $ hscSimpleIface' mb_core_program tc_result summary + +#endif diff --git a/ghcide/src/Development/IDE/GHC/Compat/Env.hs b/ghcide/src/Development/IDE/GHC/Compat/Env.hs index b7b268b5b0..cbccc1a3de 100644 --- a/ghcide/src/Development/IDE/GHC/Compat/Env.hs +++ b/ghcide/src/Development/IDE/GHC/Compat/Env.hs @@ -4,11 +4,7 @@ -- 'UnitEnv' and some DynFlags compat functions. module Development.IDE.GHC.Compat.Env ( Env.HscEnv(hsc_FC, hsc_NC, hsc_IC, hsc_mod_graph -#if MIN_VERSION_ghc(9,3,0) , hsc_type_env_vars -#else - , hsc_type_env_var -#endif ), Env.hsc_HPT, InteractiveContext(..), @@ -29,7 +25,7 @@ module Development.IDE.GHC.Compat.Env ( Home.mkHomeModule, -- * Provide backwards Compatible -- types and helper functions. - Logger(..), + Logger, UnitEnv, hscSetUnitEnv, hscSetFlags, @@ -52,13 +48,16 @@ module Development.IDE.GHC.Compat.Env ( setBackend, ghciBackend, Development.IDE.GHC.Compat.Env.platformDefaultBackend, + workingDirectory, + setWorkingDirectory, + hscSetActiveUnitId, + reexportedModules, ) where import GHC (setInteractiveDynFlags) --- See Note [Guidelines For Using CPP In GHCIDE Import Statements] - import GHC.Driver.Backend as Backend +import GHC.Driver.Env (HscEnv, hscSetActiveUnitId) import qualified GHC.Driver.Env as Env import GHC.Driver.Hooks (Hooks) import GHC.Driver.Session @@ -71,19 +70,12 @@ import GHC.Unit.Types (UnitId) import GHC.Utils.Logger import GHC.Utils.TmpFs -#if !MIN_VERSION_ghc(9,3,0) -import GHC.Driver.Env (HscEnv, hsc_EPS) -#endif - -#if MIN_VERSION_ghc(9,3,0) -import GHC.Driver.Env (HscEnv) -#endif -#if MIN_VERSION_ghc(9,3,0) hsc_EPS :: HscEnv -> UnitEnv hsc_EPS = Env.hsc_unit_env -#endif +setWorkingDirectory :: FilePath -> DynFlags -> DynFlags +setWorkingDirectory p d = d { workingDirectory = Just p } setHomeUnitId_ :: UnitId -> DynFlags -> DynFlags setHomeUnitId_ uid df = df { Session.homeUnitId_ = uid } @@ -113,22 +105,14 @@ hscHomeUnit = setBytecodeLinkerOptions :: DynFlags -> DynFlags setBytecodeLinkerOptions df = df { ghcLink = LinkInMemory -#if MIN_VERSION_ghc(9,5,0) , backend = noBackend -#else - , backend = NoBackend -#endif , ghcMode = CompManager } setInterpreterLinkerOptions :: DynFlags -> DynFlags setInterpreterLinkerOptions df = df { ghcLink = LinkInMemory -#if MIN_VERSION_ghc(9,5,0) , backend = interpreterBackend -#else - , backend = Interpreter -#endif , ghcMode = CompManager } diff --git a/ghcide/src/Development/IDE/GHC/Compat/Error.hs b/ghcide/src/Development/IDE/GHC/Compat/Error.hs new file mode 100644 index 0000000000..0255886726 --- /dev/null +++ b/ghcide/src/Development/IDE/GHC/Compat/Error.hs @@ -0,0 +1,84 @@ +{-# LANGUAGE CPP #-} +{-# LANGUAGE TemplateHaskell #-} +module Development.IDE.GHC.Compat.Error ( + -- * Top-level error types and lens for easy access + MsgEnvelope(..), + msgEnvelopeErrorL, + GhcMessage(..), + -- * Error messages for the typechecking and renamer phase + TcRnMessage (..), + TcRnMessageDetailed (..), + stripTcRnMessageContext, + -- * Parsing error message + PsMessage(..), + -- * Desugaring diagnostic + DsMessage (..), + -- * Driver error message + DriverMessage (..), + -- * General Diagnostics + Diagnostic(..), + -- * Prisms for error selection + _TcRnMessage, + _TcRnMessageWithCtx, + _GhcPsMessage, + _GhcDsMessage, + _GhcDriverMessage, + _TcRnMissingSignature, + ) where + +import Control.Lens +import GHC.Driver.Errors.Types +import GHC.HsToCore.Errors.Types +import GHC.Tc.Errors.Types +import GHC.Types.Error + +-- | Some 'TcRnMessage's are nested in other constructors for additional context. +-- For example, 'TcRnWithHsDocContext' and 'TcRnMessageWithInfo'. +-- However, in most occasions you don't need the additional context and you just want +-- the error message. @'_TcRnMessage'@ recursively unwraps these constructors, +-- until there are no more constructors with additional context. +-- +-- Use @'_TcRnMessageWithCtx'@ if you need the additional context. You can always +-- strip it later using @'stripTcRnMessageContext'@. +-- +_TcRnMessage :: Fold GhcMessage TcRnMessage +_TcRnMessage = _TcRnMessageWithCtx . to stripTcRnMessageContext + +_TcRnMessageWithCtx :: Prism' GhcMessage TcRnMessage +_TcRnMessageWithCtx = prism' GhcTcRnMessage (\case + GhcTcRnMessage tcRnMsg -> Just tcRnMsg + _ -> Nothing) + +_GhcPsMessage :: Prism' GhcMessage PsMessage +_GhcPsMessage = prism' GhcPsMessage (\case + GhcPsMessage psMsg -> Just psMsg + _ -> Nothing) + +_GhcDsMessage :: Prism' GhcMessage DsMessage +_GhcDsMessage = prism' GhcDsMessage (\case + GhcDsMessage dsMsg -> Just dsMsg + _ -> Nothing) + +_GhcDriverMessage :: Prism' GhcMessage DriverMessage +_GhcDriverMessage = prism' GhcDriverMessage (\case + GhcDriverMessage driverMsg -> Just driverMsg + _ -> Nothing) + +-- | Some 'TcRnMessage's are nested in other constructors for additional context. +-- For example, 'TcRnWithHsDocContext' and 'TcRnMessageWithInfo'. +-- However, in some occasions you don't need the additional context and you just want +-- the error message. @'stripTcRnMessageContext'@ recursively unwraps these constructors, +-- until there are no more constructors with additional context. +-- +stripTcRnMessageContext :: TcRnMessage -> TcRnMessage +stripTcRnMessageContext = \case +#if MIN_VERSION_ghc(9, 6, 1) + TcRnWithHsDocContext _ tcMsg -> stripTcRnMessageContext tcMsg +#endif + TcRnMessageWithInfo _ (TcRnMessageDetailed _ tcMsg) -> stripTcRnMessageContext tcMsg + msg -> msg + +msgEnvelopeErrorL :: Lens' (MsgEnvelope e) e +msgEnvelopeErrorL = lens errMsgDiagnostic (\envelope e -> envelope { errMsgDiagnostic = e } ) + +makePrisms ''TcRnMessage diff --git a/ghcide/src/Development/IDE/GHC/Compat/Iface.hs b/ghcide/src/Development/IDE/GHC/Compat/Iface.hs index d848083a4b..39cf9e0d45 100644 --- a/ghcide/src/Development/IDE/GHC/Compat/Iface.hs +++ b/ghcide/src/Development/IDE/GHC/Compat/Iface.hs @@ -9,6 +9,9 @@ module Development.IDE.GHC.Compat.Iface ( import Development.IDE.GHC.Compat.Env import Development.IDE.GHC.Compat.Outputable import GHC +import GHC.Driver.Session (targetProfile) +import qualified GHC.Iface.Load as Iface +import GHC.Unit.Finder.Types (FindResult) -- See Note [Guidelines For Using CPP In GHCIDE Import Statements] @@ -17,19 +20,11 @@ import GHC.Iface.Errors.Ppr (missingInterfaceErrorDia import GHC.Iface.Errors.Types (IfaceMessage) #endif - -import qualified GHC.Iface.Load as Iface -import GHC.Unit.Finder.Types (FindResult) - -#if MIN_VERSION_ghc(9,3,0) -import GHC.Driver.Session (targetProfile) -#endif - writeIfaceFile :: HscEnv -> FilePath -> ModIface -> IO () -#if MIN_VERSION_ghc(9,3,0) -writeIfaceFile env fp iface = Iface.writeIface (hsc_logger env) (targetProfile $ hsc_dflags env) fp iface +#if MIN_VERSION_ghc(9,11,0) +writeIfaceFile env fp iface = Iface.writeIface (hsc_logger env) (targetProfile $ hsc_dflags env) (Iface.flagsToIfCompression $ hsc_dflags env) fp iface #else -writeIfaceFile env fp iface = Iface.writeIface (hsc_logger env) (hsc_dflags env) fp iface +writeIfaceFile env fp iface = Iface.writeIface (hsc_logger env) (targetProfile $ hsc_dflags env) fp iface #endif cannotFindModule :: HscEnv -> ModuleName -> FindResult -> SDoc diff --git a/ghcide/src/Development/IDE/GHC/Compat/Logger.hs b/ghcide/src/Development/IDE/GHC/Compat/Logger.hs index b89dea0488..c3cc5247d0 100644 --- a/ghcide/src/Development/IDE/GHC/Compat/Logger.hs +++ b/ghcide/src/Development/IDE/GHC/Compat/Logger.hs @@ -13,39 +13,23 @@ import Development.IDE.GHC.Compat.Core import Development.IDE.GHC.Compat.Env as Env import Development.IDE.GHC.Compat.Outputable --- See Note [Guidelines For Using CPP In GHCIDE Import Statements] -import GHC.Utils.Outputable - -import GHC.Utils.Logger as Logger - -#if MIN_VERSION_ghc(9,3,0) import GHC.Types.Error -#endif +import GHC.Utils.Logger as Logger +import GHC.Utils.Outputable putLogHook :: Logger -> HscEnv -> HscEnv putLogHook logger env = env { hsc_logger = logger } -#if MIN_VERSION_ghc(9,3,0) type LogActionCompat = LogFlags -> Maybe DiagnosticReason -> Maybe Severity -> SrcSpan -> PrintUnqualified -> SDoc -> IO () -- alwaysQualify seems to still do the right thing here, according to the "unqualified warnings" test. logActionCompat :: LogActionCompat -> LogAction #if MIN_VERSION_ghc(9,7,0) logActionCompat logAction logFlags (MCDiagnostic severity (ResolvedDiagnosticReason wr) _) loc = logAction logFlags (Just wr) (Just severity) loc alwaysQualify -#elif MIN_VERSION_ghc(9,5,0) -logActionCompat logAction logFlags (MCDiagnostic severity wr _) loc = logAction logFlags (Just wr) (Just severity) loc alwaysQualify #else -logActionCompat logAction logFlags (MCDiagnostic severity wr) loc = logAction logFlags (Just wr) (Just severity) loc alwaysQualify +logActionCompat logAction logFlags (MCDiagnostic severity wr _) loc = logAction logFlags (Just wr) (Just severity) loc alwaysQualify #endif logActionCompat logAction logFlags _cls loc = logAction logFlags Nothing Nothing loc alwaysQualify -#else -type LogActionCompat = DynFlags -> WarnReason -> Severity -> SrcSpan -> PrintUnqualified -> SDoc -> IO () - --- alwaysQualify seems to still do the right thing here, according to the "unqualified warnings" test. -logActionCompat :: LogActionCompat -> LogAction -logActionCompat logAction dynFlags wr severity loc = logAction dynFlags wr severity loc alwaysQualify - -#endif diff --git a/ghcide/src/Development/IDE/GHC/Compat/Outputable.hs b/ghcide/src/Development/IDE/GHC/Compat/Outputable.hs index cd86f25e33..ccec23c9c3 100644 --- a/ghcide/src/Development/IDE/GHC/Compat/Outputable.hs +++ b/ghcide/src/Development/IDE/GHC/Compat/Outputable.hs @@ -10,21 +10,18 @@ module Development.IDE.GHC.Compat.Outputable ( printSDocQualifiedUnsafe, printWithoutUniques, mkPrintUnqualifiedDefault, - PrintUnqualified(..), + PrintUnqualified, defaultUserStyle, withPprStyle, -- * Parser errors PsWarning, PsError, -#if MIN_VERSION_ghc(9,5,0) defaultDiagnosticOpts, GhcMessage, DriverMessage, Messages, initDiagOpts, pprMessages, -#endif -#if MIN_VERSION_ghc(9,3,0) DiagnosticReason(..), renderDiagnosticMessageWithHints, pprMsgEnvelopeBagWithLoc, @@ -34,10 +31,6 @@ module Development.IDE.GHC.Compat.Outputable ( errMsgDiagnostic, unDecorated, diagnosticMessage, -#else - pprWarning, - pprError, -#endif -- * Error infrastructure DecoratedSDoc, MsgEnvelope, @@ -53,43 +46,30 @@ module Development.IDE.GHC.Compat.Outputable ( textDoc, ) where --- See Note [Guidelines For Using CPP In GHCIDE Import Statements] - - +import Data.Maybe +import GHC.Driver.Config.Diagnostic import GHC.Driver.Env +import GHC.Driver.Errors.Types (DriverMessage, GhcMessage) import GHC.Driver.Ppr import GHC.Driver.Session +import GHC.Parser.Errors.Types import qualified GHC.Types.Error as Error -#if MIN_VERSION_ghc(9,7,0) -import GHC.Types.Error (defaultDiagnosticOpts) -#endif import GHC.Types.Name.Ppr import GHC.Types.Name.Reader import GHC.Types.SourceError import GHC.Types.SrcLoc import GHC.Unit.State -import GHC.Utils.Error hiding (mkWarnMsg) +import GHC.Utils.Error import GHC.Utils.Outputable as Out import GHC.Utils.Panic -#if !MIN_VERSION_ghc(9,3,0) -import GHC.Parser.Errors -import qualified GHC.Parser.Errors.Ppr as Ppr -#endif - -#if MIN_VERSION_ghc(9,3,0) -import Data.Maybe -import GHC.Driver.Config.Diagnostic -import GHC.Parser.Errors.Types -#endif +-- See Note [Guidelines For Using CPP In GHCIDE Import Statements] -#if MIN_VERSION_ghc(9,5,0) -import GHC.Driver.Errors.Types (DriverMessage, GhcMessage) +#if MIN_VERSION_ghc(9,7,0) +import GHC.Types.Error (defaultDiagnosticOpts) #endif -#if MIN_VERSION_ghc(9,5,0) type PrintUnqualified = NamePprCtx -#endif -- | A compatible function to print `Outputable` instances -- without unique symbols. @@ -113,75 +93,41 @@ printSDocQualifiedUnsafe unqual doc = doc' = pprWithUnitState emptyUnitState doc -#if !MIN_VERSION_ghc(9,3,0) -pprWarning :: PsWarning -> MsgEnvelope DecoratedSDoc -pprWarning = - Ppr.pprWarning - -pprError :: PsError -> MsgEnvelope DecoratedSDoc -pprError = - Ppr.pprError -#endif formatErrorWithQual :: DynFlags -> MsgEnvelope DecoratedSDoc -> String formatErrorWithQual dflags e = showSDoc dflags (pprNoLocMsgEnvelope e) -#if MIN_VERSION_ghc(9,3,0) pprNoLocMsgEnvelope :: MsgEnvelope DecoratedSDoc -> SDoc -#else -pprNoLocMsgEnvelope :: Error.RenderableDiagnostic e => MsgEnvelope e -> SDoc -#endif pprNoLocMsgEnvelope (MsgEnvelope { errMsgDiagnostic = e , errMsgContext = unqual }) = sdocWithContext $ \_ctx -> withErrStyle unqual $ #if MIN_VERSION_ghc(9,7,0) - (formatBulleted e) -#elif MIN_VERSION_ghc(9,3,0) - (formatBulleted _ctx $ e) + formatBulleted e #else - (formatBulleted _ctx $ Error.renderDiagnostic e) + formatBulleted _ctx e #endif -type ErrMsg = MsgEnvelope DecoratedSDoc -#if MIN_VERSION_ghc(9,3,0) -type WarnMsg = MsgEnvelope DecoratedSDoc -#endif +type ErrMsg = MsgEnvelope GhcMessage +type WarnMsg = MsgEnvelope GhcMessage mkPrintUnqualifiedDefault :: HscEnv -> GlobalRdrEnv -> PrintUnqualified -#if MIN_VERSION_ghc(9,5,0) mkPrintUnqualifiedDefault env = mkNamePprCtx ptc (hsc_unit_env env) where ptc = initPromotionTickContext (hsc_dflags env) -#else -mkPrintUnqualifiedDefault env = - -- GHC 9.2 version - -- mkPrintUnqualified :: UnitEnv -> GlobalRdrEnv -> PrintUnqualified - mkPrintUnqualified (hsc_unit_env env) -#endif -#if MIN_VERSION_ghc(9,3,0) renderDiagnosticMessageWithHints :: forall a. Diagnostic a => a -> DecoratedSDoc renderDiagnosticMessageWithHints a = Error.unionDecoratedSDoc (diagnosticMessage -#if MIN_VERSION_ghc(9,5,0) (defaultDiagnosticOpts @a) -#endif a) (mkDecorated $ map ppr $ diagnosticHints a) -#endif -#if MIN_VERSION_ghc(9,3,0) mkWarnMsg :: DynFlags -> Maybe DiagnosticReason -> b -> SrcSpan -> PrintUnqualified -> SDoc -> MsgEnvelope DecoratedSDoc mkWarnMsg df reason _logFlags l st doc = fmap renderDiagnosticMessageWithHints $ mkMsgEnvelope (initDiagOpts df) l st (mkPlainDiagnostic (fromMaybe WarningWithoutFlag reason) [] doc) -#else -mkWarnMsg :: a -> b -> DynFlags -> SrcSpan -> PrintUnqualified -> SDoc -> MsgEnvelope DecoratedSDoc -mkWarnMsg _ _ = - const Error.mkWarnMsg -#endif textDoc :: String -> SDoc textDoc = text diff --git a/ghcide/src/Development/IDE/GHC/Compat/Parser.hs b/ghcide/src/Development/IDE/GHC/Compat/Parser.hs index 3d87cc3a91..8e2967ed30 100644 --- a/ghcide/src/Development/IDE/GHC/Compat/Parser.hs +++ b/ghcide/src/Development/IDE/GHC/Compat/Parser.hs @@ -1,36 +1,30 @@ {-# LANGUAGE CPP #-} {-# LANGUAGE PatternSynonyms #-} -{-# HLINT ignore "Unused LANGUAGE pragma" #-} -- | Parser compatibility module. module Development.IDE.GHC.Compat.Parser ( initParserOpts, initParserState, - ApiAnns, PsSpan(..), pattern HsParsedModule, type GHC.HsParsedModule, Development.IDE.GHC.Compat.Parser.hpm_module, Development.IDE.GHC.Compat.Parser.hpm_src_files, - Development.IDE.GHC.Compat.Parser.hpm_annotations, pattern ParsedModule, Development.IDE.GHC.Compat.Parser.pm_parsed_source, type GHC.ParsedModule, Development.IDE.GHC.Compat.Parser.pm_mod_summary, Development.IDE.GHC.Compat.Parser.pm_extra_src_files, - Development.IDE.GHC.Compat.Parser.pm_annotations, - mkApiAnns, -- * API Annotations +#if !MIN_VERSION_ghc(9,11,0) Anno.AnnKeywordId(..), +#endif pattern EpaLineComment, pattern EpaBlockComment ) where import Development.IDE.GHC.Compat.Core import Development.IDE.GHC.Compat.Util - --- See Note [Guidelines For Using CPP In GHCIDE Import Statements] - import qualified GHC.Parser.Annotation as Anno import qualified GHC.Parser.Lexer as Lexer import GHC.Types.SrcLoc (PsSpan (..)) @@ -42,15 +36,8 @@ import GHC (EpaCommentTok (..), pm_mod_summary, pm_parsed_source) import qualified GHC -import GHC.Hs (hpm_module, hpm_src_files) - -#if !MIN_VERSION_ghc(9,3,0) -import qualified GHC.Driver.Config as Config -#endif - -#if MIN_VERSION_ghc(9,3,0) import qualified GHC.Driver.Config.Parser as Config -#endif +import GHC.Hs (hpm_module, hpm_src_files) @@ -62,34 +49,24 @@ initParserState :: ParserOpts -> StringBuffer -> RealSrcLoc -> PState initParserState = Lexer.initParserState --- GHC 9.2 does not have ApiAnns anymore packaged in ParsedModule. Now the --- annotations are found in the ast. -type ApiAnns = () - -#if MIN_VERSION_ghc(9,5,0) -pattern HsParsedModule :: Located (HsModule GhcPs) -> [FilePath] -> ApiAnns -> GHC.HsParsedModule -#else -pattern HsParsedModule :: Located HsModule -> [FilePath] -> ApiAnns -> GHC.HsParsedModule -#endif +pattern HsParsedModule :: Located (HsModule GhcPs) -> [FilePath] -> GHC.HsParsedModule pattern HsParsedModule { hpm_module , hpm_src_files - , hpm_annotations - } <- ( (,()) -> (GHC.HsParsedModule{..}, hpm_annotations)) + } <- GHC.HsParsedModule{..} where - HsParsedModule hpm_module hpm_src_files _hpm_annotations = + HsParsedModule hpm_module hpm_src_files = GHC.HsParsedModule hpm_module hpm_src_files -pattern ParsedModule :: ModSummary -> ParsedSource -> [FilePath] -> ApiAnns -> GHC.ParsedModule +pattern ParsedModule :: ModSummary -> ParsedSource -> [FilePath] -> GHC.ParsedModule pattern ParsedModule { pm_mod_summary , pm_parsed_source , pm_extra_src_files - , pm_annotations - } <- ( (,()) -> (GHC.ParsedModule{..}, pm_annotations)) + } <- GHC.ParsedModule{..} where - ParsedModule ms parsed extra_src_files _anns = + ParsedModule ms parsed extra_src_files = GHC.ParsedModule { pm_mod_summary = ms , pm_parsed_source = parsed @@ -97,6 +74,4 @@ pattern ParsedModule } {-# COMPLETE ParsedModule :: GHC.ParsedModule #-} -mkApiAnns :: PState -> ApiAnns -mkApiAnns = const () diff --git a/ghcide/src/Development/IDE/GHC/Compat/Plugins.hs b/ghcide/src/Development/IDE/GHC/Compat/Plugins.hs index 09c4ff720a..35bf48374b 100644 --- a/ghcide/src/Development/IDE/GHC/Compat/Plugins.hs +++ b/ghcide/src/Development/IDE/GHC/Compat/Plugins.hs @@ -7,8 +7,6 @@ module Development.IDE.GHC.Compat.Plugins ( defaultPlugin, PluginWithArgs(..), applyPluginsParsedResultAction, - initializePlugins, - initPlugins, -- * Static plugins StaticPlugin(..), @@ -20,89 +18,32 @@ module Development.IDE.GHC.Compat.Plugins ( ) where import Development.IDE.GHC.Compat.Core -import Development.IDE.GHC.Compat.Env (hscSetFlags, hsc_dflags) -import Development.IDE.GHC.Compat.Parser as Parser +import Development.IDE.GHC.Compat.Parser as Parser --- See Note [Guidelines For Using CPP In GHCIDE Import Statements] +import qualified GHC.Driver.Env as Env +import GHC.Driver.Plugins (ParsedResult (..), + Plugin (..), + PluginWithArgs (..), + PsMessages (..), + StaticPlugin (..), + defaultPlugin, + staticPlugins, withPlugins) +import qualified GHC.Parser.Lexer as Lexer -import GHC.Driver.Plugins (Plugin (..), - PluginWithArgs (..), - StaticPlugin (..), - defaultPlugin, - withPlugins) -import qualified GHC.Runtime.Loader as Loader -#if !MIN_VERSION_ghc(9,3,0) -import Development.IDE.GHC.Compat.Outputable as Out -#endif - -import qualified GHC.Driver.Env as Env - -#if !MIN_VERSION_ghc(9,3,0) -import Data.Bifunctor (bimap) -#endif - -#if !MIN_VERSION_ghc(9,3,0) -import Development.IDE.GHC.Compat.Util (Bag) -#endif - -#if MIN_VERSION_ghc(9,3,0) -import GHC.Driver.Plugins (ParsedResult (..), - PsMessages (..), - staticPlugins) -import qualified GHC.Parser.Lexer as Lexer -#endif - - -#if !MIN_VERSION_ghc(9,3,0) -type PsMessages = (Bag WarnMsg, Bag ErrMsg) -#endif - -getPsMessages :: PState -> DynFlags -> PsMessages -getPsMessages pst _dflags = --dfags is only used if GHC < 9.2 -#if MIN_VERSION_ghc(9,3,0) +getPsMessages :: PState -> PsMessages +getPsMessages pst = uncurry PsMessages $ Lexer.getPsMessages pst -#else - bimap (fmap pprWarning) (fmap pprError) $ - getMessages pst -#endif -applyPluginsParsedResultAction :: HscEnv -> DynFlags -> ModSummary -> Parser.ApiAnns -> ParsedSource -> PsMessages -> IO (ParsedSource, PsMessages) -applyPluginsParsedResultAction env _dflags ms hpm_annotations parsed msgs = do - -- dflags is only used in GHC < 9.2 +applyPluginsParsedResultAction :: HscEnv -> ModSummary -> ParsedSource -> PsMessages -> IO (ParsedSource, PsMessages) +applyPluginsParsedResultAction env ms parsed msgs = do -- Apply parsedResultAction of plugins let applyPluginAction p opts = parsedResultAction p opts ms -#if MIN_VERSION_ghc(9,3,0) - fmap (\result -> (hpm_module (parsedResultModule result), (parsedResultMessages result))) $ runHsc env $ withPlugins -#else - fmap (\parsed_module -> (hpm_module parsed_module, msgs)) $ runHsc env $ withPlugins -#endif -#if MIN_VERSION_ghc(9,3,0) + fmap (\result -> (hpm_module (parsedResultModule result), parsedResultMessages result)) $ runHsc env $ withPlugins (Env.hsc_plugins env) -#else - env -#endif applyPluginAction -#if MIN_VERSION_ghc(9,3,0) - (ParsedResult (HsParsedModule parsed [] hpm_annotations) msgs) -#else - (HsParsedModule parsed [] hpm_annotations) -#endif - -initializePlugins :: HscEnv -> IO HscEnv -initializePlugins env = do - Loader.initializePlugins env + (ParsedResult (HsParsedModule parsed []) msgs) --- | Plugins aren't stored in ModSummary anymore since GHC 9.2, but this --- function still returns it for compatibility with 8.10 -initPlugins :: HscEnv -> ModSummary -> IO (ModSummary, HscEnv) -initPlugins session modSummary = do - session1 <- initializePlugins (hscSetFlags (ms_hspp_opts modSummary) session) - return (modSummary{ms_hspp_opts = hsc_dflags session1}, session1) hsc_static_plugins :: HscEnv -> [StaticPlugin] -#if MIN_VERSION_ghc(9,3,0) hsc_static_plugins = staticPlugins . Env.hsc_plugins -#else -hsc_static_plugins = Env.hsc_static_plugins -#endif diff --git a/ghcide/src/Development/IDE/GHC/Compat/Units.hs b/ghcide/src/Development/IDE/GHC/Compat/Units.hs index 2082cf10d0..f7f634e448 100644 --- a/ghcide/src/Development/IDE/GHC/Compat/Units.hs +++ b/ghcide/src/Development/IDE/GHC/Compat/Units.hs @@ -1,14 +1,10 @@ -{-# LANGUAGE CPP #-} -{-# LANGUAGE PatternSynonyms #-} +{-# LANGUAGE CPP #-} -- | Compat module for 'UnitState' and 'UnitInfo'. module Development.IDE.GHC.Compat.Units ( -- * UnitState UnitState, -#if MIN_VERSION_ghc(9,3,0) initUnits, -#endif - oldInitUnits, unitState, getUnitName, explicitUnits, @@ -39,7 +35,7 @@ module Development.IDE.GHC.Compat.Units ( installedModule, -- * Module toUnitId, - Development.IDE.GHC.Compat.Units.moduleUnitId, + moduleUnitId, moduleUnit, -- * ExternalPackageState ExternalPackageState(..), @@ -56,9 +52,17 @@ import Development.IDE.GHC.Compat.Env import Development.IDE.GHC.Compat.Outputable import Prelude hiding (mod) --- See Note [Guidelines For Using CPP In GHCIDE Import Statements] - +import Control.Monad +import qualified Data.List.NonEmpty as NE +import qualified Data.Map.Strict as Map +import qualified GHC +import qualified GHC.Data.ShortText as ST +import qualified GHC.Driver.Session as DynFlags +import GHC.Types.PkgQual (PkgQual (NoPkgQual)) import GHC.Types.Unique.Set +import GHC.Unit.External +import qualified GHC.Unit.Finder as GHC +import GHC.Unit.Home.ModInfo import qualified GHC.Unit.Info as UnitInfo import GHC.Unit.State (LookupResult, UnitInfo, UnitInfoMap, @@ -70,33 +74,6 @@ import GHC.Unit.State (LookupResult, UnitInfo, unitPackageVersion) import qualified GHC.Unit.State as State import GHC.Unit.Types -import qualified GHC.Unit.Types as Unit - - -#if !MIN_VERSION_ghc(9,3,0) -import GHC.Data.FastString - -#endif - -import qualified GHC.Data.ShortText as ST -import GHC.Unit.External -import qualified GHC.Unit.Finder as GHC - -#if !MIN_VERSION_ghc(9,3,0) -import GHC.Unit.Env -import GHC.Unit.Finder hiding - (findImportedModule) -#endif - -#if MIN_VERSION_ghc(9,3,0) -import Control.Monad -import qualified Data.List.NonEmpty as NE -import qualified Data.Map.Strict as Map -import qualified GHC -import qualified GHC.Driver.Session as DynFlags -import GHC.Types.PkgQual (PkgQual (NoPkgQual)) -import GHC.Unit.Home.ModInfo -#endif type PreloadUnitClosure = UniqSet UnitId @@ -104,14 +81,13 @@ type PreloadUnitClosure = UniqSet UnitId unitState :: HscEnv -> UnitState unitState = ue_units . hsc_unit_env -#if MIN_VERSION_ghc(9,3,0) createUnitEnvFromFlags :: NE.NonEmpty DynFlags -> HomeUnitGraph createUnitEnvFromFlags unitDflags = let newInternalUnitEnv dflags = mkHomeUnitEnv dflags emptyHomePackageTable Nothing unitEnvList = NE.map (\dflags -> (homeUnitId_ dflags, newInternalUnitEnv dflags)) unitDflags in - unitEnv_new (Map.fromList (NE.toList (unitEnvList))) + unitEnv_new (Map.fromList (NE.toList unitEnvList)) initUnits :: [DynFlags] -> HscEnv -> IO HscEnv initUnits unitDflags env = do @@ -144,21 +120,11 @@ initUnits unitDflags env = do , ue_eps = ue_eps (hsc_unit_env env) } pure $ hscSetFlags dflags1 $ hscSetUnitEnv unit_env env -#endif --- | oldInitUnits only needs to modify DynFlags for GHC <9.2 --- For GHC >= 9.2, we need to set the hsc_unit_env also, that is --- done later by initUnits -oldInitUnits :: DynFlags -> IO DynFlags -oldInitUnits = pure explicitUnits :: UnitState -> [Unit] explicitUnits ue = -#if MIN_VERSION_ghc(9,3,0) map fst $ State.explicitUnits ue -#else - State.explicitUnits ue -#endif listVisibleModuleNames :: HscEnv -> [ModuleName] listVisibleModuleNames env = @@ -171,11 +137,7 @@ getUnitName env i = lookupModuleWithSuggestions :: HscEnv -> ModuleName -#if MIN_VERSION_ghc(9,3,0) -> GHC.PkgQual -#else - -> Maybe FastString -#endif -> LookupResult lookupModuleWithSuggestions env modname mpkg = State.lookupModuleWithSuggestions (unitState env) modname mpkg @@ -210,10 +172,6 @@ installedModule :: unit -> ModuleName -> GenModule unit installedModule = Module -moduleUnitId :: Module -> UnitId -moduleUnitId = - Unit.toUnitId . Unit.moduleUnit - filterInplaceUnits :: [UnitId] -> [PackageFlag] -> ([UnitId], [PackageFlag]) filterInplaceUnits us packageFlags = partitionEithers (map isInplace packageFlags) @@ -230,11 +188,7 @@ showSDocForUser' env = showSDocForUser (hsc_dflags env) (unitState env) findImportedModule :: HscEnv -> ModuleName -> IO (Maybe Module) findImportedModule env mn = do -#if MIN_VERSION_ghc(9,3,0) res <- GHC.findImportedModule env mn NoPkgQual -#else - res <- GHC.findImportedModule env mn Nothing -#endif case res of Found _ mod -> pure . pure $ mod _ -> pure Nothing diff --git a/ghcide/src/Development/IDE/GHC/Compat/Util.hs b/ghcide/src/Development/IDE/GHC/Compat/Util.hs index f1f7d6937e..1f9e3a1609 100644 --- a/ghcide/src/Development/IDE/GHC/Compat/Util.hs +++ b/ghcide/src/Development/IDE/GHC/Compat/Util.hs @@ -1,5 +1,4 @@ -{-# LANGUAGE CPP #-} -{-# LANGUAGE ConstraintKinds #-} +{-# LANGUAGE CPP #-} -- | GHC Utils and Datastructures re-exports. -- -- Mainly handles module hierarchy re-organisation of GHC @@ -67,13 +66,11 @@ module Development.IDE.GHC.Compat.Util ( atEnd, ) where --- See Note [Guidelines For Using CPP In GHCIDE Import Statements] - import Control.Exception.Safe (MonadCatch, catch, try) import GHC.Data.Bag +import GHC.Data.Bool import GHC.Data.BooleanFormula import GHC.Data.EnumSet - import GHC.Data.FastString import GHC.Data.Maybe import GHC.Data.Pair @@ -83,12 +80,3 @@ import GHC.Types.Unique.DFM import GHC.Utils.Fingerprint import GHC.Utils.Outputable (pprHsString) import GHC.Utils.Panic hiding (try) - -#if !MIN_VERSION_ghc(9,3,0) -import GHC.Utils.Misc -#endif - -#if MIN_VERSION_ghc(9,3,0) -import GHC.Data.Bool -#endif - diff --git a/ghcide/src/Development/IDE/GHC/CoreFile.hs b/ghcide/src/Development/IDE/GHC/CoreFile.hs index 4fddbe75df..9977ad573b 100644 --- a/ghcide/src/Development/IDE/GHC/CoreFile.hs +++ b/ghcide/src/Development/IDE/GHC/CoreFile.hs @@ -1,6 +1,5 @@ -{-# LANGUAGE CPP #-} -{-# LANGUAGE FlexibleInstances #-} -{-# LANGUAGE RecordWildCards #-} +{-# LANGUAGE CPP #-} +{-# LANGUAGE RecordWildCards #-} -- | CoreFiles let us serialize Core to a file in order to later recover it -- without reparsing or retypechecking @@ -11,7 +10,7 @@ module Development.IDE.GHC.CoreFile , readBinCoreFile , writeBinCoreFile , getImplicitBinds - , occNamePrefixes) where + ) where import Control.Monad import Control.Monad.IO.Class @@ -22,22 +21,20 @@ import Data.Maybe import qualified Data.Text as T import Development.IDE.GHC.Compat import qualified Development.IDE.GHC.Compat.Util as Util -import GHC.Fingerprint -import Prelude hiding (mod) - --- See Note [Guidelines For Using CPP In GHCIDE Import Statements] - import GHC.Core import GHC.CoreToIface +import GHC.Fingerprint import GHC.Iface.Binary import GHC.Iface.Env +#if MIN_VERSION_ghc(9,11,0) +import qualified GHC.Iface.Load as Iface +#endif import GHC.Iface.Recomp.Binary (fingerprintBinMem) import GHC.IfaceToCore import GHC.Types.Id.Make -import GHC.Utils.Binary - - import GHC.Types.TypeEnv +import GHC.Utils.Binary +import Prelude hiding (mod) -- | Initial ram buffer to allocate for writing interface files @@ -93,14 +90,20 @@ readBinCoreFile name_cache fat_hi_path = do return (file, fp) -- | Write a core file -writeBinCoreFile :: FilePath -> CoreFile -> IO Fingerprint -writeBinCoreFile core_path fat_iface = do +writeBinCoreFile :: DynFlags -> FilePath -> CoreFile -> IO Fingerprint +writeBinCoreFile _dflags core_path fat_iface = do bh <- openBinMem initBinMemSize let quietTrace = QuietBinIFace - putWithUserData quietTrace bh fat_iface + putWithUserData + quietTrace +#if MIN_VERSION_ghc(9,11,0) + (Iface.flagsToIfCompression _dflags) +#endif + bh + fat_iface -- And send the result to the file writeBinMem bh core_path @@ -115,21 +118,8 @@ codeGutsToCoreFile :: Fingerprint -- ^ Hash of the interface this was generated from -> CgGuts -> CoreFile -#if MIN_VERSION_ghc(9,5,0) -- In GHC 9.6, implicit binds are tidied and part of core binds codeGutsToCoreFile hash CgGuts{..} = CoreFile (map (toIfaceTopBind1 cg_module) cg_binds) hash -#else -codeGutsToCoreFile hash CgGuts{..} = CoreFile (map (toIfaceTopBind1 cg_module) $ filter isNotImplictBind cg_binds) hash - --- | Implicit binds can be generated from the interface and are not tidied, --- so we must filter them out -isNotImplictBind :: CoreBind -> Bool -isNotImplictBind bind = any (not . isImplicitId) $ bindBindings bind - -bindBindings :: CoreBind -> [Var] -bindBindings (NonRec b _) = [b] -bindBindings (Rec bnds) = map fst bnds -#endif getImplicitBinds :: TyCon -> [CoreBind] getImplicitBinds tc = cls_binds ++ getTyConImplicitBinds tc @@ -147,7 +137,11 @@ getClassImplicitBinds cls | (op, val_index) <- classAllSelIds cls `zip` [0..] ] get_defn :: Id -> CoreBind -get_defn identifier = NonRec identifier (unfoldingTemplate (realIdUnfolding identifier)) +get_defn identifier = NonRec identifier templ + where + templ = case maybeUnfoldingTemplate (realIdUnfolding identifier) of + Nothing -> error "get_dfn: no unfolding template" + Just x -> x toIfaceTopBndr1 :: Module -> Id -> IfaceId toIfaceTopBndr1 mod identifier @@ -189,7 +183,7 @@ tcTopIfaceBindings1 :: IORef TypeEnv -> [TopIfaceBinding IfaceId] -> IfL [CoreBind] tcTopIfaceBindings1 ty_var ver_decls = do - int <- mapM (traverse $ tcIfaceId) ver_decls + int <- mapM (traverse tcIfaceId) ver_decls let all_ids = concatMap toList int liftIO $ modifyIORef ty_var (flip extendTypeEnvList $ map AnId all_ids) extendIfaceIdEnv all_ids $ mapM tc_iface_bindings int @@ -203,6 +197,7 @@ tcIfaceId = fmap getIfaceId . tcIfaceDecl False <=< unmangle_decl_name name' <- newIfaceName (mkVarOcc $ getOccString name) pure $ ifid{ ifName = name' } | otherwise = pure ifid + unmangle_decl_name _ifid = error "tcIfaceId: got non IfaceId: " -- invariant: 'IfaceId' is always a 'IfaceId' constructor getIfaceId (AnId identifier) = identifier getIfaceId _ = error "tcIfaceId: got non Id" @@ -212,47 +207,6 @@ tc_iface_bindings (TopIfaceNonRec v e) = do e' <- tcIfaceExpr e pure $ NonRec v e' tc_iface_bindings (TopIfaceRec vs) = do - vs' <- traverse (\(v, e) -> (,) <$> pure v <*> tcIfaceExpr e) vs + vs' <- traverse (\(v, e) -> (v,) <$> tcIfaceExpr e) vs pure $ Rec vs' --- | Prefixes that can occur in a GHC OccName -occNamePrefixes :: [T.Text] -occNamePrefixes = - [ - -- long ones - "$con2tag_" - , "$tag2con_" - , "$maxtag_" - - -- four chars - , "$sel:" - , "$tc'" - - -- three chars - , "$dm" - , "$co" - , "$tc" - , "$cp" - , "$fx" - - -- two chars - , "$W" - , "$w" - , "$m" - , "$b" - , "$c" - , "$d" - , "$i" - , "$s" - , "$f" - , "$r" - , "C:" - , "N:" - , "D:" - , "$p" - , "$L" - , "$f" - , "$t" - , "$c" - , "$m" - ] diff --git a/ghcide/src/Development/IDE/GHC/Error.hs b/ghcide/src/Development/IDE/GHC/Error.hs index 8b5c9edc29..048987f8ae 100644 --- a/ghcide/src/Development/IDE/GHC/Error.hs +++ b/ghcide/src/Development/IDE/GHC/Error.hs @@ -1,11 +1,15 @@ -{-# LANGUAGE CPP #-} +{-# LANGUAGE CPP #-} +{-# LANGUAGE DisambiguateRecordFields #-} -- Copyright (c) 2019 The DAML Authors. All rights reserved. -- SPDX-License-Identifier: Apache-2.0 module Development.IDE.GHC.Error ( -- * Producing Diagnostic values - diagFromErrMsgs + diagFromGhcErrorMessages + , diagFromErrMsgs , diagFromErrMsg + , diagFromSDocErrMsgs + , diagFromSDocErrMsg , diagFromString , diagFromStrings , diagFromGhcException @@ -17,6 +21,8 @@ module Development.IDE.GHC.Error , realSrcSpanToRange , realSrcLocToPosition , realSrcSpanToLocation + , realSrcSpanToCodePointRange + , realSrcLocToCodePointPosition , srcSpanToFilename , rangeToSrcSpan , rangeToRealSrcSpan @@ -31,10 +37,13 @@ module Development.IDE.GHC.Error , toDSeverity ) where +import Control.Lens import Data.Maybe import Data.String (fromString) import qualified Data.Text as T -import Development.IDE.GHC.Compat (DecoratedSDoc, MsgEnvelope, +import Data.Tuple.Extra (uncurry3) +import Development.IDE.GHC.Compat (GhcMessage, MsgEnvelope, + errMsgDiagnostic, errMsgSeverity, errMsgSpan, formatErrorWithQual, srcErrorMessages) @@ -45,32 +54,49 @@ import Development.IDE.Types.Diagnostics as D import Development.IDE.Types.Location import GHC import Language.LSP.Protocol.Types (isSubrangeOf) +import Language.LSP.VFS (CodePointPosition (CodePointPosition), + CodePointRange (CodePointRange)) -diagFromText :: T.Text -> D.DiagnosticSeverity -> SrcSpan -> T.Text -> FileDiagnostic -diagFromText diagSource sev loc msg = (toNormalizedFilePath' $ fromMaybe noFilePath $ srcSpanToFilename loc,ShowDiag,) - Diagnostic - { _range = fromMaybe noRange $ srcSpanToRange loc - , _severity = Just sev - , _source = Just diagSource -- not shown in the IDE, but useful for ghcide developers - , _message = msg - , _code = Nothing - , _relatedInformation = Nothing - , _tags = Nothing - , _codeDescription = Nothing - , _data_ = Nothing - } +diagFromText :: T.Text -> D.DiagnosticSeverity -> SrcSpan -> T.Text -> Maybe (MsgEnvelope GhcMessage) -> FileDiagnostic +diagFromText diagSource sev loc msg origMsg = + D.ideErrorWithSource + (Just diagSource) (Just sev) + (toNormalizedFilePath' $ fromMaybe noFilePath $ srcSpanToFilename loc) + msg origMsg + & fdLspDiagnosticL %~ \diag -> diag { D._range = fromMaybe noRange $ srcSpanToRange loc } -- | Produce a GHC-style error from a source span and a message. -diagFromErrMsg :: T.Text -> DynFlags -> MsgEnvelope DecoratedSDoc -> [FileDiagnostic] -diagFromErrMsg diagSource dflags e = - [ diagFromText diagSource sev (errMsgSpan e) - $ T.pack $ formatErrorWithQual dflags e - | Just sev <- [toDSeverity $ errMsgSeverity e]] - -diagFromErrMsgs :: T.Text -> DynFlags -> Compat.Bag (MsgEnvelope DecoratedSDoc) -> [FileDiagnostic] +diagFromErrMsg :: T.Text -> DynFlags -> MsgEnvelope GhcMessage -> [FileDiagnostic] +diagFromErrMsg diagSource dflags origErr = + let err = fmap (\e -> (Compat.renderDiagnosticMessageWithHints e, Just origErr)) origErr + in + diagFromSDocWithOptionalOrigMsg diagSource dflags err + +-- | Compatibility function for creating '[FileDiagnostic]' from +-- a 'Compat.Bag' of GHC error messages. +-- The function signature changes based on the GHC version. +-- While this is not desirable, it avoids more CPP statements in code +-- that implements actual logic. +diagFromGhcErrorMessages :: T.Text -> DynFlags -> Compat.Bag (MsgEnvelope GhcMessage) -> [FileDiagnostic] +diagFromGhcErrorMessages sourceParser dflags errs = + diagFromErrMsgs sourceParser dflags errs + +diagFromErrMsgs :: T.Text -> DynFlags -> Compat.Bag (MsgEnvelope GhcMessage) -> [FileDiagnostic] diagFromErrMsgs diagSource dflags = concatMap (diagFromErrMsg diagSource dflags) . Compat.bagToList +diagFromSDocErrMsg :: T.Text -> DynFlags -> MsgEnvelope Compat.DecoratedSDoc -> [FileDiagnostic] +diagFromSDocErrMsg diagSource dflags err = + diagFromSDocWithOptionalOrigMsg diagSource dflags (fmap (,Nothing) err) + +diagFromSDocErrMsgs :: T.Text -> DynFlags -> Compat.Bag (MsgEnvelope Compat.DecoratedSDoc) -> [FileDiagnostic] +diagFromSDocErrMsgs diagSource dflags = concatMap (diagFromSDocErrMsg diagSource dflags) . Compat.bagToList + +diagFromSDocWithOptionalOrigMsg :: T.Text -> DynFlags -> MsgEnvelope (Compat.DecoratedSDoc, Maybe (MsgEnvelope GhcMessage)) -> [FileDiagnostic] +diagFromSDocWithOptionalOrigMsg diagSource dflags err = + [ diagFromText diagSource sev (errMsgSpan err) (T.pack (formatErrorWithQual dflags (fmap fst err))) (snd (errMsgDiagnostic err)) + | Just sev <- [toDSeverity $ errMsgSeverity err]] + -- | Convert a GHC SrcSpan to a DAML compiler Range srcSpanToRange :: SrcSpan -> Maybe Range srcSpanToRange (UnhelpfulSpan _) = Nothing @@ -86,6 +112,29 @@ realSrcLocToPosition :: RealSrcLoc -> Position realSrcLocToPosition real = Position (fromIntegral $ srcLocLine real - 1) (fromIntegral $ srcLocCol real - 1) +-- Note [Unicode support] +-- ~~~~~~~~~~~~~~~~~~~~~~ +-- the current situation is: +-- LSP Positions use UTF-16 code units(Unicode may count as variable columns); +-- GHC use Unicode code points(Unicode count as one column). +-- To support unicode, ideally range should be in lsp standard, +-- and codePoint should be in ghc standard. +-- see https://github.com/haskell/lsp/pull/407 + +-- | Convert a GHC SrcSpan to CodePointRange +-- see Note [Unicode support] +realSrcSpanToCodePointRange :: RealSrcSpan -> CodePointRange +realSrcSpanToCodePointRange real = + CodePointRange + (realSrcLocToCodePointPosition $ Compat.realSrcSpanStart real) + (realSrcLocToCodePointPosition $ Compat.realSrcSpanEnd real) + +-- | Convert a GHC RealSrcLoc to CodePointPosition +-- see Note [Unicode support] +realSrcLocToCodePointPosition :: RealSrcLoc -> CodePointPosition +realSrcLocToCodePointPosition real = + CodePointPosition (fromIntegral $ srcLocLine real - 1) (fromIntegral $ srcLocCol real - 1) + -- | Extract a file name from a GHC SrcSpan (use message for unhelpful ones) -- FIXME This may not be an _absolute_ file name, needs fixing. srcSpanToFilename :: SrcSpan -> Maybe FilePath @@ -130,27 +179,19 @@ spanContainsRange srcSpan range = (range `isSubrangeOf`) <$> srcSpanToRange srcS -- | Convert a GHC severity to a DAML compiler Severity. Severities below -- "Warning" level are dropped (returning Nothing). toDSeverity :: GHC.Severity -> Maybe D.DiagnosticSeverity -#if !MIN_VERSION_ghc(9,3,0) -toDSeverity SevOutput = Nothing -toDSeverity SevInteractive = Nothing -toDSeverity SevDump = Nothing -toDSeverity SevInfo = Just DiagnosticSeverity_Information -toDSeverity SevFatal = Just DiagnosticSeverity_Error -#else -toDSeverity SevIgnore = Nothing -#endif -toDSeverity SevWarning = Just DiagnosticSeverity_Warning -toDSeverity SevError = Just DiagnosticSeverity_Error +toDSeverity SevIgnore = Nothing +toDSeverity SevWarning = Just DiagnosticSeverity_Warning +toDSeverity SevError = Just DiagnosticSeverity_Error -- | Produce a bag of GHC-style errors (@ErrorMessages@) from the given -- (optional) locations and message strings. -diagFromStrings :: T.Text -> D.DiagnosticSeverity -> [(SrcSpan, String)] -> [FileDiagnostic] -diagFromStrings diagSource sev = concatMap (uncurry (diagFromString diagSource sev)) +diagFromStrings :: T.Text -> D.DiagnosticSeverity -> [(SrcSpan, String, Maybe (MsgEnvelope GhcMessage))] -> [FileDiagnostic] +diagFromStrings diagSource sev = concatMap (uncurry3 (diagFromString diagSource sev)) -- | Produce a GHC-style error from a source span and a message. -diagFromString :: T.Text -> D.DiagnosticSeverity -> SrcSpan -> String -> [FileDiagnostic] -diagFromString diagSource sev sp x = [diagFromText diagSource sev sp $ T.pack x] +diagFromString :: T.Text -> D.DiagnosticSeverity -> SrcSpan -> String -> Maybe (MsgEnvelope GhcMessage) -> [FileDiagnostic] +diagFromString diagSource sev sp x origMsg = [diagFromText diagSource sev sp (T.pack x) origMsg] -- | Produces an "unhelpful" source span with the given string. @@ -180,15 +221,11 @@ catchSrcErrors dflags fromWhere ghcM = do Right <$> ghcM where ghcExceptionToDiagnostics = return . Left . diagFromGhcException fromWhere dflags - sourceErrorToDiagnostics = return . Left . diagFromErrMsgs fromWhere dflags -#if MIN_VERSION_ghc(9,3,0) - . fmap (fmap Compat.renderDiagnosticMessageWithHints) . Compat.getMessages -#endif - . srcErrorMessages - + sourceErrorToDiagnostics diag = pure $ Left $ + diagFromErrMsgs fromWhere dflags (Compat.getMessages (srcErrorMessages diag)) diagFromGhcException :: T.Text -> DynFlags -> GhcException -> [FileDiagnostic] -diagFromGhcException diagSource dflags exc = diagFromString diagSource DiagnosticSeverity_Error (noSpan "") (showGHCE dflags exc) +diagFromGhcException diagSource dflags exc = diagFromString diagSource DiagnosticSeverity_Error (noSpan "") (showGHCE dflags exc) Nothing showGHCE :: DynFlags -> GhcException -> String showGHCE dflags exc = case exc of diff --git a/ghcide/src/Development/IDE/GHC/Orphans.hs b/ghcide/src/Development/IDE/GHC/Orphans.hs index d8d16ca69f..543c6f4387 100644 --- a/ghcide/src/Development/IDE/GHC/Orphans.hs +++ b/ghcide/src/Development/IDE/GHC/Orphans.hs @@ -1,46 +1,40 @@ -- Copyright (c) 2019 The DAML Authors. All rights reserved. -- SPDX-License-Identifier: Apache-2.0 -{-# LANGUAGE CPP #-} -{-# LANGUAGE FlexibleInstances #-} +{-# LANGUAGE CPP #-} {-# OPTIONS_GHC -Wno-orphans #-} -- | Orphan instances for GHC. -- Note that the 'NFData' instances may not be law abiding. module Development.IDE.GHC.Orphans() where -import Development.IDE.GHC.Compat +import Development.IDE.GHC.Compat hiding + (DuplicateRecordFields, + FieldSelectors) import Development.IDE.GHC.Util import Control.DeepSeq -import Control.Monad.Trans.Reader (ReaderT (..)) +import Control.Monad.Trans.Reader (ReaderT (..)) import Data.Aeson import Data.Hashable -import Data.String (IsString (fromString)) -import Data.Text (unpack) - --- See Note [Guidelines For Using CPP In GHCIDE Import Statements] +import Data.String (IsString (fromString)) +import Data.Text (unpack) +import Data.Bifunctor (Bifunctor (..)) import GHC.ByteCode.Types import GHC.Data.Bag import GHC.Data.FastString -import qualified GHC.Data.StringBuffer as SB -import GHC.Types.SrcLoc - -#if !MIN_VERSION_ghc(9,3,0) -import GHC (ModuleGraph) -import GHC.Types.Unique (getKey) -#endif - -import Data.Bifunctor (Bifunctor (..)) +import qualified GHC.Data.StringBuffer as SB import GHC.Parser.Annotation - -#if MIN_VERSION_ghc(9,3,0) +import GHC.Types.FieldLabel (DuplicateRecordFields (DuplicateRecordFields, NoDuplicateRecordFields), + FieldSelectors (FieldSelectors, NoFieldSelectors)) import GHC.Types.PkgQual -#endif +import GHC.Types.SrcLoc + +-- See Note [Guidelines For Using CPP In GHCIDE Import Statements] -#if MIN_VERSION_ghc(9,5,0) import GHC.Unit.Home.ModInfo -#endif +import GHC.Unit.Module.Location (ModLocation (..)) +import GHC.Unit.Module.WholeCoreBindings -- Orphan instance for Shake.hs -- https://hub.darcs.net/ross/transformers/issue/86 @@ -55,12 +49,41 @@ instance Show ModDetails where show = const "" instance NFData ModDetails where rnf = rwhnf instance NFData SafeHaskellMode where rnf = rwhnf instance Show Linkable where show = unpack . printOutputable +#if MIN_VERSION_ghc(9,11,0) +instance NFData Linkable where rnf (Linkable a b c) = rnf a `seq` rnf b `seq` rnf c +instance NFData LinkableObjectSort where rnf = rwhnf +instance NFData LinkablePart where + rnf (DotO a b) = rnf a `seq` rnf b + rnf (DotA f) = rnf f + rnf (DotDLL f) = rnf f + rnf (BCOs a) = seqCompiledByteCode a + rnf (CoreBindings wcb) = rnf wcb + rnf (LazyBCOs a b) = seqCompiledByteCode a `seq` rnf b +#else instance NFData Linkable where rnf (LM a b c) = rnf a `seq` rnf b `seq` rnf c instance NFData Unlinked where - rnf (DotO f) = rnf f - rnf (DotA f) = rnf f - rnf (DotDLL f) = rnf f - rnf (BCOs a b) = seqCompiledByteCode a `seq` liftRnf rwhnf b + rnf (DotO f) = rnf f + rnf (DotA f) = rnf f + rnf (DotDLL f) = rnf f + rnf (BCOs a b) = seqCompiledByteCode a `seq` liftRnf rwhnf b + rnf (CoreBindings wcb) = rnf wcb + rnf (LoadedBCOs us) = rnf us +#endif + +instance NFData WholeCoreBindings where +#if MIN_VERSION_ghc(9,11,0) + rnf (WholeCoreBindings bs m ml f) = rnf bs `seq` rnf m `seq` rnf ml `seq` rnf f +#else + rnf (WholeCoreBindings bs m ml) = rnf bs `seq` rnf m `seq` rnf ml +#endif + +instance NFData ModLocation where +#if MIN_VERSION_ghc(9,11,0) + rnf (OsPathModLocation mf f1 f2 f3 f4 f5) = rnf mf `seq` rnf f1 `seq` rnf f2 `seq` rnf f3 `seq` rnf f4 `seq` rnf f5 +#else + rnf (ModLocation mf f1 f2 f3 f4 f5) = rnf mf `seq` rnf f1 `seq` rnf f2 `seq` rnf f3 `seq` rnf f4 `seq` rnf f5 +#endif + instance Show PackageFlag where show = unpack . printOutputable instance Show InteractiveImport where show = unpack . printOutputable instance Show PackageName where show = unpack . printOutputable @@ -74,15 +97,6 @@ instance NFData SB.StringBuffer where rnf = rwhnf instance Show Module where show = moduleNameString . moduleName -#if !MIN_VERSION_ghc(9,3,0) -instance Outputable a => Show (GenLocated SrcSpan a) where show = unpack . printOutputable -#endif - -#if !MIN_VERSION_ghc(9,5,0) -instance (NFData l, NFData e) => NFData (GenLocated l e) where - rnf (L l e) = rnf l `seq` rnf e -#endif - instance Show ModSummary where show = show . ms_mod @@ -95,14 +109,19 @@ instance NFData ModSummary where instance Ord FastString where compare a b = if a == b then EQ else compare (fs_sbs a) (fs_sbs b) + +#if MIN_VERSION_ghc(9,9,0) +instance NFData (EpAnn a) where + rnf = rwhnf +#else instance NFData (SrcSpanAnn' a) where rnf = rwhnf +deriving instance Functor SrcSpanAnn' +#endif -instance Bifunctor (GenLocated) where +instance Bifunctor GenLocated where bimap f g (L l x) = L (f l) (g x) -deriving instance Functor SrcSpanAnn' - instance NFData ParsedModule where rnf = rwhnf @@ -112,12 +131,6 @@ instance Show HieFile where instance NFData HieFile where rnf = rwhnf -#if !MIN_VERSION_ghc(9,3,0) -deriving instance Eq SourceModified -deriving instance Show SourceModified -instance NFData SourceModified where - rnf = rwhnf -#endif instance Hashable ModuleName where hashWithSalt salt = hashWithSalt salt . show @@ -166,11 +179,6 @@ instance NFData Type where instance Show a => Show (Bag a) where show = show . bagToList -#if !MIN_VERSION_ghc(9,5,0) -instance NFData HsDocString where - rnf = rwhnf -#endif - instance Show ModGuts where show _ = "modguts" instance NFData ModGuts where @@ -179,11 +187,7 @@ instance NFData ModGuts where instance NFData (ImportDecl GhcPs) where rnf = rwhnf -#if MIN_VERSION_ghc(9,5,0) instance (NFData (HsModule a)) where -#else -instance (NFData HsModule) where -#endif rnf = rwhnf instance Show OccName where show = unpack . printOutputable @@ -203,7 +207,6 @@ instance NFData ModuleGraph where rnf = rwhnf instance NFData HomeModInfo where rnf (HomeModInfo iface dets link) = rwhnf iface `seq` rnf dets `seq` rnf link -#if MIN_VERSION_ghc(9,3,0) instance NFData PkgQual where rnf NoPkgQual = () rnf (ThisPkg uid) = rnf uid @@ -214,12 +217,9 @@ instance NFData UnitId where instance NFData NodeKey where rnf = rwhnf -#endif -#if MIN_VERSION_ghc(9,5,0) instance NFData HomeModLinkable where rnf = rwhnf -#endif instance NFData (HsExpr (GhcPass Renamed)) where rnf = rwhnf @@ -227,6 +227,12 @@ instance NFData (HsExpr (GhcPass Renamed)) where instance NFData (Pat (GhcPass Renamed)) where rnf = rwhnf +instance NFData (HsExpr (GhcPass Typechecked)) where + rnf = rwhnf + +instance NFData (Pat (GhcPass Typechecked)) where + rnf = rwhnf + instance NFData Extension where rnf = rwhnf diff --git a/ghcide/src/Development/IDE/GHC/Util.hs b/ghcide/src/Development/IDE/GHC/Util.hs index 0967e4e6fc..fb051bda5a 100644 --- a/ghcide/src/Development/IDE/GHC/Util.hs +++ b/ghcide/src/Development/IDE/GHC/Util.hs @@ -27,7 +27,8 @@ module Development.IDE.GHC.Util( dontWriteHieFiles, disableWarningsAsErrors, printOutputable, - getExtensions + getExtensions, + stripOccNamePrefix, ) where import Control.Concurrent @@ -62,9 +63,7 @@ import GHC.IO.Handle.Types import Ide.PluginUtils (unescape) import System.FilePath --- See Note [Guidelines For Using CPP In GHCIDE Import Statements] - - +import Data.Monoid (First (..)) import GHC.Data.EnumSet import GHC.Data.FastString import GHC.Data.StringBuffer @@ -168,7 +167,7 @@ cgGutsToCoreModule safeMode guts modDetails = CoreModule -- Will produce an 8 byte unreadable ByteString. fingerprintToBS :: Fingerprint -> BS.ByteString fingerprintToBS (Fingerprint a b) = BS.unsafeCreate 8 $ \ptr -> do - ptr' <- pure $ castPtr ptr + let ptr' = castPtr ptr pokeElemOff ptr' 0 a pokeElemOff ptr' 1 b @@ -258,7 +257,6 @@ ioe_dupHandlesNotCompatible h = -- Tracing exactprint terms -- | Print a GHC value in `defaultUserStyle` without unique symbols. --- It uses `showSDocUnsafe` with `unsafeGlobalDynFlags` internally. -- -- This is the most common print utility. -- It will do something additionally compared to what the 'Outputable' instance does. @@ -275,3 +273,55 @@ printOutputable = getExtensions :: ParsedModule -> [Extension] getExtensions = toList . extensionFlags . ms_hspp_opts . pm_mod_summary + +-- | When e.g. DuplicateRecordFields is enabled, compiler generates +-- names like "$sel:accessor:One" and "$sel:accessor:Two" to +-- disambiguate record selectors +-- https://ghc.haskell.org/trac/ghc/wiki/Records/OverloadedRecordFields/DuplicateRecordFields#Implementation +stripOccNamePrefix :: T.Text -> T.Text +stripOccNamePrefix name = T.takeWhile (/=':') $ fromMaybe name $ + getFirst $ foldMap (First . (`T.stripPrefix` name)) + occNamePrefixes + +-- | Prefixes that can occur in a GHC OccName +occNamePrefixes :: [T.Text] +occNamePrefixes = + [ + -- long ones + "$con2tag_" + , "$tag2con_" + , "$maxtag_" + + -- four chars + , "$sel:" + , "$tc'" + + -- three chars + , "$dm" + , "$co" + , "$tc" + , "$cp" + , "$fx" + + -- two chars + , "$W" + , "$w" + , "$m" + , "$b" + , "$c" + , "$d" + , "$i" + , "$s" + , "$f" + , "$r" + , "C:" + , "N:" + , "D:" + , "$p" + , "$L" + , "$f" + , "$t" + , "$c" + , "$m" + ] + diff --git a/ghcide/src/Development/IDE/GHC/Warnings.hs b/ghcide/src/Development/IDE/GHC/Warnings.hs index ff82af1d65..fe77ea8456 100644 --- a/ghcide/src/Development/IDE/GHC/Warnings.hs +++ b/ghcide/src/Development/IDE/GHC/Warnings.hs @@ -6,14 +6,36 @@ module Development.IDE.GHC.Warnings(withWarnings) where import Control.Concurrent.Strict -import Data.List +import Control.Lens (over) import qualified Data.Text as T import Development.IDE.GHC.Compat import Development.IDE.GHC.Error import Development.IDE.Types.Diagnostics -import Language.LSP.Protocol.Types (type (|?) (..)) +{- + Note [withWarnings and its dangers] + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + withWarnings collects warnings by registering a custom logger which extracts + the SDocs of those warnings. If you receive warnings this way, you will not + get them in a structured form. In the medium term we'd like to remove all + uses of withWarnings to get structured messages everywhere we can. + + For the time being, withWarnings is no longer used for anything in the main + typecheckModule codepath, but it is still used for bytecode/object code + generation, as well as a few other places. + + I suspect some of these functions (e.g. codegen) will need deeper changes to + be able to get diagnostics as a list, though I don't have great evidence for + that atm. I haven't taken a look to see if those functions that are wrapped + with this could produce diagnostics another way. + + It would be good for someone to take a look. What we've done so far gives us + diagnostics for renaming and typechecking, and doesn't require us to copy + too much code from GHC or make any deeper changes, and lets us get started + with the bulk of the useful plugin work, but it would be good to have all + diagnostics with structure be collected that way. +-} -- | Take a GHC monadic action (e.g. @typecheckModule pm@ for some -- parsed module 'pm@') and produce a "decorated" action that will @@ -24,42 +46,16 @@ import Language.LSP.Protocol.Types (type (|?) (..)) -- https://github.com/ghc/ghc/blob/5f1d949ab9e09b8d95319633854b7959df06eb58/compiler/main/GHC.hs#L623-L640 -- which basically says that log_action is taken from the ModSummary when GHC feels like it. -- The given argument lets you refresh a ModSummary log_action -#if MIN_VERSION_ghc(9,3,0) +-- +-- Also, See Note [withWarnings and its dangers] for some commentary on this function. withWarnings :: T.Text -> ((HscEnv -> HscEnv) -> IO a) -> IO ([(Maybe DiagnosticReason, FileDiagnostic)], a) -#else -withWarnings :: T.Text -> ((HscEnv -> HscEnv) -> IO a) -> IO ([(WarnReason, FileDiagnostic)], a) -#endif withWarnings diagSource action = do warnings <- newVar [] let newAction :: DynFlags -> LogActionCompat newAction dynFlags logFlags wr _ loc prUnqual msg = do - let wr_d = map ((wr,) . third3 (attachReason wr)) $ diagFromErrMsg diagSource dynFlags $ mkWarnMsg dynFlags wr logFlags loc prUnqual msg + let wr_d = map ((wr,) . over fdLspDiagnosticL (attachReason wr)) $ diagFromSDocErrMsg diagSource dynFlags (mkWarnMsg dynFlags wr logFlags loc prUnqual msg) modifyVar_ warnings $ return . (wr_d:) newLogger env = pushLogHook (const (logActionCompat (newAction (hsc_dflags env)))) (hsc_logger env) res <- action $ \env -> putLogHook (newLogger env) env warns <- readVar warnings return (reverse $ concat warns, res) - where - third3 :: (c -> d) -> (a, b, c) -> (a, b, d) - third3 f (a, b, c) = (a, b, f c) - -#if MIN_VERSION_ghc(9,3,0) -attachReason :: Maybe DiagnosticReason -> Diagnostic -> Diagnostic -attachReason Nothing d = d -attachReason (Just wr) d = d{_code = InR <$> showReason wr} - where - showReason = \case - WarningWithFlag flag -> showFlag flag - _ -> Nothing -#else -attachReason :: WarnReason -> Diagnostic -> Diagnostic -attachReason wr d = d{_code = InR <$> showReason wr} - where - showReason = \case - NoReason -> Nothing - Reason flag -> showFlag flag - ErrReason flag -> showFlag =<< flag -#endif - -showFlag :: WarningFlag -> Maybe T.Text -showFlag flag = ("-W" <>) . T.pack . flagSpecName <$> find ((== flag) . flagSpecFlag) wWarningFlags diff --git a/ghcide/src/Development/IDE/Import/DependencyInformation.hs b/ghcide/src/Development/IDE/Import/DependencyInformation.hs index 6ae27e2912..471cf52eab 100644 --- a/ghcide/src/Development/IDE/Import/DependencyInformation.hs +++ b/ghcide/src/Development/IDE/Import/DependencyInformation.hs @@ -20,6 +20,7 @@ module Development.IDE.Import.DependencyInformation , insertImport , pathToId , idToPath + , idToModLocation , reachableModules , processDependencyInformation , transitiveDeps @@ -28,6 +29,7 @@ module Development.IDE.Import.DependencyInformation , lookupModuleFile , BootIdMap , insertBootId + , lookupFingerprint ) where import Control.DeepSeq @@ -47,21 +49,16 @@ import Data.List.NonEmpty (NonEmpty (..), nonEmpty) import qualified Data.List.NonEmpty as NonEmpty import Data.Maybe import Data.Tuple.Extra hiding (first, second) +import Development.IDE.GHC.Compat +import Development.IDE.GHC.Compat.Util (Fingerprint) +import qualified Development.IDE.GHC.Compat.Util as Util import Development.IDE.GHC.Orphans () -import GHC.Generics (Generic) -import Prelude hiding (mod) - import Development.IDE.Import.FindImports (ArtifactsLocation (..)) import Development.IDE.Types.Diagnostics import Development.IDE.Types.Location +import GHC.Generics (Generic) +import Prelude hiding (mod) -import Development.IDE.GHC.Compat - --- See Note [Guidelines For Using CPP In GHCIDE Import Statements] - -#if !MIN_VERSION_ghc(9,3,0) -import GHC -#endif -- | The imports for a given module. newtype ModuleImports = ModuleImports @@ -142,23 +139,35 @@ data RawDependencyInformation = RawDependencyInformation data DependencyInformation = DependencyInformation - { depErrorNodes :: !(FilePathIdMap (NonEmpty NodeError)) + { depErrorNodes :: !(FilePathIdMap (NonEmpty NodeError)) -- ^ Nodes that cannot be processed correctly. - , depModules :: !(FilePathIdMap ShowableModule) - , depModuleDeps :: !(FilePathIdMap FilePathIdSet) + , depModules :: !(FilePathIdMap ShowableModule) + , depModuleDeps :: !(FilePathIdMap FilePathIdSet) -- ^ For a non-error node, this contains the set of module immediate dependencies -- in the same package. - , depReverseModuleDeps :: !(IntMap IntSet) + , depReverseModuleDeps :: !(IntMap IntSet) -- ^ Contains a reverse mapping from a module to all those that immediately depend on it. - , depPathIdMap :: !PathIdMap + , depPathIdMap :: !PathIdMap -- ^ Map from FilePath to FilePathId - , depBootMap :: !BootIdMap + , depBootMap :: !BootIdMap -- ^ Map from hs-boot file to the corresponding hs file - , depModuleFiles :: !(ShowableModuleEnv FilePathId) + , depModuleFiles :: !(ShowableModuleEnv FilePathId) -- ^ Map from Module to the corresponding non-boot hs file - , depModuleGraph :: !ModuleGraph + , depModuleGraph :: !ModuleGraph + , depTransDepsFingerprints :: !(FilePathIdMap Fingerprint) + -- ^ Map from Module to fingerprint of the transitive dependencies of the module. + , depTransReverseDepsFingerprints :: !(FilePathIdMap Fingerprint) + -- ^ Map from FilePathId to the fingerprint of the transitive reverse dependencies of the module. + , depImmediateReverseDepsFingerprints :: !(FilePathIdMap Fingerprint) + -- ^ Map from FilePathId to the fingerprint of the immediate reverse dependencies of the module. } deriving (Show, Generic) +lookupFingerprint :: NormalizedFilePath -> DependencyInformation -> FilePathIdMap Fingerprint -> Maybe Fingerprint +lookupFingerprint fileId DependencyInformation {..} depFingerprintMap = + do + FilePathId cur_id <- lookupPathToId depPathIdMap fileId + IntMap.lookup cur_id depFingerprintMap + newtype ShowableModule = ShowableModule {showableModule :: Module} deriving NFData @@ -234,8 +243,8 @@ instance Semigroup NodeResult where SuccessNode _ <> ErrorNode errs = ErrorNode errs SuccessNode a <> SuccessNode _ = SuccessNode a -processDependencyInformation :: RawDependencyInformation -> BootIdMap -> ModuleGraph -> DependencyInformation -processDependencyInformation RawDependencyInformation{..} rawBootMap mg = +processDependencyInformation :: RawDependencyInformation -> BootIdMap -> ModuleGraph -> FilePathIdMap Fingerprint -> DependencyInformation +processDependencyInformation RawDependencyInformation{..} rawBootMap mg shallowFingerMap = DependencyInformation { depErrorNodes = IntMap.fromList errorNodes , depModuleDeps = moduleDeps @@ -245,6 +254,9 @@ processDependencyInformation RawDependencyInformation{..} rawBootMap mg = , depBootMap = rawBootMap , depModuleFiles = ShowableModuleEnv reverseModuleMap , depModuleGraph = mg + , depTransDepsFingerprints = buildTransDepsFingerprintMap moduleDeps shallowFingerMap + , depTransReverseDepsFingerprints = buildTransDepsFingerprintMap reverseModuleDeps shallowFingerMap + , depImmediateReverseDepsFingerprints = buildImmediateDepsFingerprintMap reverseModuleDeps shallowFingerMap } where resultGraph = buildResultGraph rawImports (errorNodes, successNodes) = partitionNodeResults $ IntMap.toList resultGraph @@ -404,3 +416,44 @@ instance NFData NamedModuleDep where instance Show NamedModuleDep where show NamedModuleDep{..} = show nmdFilePath + + +buildImmediateDepsFingerprintMap :: FilePathIdMap FilePathIdSet -> FilePathIdMap Fingerprint -> FilePathIdMap Fingerprint +buildImmediateDepsFingerprintMap modulesDeps shallowFingers = + IntMap.fromList + $ map + ( \k -> + ( k, + Util.fingerprintFingerprints $ + map + (shallowFingers IntMap.!) + (k : IntSet.toList (IntMap.findWithDefault IntSet.empty k modulesDeps)) + ) + ) + $ IntMap.keys shallowFingers + +-- | Build a map from file path to its full fingerprint. +-- The fingerprint is depend on both the fingerprints of the file and all its dependencies. +-- This is used to determine if a file has changed and needs to be reloaded. +buildTransDepsFingerprintMap :: FilePathIdMap FilePathIdSet -> FilePathIdMap Fingerprint -> FilePathIdMap Fingerprint +buildTransDepsFingerprintMap modulesDeps shallowFingers = go keys IntMap.empty + where + keys = IntMap.keys shallowFingers + go :: [IntSet.Key] -> FilePathIdMap Fingerprint -> FilePathIdMap Fingerprint + go keys acc = + case keys of + [] -> acc + k : ks -> + if IntMap.member k acc + -- already in the map, so we can skip + then go ks acc + -- not in the map, so we need to add it + else + let -- get the dependencies of the current key + deps = IntSet.toList $ IntMap.findWithDefault IntSet.empty k modulesDeps + -- add fingerprints of the dependencies to the accumulator + depFingerprints = go deps acc + -- combine the fingerprints of the dependencies with the current key + combinedFingerprints = Util.fingerprintFingerprints $ shallowFingers IntMap.! k : map (depFingerprints IntMap.!) deps + in -- add the combined fingerprints to the accumulator + go ks (IntMap.insert k combinedFingerprints depFingerprints) diff --git a/ghcide/src/Development/IDE/Import/FindImports.hs b/ghcide/src/Development/IDE/Import/FindImports.hs index 358666a0e9..7c4046a63a 100644 --- a/ghcide/src/Development/IDE/Import/FindImports.hs +++ b/ghcide/src/Development/IDE/Import/FindImports.hs @@ -14,28 +14,23 @@ module Development.IDE.Import.FindImports ) where import Control.DeepSeq +import Control.Monad.Extra +import Control.Monad.IO.Class +import Data.List (find, isSuffixOf) +import Data.Maybe +import qualified Data.Set as S import Development.IDE.GHC.Compat as Compat import Development.IDE.GHC.Error as ErrUtils import Development.IDE.GHC.Orphans () import Development.IDE.Types.Diagnostics import Development.IDE.Types.Location - --- standard imports -import Control.Monad.Extra -import Control.Monad.IO.Class -import Data.List (isSuffixOf) -import Data.Maybe +import GHC.Types.PkgQual +import GHC.Unit.State import System.FilePath --- See Note [Guidelines For Using CPP In GHCIDE Import Statements] - -#if !MIN_VERSION_ghc(9,3,0) -import Development.IDE.GHC.Compat.Util -#endif -#if MIN_VERSION_ghc(9,3,0) -import GHC.Types.PkgQual -import GHC.Unit.State +#if MIN_VERSION_ghc(9,11,0) +import GHC.Driver.DynFlags #endif data Import @@ -70,19 +65,30 @@ modSummaryToArtifactsLocation nfp ms = ArtifactsLocation nfp (ms_location <$> ms Just modSum -> isSource (ms_hsc_src modSum) mbMod = ms_mod <$> ms +data LocateResult + = LocateNotFound + | LocateFoundReexport UnitId + | LocateFoundFile UnitId NormalizedFilePath + -- | locate a module in the file system. Where we go from *daml to Haskell locateModuleFile :: MonadIO m - => [(UnitId, [FilePath])] + => [(UnitId, [FilePath], S.Set ModuleName)] -> [String] -> (ModuleName -> NormalizedFilePath -> m (Maybe NormalizedFilePath)) -> Bool -> ModuleName - -> m (Maybe (UnitId, NormalizedFilePath)) + -> m LocateResult locateModuleFile import_dirss exts targetFor isSource modName = do let candidates import_dirs = [ toNormalizedFilePath' (prefix moduleNameSlashes modName <.> maybeBoot ext) | prefix <- import_dirs , ext <- exts] - firstJustM go (concat [map (uid,) (candidates dirs) | (uid, dirs) <- import_dirss]) + mf <- firstJustM go (concat [map (uid,) (candidates dirs) | (uid, dirs, _) <- import_dirss]) + case mf of + Nothing -> + case find (\(_ , _, reexports) -> S.member modName reexports) import_dirss of + Just (uid,_,_) -> pure $ LocateFoundReexport uid + Nothing -> pure LocateNotFound + Just (uid,file) -> pure $ LocateFoundFile uid file where go (uid, candidate) = fmap ((uid,) <$>) $ targetFor modName candidate maybeBoot ext @@ -93,12 +99,11 @@ locateModuleFile import_dirss exts targetFor isSource modName = do -- It only returns Just for unit-ids which are possible to import into the -- current module. In particular, it will return Nothing for 'main' components -- as they can never be imported into another package. -#if MIN_VERSION_ghc(9,3,0) -mkImportDirs :: HscEnv -> (UnitId, DynFlags) -> Maybe (UnitId, [FilePath]) -mkImportDirs _env (i, flags) = Just (i, importPaths flags) +mkImportDirs :: HscEnv -> (UnitId, DynFlags) -> Maybe (UnitId, ([FilePath], S.Set ModuleName)) +#if MIN_VERSION_ghc(9,11,0) +mkImportDirs _env (i, flags) = Just (i, (importPaths flags, S.fromList $ map reexportTo $ reexportedModules flags)) #else -mkImportDirs :: HscEnv -> (UnitId, DynFlags) -> Maybe (PackageName, (UnitId, [FilePath])) -mkImportDirs env (i, flags) = (, (i, importPaths flags)) <$> getUnitName env i +mkImportDirs _env (i, flags) = Just (i, (importPaths flags, reexportedModules flags)) #endif -- | locate a module in either the file system or the package database. Where we go from *daml to @@ -110,87 +115,66 @@ locateModule -> [String] -- ^ File extensions -> (ModuleName -> NormalizedFilePath -> m (Maybe NormalizedFilePath)) -- ^ does file exist predicate -> Located ModuleName -- ^ Module name -#if MIN_VERSION_ghc(9,3,0) -> PkgQual -- ^ Package name -#else - -> Maybe FastString -- ^ Package name -#endif -> Bool -- ^ Is boot module -> m (Either [FileDiagnostic] Import) locateModule env comp_info exts targetFor modName mbPkgName isSource = do case mbPkgName of - -- "this" means that we should only look in the current package -#if MIN_VERSION_ghc(9,3,0) - ThisPkg _ -> do -#else - Just "this" -> do -#endif - lookupLocal (homeUnitId_ dflags) (importPaths dflags) + -- 'ThisPkg' just means some home module, not the current unit + ThisPkg uid + | Just (dirs, reexports) <- lookup uid import_paths + -> lookupLocal uid dirs reexports + | otherwise -> return $ Left $ notFoundErr env modName $ LookupNotFound [] -- if a package name is given we only go look for a package -#if MIN_VERSION_ghc(9,3,0) OtherPkg uid - | Just dirs <- lookup uid import_paths - -> lookupLocal uid dirs -#else - Just pkgName - | Just (uid, dirs) <- lookup (PackageName pkgName) import_paths - -> lookupLocal uid dirs -#endif + | Just (dirs, reexports) <- lookup uid import_paths + -> lookupLocal uid dirs reexports | otherwise -> lookupInPackageDB -#if MIN_VERSION_ghc(9,3,0) NoPkgQual -> do -#else - Nothing -> do -#endif - mbFile <- locateModuleFile ((homeUnitId_ dflags, importPaths dflags) : other_imports) exts targetFor isSource $ unLoc modName + -- Reexports for current unit have to be empty because they only apply to other units depending on the + -- current unit. If we set the reexports to be the actual reexports then we risk looping forever trying + -- to find the module from the perspective of the current unit. + mbFile <- locateModuleFile ((homeUnitId_ dflags, importPaths dflags, S.empty) : other_imports) exts targetFor isSource $ unLoc modName case mbFile of - Nothing -> lookupInPackageDB - Just (uid, file) -> toModLocation uid file + LocateNotFound -> lookupInPackageDB + -- Lookup again with the perspective of the unit reexporting the file + LocateFoundReexport uid -> locateModule (hscSetActiveUnitId uid env) comp_info exts targetFor modName noPkgQual isSource + LocateFoundFile uid file -> toModLocation uid file where dflags = hsc_dflags env import_paths = mapMaybe (mkImportDirs env) comp_info other_imports = -#if MIN_VERSION_ghc(9,4,0) - -- On 9.4+ instead of bringing all the units into scope, only bring into scope the units - -- this one depends on + -- Instead of bringing all the units into scope, only bring into scope the units + -- this one depends on. -- This way if you have multiple units with the same module names, we won't get confused -- For example if unit a imports module M from unit B, when there is also a module M in unit C, -- and unit a only depends on unit b, without this logic there is the potential to get confused -- about which module unit a imports. -- Without multi-component support it is hard to recontruct the dependency environment so -- unit a will have both unit b and unit c in scope. - map (\uid -> (uid, importPaths (homeUnitEnv_dflags (ue_findHomeUnitEnv uid ue)))) hpt_deps +#if MIN_VERSION_ghc(9,11,0) + map (\uid -> let this_df = homeUnitEnv_dflags (ue_findHomeUnitEnv uid ue) in (uid, importPaths this_df, S.fromList $ map reexportTo $ reexportedModules this_df)) hpt_deps +#else + map (\uid -> let this_df = homeUnitEnv_dflags (ue_findHomeUnitEnv uid ue) in (uid, importPaths this_df, reexportedModules this_df)) hpt_deps +#endif ue = hsc_unit_env env units = homeUnitEnv_units $ ue_findHomeUnitEnv (homeUnitId_ dflags) ue hpt_deps :: [UnitId] hpt_deps = homeUnitDepends units -#else - _import_paths' -#endif - - -- first try to find the module as a file. If we can't find it try to find it in the package - -- database. - -- Here the importPaths for the current modules are added to the front of the import paths from the other components. - -- This is particularly important for Paths_* modules which get generated for every component but unless you use it in - -- each component will end up being found in the wrong place and cause a multi-cradle match failure. - _import_paths' = -- import_paths' is only used in GHC < 9.4 -#if MIN_VERSION_ghc(9,3,0) - import_paths -#else - map snd import_paths -#endif toModLocation uid file = liftIO $ do loc <- mkHomeModLocation dflags (unLoc modName) (fromNormalizedFilePath file) let genMod = mkModule (RealUnit $ Definite uid) (unLoc modName) -- TODO support backpack holes return $ Right $ FileImport $ ArtifactsLocation file (Just loc) (not isSource) (Just genMod) - lookupLocal uid dirs = do - mbFile <- locateModuleFile [(uid, dirs)] exts targetFor isSource $ unLoc modName + lookupLocal uid dirs reexports = do + mbFile <- locateModuleFile [(uid, dirs, reexports)] exts targetFor isSource $ unLoc modName case mbFile of - Nothing -> return $ Left $ notFoundErr env modName $ LookupNotFound [] - Just (uid', file) -> toModLocation uid' file + LocateNotFound -> return $ Left $ notFoundErr env modName $ LookupNotFound [] + -- Lookup again with the perspective of the unit reexporting the file + LocateFoundReexport uid' -> locateModule (hscSetActiveUnitId uid' env) comp_info exts targetFor modName noPkgQual isSource + LocateFoundFile uid' file -> toModLocation uid' file lookupInPackageDB = do case Compat.lookupModuleWithSuggestions env (unLoc modName) mbPkgName of @@ -203,7 +187,7 @@ notFoundErr env modName reason = mkError' $ ppr' $ cannotFindModule env modName0 $ lookupToFindResult reason where dfs = hsc_dflags env - mkError' = diagFromString "not found" DiagnosticSeverity_Error (Compat.getLoc modName) + mkError' doc = diagFromString "not found" DiagnosticSeverity_Error (Compat.getLoc modName) doc Nothing modName0 = unLoc modName ppr' = showSDoc dfs -- We convert the lookup result to a find result to reuse GHC's cannotFindModule pretty printer. @@ -219,7 +203,11 @@ notFoundErr env modName reason = } LookupUnusable unusable -> let unusables' = map get_unusable unusable +#if MIN_VERSION_ghc(9,6,4) && (!MIN_VERSION_ghc(9,8,1) || MIN_VERSION_ghc(9,8,2)) + get_unusable (_m, ModUnusable r) = r +#else get_unusable (m, ModUnusable r) = (moduleUnit m, r) +#endif get_unusable (_, r) = pprPanic "findLookupResult: unexpected origin" (ppr r) in notFound {fr_unusables = unusables'} @@ -235,3 +223,6 @@ notFound = NotFound , fr_unusables = [] , fr_suggestions = [] } + +noPkgQual :: PkgQual +noPkgQual = NoPkgQual diff --git a/ghcide/src/Development/IDE/LSP/HoverDefinition.hs b/ghcide/src/Development/IDE/LSP/HoverDefinition.hs index eefe1a14f4..0ba6e22530 100644 --- a/ghcide/src/Development/IDE/LSP/HoverDefinition.hs +++ b/ghcide/src/Development/IDE/LSP/HoverDefinition.hs @@ -1,15 +1,16 @@ -- Copyright (c) 2019 The DAML Authors. All rights reserved. -- SPDX-License-Identifier: Apache-2.0 -{-# LANGUAGE GADTs #-} -{-# LANGUAGE RankNTypes #-} +{-# LANGUAGE GADTs #-} -- | Display information on hover. module Development.IDE.LSP.HoverDefinition - ( + ( Log(..) -- * For haskell-language-server - hover + , hover + , foundHover , gotoDefinition , gotoTypeDefinition + , gotoImplementation , documentHighlight , references , wsSymbols @@ -19,38 +20,51 @@ import Control.Monad.Except (ExceptT) import Control.Monad.IO.Class import Data.Maybe (fromMaybe) import Development.IDE.Core.Actions -import Development.IDE.Core.Rules -import Development.IDE.Core.Shake +import qualified Development.IDE.Core.Rules as Shake +import Development.IDE.Core.Shake (IdeAction, IdeState (..), + runIdeAction) import Development.IDE.Types.Location import Ide.Logger import Ide.Plugin.Error import Ide.Types import Language.LSP.Protocol.Message import Language.LSP.Protocol.Types -import qualified Language.LSP.Server as LSP import qualified Data.Text as T -gotoDefinition :: IdeState -> TextDocumentPositionParams -> ExceptT PluginError (LSP.LspM c) (MessageResult Method_TextDocumentDefinition) -hover :: IdeState -> TextDocumentPositionParams -> ExceptT PluginError (LSP.LspM c) (Hover |? Null) -gotoTypeDefinition :: IdeState -> TextDocumentPositionParams -> ExceptT PluginError (LSP.LspM c) (MessageResult Method_TextDocumentTypeDefinition) -documentHighlight :: IdeState -> TextDocumentPositionParams -> ExceptT PluginError (LSP.LspM c) ([DocumentHighlight] |? Null) -gotoDefinition = request "Definition" getDefinition (InR $ InR Null) (InL . Definition. InR) -gotoTypeDefinition = request "TypeDefinition" getTypeDefinition (InR $ InR Null) (InL . Definition. InR) + +data Log + = LogWorkspaceSymbolRequest !T.Text + | LogRequest !T.Text !Position !NormalizedFilePath + deriving (Show) + +instance Pretty Log where + pretty = \case + LogWorkspaceSymbolRequest query -> "Workspace symbols request:" <+> pretty query + LogRequest label pos nfp -> + pretty label <+> "request at position" <+> pretty (showPosition pos) <+> + "in file:" <+> pretty (fromNormalizedFilePath nfp) + +gotoDefinition :: Recorder (WithPriority Log) -> IdeState -> TextDocumentPositionParams -> ExceptT PluginError (HandlerM c) (MessageResult Method_TextDocumentDefinition) +hover :: Recorder (WithPriority Log) -> IdeState -> TextDocumentPositionParams -> ExceptT PluginError (HandlerM c) (Hover |? Null) +gotoTypeDefinition :: Recorder (WithPriority Log) -> IdeState -> TextDocumentPositionParams -> ExceptT PluginError (HandlerM c) (MessageResult Method_TextDocumentTypeDefinition) +gotoImplementation :: Recorder (WithPriority Log) -> IdeState -> TextDocumentPositionParams -> ExceptT PluginError (HandlerM c) (MessageResult Method_TextDocumentImplementation) +documentHighlight :: Recorder (WithPriority Log) -> IdeState -> TextDocumentPositionParams -> ExceptT PluginError (HandlerM c) ([DocumentHighlight] |? Null) +gotoDefinition = request "Definition" getDefinition (InR $ InR Null) (InL . Definition . InR . map fst) +gotoTypeDefinition = request "TypeDefinition" getTypeDefinition (InR $ InR Null) (InL . Definition . InR . map fst) +gotoImplementation = request "Implementation" getImplementationDefinition (InR $ InR Null) (InL . Definition . InR) hover = request "Hover" getAtPoint (InR Null) foundHover documentHighlight = request "DocumentHighlight" highlightAtPoint (InR Null) InL -references :: PluginMethodHandler IdeState Method_TextDocumentReferences -references ide _ (ReferenceParams (TextDocumentIdentifier uri) pos _ _ _) = do +references :: Recorder (WithPriority Log) -> PluginMethodHandler IdeState Method_TextDocumentReferences +references recorder ide _ (ReferenceParams (TextDocumentIdentifier uri) pos _ _ _) = do nfp <- getNormalizedFilePathE uri - liftIO $ logDebug (ideLogger ide) $ - "References request at position " <> T.pack (showPosition pos) <> - " in file: " <> T.pack (show nfp) - InL <$> (liftIO $ runAction "references" ide $ refsAtPoint nfp pos) + liftIO $ logWith recorder Debug $ LogRequest "References" pos nfp + InL <$> (liftIO $ Shake.runAction "references" ide $ refsAtPoint nfp pos) -wsSymbols :: PluginMethodHandler IdeState Method_WorkspaceSymbol -wsSymbols ide _ (WorkspaceSymbolParams _ _ query) = liftIO $ do - logDebug (ideLogger ide) $ "Workspace symbols request: " <> query +wsSymbols :: Recorder (WithPriority Log) -> PluginMethodHandler IdeState Method_WorkspaceSymbol +wsSymbols recorder ide _ (WorkspaceSymbolParams _ _ query) = liftIO $ do + logWith recorder Debug $ LogWorkspaceSymbolRequest query runIdeAction "WorkspaceSymbols" (shakeExtras ide) $ InL . fromMaybe [] <$> workspaceSymbols query foundHover :: (Maybe Range, [T.Text]) -> Hover |? Null @@ -63,19 +77,18 @@ request -> (NormalizedFilePath -> Position -> IdeAction (Maybe a)) -> b -> (a -> b) + -> Recorder (WithPriority Log) -> IdeState -> TextDocumentPositionParams - -> ExceptT PluginError (LSP.LspM c) b -request label getResults notFound found ide (TextDocumentPositionParams (TextDocumentIdentifier uri) pos) = liftIO $ do + -> ExceptT PluginError (HandlerM c) b +request label getResults notFound found recorder ide (TextDocumentPositionParams (TextDocumentIdentifier uri) pos) = liftIO $ do mbResult <- case uriToFilePath' uri of - Just path -> logAndRunRequest label getResults ide pos path + Just path -> logAndRunRequest recorder label getResults ide pos path Nothing -> pure Nothing pure $ maybe notFound found mbResult -logAndRunRequest :: T.Text -> (NormalizedFilePath -> Position -> IdeAction b) -> IdeState -> Position -> String -> IO b -logAndRunRequest label getResults ide pos path = do +logAndRunRequest :: Recorder (WithPriority Log) -> T.Text -> (NormalizedFilePath -> Position -> IdeAction b) -> IdeState -> Position -> String -> IO b +logAndRunRequest recorder label getResults ide pos path = do let filePath = toNormalizedFilePath' path - logDebug (ideLogger ide) $ - label <> " request at position " <> T.pack (showPosition pos) <> - " in file: " <> T.pack path + logWith recorder Debug $ LogRequest label pos filePath runIdeAction (T.unpack label) (shakeExtras ide) (getResults filePath pos) diff --git a/ghcide/src/Development/IDE/LSP/LanguageServer.hs b/ghcide/src/Development/IDE/LSP/LanguageServer.hs index 90175cb730..cf7845ce08 100644 --- a/ghcide/src/Development/IDE/LSP/LanguageServer.hs +++ b/ghcide/src/Development/IDE/LSP/LanguageServer.hs @@ -1,18 +1,16 @@ - -- Copyright (c) 2019 The DAML Authors. All rights reserved. +-- Copyright (c) 2019 The DAML Authors. All rights reserved. -- SPDX-License-Identifier: Apache-2.0 -{-# LANGUAGE DuplicateRecordFields #-} -{-# LANGUAGE ExistentialQuantification #-} -{-# LANGUAGE GADTs #-} -{-# LANGUAGE PolyKinds #-} -{-# LANGUAGE RankNTypes #-} -{-# LANGUAGE StarIsType #-} +{-# LANGUAGE DuplicateRecordFields #-} +{-# LANGUAGE GADTs #-} -- WARNING: A copy of DA.Daml.LanguageServer, try to keep them in sync -- This version removes the daml: handling module Development.IDE.LSP.LanguageServer ( runLanguageServer , setupLSP , Log(..) + , ThreadQueue + , runWithWorkerThreads ) where import Control.Concurrent.STM @@ -37,16 +35,18 @@ import UnliftIO.Exception import qualified Colog.Core as Colog import Control.Monad.IO.Unlift (MonadUnliftIO) +import Control.Monad.Trans.Cont (evalContT) import Development.IDE.Core.IdeConfiguration -import Development.IDE.Core.Shake hiding (Log, Priority) +import Development.IDE.Core.Shake hiding (Log) import Development.IDE.Core.Tracing +import Development.IDE.Core.WorkerThread (withWorkerQueue) import qualified Development.IDE.Session as Session -import Development.IDE.Types.Shake (WithHieDb) +import Development.IDE.Types.Shake (WithHieDb, + WithHieDbShield (..)) import Ide.Logger import Language.LSP.Server (LanguageContextEnv, LspServerLog, type (<~>)) -import System.IO.Unsafe (unsafeInterleaveIO) data Log = LogRegisteringIdeConfig !IdeConfiguration | LogReactorThreadException !SomeException @@ -55,6 +55,7 @@ data Log | LogCancelledRequest !SomeLspId | LogSession Session.Log | LogLspServer LspServerLog + | LogServerShutdownMessage deriving Show instance Pretty Log where @@ -78,9 +79,8 @@ instance Pretty Log where "Cancelled request" <+> viaShow requestId LogSession msg -> pretty msg LogLspServer msg -> pretty msg + LogServerShutdownMessage -> "Received shutdown message" --- used to smuggle RankNType WithHieDb through dbMVar -newtype WithHieDbShield = WithHieDbShield WithHieDb runLanguageServer :: forall config a m. (Show config) @@ -92,7 +92,7 @@ runLanguageServer -> (config -> Value -> Either T.Text config) -> (config -> m config ()) -> (MVar () - -> IO (LSP.LanguageContextEnv config -> TRequestMessage Method_Initialize -> IO (Either ResponseError (LSP.LanguageContextEnv config, a)), + -> IO (LSP.LanguageContextEnv config -> TRequestMessage Method_Initialize -> IO (Either (TResponseError Method_Initialize) (LSP.LanguageContextEnv config, a)), LSP.Handlers (m config), (LanguageContextEnv config, a) -> m config <~> IO)) -> IO () @@ -110,7 +110,7 @@ runLanguageServer recorder options inH outH defaultConfig parseConfig onConfigCh -- TODO: magic string , LSP.configSection = "haskell" , LSP.doInitialize = doInitialize - , LSP.staticHandlers = (const staticHandlers) + , LSP.staticHandlers = const staticHandlers , LSP.interpretHandler = interpretHandler , LSP.options = modifyOptions options } @@ -129,14 +129,15 @@ runLanguageServer recorder options inH outH defaultConfig parseConfig onConfigCh setupLSP :: forall config err. Recorder (WithPriority Log) + -> FilePath -- ^ root directory, see Note [Root Directory] -> (FilePath -> IO FilePath) -- ^ Map root paths to the location of the hiedb for the project -> LSP.Handlers (ServerM config) - -> (LSP.LanguageContextEnv config -> Maybe FilePath -> WithHieDb -> IndexQueue -> IO IdeState) + -> (LSP.LanguageContextEnv config -> FilePath -> WithHieDb -> ThreadQueue -> IO IdeState) -> MVar () -> IO (LSP.LanguageContextEnv config -> TRequestMessage Method_Initialize -> IO (Either err (LSP.LanguageContextEnv config, IdeState)), LSP.Handlers (ServerM config), (LanguageContextEnv config, IdeState) -> ServerM config <~> IO) -setupLSP recorder getHieDbLoc userHandlers getIdeState clientMsgVar = do +setupLSP recorder defaultRoot getHieDbLoc userHandlers getIdeState clientMsgVar = do -- Send everything over a channel, since you need to wait until after initialise before -- LspFuncs is available clientMsgChan :: Chan ReactorMessage <- newChan @@ -159,7 +160,7 @@ setupLSP recorder getHieDbLoc userHandlers getIdeState clientMsgVar = do -- We want to avoid that the list of cancelled requests -- keeps growing if we receive cancellations for requests -- that do not exist or have already been processed. - when (reqId `elem` queued) $ + when (reqId `Set.member` queued) $ modifyTVar cancelledRequests (Set.insert reqId) let clearReqId reqId = atomically $ do modifyTVar pendingRequests (Set.delete reqId) @@ -174,12 +175,12 @@ setupLSP recorder getHieDbLoc userHandlers getIdeState clientMsgVar = do [ userHandlers , cancelHandler cancelRequest , exitHandler exit - , shutdownHandler stopReactorLoop + , shutdownHandler recorder stopReactorLoop ] -- Cancel requests are special since they need to be handled -- out of order to be useful. Existing handlers are run afterwards. - let doInitialize = handleInit recorder getHieDbLoc getIdeState reactorLifetime exit clearReqId waitForCancel clientMsgChan + let doInitialize = handleInit recorder defaultRoot getHieDbLoc getIdeState reactorLifetime exit clearReqId waitForCancel clientMsgChan let interpretHandler (env, st) = LSP.Iso (LSP.runLspT env . flip (runReaderT . unServerM) (clientMsgChan,st)) liftIO @@ -188,31 +189,27 @@ setupLSP recorder getHieDbLoc userHandlers getIdeState clientMsgVar = do handleInit :: Recorder (WithPriority Log) + -> FilePath -- ^ root directory, see Note [Root Directory] -> (FilePath -> IO FilePath) - -> (LSP.LanguageContextEnv config -> Maybe FilePath -> WithHieDb -> IndexQueue -> IO IdeState) + -> (LSP.LanguageContextEnv config -> FilePath -> WithHieDb -> ThreadQueue -> IO IdeState) -> MVar () -> IO () -> (SomeLspId -> IO ()) -> (SomeLspId -> IO ()) -> Chan ReactorMessage -> LSP.LanguageContextEnv config -> TRequestMessage Method_Initialize -> IO (Either err (LSP.LanguageContextEnv config, IdeState)) -handleInit recorder getHieDbLoc getIdeState lifetime exitClientMsg clearReqId waitForCancel clientMsgChan env (TRequestMessage _ _ m params) = otTracedHandler "Initialize" (show m) $ \sp -> do +handleInit recorder defaultRoot getHieDbLoc getIdeState lifetime exitClientMsg clearReqId waitForCancel clientMsgChan env (TRequestMessage _ _ m params) = otTracedHandler "Initialize" (show m) $ \sp -> do traceWithSpan sp params - let root = LSP.resRootPath env - dir <- maybe getCurrentDirectory return root - dbLoc <- getHieDbLoc dir - - -- The database needs to be open for the duration of the reactor thread, but we need to pass in a reference - -- to 'getIdeState', so we use this dirty trick - dbMVar <- newEmptyMVar - ~(WithHieDbShield withHieDb,hieChan) <- unsafeInterleaveIO $ takeMVar dbMVar - - ide <- getIdeState env root withHieDb hieChan - + -- only shift if lsp root is different from the rootDir + -- see Note [Root Directory] + root <- case LSP.resRootPath env of + Just lspRoot | lspRoot /= defaultRoot -> setCurrentDirectory lspRoot >> return lspRoot + _ -> pure defaultRoot + dbLoc <- getHieDbLoc root let initConfig = parseConfiguration params - logWith recorder Info $ LogRegisteringIdeConfig initConfig - registerIdeConfiguration (shakeExtras ide) initConfig + dbMVar <- newEmptyMVar + let handleServerException (Left e) = do logWith recorder Error $ LogReactorThreadException e @@ -222,25 +219,27 @@ handleInit recorder getHieDbLoc getIdeState lifetime exitClientMsg clearReqId wa exceptionInHandler e = do logWith recorder Error $ LogReactorMessageActionException e + checkCancelled :: forall m . LspId m -> IO () -> (TResponseError m -> IO ()) -> IO () checkCancelled _id act k = - flip finally (clearReqId _id) $ + let sid = SomeLspId _id + in flip finally (clearReqId sid) $ catch (do -- We could optimize this by first checking if the id -- is in the cancelled set. However, this is unlikely to be a -- bottleneck and the additional check might hide -- issues with async exceptions that need to be fixed. - cancelOrRes <- race (waitForCancel _id) act + cancelOrRes <- race (waitForCancel sid) act case cancelOrRes of Left () -> do - logWith recorder Debug $ LogCancelledRequest _id - k $ ResponseError (InL LSPErrorCodes_RequestCancelled) "" Nothing + logWith recorder Debug $ LogCancelledRequest sid + k $ TResponseError (InL LSPErrorCodes_RequestCancelled) "" Nothing Right res -> pure res ) $ \(e :: SomeException) -> do exceptionInHandler e - k $ ResponseError (InR ErrorCodes_InternalError) (T.pack $ show e) Nothing + k $ TResponseError (InR ErrorCodes_InternalError) (T.pack $ show e) Nothing _ <- flip forkFinally handleServerException $ do - untilMVar lifetime $ runWithDb (cmapWithPrio LogSession recorder) dbLoc $ \withHieDb' hieChan' -> do - putMVar dbMVar (WithHieDbShield withHieDb',hieChan') + untilMVar lifetime $ runWithWorkerThreads (cmapWithPrio LogSession recorder) dbLoc $ \withHieDb' threadQueue' -> do + putMVar dbMVar (WithHieDbShield withHieDb',threadQueue') forever $ do msg <- readChan clientMsgChan -- We dispatch notifications synchronously and requests asynchronously @@ -249,9 +248,23 @@ handleInit recorder getHieDbLoc getIdeState lifetime exitClientMsg clearReqId wa ReactorNotification act -> handle exceptionInHandler act ReactorRequest _id act k -> void $ async $ checkCancelled _id act k logWith recorder Info LogReactorThreadStopped + + (WithHieDbShield withHieDb, threadQueue) <- takeMVar dbMVar + ide <- getIdeState env root withHieDb threadQueue + registerIdeConfiguration (shakeExtras ide) initConfig pure $ Right (env,ide) +-- | runWithWorkerThreads +-- create several threads to run the session, db and session loader +-- see Note [Serializing runs in separate thread] +runWithWorkerThreads :: Recorder (WithPriority Session.Log) -> FilePath -> (WithHieDb -> ThreadQueue -> IO ()) -> IO () +runWithWorkerThreads recorder dbLoc f = evalContT $ do + sessionRestartTQueue <- withWorkerQueue id + sessionLoaderTQueue <- withWorkerQueue id + (WithHieDbShield hiedb, threadQueue) <- runWithDb recorder dbLoc + liftIO $ f hiedb (ThreadQueue threadQueue sessionRestartTQueue sessionLoaderTQueue) + -- | Runs the action until it ends or until the given MVar is put. -- Rethrows any exceptions. untilMVar :: MonadUnliftIO m => MVar () -> m () -> m () @@ -265,10 +278,10 @@ cancelHandler cancelRequest = LSP.notificationHandler SMethod_CancelRequest $ \T toLspId (InL x) = IdInt x toLspId (InR y) = IdString y -shutdownHandler :: IO () -> LSP.Handlers (ServerM c) -shutdownHandler stopReactor = LSP.requestHandler SMethod_Shutdown $ \_ resp -> do +shutdownHandler :: Recorder (WithPriority Log) -> IO () -> LSP.Handlers (ServerM c) +shutdownHandler recorder stopReactor = LSP.requestHandler SMethod_Shutdown $ \_ resp -> do (_, ide) <- ask - liftIO $ logDebug (ideLogger ide) "Received shutdown message" + liftIO $ logWith recorder Debug LogServerShutdownMessage -- stop the reactor to free up the hiedb connection liftIO stopReactor -- flush out the Shake session to record a Shake profile if applicable diff --git a/ghcide/src/Development/IDE/LSP/Notifications.hs b/ghcide/src/Development/IDE/LSP/Notifications.hs index d0967a25a4..4f5475442c 100644 --- a/ghcide/src/Development/IDE/LSP/Notifications.hs +++ b/ghcide/src/Development/IDE/LSP/Notifications.hs @@ -3,8 +3,6 @@ {-# LANGUAGE DuplicateRecordFields #-} {-# LANGUAGE GADTs #-} -{-# LANGUAGE PolyKinds #-} -{-# LANGUAGE RankNTypes #-} module Development.IDE.LSP.Notifications ( whenUriFile @@ -33,7 +31,7 @@ import qualified Development.IDE.Core.FileStore as FileStore import Development.IDE.Core.IdeConfiguration import Development.IDE.Core.OfInterest hiding (Log, LogShake) import Development.IDE.Core.Service hiding (Log, LogShake) -import Development.IDE.Core.Shake hiding (Log, Priority) +import Development.IDE.Core.Shake hiding (Log) import qualified Development.IDE.Core.Shake as Shake import Development.IDE.Types.Location import Ide.Logger @@ -43,51 +41,63 @@ import Numeric.Natural data Log = LogShake Shake.Log | LogFileStore FileStore.Log + | LogOpenedTextDocument !Uri + | LogModifiedTextDocument !Uri + | LogSavedTextDocument !Uri + | LogClosedTextDocument !Uri + | LogWatchedFileEvents !Text.Text + | LogWarnNoWatchedFilesSupport deriving Show instance Pretty Log where pretty = \case LogShake msg -> pretty msg LogFileStore msg -> pretty msg + LogOpenedTextDocument uri -> "Opened text document:" <+> pretty (getUri uri) + LogModifiedTextDocument uri -> "Modified text document:" <+> pretty (getUri uri) + LogSavedTextDocument uri -> "Saved text document:" <+> pretty (getUri uri) + LogClosedTextDocument uri -> "Closed text document:" <+> pretty (getUri uri) + LogWatchedFileEvents msg -> "Watched file events:" <+> pretty msg + LogWarnNoWatchedFilesSupport -> "Client does not support watched files. Falling back to OS polling" whenUriFile :: Uri -> (NormalizedFilePath -> IO ()) -> IO () whenUriFile uri act = whenJust (LSP.uriToFilePath uri) $ act . toNormalizedFilePath' descriptor :: Recorder (WithPriority Log) -> PluginId -> PluginDescriptor IdeState -descriptor recorder plId = (defaultPluginDescriptor plId) { pluginNotificationHandlers = mconcat +descriptor recorder plId = (defaultPluginDescriptor plId desc) { pluginNotificationHandlers = mconcat [ mkPluginNotificationHandler LSP.SMethod_TextDocumentDidOpen $ \ide vfs _ (DidOpenTextDocumentParams TextDocumentItem{_uri,_version}) -> liftIO $ do atomically $ updatePositionMapping ide (VersionedTextDocumentIdentifier _uri _version) [] whenUriFile _uri $ \file -> do -- We don't know if the file actually exists, or if the contents match those on disk -- For example, vscode restores previously unsaved contents on open - addFileOfInterest ide file Modified{firstOpen=True} - setFileModified (cmapWithPrio LogFileStore recorder) (VFSModified vfs) ide False file - logDebug (ideLogger ide) $ "Opened text document: " <> getUri _uri + setFileModified (cmapWithPrio LogFileStore recorder) (VFSModified vfs) ide False file $ + addFileOfInterest ide file Modified{firstOpen=True} + logWith recorder Debug $ LogOpenedTextDocument _uri , mkPluginNotificationHandler LSP.SMethod_TextDocumentDidChange $ \ide vfs _ (DidChangeTextDocumentParams identifier@VersionedTextDocumentIdentifier{_uri} changes) -> liftIO $ do atomically $ updatePositionMapping ide identifier changes whenUriFile _uri $ \file -> do - addFileOfInterest ide file Modified{firstOpen=False} - setFileModified (cmapWithPrio LogFileStore recorder) (VFSModified vfs) ide False file - logDebug (ideLogger ide) $ "Modified text document: " <> getUri _uri + setFileModified (cmapWithPrio LogFileStore recorder) (VFSModified vfs) ide False file $ + addFileOfInterest ide file Modified{firstOpen=False} + logWith recorder Debug $ LogModifiedTextDocument _uri , mkPluginNotificationHandler LSP.SMethod_TextDocumentDidSave $ \ide vfs _ (DidSaveTextDocumentParams TextDocumentIdentifier{_uri} _) -> liftIO $ do whenUriFile _uri $ \file -> do - addFileOfInterest ide file OnDisk - setFileModified (cmapWithPrio LogFileStore recorder) (VFSModified vfs) ide True file - logDebug (ideLogger ide) $ "Saved text document: " <> getUri _uri + setFileModified (cmapWithPrio LogFileStore recorder) (VFSModified vfs) ide True file $ + addFileOfInterest ide file OnDisk + logWith recorder Debug $ LogSavedTextDocument _uri , mkPluginNotificationHandler LSP.SMethod_TextDocumentDidClose $ \ide vfs _ (DidCloseTextDocumentParams TextDocumentIdentifier{_uri}) -> liftIO $ do whenUriFile _uri $ \file -> do - deleteFileOfInterest ide file let msg = "Closed text document: " <> getUri _uri - scheduleGarbageCollection ide - setSomethingModified (VFSModified vfs) ide [] $ Text.unpack msg - logDebug (ideLogger ide) msg + setSomethingModified (VFSModified vfs) ide (Text.unpack msg) $ do + scheduleGarbageCollection ide + deleteFileOfInterest ide file + logWith recorder Debug $ LogClosedTextDocument _uri , mkPluginNotificationHandler LSP.SMethod_WorkspaceDidChangeWatchedFiles $ \ide vfs _ (DidChangeWatchedFilesParams fileEvents) -> liftIO $ do @@ -104,10 +114,11 @@ descriptor recorder plId = (defaultPluginDescriptor plId) { pluginNotificationHa ] unless (null fileEvents') $ do let msg = show fileEvents' - logDebug (ideLogger ide) $ "Watched file events: " <> Text.pack msg - modifyFileExists ide fileEvents' - resetFileStore ide fileEvents' - setSomethingModified (VFSModified vfs) ide [] msg + logWith recorder Debug $ LogWatchedFileEvents (Text.pack msg) + setSomethingModified (VFSModified vfs) ide msg $ do + ks1 <- resetFileStore ide fileEvents' + ks2 <- modifyFileExists ide fileEvents' + return (ks1 <> ks2) , mkPluginNotificationHandler LSP.SMethod_WorkspaceDidChangeWorkspaceFolders $ \ide _ _ (DidChangeWorkspaceFoldersParams events) -> liftIO $ do @@ -135,13 +146,15 @@ descriptor recorder plId = (defaultPluginDescriptor plId) { pluginNotificationHa let globs = watchedGlobs opts success <- registerFileWatches globs unless success $ - liftIO $ logDebug (ideLogger ide) "Warning: Client does not support watched files. Falling back to OS polling" + liftIO $ logWith recorder Warning LogWarnNoWatchedFilesSupport ], -- The ghcide descriptors should come last'ish so that the notification handlers -- (which restart the Shake build) run after everything else pluginPriority = ghcideNotificationsPluginPriority } + where + desc = "Handles basic notifications for ghcide" ghcideNotificationsPluginPriority :: Natural ghcideNotificationsPluginPriority = defaultPluginPriority - 900 diff --git a/ghcide/src/Development/IDE/LSP/Outline.hs b/ghcide/src/Development/IDE/LSP/Outline.hs index c9c3de1540..af2a0f1c97 100644 --- a/ghcide/src/Development/IDE/LSP/Outline.hs +++ b/ghcide/src/Development/IDE/LSP/Outline.hs @@ -2,7 +2,6 @@ {-# LANGUAGE DuplicateRecordFields #-} {-# LANGUAGE GADTs #-} -{-# LANGUAGE RankNTypes #-} module Development.IDE.LSP.Outline ( moduleOutline @@ -10,32 +9,27 @@ module Development.IDE.LSP.Outline where import Control.Monad.IO.Class +import Data.Foldable (toList) import Data.Functor import Data.Generics hiding (Prefix) +import Data.List.NonEmpty (nonEmpty) import Data.Maybe import Development.IDE.Core.Rules import Development.IDE.Core.Shake import Development.IDE.GHC.Compat import Development.IDE.GHC.Error (rangeToRealSrcSpan, realSrcSpanToRange) -import Development.IDE.Types.Location import Development.IDE.GHC.Util (printOutputable) +import Development.IDE.Types.Location import Ide.Types -import Language.LSP.Protocol.Types (DocumentSymbol (..), +import Language.LSP.Protocol.Message +import Language.LSP.Protocol.Types (DocumentSymbol (..), DocumentSymbolParams (DocumentSymbolParams, _textDocument), SymbolKind (..), TextDocumentIdentifier (TextDocumentIdentifier), - type (|?) (InL, InR), uriToFilePath) -import Language.LSP.Protocol.Message - --- See Note [Guidelines For Using CPP In GHCIDE Import Statements] + type (|?) (InL, InR), + uriToFilePath) -import Data.List.NonEmpty (nonEmpty) -import Data.Foldable (toList) - -#if !MIN_VERSION_ghc(9,3,0) -import qualified Data.Text as T -#endif moduleOutline :: PluginMethodHandler IdeState Method_TextDocumentDocumentSymbol @@ -120,21 +114,13 @@ documentSymbolForDecl (L (locA -> (RealSrcSpan l _)) (TyClD _ DataDecl { tcdLNam , L (locA -> RealSrcSpan l' _) n <- cs , let l'' = case con of L (locA -> RealSrcSpan l''' _) _ -> l''' - _ -> l' + _ -> l' ] } where cvtFld :: LFieldOcc GhcPs -> Maybe DocumentSymbol -#if MIN_VERSION_ghc(9,3,0) cvtFld (L (locA -> RealSrcSpan l' _) n) = Just $ (defDocumentSymbol l' :: DocumentSymbol) -#else - cvtFld (L (RealSrcSpan l' _) n) = Just $ (defDocumentSymbol l' :: DocumentSymbol) -#endif -#if MIN_VERSION_ghc(9,3,0) { _name = printOutputable (unLoc (foLabel n)) -#else - { _name = printOutputable (unLoc (rdrNameFieldOcc n)) -#endif , _kind = SymbolKind_Field } cvtFld _ = Nothing @@ -150,23 +136,13 @@ documentSymbolForDecl (L (locA -> (RealSrcSpan l _)) (InstD _ ClsInstD { cid_ins documentSymbolForDecl (L (locA -> (RealSrcSpan l _)) (InstD _ DataFamInstD { dfid_inst = DataFamInstDecl FamEqn { feqn_tycon, feqn_pats } })) = Just (defDocumentSymbol l :: DocumentSymbol) { _name = -#if MIN_VERSION_ghc(9,3,0) - printOutputable $ pprHsArgsApp (unLoc feqn_tycon) Prefix (feqn_pats) -#else - printOutputable (unLoc feqn_tycon) <> " " <> T.unwords - (map printOutputable feqn_pats) -#endif + printOutputable $ pprHsArgsApp (unLoc feqn_tycon) Prefix feqn_pats , _kind = SymbolKind_Interface } documentSymbolForDecl (L (locA -> (RealSrcSpan l _)) (InstD _ TyFamInstD { tfid_inst = TyFamInstDecl _ FamEqn { feqn_tycon, feqn_pats } })) = Just (defDocumentSymbol l :: DocumentSymbol) { _name = -#if MIN_VERSION_ghc(9,3,0) - printOutputable $ pprHsArgsApp (unLoc feqn_tycon) Prefix (feqn_pats) -#else - printOutputable (unLoc feqn_tycon) <> " " <> T.unwords - (map printOutputable feqn_pats) -#endif + printOutputable $ pprHsArgsApp (unLoc feqn_tycon) Prefix feqn_pats , _kind = SymbolKind_Interface } documentSymbolForDecl (L (locA -> (RealSrcSpan l _)) (DerivD _ DerivDecl { deriv_type })) = @@ -191,12 +167,10 @@ documentSymbolForDecl (L (locA -> (RealSrcSpan l _)) (ForD _ x)) = Just { _name = case x of ForeignImport{} -> name ForeignExport{} -> name - XForeignDecl{} -> "?" , _kind = SymbolKind_Object , _detail = case x of ForeignImport{} -> Just "import" ForeignExport{} -> Just "export" - XForeignDecl{} -> Nothing } where name = printOutputable $ unLoc $ fd_name x @@ -271,19 +245,19 @@ hsConDeclsBinders cons get_flds_h98 :: HsConDeclH98Details GhcPs -> [LFieldOcc GhcPs] get_flds_h98 (RecCon flds) = get_flds (reLoc flds) - get_flds_h98 _ = [] + get_flds_h98 _ = [] get_flds_gadt :: HsConDeclGADTDetails GhcPs - -> ([LFieldOcc GhcPs]) -#if MIN_VERSION_ghc(9,3,0) - get_flds_gadt (RecConGADT flds _) = get_flds (reLoc flds) + -> [LFieldOcc GhcPs] +#if MIN_VERSION_ghc(9,9,0) + get_flds_gadt (RecConGADT _ flds) = get_flds (reLoc flds) #else - get_flds_gadt (RecConGADT flds) = get_flds (reLoc flds) + get_flds_gadt (RecConGADT flds _) = get_flds (reLoc flds) #endif - get_flds_gadt _ = [] + get_flds_gadt _ = [] get_flds :: Located [LConDeclField GhcPs] - -> ([LFieldOcc GhcPs]) + -> [LFieldOcc GhcPs] get_flds flds = concatMap (cd_fld_names . unLoc) (unLoc flds) diff --git a/ghcide/src/Development/IDE/LSP/Server.hs b/ghcide/src/Development/IDE/LSP/Server.hs index 28bba2d526..605250491b 100644 --- a/ghcide/src/Development/IDE/LSP/Server.hs +++ b/ghcide/src/Development/IDE/LSP/Server.hs @@ -1,4 +1,3 @@ -{-# LANGUAGE FlexibleInstances #-} {-# LANGUAGE UndecidableInstances #-} -- Copyright (c) 2019 The DAML Authors. All rights reserved. -- SPDX-License-Identifier: Apache-2.0 @@ -23,7 +22,7 @@ import UnliftIO.Chan data ReactorMessage = ReactorNotification (IO ()) - | ReactorRequest SomeLspId (IO ()) (ResponseError -> IO ()) + | forall m . ReactorRequest (LspId m) (IO ()) (TResponseError m -> IO ()) type ReactorChan = Chan ReactorMessage newtype ServerM c a = ServerM { unServerM :: ReaderT (ReactorChan, IdeState) (LspM c) a } @@ -32,17 +31,17 @@ newtype ServerM c a = ServerM { unServerM :: ReaderT (ReactorChan, IdeState) (Ls requestHandler :: forall m c. PluginMethod Request m => SMethod m - -> (IdeState -> MessageParams m -> LspM c (Either ResponseError (MessageResult m))) + -> (IdeState -> MessageParams m -> LspM c (Either (TResponseError m) (MessageResult m))) -> Handlers (ServerM c) requestHandler m k = LSP.requestHandler m $ \TRequestMessage{_method,_id,_params} resp -> do st@(chan,ide) <- ask env <- LSP.getLspEnv - let resp' :: Either ResponseError (MessageResult m) -> LspM c () + let resp' :: Either (TResponseError m) (MessageResult m) -> LspM c () resp' = flip (runReaderT . unServerM) st . resp trace x = otTracedHandler "Request" (show _method) $ \sp -> do traceWithSpan sp _params x - writeChan chan $ ReactorRequest (SomeLspId _id) (trace $ LSP.runLspT env $ resp' =<< k ide _params) (LSP.runLspT env . resp' . Left) + writeChan chan $ ReactorRequest _id (trace $ LSP.runLspT env $ resp' =<< k ide _params) (LSP.runLspT env . resp' . Left) notificationHandler :: forall m c. PluginMethod Notification m => diff --git a/ghcide/src/Development/IDE/Main.hs b/ghcide/src/Development/IDE/Main.hs index bad9ed7ba7..872e957364 100644 --- a/ghcide/src/Development/IDE/Main.hs +++ b/ghcide/src/Development/IDE/Main.hs @@ -1,5 +1,4 @@ {-# OPTIONS_GHC -Wno-orphans #-} -{-# LANGUAGE RankNTypes #-} module Development.IDE.Main (Arguments(..) ,defaultArguments @@ -16,16 +15,12 @@ import Control.Concurrent.Extra (withNumCapabilities) import Control.Concurrent.MVar (newEmptyMVar, putMVar, tryReadMVar) import Control.Concurrent.STM.Stats (dumpSTMStats) -import Control.Exception.Safe (SomeException, - catchAny, - displayException) import Control.Monad.Extra (concatMapM, unless, when) import Control.Monad.IO.Class (liftIO) import qualified Data.Aeson as J import Data.Coerce (coerce) import Data.Default (Default (def)) -import Data.Foldable (traverse_) import Data.Hashable (hashed) import qualified Data.HashMap.Strict as HashMap import Data.List.Extra (intercalate, @@ -34,9 +29,8 @@ import Data.List.Extra (intercalate, import Data.Maybe (catMaybes, isJust) import qualified Data.Text as T import Development.IDE (Action, - Priority (Debug, Error), - Rules, emptyFilePath, - hDuplicateTo') + Priority (Debug), + Rules, hDuplicateTo') import Development.IDE.Core.Debouncer (Debouncer, newAsyncDebouncer) import Development.IDE.Core.FileStore (isWatchSupported, @@ -56,17 +50,17 @@ import Development.IDE.Core.Service (initialise, runAction) import qualified Development.IDE.Core.Service as Service import Development.IDE.Core.Shake (IdeState (shakeExtras), - IndexQueue, + ThreadQueue (tLoaderQueue), shakeSessionInit, uses) import qualified Development.IDE.Core.Shake as Shake import Development.IDE.Graph (action) import Development.IDE.LSP.LanguageServer (runLanguageServer, + runWithWorkerThreads, setupLSP) import qualified Development.IDE.LSP.LanguageServer as LanguageServer import Development.IDE.Main.HeapStats (withHeapStats) import qualified Development.IDE.Main.HeapStats as HeapStats -import qualified Development.IDE.Monitoring.EKG as EKG import qualified Development.IDE.Monitoring.OpenTelemetry as OpenTelemetry import Development.IDE.Plugin (Plugin (pluginHandlers, pluginModifyDynflags, pluginRules)) import Development.IDE.Plugin.HLS (asGhcIdePlugin) @@ -75,10 +69,9 @@ import qualified Development.IDE.Plugin.HLS.GhcIde as GhcIde import qualified Development.IDE.Plugin.Test as Test import Development.IDE.Session (SessionLoadingOptions, getHieDbLoc, + getInitialGhcLibDirDefault, loadSessionWithOptions, - retryOnSqliteBusy, - runWithDb, - setInitialDynFlags) + retryOnSqliteBusy) import qualified Development.IDE.Session as Session import Development.IDE.Types.Location (NormalizedUri, toNormalizedFilePath') @@ -90,20 +83,20 @@ import Development.IDE.Types.Options (IdeGhcSession, defaultIdeOptions, optModifyDynFlags, optTesting) -import Development.IDE.Types.Shake (WithHieDb, toKey) +import Development.IDE.Types.Shake (WithHieDb, + toNoFileKey) import GHC.Conc (getNumProcessors) import GHC.IO.Encoding (setLocaleEncoding) import GHC.IO.Handle (hDuplicate) import HIE.Bios.Cradle (findCradle) import qualified HieDb.Run as HieDb -import Ide.Logger (Logger, - Pretty (pretty), +import Ide.Logger (Pretty (pretty), Priority (Info), Recorder, WithPriority, cmapWithPrio, - logDebug, logWith, - nest, vsep, (<+>)) + logWith, nest, vsep, + (<+>)) import Ide.Plugin.Config (CheckParents (NeverCheck), Config, checkParents, checkProject, @@ -140,7 +133,7 @@ data Log | LogLspStart [PluginId] | LogLspStartDuration !Seconds | LogShouldRunSubset !Bool - | LogSetInitialDynFlagsException !SomeException + | LogConfigurationChange T.Text | LogService Service.Log | LogShake Shake.Log | LogGhcIde GhcIde.Log @@ -163,8 +156,7 @@ instance Pretty Log where "Started LSP server in" <+> pretty (showDuration duration) LogShouldRunSubset shouldRunSubset -> "shouldRunSubset:" <+> pretty shouldRunSubset - LogSetInitialDynFlagsException e -> - "setInitialDynFlags:" <+> pretty (displayException e) + LogConfigurationChange msg -> "Configuration changed:" <+> pretty msg LogService msg -> pretty msg LogShake msg -> pretty msg LogGhcIde msg -> pretty msg @@ -209,9 +201,8 @@ commandP plugins = data Arguments = Arguments - { argsProjectRoot :: Maybe FilePath + { argsProjectRoot :: FilePath , argCommand :: Command - , argsLogger :: IO Logger , argsRules :: Rules () , argsHlsPlugins :: IdePlugins IdeState , argsGhcidePlugin :: Plugin Config -- ^ Deprecated @@ -225,14 +216,14 @@ data Arguments = Arguments , argsHandleOut :: IO Handle , argsThreads :: Maybe Natural , argsMonitoring :: IO Monitoring + , argsDisableKick :: Bool -- ^ flag to disable kick used for testing } -defaultArguments :: Recorder (WithPriority Log) -> Logger -> IdePlugins IdeState -> Arguments -defaultArguments recorder logger plugins = Arguments - { argsProjectRoot = Nothing +defaultArguments :: Recorder (WithPriority Log) -> FilePath -> IdePlugins IdeState -> Arguments +defaultArguments recorder projectRoot plugins = Arguments + { argsProjectRoot = projectRoot -- ^ see Note [Root Directory] , argCommand = LSP - , argsLogger = pure logger - , argsRules = mainRule (cmapWithPrio LogRules recorder) def >> action kick + , argsRules = mainRule (cmapWithPrio LogRules recorder) def , argsGhcidePlugin = mempty , argsHlsPlugins = pluginDescToIdePlugins (GhcIde.descriptors (cmapWithPrio LogGhcIde recorder)) <> plugins , argsSessionLoadingOptions = def @@ -240,7 +231,15 @@ defaultArguments recorder logger plugins = Arguments { optCheckProject = pure $ checkProject config , optCheckParents = pure $ checkParents config } - , argsLspOptions = def {LSP.optCompletionTriggerCharacters = Just "."} + , argsLspOptions = def + { LSP.optCompletionTriggerCharacters = Just "." + -- Generally people start to notice that something is taking a while at about 1s, so + -- that's when we start reporting progress + , LSP.optProgressStartDelay = 1_000_000 + -- Once progress is being reported, it's nice to see that it's moving reasonably quickly, + -- but not so fast that it's ugly. This number is a bit made up + , LSP.optProgressUpdateDelay = 1_00_000 + } , argsDefaultHlsConfig = def , argsGetHieDbLoc = getHieDbLoc , argsDebouncer = newAsyncDebouncer @@ -260,15 +259,16 @@ defaultArguments recorder logger plugins = Arguments -- the language server tests without the redirection. putStr " " >> hFlush stdout return newStdout - , argsMonitoring = OpenTelemetry.monitoring <> EKG.monitoring logger 8999 + , argsMonitoring = OpenTelemetry.monitoring + , argsDisableKick = False } -testing :: Recorder (WithPriority Log) -> Logger -> IdePlugins IdeState -> Arguments -testing recorder logger plugins = +testing :: Recorder (WithPriority Log) -> FilePath -> IdePlugins IdeState -> Arguments +testing recorder projectRoot plugins = let - arguments@Arguments{ argsHlsPlugins, argsIdeOptions } = - defaultArguments recorder logger plugins + arguments@Arguments{ argsHlsPlugins, argsIdeOptions, argsLspOptions } = + defaultArguments recorder projectRoot plugins hlsPlugins = pluginDescToIdePlugins $ idePluginsToPluginDesc argsHlsPlugins ++ [Test.blockCommandDescriptor "block-command", Test.plugin] @@ -277,10 +277,12 @@ testing recorder logger plugins = defOptions = argsIdeOptions config sessionLoader in defOptions{ optTesting = IdeTesting True } + lspOptions = argsLspOptions { LSP.optProgressStartDelay = 0, LSP.optProgressUpdateDelay = 0 } in arguments { argsHlsPlugins = hlsPlugins , argsIdeOptions = ideOptions + , argsLspOptions = lspOptions } defaultMain :: Recorder (WithPriority Log) -> Arguments -> IO () @@ -289,7 +291,6 @@ defaultMain recorder Arguments{..} = withHeapStats (cmapWithPrio LogHeapStats re fun = do setLocaleEncoding utf8 pid <- T.pack . show <$> getProcessID - logger <- argsLogger hSetBuffering stderr LineBuffering let hlsPlugin = asGhcIdePlugin (cmapWithPrio LogPluginHLS recorder) argsHlsPlugins @@ -297,7 +298,13 @@ defaultMain recorder Arguments{..} = withHeapStats (cmapWithPrio LogHeapStats re plugins = hlsPlugin <> argsGhcidePlugin options = argsLspOptions { LSP.optExecuteCommandCommands = LSP.optExecuteCommandCommands argsLspOptions <> Just hlsCommands } argsParseConfig = getConfigFromNotification argsHlsPlugins - rules = argsRules >> pluginRules plugins + rules = do + argsRules + unless argsDisableKick $ action kick + pluginRules plugins + -- install the main and ghcide-plugin rules + -- install the kick action, which triggers a typecheck on every + -- Shake database restart, i.e. on every user edit. debouncer <- argsDebouncer inH <- argsHandleIn @@ -312,22 +319,11 @@ defaultMain recorder Arguments{..} = withHeapStats (cmapWithPrio LogHeapStats re logWith recorder Info $ LogLspStart (pluginId <$> ipMap argsHlsPlugins) ideStateVar <- newEmptyMVar - let getIdeState :: LSP.LanguageContextEnv Config -> Maybe FilePath -> WithHieDb -> IndexQueue -> IO IdeState - getIdeState env rootPath withHieDb hieChan = do - traverse_ IO.setCurrentDirectory rootPath + let getIdeState :: LSP.LanguageContextEnv Config -> FilePath -> WithHieDb -> Shake.ThreadQueue -> IO IdeState + getIdeState env rootPath withHieDb threadQueue = do t <- ioT logWith recorder Info $ LogLspStartDuration t - - dir <- maybe IO.getCurrentDirectory return rootPath - - -- We want to set the global DynFlags right now, so that we can use - -- `unsafeGlobalDynFlags` even before the project is configured - _mlibdir <- - setInitialDynFlags (cmapWithPrio LogSession recorder) dir argsSessionLoadingOptions - -- TODO: should probably catch/log/rethrow at top level instead - `catchAny` (\e -> logWith recorder Error (LogSetInitialDynFlagsException e) >> pure Nothing) - - sessionLoader <- loadSessionWithOptions (cmapWithPrio LogSession recorder) argsSessionLoadingOptions dir + sessionLoader <- loadSessionWithOptions (cmapWithPrio LogSession recorder) argsSessionLoadingOptions rootPath (tLoaderQueue threadQueue) config <- LSP.runLspT env LSP.getConfig let def_options = argsIdeOptions config sessionLoader @@ -348,16 +344,16 @@ defaultMain recorder Arguments{..} = withHeapStats (cmapWithPrio LogHeapStats re argsHlsPlugins rules (Just env) - logger debouncer ideOptions withHieDb - hieChan + threadQueue monitoring + rootPath putMVar ideStateVar ide pure ide - let setup = setupLSP (cmapWithPrio LogLanguageServer recorder) argsGetHieDbLoc (pluginHandlers plugins) getIdeState + let setup = setupLSP (cmapWithPrio LogLanguageServer recorder) argsProjectRoot argsGetHieDbLoc (pluginHandlers plugins) getIdeState -- See Note [Client configuration in Rules] onConfigChange cfg = do -- TODO: this is nuts, we're converting back to JSON just to get a fingerprint @@ -367,16 +363,17 @@ defaultMain recorder Arguments{..} = withHeapStats (cmapWithPrio LogHeapStats re Nothing -> pure () Just ide -> liftIO $ do let msg = T.pack $ show cfg - logDebug (Shake.ideLogger ide) $ "Configuration changed: " <> msg - modifyClientSettings ide (const $ Just cfgObj) - setSomethingModified Shake.VFSUnmodified ide [toKey Rules.GetClientSettings emptyFilePath] "config change" + setSomethingModified Shake.VFSUnmodified ide "config change" $ do + logWith recorder Debug $ LogConfigurationChange msg + modifyClientSettings ide (const $ Just cfgObj) + return [toNoFileKey Rules.GetClientSettings] runLanguageServer (cmapWithPrio LogLanguageServer recorder) options inH outH argsDefaultHlsConfig argsParseConfig onConfigChange setup dumpSTMStats Check argFiles -> do - dir <- maybe IO.getCurrentDirectory return argsProjectRoot + let dir = argsProjectRoot dbLoc <- getHieDbLoc dir - runWithDb (cmapWithPrio LogSession recorder) dbLoc $ \hiedb hieChan -> do + runWithWorkerThreads (cmapWithPrio LogSession recorder) dbLoc $ \hiedb threadQueue -> do -- GHC produces messages with UTF8 in them, so make sure the terminal doesn't error hSetEncoding stdout utf8 hSetEncoding stderr utf8 @@ -397,14 +394,14 @@ defaultMain recorder Arguments{..} = withHeapStats (cmapWithPrio LogHeapStats re putStrLn $ "Found " ++ show n ++ " cradle" ++ ['s' | n /= 1] when (n > 0) $ putStrLn $ " (" ++ intercalate ", " (catMaybes ucradles) ++ ")" putStrLn "\nStep 3/4: Initializing the IDE" - sessionLoader <- loadSessionWithOptions (cmapWithPrio LogSession recorder) argsSessionLoadingOptions dir + sessionLoader <- loadSessionWithOptions (cmapWithPrio LogSession recorder) argsSessionLoadingOptions dir (tLoaderQueue threadQueue) let def_options = argsIdeOptions argsDefaultHlsConfig sessionLoader ideOptions = def_options { optCheckParents = pure NeverCheck , optCheckProject = pure False , optModifyDynFlags = optModifyDynFlags def_options <> pluginModifyDynflags plugins } - ide <- initialise (cmapWithPrio LogService recorder) argsDefaultHlsConfig argsHlsPlugins rules Nothing logger debouncer ideOptions hiedb hieChan mempty + ide <- initialise (cmapWithPrio LogService recorder) argsDefaultHlsConfig argsHlsPlugins rules Nothing debouncer ideOptions hiedb threadQueue mempty dir shakeSessionInit (cmapWithPrio LogShake recorder) ide registerIdeConfiguration (shakeExtras ide) $ IdeConfiguration mempty (hashed Nothing) @@ -422,27 +419,27 @@ defaultMain recorder Arguments{..} = withHeapStats (cmapWithPrio LogHeapStats re unless (null failed) (exitWith $ ExitFailure (length failed)) Db opts cmd -> do - root <- maybe IO.getCurrentDirectory return argsProjectRoot + let root = argsProjectRoot dbLoc <- getHieDbLoc root hPutStrLn stderr $ "Using hiedb at: " ++ dbLoc - mlibdir <- setInitialDynFlags (cmapWithPrio LogSession recorder) root def + mlibdir <- getInitialGhcLibDirDefault (cmapWithPrio LogSession recorder) root rng <- newStdGen case mlibdir of Nothing -> exitWith $ ExitFailure 1 Just libdir -> retryOnSqliteBusy (cmapWithPrio LogSession recorder) rng (HieDb.runCommand libdir opts{HieDb.database = dbLoc} cmd) Custom (IdeCommand c) -> do - root <- maybe IO.getCurrentDirectory return argsProjectRoot + let root = argsProjectRoot dbLoc <- getHieDbLoc root - runWithDb (cmapWithPrio LogSession recorder) dbLoc $ \hiedb hieChan -> do - sessionLoader <- loadSessionWithOptions (cmapWithPrio LogSession recorder) argsSessionLoadingOptions "." + runWithWorkerThreads (cmapWithPrio LogSession recorder) dbLoc $ \hiedb threadQueue -> do + sessionLoader <- loadSessionWithOptions (cmapWithPrio LogSession recorder) argsSessionLoadingOptions "." (tLoaderQueue threadQueue) let def_options = argsIdeOptions argsDefaultHlsConfig sessionLoader ideOptions = def_options { optCheckParents = pure NeverCheck , optCheckProject = pure False , optModifyDynFlags = optModifyDynFlags def_options <> pluginModifyDynflags plugins } - ide <- initialise (cmapWithPrio LogService recorder) argsDefaultHlsConfig argsHlsPlugins rules Nothing logger debouncer ideOptions hiedb hieChan mempty + ide <- initialise (cmapWithPrio LogService recorder) argsDefaultHlsConfig argsHlsPlugins rules Nothing debouncer ideOptions hiedb threadQueue mempty root shakeSessionInit (cmapWithPrio LogShake recorder) ide registerIdeConfiguration (shakeExtras ide) $ IdeConfiguration mempty (hashed Nothing) c ide diff --git a/ghcide/src/Development/IDE/Main/HeapStats.hs b/ghcide/src/Development/IDE/Main/HeapStats.hs index 0a19f6339b..a6f685b68c 100644 --- a/ghcide/src/Development/IDE/Main/HeapStats.hs +++ b/ghcide/src/Development/IDE/Main/HeapStats.hs @@ -1,4 +1,3 @@ -{-# LANGUAGE NumericUnderscores #-} -- | Logging utilities for reporting heap statistics module Development.IDE.Main.HeapStats ( withHeapStats, Log(..)) where diff --git a/ghcide/src/Development/IDE/Monitoring/EKG.hs b/ghcide/src/Development/IDE/Monitoring/EKG.hs deleted file mode 100644 index 26414fdf04..0000000000 --- a/ghcide/src/Development/IDE/Monitoring/EKG.hs +++ /dev/null @@ -1,49 +0,0 @@ -{-# LANGUAGE CPP #-} -module Development.IDE.Monitoring.EKG(monitoring) where - -import Development.IDE.Types.Monitoring (Monitoring (..)) -import Ide.Logger (Logger) - -#ifdef MONITORING_EKG -import Control.Concurrent (killThread) -import Control.Concurrent.Async (async, waitCatch) -import Control.Monad (forM_) -import Data.Text (pack) -import Ide.Logger (logInfo) -import qualified System.Metrics as Monitoring -import qualified System.Remote.Monitoring.Wai as Monitoring - --- | Monitoring using EKG -monitoring :: Logger -> Int -> IO Monitoring -monitoring logger port = do - store <- Monitoring.newStore - Monitoring.registerGcMetrics store - let registerCounter name read = Monitoring.registerCounter name read store - registerGauge name read = Monitoring.registerGauge name read store - start = do - server <- do - let startServer = Monitoring.forkServerWith store "localhost" port - -- this can fail if the port is busy, throwing an async exception back to us - -- to handle that, wrap the server thread in an async - mb_server <- async startServer >>= waitCatch - case mb_server of - Right s -> do - logInfo logger $ pack $ - "Started monitoring server on port " <> show port - return $ Just s - Left e -> do - logInfo logger $ pack $ - "Unable to bind monitoring server on port " - <> show port <> ":" <> show e - return Nothing - return $ forM_ server $ \s -> do - logInfo logger "Stopping monitoring server" - killThread $ Monitoring.serverThreadId s - return $ Monitoring {..} - -#else - -monitoring :: Logger -> Int -> IO Monitoring -monitoring _ _ = mempty - -#endif diff --git a/ghcide/src/Development/IDE/Plugin/Completions.hs b/ghcide/src/Development/IDE/Plugin/Completions.hs index a588f46f34..d92bf1da85 100644 --- a/ghcide/src/Development/IDE/Plugin/Completions.hs +++ b/ghcide/src/Development/IDE/Plugin/Completions.hs @@ -1,6 +1,5 @@ {-# LANGUAGE CPP #-} {-# LANGUAGE OverloadedLabels #-} -{-# LANGUAGE RankNTypes #-} {-# LANGUAGE TypeFamilies #-} module Development.IDE.Plugin.Completions @@ -20,6 +19,7 @@ import qualified Data.HashSet as Set import Data.Maybe import qualified Data.Text as T import Development.IDE.Core.Compile +import Development.IDE.Core.FileStore (getUriContents) import Development.IDE.Core.PluginUtils import Development.IDE.Core.PositionMapping import Development.IDE.Core.RuleTypes @@ -48,7 +48,6 @@ import Ide.Types import qualified Language.LSP.Protocol.Lens as L import Language.LSP.Protocol.Message import Language.LSP.Protocol.Types -import qualified Language.LSP.Server as LSP import Numeric.Natural import Prelude hiding (mod) import Text.Fuzzy.Parallel (Scored (..)) @@ -57,8 +56,6 @@ import Development.IDE.Core.Rules (usePropertyAction) import qualified Ide.Plugin.Config as Config --- See Note [Guidelines For Using CPP In GHCIDE Import Statements] - import qualified GHC.LanguageExtensions as LangExt data Log = LogShake Shake.Log deriving Show @@ -71,13 +68,15 @@ ghcideCompletionsPluginPriority :: Natural ghcideCompletionsPluginPriority = defaultPluginPriority descriptor :: Recorder (WithPriority Log) -> PluginId -> PluginDescriptor IdeState -descriptor recorder plId = (defaultPluginDescriptor plId) +descriptor recorder plId = (defaultPluginDescriptor plId desc) { pluginRules = produceCompletions recorder , pluginHandlers = mkPluginHandler SMethod_TextDocumentCompletion getCompletionsLSP <> mkResolveHandler SMethod_CompletionItemResolve resolveCompletion , pluginConfigDescriptor = defaultConfigDescriptor {configCustomConfig = mkCustomConfig properties} , pluginPriority = ghcideCompletionsPluginPriority } + where + desc = "Provides Haskell completions" produceCompletions :: Recorder (WithPriority Log) -> Rules () @@ -115,15 +114,10 @@ produceCompletions recorder = do -- Drop any explicit imports in ImportDecl if not hidden dropListFromImportDecl :: LImportDecl GhcPs -> LImportDecl GhcPs dropListFromImportDecl iDecl = let -#if MIN_VERSION_ghc(9,5,0) f d@ImportDecl {ideclImportList} = case ideclImportList of Just (Exactly, _) -> d {ideclImportList=Nothing} -#else - f d@ImportDecl {ideclHiding} = case ideclHiding of - Just (False, _) -> d {ideclHiding=Nothing} -#endif -- if hiding or Nothing just return d - _ -> d + _ -> d f x = x in f <$> iDecl @@ -135,15 +129,11 @@ resolveCompletion ide _pid comp@CompletionItem{_detail,_documentation,_data_} ur $ runIdeActionE "CompletionResolve.GhcSessionDeps" (shakeExtras ide) $ useWithStaleFastE GhcSessionDeps file let nc = ideNc $ shakeExtras ide -#if MIN_VERSION_ghc(9,3,0) name <- liftIO $ lookupNameCache nc mod occ -#else - name <- liftIO $ upNameCache nc (lookupNameCache mod occ) -#endif mdkm <- liftIO $ runIdeAction "CompletionResolve.GetDocMap" (shakeExtras ide) $ useWithStaleFast GetDocMap file let (dm,km) = case mdkm of - Just (DKMap docMap kindMap, _) -> (docMap,kindMap) - Nothing -> (mempty, mempty) + Just (DKMap docMap tyThingMap, _) -> (docMap,tyThingMap) + Nothing -> (mempty, mempty) doc <- case lookupNameEnv dm name of Just doc -> pure $ spanDocToMarkdown doc Nothing -> liftIO $ spanDocToMarkdown <$> getDocumentationTryGhc (hscEnv sess) name @@ -171,8 +161,9 @@ getCompletionsLSP ide plId CompletionParams{_textDocument=TextDocumentIdentifier uri ,_position=position ,_context=completionContext} = ExceptT $ do - contents <- LSP.getVirtualFile $ toNormalizedUri uri - fmap Right $ case (contents, uriToFilePath' uri) of + contentsMaybe <- + liftIO $ runAction "Completion" ide $ getUriContents $ toNormalizedUri uri + fmap Right $ case (contentsMaybe, uriToFilePath' uri) of (Just cnts, Just path) -> do let npath = toNormalizedFilePath' path (ideOpts, compls, moduleExports, astres) <- liftIO $ runIdeAction "Completion" (shakeExtras ide) $ do @@ -182,7 +173,7 @@ getCompletionsLSP ide plId pm <- useWithStaleFast GetParsedModule npath binds <- fromMaybe (mempty, zeroMapping) <$> useWithStaleFast GetBindings npath knownTargets <- liftIO $ runAction "Completion" ide $ useNoFile GetKnownTargets - let localModules = maybe [] Map.keys knownTargets + let localModules = maybe [] (Map.keys . targetMap) knownTargets let lModules = mempty{importableModules = map toModueNameText localModules} -- set up the exports map including both package and project-level identifiers packageExportsMapIO <- fmap(envPackageExports . fst) <$> useWithStaleFast GhcSession npath @@ -206,7 +197,7 @@ getCompletionsLSP ide plId pure (opts, fmap (,pm,binds) compls, moduleExports, astres) case compls of Just (cci', parsedMod, bindMap) -> do - let pfix = getCompletionPrefix position cnts + let pfix = getCompletionPrefixFromRope position cnts case (pfix, completionContext) of (PosPrefixInfo _ "" _ _, Just CompletionContext { _triggerCharacter = Just "."}) -> return (InL []) @@ -215,7 +206,7 @@ getCompletionsLSP ide plId plugins = idePlugins $ shakeExtras ide config <- liftIO $ runAction "" ide $ getCompletionsConfig plId - allCompletions <- liftIO $ getCompletions plugins ideOpts cci' parsedMod astres bindMap pfix clientCaps config moduleExports uri + let allCompletions = getCompletions plugins ideOpts cci' parsedMod astres bindMap pfix clientCaps config moduleExports uri pure $ InL (orderedCompletions allCompletions) _ -> return (InL []) _ -> return (InL []) diff --git a/ghcide/src/Development/IDE/Plugin/Completions/Logic.hs b/ghcide/src/Development/IDE/Plugin/Completions/Logic.hs index e3935e04e8..a00705ba39 100644 --- a/ghcide/src/Development/IDE/Plugin/Completions/Logic.hs +++ b/ghcide/src/Development/IDE/Plugin/Completions/Logic.hs @@ -2,7 +2,6 @@ {-# LANGUAGE DuplicateRecordFields #-} {-# LANGUAGE GADTs #-} {-# LANGUAGE MultiWayIf #-} -{-# LANGUAGE OverloadedLabels #-} -- Mostly taken from "haskell-ide-engine" module Development.IDE.Plugin.Completions.Logic ( @@ -12,6 +11,7 @@ module Development.IDE.Plugin.Completions.Logic ( , getCompletions , fromIdentInfo , getCompletionPrefix +, getCompletionPrefixFromRope ) where import Control.Applicative @@ -23,7 +23,6 @@ import Data.Generics import Data.List.Extra as List hiding (stripPrefix) import qualified Data.Map as Map -import Data.Row import Prelude hiding (mod) import Data.Maybe (fromMaybe, isJust, @@ -38,14 +37,12 @@ import Data.Aeson (ToJSON (toJSON)) import Data.Function (on) import qualified Data.HashSet as HashSet -import Data.Monoid (First (..)) import Data.Ord (Down (Down)) import qualified Data.Set as Set import Development.IDE.Core.PositionMapping import Development.IDE.GHC.Compat hiding (isQual, ppr) import qualified Development.IDE.GHC.Compat as GHC import Development.IDE.GHC.Compat.Util -import Development.IDE.GHC.CoreFile (occNamePrefixes) import Development.IDE.GHC.Error import Development.IDE.GHC.Util import Development.IDE.Plugin.Completions.Types @@ -56,32 +53,26 @@ import Ide.PluginUtils (mkLspCommand) import Ide.Types (CommandId (..), IdePlugins (..), PluginId) +import Language.Haskell.Syntax.Basic import qualified Language.LSP.Protocol.Lens as L import Language.LSP.Protocol.Types import qualified Language.LSP.VFS as VFS import Text.Fuzzy.Parallel (Scored (score), original) -import qualified Data.Text.Utf16.Rope as Rope +import qualified Data.Text.Utf16.Rope.Mixed as Rope import Development.IDE hiding (line) import Development.IDE.Spans.AtPoint (pointCommand) --- See Note [Guidelines For Using CPP In GHCIDE Import Statements] import GHC.Plugins (Depth (AllTheWay), mkUserStyle, neverQualify, sdocStyle) -#if !MIN_VERSION_ghc(9,3,0) -import GHC.Plugins (defaultSDocContext, - renderWithContext) -#endif +-- See Note [Guidelines For Using CPP In GHCIDE Import Statements] -#if MIN_VERSION_ghc(9,5,0) -import Language.Haskell.Syntax.Basic -#endif -- Chunk size used for parallelizing fuzzy matching chunkSize :: Int @@ -143,42 +134,23 @@ getCContext pos pm | pos `isInsideSrcSpan` r = Just TypeContext goInline _ = Nothing -#if MIN_VERSION_ghc(9,5,0) importGo :: GHC.LImportDecl GhcPs -> Maybe Context importGo (L (locA -> r) impDecl) | pos `isInsideSrcSpan` r = importInline importModuleName (fmap (fmap reLoc) $ ideclImportList impDecl) -#else - importGo :: GHC.LImportDecl GhcPs -> Maybe Context - importGo (L (locA -> r) impDecl) - | pos `isInsideSrcSpan` r - = importInline importModuleName (fmap (fmap reLoc) $ ideclHiding impDecl) -#endif <|> Just (ImportContext importModuleName) | otherwise = Nothing where importModuleName = moduleNameString $ unLoc $ ideclName impDecl -- importInline :: String -> Maybe (Bool, GHC.Located [LIE GhcPs]) -> Maybe Context -#if MIN_VERSION_ghc(9,5,0) importInline modName (Just (EverythingBut, L r _)) | pos `isInsideSrcSpan` r = Just $ ImportHidingContext modName | otherwise = Nothing -#else - importInline modName (Just (True, L r _)) - | pos `isInsideSrcSpan` r = Just $ ImportHidingContext modName - | otherwise = Nothing -#endif -#if MIN_VERSION_ghc(9,5,0) importInline modName (Just (Exactly, L r _)) | pos `isInsideSrcSpan` r = Just $ ImportListContext modName | otherwise = Nothing -#else - importInline modName (Just (False, L r _)) - | pos `isInsideSrcSpan` r = Just $ ImportListContext modName - | otherwise = Nothing -#endif importInline _ _ = Nothing @@ -266,7 +238,7 @@ mkNameCompItem doc thingParent origName provenance isInfix !imp mod = CI {..} compKind = occNameToComKind origName isTypeCompl = isTcOcc origName typeText = Nothing - label = stripPrefix $ printOutputable origName + label = stripOccNamePrefix $ printOutputable origName insertText = case isInfix of Nothing -> label Just LeftSide -> label <> "`" @@ -514,13 +486,8 @@ findRecordCompl uri mn DataDecl {tcdLName, tcdDataDefn} = result -- -- is encoded as @[[arg1, arg2], [arg3], [arg4]]@ -- Hence, we must concat nested arguments into one to get all the fields. -#if MIN_VERSION_ghc(9,3,0) extract ConDeclField{..} = map (foLabel . unLoc) cd_fld_names -#else - extract ConDeclField{..} - = map (rdrNameFieldOcc . unLoc) cd_fld_names -#endif -- XConDeclField extract _ = [] findRecordCompl _ _ _ = [] @@ -530,7 +497,7 @@ toggleSnippets ClientCapabilities {_textDocument} CompletionsConfig{..} = removeSnippetsWhen (not $ enableSnippets && supported) where supported = - Just True == (_textDocument >>= _completion >>= view L.completionItem >>= (\x -> x .! #snippetSupport)) + Just True == (_textDocument >>= _completion >>= view L.completionItem >>= view L.snippetSupport) toggleAutoExtend :: CompletionsConfig -> CompItem -> CompItem toggleAutoExtend CompletionsConfig{enableAutoExtend=False} x = x {additionalTextEdits = Nothing} @@ -559,10 +526,54 @@ getCompletions -> CompletionsConfig -> ModuleNameEnv (HashSet.HashSet IdentInfo) -> Uri - -> IO [Scored CompletionItem] -getCompletions plugins ideOpts CC {allModNamesAsNS, anyQualCompls, unqualCompls, qualCompls, importableModules} - maybe_parsed maybe_ast_res (localBindings, bmapping) prefixInfo caps config moduleExportsMap uri = do - let PosPrefixInfo { fullLine, prefixScope, prefixText } = prefixInfo + -> [Scored CompletionItem] +getCompletions + plugins + ideOpts + CC {allModNamesAsNS, anyQualCompls, unqualCompls, qualCompls, importableModules} + maybe_parsed + maybe_ast_res + (localBindings, bmapping) + prefixInfo@(PosPrefixInfo { fullLine, prefixScope, prefixText }) + caps + config + moduleExportsMap + uri + -- ------------------------------------------------------------------------ + -- IMPORT MODULENAME (NAM|) + | Just (ImportListContext moduleName) <- maybeContext + = moduleImportListCompletions moduleName + + | Just (ImportHidingContext moduleName) <- maybeContext + = moduleImportListCompletions moduleName + + -- ------------------------------------------------------------------------ + -- IMPORT MODULENAM| + | Just (ImportContext _moduleName) <- maybeContext + = filtImportCompls + + -- ------------------------------------------------------------------------ + -- {-# LA| #-} + -- we leave this condition here to avoid duplications and return empty list + -- since HLS implements these completions (#haskell-language-server/pull/662) + | "{-# " `T.isPrefixOf` fullLine + = [] + + -- ------------------------------------------------------------------------ + | otherwise = + -- assumes that nubOrdBy is stable + let uniqueFiltCompls = nubOrdBy (uniqueCompl `on` snd . Fuzzy.original) filtCompls + compls = (fmap.fmap.fmap) (mkCompl pId ideOpts uri) uniqueFiltCompls + pId = lookupCommandProvider plugins (CommandId extendImportCommandId) + in + (fmap.fmap) snd $ + sortBy (compare `on` lexicographicOrdering) $ + mergeListsBy (flip compare `on` score) + [ (fmap.fmap) (notQual,) filtModNameCompls + , (fmap.fmap) (notQual,) filtKeywordCompls + , (fmap.fmap.fmap) (toggleSnippets caps config) compls + ] + where enteredQual = if T.null prefixScope then "" else prefixScope <> "." fullPrefix = enteredQual <> prefixText @@ -585,11 +596,9 @@ getCompletions plugins ideOpts CC {allModNamesAsNS, anyQualCompls, unqualCompls, $ Fuzzy.simpleFilter chunkSize maxC fullPrefix $ (if T.null enteredQual then id else mapMaybe (T.stripPrefix enteredQual)) allModNamesAsNS - - filtCompls = Fuzzy.filter chunkSize maxC prefixText ctxCompls (label . snd) - where - - mcc = case maybe_parsed of + -- If we have a parsed module, use it to determine which completion to show. + maybeContext :: Maybe Context + maybeContext = case maybe_parsed of Nothing -> Nothing Just (pm, pmapping) -> let PositionMapping pDelta = pmapping @@ -598,7 +607,9 @@ getCompletions plugins ideOpts CC {allModNamesAsNS, anyQualCompls, unqualCompls, hpos = upperRange position' in getCContext lpos pm <|> getCContext hpos pm - + filtCompls :: [Scored (Bool, CompItem)] + filtCompls = Fuzzy.filter chunkSize maxC prefixText ctxCompls (label . snd) + where -- We need the hieast to be "fresh". We can't get types from "stale" hie files, so hasfield won't work, -- since it gets the record fields from the types. -- Perhaps this could be fixed with a refactor to GHC's IfaceTyCon, to have it also contain record fields. @@ -636,7 +647,7 @@ getCompletions plugins ideOpts CC {allModNamesAsNS, anyQualCompls, unqualCompls, }) -- completions specific to the current context - ctxCompls' = case mcc of + ctxCompls' = case maybeContext of Nothing -> compls Just TypeContext -> filter ( isTypeCompl . snd) compls Just ValueContext -> filter (not . isTypeCompl . snd) compls @@ -660,7 +671,7 @@ getCompletions plugins ideOpts CC {allModNamesAsNS, anyQualCompls, unqualCompls, pn = showForSnippet name ty = showForSnippet <$> typ thisModName = Local $ nameSrcSpan name - dets = NameDetails <$> (nameModule_maybe name) <*> pure (nameOccName name) + dets = NameDetails <$> nameModule_maybe name <*> pure (nameOccName name) -- When record-dot-syntax completions are available, we return them exclusively. -- They are only available when we write i.e. `myrecord.` with OverloadedRecordDot enabled. @@ -677,54 +688,36 @@ getCompletions plugins ideOpts CC {allModNamesAsNS, anyQualCompls, unqualCompls, , enteredQual `T.isPrefixOf` original label ] + moduleImportListCompletions :: String -> [Scored CompletionItem] + moduleImportListCompletions moduleNameS = + let moduleName = T.pack moduleNameS + funcs = lookupWithDefaultUFM moduleExportsMap HashSet.empty $ mkModuleName moduleNameS + funs = map (show . name) $ HashSet.toList funcs + in filterModuleExports moduleName $ map T.pack funs + + filtImportCompls :: [Scored CompletionItem] filtImportCompls = filtListWith (mkImportCompl enteredQual) importableModules + + filterModuleExports :: T.Text -> [T.Text] -> [Scored CompletionItem] filterModuleExports moduleName = filtListWith $ mkModuleFunctionImport moduleName + + filtKeywordCompls :: [Scored CompletionItem] filtKeywordCompls | T.null prefixScope = filtListWith mkExtCompl (optKeywords ideOpts) | otherwise = [] - if - -- TODO: handle multiline imports - | "import " `T.isPrefixOf` fullLine - && (List.length (words (T.unpack fullLine)) >= 2) - && "(" `isInfixOf` T.unpack fullLine - -> do - let moduleName = words (T.unpack fullLine) !! 1 - funcs = lookupWithDefaultUFM moduleExportsMap HashSet.empty $ mkModuleName moduleName - funs = map (renderOcc . name) $ HashSet.toList funcs - return $ filterModuleExports (T.pack moduleName) funs - | "import " `T.isPrefixOf` fullLine - -> return filtImportCompls - -- we leave this condition here to avoid duplications and return empty list - -- since HLS implements these completions (#haskell-language-server/pull/662) - | "{-# " `T.isPrefixOf` fullLine - -> return [] - | otherwise -> do - -- assumes that nubOrdBy is stable - let uniqueFiltCompls = nubOrdBy (uniqueCompl `on` snd . Fuzzy.original) filtCompls - let compls = (fmap.fmap.fmap) (mkCompl pId ideOpts uri) uniqueFiltCompls - pId = lookupCommandProvider plugins (CommandId extendImportCommandId) - return $ - (fmap.fmap) snd $ - sortBy (compare `on` lexicographicOrdering) $ - mergeListsBy (flip compare `on` score) - [ (fmap.fmap) (notQual,) filtModNameCompls - , (fmap.fmap) (notQual,) filtKeywordCompls - , (fmap.fmap.fmap) (toggleSnippets caps config) compls - ] - where - -- We use this ordering to alphabetically sort suggestions while respecting - -- all the previously applied ordering sources. These are: - -- 1. Qualified suggestions go first - -- 2. Fuzzy score ranks next - -- 3. In-scope completions rank next - -- 4. label alphabetical ordering next - -- 4. detail alphabetical ordering (proxy for module) - lexicographicOrdering Fuzzy.Scored{score, original} = - case original of - (isQual, CompletionItem{_label,_detail}) -> do - let isLocal = maybe False (":" `T.isPrefixOf`) _detail - (Down isQual, Down score, Down isLocal, _label, _detail) + -- We use this ordering to alphabetically sort suggestions while respecting + -- all the previously applied ordering sources. These are: + -- 1. Qualified suggestions go first + -- 2. Fuzzy score ranks next + -- 3. In-scope completions rank next + -- 4. label alphabetical ordering next + -- 4. detail alphabetical ordering (proxy for module) + lexicographicOrdering Fuzzy.Scored{score, original} = + case original of + (isQual, CompletionItem{_label,_detail}) -> do + let isLocal = maybe False (":" `T.isPrefixOf`) _detail + (Down isQual, Down score, Down isLocal, _label, _detail) @@ -736,7 +729,7 @@ uniqueCompl candidate unique = EQ -> -- preserve completions for duplicate record fields where the only difference is in the type -- remove redundant completions with less type info than the previous - if (isLocalCompletion unique) + if isLocalCompletion unique -- filter global completions when we already have a local one || not(isLocalCompletion candidate) && isLocalCompletion unique then EQ @@ -785,17 +778,6 @@ openingBacktick line prefixModule prefixText Position { _character=(fromIntegral -- --------------------------------------------------------------------- --- | Under certain circumstance GHC generates some extra stuff that we --- don't want in the autocompleted symbols - {- When e.g. DuplicateRecordFields is enabled, compiler generates - names like "$sel:accessor:One" and "$sel:accessor:Two" to disambiguate record selectors - https://ghc.haskell.org/trac/ghc/wiki/Records/OverloadedRecordFields/DuplicateRecordFields#Implementation - -} --- TODO: Turn this into an alex lexer that discards prefixes as if they were whitespace. -stripPrefix :: T.Text -> T.Text -stripPrefix name = T.takeWhile (/=':') $ fromMaybe name $ - getFirst $ foldMap (First . (`T.stripPrefix` name)) occNamePrefixes - mkRecordSnippetCompItem :: Uri -> Maybe T.Text -> T.Text -> [T.Text] -> Provenance -> Maybe (LImportDecl GhcPs) -> CompItem mkRecordSnippetCompItem uri parent ctxStr compl importedFrom imp = r where @@ -872,13 +854,16 @@ mergeListsBy cmp all_lists = merge_lists all_lists -- |From the given cursor position, gets the prefix module or record for autocompletion getCompletionPrefix :: Position -> VFS.VirtualFile -> PosPrefixInfo -getCompletionPrefix pos@(Position l c) (VFS.VirtualFile _ _ ropetext) = +getCompletionPrefix pos (VFS.VirtualFile _ _ ropetext) = getCompletionPrefixFromRope pos ropetext + +getCompletionPrefixFromRope :: Position -> Rope.Rope -> PosPrefixInfo +getCompletionPrefixFromRope pos@(Position l c) ropetext = fromMaybe (PosPrefixInfo "" "" "" pos) $ do -- Maybe monad let headMaybe = listToMaybe lastMaybe = headMaybe . reverse -- grab the entire line the cursor is at - curLine <- headMaybe $ T.lines $ Rope.toText + curLine <- headMaybe $ Rope.lines $ fst $ Rope.splitAtLine 1 $ snd $ Rope.splitAtLine (fromIntegral l) ropetext let beforePos = T.take (fromIntegral c) curLine -- the word getting typed, after previous space and before cursor @@ -893,7 +878,9 @@ getCompletionPrefix pos@(Position l c) (VFS.VirtualFile _ _ ropetext) = [] -> Nothing (x:xs) -> do let modParts = reverse $ filter (not .T.null) xs - modName = T.intercalate "." modParts + -- Must check the prefix is a valid module name, else record dot accesses treat + -- the record name as a qualName for search and generated imports + modName = if all (isUpper . T.head) modParts then T.intercalate "." modParts else "" return $ PosPrefixInfo { fullLine = curLine, prefixScope = modName, prefixText = x, cursorPos = pos } completionPrefixPos :: PosPrefixInfo -> Position diff --git a/ghcide/src/Development/IDE/Plugin/Completions/Types.hs b/ghcide/src/Development/IDE/Plugin/Completions/Types.hs index f2b3be0712..338b969bab 100644 --- a/ghcide/src/Development/IDE/Plugin/Completions/Types.hs +++ b/ghcide/src/Development/IDE/Plugin/Completions/Types.hs @@ -16,30 +16,26 @@ import Data.Aeson import Data.Aeson.Types import Data.Hashable (Hashable) import Data.Text (Text) -import Data.Typeable (Typeable) import Development.IDE.GHC.Compat import Development.IDE.Graph (RuleResult) import Development.IDE.Spans.Common () import GHC.Generics (Generic) +import qualified GHC.Types.Name.Occurrence as Occ import Ide.Plugin.Properties import Language.LSP.Protocol.Types (CompletionItemKind (..), Uri) import qualified Language.LSP.Protocol.Types as J --- See Note [Guidelines For Using CPP In GHCIDE Import Statements] - -import qualified GHC.Types.Name.Occurrence as Occ - -- | Produce completions info for a file type instance RuleResult LocalCompletions = CachedCompletions type instance RuleResult NonLocalCompletions = CachedCompletions data LocalCompletions = LocalCompletions - deriving (Eq, Show, Typeable, Generic) + deriving (Eq, Show, Generic) instance Hashable LocalCompletions instance NFData LocalCompletions data NonLocalCompletions = NonLocalCompletions - deriving (Eq, Show, Typeable, Generic) + deriving (Eq, Show, Generic) instance Hashable NonLocalCompletions instance NFData NonLocalCompletions diff --git a/ghcide/src/Development/IDE/Plugin/HLS.hs b/ghcide/src/Development/IDE/Plugin/HLS.hs index 3e58a57ccb..f5190e9274 100644 --- a/ghcide/src/Development/IDE/Plugin/HLS.hs +++ b/ghcide/src/Development/IDE/Plugin/HLS.hs @@ -1,7 +1,6 @@ {-# LANGUAGE DataKinds #-} {-# LANGUAGE GADTs #-} {-# LANGUAGE OverloadedStrings #-} -{-# LANGUAGE PolyKinds #-} module Development.IDE.Plugin.HLS ( @@ -11,7 +10,10 @@ module Development.IDE.Plugin.HLS ) where import Control.Exception (SomeException) +import Control.Lens ((^.)) import Control.Monad +import qualified Control.Monad.Extra as Extra +import Control.Monad.IO.Class (MonadIO) import Control.Monad.Trans.Except (runExceptT) import qualified Data.Aeson as A import Data.Bifunctor (first) @@ -23,6 +25,7 @@ import qualified Data.List as List import Data.List.NonEmpty (NonEmpty, nonEmpty, toList) import qualified Data.List.NonEmpty as NE import qualified Data.Map as Map +import Data.Maybe (isNothing, mapMaybe) import Data.Some import Data.String import Data.Text (Text) @@ -36,8 +39,10 @@ import qualified Development.IDE.Plugin as P import Ide.Logger import Ide.Plugin.Config import Ide.Plugin.Error +import Ide.Plugin.HandleRequestTypes import Ide.PluginUtils (getClientConfig) import Ide.Types as HLS +import qualified Language.LSP.Protocol.Lens as JL import Language.LSP.Protocol.Message import Language.LSP.Protocol.Types import qualified Language.LSP.Server as LSP @@ -53,10 +58,11 @@ import UnliftIO.Exception (catchAny) data Log = LogPluginError PluginId PluginError - | LogResponseError PluginId ResponseError + | forall m . A.ToJSON (ErrorData m) => LogResponseError PluginId (TResponseError m) | LogNoPluginForMethod (Some SMethod) | LogInvalidCommandIdentifier | ExceptionInPlugin PluginId (Some SMethod) SomeException + | LogResolveDefaultHandler (Some SMethod) instance Pretty Log where pretty = \case @@ -65,23 +71,31 @@ instance Pretty Log where LogResponseError (PluginId pId) err -> pretty pId <> ":" <+> pretty err LogNoPluginForMethod (Some method) -> - "No plugin enabled for " <> pretty method + "No plugin handles this " <> pretty method <> " request." LogInvalidCommandIdentifier-> "Invalid command identifier" ExceptionInPlugin plId (Some method) exception -> "Exception in plugin " <> viaShow plId <> " while processing " <> pretty method <> ": " <> viaShow exception + LogResolveDefaultHandler (Some method) -> + "No plugin can handle" <+> pretty method <+> "request. Return object unchanged." instance Show Log where show = renderString . layoutCompact . pretty -noPluginEnabled :: Recorder (WithPriority Log) -> SMethod m -> [PluginId] -> IO (Either ResponseError c) -noPluginEnabled recorder m fs' = do +noPluginHandles :: Recorder (WithPriority Log) -> SMethod m -> [(PluginId, HandleRequestResult)] -> IO (Either (TResponseError m) c) +noPluginHandles recorder m fs' = do logWith recorder Warning (LogNoPluginForMethod $ Some m) - let err = ResponseError (InR ErrorCodes_MethodNotFound) msg Nothing - msg = pluginNotEnabled m fs' + let err = TResponseError (InR ErrorCodes_MethodNotFound) msg Nothing + msg = noPluginHandlesMsg m fs' return $ Left err - where pluginNotEnabled :: SMethod m -> [PluginId] -> Text - pluginNotEnabled method availPlugins = - "No plugin enabled for " <> T.pack (show method) <> ", potentially available: " - <> (T.intercalate ", " $ map (\(PluginId plid) -> plid) availPlugins) + where noPluginHandlesMsg :: SMethod m -> [(PluginId, HandleRequestResult)] -> Text + noPluginHandlesMsg method [] = "No plugins are available to handle this " <> T.pack (show method) <> " request." + noPluginHandlesMsg method availPlugins = + "No plugins are available to handle this " <> T.pack (show method) <> " request.\n Plugins installed for this method, but not available to handle this request are:\n" + <> (T.intercalate "\n" $ + map (\(PluginId plid, pluginStatus) -> + plid + <> " " + <> (renderStrict . layoutCompact . pretty) pluginStatus) + availPlugins) pluginDoesntExist :: PluginId -> Text pluginDoesntExist (PluginId pid) = "Plugin " <> pid <> " doesn't exist" @@ -105,9 +119,9 @@ exceptionInPlugin plId method exception = "Exception in plugin " <> T.pack (show plId) <> " while processing "<> T.pack (show method) <> ": " <> T.pack (show exception) -- | Build a ResponseError and log it before returning to the caller -logAndReturnError :: Recorder (WithPriority Log) -> PluginId -> (LSPErrorCodes |? ErrorCodes) -> Text -> LSP.LspT Config IO (Either ResponseError a) +logAndReturnError :: A.ToJSON (ErrorData m) => Recorder (WithPriority Log) -> PluginId -> (LSPErrorCodes |? ErrorCodes) -> Text -> LSP.LspT Config IO (Either (TResponseError m) a) logAndReturnError recorder p errCode msg = do - let err = ResponseError errCode msg Nothing + let err = TResponseError errCode msg Nothing logWith recorder Warning $ LogResponseError p err pure $ Left err @@ -169,8 +183,8 @@ executeCommandHandlers recorder ecs = requestHandler SMethod_WorkspaceExecuteCom _ -> Nothing -- The parameters to the HLS command are always the first element - execCmd :: IdeState -> ExecuteCommandParams -> LSP.LspT Config IO (Either ResponseError (A.Value |? Null)) - execCmd ide (ExecuteCommandParams _ cmdId args) = do + execCmd :: IdeState -> ExecuteCommandParams -> LSP.LspT Config IO (Either (TResponseError Method_WorkspaceExecuteCommand) (A.Value |? Null)) + execCmd ide (ExecuteCommandParams mtoken cmdId args) = do let cmdParams :: A.Value cmdParams = case args of Just ((x:_)) -> x @@ -189,20 +203,22 @@ executeCommandHandlers recorder ecs = requestHandler SMethod_WorkspaceExecuteCom -- If we have a command, continue to execute it Just (Command _ innerCmdId innerArgs) -> execCmd ide (ExecuteCommandParams Nothing innerCmdId innerArgs) + -- TODO: This should be a response error? Nothing -> return $ Right $ InR Null + -- TODO: This should be a response error? A.Error _str -> return $ Right $ InR Null -- Just an ordinary HIE command - Just (plugin, cmd) -> runPluginCommand ide plugin cmd cmdParams + Just (plugin, cmd) -> runPluginCommand ide plugin cmd mtoken cmdParams -- Couldn't parse the command identifier _ -> do logWith recorder Warning LogInvalidCommandIdentifier - return $ Left $ ResponseError (InR ErrorCodes_InvalidParams) "Invalid command identifier" Nothing + return $ Left $ TResponseError (InR ErrorCodes_InvalidParams) "Invalid command identifier" Nothing - runPluginCommand :: IdeState -> PluginId -> CommandId -> A.Value -> LSP.LspT Config IO (Either ResponseError (A.Value |? Null)) - runPluginCommand ide p com arg = + runPluginCommand :: IdeState -> PluginId -> CommandId -> Maybe ProgressToken -> A.Value -> LSP.LspT Config IO (Either (TResponseError Method_WorkspaceExecuteCommand) (A.Value |? Null)) + runPluginCommand ide p com mtoken arg = case Map.lookup p pluginMap of Nothing -> logAndReturnError recorder p (InR ErrorCodes_InvalidRequest) (pluginDoesntExist p) Just xs -> case List.find ((com ==) . commandId) xs of @@ -210,11 +226,11 @@ executeCommandHandlers recorder ecs = requestHandler SMethod_WorkspaceExecuteCom Just (PluginCommand _ _ f) -> case A.fromJSON arg of A.Error err -> logAndReturnError recorder p (InR ErrorCodes_InvalidParams) (failedToParseArgs com p err arg) A.Success a -> do - res <- runExceptT (f ide a) `catchAny` -- See Note [Exception handling in plugins] + res <- runHandlerM (runExceptT (f ide mtoken a)) `catchAny` -- See Note [Exception handling in plugins] (\e -> pure $ Left $ PluginInternalError (exceptionInPlugin p SMethod_WorkspaceExecuteCommand e)) case res of - (Left (PluginRequestRefused _)) -> - liftIO $ noPluginEnabled recorder SMethod_WorkspaceExecuteCommand (fst <$> ecs) + (Left (PluginRequestRefused r)) -> + liftIO $ noPluginHandles recorder SMethod_WorkspaceExecuteCommand [(p,DoesNotHandleRequest r)] (Left pluginErr) -> do liftIO $ logErrors recorder [(p, pluginErr)] pure $ Left $ toResponseError (p, pluginErr) @@ -236,14 +252,24 @@ extensiblePlugins recorder plugins = mempty { P.pluginHandlers = handlers } (IdeMethod m :=> IdeHandler fs') <- DMap.assocs handlers' pure $ requestHandler m $ \ide params -> do config <- Ide.PluginUtils.getClientConfig - -- Only run plugins that are allowed to run on this request - let fs = filter (\(_, desc, _) -> pluginEnabled m params desc config) fs' + -- Only run plugins that are allowed to run on this request, save the + -- list of disabled plugins incase that's all we have + let (fs, dfs) = List.partition (\(_, desc, _) -> handlesRequest m params desc config == HandlesRequest) fs' + let disabledPluginsReason = (\(x, desc, _) -> (x, handlesRequest m params desc config)) <$> dfs -- Clients generally don't display ResponseErrors so instead we log any that we come across + -- However, some clients do display ResponseErrors! See for example the issues: + -- https://github.com/haskell/haskell-language-server/issues/4467 + -- https://github.com/haskell/haskell-language-server/issues/4451 case nonEmpty fs of - Nothing -> liftIO $ noPluginEnabled recorder m ((\(x, _, _) -> x) <$> fs') + Nothing -> do + liftIO (fallbackResolveHandler recorder m params) >>= \case + Nothing -> + liftIO $ noPluginHandles recorder m disabledPluginsReason + Just result -> + pure $ Right result Just neFs -> do let plidsAndHandlers = fmap (\(plid,_,handler) -> (plid,handler)) neFs - es <- runConcurrently exceptionInPlugin m plidsAndHandlers ide params + es <- runHandlerM $ runConcurrently exceptionInPlugin m plidsAndHandlers ide params caps <- LSP.getClientCapabilities let (errs,succs) = partitionEithers $ toList $ join $ NE.zipWith (\(pId,_) -> fmap (first (pId,))) plidsAndHandlers es liftIO $ unless (null errs) $ logErrors recorder errs @@ -251,13 +277,82 @@ extensiblePlugins recorder plugins = mempty { P.pluginHandlers = handlers } Nothing -> do let noRefused (_, PluginRequestRefused _) = False noRefused (_, _) = True - filteredErrs = filter noRefused errs - case nonEmpty filteredErrs of - Nothing -> liftIO $ noPluginEnabled recorder m ((\(x, _, _) -> x) <$> fs') + (asErrors, asRefused) = List.partition noRefused errs + convertPRR (pId, PluginRequestRefused r) = Just (pId, DoesNotHandleRequest r) + convertPRR _ = Nothing + asRefusedReason = mapMaybe convertPRR asRefused + case nonEmpty asErrors of + Nothing -> liftIO $ noPluginHandles recorder m (disabledPluginsReason <> asRefusedReason) Just xs -> pure $ Left $ combineErrors xs Just xs -> do pure $ Right $ combineResponses m config caps params xs +-- | Fallback Handler for resolve requests. +-- For all kinds of `*/resolve` requests, if they don't have a 'data_' value, +-- produce the original item, since no other plugin has any resolve data. +-- +-- This is an internal handler, so it cannot be turned off and should be opaque +-- to the end-user. +-- This function does not take the ServerCapabilities into account, and assumes +-- clients will only send these requests, if and only if the Language Server +-- advertised support for it. +-- +-- See Note [Fallback Handler for LSP resolve requests] for justification and reasoning. +fallbackResolveHandler :: MonadIO m => Recorder (WithPriority Log) -> SMethod s -> MessageParams s -> m (Maybe (MessageResult s)) +fallbackResolveHandler recorder m params = do + let result = case m of + SMethod_InlayHintResolve + | noResolveData params -> Just params + SMethod_CompletionItemResolve + | noResolveData params -> Just params + SMethod_CodeActionResolve + | noResolveData params -> Just params + SMethod_WorkspaceSymbolResolve + | noResolveData params -> Just params + SMethod_CodeLensResolve + | noResolveData params -> Just params + SMethod_DocumentLinkResolve + | noResolveData params -> Just params + _ -> Nothing + logResolveHandling result + pure result + where + noResolveData :: JL.HasData_ p (Maybe a) => p -> Bool + noResolveData p = isNothing $ p ^. JL.data_ + + -- We only log if we are handling the request. + -- If we don't handle this request, this should be logged + -- on call-site. + logResolveHandling p = Extra.whenJust p $ \_ -> do + logWith recorder Debug $ LogResolveDefaultHandler (Some m) + +{- Note [Fallback Handler for LSP resolve requests] +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +We have a special fallback for `*/resolve` requests. + +We had multiple reports, where `resolve` requests (such as +`completion/resolve` and `codeAction/resolve`) are rejected +by HLS since the `_data_` field of the respective LSP feature has not been +populated by HLS. +This makes sense, as we only support `resolve` for certain kinds of +`CodeAction`/`Completions`, when they contain particularly expensive +properties, such as documentation or non-local type signatures. + +So what to do? We can see two options: + +1. Be dumb and permissive: if no plugin wants to resolve a request, then + just respond positively with the original item! Potentially this masks + real issues, but may not be too bad. If a plugin thinks it can + handle the request but it then fails to resolve it, we should still return a failure. +2. Try and be smart: we try to figure out requests that we're "supposed" to + resolve (e.g. those with a data field), and fail if no plugin wants to handle those. + This is possible since we set data. + So as long as we maintain the invariant that only things which need resolving get + data, then it could be okay. + +In 'fallbackResolveHandler', we implement the option (2). +-} -- --------------------------------------------------------------------- @@ -274,8 +369,8 @@ extensibleNotificationPlugins recorder xs = mempty { P.pluginHandlers = handlers (IdeNotification m :=> IdeNotificationHandler fs') <- DMap.assocs handlers' pure $ notificationHandler m $ \ide vfs params -> do config <- Ide.PluginUtils.getClientConfig - -- Only run plugins that are allowed to run on this request - let fs = filter (\(_, desc, _) -> pluginEnabled m params desc config) fs' + -- Only run plugins that are enabled for this request + let fs = filter (\(_, desc, _) -> handlesRequest m params desc config == HandlesRequest) fs' case nonEmpty fs of Nothing -> do logWith recorder Warning (LogNoPluginForMethod $ Some m) @@ -302,13 +397,13 @@ runConcurrently msg method fs a b = forConcurrently fs $ \(pid,f) -> otTracedPro f a b -- See Note [Exception handling in plugins] `catchAny` (\e -> pure $ pure $ Left $ PluginInternalError (msg pid method e)) -combineErrors :: NonEmpty (PluginId, PluginError) -> ResponseError +combineErrors :: NonEmpty (PluginId, PluginError) -> TResponseError m combineErrors (x NE.:| []) = toResponseError x combineErrors xs = toResponseError $ NE.last $ NE.sortWith (toPriority . snd) xs -toResponseError :: (PluginId, PluginError) -> ResponseError +toResponseError :: (PluginId, PluginError) -> TResponseError m toResponseError (PluginId plId, err) = - ResponseError (toErrorCode err) (plId <> ": " <> tPretty err) Nothing + TResponseError (toErrorCode err) (plId <> ": " <> tPretty err) Nothing where tPretty = T.pack . show . pretty logErrors :: Recorder (WithPriority Log) -> [(PluginId, PluginError)] -> IO () @@ -321,7 +416,7 @@ logErrors recorder errs = do -- | Combine the 'PluginHandler' for all plugins newtype IdeHandler (m :: Method ClientToServer Request) - = IdeHandler [(PluginId, PluginDescriptor IdeState, IdeState -> MessageParams m -> LSP.LspM Config (NonEmpty (Either PluginError (MessageResult m))))] + = IdeHandler [(PluginId, PluginDescriptor IdeState, IdeState -> MessageParams m -> HandlerM Config (NonEmpty (Either PluginError (MessageResult m))))] -- | Combine the 'PluginHandler' for all plugins newtype IdeNotificationHandler (m :: Method ClientToServer Notification) @@ -347,6 +442,7 @@ instance Monoid IdeNotificationHandlers where mempty = IdeNotificationHandlers mempty {- Note [Exception handling in plugins] + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Plugins run in LspM, and so have access to IO. This means they are likely to throw exceptions, even if only by accident or through calling libraries that throw exceptions. Ultimately, we're running a bunch of less-trusted IO code, diff --git a/ghcide/src/Development/IDE/Plugin/HLS/GhcIde.hs b/ghcide/src/Development/IDE/Plugin/HLS/GhcIde.hs index f85f0c8522..ada0f9e682 100644 --- a/ghcide/src/Development/IDE/Plugin/HLS/GhcIde.hs +++ b/ghcide/src/Development/IDE/Plugin/HLS/GhcIde.hs @@ -7,9 +7,9 @@ module Development.IDE.Plugin.HLS.GhcIde descriptors , Log(..) ) where -import Control.Monad.IO.Class + import Development.IDE -import Development.IDE.LSP.HoverDefinition +import qualified Development.IDE.LSP.HoverDefinition as Hover import qualified Development.IDE.LSP.Notifications as Notifications import Development.IDE.LSP.Outline import qualified Development.IDE.Plugin.Completions as Completions @@ -23,6 +23,7 @@ data Log = LogNotifications Notifications.Log | LogCompletions Completions.Log | LogTypeLenses TypeLenses.Log + | LogHover Hover.Log deriving Show instance Pretty Log where @@ -30,10 +31,11 @@ instance Pretty Log where LogNotifications msg -> pretty msg LogCompletions msg -> pretty msg LogTypeLenses msg -> pretty msg + LogHover msg -> pretty msg descriptors :: Recorder (WithPriority Log) -> [PluginDescriptor IdeState] descriptors recorder = - [ descriptor "ghcide-hover-and-symbols", + [ descriptor (cmapWithPrio LogHover recorder) "ghcide-hover-and-symbols", Completions.descriptor (cmapWithPrio LogCompletions recorder) "ghcide-completions", TypeLenses.descriptor (cmapWithPrio LogTypeLenses recorder) "ghcide-type-lenses", Notifications.descriptor (cmapWithPrio LogNotifications recorder) "ghcide-core" @@ -41,25 +43,28 @@ descriptors recorder = -- --------------------------------------------------------------------- -descriptor :: PluginId -> PluginDescriptor IdeState -descriptor plId = (defaultPluginDescriptor plId) - { pluginHandlers = mkPluginHandler SMethod_TextDocumentHover hover' +descriptor :: Recorder (WithPriority Hover.Log) -> PluginId -> PluginDescriptor IdeState +descriptor recorder plId = (defaultPluginDescriptor plId desc) + { pluginHandlers = mkPluginHandler SMethod_TextDocumentHover (hover' recorder) <> mkPluginHandler SMethod_TextDocumentDocumentSymbol moduleOutline <> mkPluginHandler SMethod_TextDocumentDefinition (\ide _ DefinitionParams{..} -> - gotoDefinition ide TextDocumentPositionParams{..}) + Hover.gotoDefinition recorder ide TextDocumentPositionParams{..}) <> mkPluginHandler SMethod_TextDocumentTypeDefinition (\ide _ TypeDefinitionParams{..} -> - gotoTypeDefinition ide TextDocumentPositionParams{..}) + Hover.gotoTypeDefinition recorder ide TextDocumentPositionParams{..}) + <> mkPluginHandler SMethod_TextDocumentImplementation (\ide _ ImplementationParams{..} -> + Hover.gotoImplementation recorder ide TextDocumentPositionParams{..}) <> mkPluginHandler SMethod_TextDocumentDocumentHighlight (\ide _ DocumentHighlightParams{..} -> - documentHighlight ide TextDocumentPositionParams{..}) - <> mkPluginHandler SMethod_TextDocumentReferences references - <> mkPluginHandler SMethod_WorkspaceSymbol wsSymbols, + Hover.documentHighlight recorder ide TextDocumentPositionParams{..}) + <> mkPluginHandler SMethod_TextDocumentReferences (Hover.references recorder) + <> mkPluginHandler SMethod_WorkspaceSymbol (Hover.wsSymbols recorder), pluginConfigDescriptor = defaultConfigDescriptor } + where + desc = "Provides core IDE features for Haskell" -- --------------------------------------------------------------------- -hover' :: PluginMethodHandler IdeState Method_TextDocumentHover -hover' ideState _ HoverParams{..} = do - liftIO $ logDebug (ideLogger ideState) "GhcIde.hover entered (ideLogger)" -- AZ - hover ideState TextDocumentPositionParams{..} +hover' :: Recorder (WithPriority Hover.Log) -> PluginMethodHandler IdeState Method_TextDocumentHover +hover' recorder ideState _ HoverParams{..} = + Hover.hover recorder ideState TextDocumentPositionParams{..} diff --git a/ghcide/src/Development/IDE/Plugin/Test.hs b/ghcide/src/Development/IDE/Plugin/Test.hs index 72a1d5b912..e24bcfeee9 100644 --- a/ghcide/src/Development/IDE/Plugin/Test.hs +++ b/ghcide/src/Development/IDE/Plugin/Test.hs @@ -2,7 +2,6 @@ {-# LANGUAGE DerivingStrategies #-} {-# LANGUAGE GADTs #-} {-# LANGUAGE PackageImports #-} -{-# LANGUAGE PolyKinds #-} -- | A plugin that adds custom messages for use in tests module Development.IDE.Plugin.Test ( TestRequest(..) @@ -50,7 +49,6 @@ import Ide.Plugin.Error import Ide.Types import Language.LSP.Protocol.Message import Language.LSP.Protocol.Types -import qualified Language.LSP.Server as LSP import qualified "list-t" ListT import qualified StmContainers.Map as STM import System.Time.Extra @@ -77,7 +75,7 @@ newtype WaitForIdeRuleResult = WaitForIdeRuleResult { ideResultSuccess::Bool} deriving newtype (FromJSON, ToJSON) plugin :: PluginDescriptor IdeState -plugin = (defaultPluginDescriptor "test") { +plugin = (defaultPluginDescriptor "test" "") { pluginHandlers = mkPluginHandler (SMethod_CustomMethod (Proxy @"test")) $ \st _ -> testRequestHandler' st } @@ -92,9 +90,9 @@ plugin = (defaultPluginDescriptor "test") { testRequestHandler :: IdeState -> TestRequest - -> LSP.LspM c (Either PluginError Value) + -> HandlerM config (Either PluginError Value) testRequestHandler _ (BlockSeconds secs) = do - LSP.sendNotification (SMethod_CustomMethod (Proxy @"ghcide/blocking/request")) $ + pluginSendNotification (SMethod_CustomMethod (Proxy @"ghcide/blocking/request")) $ toJSON secs liftIO $ sleep secs return (Right A.Null) @@ -166,12 +164,12 @@ blockCommandId :: Text blockCommandId = "ghcide.command.block" blockCommandDescriptor :: PluginId -> PluginDescriptor state -blockCommandDescriptor plId = (defaultPluginDescriptor plId) { +blockCommandDescriptor plId = (defaultPluginDescriptor plId "") { pluginCommands = [PluginCommand (CommandId blockCommandId) "blocks forever" blockCommandHandler] } blockCommandHandler :: CommandFunction state ExecuteCommandParams -blockCommandHandler _ideState _params = do - lift $ LSP.sendNotification (SMethod_CustomMethod (Proxy @"ghcide/blocking/command")) A.Null +blockCommandHandler _ideState _ _params = do + lift $ pluginSendNotification (SMethod_CustomMethod (Proxy @"ghcide/blocking/command")) A.Null liftIO $ threadDelay maxBound pure $ InR Null diff --git a/ghcide/src/Development/IDE/Plugin/TypeLenses.hs b/ghcide/src/Development/IDE/Plugin/TypeLenses.hs index 347f7622a3..c596d1fb82 100644 --- a/ghcide/src/Development/IDE/Plugin/TypeLenses.hs +++ b/ghcide/src/Development/IDE/Plugin/TypeLenses.hs @@ -1,3 +1,4 @@ +{-# LANGUAGE CPP #-} {-# LANGUAGE DeriveAnyClass #-} {-# LANGUAGE OverloadedLabels #-} {-# LANGUAGE TypeFamilies #-} @@ -15,7 +16,7 @@ module Development.IDE.Plugin.TypeLenses ( import Control.Concurrent.STM.Stats (atomically) import Control.DeepSeq (rwhnf) -import Control.Lens ((?~)) +import Control.Lens (to, (?~), (^?)) import Control.Monad (mzero) import Control.Monad.Extra (whenMaybe) import Control.Monad.IO.Class (MonadIO (liftIO)) @@ -24,12 +25,17 @@ import Data.Aeson.Types (toJSON) import qualified Data.Aeson.Types as A import Data.List (find) import qualified Data.Map as Map -import Data.Maybe (catMaybes, maybeToList) +import Data.Maybe (catMaybes, isJust, + maybeToList) import qualified Data.Text as T -import Development.IDE (GhcSession (..), +import Development.IDE (FileDiagnostic (..), + GhcSession (..), HscEnvEq (hscEnv), RuleResult, Rules, Uri, - define, srcSpanToRange, + _SomeStructuredMessage, + define, + fdStructuredMessageL, + srcSpanToRange, usePropertyAction) import Development.IDE.Core.Compile (TcModuleResult (..)) import Development.IDE.Core.PluginUtils @@ -43,6 +49,10 @@ import Development.IDE.Core.Shake (getHiddenDiagnostics, use) import qualified Development.IDE.Core.Shake as Shake import Development.IDE.GHC.Compat +import Development.IDE.GHC.Compat.Error (_TcRnMessage, + _TcRnMissingSignature, + msgEnvelopeErrorL, + stripTcRnMessageContext) import Development.IDE.GHC.Util (printName) import Development.IDE.Graph.Classes import Development.IDE.Types.Location (Position (Position, _line), @@ -66,7 +76,8 @@ import Ide.Types (CommandFunction, defaultPluginDescriptor, mkCustomConfig, mkPluginHandler, - mkResolveHandler) + mkResolveHandler, + pluginSendRequest) import qualified Language.LSP.Protocol.Lens as L import Language.LSP.Protocol.Message (Method (Method_CodeLensResolve, Method_TextDocumentCodeLens), SMethod (..)) @@ -79,7 +90,6 @@ import Language.LSP.Protocol.Types (ApplyWorkspaceEditParams TextEdit (TextEdit), WorkspaceEdit (WorkspaceEdit), type (|?) (..)) -import qualified Language.LSP.Server as LSP import Text.Regex.TDFA ((=~)) data Log = LogShake Shake.Log deriving Show @@ -94,13 +104,15 @@ typeLensCommandId = "typesignature.add" descriptor :: Recorder (WithPriority Log) -> PluginId -> PluginDescriptor IdeState descriptor recorder plId = - (defaultPluginDescriptor plId) + (defaultPluginDescriptor plId desc) { pluginHandlers = mkPluginHandler SMethod_TextDocumentCodeLens codeLensProvider <> mkResolveHandler SMethod_CodeLensResolve codeLensResolveProvider , pluginCommands = [PluginCommand (CommandId typeLensCommandId) "adds a signature" commandHandler] , pluginRules = rules recorder , pluginConfigDescriptor = defaultConfigDescriptor {configCustomConfig = mkCustomConfig properties} } + where + desc = "Provides code lenses type signatures" properties :: Properties '[ 'PropertyKey "mode" (TEnum Mode)] properties = emptyProperties @@ -124,8 +136,9 @@ codeLensProvider ideState pId CodeLensParams{_textDocument = TextDocumentIdentif -- We don't actually pass any data to resolve, however we need this -- dummy type to make sure HLS resolves our lens [ CodeLens _range Nothing (Just $ toJSON TypeLensesResolve) - | (dFile, _, diag@Diagnostic{_range}) <- diags - , dFile == nfp + | diag <- diags + , let Diagnostic {_range} = fdLspDiagnostic diag + , fdFilePath diag == nfp , isGlobalDiagnostic diag] -- The second option is to generate lenses from the GlobalBindingTypeSig -- rule. This is the only type that needs to have the range adjusted @@ -181,7 +194,7 @@ codeLensResolveProvider ideState pId lens@CodeLens{_range} uri TypeLensesResolve generateLensCommand :: PluginId -> Uri -> T.Text -> TextEdit -> Command generateLensCommand pId uri title edit = - let wEdit = WorkspaceEdit (Just $ Map.singleton uri $ [edit]) Nothing Nothing + let wEdit = WorkspaceEdit (Just $ Map.singleton uri [edit]) Nothing Nothing in mkLspCommand pId (CommandId typeLensCommandId) title (Just [toJSON wEdit]) -- Since the lenses are created with diagnostics, and since the globalTypeSig @@ -190,12 +203,12 @@ generateLensCommand pId uri title edit = -- recompute the edit upon command. Hence the command here just takes a edit -- and applies it. commandHandler :: CommandFunction IdeState WorkspaceEdit -commandHandler _ideState wedit = do - _ <- lift $ LSP.sendRequest SMethod_WorkspaceApplyEdit (ApplyWorkspaceEditParams Nothing wedit) (\_ -> pure ()) +commandHandler _ideState _ wedit = do + _ <- lift $ pluginSendRequest SMethod_WorkspaceApplyEdit (ApplyWorkspaceEditParams Nothing wedit) (\_ -> pure ()) pure $ InR Null -------------------------------------------------------------------------------- -suggestSignature :: Bool -> Maybe GlobalBindingTypeSigsResult -> Diagnostic -> [(T.Text, TextEdit)] +suggestSignature :: Bool -> Maybe GlobalBindingTypeSigsResult -> FileDiagnostic -> [(T.Text, TextEdit)] suggestSignature isQuickFix mGblSigs diag = maybeToList (suggestGlobalSignature isQuickFix mGblSigs diag) @@ -203,14 +216,19 @@ suggestSignature isQuickFix mGblSigs diag = -- works with a diagnostic, which then calls the secondary function with -- whatever pieces of the diagnostic it needs. This allows the resolve function, -- which no longer has the Diagnostic, to still call the secondary functions. -suggestGlobalSignature :: Bool -> Maybe GlobalBindingTypeSigsResult -> Diagnostic -> Maybe (T.Text, TextEdit) -suggestGlobalSignature isQuickFix mGblSigs diag@Diagnostic{_range} +suggestGlobalSignature :: Bool -> Maybe GlobalBindingTypeSigsResult -> FileDiagnostic -> Maybe (T.Text, TextEdit) +suggestGlobalSignature isQuickFix mGblSigs diag@FileDiagnostic {fdLspDiagnostic = Diagnostic {_range}} | isGlobalDiagnostic diag = suggestGlobalSignature' isQuickFix mGblSigs Nothing _range | otherwise = Nothing -isGlobalDiagnostic :: Diagnostic -> Bool -isGlobalDiagnostic Diagnostic{_message} = _message =~ ("(Top-level binding|Pattern synonym) with no type signature" :: T.Text) +isGlobalDiagnostic :: FileDiagnostic -> Bool +isGlobalDiagnostic diag = diag ^? fdStructuredMessageL + . _SomeStructuredMessage + . msgEnvelopeErrorL + . _TcRnMessage + . _TcRnMissingSignature + & isJust -- If a PositionMapping is supplied, this function will call -- gblBindingTypeSigToEdit with it to create a TextEdit in the right location. @@ -317,7 +335,11 @@ gblBindingType (Just hsc) (Just gblEnv) = do let name = idName identifier hasSig name $ do env <- tcInitTidyEnv +#if MIN_VERSION_ghc(9,11,0) + let ty = tidyOpenType env (idType identifier) +#else let (_, ty) = tidyOpenType env (idType identifier) +#endif pure $ GlobalBindingTypeSig name (printName name <> " :: " <> showDoc (pprSigmaType ty)) (name `elemNameSet` exports) patToSig p = do let name = patSynName p diff --git a/ghcide/src/Development/IDE/Spans/AtPoint.hs b/ghcide/src/Development/IDE/Spans/AtPoint.hs index 5f1c68b83b..a577cae32e 100644 --- a/ghcide/src/Development/IDE/Spans/AtPoint.hs +++ b/ghcide/src/Development/IDE/Spans/AtPoint.hs @@ -1,10 +1,8 @@ -- Copyright (c) 2019 The DAML Authors. All rights reserved. -- SPDX-License-Identifier: Apache-2.0 -{-# LANGUAGE CPP #-} -{-# LANGUAGE GADTs #-} -{-# LANGUAGE RankNTypes #-} -{-# LANGUAGE ScopedTypeVariables #-} +{-# LANGUAGE CPP #-} +{-# LANGUAGE GADTs #-} -- | Gives information about symbols at a given point in DAML files. -- These are all pure functions that should execute quickly. @@ -12,6 +10,7 @@ module Development.IDE.Spans.AtPoint ( atPoint , gotoDefinition , gotoTypeDefinition + , gotoImplementation , documentHighlight , pointCommand , referencesAtPoint @@ -25,6 +24,10 @@ module Development.IDE.Spans.AtPoint ( , LookupModule ) where + +import GHC.Data.FastString (lengthFS) +import qualified GHC.Utils.Outputable as O + import Development.IDE.GHC.Error import Development.IDE.GHC.Orphans () import Development.IDE.Types.Location @@ -33,6 +36,7 @@ import Language.LSP.Protocol.Types hiding import Prelude hiding (mod) -- compiler and infrastructure +import Development.IDE.Core.Compile (setNonHomeFCHook) import Development.IDE.Core.PositionMapping import Development.IDE.Core.RuleTypes import Development.IDE.GHC.Compat @@ -54,9 +58,13 @@ import qualified Data.Text as T import qualified Data.Array as A import Data.Either -import Data.List (isSuffixOf) import Data.List.Extra (dropEnd1, nubOrd) + +import Data.Either.Extra (eitherToMaybe) +import Data.List (isSuffixOf, sortOn) +import Data.Tree +import qualified Data.Tree as T import Data.Version (showVersion) import Development.IDE.Types.Shake (WithHieDb) import HieDb hiding (pointCommand, @@ -97,10 +105,10 @@ foiReferencesAtPoint file pos (FOIReferences asts) = adjustedLocs = HM.foldr go [] asts go (HAR _ _ rf tr _, goMapping) xs = refs ++ typerefs ++ xs where - refs = mapMaybe (toCurrentLocation goMapping . realSrcSpanToLocation . fst) - $ concat $ mapMaybe (\n -> M.lookup (Right n) rf) names - typerefs = mapMaybe (toCurrentLocation goMapping . realSrcSpanToLocation) - $ concat $ mapMaybe (`M.lookup` tr) names + refs = concatMap (mapMaybe (toCurrentLocation goMapping . realSrcSpanToLocation . fst)) + (mapMaybe (\n -> M.lookup (Right n) rf) names) + typerefs = concatMap (mapMaybe (toCurrentLocation goMapping . realSrcSpanToLocation)) + (mapMaybe (`M.lookup` tr) names) in (names, adjustedLocs,map fromNormalizedFilePath $ HM.keys asts) getNamesAtPoint :: HieASTs a -> Position -> PositionMapping -> [Name] @@ -169,18 +177,23 @@ documentHighlight hf rf pos = pure highlights where -- We don't want to show document highlights for evidence variables, which are supposed to be invisible notEvidence = not . any isEvidenceContext . identInfo - ns = concat $ pointCommand hf pos (rights . M.keys . M.filter notEvidence . getNodeIds) + ns = concat $ pointCommand hf pos (rights . M.keys . M.filter notEvidence . getSourceNodeIds) highlights = do n <- ns ref <- fromMaybe [] (M.lookup (Right n) rf) - pure $ makeHighlight ref - makeHighlight (sp,dets) = - DocumentHighlight (realSrcSpanToRange sp) (Just $ highlightType $ identInfo dets) + maybeToList (makeHighlight n ref) + makeHighlight n (sp,dets) + | isTvNameSpace (nameNameSpace n) && isBadSpan n sp = Nothing + | otherwise = Just $ DocumentHighlight (realSrcSpanToRange sp) (Just $ highlightType $ identInfo dets) highlightType s = if any (isJust . getScopeFromContext) s then DocumentHighlightKind_Write else DocumentHighlightKind_Read + isBadSpan :: Name -> RealSrcSpan -> Bool + isBadSpan n sp = srcSpanStartLine sp /= srcSpanEndLine sp || (srcSpanEndCol sp - srcSpanStartCol sp > lengthFS (occNameFS $ nameOccName n)) + +-- | Locate the type definition of the name at a given position. gotoTypeDefinition :: MonadIO m => WithHieDb @@ -188,7 +201,7 @@ gotoTypeDefinition -> IdeOptions -> HieAstResult -> Position - -> MaybeT m [Location] + -> MaybeT m [(Location, Identifier)] gotoTypeDefinition withHieDb lookupModule ideOpts srcSpans pos = lift $ typeLocationsAtPoint withHieDb lookupModule ideOpts pos srcSpans @@ -199,27 +212,40 @@ gotoDefinition -> LookupModule m -> IdeOptions -> M.Map ModuleName NormalizedFilePath - -> HieASTs a + -> HieAstResult -> Position - -> MaybeT m [Location] + -> MaybeT m [(Location, Identifier)] gotoDefinition withHieDb getHieFile ideOpts imports srcSpans pos = lift $ locationsAtPoint withHieDb getHieFile ideOpts imports pos srcSpans +-- | Locate the implementation definition of the name at a given position. +-- Goto Implementation for an overloaded function. +gotoImplementation + :: MonadIO m + => WithHieDb + -> LookupModule m + -> IdeOptions + -> HieAstResult + -> Position + -> MaybeT m [Location] +gotoImplementation withHieDb getHieFile ideOpts srcSpans pos + = lift $ instanceLocationsAtPoint withHieDb getHieFile ideOpts pos srcSpans + -- | Synopsis for the name at a given position. atPoint :: IdeOptions -> HieAstResult - -> DocAndKindMap + -> DocAndTyThingMap -> HscEnv -> Position -> IO (Maybe (Maybe Range, [T.Text])) -atPoint IdeOptions{} (HAR _ hf _ _ (kind :: HieKind hietype)) (DKMap dm km) env pos = +atPoint IdeOptions{} (HAR _ (hf :: HieASTs a) rf _ (kind :: HieKind hietype)) (DKMap dm km) env pos = listToMaybe <$> sequence (pointCommand hf pos hoverInfo) where -- Hover info for values/data hoverInfo :: HieAST hietype -> IO (Maybe Range, [T.Text]) hoverInfo ast = do - prettyNames <- mapM prettyName filteredNames + prettyNames <- mapM prettyName names pure (Just range, prettyNames ++ pTypes) where pTypes :: [T.Text] @@ -236,24 +262,34 @@ atPoint IdeOptions{} (HAR _ hf _ _ (kind :: HieKind hietype)) (DKMap dm km) env info :: NodeInfo hietype info = nodeInfoH kind ast + -- We want evidence variables to be displayed last. + -- Evidence trees contain information of secondary relevance. names :: [(Identifier, IdentifierDetails hietype)] - names = M.assocs $ nodeIdentifiers info - - -- Check for evidence bindings - isInternal :: (Identifier, IdentifierDetails a) -> Bool - isInternal (Right _, dets) = - any isEvidenceContext $ identInfo dets - isInternal (Left _, _) = False - - filteredNames :: [(Identifier, IdentifierDetails hietype)] - filteredNames = filter (not . isInternal) names + names = sortOn (any isEvidenceUse . identInfo . snd) $ M.assocs $ nodeIdentifiers info prettyName :: (Either ModuleName Name, IdentifierDetails hietype) -> IO T.Text - prettyName (Right n, dets) = pure $ T.unlines $ - wrapHaskell (printOutputable n <> maybe "" (" :: " <>) ((prettyType <$> identType dets) <|> maybeKind)) - : maybeToList (pretty (definedAt n) (prettyPackageName n)) - ++ catMaybes [ T.unlines . spanDocToMarkdown <$> lookupNameEnv dm n - ] + prettyName (Right n, dets) + -- We want to print evidence variable using a readable tree structure. + -- Evidence variables contain information why a particular instance or + -- type equality was chosen, paired with location information. + | any isEvidenceUse (identInfo dets) = + let + -- The evidence tree may not be present for some reason, e.g., the 'Name' is not + -- present in the tree. + -- Thus, we need to handle it here, but in practice, this should never be 'Nothing'. + evidenceTree = maybe "" (printOutputable . renderEvidenceTree) (getEvidenceTree rf n) + in + pure $ evidenceTree <> "\n" + -- Identifier details that are not evidence variables are used to display type information and + -- documentation of that name. + | otherwise = + let + typeSig = wrapHaskell (printOutputable n <> maybe "" (" :: " <>) ((prettyType <$> identType dets) <|> maybeKind)) + definitionLoc = maybeToList (pretty (definedAt n) (prettyPackageName n)) + docs = maybeToList (T.unlines . spanDocToMarkdown <$> lookupNameEnv dm n) + in + pure $ T.unlines $ + [typeSig] ++ definitionLoc ++ docs where maybeKind = fmap printOutputable $ safeTyThingType =<< lookupNameEnv km n pretty Nothing Nothing = Nothing pretty (Just define) Nothing = Just $ define <> "\n" @@ -271,7 +307,7 @@ atPoint IdeOptions{} (HAR _ hf _ _ (kind :: HieKind hietype)) (DKMap dm km) env -- the package(with version) this `ModuleName` belongs to. packageNameForImportStatement :: ModuleName -> IO T.Text packageNameForImportStatement mod = do - mpkg <- findImportedModule env mod :: IO (Maybe Module) + mpkg <- findImportedModule (setNonHomeFCHook env) mod :: IO (Maybe Module) let moduleName = printOutputable mod case mpkg >>= packageNameWithVersion of Nothing -> pure moduleName @@ -287,7 +323,7 @@ atPoint IdeOptions{} (HAR _ hf _ _ (kind :: HieKind hietype)) (DKMap dm km) env version = T.pack $ showVersion (unitPackageVersion conf) pure $ pkgName <> "-" <> version - -- Type info for the current node, it may contains several symbols + -- Type info for the current node, it may contain several symbols -- for one range, like wildcard types :: [hietype] types = nodeType info @@ -296,9 +332,12 @@ atPoint IdeOptions{} (HAR _ hf _ _ (kind :: HieKind hietype)) (DKMap dm km) env prettyTypes = map (("_ :: "<>) . prettyType) types prettyType :: hietype -> T.Text - prettyType t = case kind of - HieFresh -> printOutputable t - HieFromDisk full_file -> printOutputable $ hieTypeToIface $ recoverFullType t (hie_types full_file) + prettyType = printOutputable . expandType + + expandType :: a -> SDoc + expandType t = case kind of + HieFresh -> ppr t + HieFromDisk full_file -> ppr $ hieTypeToIface $ recoverFullType t (hie_types full_file) definedAt :: Name -> Maybe T.Text definedAt name = @@ -308,6 +347,67 @@ atPoint IdeOptions{} (HAR _ hf _ _ (kind :: HieKind hietype)) (DKMap dm km) env UnhelpfulLoc {} | isInternalName name || isSystemName name -> Nothing _ -> Just $ "*Defined " <> printOutputable (pprNameDefnLoc name) <> "*" + -- We want to render the root constraint even if it is a let, + -- but we don't want to render any subsequent lets + renderEvidenceTree :: Tree (EvidenceInfo a) -> SDoc + -- However, if the root constraint is simply a (Show (,), Show [], Show Int, Show Bool)@ + -- + -- It is also quite helpful to look at the @.hie@ file directly to see how the + -- evidence information is presented on disk. @hiedb dump @ + renderEvidenceTree (T.Node (EvidenceInfo{evidenceDetails=Just (EvLetBind _,_,_)}) [x]) + = renderEvidenceTree x + renderEvidenceTree (T.Node (EvidenceInfo{evidenceDetails=Just (EvLetBind _,_,_), ..}) xs) + = hang (text "Evidence of constraint `" O.<> expandType evidenceType O.<> "`") 2 $ + vcat $ text "constructed using:" : map renderEvidenceTree' xs + renderEvidenceTree (T.Node (EvidenceInfo{..}) _) + = hang (text "Evidence of constraint `" O.<> expandType evidenceType O.<> "`") 2 $ + vcat $ printDets evidenceSpan evidenceDetails : map (text . T.unpack) (maybeToList $ definedAt evidenceVar) + + -- renderEvidenceTree' skips let bound evidence variables and prints the children directly + renderEvidenceTree' (T.Node (EvidenceInfo{evidenceDetails=Just (EvLetBind _,_,_)}) xs) + = vcat (map renderEvidenceTree' xs) + renderEvidenceTree' (T.Node (EvidenceInfo{..}) _) + = hang (text "- `" O.<> expandType evidenceType O.<> "`") 2 $ + vcat $ + printDets evidenceSpan evidenceDetails : map (text . T.unpack) (maybeToList $ definedAt evidenceVar) + + printDets :: RealSrcSpan -> Maybe (EvVarSource, Scope, Maybe Span) -> SDoc + printDets _ Nothing = text "using an external instance" + printDets ospn (Just (src,_,mspn)) = pprSrc + $$ text "at" <+> text (T.unpack $ srcSpanToMdLink location) + where + location = realSrcSpanToLocation spn + -- Use the bind span if we have one, else use the occurrence span + spn = fromMaybe ospn mspn + pprSrc = case src of + -- Users don't know what HsWrappers are + EvWrapperBind -> "bound by type signature or pattern" + _ -> ppr src + +-- | Find 'Location's of type definition at a specific point and return them along with their 'Identifier's. typeLocationsAtPoint :: forall m . MonadIO m @@ -316,14 +416,14 @@ typeLocationsAtPoint -> IdeOptions -> Position -> HieAstResult - -> m [Location] + -> m [(Location, Identifier)] typeLocationsAtPoint withHieDb lookupModule _ideOptions pos (HAR _ ast _ _ hieKind) = case hieKind of HieFromDisk hf -> let arr = hie_types hf ts = concat $ pointCommand ast pos getts unfold = map (arr A.!) - getts x = nodeType ni ++ (mapMaybe identType $ M.elems $ nodeIdentifiers ni) + getts x = nodeType ni ++ mapMaybe identType (M.elems $ nodeIdentifiers ni) where ni = nodeInfo' x getTypes' ts' = flip concatMap (unfold ts') $ \case HTyVarTy n -> [n] @@ -334,42 +434,64 @@ typeLocationsAtPoint withHieDb lookupModule _ideOptions pos (HAR _ ast _ _ hieKi HQualTy a b -> getTypes' [a,b] HCastTy a -> getTypes' [a] _ -> [] - in fmap nubOrd $ concatMapM (fmap (fromMaybe []) . nameToLocation withHieDb lookupModule) (getTypes' ts) + in fmap nubOrd $ concatMapM (\n -> fmap (maybe [] (fmap (,Right n))) (nameToLocation withHieDb lookupModule n)) (getTypes' ts) HieFresh -> let ts = concat $ pointCommand ast pos getts - getts x = nodeType ni ++ (mapMaybe identType $ M.elems $ nodeIdentifiers ni) + getts x = nodeType ni ++ mapMaybe identType (M.elems $ nodeIdentifiers ni) where ni = nodeInfo x - in fmap nubOrd $ concatMapM (fmap (fromMaybe []) . nameToLocation withHieDb lookupModule) (getTypes ts) + in fmap nubOrd $ concatMapM (\n -> fmap (maybe [] (fmap (,Right n))) (nameToLocation withHieDb lookupModule n)) (getTypes ts) namesInType :: Type -> [Name] namesInType (TyVarTy n) = [varName n] namesInType (AppTy a b) = getTypes [a,b] namesInType (TyConApp tc ts) = tyConName tc : getTypes ts namesInType (ForAllTy b t) = varName (binderVar b) : namesInType t -namesInType (FunTy a b) = getTypes [a,b] +namesInType (FunTy _ a b) = getTypes [a,b] namesInType (CastTy t _) = namesInType t namesInType (LitTy _) = [] namesInType _ = [] getTypes :: [Type] -> [Name] -getTypes ts = concatMap namesInType ts +getTypes = concatMap namesInType +-- | Find 'Location's of definition at a specific point and return them along with their 'Identifier's. locationsAtPoint - :: forall m a + :: forall m . MonadIO m => WithHieDb -> LookupModule m -> IdeOptions -> M.Map ModuleName NormalizedFilePath -> Position - -> HieASTs a - -> m [Location] -locationsAtPoint withHieDb lookupModule _ideOptions imports pos ast = + -> HieAstResult + -> m [(Location, Identifier)] +locationsAtPoint withHieDb lookupModule _ideOptions imports pos (HAR _ ast _rm _ _) = let ns = concat $ pointCommand ast pos (M.keys . getNodeIds) zeroPos = Position 0 0 zeroRange = Range zeroPos zeroPos - modToLocation m = fmap (\fs -> pure $ Location (fromNormalizedUri $ filePathToUri' fs) zeroRange) $ M.lookup m imports - in fmap (nubOrd . concat) $ mapMaybeM (either (pure . modToLocation) $ nameToLocation withHieDb lookupModule) ns + modToLocation m = fmap (\fs -> pure (Location (fromNormalizedUri $ filePathToUri' fs) zeroRange)) $ M.lookup m imports + in fmap (nubOrd . concat) $ mapMaybeM + (either (\m -> pure ((fmap $ fmap (,Left m)) (modToLocation m))) + (\n -> fmap (fmap $ fmap (,Right n)) (nameToLocation withHieDb lookupModule n))) + ns + +-- | Find 'Location's of a implementation definition at a specific point. +instanceLocationsAtPoint + :: forall m + . MonadIO m + => WithHieDb + -> LookupModule m + -> IdeOptions + -> Position + -> HieAstResult + -> m [Location] +instanceLocationsAtPoint withHieDb lookupModule _ideOptions pos (HAR _ ast _rm _ _) = + let ns = concat $ pointCommand ast pos (M.keys . getNodeIds) + evTrees = mapMaybe (eitherToMaybe >=> getEvidenceTree _rm) ns + evNs = concatMap (map (evidenceVar) . T.flatten) evTrees + in fmap (nubOrd . concat) $ mapMaybeM + (nameToLocation withHieDb lookupModule) + evNs -- | Given a 'Name' attempt to find the location where it is defined. nameToLocation :: MonadIO m => WithHieDb -> LookupModule m -> Name -> m (Maybe [Location]) @@ -441,10 +563,10 @@ defRowToSymbolInfo _ = Nothing pointCommand :: HieASTs t -> Position -> (HieAST t -> a) -> [a] pointCommand hf pos k = - catMaybes $ M.elems $ flip M.mapWithKey (getAsts hf) $ \fs ast -> + M.elems $ flip M.mapMaybeWithKey (getAsts hf) $ \fs ast -> -- Since GHC 9.2: -- getAsts :: Map HiePath (HieAst a) - -- type HiePath = LexialFastString + -- type HiePath = LexicalFastString -- -- but before: -- getAsts :: Map HiePath (HieAst a) diff --git a/ghcide/src/Development/IDE/Spans/Common.hs b/ghcide/src/Development/IDE/Spans/Common.hs index 2ec1e98e94..f3e86d792d 100644 --- a/ghcide/src/Development/IDE/Spans/Common.hs +++ b/ghcide/src/Development/IDE/Spans/Common.hs @@ -12,33 +12,33 @@ module Development.IDE.Spans.Common ( , spanDocToMarkdown , spanDocToMarkdownForTest , DocMap -, KindMap +, TyThingMap +, srcSpanToMdLink ) where import Control.DeepSeq +import Data.Bifunctor (second) import Data.List.Extra import Data.Maybe import qualified Data.Text as T -import GHC.Generics - +import Development.IDE.GHC.Util +import qualified Documentation.Haddock.Parser as H +import qualified Documentation.Haddock.Types as H import GHC +import GHC.Generics +import System.FilePath -import Data.Bifunctor (second) +import Control.Lens import Development.IDE.GHC.Compat import Development.IDE.GHC.Orphans () -import Development.IDE.GHC.Util -import qualified Documentation.Haddock.Parser as H -import qualified Documentation.Haddock.Types as H +import qualified Language.LSP.Protocol.Lens as JL +import Language.LSP.Protocol.Types type DocMap = NameEnv SpanDoc -type KindMap = NameEnv TyThing +type TyThingMap = NameEnv TyThing -- | Shows IEWrappedName, without any modifier, qualifier or unique identifier. -#if MIN_VERSION_ghc(9,5,0) unqualIEWrapName :: IEWrappedName GhcPs -> T.Text -#else -unqualIEWrapName :: IEWrappedName RdrName -> T.Text -#endif unqualIEWrapName = printOutputable . rdrNameOcc . ieWrappedName -- From haskell-ide-engine/src/Haskell/Ide/Engine/Support/HieExtras.hs @@ -54,13 +54,8 @@ safeTyThingId (AConLike (RealDataCon dataCon)) = Just (dataConWrapId dataCon) safeTyThingId _ = Nothing -- Possible documentation for an element in the code -#if MIN_VERSION_ghc(9,3,0) data SpanDoc = SpanDocString [HsDocString] SpanDocUris -#else -data SpanDoc - = SpanDocString HsDocString SpanDocUris -#endif | SpanDocText [T.Text] SpanDocUris deriving stock (Eq, Show, Generic) deriving anyclass NFData @@ -97,11 +92,7 @@ spanDocToMarkdown :: SpanDoc -> [T.Text] spanDocToMarkdown = \case (SpanDocString docs uris) -> let doc = T.pack $ haddockToMarkdown $ H.toRegular $ H._doc $ H.parseParas Nothing $ -#if MIN_VERSION_ghc(9,3,0) renderHsDocStrings docs -#else - unpackHDS docs -#endif in go [doc] uris (SpanDocText txt uris) -> go txt uris where @@ -118,7 +109,13 @@ spanDocUrisToMarkdown (SpanDocUris mdoc msrc) = catMaybes [ linkify "Documentation" <$> mdoc , linkify "Source" <$> msrc ] - where linkify title uri = "[" <> title <> "](" <> uri <> ")" + +-- | Generate a markdown link. +-- +-- >>> linkify "Title" "uri" +-- "[Title](Uri)" +linkify :: T.Text -> T.Text -> T.Text +linkify title uri = "[" <> title <> "](" <> uri <> ")" spanDocToMarkdownForTest :: String -> String spanDocToMarkdownForTest @@ -224,3 +221,35 @@ splitForList s = case lines s of [] -> "" (first:rest) -> unlines $ first : map ((" " ++) . trimStart) rest + +-- | Generate a source link for the 'Location' according to VSCode's supported form: +-- https://github.com/microsoft/vscode/blob/b3ec8181fc49f5462b5128f38e0723ae85e295c2/src/vs/platform/opener/common/opener.ts#L151-L160 +-- +srcSpanToMdLink :: Location -> T.Text +srcSpanToMdLink location = + let + uri = location ^. JL.uri + range = location ^. JL.range + -- LSP 'Range' starts at '0', but link locations start at '1'. + intText n = T.pack $ show (n + 1) + srcRangeText = + T.concat + [ "L" + , intText (range ^. JL.start . JL.line) + , "," + , intText (range ^. JL.start . JL.character) + , "-L" + , intText (range ^. JL.end . JL.line) + , "," + , intText (range ^. JL.end . JL.character) + ] + + -- If the 'Location' is a 'FilePath', display it in shortened form. + -- This avoids some redundancy and better readability for the user. + title = case uriToFilePath uri of + Just fp -> T.pack (takeFileName fp) <> ":" <> intText (range ^. JL.start . JL.line) + Nothing -> getUri uri + + srcLink = getUri uri <> "#" <> srcRangeText + in + linkify title srcLink diff --git a/ghcide/src/Development/IDE/Spans/Documentation.hs b/ghcide/src/Development/IDE/Spans/Documentation.hs index 7f74b936a0..85f2ef1037 100644 --- a/ghcide/src/Development/IDE/Spans/Documentation.hs +++ b/ghcide/src/Development/IDE/Spans/Documentation.hs @@ -1,8 +1,7 @@ -{-# LANGUAGE RankNTypes #-} -- Copyright (c) 2019 The DAML Authors. All rights reserved. -- SPDX-License-Identifier: Apache-2.0 -{-# LANGUAGE CPP #-} +{-# LANGUAGE CPP #-} module Development.IDE.Spans.Documentation ( getDocumentation @@ -39,19 +38,11 @@ mkDocMap :: HscEnv -> RefMap a -> TcGblEnv - -> IO DocAndKindMap + -> IO DocAndTyThingMap mkDocMap env rm this_mod = do -#if MIN_VERSION_ghc(9,3,0) (Just Docs{docs_decls = UniqMap this_docs}) <- extractDocs (hsc_dflags env) this_mod -#else - (_ , DeclDocMap this_docs, _) <- extractDocs this_mod -#endif -#if MIN_VERSION_ghc(9,3,0) d <- foldrM getDocs (fmap (\(_, x) -> (map hsDocString x) `SpanDocString` SpanDocUris Nothing Nothing) this_docs) names -#else - d <- foldrM getDocs (mkNameEnv $ M.toList $ fmap (`SpanDocString` SpanDocUris Nothing Nothing) this_docs) names -#endif k <- foldrM getType (tcg_type_env this_mod) names pure $ DKMap d k where @@ -61,8 +52,7 @@ mkDocMap env rm this_mod = doc <- getDocumentationTryGhc env n pure $ extendNameEnv nameMap n doc getType n nameMap - | isTcOcc $ occName n - , Nothing <- lookupNameEnv nameMap n + | Nothing <- lookupNameEnv nameMap n = do kind <- lookupKind env n pure $ maybe nameMap (extendNameEnv nameMap n) kind | otherwise = pure nameMap @@ -86,11 +76,7 @@ getDocumentationsTryGhc env names = do Left _ -> return [] Right res -> zipWithM unwrap res names where -#if MIN_VERSION_ghc(9,3,0) unwrap (Right (Just docs, _)) n = SpanDocString (map hsDocString docs) <$> getUris n -#else - unwrap (Right (Just docs, _)) n = SpanDocString docs <$> getUris n -#endif unwrap _ n = mkSpanDocText n mkSpanDocText name = diff --git a/ghcide/src/Development/IDE/Spans/LocalBindings.hs b/ghcide/src/Development/IDE/Spans/LocalBindings.hs index 0fd74cf0dc..8ca811eaa0 100644 --- a/ghcide/src/Development/IDE/Spans/LocalBindings.hs +++ b/ghcide/src/Development/IDE/Spans/LocalBindings.hs @@ -22,7 +22,8 @@ import Development.IDE.GHC.Compat (Name, NameEnv, RealSrcSpan, getBindSiteFromContext, getScopeFromContext, identInfo, identType, isSystemName, - nameEnvElts, realSrcSpanEnd, + nonDetNameEnvElts, + realSrcSpanEnd, realSrcSpanStart, unitNameEnv) import Development.IDE.GHC.Error @@ -99,7 +100,7 @@ instance Show Bindings where -- 'RealSrcSpan', getLocalScope :: Bindings -> RealSrcSpan -> [(Name, Maybe Type)] getLocalScope bs rss - = nameEnvElts + = nonDetNameEnvElts $ foldMap snd $ IM.dominators (realSrcSpanToInterval rss) $ getLocalBindings bs @@ -109,7 +110,7 @@ getLocalScope bs rss -- 'RealSrcSpan', getDefiningBindings :: Bindings -> RealSrcSpan -> [(Name, Maybe Type)] getDefiningBindings bs rss - = nameEnvElts + = nonDetNameEnvElts $ foldMap snd $ IM.dominators (realSrcSpanToInterval rss) $ getBindingSites bs @@ -121,7 +122,7 @@ getDefiningBindings bs rss getFuzzyScope :: Bindings -> Position -> Position -> [(Name, Maybe Type)] getFuzzyScope bs a b = filter (not . isSystemName . fst) - $ nameEnvElts + $ nonDetNameEnvElts $ foldMap snd $ IM.intersections (Interval a b) $ getLocalBindings bs @@ -133,7 +134,7 @@ getFuzzyScope bs a b -- `PositionMapping` getFuzzyDefiningBindings :: Bindings -> Position -> Position -> [(Name, Maybe Type)] getFuzzyDefiningBindings bs a b - = nameEnvElts + = nonDetNameEnvElts $ foldMap snd $ IM.intersections (Interval a b) $ getBindingSites bs diff --git a/ghcide/src/Development/IDE/Spans/Pragmas.hs b/ghcide/src/Development/IDE/Spans/Pragmas.hs index a2b4981a38..4df16c6704 100644 --- a/ghcide/src/Development/IDE/Spans/Pragmas.hs +++ b/ghcide/src/Development/IDE/Spans/Pragmas.hs @@ -15,6 +15,8 @@ import qualified Data.List as List import qualified Data.Maybe as Maybe import Data.Text (Text, pack) import qualified Data.Text as Text +import Data.Text.Utf16.Rope.Mixed (Rope) +import qualified Data.Text.Utf16.Rope.Mixed as Rope import Development.IDE (srcSpanToRange, IdeState, NormalizedFilePath, GhcSession (..), getFileContents, hscEnv, runAction) import Development.IDE.GHC.Compat import Development.IDE.GHC.Compat.Util @@ -27,10 +29,10 @@ import qualified Data.Text as T import Development.IDE.Core.PluginUtils import qualified Language.LSP.Protocol.Lens as L -getNextPragmaInfo :: DynFlags -> Maybe Text -> NextPragmaInfo -getNextPragmaInfo dynFlags mbSourceText = - if | Just sourceText <- mbSourceText - , let sourceStringBuffer = stringToStringBuffer (Text.unpack sourceText) +getNextPragmaInfo :: DynFlags -> Maybe Rope -> NextPragmaInfo +getNextPragmaInfo dynFlags mbSource = + if | Just source <- mbSource + , let sourceStringBuffer = stringToStringBuffer (Text.unpack (Rope.toText source)) , POk _ parserState <- parsePreDecl dynFlags sourceStringBuffer -> case parserState of ParserStateNotDone{ nextPragma } -> nextPragma @@ -56,7 +58,7 @@ insertNewPragma (NextPragmaInfo nextPragmaLine _) newPragma = LSP.TextEdit prag getFirstPragma :: MonadIO m => PluginId -> IdeState -> NormalizedFilePath -> ExceptT PluginError m NextPragmaInfo getFirstPragma (PluginId pId) state nfp = do (hscEnv -> hsc_dflags -> sessionDynFlags, _) <- runActionE (T.unpack pId <> ".GhcSession") state $ useWithStaleE GhcSession nfp - (_, fileContents) <- liftIO $ runAction (T.unpack pId <> ".GetFileContents") state $ getFileContents nfp + fileContents <- liftIO $ runAction (T.unpack pId <> ".GetFileContents") state $ getFileContents nfp pure $ getNextPragmaInfo sessionDynFlags fileContents -- Pre-declaration comments parser ----------------------------------------------------- diff --git a/ghcide/src/Development/IDE/Types/Diagnostics.hs b/ghcide/src/Development/IDE/Types/Diagnostics.hs index 8189ff89c1..5072fa7ffa 100644 --- a/ghcide/src/Development/IDE/Types/Diagnostics.hs +++ b/ghcide/src/Development/IDE/Types/Diagnostics.hs @@ -1,32 +1,61 @@ -- Copyright (c) 2019 The DAML Authors. All rights reserved. -- SPDX-License-Identifier: Apache-2.0 +{-# LANGUAGE CPP #-} +{-# LANGUAGE DeriveGeneric #-} +{-# LANGUAGE TemplateHaskell #-} module Development.IDE.Types.Diagnostics ( LSP.Diagnostic(..), ShowDiagnostic(..), - FileDiagnostic, + FileDiagnostic(..), + fdFilePathL, + fdLspDiagnosticL, + fdShouldShowDiagnosticL, + fdStructuredMessageL, + StructuredMessage(..), + _NoStructuredMessage, + _SomeStructuredMessage, IdeResult, LSP.DiagnosticSeverity(..), DiagnosticStore, ideErrorText, ideErrorWithSource, + ideErrorFromLspDiag, showDiagnostics, showDiagnosticsColored, - IdeResultNoDiagnosticsEarlyCutoff) where + showGhcCode, + IdeResultNoDiagnosticsEarlyCutoff, + attachReason, + attachedReason) where +import Control.Applicative ((<|>)) import Control.DeepSeq +import Control.Lens +import qualified Data.Aeson as JSON +import qualified Data.Aeson.Lens as JSON import Data.ByteString (ByteString) +import Data.Foldable import Data.Maybe as Maybe import qualified Data.Text as T +import Development.IDE.GHC.Compat (GhcMessage, MsgEnvelope, + WarningFlag, flagSpecFlag, + flagSpecName, wWarningFlags) import Development.IDE.Types.Location +import GHC.Generics +import GHC.Types.Error (DiagnosticCode (..), + DiagnosticReason (..), + diagnosticCode, + diagnosticReason, + errMsgDiagnostic) import Language.LSP.Diagnostics -import Language.LSP.Protocol.Types as LSP (Diagnostic (..), - DiagnosticSeverity (..)) +import Language.LSP.Protocol.Lens (data_) +import Language.LSP.Protocol.Types as LSP import Prettyprinter import Prettyprinter.Render.Terminal (Color (..), color) import qualified Prettyprinter.Render.Terminal as Terminal import Prettyprinter.Render.Text +import Text.Printf (printf) -- | The result of an IDE operation. Warnings and errors are in the Diagnostic, @@ -44,26 +73,97 @@ type IdeResult v = ([FileDiagnostic], Maybe v) -- | an IdeResult with a fingerprint type IdeResultNoDiagnosticsEarlyCutoff v = (Maybe ByteString, Maybe v) +-- | Produce a 'FileDiagnostic' for the given 'NormalizedFilePath' +-- with an error message. ideErrorText :: NormalizedFilePath -> T.Text -> FileDiagnostic -ideErrorText = ideErrorWithSource (Just "compiler") (Just DiagnosticSeverity_Error) +ideErrorText nfp msg = + ideErrorWithSource (Just "compiler") (Just DiagnosticSeverity_Error) nfp msg Nothing + +-- | Create a 'FileDiagnostic' from an existing 'LSP.Diagnostic' for a +-- specific 'NormalizedFilePath'. +-- The optional 'MsgEnvelope GhcMessage' is the original error message +-- that was used for creating the 'LSP.Diagnostic'. +-- It is included here, to allow downstream consumers, such as HLS plugins, +-- to provide LSP features based on the structured error messages. +-- Additionally, if available, we insert the ghc error code into the +-- 'LSP.Diagnostic'. These error codes are used in https://errors.haskell.org/ +-- to provide documentation and explanations for error messages. +ideErrorFromLspDiag + :: LSP.Diagnostic + -> NormalizedFilePath + -> Maybe (MsgEnvelope GhcMessage) + -> FileDiagnostic +ideErrorFromLspDiag lspDiag fdFilePath mbOrigMsg = + let fdShouldShowDiagnostic = ShowDiag + fdStructuredMessage = + case mbOrigMsg of + Nothing -> NoStructuredMessage + Just msg -> SomeStructuredMessage msg + fdLspDiagnostic = + lspDiag + & attachReason (fmap (diagnosticReason . errMsgDiagnostic) mbOrigMsg) + & attachDiagnosticCode ((diagnosticCode . errMsgDiagnostic) =<< mbOrigMsg) + in + FileDiagnostic {..} + +-- | Set the code of the 'LSP.Diagnostic' to the GHC diagnostic code, and include the link +-- to https://errors.haskell.org/. +attachDiagnosticCode :: Maybe DiagnosticCode -> LSP.Diagnostic -> LSP.Diagnostic +attachDiagnosticCode Nothing diag = diag +attachDiagnosticCode (Just code) diag = + let + textualCode = showGhcCode code + codeDesc = LSP.CodeDescription{ _href = Uri $ "https://errors.haskell.org/messages/" <> textualCode } + in diag { _code = Just (InR textualCode), _codeDescription = Just codeDesc} + +#if MIN_VERSION_ghc(9,9,0) +-- DiagnosticCode only got a show instance in 9.10.1 +showGhcCode :: DiagnosticCode -> T.Text +showGhcCode = T.pack . show +#else +showGhcCode :: DiagnosticCode -> T.Text +showGhcCode (DiagnosticCode prefix c) = T.pack $ prefix ++ "-" ++ printf "%05d" c +#endif + +attachedReason :: Traversal' Diagnostic (Maybe JSON.Value) +attachedReason = data_ . non (JSON.object []) . JSON.atKey "attachedReason" + +attachReason :: Maybe DiagnosticReason -> Diagnostic -> Diagnostic +attachReason Nothing = id +attachReason (Just wr) = attachedReason .~ fmap JSON.toJSON (showReason wr) + where + showReason = \case + WarningWithFlag flag -> Just $ catMaybes [showFlag flag] +#if MIN_VERSION_ghc(9,7,0) + WarningWithFlags flags -> Just $ catMaybes (fmap showFlag $ toList flags) +#endif + _ -> Nothing + +showFlag :: WarningFlag -> Maybe T.Text +showFlag flag = ("-W" <>) . T.pack . flagSpecName <$> find ((== flag) . flagSpecFlag) wWarningFlags ideErrorWithSource :: Maybe T.Text -> Maybe DiagnosticSeverity - -> a + -> NormalizedFilePath -> T.Text - -> (a, ShowDiagnostic, Diagnostic) -ideErrorWithSource source sev fp msg = (fp, ShowDiag, LSP.Diagnostic { - _range = noRange, - _severity = sev, - _code = Nothing, - _source = source, - _message = msg, - _relatedInformation = Nothing, - _tags = Nothing, - _codeDescription = Nothing, - _data_ = Nothing - }) + -> Maybe (MsgEnvelope GhcMessage) + -> FileDiagnostic +ideErrorWithSource source sev fdFilePath msg origMsg = + let lspDiagnostic = + LSP.Diagnostic { + _range = noRange, + _severity = sev, + _code = Nothing, + _source = source, + _message = msg, + _relatedInformation = Nothing, + _tags = Nothing, + _codeDescription = Nothing, + _data_ = Nothing + } + in + ideErrorFromLspDiag lspDiagnostic fdFilePath origMsg -- | Defines whether a particular diagnostic should be reported -- back to the user. @@ -80,13 +180,78 @@ data ShowDiagnostic instance NFData ShowDiagnostic where rnf = rwhnf +-- | A Maybe-like wrapper for a GhcMessage that doesn't try to compare, show, or +-- force the GhcMessage inside, so that we can derive Show, Eq, Ord, NFData on +-- FileDiagnostic. FileDiagnostic only uses this as metadata so we can safely +-- ignore it in fields. +-- +-- Instead of pattern matching on these constructors directly, consider 'Prism' from +-- the 'lens' package. This allows to conveniently pattern match deeply into the 'MsgEnvelope GhcMessage' +-- constructor. +-- The module 'Development.IDE.GHC.Compat.Error' implements additional 'Lens's and 'Prism's, +-- allowing you to avoid importing GHC modules directly. +-- +-- For example, to pattern match on a 'TcRnMessage' you can use the lens: +-- +-- @ +-- message ^? _SomeStructuredMessage . msgEnvelopeErrorL . _TcRnMessage +-- @ +-- +-- This produces a value of type `Maybe TcRnMessage`. +-- +-- Further, consider utility functions such as 'stripTcRnMessageContext', which strip +-- context from error messages which may be more convenient in certain situations. +data StructuredMessage + = NoStructuredMessage + | SomeStructuredMessage (MsgEnvelope GhcMessage) + deriving (Generic) + +instance Show StructuredMessage where + show NoStructuredMessage = "NoStructuredMessage" + show SomeStructuredMessage {} = "SomeStructuredMessage" + +instance Eq StructuredMessage where + (==) NoStructuredMessage NoStructuredMessage = True + (==) SomeStructuredMessage {} SomeStructuredMessage {} = True + (==) _ _ = False + +instance Ord StructuredMessage where + compare NoStructuredMessage NoStructuredMessage = EQ + compare SomeStructuredMessage {} SomeStructuredMessage {} = EQ + compare NoStructuredMessage SomeStructuredMessage {} = GT + compare SomeStructuredMessage {} NoStructuredMessage = LT + +instance NFData StructuredMessage where + rnf NoStructuredMessage = () + rnf SomeStructuredMessage {} = () + -- | Human readable diagnostics for a specific file. -- -- This type packages a pretty printed, human readable error message -- along with the related source location so that we can display the error -- on either the console or in the IDE at the right source location. -- -type FileDiagnostic = (NormalizedFilePath, ShowDiagnostic, Diagnostic) +-- It also optionally keeps a structured diagnostic message GhcMessage in +-- StructuredMessage. +-- +data FileDiagnostic = FileDiagnostic + { fdFilePath :: NormalizedFilePath + , fdShouldShowDiagnostic :: ShowDiagnostic + , fdLspDiagnostic :: Diagnostic + -- | The original diagnostic that was used to produce 'fdLspDiagnostic'. + -- We keep it here, so downstream consumers, e.g. HLS plugins, can use the + -- the structured error messages and don't have to resort to parsing + -- error messages via regexes or similar. + -- + -- The optional GhcMessage inside of this StructuredMessage is ignored for + -- Eq, Ord, Show, and NFData instances. This is fine because this field + -- should only ever be metadata and should never be used to distinguish + -- between FileDiagnostics. + , fdStructuredMessage :: StructuredMessage + } + deriving (Eq, Ord, Show, Generic) + +instance NFData FileDiagnostic prettyRange :: Range -> Doc Terminal.AnsiStyle prettyRange Range{..} = f _start <> "-" <> f _end @@ -106,13 +271,17 @@ prettyDiagnostics :: [FileDiagnostic] -> Doc Terminal.AnsiStyle prettyDiagnostics = vcat . map prettyDiagnostic prettyDiagnostic :: FileDiagnostic -> Doc Terminal.AnsiStyle -prettyDiagnostic (fp, sh, LSP.Diagnostic{..}) = +prettyDiagnostic FileDiagnostic { fdFilePath, fdShouldShowDiagnostic, fdLspDiagnostic = LSP.Diagnostic{..} } = vcat - [ slabel_ "File: " $ pretty (fromNormalizedFilePath fp) - , slabel_ "Hidden: " $ if sh == ShowDiag then "no" else "yes" + [ slabel_ "File: " $ pretty (fromNormalizedFilePath fdFilePath) + , slabel_ "Hidden: " $ if fdShouldShowDiagnostic == ShowDiag then "no" else "yes" , slabel_ "Range: " $ prettyRange _range , slabel_ "Source: " $ pretty _source , slabel_ "Severity:" $ pretty $ show sev + , slabel_ "Code: " $ case _code of + Just (InR text) -> pretty text + Just (InL i) -> pretty i + Nothing -> "" , slabel_ "Message: " $ case sev of LSP.DiagnosticSeverity_Error -> annotate $ color Red @@ -150,3 +319,9 @@ srenderColored = defaultTermWidth :: Int defaultTermWidth = 80 + +makePrisms ''StructuredMessage + +makeLensesWith + (lensRules & lensField .~ mappingNamer (pure . (++ "L"))) + ''FileDiagnostic diff --git a/ghcide/src/Development/IDE/Types/Exports.hs b/ghcide/src/Development/IDE/Types/Exports.hs index d2a1739f4a..3b40ce1653 100644 --- a/ghcide/src/Development/IDE/Types/Exports.hs +++ b/ghcide/src/Development/IDE/Types/Exports.hs @@ -1,6 +1,5 @@ {-# LANGUAGE DeriveAnyClass #-} {-# LANGUAGE DerivingStrategies #-} -{-# LANGUAGE RankNTypes #-} module Development.IDE.Types.Exports ( IdentInfo(..), @@ -208,7 +207,7 @@ identInfoToKeyVal identInfo = buildModuleExportMap:: [(ModuleName, HashSet IdentInfo)] -> ModuleNameEnv (HashSet IdentInfo) buildModuleExportMap exportsMap = do - let lst = concatMap (Set.toList. snd) exportsMap + let lst = concatMap (Set.toList . snd) exportsMap let lstThree = map identInfoToKeyVal lst sortAndGroup lstThree @@ -224,4 +223,4 @@ extractModuleExports modIFace = do (modName, functionSet) sortAndGroup :: [(ModuleName, IdentInfo)] -> ModuleNameEnv (HashSet IdentInfo) -sortAndGroup assocs = listToUFM_C (<>) [(k, Set.fromList [v]) | (k, v) <- assocs] +sortAndGroup assocs = listToUFM_C (<>) [(k, Set.singleton v) | (k, v) <- assocs] diff --git a/ghcide/src/Development/IDE/Types/HscEnvEq.hs b/ghcide/src/Development/IDE/Types/HscEnvEq.hs index 502c265077..1c2ed1732f 100644 --- a/ghcide/src/Development/IDE/Types/HscEnvEq.hs +++ b/ghcide/src/Development/IDE/Types/HscEnvEq.hs @@ -1,25 +1,21 @@ +{-# LANGUAGE CPP #-} module Development.IDE.Types.HscEnvEq ( HscEnvEq, hscEnv, newHscEnvEq, - hscEnvWithImportPaths, - newHscEnvEqPreserveImportPaths, - newHscEnvEqWithImportPaths, updateHscEnvEq, - envImportPaths, envPackageExports, envVisibleModuleNames, - deps ) where import Control.Concurrent.Async (Async, async, waitCatch) import Control.Concurrent.Strict (modifyVar, newVar) -import Control.DeepSeq (force) +import Control.DeepSeq (force, rwhnf) import Control.Exception (evaluate, mask, throwIO) import Control.Monad.Extra (eitherM, join, mapMaybeM) import Data.Either (fromRight) -import Data.Set (Set) -import qualified Data.Set as Set +import Data.IORef +import qualified Data.Map as M import Data.Unique (Unique) import qualified Data.Unique as Unique import Development.IDE.GHC.Compat hiding (newUnique) @@ -28,9 +24,11 @@ import Development.IDE.GHC.Error (catchSrcErrors) import Development.IDE.GHC.Util (lookupPackageConfig) import Development.IDE.Graph.Classes import Development.IDE.Types.Exports (ExportsMap, createExportsMap) +import GHC.Driver.Env (hsc_all_home_unit_ids) +import Ide.PluginUtils (toAbsolute) import OpenTelemetry.Eventlog (withSpan) import System.Directory (makeAbsolute) -import System.FilePath + -- | An 'HscEnv' with equality. Two values are considered equal -- if they are created with the same call to 'newHscEnvEq' or @@ -38,13 +36,6 @@ import System.FilePath data HscEnvEq = HscEnvEq { envUnique :: !Unique , hscEnv :: !HscEnv - , deps :: [(UnitId, DynFlags)] - -- ^ In memory components for this HscEnv - -- This is only used at the moment for the import dirs in - -- the DynFlags - , envImportPaths :: Maybe (Set FilePath) - -- ^ If Just, import dirs originally configured in this env - -- If Nothing, the env import dirs are unaltered , envPackageExports :: IO ExportsMap , envVisibleModuleNames :: IO (Maybe [ModuleName]) -- ^ 'listVisibleModuleNames' is a pure function, @@ -59,19 +50,32 @@ updateHscEnvEq oldHscEnvEq newHscEnv = do update <$> Unique.newUnique -- | Wrap an 'HscEnv' into an 'HscEnvEq'. -newHscEnvEq :: FilePath -> HscEnv -> [(UnitId, DynFlags)] -> IO HscEnvEq -newHscEnvEq cradlePath hscEnv0 deps = do - let relativeToCradle = (takeDirectory cradlePath ) - hscEnv = removeImportPaths hscEnv0 - - -- Make Absolute since targets are also absolute - importPathsCanon <- - mapM makeAbsolute $ relativeToCradle <$> importPaths (hsc_dflags hscEnv0) - - newHscEnvEqWithImportPaths (Just $ Set.fromList importPathsCanon) hscEnv deps - -newHscEnvEqWithImportPaths :: Maybe (Set FilePath) -> HscEnv -> [(UnitId, DynFlags)] -> IO HscEnvEq -newHscEnvEqWithImportPaths envImportPaths hscEnv deps = do +newHscEnvEq :: HscEnv -> IO HscEnvEq +newHscEnvEq hscEnv' = do + + mod_cache <- newIORef emptyInstalledModuleEnv + file_cache <- newIORef M.empty + -- This finder cache is for things which are outside of things which are tracked + -- by HLS. For example, non-home modules, dependent object files etc +#if MIN_VERSION_ghc(9,11,0) + let hscEnv = hscEnv' + { hsc_FC = FinderCache + { flushFinderCaches = \_ -> error "GHC should never call flushFinderCaches outside the driver" + , addToFinderCache = \(GWIB im _) val -> do + if moduleUnit im `elem` hsc_all_home_unit_ids hscEnv' + then error "tried to add home module to FC" + else atomicModifyIORef' mod_cache $ \c -> (extendInstalledModuleEnv c im val, ()) + , lookupFinderCache = \(GWIB im _) -> do + if moduleUnit im `elem` hsc_all_home_unit_ids hscEnv' + then error ("tried to lookup home module from FC" ++ showSDocUnsafe (ppr (im, hsc_all_home_unit_ids hscEnv'))) + else lookupInstalledModuleEnv <$> readIORef mod_cache <*> pure im + , lookupFileCache = \fp -> error ("not used by HLS" ++ fp) + } + } + +#else + let hscEnv = hscEnv' +#endif let dflags = hsc_dflags hscEnv @@ -113,23 +117,6 @@ newHscEnvEqWithImportPaths envImportPaths hscEnv deps = do return HscEnvEq{..} --- | Wrap an 'HscEnv' into an 'HscEnvEq'. -newHscEnvEqPreserveImportPaths - :: HscEnv -> [(UnitId, DynFlags)] -> IO HscEnvEq -newHscEnvEqPreserveImportPaths = newHscEnvEqWithImportPaths Nothing - --- | Unwrap the 'HscEnv' with the original import paths. --- Used only for locating imports -hscEnvWithImportPaths :: HscEnvEq -> HscEnv -hscEnvWithImportPaths HscEnvEq{..} - | Just imps <- envImportPaths - = hscSetFlags (setImportPaths (Set.toList imps) (hsc_dflags hscEnv)) hscEnv - | otherwise - = hscEnv - -removeImportPaths :: HscEnv -> HscEnv -removeImportPaths hsc = hscSetFlags (setImportPaths [] (hsc_dflags hsc)) hsc - instance Show HscEnvEq where show HscEnvEq{envUnique} = "HscEnvEq " ++ show (Unique.hashUnique envUnique) @@ -137,9 +124,9 @@ instance Eq HscEnvEq where a == b = envUnique a == envUnique b instance NFData HscEnvEq where - rnf (HscEnvEq a b c d _ _) = + rnf (HscEnvEq a b _ _) = -- deliberately skip the package exports map and visible module names - rnf (Unique.hashUnique a) `seq` b `seq` c `seq` rnf d + rnf (Unique.hashUnique a) `seq` rwhnf b instance Hashable HscEnvEq where hashWithSalt s = hashWithSalt s . envUnique diff --git a/ghcide/src/Development/IDE/Types/KnownTargets.hs b/ghcide/src/Development/IDE/Types/KnownTargets.hs index 7f49ced08d..6ae6d52ba3 100644 --- a/ghcide/src/Development/IDE/Types/KnownTargets.hs +++ b/ghcide/src/Development/IDE/Types/KnownTargets.hs @@ -1,6 +1,11 @@ {-# LANGUAGE DeriveAnyClass #-} {-# LANGUAGE DerivingStrategies #-} -module Development.IDE.Types.KnownTargets (KnownTargets, Target(..), toKnownFiles) where +module Development.IDE.Types.KnownTargets ( KnownTargets(..) + , emptyKnownTargets + , mkKnownTargets + , unionKnownTargets + , Target(..) + , toKnownFiles) where import Control.DeepSeq import Data.Hashable @@ -14,11 +19,53 @@ import Development.IDE.Types.Location import GHC.Generics -- | A mapping of module name to known files -type KnownTargets = HashMap Target (HashSet NormalizedFilePath) +data KnownTargets = KnownTargets + { targetMap :: !(HashMap Target (HashSet NormalizedFilePath)) + -- | 'normalisingMap' is a cached copy of `HMap.mapKey const targetMap` + -- + -- At startup 'GetLocatedImports' is called on all known files. Say you have 10000 + -- modules in your project then this leads to 10000 calls to 'GetLocatedImports' + -- running concurrently. + -- + -- In `GetLocatedImports` the known targets are consulted and the targetsMap + -- is created by mapping the known targets. This map is used for introducing + -- sharing amongst filepaths. This operation copies a local copy of the `target` + -- map which is local to the rule. + -- + -- @ + -- let targetsMap = HMap.mapWithKey const targets + -- @ + -- + -- So now each rule has a 'HashMap' of size 10000 held locally to it and depending + -- on how the threads are scheduled there will be 10000^2 elements in total + -- allocated in 'HashMap's. This used a lot of memory. + -- + -- Solution: Return the 'normalisingMap' in the result of the `GetKnownTargets` rule so it is shared across threads. + , normalisingMap :: !(HashMap Target Target) } deriving Show + + +unionKnownTargets :: KnownTargets -> KnownTargets -> KnownTargets +unionKnownTargets (KnownTargets tm nm) (KnownTargets tm' nm') = + KnownTargets (HMap.unionWith (<>) tm tm') (HMap.union nm nm') + +mkKnownTargets :: [(Target, HashSet NormalizedFilePath)] -> KnownTargets +mkKnownTargets vs = KnownTargets (HMap.fromList vs) (HMap.fromList [(k,k) | (k,_) <- vs ]) + +instance NFData KnownTargets where + rnf (KnownTargets tm nm) = rnf tm `seq` rnf nm `seq` () + +instance Eq KnownTargets where + k1 == k2 = targetMap k1 == targetMap k2 + +instance Hashable KnownTargets where + hashWithSalt s (KnownTargets hm _) = hashWithSalt s hm + +emptyKnownTargets :: KnownTargets +emptyKnownTargets = KnownTargets HMap.empty HMap.empty data Target = TargetModule ModuleName | TargetFile NormalizedFilePath - deriving ( Eq, Generic, Show ) + deriving ( Eq, Ord, Generic, Show ) deriving anyclass (Hashable, NFData) toKnownFiles :: KnownTargets -> HashSet NormalizedFilePath -toKnownFiles = HSet.unions . HMap.elems +toKnownFiles = HSet.unions . HMap.elems . targetMap diff --git a/ghcide/src/Development/IDE/Types/Location.hs b/ghcide/src/Development/IDE/Types/Location.hs index 7623c1cf25..06ca9cbeca 100644 --- a/ghcide/src/Development/IDE/Types/Location.hs +++ b/ghcide/src/Development/IDE/Types/Location.hs @@ -2,7 +2,6 @@ -- SPDX-License-Identifier: Apache-2.0 {-# LANGUAGE CPP #-} - -- | Types and functions for working with source code locations. module Development.IDE.Types.Location ( Location(..) @@ -36,8 +35,6 @@ import Language.LSP.Protocol.Types (Location (..), Position (..), import qualified Language.LSP.Protocol.Types as LSP import Text.ParserCombinators.ReadP as ReadP --- See Note [Guidelines For Using CPP In GHCIDE Import Statements] - import GHC.Data.FastString import GHC.Types.SrcLoc as GHC diff --git a/ghcide/src/Development/IDE/Types/Options.hs b/ghcide/src/Development/IDE/Types/Options.hs index 1291e044f4..be3ea20932 100644 --- a/ghcide/src/Development/IDE/Types/Options.hs +++ b/ghcide/src/Development/IDE/Types/Options.hs @@ -2,7 +2,6 @@ -- SPDX-License-Identifier: Apache-2.0 -- | Options -{-# LANGUAGE RankNTypes #-} module Development.IDE.Types.Options ( IdeOptions(..) , IdePreprocessedSource(..) @@ -90,9 +89,9 @@ data OptHaddockParse = HaddockParse | NoHaddockParse deriving (Eq,Ord,Show,Enum) data IdePreprocessedSource = IdePreprocessedSource - { preprocWarnings :: [(GHC.SrcSpan, String)] + { preprocWarnings :: [(GHC.SrcSpan, String)] -- TODO: Future work could we make these warnings structured as well? -- ^ Warnings emitted by the preprocessor. - , preprocErrors :: [(GHC.SrcSpan, String)] + , preprocErrors :: [(GHC.SrcSpan, String)] -- TODO: Future work could we make these errors structured as well? -- ^ Errors emitted by the preprocessor. , preprocSource :: GHC.ParsedSource -- ^ New parse tree emitted by the preprocessor. diff --git a/ghcide/src/Development/IDE/Types/Shake.hs b/ghcide/src/Development/IDE/Types/Shake.hs index 1ebf9e125f..cc8f84e3b6 100644 --- a/ghcide/src/Development/IDE/Types/Shake.hs +++ b/ghcide/src/Development/IDE/Types/Shake.hs @@ -1,20 +1,18 @@ -{-# LANGUAGE DerivingStrategies #-} -{-# LANGUAGE ExistentialQuantification #-} -{-# LANGUAGE PatternSynonyms #-} -{-# LANGUAGE RankNTypes #-} -{-# LANGUAGE TypeFamilies #-} +{-# LANGUAGE DerivingStrategies #-} +{-# LANGUAGE PatternSynonyms #-} +{-# LANGUAGE TypeFamilies #-} module Development.IDE.Types.Shake ( Q (..), A (..), Value (..), ValueWithDiagnostics (..), Values, - Key (..), + Key, BadDependency (..), ShakeValue(..), currentValue, isBadDependency, - toShakeValue,encodeShakeValue,decodeShakeValue,toKey,toNoFileKey,fromKey,fromKeyType,WithHieDb) + toShakeValue,encodeShakeValue,decodeShakeValue,toKey,toNoFileKey,fromKey,fromKeyType,WithHieDb,WithHieDbShield(..)) where import Control.DeepSeq @@ -26,7 +24,8 @@ import Data.Typeable (cast) import Data.Vector (Vector) import Development.IDE.Core.PositionMapping import Development.IDE.Core.RuleTypes (FileVersion) -import Development.IDE.Graph (Key (..), RuleResult, newKey) +import Development.IDE.Graph (Key, RuleResult, newKey, + pattern Key) import qualified Development.IDE.Graph as Shake import Development.IDE.Types.Diagnostics import Development.IDE.Types.Location @@ -34,15 +33,17 @@ import GHC.Generics import HieDb.Types (HieDb) import qualified StmContainers.Map as STM import Type.Reflection (SomeTypeRep (SomeTypeRep), - pattern App, pattern Con, - typeOf, typeRep, - typeRepTyCon) -import Unsafe.Coerce (unsafeCoerce) + eqTypeRep, pattern App, + type (:~~:) (HRefl), + typeOf, typeRep) -- | Intended to represent HieDb calls wrapped with (currently) retry -- functionality type WithHieDb = forall a. (HieDb -> IO a) -> IO a +-- used to smuggle RankNType WithHieDb through dbMVar +newtype WithHieDbShield = WithHieDbShield WithHieDb + data Value v = Succeeded (Maybe FileVersion) v | Stale (Maybe PositionDelta) (Maybe FileVersion) v @@ -84,11 +85,12 @@ fromKey (Key k) -- | fromKeyType (Q (k,f)) = (typeOf k, f) fromKeyType :: Key -> Maybe (SomeTypeRep, NormalizedFilePath) -fromKeyType (Key k) = case typeOf k of - App (Con tc) a | tc == typeRepTyCon (typeRep @Q) - -> case unsafeCoerce k of - Q (_ :: (), f) -> Just (SomeTypeRep a, f) - _ -> Nothing +fromKeyType (Key k) + | App tc a <- typeOf k + , Just HRefl <- tc `eqTypeRep` (typeRep @Q) + , Q (_, f) <- k + = Just (SomeTypeRep a, f) + | otherwise = Nothing toNoFileKey :: (Show k, Typeable k, Eq k, Hashable k) => k -> Key toNoFileKey k = newKey $ Q (k, emptyFilePath) @@ -99,13 +101,11 @@ newtype Q k = Q (k, NormalizedFilePath) instance Show k => Show (Q k) where show (Q (k, file)) = show k ++ "; " ++ fromNormalizedFilePath file --- | Invariant: the 'v' must be in normal form (fully evaluated). +-- | Invariant: the @v@ must be in normal form (fully evaluated). -- Otherwise we keep repeatedly 'rnf'ing values taken from the Shake database newtype A v = A (Value v) deriving Show -instance NFData (A v) where rnf (A v) = v `seq` () - -- In the Shake database we only store one type of key/result pairs, -- namely Q (question) / A (answer). type instance RuleResult (Q k) = A (RuleResult k) diff --git a/ghcide/src/Generics/SYB/GHC.hs b/ghcide/src/Generics/SYB/GHC.hs index f0d600c87d..10ab699633 100644 --- a/ghcide/src/Generics/SYB/GHC.hs +++ b/ghcide/src/Generics/SYB/GHC.hs @@ -1,5 +1,4 @@ {-# LANGUAGE DerivingVia #-} -{-# LANGUAGE RankNTypes #-} -- | Custom SYB traversals explicitly designed for operating over the GHC AST. module Generics.SYB.GHC diff --git a/ghcide/src/Text/Fuzzy/Parallel.hs b/ghcide/src/Text/Fuzzy/Parallel.hs index 13039e1e55..4d7a1d67e0 100644 --- a/ghcide/src/Text/Fuzzy/Parallel.hs +++ b/ghcide/src/Text/Fuzzy/Parallel.hs @@ -89,8 +89,7 @@ simpleFilter :: Int -- ^ Chunk size. 1000 works well. -> T.Text -- ^ Pattern to look for. -> [T.Text] -- ^ List of texts to check. -> [Scored T.Text] -- ^ The ones that match. -simpleFilter chunk maxRes pattern xs = - filter chunk maxRes pattern xs id +simpleFilter chunk maxRes pat xs = filter chunk maxRes pat xs id -- | The function to filter a list of values by fuzzy search on the text extracted from them, @@ -104,15 +103,15 @@ filter' :: Int -- ^ Chunk size. 1000 works well. -- ^ Custom scoring function to use for calculating how close words are -- When the function returns Nothing, this means the values are incomparable. -> [Scored t] -- ^ The list of results, sorted, highest score first. -filter' chunkSize maxRes pattern ts extract match' = partialSortByAscScore maxRes perfectScore (concat vss) +filter' chunkSize maxRes pat ts extract match' = partialSortByAscScore maxRes perfectScore (concat vss) where -- Preserve case for the first character, make all others lowercase - pattern' = case T.uncons pattern of + pat' = case T.uncons pat of Just (c, rest) -> T.cons c (T.toLower rest) - _ -> pattern - vss = map (mapMaybe (\t -> flip Scored t <$> match' pattern' (extract t))) (chunkList chunkSize ts) + _ -> pat + vss = map (mapMaybe (\t -> flip Scored t <$> match' pat' (extract t))) (chunkList chunkSize ts) `using` parList (evalList rseq) - perfectScore = fromMaybe (error $ T.unpack pattern) $ match' pattern' pattern' + perfectScore = fromMaybe (error $ T.unpack pat) $ match' pat' pat' -- | The function to filter a list of values by fuzzy search on the text extracted from them, -- using a custom matching function which determines how close words are. @@ -122,8 +121,8 @@ filter :: Int -- ^ Chunk size. 1000 works well. -> [t] -- ^ The list of values containing the text to search in. -> (t -> T.Text) -- ^ The function to extract the text from the container. -> [Scored t] -- ^ The list of results, sorted, highest score first. -filter chunkSize maxRes pattern ts extract = - filter' chunkSize maxRes pattern ts extract match +filter chunkSize maxRes pat ts extract = + filter' chunkSize maxRes pat ts extract match -- | Return all elements of the list that have a fuzzy match against the pattern, -- the closeness of the match is determined using the custom scoring match function that is passed. @@ -136,8 +135,8 @@ simpleFilter' :: Int -- ^ Chunk size. 1000 works well. -> (T.Text -> T.Text -> Maybe Int) -- ^ Custom scoring function to use for calculating how close words are -> [Scored T.Text] -- ^ The ones that match. -simpleFilter' chunk maxRes pattern xs match' = - filter' chunk maxRes pattern xs id match' +simpleFilter' chunk maxRes pat xs match' = + filter' chunk maxRes pat xs id match' -------------------------------------------------------------------------------- chunkList :: Int -> [a] -> [[a]] diff --git a/ghcide/test/cabal/Development/IDE/Test/Runfiles.hs b/ghcide/test/cabal/Development/IDE/Test/Runfiles.hs deleted file mode 100644 index 83b7e8c368..0000000000 --- a/ghcide/test/cabal/Development/IDE/Test/Runfiles.hs +++ /dev/null @@ -1,9 +0,0 @@ --- Copyright (c) 2019 The DAML Authors. All rights reserved. --- SPDX-License-Identifier: Apache-2.0 - -module Development.IDE.Test.Runfiles - ( locateGhcideExecutable - ) where - -locateGhcideExecutable :: IO FilePath -locateGhcideExecutable = pure "ghcide" diff --git a/ghcide/test/data/plugin-recorddot/RecordDot.hs b/ghcide/test/data/plugin-recorddot/RecordDot.hs deleted file mode 100644 index a0e30599e9..0000000000 --- a/ghcide/test/data/plugin-recorddot/RecordDot.hs +++ /dev/null @@ -1,6 +0,0 @@ -{-# LANGUAGE DuplicateRecordFields, TypeApplications, TypeFamilies, UndecidableInstances, FlexibleContexts, DataKinds, MultiParamTypeClasses, TypeSynonymInstances, FlexibleInstances #-} -{-# OPTIONS_GHC -fplugin=RecordDotPreprocessor #-} -module RecordDot (Company(..), display) where -data Company = Company {name :: String} -display :: Company -> String -display c = c.name diff --git a/ghcide/test/data/plugin-recorddot/plugin.cabal b/ghcide/test/data/plugin-recorddot/plugin.cabal deleted file mode 100644 index bd85313914..0000000000 --- a/ghcide/test/data/plugin-recorddot/plugin.cabal +++ /dev/null @@ -1,9 +0,0 @@ -cabal-version: 1.18 -name: plugin -version: 1.0.0 -build-type: Simple - -library - build-depends: base, record-dot-preprocessor, record-hasfield - exposed-modules: RecordDot - hs-source-dirs: . diff --git a/ghcide/test/exe/CodeLensTests.hs b/ghcide/test/exe/CodeLensTests.hs deleted file mode 100644 index 818e6953d5..0000000000 --- a/ghcide/test/exe/CodeLensTests.hs +++ /dev/null @@ -1,122 +0,0 @@ -{-# LANGUAGE GADTs #-} - -module CodeLensTests (tests) where - -import Control.Applicative.Combinators -import Control.Lens ((^.)) -import Control.Monad (void) -import Control.Monad.IO.Class (liftIO) -import qualified Data.Aeson as A -import Data.Maybe -import qualified Data.Text as T -import Data.Tuple.Extra -import Development.IDE.GHC.Compat (GhcVersion (..), ghcVersion) -import qualified Language.LSP.Protocol.Lens as L -import Language.LSP.Protocol.Message -import Language.LSP.Protocol.Types hiding - (SemanticTokenAbsolute (..), - SemanticTokenRelative (..), - SemanticTokensEdit (..), - mkRange) -import Language.LSP.Test -import Test.Tasty -import Test.Tasty.HUnit -import TestUtils - -tests :: TestTree -tests = testGroup "code lenses" - [ addSigLensesTests - ] - - -addSigLensesTests :: TestTree -addSigLensesTests = - let pragmas = "{-# OPTIONS_GHC -Wmissing-signatures -Wmissing-pattern-synonym-signatures #-}" - moduleH exported = - T.unlines - [ "{-# LANGUAGE PatternSynonyms,TypeApplications,DataKinds,RankNTypes,ScopedTypeVariables,TypeOperators,GADTs,BangPatterns #-}" - , "module Sigs(" <> exported <> ") where" - , "import qualified Data.Complex as C" - , "import Data.Data (Proxy (..), type (:~:) (..), mkCharType)" - , "data T1 a where" - , " MkT1 :: (Show b) => a -> b -> T1 a" - ] - before enableGHCWarnings exported (def, _) others = - T.unlines $ [pragmas | enableGHCWarnings] <> [moduleH exported, def] <> others - after' enableGHCWarnings exported (def, sig) others = - T.unlines $ [pragmas | enableGHCWarnings] <> [moduleH exported] <> maybe [] pure sig <> [def] <> others - createConfig mode = A.object ["plugin" A..= A.object ["ghcide-type-lenses" A..= A.object ["config" A..= A.object ["mode" A..= A.String mode]]]] - sigSession testName enableGHCWarnings waitForDiags mode exported def others = testSession testName $ do - let originalCode = before enableGHCWarnings exported def others - let expectedCode = after' enableGHCWarnings exported def others - setConfigSection "haskell" (createConfig mode) - doc <- createDoc "Sigs.hs" "haskell" originalCode - -- Because the diagnostics mode is really relying only on diagnostics now - -- to generate the code lens we need to make sure we wait till the file - -- is parsed before asking for codelenses, otherwise we will get nothing. - if waitForDiags - then void waitForDiagnostics - else waitForProgressDone - codeLenses <- getAndResolveCodeLenses doc - if not $ null $ snd def - then do - liftIO $ length codeLenses == 1 @? "Expected 1 code lens, but got: " <> show codeLenses - executeCommand $ fromJust $ head codeLenses ^. L.command - modifiedCode <- skipManyTill anyMessage (getDocumentEdit doc) - liftIO $ expectedCode @=? modifiedCode - else liftIO $ null codeLenses @? "Expected no code lens, but got: " <> show codeLenses - cases = - [ ("abc = True", "abc :: Bool") - , ("foo a b = a + b", "foo :: Num a => a -> a -> a") - , ("bar a b = show $ a + b", "bar :: (Show a, Num a) => a -> a -> String") - , ("(!!!) a b = a > b", "(!!!) :: Ord a => a -> a -> Bool") - , ("a >>>> b = a + b", "(>>>>) :: Num a => a -> a -> a") - , ("a `haha` b = a b", "haha :: (t1 -> t2) -> t1 -> t2") - , ("pattern Some a = Just a", "pattern Some :: a -> Maybe a") - , ("pattern Some a <- Just a", "pattern Some :: a -> Maybe a") - , ("pattern Some a <- Just a\n where Some a = Just a", "pattern Some :: a -> Maybe a") - , ("pattern Some a <- Just !a\n where Some !a = Just a", "pattern Some :: a -> Maybe a") - , ("pattern Point{x, y} = (x, y)", "pattern Point :: a -> b -> (a, b)") - , ("pattern Point{x, y} <- (x, y)", "pattern Point :: a -> b -> (a, b)") - , ("pattern Point{x, y} <- (x, y)\n where Point x y = (x, y)", "pattern Point :: a -> b -> (a, b)") - , ("pattern MkT1' b = MkT1 42 b", "pattern MkT1' :: (Eq a, Num a) => Show b => b -> T1 a") - , ("pattern MkT1' b <- MkT1 42 b", "pattern MkT1' :: (Eq a, Num a) => Show b => b -> T1 a") - , ("pattern MkT1' b <- MkT1 42 b\n where MkT1' b = MkT1 42 b", "pattern MkT1' :: (Eq a, Num a) => Show b => b -> T1 a") - , ("qualifiedSigTest= C.realPart", "qualifiedSigTest :: C.Complex a -> a") - , ("head = 233", "head :: Integer") - , ("rank2Test (k :: forall a . a -> a) = (k 233 :: Int, k \"QAQ\")", "rank2Test :: (forall a. a -> a) -> (Int, " <> listOfChar <> ")") - , ("symbolKindTest = Proxy @\"qwq\"", "symbolKindTest :: Proxy \"qwq\"") - , ("promotedKindTest = Proxy @Nothing", if ghcVersion >= GHC96 then "promotedKindTest :: Proxy Nothing" else "promotedKindTest :: Proxy 'Nothing") - , ("typeOperatorTest = Refl", if ghcVersion >= GHC92 then "typeOperatorTest :: forall {k} {a :: k}. a :~: a" else "typeOperatorTest :: a :~: a") - , ("notInScopeTest = mkCharType", "notInScopeTest :: String -> Data.Data.DataType") - , ("aVeryLongSignature a b c d e f g h i j k l m n = a && b && c && d && e && f && g && h && i && j && k && l && m && n", "aVeryLongSignature :: Bool -> Bool -> Bool -> Bool -> Bool -> Bool -> Bool -> Bool -> Bool -> Bool -> Bool -> Bool -> Bool -> Bool -> Bool") - ] - in testGroup - "add signature" - [ testGroup "signatures are correct" [sigSession (T.unpack $ T.replace "\n" "\\n" def) False False "always" "" (def, Just sig) [] | (def, sig) <- cases] - , sigSession "exported mode works" False False "exported" "xyz" ("xyz = True", Just "xyz :: Bool") (fst <$> take 3 cases) - , testGroup - "diagnostics mode works" - [ sigSession "with GHC warnings" True True "diagnostics" "" (second Just $ head cases) [] - , sigSession "without GHC warnings" False False "diagnostics" "" (second (const Nothing) $ head cases) [] - ] - , testSession "keep stale lens" $ do - let content = T.unlines - [ "module Stale where" - , "f = _" - ] - doc <- createDoc "Stale.hs" "haskell" content - oldLens <- getCodeLenses doc - liftIO $ length oldLens @?= 1 - let edit = TextEdit (mkRange 0 4 0 5) "" -- Remove the `_` - _ <- applyEdit doc edit - newLens <- getCodeLenses doc - liftIO $ newLens @?= oldLens - ] - --- | Before ghc9, lists of Char is displayed as [Char], but with ghc9 and up, it's displayed as String -listOfChar :: T.Text -listOfChar | ghcVersion >= GHC90 = "String" - | otherwise = "[Char]" - - diff --git a/ghcide/test/exe/FindDefinitionAndHoverTests.hs b/ghcide/test/exe/FindDefinitionAndHoverTests.hs deleted file mode 100644 index 9627546ac8..0000000000 --- a/ghcide/test/exe/FindDefinitionAndHoverTests.hs +++ /dev/null @@ -1,249 +0,0 @@ - -{-# LANGUAGE MultiWayIf #-} - -module FindDefinitionAndHoverTests (tests) where - -import Control.Monad -import Control.Monad.IO.Class (liftIO) -import Data.Foldable -import Data.Maybe -import qualified Data.Text as T -import Development.IDE.GHC.Compat (GhcVersion (..), ghcVersion) -import Development.IDE.GHC.Util -import Development.IDE.Test (expectDiagnostics, - standardizeQuotes) -import Development.IDE.Types.Location -import qualified Language.LSP.Protocol.Lens as L -import Language.LSP.Protocol.Types hiding - (SemanticTokenAbsolute (..), - SemanticTokenRelative (..), - SemanticTokensEdit (..), - mkRange) -import Language.LSP.Test -import System.FilePath -import System.Info.Extra (isWindows) --- import Test.QuickCheck.Instances () -import Control.Lens ((^.)) -import Test.Tasty -import Test.Tasty.HUnit -import TestUtils -import Text.Regex.TDFA ((=~)) - -tests :: TestTree -tests = let - - tst :: (TextDocumentIdentifier -> Position -> Session a, a -> Session [Expect] -> Session ()) -> Position -> String -> Session [Expect] -> String -> TestTree - tst (get, check) pos sfp targetRange title = testSessionWithExtraFiles "hover" title $ \dir -> do - - -- Dirty the cache to check that definitions work even in the presence of iface files - liftIO $ runInDir dir $ do - let fooPath = dir "Foo.hs" - fooSource <- liftIO $ readFileUtf8 fooPath - fooDoc <- createDoc fooPath "haskell" fooSource - _ <- getHover fooDoc $ Position 4 3 - closeDoc fooDoc - - doc <- openTestDataDoc (dir sfp) - waitForProgressDone - found <- get doc pos - check found targetRange - - - - checkHover :: Maybe Hover -> Session [Expect] -> Session () - checkHover hover expectations = traverse_ check =<< expectations where - - check expected = - case hover of - Nothing -> unless (expected == ExpectNoHover) $ liftIO $ assertFailure "no hover found" - Just Hover{_contents = (InL MarkupContent{_value = standardizeQuotes -> msg}) - ,_range = rangeInHover } -> - case expected of - ExpectRange expectedRange -> checkHoverRange expectedRange rangeInHover msg - ExpectHoverRange expectedRange -> checkHoverRange expectedRange rangeInHover msg - ExpectHoverText snippets -> liftIO $ traverse_ (`assertFoundIn` msg) snippets - ExpectHoverExcludeText snippets -> liftIO $ traverse_ (`assertNotFoundIn` msg) snippets - ExpectHoverTextRegex re -> liftIO $ assertBool ("Regex not found in " <> T.unpack msg) (msg =~ re :: Bool) - ExpectNoHover -> liftIO $ assertFailure $ "Expected no hover but got " <> show hover - _ -> pure () -- all other expectations not relevant to hover - _ -> liftIO $ assertFailure $ "test not expecting this kind of hover info" <> show hover - - extractLineColFromHoverMsg :: T.Text -> [T.Text] - extractLineColFromHoverMsg = T.splitOn ":" . head . T.splitOn "*" . last . T.splitOn (sourceFileName <> ":") - - checkHoverRange :: Range -> Maybe Range -> T.Text -> Session () - checkHoverRange expectedRange rangeInHover msg = - let - lineCol = extractLineColFromHoverMsg msg - -- looks like hovers use 1-based numbering while definitions use 0-based - -- turns out that they are stored 1-based in RealSrcLoc by GHC itself. - adjust Position{_line = l, _character = c} = - Position{_line = l + 1, _character = c + 1} - in - case map (read . T.unpack) lineCol of - [l,c] -> liftIO $ adjust (expectedRange ^. L.start) @=? Position l c - _ -> liftIO $ assertFailure $ - "expected: " <> show ("[...]" <> sourceFileName <> "::**[...]", Just expectedRange) <> - "\n but got: " <> show (msg, rangeInHover) - - assertFoundIn :: T.Text -> T.Text -> Assertion - assertFoundIn part whole = assertBool - (T.unpack $ "failed to find: `" <> part <> "` in hover message:\n" <> whole) - (part `T.isInfixOf` whole) - - assertNotFoundIn :: T.Text -> T.Text -> Assertion - assertNotFoundIn part whole = assertBool - (T.unpack $ "found unexpected: `" <> part <> "` in hover message:\n" <> whole) - (not . T.isInfixOf part $ whole) - - sourceFilePath = T.unpack sourceFileName - sourceFileName = "GotoHover.hs" - - mkFindTests tests = testGroup "get" - [ testGroup "definition" $ mapMaybe fst tests - , testGroup "hover" $ mapMaybe snd tests - , checkFileCompiles sourceFilePath $ - expectDiagnostics - [ ( "GotoHover.hs", [(DiagnosticSeverity_Error, (62, 7), "Found hole: _")]) - , ( "GotoHover.hs", [(DiagnosticSeverity_Error, (65, 8), "Found hole: _")]) - ] - , testGroup "type-definition" typeDefinitionTests - , testGroup "hover-record-dot-syntax" recordDotSyntaxTests ] - - typeDefinitionTests = [ tst (getTypeDefinitions, checkDefs) aaaL14 sourceFilePath (pure tcData) "Saturated data con" - , tst (getTypeDefinitions, checkDefs) aL20 sourceFilePath (pure [ExpectNoDefinitions]) "Polymorphic variable"] - - recordDotSyntaxTests - | ghcVersion >= GHC92 = - [ tst (getHover, checkHover) (Position 17 24) (T.unpack "RecordDotSyntax.hs") (pure [ExpectHoverText ["x :: MyRecord"]]) "hover over parent" - , tst (getHover, checkHover) (Position 17 25) (T.unpack "RecordDotSyntax.hs") (pure [ExpectHoverText ["_ :: MyChild"]]) "hover over dot shows child" - , tst (getHover, checkHover) (Position 17 26) (T.unpack "RecordDotSyntax.hs") (pure [ExpectHoverText ["_ :: MyChild"]]) "hover over child" - ] - | otherwise = [] - - test runDef runHover look expect = testM runDef runHover look (return expect) - - testM runDef runHover look expect title = - ( runDef $ tst def look sourceFilePath expect title - , runHover $ tst hover look sourceFilePath expect title ) where - def = (getDefinitions, checkDefs) - hover = (getHover , checkHover) - - -- search locations expectations on results - fffL4 = fffR ^. L.start; fffR = mkRange 8 4 8 7 ; fff = [ExpectRange fffR] - fffL8 = Position 12 4 ; - fffL14 = Position 18 7 ; - aL20 = Position 19 15 - aaaL14 = Position 18 20 ; aaa = [mkR 11 0 11 3] - dcL7 = Position 11 11 ; tcDC = [mkR 7 23 9 16] - dcL12 = Position 16 11 ; - xtcL5 = Position 9 11 ; xtc = [ExpectExternFail, ExpectHoverText ["Int", "Defined in ", "GHC.Types", "ghc-prim"]] - tcL6 = Position 10 11 ; tcData = [mkR 7 0 9 16, ExpectHoverText ["TypeConstructor", "GotoHover.hs:8:1"]] - vvL16 = Position 20 12 ; vv = [mkR 20 4 20 6] - opL16 = Position 20 15 ; op = [mkR 21 2 21 4] - opL18 = Position 22 22 ; opp = [mkR 22 13 22 17] - aL18 = Position 22 20 ; apmp = [mkR 22 10 22 11] - b'L19 = Position 23 13 ; bp = [mkR 23 6 23 7] - xvL20 = Position 24 8 ; xvMsg = [ExpectExternFail, ExpectHoverText ["pack", ":: String -> Text", "Data.Text", "text"]] - clL23 = Position 27 11 ; cls = [mkR 25 0 26 20, ExpectHoverText ["MyClass", "GotoHover.hs:26:1"]] - clL25 = Position 29 9 - eclL15 = Position 19 8 ; ecls = [ExpectExternFail, ExpectHoverText ["Num", "Defined in ", "GHC.Num", "base"]] - dnbL29 = Position 33 18 ; dnb = [ExpectHoverText [":: ()"], mkR 33 12 33 21] - dnbL30 = Position 34 23 - lcbL33 = Position 37 26 ; lcb = [ExpectHoverText [":: Char"], mkR 37 26 37 27] - lclL33 = Position 37 22 - mclL36 = Position 40 1 ; mcl = [mkR 40 0 40 14] - mclL37 = Position 41 1 - spaceL37 = Position 41 24 ; space = [ExpectNoDefinitions, ExpectHoverText [":: Char"]] - docL41 = Position 45 1 ; doc = [ExpectHoverText ["Recognizable docs: kpqz"]] - ; constr = [ExpectHoverText ["Monad m"]] - eitL40 = Position 44 28 ; kindE = [ExpectHoverText [if ghcVersion >= GHC92 then ":: Type -> Type -> Type\n" else ":: * -> * -> *\n"]] - intL40 = Position 44 34 ; kindI = [ExpectHoverText [if ghcVersion >= GHC92 then ":: Type\n" else ":: *\n"]] - tvrL40 = Position 44 37 ; kindV = [ExpectHoverText [":: * -> *\n"]] - intL41 = Position 45 20 ; litI = [ExpectHoverText ["7518"]] - chrL36 = Position 41 24 ; litC = [ExpectHoverText ["'f'"]] - txtL8 = Position 12 14 ; litT = [ExpectHoverText ["\"dfgy\""]] - lstL43 = Position 47 12 ; litL = [ExpectHoverText ["[8391 :: Int, 6268]"]] - outL45 = Position 49 3 ; outSig = [ExpectHoverText ["outer", "Bool"], mkR 50 0 50 5] - innL48 = Position 52 5 ; innSig = [ExpectHoverText ["inner", "Char"], mkR 49 2 49 7] - holeL60 = Position 62 7 ; hleInfo = [ExpectHoverText ["_ ::"]] - holeL65 = Position 65 8 ; hleInfo2 = [ExpectHoverText ["_ :: a -> Maybe a"]] - cccL17 = Position 17 16 ; docLink = [ExpectHoverTextRegex "\\*Defined in 'GHC.Types'\\* \\*\\(ghc-prim-[0-9.]+\\)\\*\n\n"] - imported = Position 56 13 ; importedSig = getDocUri "Foo.hs" >>= \foo -> return [ExpectHoverText ["foo", "Foo", "Haddock"], mkL foo 5 0 5 3] - reexported = Position 55 14 ; reexportedSig = getDocUri "Bar.hs" >>= \bar -> return [ExpectHoverText ["Bar", "Bar", "Haddock"], mkL bar 3 (if ghcVersion >= GHC94 then 5 else 0) 3 (if ghcVersion >= GHC94 then 8 else 14)] - thLocL57 = Position 59 10 ; thLoc = [ExpectHoverText ["Identity"]] - cmtL68 = Position 67 0 ; lackOfdEq = [ExpectHoverExcludeText ["$dEq"]] - import310 = Position 3 10; pkgTxt = [ExpectHoverText ["Data.Text\n\ntext-"]] - in - mkFindTests - -- def hover look expect - [ - if ghcVersion >= GHC90 then - -- It suggests either going to the constructor or to the field - test broken yes fffL4 fff "field in record definition" - else - test yes yes fffL4 fff "field in record definition" - , test yes yes fffL8 fff "field in record construction #1102" - , test yes yes fffL14 fff "field name used as accessor" -- https://github.com/haskell/ghcide/pull/120 in Calculate.hs - , test yes yes aaaL14 aaa "top-level name" -- https://github.com/haskell/ghcide/pull/120 - , test yes yes dcL7 tcDC "data constructor record #1029" - , test yes yes dcL12 tcDC "data constructor plain" -- https://github.com/haskell/ghcide/pull/121 - , test yes yes tcL6 tcData "type constructor #1028" -- https://github.com/haskell/ghcide/pull/147 - , test broken yes xtcL5 xtc "type constructor external #717,1028" - , test broken yes xvL20 xvMsg "value external package #717" -- https://github.com/haskell/ghcide/pull/120 - , test yes yes vvL16 vv "plain parameter" -- https://github.com/haskell/ghcide/pull/120 - , test yes yes aL18 apmp "pattern match name" -- https://github.com/haskell/ghcide/pull/120 - , test yes yes opL16 op "top-level operator #713" -- https://github.com/haskell/ghcide/pull/120 - , test yes yes opL18 opp "parameter operator" -- https://github.com/haskell/ghcide/pull/120 - , test yes yes b'L19 bp "name in backticks" -- https://github.com/haskell/ghcide/pull/120 - , test yes yes clL23 cls "class in instance declaration #1027" - , test yes yes clL25 cls "class in signature #1027" -- https://github.com/haskell/ghcide/pull/147 - , test broken yes eclL15 ecls "external class in signature #717,1027" - , test yes yes dnbL29 dnb "do-notation bind #1073" - , test yes yes dnbL30 dnb "do-notation lookup" - , test yes yes lcbL33 lcb "listcomp bind #1073" - , test yes yes lclL33 lcb "listcomp lookup" - , test yes yes mclL36 mcl "top-level fn 1st clause" - , test yes yes mclL37 mcl "top-level fn 2nd clause #1030" - , test yes yes spaceL37 space "top-level fn on space #1002" - , test no yes docL41 doc "documentation #1129" - , test no yes eitL40 kindE "kind of Either #1017" - , test no yes intL40 kindI "kind of Int #1017" - , test no broken tvrL40 kindV "kind of (* -> *) type variable #1017" - , test no broken intL41 litI "literal Int in hover info #1016" - , test no broken chrL36 litC "literal Char in hover info #1016" - , test no broken txtL8 litT "literal Text in hover info #1016" - , test no broken lstL43 litL "literal List in hover info #1016" - , test yes yes cmtL68 lackOfdEq "no Core symbols #3280" - , if ghcVersion >= GHC90 then - test no yes docL41 constr "type constraint in hover info #1012" - else - test no broken docL41 constr "type constraint in hover info #1012" - , test no yes outL45 outSig "top-level signature #767" - , test broken broken innL48 innSig "inner signature #767" - , test no yes holeL60 hleInfo "hole without internal name #831" - , test no yes holeL65 hleInfo2 "hole with variable" - , test no yes cccL17 docLink "Haddock html links" - , testM yes yes imported importedSig "Imported symbol" - , if | isWindows -> - -- Flaky on Windows: https://github.com/haskell/haskell-language-server/issues/2997 - testM no yes reexported reexportedSig "Imported symbol (reexported)" - | otherwise -> - testM yes yes reexported reexportedSig "Imported symbol (reexported)" - , if | ghcVersion == GHC90 && isWindows -> - test no broken thLocL57 thLoc "TH Splice Hover" - | otherwise -> - test no yes thLocL57 thLoc "TH Splice Hover" - , test yes yes import310 pkgTxt "show package name and its version" - ] - where yes, broken :: (TestTree -> Maybe TestTree) - yes = Just -- test should run and pass - broken = Just . (`xfail` "known broken") - no = const Nothing -- don't run this test at all - skip = const Nothing -- unreliable, don't run - -checkFileCompiles :: FilePath -> Session () -> TestTree -checkFileCompiles fp diag = - testSessionWithExtraFiles "hover" ("Does " ++ fp ++ " compile") $ \dir -> do - void (openTestDataDoc (dir fp)) - diag diff --git a/ghcide/test/exe/FuzzySearch.hs b/ghcide/test/exe/FuzzySearch.hs deleted file mode 100644 index f565b94526..0000000000 --- a/ghcide/test/exe/FuzzySearch.hs +++ /dev/null @@ -1,129 +0,0 @@ -module FuzzySearch (tests) where - -import Data.Char (toLower) -import Data.Maybe (catMaybes) -import qualified Data.Monoid.Textual as T -import Data.Text (Text, inits, pack) -import qualified Data.Text as Text -import Prelude hiding (filter) -import System.Directory (doesFileExist) -import System.IO.Unsafe (unsafePerformIO) -import Test.QuickCheck -import Test.Tasty -import Test.Tasty.ExpectedFailure -import Test.Tasty.QuickCheck (testProperty) -import qualified Text.Fuzzy as Fuzzy -import Text.Fuzzy (Fuzzy (..)) -import Text.Fuzzy.Parallel - -tests :: TestTree -tests = - testGroup - "Fuzzy search" - [ needDictionary $ - testGroup - "match works as expected on the english dictionary" - [ testProperty "for legit words" propLegit, - testProperty "for prefixes" propPrefix, - testProperty "for typos" propTypo - ] - ] - -test :: Text -> Bool -test candidate = do - let previous = - catMaybes - [ (d,) . Fuzzy.score - <$> referenceImplementation candidate d "" "" id - | d <- dictionary - ] - new = catMaybes [(d,) <$> match candidate d | d <- dictionary] - previous == new - -propLegit :: Property -propLegit = forAll (elements dictionary) test - -propPrefix :: Property -propPrefix = forAll (elements dictionary >>= elements . inits) test - -propTypo :: Property -propTypo = forAll typoGen test - -typoGen :: Gen Text -typoGen = do - w <- elements dictionary - l <- elements [0 .. Text.length w -1] - let wl = Text.index w l - c <- elements [ c | c <- ['a' .. 'z'], c /= wl] - return $ replaceAt w l c - -replaceAt :: Text -> Int -> Char -> Text -replaceAt t i c = - let (l, r) = Text.splitAt i t - in l <> Text.singleton c <> r - -dictionaryPath :: FilePath -dictionaryPath = "/usr/share/dict/words" - -{-# NOINLINE dictionary #-} -dictionary :: [Text] -dictionary = unsafePerformIO $ do - existsDictionary <- doesFileExist dictionaryPath - if existsDictionary - then map pack . words <$> readFile dictionaryPath - else pure [] - -referenceImplementation :: - (T.TextualMonoid s) => - -- | Pattern in lowercase except for first character - s -> - -- | The value containing the text to search in. - t -> - -- | The text to add before each match. - s -> - -- | The text to add after each match. - s -> - -- | The function to extract the text from the container. - (t -> s) -> - -- | The original value, rendered string and score. - Maybe (Fuzzy t s) -referenceImplementation pattern t pre post extract = - if null pat then Just (Fuzzy t result totalScore) else Nothing - where - null :: (T.TextualMonoid s) => s -> Bool - null = not . T.any (const True) - - s = extract t - (totalScore, _currScore, result, pat, _) = - T.foldl' - undefined - ( \(tot, cur, res, pat, isFirst) c -> - case T.splitCharacterPrefix pat of - Nothing -> (tot, 0, res <> T.singleton c, pat, isFirst) - Just (x, xs) -> - -- the case of the first character has to match - -- otherwise use lower case since the pattern is assumed lower - let !c' = if isFirst then c else toLower c - in if x == c' - then - let cur' = cur * 2 + 1 - in ( tot + cur', - cur', - res <> pre <> T.singleton c <> post, - xs, - False - ) - else (tot, 0, res <> T.singleton c, pat, isFirst) - ) - ( 0, - 1, -- matching at the start gives a bonus (cur = 1) - mempty, - pattern, - True - ) - s - -needDictionary :: TestTree -> TestTree -needDictionary - | null dictionary = ignoreTestBecause ("not found: " <> dictionaryPath) - | otherwise = id diff --git a/ghcide/test/exe/Main.hs b/ghcide/test/exe/Main.hs deleted file mode 100644 index 18296dce16..0000000000 --- a/ghcide/test/exe/Main.hs +++ /dev/null @@ -1,127 +0,0 @@ --- Copyright (c) 2019 The DAML Authors. All rights reserved. --- SPDX-License-Identifier: Apache-2.0 - -{- - NOTE On enforcing determinism - - The tests below use two mechanisms to enforce deterministic LSP sequences: - - 1. Progress reporting: waitForProgress(Begin|Done) - 2. Diagnostics: expectDiagnostics - - Either is fine, but diagnostics are generally more reliable. - - Mixing them both in the same test is NOT FINE as it will introduce race - conditions since multiple interleavings are possible. In other words, - the sequence of diagnostics and progress reports is not deterministic. - For example: - - < do something > - waitForProgressDone - expectDiagnostics [...] - - - When the diagnostics arrive after the progress done message, as they usually do, the test will pass - - When the diagnostics arrive before the progress done msg, when on a slow machine occasionally, the test will timeout - - Therefore, avoid mixing both progress reports and diagnostics in the same test - -} - - - -module Main (main) where --- import Test.QuickCheck.Instances () -import Data.Function ((&)) -import Ide.Logger (Logger (Logger), - LoggingColumn (DataColumn, PriorityColumn), - Pretty (pretty), - Priority (Debug), - Recorder (Recorder, logger_), - WithPriority (WithPriority, priority), - cfilter, - cmapWithPrio, - makeDefaultStderrRecorder) -import GHC.Stack (emptyCallStack) -import qualified HieDbRetry -import Test.Tasty -import Test.Tasty.Ingredients.Rerun - -import LogType () -import OpenCloseTest -import InitializeResponseTests -import CompletionTests -import CPPTests -import DiagnosticTests -import CodeLensTests -import OutlineTests -import HighlightTests -import FindDefinitionAndHoverTests -import PluginSimpleTests -import PluginParsedResultTests -import PreprocessorTests -import THTests -import SymlinkTests -import SafeTests -import UnitTests -import HaddockTests -import PositionMappingTests -import WatchedFileTests -import CradleTests -import DependentFileTest -import NonLspCommandLine -import IfaceTests -import BootTests -import RootUriTests -import AsyncTests -import ClientSettingsTests -import ReferenceTests -import GarbageCollectionTests -import ExceptionTests - -main :: IO () -main = do - docWithPriorityRecorder <- makeDefaultStderrRecorder (Just [PriorityColumn, DataColumn]) - - let docWithFilteredPriorityRecorder@Recorder{ logger_ } = - docWithPriorityRecorder - & cfilter (\WithPriority{ priority } -> priority >= Debug) - - -- exists so old-style logging works. intended to be phased out - let logger = Logger $ \p m -> logger_ (WithPriority p emptyCallStack (pretty m)) - - let recorder = docWithFilteredPriorityRecorder - & cmapWithPrio pretty - - -- We mess with env vars so run single-threaded. - defaultMainWithRerun $ testGroup "ghcide" - [ OpenCloseTest.tests - , InitializeResponseTests.tests - , CompletionTests.tests - , CPPTests.tests - , DiagnosticTests.tests - , CodeLensTests.tests - , OutlineTests.tests - , HighlightTests.tests - , FindDefinitionAndHoverTests.tests - , PluginSimpleTests.tests - , PluginParsedResultTests.tests - , PreprocessorTests.tests - , THTests.tests - , SymlinkTests.tests - , SafeTests.tests - , UnitTests.tests recorder logger - , HaddockTests.tests - , PositionMappingTests.tests - , WatchedFileTests.tests - , CradleTests.tests - , DependentFileTest.tests - , NonLspCommandLine.tests - , IfaceTests.tests - , BootTests.tests - , RootUriTests.tests - , AsyncTests.tests - , ClientSettingsTests.tests - , ReferenceTests.tests - , GarbageCollectionTests.tests - , HieDbRetry.tests - , ExceptionTests.tests recorder logger - ] diff --git a/ghcide/test/exe/NonLspCommandLine.hs b/ghcide/test/exe/NonLspCommandLine.hs deleted file mode 100644 index 51eeb95ea0..0000000000 --- a/ghcide/test/exe/NonLspCommandLine.hs +++ /dev/null @@ -1,27 +0,0 @@ - -module NonLspCommandLine (tests) where - -import Development.IDE.Test.Runfiles -import System.Environment.Blank (setEnv) -import System.Exit (ExitCode (ExitSuccess)) -import System.Process.Extra (CreateProcess (cwd), proc, - readCreateProcessWithExitCode) -import Test.Tasty -import Test.Tasty.HUnit -import TestUtils - - --- A test to ensure that the command line ghcide workflow stays working -tests :: TestTree -tests = testGroup "ghcide command line" - [ testCase "works" $ withTempDir $ \dir -> do - ghcide <- locateGhcideExecutable - copyTestDataFiles dir "multi" - let cmd = (proc ghcide ["a/A.hs"]){cwd = Just dir} - - setEnv "HOME" "/homeless-shelter" False - - (ec, _, _) <- readCreateProcessWithExitCode cmd "" - - ec @?= ExitSuccess - ] diff --git a/ghcide/test/exe/OutlineTests.hs b/ghcide/test/exe/OutlineTests.hs deleted file mode 100644 index 6459e1deca..0000000000 --- a/ghcide/test/exe/OutlineTests.hs +++ /dev/null @@ -1,189 +0,0 @@ - -module OutlineTests (tests) where - -import Control.Monad.IO.Class (liftIO) -import qualified Data.Text as T -import Language.LSP.Protocol.Types hiding (SemanticTokenAbsolute (..), - SemanticTokenRelative (..), - SemanticTokensEdit (..), mkRange) -import Language.LSP.Test -import Test.Tasty -import Test.Tasty.HUnit -import TestUtils - -tests :: TestTree -tests = testGroup - "outline" - [ testSessionWait "type class" $ do - let source = T.unlines ["module A where", "class A a where a :: a -> Bool"] - docId <- createDoc "A.hs" "haskell" source - symbols <- getDocumentSymbols docId - liftIO $ symbols @?= Right - [ moduleSymbol - "A" - (R 0 7 0 8) - [ classSymbol "A a" - (R 1 0 1 30) - [docSymbol' "a" SymbolKind_Method (R 1 16 1 30) (R 1 16 1 17)] - ] - ] - , testSessionWait "type class instance " $ do - let source = T.unlines ["class A a where", "instance A () where"] - docId <- createDoc "A.hs" "haskell" source - symbols <- getDocumentSymbols docId - liftIO $ symbols @?= Right - [ classSymbol "A a" (R 0 0 0 15) [] - , docSymbol "A ()" SymbolKind_Interface (R 1 0 1 19) - ] - , testSessionWait "type family" $ do - let source = T.unlines ["{-# language TypeFamilies #-}", "type family A"] - docId <- createDoc "A.hs" "haskell" source - symbols <- getDocumentSymbols docId - liftIO $ symbols @?= Right [docSymbolD "A" "type family" SymbolKind_Function (R 1 0 1 13)] - , testSessionWait "type family instance " $ do - let source = T.unlines - [ "{-# language TypeFamilies #-}" - , "type family A a" - , "type instance A () = ()" - ] - docId <- createDoc "A.hs" "haskell" source - symbols <- getDocumentSymbols docId - liftIO $ symbols @?= Right - [ docSymbolD "A a" "type family" SymbolKind_Function (R 1 0 1 15) - , docSymbol "A ()" SymbolKind_Interface (R 2 0 2 23) - ] - , testSessionWait "data family" $ do - let source = T.unlines ["{-# language TypeFamilies #-}", "data family A"] - docId <- createDoc "A.hs" "haskell" source - symbols <- getDocumentSymbols docId - liftIO $ symbols @?= Right [docSymbolD "A" "data family" SymbolKind_Function (R 1 0 1 11)] - , testSessionWait "data family instance " $ do - let source = T.unlines - [ "{-# language TypeFamilies #-}" - , "data family A a" - , "data instance A () = A ()" - ] - docId <- createDoc "A.hs" "haskell" source - symbols <- getDocumentSymbols docId - liftIO $ symbols @?= Right - [ docSymbolD "A a" "data family" SymbolKind_Function (R 1 0 1 11) - , docSymbol "A ()" SymbolKind_Interface (R 2 0 2 25) - ] - , testSessionWait "constant" $ do - let source = T.unlines ["a = ()"] - docId <- createDoc "A.hs" "haskell" source - symbols <- getDocumentSymbols docId - liftIO $ symbols @?= Right - [docSymbol "a" SymbolKind_Function (R 0 0 0 6)] - , testSessionWait "pattern" $ do - let source = T.unlines ["Just foo = Just 21"] - docId <- createDoc "A.hs" "haskell" source - symbols <- getDocumentSymbols docId - liftIO $ symbols @?= Right - [docSymbol "Just foo" SymbolKind_Function (R 0 0 0 18)] - , testSessionWait "pattern with type signature" $ do - let source = T.unlines ["{-# language ScopedTypeVariables #-}", "a :: () = ()"] - docId <- createDoc "A.hs" "haskell" source - symbols <- getDocumentSymbols docId - liftIO $ symbols @?= Right - [docSymbol "a :: ()" SymbolKind_Function (R 1 0 1 12)] - , testSessionWait "function" $ do - let source = T.unlines ["a _x = ()"] - docId <- createDoc "A.hs" "haskell" source - symbols <- getDocumentSymbols docId - liftIO $ symbols @?= Right [docSymbol "a" SymbolKind_Function (R 0 0 0 9)] - , testSessionWait "type synonym" $ do - let source = T.unlines ["type A = Bool"] - docId <- createDoc "A.hs" "haskell" source - symbols <- getDocumentSymbols docId - liftIO $ symbols @?= Right - [docSymbol' "A" SymbolKind_TypeParameter (R 0 0 0 13) (R 0 5 0 6)] - , testSessionWait "datatype" $ do - let source = T.unlines ["data A = C"] - docId <- createDoc "A.hs" "haskell" source - symbols <- getDocumentSymbols docId - liftIO $ symbols @?= Right - [ docSymbolWithChildren "A" - SymbolKind_Struct - (R 0 0 0 10) - [docSymbol "C" SymbolKind_Constructor (R 0 9 0 10)] - ] - , testSessionWait "record fields" $ do - let source = T.unlines ["data A = B {", " x :: Int", " , y :: Int}"] - docId <- createDoc "A.hs" "haskell" source - symbols <- getDocumentSymbols docId - liftIO $ symbols @?= Right - [ docSymbolWithChildren "A" SymbolKind_Struct (R 0 0 2 13) - [ docSymbolWithChildren' "B" SymbolKind_Constructor (R 0 9 2 13) (R 0 9 0 10) - [ docSymbol "x" SymbolKind_Field (R 1 2 1 3) - , docSymbol "y" SymbolKind_Field (R 2 4 2 5) - ] - ] - ] - , testSessionWait "import" $ do - let source = T.unlines ["import Data.Maybe ()"] - docId <- createDoc "A.hs" "haskell" source - symbols <- getDocumentSymbols docId - liftIO $ symbols @?= Right - [docSymbolWithChildren "imports" - SymbolKind_Module - (R 0 0 0 20) - [ docSymbol "import Data.Maybe" SymbolKind_Module (R 0 0 0 20) - ] - ] - , testSessionWait "multiple import" $ do - let source = T.unlines ["", "import Data.Maybe ()", "", "import Control.Exception ()", ""] - docId <- createDoc "A.hs" "haskell" source - symbols <- getDocumentSymbols docId - liftIO $ symbols @?= Right - [docSymbolWithChildren "imports" - SymbolKind_Module - (R 1 0 3 27) - [ docSymbol "import Data.Maybe" SymbolKind_Module (R 1 0 1 20) - , docSymbol "import Control.Exception" SymbolKind_Module (R 3 0 3 27) - ] - ] - , testSessionWait "foreign import" $ do - let source = T.unlines - [ "{-# language ForeignFunctionInterface #-}" - , "foreign import ccall \"a\" a :: Int" - ] - docId <- createDoc "A.hs" "haskell" source - symbols <- getDocumentSymbols docId - liftIO $ symbols @?= Right [docSymbolD "a" "import" SymbolKind_Object (R 1 0 1 33)] - , testSessionWait "foreign export" $ do - let source = T.unlines - [ "{-# language ForeignFunctionInterface #-}" - , "foreign export ccall odd :: Int -> Bool" - ] - docId <- createDoc "A.hs" "haskell" source - symbols <- getDocumentSymbols docId - liftIO $ symbols @?= Right [docSymbolD "odd" "export" SymbolKind_Object (R 1 0 1 39)] - ] - where - docSymbol name kind loc = - DocumentSymbol name Nothing kind Nothing Nothing loc loc Nothing - docSymbol' name kind loc selectionLoc = - DocumentSymbol name Nothing kind Nothing Nothing loc selectionLoc Nothing - docSymbolD name detail kind loc = - DocumentSymbol name (Just detail) kind Nothing Nothing loc loc Nothing - docSymbolWithChildren name kind loc cc = - DocumentSymbol name Nothing kind Nothing Nothing loc loc (Just cc) - docSymbolWithChildren' name kind loc selectionLoc cc = - DocumentSymbol name Nothing kind Nothing Nothing loc selectionLoc (Just cc) - moduleSymbol name loc cc = DocumentSymbol name - Nothing - SymbolKind_File - Nothing - Nothing - (R 0 0 maxBound 0) - loc - (Just cc) - classSymbol name loc cc = DocumentSymbol name - (Just "class") - SymbolKind_Interface - Nothing - Nothing - loc - loc - (Just cc) diff --git a/ghcide/test/exe/PluginParsedResultTests.hs b/ghcide/test/exe/PluginParsedResultTests.hs deleted file mode 100644 index f33a998df9..0000000000 --- a/ghcide/test/exe/PluginParsedResultTests.hs +++ /dev/null @@ -1,16 +0,0 @@ - -module PluginParsedResultTests (tests) where - -import Development.IDE.Test (expectNoMoreDiagnostics) -import Language.LSP.Test -import System.FilePath --- import Test.QuickCheck.Instances () -import Test.Tasty -import TestUtils - -tests :: TestTree -tests = - ignoreForGHC92Plus "No need for this plugin anymore!" $ - testSessionWithExtraFiles "plugin-recorddot" "parsedResultAction plugin" $ \dir -> do - _ <- openDoc (dir "RecordDot.hs") "haskell" - expectNoMoreDiagnostics 2 diff --git a/ghcide/test/exe/TestUtils.hs b/ghcide/test/exe/TestUtils.hs deleted file mode 100644 index 676cad1b34..0000000000 --- a/ghcide/test/exe/TestUtils.hs +++ /dev/null @@ -1,328 +0,0 @@ - -{-# LANGUAGE GADTs #-} -{-# LANGUAGE PatternSynonyms #-} -{-# LANGUAGE TypeOperators #-} - -module TestUtils where - -import Control.Applicative.Combinators -import Control.Concurrent.Async -import Control.Exception (bracket_, finally, throw) -import Control.Lens ((.~), (^.)) -import qualified Control.Lens as Lens -import qualified Control.Lens.Extras as Lens -import Control.Monad -import Control.Monad.IO.Class (liftIO) -import Data.Foldable -import Data.Function ((&)) -import Data.Maybe -import qualified Data.Text as T -import Development.IDE.GHC.Compat (GhcVersion (..), ghcVersion) -import Development.IDE.GHC.Util -import qualified Development.IDE.Main as IDE -import Development.IDE.Test (canonicalizeUri, - configureCheckProject, - expectNoMoreDiagnostics) -import Development.IDE.Test.Runfiles -import Development.IDE.Types.Location -import Development.Shake (getDirectoryFilesIO) -import Ide.Logger (Recorder, WithPriority, - cmapWithPrio) -import qualified Language.LSP.Protocol.Lens as L -import Language.LSP.Protocol.Message -import Language.LSP.Protocol.Types hiding - (SemanticTokenAbsolute (..), - SemanticTokenRelative (..), - SemanticTokensEdit (..), - mkRange) -import Language.LSP.Test -import System.Directory -import System.Environment.Blank (getEnv, setEnv, unsetEnv) -import System.FilePath -import System.Info.Extra (isMac, isWindows) -import qualified System.IO.Extra -import System.Process.Extra (createPipe) -import Test.Tasty -import Test.Tasty.ExpectedFailure -import Test.Tasty.HUnit - -import LogType - -import Data.Traversable (for) - --- | Wait for the next progress begin step -waitForProgressBegin :: Session () -waitForProgressBegin = skipManyTill anyMessage $ satisfyMaybe $ \case - FromServerMess SMethod_Progress (TNotificationMessage _ _ (ProgressParams _ v)) | Lens.is _workDoneProgressBegin v-> Just () - _ -> Nothing - --- | Wait for the first progress end step --- Also implemented in hls-test-utils Test.Hls -waitForProgressDone :: Session () -waitForProgressDone = skipManyTill anyMessage $ satisfyMaybe $ \case - FromServerMess SMethod_Progress (TNotificationMessage _ _ (ProgressParams _ v)) | Lens.is _workDoneProgressEnd v -> Just () - _ -> Nothing - --- | Wait for all progress to be done --- Needs at least one progress done notification to return --- Also implemented in hls-test-utils Test.Hls -waitForAllProgressDone :: Session () -waitForAllProgressDone = loop - where - loop = do - ~() <- skipManyTill anyMessage $ satisfyMaybe $ \case - FromServerMess SMethod_Progress (TNotificationMessage _ _ (ProgressParams _ v)) |Lens.is _workDoneProgressEnd v-> Just () - _ -> Nothing - done <- null <$> getIncompleteProgressSessions - unless done loop - -run :: Session a -> IO a -run s = run' (const s) - -run' :: (FilePath -> Session a) -> IO a -run' s = withTempDir $ \dir -> runInDir dir (s dir) - -runInDir :: FilePath -> Session a -> IO a -runInDir dir = runInDir' dir "." "." [] - --- | Takes a directory as well as relative paths to where we should launch the executable as well as the session root. -runInDir' :: FilePath -> FilePath -> FilePath -> [String] -> Session a -> IO a -runInDir' = runInDir'' lspTestCaps - -runInDir'' - :: ClientCapabilities - -> FilePath - -> FilePath - -> FilePath - -> [String] - -> Session b - -> IO b -runInDir'' lspCaps dir startExeIn startSessionIn extraOptions s = do - - ghcideExe <- locateGhcideExecutable - let startDir = dir startExeIn - let projDir = dir startSessionIn - - createDirectoryIfMissing True startDir - createDirectoryIfMissing True projDir - -- Temporarily hack around https://github.com/mpickering/hie-bios/pull/56 - -- since the package import test creates "Data/List.hs", which otherwise has no physical home - createDirectoryIfMissing True $ projDir ++ "/Data" - - shakeProfiling <- getEnv "SHAKE_PROFILING" - let cmd = unwords $ - [ghcideExe, "--lsp", "--test", "--verify-core-file", "--verbose", "-j2", "--cwd", startDir - ] ++ ["--shake-profiling=" <> dir | Just dir <- [shakeProfiling] - ] ++ extraOptions - -- HIE calls getXgdDirectory which assumes that HOME is set. - -- Only sets HOME if it wasn't already set. - setEnv "HOME" "/homeless-shelter" False - conf <- getConfigFromEnv - runSessionWithConfig conf cmd lspCaps projDir $ do - configureCheckProject False - s - --- | Version of 'System.IO.Extra.withTempDir' that canonicalizes the path --- Which we need to do on macOS since the $TMPDIR can be in @/private/var@ or --- @/var@ -withTempDir :: (FilePath -> IO a) -> IO a -withTempDir f = System.IO.Extra.withTempDir $ \dir -> do - dir' <- canonicalizePath dir - f dir' - -lspTestCaps :: ClientCapabilities -lspTestCaps = fullCaps { _window = Just $ WindowClientCapabilities (Just True) Nothing Nothing } - -getConfigFromEnv :: IO SessionConfig -getConfigFromEnv = do - logColor <- fromMaybe True <$> checkEnv "LSP_TEST_LOG_COLOR" - timeoutOverride <- fmap read <$> getEnv "LSP_TIMEOUT" - return defaultConfig - { messageTimeout = fromMaybe (messageTimeout defaultConfig) timeoutOverride - , logColor - } - where - checkEnv :: String -> IO (Maybe Bool) - checkEnv s = fmap convertVal <$> getEnv s - convertVal "0" = False - convertVal _ = True - -testSessionWait :: HasCallStack => String -> Session () -> TestTree -testSessionWait name = testSession name . - -- Check that any diagnostics produced were already consumed by the test case. - -- - -- If in future we add test cases where we don't care about checking the diagnostics, - -- this could move elsewhere. - -- - -- Experimentally, 0.5s seems to be long enough to wait for any final diagnostics to appear. - ( >> expectNoMoreDiagnostics 0.5) - -testSession :: String -> Session () -> TestTree -testSession name = testCase name . run - -xfail :: TestTree -> String -> TestTree -xfail = flip expectFailBecause - -ignoreInWindowsBecause :: String -> TestTree -> TestTree -ignoreInWindowsBecause = ignoreFor (BrokenForOS Windows) - -ignoreForGHC92Plus :: String -> TestTree -> TestTree -ignoreForGHC92Plus = ignoreFor (BrokenForGHC [GHC92, GHC94, GHC96, GHC98]) - -knownBrokenForGhcVersions :: [GhcVersion] -> String -> TestTree -> TestTree -knownBrokenForGhcVersions ghcVers = knownBrokenFor (BrokenForGHC ghcVers) - -data BrokenOS = Linux | MacOS | Windows deriving (Show) - -data IssueSolution = Broken | Ignore deriving (Show) - -data BrokenTarget = - BrokenSpecific BrokenOS [GhcVersion] - -- ^Broken for `BrokenOS` with `GhcVersion` - | BrokenForOS BrokenOS - -- ^Broken for `BrokenOS` - | BrokenForGHC [GhcVersion] - -- ^Broken for `GhcVersion` - deriving (Show) - --- | Ignore test for specific os and ghc with reason. -ignoreFor :: BrokenTarget -> String -> TestTree -> TestTree -ignoreFor = knownIssueFor Ignore - --- | Known broken for specific os and ghc with reason. -knownBrokenFor :: BrokenTarget -> String -> TestTree -> TestTree -knownBrokenFor = knownIssueFor Broken - --- | Deal with `IssueSolution` for specific OS and GHC. -knownIssueFor :: IssueSolution -> BrokenTarget -> String -> TestTree -> TestTree -knownIssueFor solution = go . \case - BrokenSpecific bos vers -> isTargetOS bos && isTargetGhc vers - BrokenForOS bos -> isTargetOS bos - BrokenForGHC vers -> isTargetGhc vers - where - isTargetOS = \case - Windows -> isWindows - MacOS -> isMac - Linux -> not isWindows && not isMac - - isTargetGhc = elem ghcVersion - - go True = case solution of - Broken -> expectFailBecause - Ignore -> ignoreTestBecause - go False = const id - -data Expect - = ExpectRange Range -- Both gotoDef and hover should report this range - | ExpectLocation Location --- | ExpectDefRange Range -- Only gotoDef should report this range - | ExpectHoverRange Range -- Only hover should report this range - | ExpectHoverText [T.Text] -- the hover message must contain these snippets - | ExpectHoverExcludeText [T.Text] -- the hover message must _not_ contain these snippets - | ExpectHoverTextRegex T.Text -- the hover message must match this pattern - | ExpectExternFail -- definition lookup in other file expected to fail - | ExpectNoDefinitions - | ExpectNoHover --- | ExpectExtern -- TODO: as above, but expected to succeed: need some more info in here, once we have some working examples - deriving Eq - -mkR :: UInt -> UInt -> UInt -> UInt -> Expect -mkR startLine startColumn endLine endColumn = ExpectRange $ mkRange startLine startColumn endLine endColumn - -mkL :: Uri -> UInt -> UInt -> UInt -> UInt -> Expect -mkL uri startLine startColumn endLine endColumn = ExpectLocation $ Location uri $ mkRange startLine startColumn endLine endColumn - - - -testSessionWithExtraFiles :: FilePath -> String -> (FilePath -> Session ()) -> TestTree -testSessionWithExtraFiles prefix name = testCase name . runWithExtraFiles prefix - -testSession' :: String -> (FilePath -> Session ()) -> TestTree -testSession' name = testCase name . run' - - - -mkRange :: UInt -> UInt -> UInt -> UInt -> Range -mkRange a b c d = Range (Position a b) (Position c d) - - -runWithExtraFiles :: FilePath -> (FilePath -> Session a) -> IO a -runWithExtraFiles prefix s = withTempDir $ \dir -> do - copyTestDataFiles dir prefix - runInDir dir (s dir) - -copyTestDataFiles :: FilePath -> FilePath -> IO () -copyTestDataFiles dir prefix = do - -- Copy all the test data files to the temporary workspace - testDataFiles <- getDirectoryFilesIO ("test/data" prefix) ["//*"] - for_ testDataFiles $ \f -> do - createDirectoryIfMissing True $ dir takeDirectory f - copyFile ("test/data" prefix f) (dir f) - -withLongTimeout :: IO a -> IO a -withLongTimeout = bracket_ (setEnv "LSP_TIMEOUT" "120" True) (unsetEnv "LSP_TIMEOUT") - - - -lspTestCapsNoFileWatches :: ClientCapabilities -lspTestCapsNoFileWatches = lspTestCaps & L.workspace . Lens._Just . L.didChangeWatchedFiles .~ Nothing - -openTestDataDoc :: FilePath -> Session TextDocumentIdentifier -openTestDataDoc path = do - source <- liftIO $ readFileUtf8 $ "test/data" path - createDoc path "haskell" source - -pattern R :: UInt -> UInt -> UInt -> UInt -> Range -pattern R x y x' y' = Range (Position x y) (Position x' y') - -checkDefs :: Definition |? ([DefinitionLink] |? Null) -> Session [Expect] -> Session () -checkDefs (defToLocation -> defs) mkExpectations = traverse_ check =<< mkExpectations where - check (ExpectRange expectedRange) = do - def <- assertOneDefinitionFound defs - assertRangeCorrect def expectedRange - check (ExpectLocation expectedLocation) = do - def <- assertOneDefinitionFound defs - liftIO $ do - canonActualLoc <- canonicalizeLocation def - canonExpectedLoc <- canonicalizeLocation expectedLocation - canonActualLoc @?= canonExpectedLoc - check ExpectNoDefinitions = do - liftIO $ assertBool "Expecting no definitions" $ null defs - check ExpectExternFail = liftIO $ assertFailure "Expecting to fail to find in external file" - check _ = pure () -- all other expectations not relevant to getDefinition - - assertOneDefinitionFound :: [Location] -> Session Location - assertOneDefinitionFound [def] = pure def - assertOneDefinitionFound _ = liftIO $ assertFailure "Expecting exactly one definition" - - assertRangeCorrect Location{_range = foundRange} expectedRange = - liftIO $ expectedRange @=? foundRange - -canonicalizeLocation :: Location -> IO Location -canonicalizeLocation (Location uri range) = Location <$> canonicalizeUri uri <*> pure range - -defToLocation :: Definition |? ([DefinitionLink] |? Null) -> [Location] -defToLocation (InL (Definition (InL l))) = [l] -defToLocation (InL (Definition (InR ls))) = ls -defToLocation (InR (InL defLink)) = (\(DefinitionLink LocationLink{_targetUri,_targetRange}) -> Location _targetUri _targetRange) <$> defLink -defToLocation (InR (InR Null)) = [] - --- | Ghc 9 doesn't include the $-sign in TH warnings like earlier versions did -thDollarIdx :: UInt -thDollarIdx | ghcVersion >= GHC90 = 1 - | otherwise = 0 - -testIde :: Recorder (WithPriority Log) -> IDE.Arguments -> Session () -> IO () -testIde recorder arguments session = do - config <- getConfigFromEnv - cwd <- getCurrentDirectory - (hInRead, hInWrite) <- createPipe - (hOutRead, hOutWrite) <- createPipe - let projDir = "." - let server = IDE.defaultMain (cmapWithPrio LogIDEMain recorder) arguments - { IDE.argsHandleIn = pure hInRead - , IDE.argsHandleOut = pure hOutWrite - } - - flip finally (setCurrentDirectory cwd) $ withAsync server $ \_ -> - runSessionWithHandles hInWrite hOutRead config lspTestCaps projDir session diff --git a/ghcide/test/ghcide-test-utils.cabal b/ghcide/test/ghcide-test-utils.cabal deleted file mode 100644 index b55e6e6ca0..0000000000 --- a/ghcide/test/ghcide-test-utils.cabal +++ /dev/null @@ -1,60 +0,0 @@ -cabal-version: 3.0 --- This library is a copy of the sublibrary ghcide-test-utils until stack and hackage support public sublibraries -build-type: Simple -category: Development -name: ghcide-test-utils -version: 1.9.0.0 -license: Apache-2.0 -license-file: LICENSE -author: Digital Asset and Ghcide contributors -maintainer: Ghcide contributors -copyright: Digital Asset and Ghcide contributors 2018-2022 -synopsis: Test utils for ghcide -description: - Test utils for ghcide -homepage: https://github.com/haskell/haskell-language-server/tree/master/ghcide#readme -bug-reports: https://github.com/haskell/haskell-language-server/issues -tested-with: GHC == 9.0.2 || == 9.2.3 || == 9.2.4 - -source-repository head - type: git - location: https://github.com/haskell/haskell-language-server.git - - -library - default-language: Haskell2010 - build-depends: - aeson, - base > 4.9 && < 5, - containers, - data-default, - directory, - extra, - filepath, - ghcide, - lsp-types, - hls-plugin-api, - lens, - lsp-test ^>= 0.16, - tasty-hunit >= 0.10, - text, - row-types, - hs-source-dirs: src - exposed-modules: - Development.IDE.Test - Development.IDE.Test.Diagnostic - default-extensions: - BangPatterns - DeriveFunctor - DeriveGeneric - FlexibleContexts - GeneralizedNewtypeDeriving - LambdaCase - NamedFieldPuns - OverloadedStrings - RecordWildCards - ScopedTypeVariables - StandaloneDeriving - TupleSections - TypeApplications - ViewPatterns diff --git a/haskell-language-server.cabal b/haskell-language-server.cabal index c925b91691..42e8d11b60 100644 --- a/haskell-language-server.cabal +++ b/haskell-language-server.cabal @@ -1,7 +1,7 @@ -cabal-version: 3.0 +cabal-version: 3.4 category: Development name: haskell-language-server -version: 2.4.0.0 +version: 2.11.0.0 synopsis: LSP server for GHC description: Please see the README on GitHub at @@ -14,7 +14,7 @@ copyright: The Haskell IDE Team license: Apache-2.0 license-file: LICENSE build-type: Simple -tested-with: GHC == 9.0.2 || ==9.2.5 +tested-with: GHC == {9.12.2, 9.10.1, 9.8.4, 9.6.7} extra-source-files: README.md ChangeLog.md @@ -22,34 +22,67 @@ extra-source-files: test/testdata/**/*.cabal test/testdata/**/*.yaml test/testdata/**/*.hs - bindist/wrapper.in + test/testdata/**/*.json + + -- These globs should only match test/testdata + plugins/**/*.project + plugins/**/*.expected + plugins/**/*.cabal + plugins/**/*.yaml + plugins/**/*.txt + plugins/**/*.hs + + ghcide-test/data/**/*.cabal + ghcide-test/data/**/*.hs + ghcide-test/data/**/*.hs-boot + ghcide-test/data/**/*.project + ghcide-test/data/**/*.yaml -flag pedantic - description: Enable -Werror - default: False - manual: True + bindist/wrapper.in source-repository head type: git location: https://github.com/haskell/haskell-language-server -common common-deps +common defaults + default-language: GHC2021 + -- Should have been in GHC2021, an oversight + default-extensions: ExplicitNamespaces build-depends: - , base >=4.12 && <5 - , directory - , extra - , filepath - , text - , prettyprinter >= 1.7 + , base >=4.12 && <5 + +common test-defaults + ghc-options: -threaded -rtsopts -with-rtsopts=-N + if impl(ghc >= 9.8) + -- We allow using partial functions in tests + ghc-options: -Wno-x-partial -- Default warnings in HLS common warnings - ghc-options: -Wall -Wredundant-constraints -Wno-name-shadowing -Wno-unticked-promoted-constructors + ghc-options: -Wall + -Wredundant-constraints + -Wunused-packages + -Wno-name-shadowing + -Wno-unticked-promoted-constructors + +flag pedantic + description: Enable -Werror + default: False + manual: True -- Allow compiling in pedantic mode common pedantic if flag(pedantic) - ghc-options: -Werror + ghc-options: + -Werror + -- Note [unused-packages] + -- ~~~~~~~~~~~~~~~~~~~~~~ + -- Some packages need CPP conditioned on MIN_VERSION_ghc(x,y,z). + -- MIN_VERSION_ is CPP macro that cabal defines only when is declared as a dependency. + -- But -Wunused-packages still reports it as unused dependency if it's not imported. + -- For packages with such "unused" dependencies we demote -Wunused-packages error + -- (enabled by --flag=pedantic) to warning via -Wwarn=unused-packages. + -Wwarn=unused-packages -- Plugin flags are designed for 'cabal install haskell-language-server': -- - Bulk flags should be default:False @@ -63,285 +96,1748 @@ flag ignore-plugins-ghc-bounds default: False manual: True - -flag cabal - description: Enable cabal plugin +flag dynamic + description: Build with the dyn rts default: True manual: True -flag class - description: Enable class plugin - default: True - manual: True +---------------------------- +---------------------------- +-- PLUGINS +---------------------------- +---------------------------- -flag callHierarchy - description: Enable call hierarchy plugin - default: True - manual: True +----------------------------- +-- cabal-fmt plugin +----------------------------- -flag eval - description: Enable eval plugin +flag cabalfmt + description: Enable cabal-fmt plugin default: True manual: True -flag importLens - description: Enable importLens plugin - default: True - manual: True +common cabalfmt + if flag(cabalfmt) && flag(cabal) + build-depends: haskell-language-server:hls-cabal-fmt-plugin + cpp-options: -Dhls_cabalfmt -flag rename - description: Enable rename plugin - default: True +flag isolateCabalfmtTests + description: Should tests search for 'cabal-fmt' on the $PATH or shall we install it via build-tool-depends? + -- By default, search on the PATH + default: False manual: True -flag retrie - description: Enable retrie plugin - default: True - manual: True +library hls-cabal-fmt-plugin + import: defaults, pedantic, warnings + if !flag(cabalfmt) || !flag(cabal) + buildable: False + exposed-modules: Ide.Plugin.CabalFmt + hs-source-dirs: plugins/hls-cabal-fmt-plugin/src + build-depends: + , directory + , filepath + , ghcide == 2.11.0.0 + , hls-plugin-api == 2.11.0.0 + , lens + , lsp-types + , mtl + , process-extras + , text -flag hlint - description: Enable hlint plugin - default: True - manual: True +-- The `hls-cabal-plugin` is needed for tests, as we need to install notification handlers +test-suite hls-cabal-fmt-plugin-tests + import: defaults, pedantic, test-defaults, warnings + if !flag(cabalfmt) || !flag(cabal) + buildable: False + type: exitcode-stdio-1.0 + hs-source-dirs: plugins/hls-cabal-fmt-plugin/test + main-is: Main.hs + build-depends: + , directory + , filepath + , haskell-language-server:hls-cabal-plugin + , haskell-language-server:hls-cabal-fmt-plugin + , hls-plugin-api == 2.11.0.0 + , hls-test-utils == 2.11.0.0 -flag stan - description: Enable stan plugin - default: True - manual: True + if flag(isolateCabalfmtTests) + build-tool-depends: cabal-fmt:cabal-fmt ^>=0.1.12 + cpp-options: -Dhls_isolate_cabalfmt_tests -flag moduleName - description: Enable moduleName plugin - default: True - manual: True +----------------------------- +-- cabal-gild plugin +----------------------------- -flag pragmas - description: Enable pragmas plugin +flag cabalgild + description: Enable cabal-gild plugin default: True manual: True -flag splice - description: Enable splice plugin - default: True - manual: True +common cabalgild + if flag(cabalgild) && flag(cabal) + build-depends: haskell-language-server:hls-cabal-gild-plugin + cpp-options: -Dhls_cabalgild -flag alternateNumberFormat - description: Enable Alternate Number Format plugin - default: True +flag isolateCabalGildTests + description: Should tests search for 'cabal-gild' on the $PATH or shall we install it via build-tool-depends? + -- By default, search on the PATH + default: False manual: True -flag qualifyImportedNames - description: Enable qualifyImportedNames plugin - default: True - manual: True +library hls-cabal-gild-plugin + import: defaults, pedantic, warnings + if !flag(cabalgild) || !flag(cabal) + buildable: False + exposed-modules: Ide.Plugin.CabalGild + hs-source-dirs: plugins/hls-cabal-gild-plugin/src + build-depends: + , directory + , filepath + , ghcide == 2.11.0.0 + , hls-plugin-api == 2.11.0.0 + , lsp-types + , text + , mtl + , process-extras + +-- The `hls-cabal-plugin` is needed for tests, as we need to install notification handlers +test-suite hls-cabal-gild-plugin-tests + import: defaults, pedantic, test-defaults, warnings + if !flag(cabalgild) || !flag(cabal) + buildable: False + type: exitcode-stdio-1.0 + hs-source-dirs: plugins/hls-cabal-gild-plugin/test + main-is: Main.hs + build-depends: + , directory + , filepath + , haskell-language-server:hls-cabal-plugin + , haskell-language-server:hls-cabal-gild-plugin + , hls-plugin-api == 2.11.0.0 + , hls-test-utils == 2.11.0.0 -flag codeRange - description: Enable Code Range plugin - default: True - manual: True + if flag(isolateCabalGildTests) + -- https://github.com/tfausak/cabal-gild/issues/89 + build-tool-depends: cabal-gild:cabal-gild >= 1.3 && < 1.3.2 + cpp-options: -Dhls_isolate_cabalgild_tests -flag changeTypeSignature - description: Enable changeTypeSignature plugin - default: True - manual: True +----------------------------- +-- cabal plugin +----------------------------- -flag gadt - description: Enable gadt plugin +flag cabal + description: Enable cabal plugin default: True manual: True -flag explicitFixity - description: Enable explicitFixity plugin - default: True - manual: True +common cabal + if flag(cabal) + build-depends: haskell-language-server:hls-cabal-plugin + cpp-options: -Dhls_cabal -flag explicitFields - description: Enable explicitFields plugin - default: True - manual: True +library hls-cabal-plugin + import: defaults, pedantic, warnings + if !flag(cabal) + buildable: False + exposed-modules: + Ide.Plugin.Cabal + Ide.Plugin.Cabal.Diagnostics + Ide.Plugin.Cabal.Completion.CabalFields + Ide.Plugin.Cabal.Completion.Completer.FilePath + Ide.Plugin.Cabal.Completion.Completer.Module + Ide.Plugin.Cabal.Completion.Completer.Paths + Ide.Plugin.Cabal.Completion.Completer.Simple + Ide.Plugin.Cabal.Completion.Completer.Snippet + Ide.Plugin.Cabal.Completion.Completer.Types + Ide.Plugin.Cabal.Completion.Completions + Ide.Plugin.Cabal.Completion.Data + Ide.Plugin.Cabal.Completion.Types + Ide.Plugin.Cabal.Definition + Ide.Plugin.Cabal.FieldSuggest + Ide.Plugin.Cabal.LicenseSuggest + Ide.Plugin.Cabal.CabalAdd + Ide.Plugin.Cabal.Orphans + Ide.Plugin.Cabal.Outline + Ide.Plugin.Cabal.Parse -flag overloadedRecordDot - description: Enable overloadedRecordDot plugin - default: True - manual: True --- formatters + build-depends: + , bytestring + , Cabal-syntax >= 3.7 + , containers + , deepseq + , directory + , filepath + , extra >=1.7.4 + , ghcide == 2.11.0.0 + , hashable + , hls-plugin-api == 2.11.0.0 + , hls-graph == 2.11.0.0 + , lens + , lsp ^>=2.7 + , lsp-types ^>=2.3 + , regex-tdfa ^>=1.3.1 + , text + , text-rope + , transformers + , unordered-containers >=0.2.10.0 + , containers + , cabal-add + , process + , aeson + , Cabal + , pretty -flag floskell - description: Enable floskell plugin - default: True - manual: True + hs-source-dirs: plugins/hls-cabal-plugin/src -flag fourmolu - description: Enable fourmolu plugin - default: True - manual: True +test-suite hls-cabal-plugin-tests + import: defaults, pedantic, test-defaults, warnings + if !flag(cabal) + buildable: False + type: exitcode-stdio-1.0 + hs-source-dirs: plugins/hls-cabal-plugin/test + main-is: Main.hs + other-modules: + CabalAdd + Completer + Context + Definition + Outline + Utils + build-depends: + , bytestring + , Cabal-syntax >= 3.7 + , extra + , filepath + , ghcide + , haskell-language-server:hls-cabal-plugin + , hls-test-utils == 2.11.0.0 + , lens + , lsp-types + , text + , hls-plugin-api -flag ormolu - description: Enable ormolu plugin - default: True - manual: True +----------------------------- +-- class plugin +----------------------------- -flag stylishHaskell - description: Enable stylishHaskell plugin +flag class + description: Enable class plugin default: True manual: True -flag refactor - description: Enable refactor plugin - default: True - manual: True +common class + if flag(class) + build-depends: haskell-language-server:hls-class-plugin + cpp-options: -Dhls_class -flag dynamic - description: Build with the dyn rts - default: True - manual: True +library hls-class-plugin + import: defaults, pedantic, warnings + if !flag(class) + buildable: False + exposed-modules: Ide.Plugin.Class + other-modules: Ide.Plugin.Class.CodeAction + , Ide.Plugin.Class.CodeLens + , Ide.Plugin.Class.ExactPrint + , Ide.Plugin.Class.Types + , Ide.Plugin.Class.Utils + hs-source-dirs: plugins/hls-class-plugin/src + build-depends: + , aeson + , containers + , deepseq + , extra + , ghc + , ghc-exactprint >= 1.5 && < 1.13.0.0 + , ghcide == 2.11.0.0 + , hls-graph + , hls-plugin-api == 2.11.0.0 + , lens + , lsp + , mtl + , text + , transformers -flag cabalfmt - description: Enable cabal-fmt plugin - default: True - manual: True + default-extensions: + DataKinds + OverloadedStrings -common cabalfmt - if flag(cabalfmt) - build-depends: hls-cabal-fmt-plugin == 2.4.0.0 - cpp-options: -Dhls_cabalfmt +test-suite hls-class-plugin-tests + import: defaults, pedantic, test-defaults, warnings + if !flag(class) + buildable: False + type: exitcode-stdio-1.0 + hs-source-dirs: plugins/hls-class-plugin/test + main-is: Main.hs + build-depends: + , filepath + , haskell-language-server:hls-class-plugin + , hls-test-utils == 2.11.0.0 + , lens + , lsp-types + , text -common cabal - if flag(cabal) - build-depends: hls-cabal-plugin == 2.4.0.0 - cpp-options: -Dhls_cabal +----------------------------- +-- call-hierarchy plugin +----------------------------- -common class - if flag(class) && (impl(ghc < 9.8.0) || flag(ignore-plugins-ghc-bounds)) - build-depends: hls-class-plugin == 2.4.0.0 - cpp-options: -Dhls_class +flag callHierarchy + description: Enable call hierarchy plugin + default: True + manual: True common callHierarchy if flag(callHierarchy) - build-depends: hls-call-hierarchy-plugin == 2.4.0.0 + build-depends: haskell-language-server:hls-call-hierarchy-plugin cpp-options: -Dhls_callHierarchy +library hls-call-hierarchy-plugin + import: defaults, pedantic, warnings + if !flag(callHierarchy) + buildable: False + exposed-modules: Ide.Plugin.CallHierarchy + other-modules: + Ide.Plugin.CallHierarchy.Internal + Ide.Plugin.CallHierarchy.Query + Ide.Plugin.CallHierarchy.Types + + hs-source-dirs: plugins/hls-call-hierarchy-plugin/src + build-depends: + , aeson + , containers + , extra + , ghcide == 2.11.0.0 + , hiedb ^>= 0.7.0.0 + , hls-plugin-api == 2.11.0.0 + , lens + , lsp >=2.7 + , sqlite-simple + , text + + default-extensions: DataKinds + +test-suite hls-call-hierarchy-plugin-tests + import: defaults, pedantic, test-defaults, warnings + if !flag(callHierarchy) + buildable: False + type: exitcode-stdio-1.0 + hs-source-dirs: plugins/hls-call-hierarchy-plugin/test + main-is: Main.hs + build-depends: + , aeson + , containers + , extra + , filepath + , haskell-language-server:hls-call-hierarchy-plugin + , hls-test-utils == 2.11.0.0 + , lens + , lsp + , lsp-test + , text + +----------------------------- +-- eval plugin +----------------------------- + +flag eval + description: Enable eval plugin + default: True + manual: True + common eval if flag(eval) - build-depends: hls-eval-plugin == 2.4.0.0 + build-depends: haskell-language-server:hls-eval-plugin cpp-options: -Dhls_eval -common importLens - if flag(importLens) - build-depends: hls-explicit-imports-plugin == 2.4.0.0 - cpp-options: -Dhls_importLens +library hls-eval-plugin + import: defaults, pedantic, warnings + if !flag(eval) + buildable: False + exposed-modules: + Ide.Plugin.Eval + Ide.Plugin.Eval.Types -common rename - if flag(rename) && (impl(ghc < 9.8.0) || flag(ignore-plugins-ghc-bounds)) - build-depends: hls-rename-plugin == 2.4.0.0 - cpp-options: -Dhls_rename + hs-source-dirs: plugins/hls-eval-plugin/src + other-modules: + Ide.Plugin.Eval.Code + Ide.Plugin.Eval.Config + Ide.Plugin.Eval.GHC + Ide.Plugin.Eval.Handlers + Ide.Plugin.Eval.Parse.Comments + Ide.Plugin.Eval.Parse.Option + Ide.Plugin.Eval.Rules + Ide.Plugin.Eval.Util -common retrie - if flag(retrie) && (impl(ghc < 9.8.0) || flag(ignore-plugins-ghc-bounds)) - build-depends: hls-retrie-plugin == 2.4.0.0 - cpp-options: -Dhls_retrie + build-depends: + , aeson + , bytestring + , containers + , deepseq + , Diff ^>=0.5 || ^>=1.0.0 + , dlist + , extra + , filepath + , ghc + , ghc-boot-th + , ghcide == 2.11.0.0 + , hls-graph + , hls-plugin-api == 2.11.0.0 + , lens + , lsp + , lsp-types + , megaparsec >=9.0 + , mtl + , parser-combinators >=1.2 + , text + , text-rope + , transformers + , unliftio + , unordered-containers + + default-extensions: + DataKinds + +test-suite hls-eval-plugin-tests + import: defaults, pedantic, test-defaults, warnings + if !flag(eval) + buildable: False + type: exitcode-stdio-1.0 + hs-source-dirs: plugins/hls-eval-plugin/test + main-is: Main.hs + ghc-options: -fno-ignore-asserts + build-depends: + , aeson + , containers + , extra + , filepath + , haskell-language-server:hls-eval-plugin + , hls-plugin-api + , hls-test-utils == 2.11.0.0 + , lens + , lsp-types + , text + +----------------------------- +-- import lens plugin +----------------------------- + +flag importLens + description: Enable importLens plugin + default: True + manual: False + +common importLens + if flag(importLens) + build-depends: haskell-language-server:hls-explicit-imports-plugin + cpp-options: -Dhls_importLens + +library hls-explicit-imports-plugin + import: defaults, pedantic, warnings + if !flag(importlens) + buildable: False + exposed-modules: Ide.Plugin.ExplicitImports + hs-source-dirs: plugins/hls-explicit-imports-plugin/src + build-depends: + , aeson + , containers + , deepseq + , ghc + , ghcide == 2.11.0.0 + , hls-graph + , hls-plugin-api == 2.11.0.0 + , lens + , lsp + , mtl + , text + , transformers + + default-extensions: + DataKinds + +test-suite hls-explicit-imports-plugin-tests + import: defaults, pedantic, test-defaults, warnings + if !flag(importlens) + buildable: False + type: exitcode-stdio-1.0 + hs-source-dirs: plugins/hls-explicit-imports-plugin/test + main-is: Main.hs + build-depends: + , extra + , filepath + , haskell-language-server:hls-explicit-imports-plugin + , hls-test-utils == 2.11.0.0 + , lens + , lsp-types + , text + +----------------------------- +-- rename plugin +----------------------------- + +flag rename + description: Enable rename plugin + default: True + manual: True + +common rename + if flag(rename) + build-depends: haskell-language-server:hls-rename-plugin + cpp-options: -Dhls_rename + +library hls-rename-plugin + import: defaults, pedantic, warnings + if !flag(rename) + buildable: False + exposed-modules: Ide.Plugin.Rename + hs-source-dirs: plugins/hls-rename-plugin/src + build-depends: + , containers + , ghcide == 2.11.0.0 + , hashable + , hiedb ^>= 0.7.0.0 + , hie-compat + , hls-plugin-api == 2.11.0.0 + , haskell-language-server:hls-refactor-plugin + , lens + , lsp-types + , mtl + , mod + , syb + , text + , transformers + , unordered-containers + + +test-suite hls-rename-plugin-tests + import: defaults, pedantic, test-defaults, warnings + if !flag(rename) + buildable: False + type: exitcode-stdio-1.0 + hs-source-dirs: plugins/hls-rename-plugin/test + main-is: Main.hs + build-depends: + , aeson + , containers + , filepath + , hls-plugin-api + , haskell-language-server:hls-rename-plugin + , hls-test-utils == 2.11.0.0 + , lens + , lsp-types + , text + +----------------------------- +-- retrie plugin +----------------------------- + +flag retrie + description: Enable retrie plugin + default: True + manual: True + +common retrie + if flag(retrie) && (impl(ghc < 9.10) || flag(ignore-plugins-ghc-bounds)) + build-depends: haskell-language-server:hls-retrie-plugin + cpp-options: -Dhls_retrie + +library hls-retrie-plugin + import: defaults, pedantic, warnings + if !(flag(retrie) && (impl(ghc < 9.10) || flag(ignore-plugins-ghc-bounds))) + buildable: False + exposed-modules: Ide.Plugin.Retrie + hs-source-dirs: plugins/hls-retrie-plugin/src + build-depends: + , aeson + , bytestring + , containers + , extra + , ghc + , ghcide == 2.11.0.0 + , hashable + , hls-plugin-api == 2.11.0.0 + , haskell-language-server:hls-refactor-plugin + , lens + , lsp + , lsp-types + , mtl + , retrie >=0.1.1.0 + , safe-exceptions + , stm + , text + , text-rope + , transformers + , unordered-containers + + default-extensions: + DataKinds + +test-suite hls-retrie-plugin-tests + import: defaults, pedantic, test-defaults, warnings + if !(flag(retrie) && (impl(ghc < 9.10) || flag(ignore-plugins-ghc-bounds))) + buildable: False + type: exitcode-stdio-1.0 + hs-source-dirs: plugins/hls-retrie-plugin/test + main-is: Main.hs + build-depends: + , containers + , filepath + , hls-plugin-api + , haskell-language-server:{hls-refactor-plugin, hls-retrie-plugin} + , hls-test-utils == 2.11.0.0 + , text + +----------------------------- +-- hlint plugin +----------------------------- + +flag ghc-lib + description: + Use ghc-lib-parser rather than the ghc library (requires hlint and + ghc-lib-parser-ex to also be built with it) + default: True + manual: True + +flag hlint + description: Enable hlint plugin + default: True + manual: True + +common hlint + if flag(hlint) + build-depends: haskell-language-server:hls-hlint-plugin + cpp-options: -Dhls_hlint + +library hls-hlint-plugin + import: defaults, pedantic, warnings + -- https://github.com/ndmitchell/hlint/pull/1594 + if !flag(hlint) + buildable: False + exposed-modules: Ide.Plugin.Hlint + hs-source-dirs: plugins/hls-hlint-plugin/src + build-depends: + , aeson + , bytestring + , containers + , deepseq + , filepath + , ghcide == 2.11.0.0 + , hashable + , hlint >= 3.5 && < 3.11 + , hls-plugin-api == 2.11.0.0 + , lens + , mtl + , refact + , regex-tdfa + , stm + , temporary + , text + , text-rope + , transformers + , unordered-containers + , ghc-lib-parser-ex + , lsp-types + + -- apply-refact doesn't work on 9.10, or even have a buildable + -- configuration + if impl(ghc >= 9.11) || impl(ghc < 9.10) + cpp-options: -DAPPLY_REFACT + build-depends: apply-refact + + if flag(ghc-lib) + cpp-options: -DGHC_LIB + build-depends: + ghc-lib-parser + else + build-depends: + ghc + , ghc-boot + + default-extensions: + DataKinds + +test-suite hls-hlint-plugin-tests + import: defaults, pedantic, test-defaults, warnings + if !flag(hlint) + buildable: False + type: exitcode-stdio-1.0 + hs-source-dirs: plugins/hls-hlint-plugin/test + main-is: Main.hs + -- Work around https://gitlab.haskell.org/ghc/ghc/-/issues/24648 + if os(darwin) + ghc-options: -optl-Wl,-ld_classic + + build-depends: + aeson + , containers + , filepath + , haskell-language-server:hls-hlint-plugin + , hls-plugin-api + , hls-test-utils == 2.11.0.0 + , lens + , lsp-types + , text + +----------------------------- +-- stan plugin +----------------------------- + +flag stan + description: Enable stan plugin + default: True + manual: True + +common stan + if flag(stan) && (impl(ghc < 9.11) || flag(ignore-plugins-ghc-bounds)) + build-depends: haskell-language-server:hls-stan-plugin + cpp-options: -Dhls_stan + +library hls-stan-plugin + import: defaults, pedantic, warnings + if !flag(stan) || (impl(ghc > 9.11) && !flag(ignore-plugins-ghc-bounds)) + buildable: False + exposed-modules: Ide.Plugin.Stan + hs-source-dirs: plugins/hls-stan-plugin/src + build-depends: + , deepseq + , hashable + , hie-compat + , hls-plugin-api + , ghcide + , lsp-types + , text + , unordered-containers + , stan >= 0.2.1.0 + , trial + , directory + + default-extensions: + LambdaCase + TypeFamilies + DuplicateRecordFields + OverloadedStrings + +test-suite hls-stan-plugin-tests + import: defaults, pedantic, test-defaults, warnings + if !flag(stan) || (impl(ghc > 9.11) && !flag(ignore-plugins-ghc-bounds)) + buildable: False + type: exitcode-stdio-1.0 + hs-source-dirs: plugins/hls-stan-plugin/test + main-is: Main.hs + build-depends: + , filepath + , haskell-language-server:hls-stan-plugin + , hls-plugin-api + , hls-test-utils == 2.11.0.0 + , lens + , lsp-types + , text + default-extensions: + OverloadedStrings + +----------------------------- +-- module name plugin +----------------------------- + +flag moduleName + description: Enable moduleName plugin + default: True + manual: True + +common moduleName + if flag(moduleName) + build-depends: haskell-language-server:hls-module-name-plugin + cpp-options: -Dhls_moduleName + +library hls-module-name-plugin + import: defaults, pedantic, warnings + if !flag(modulename) + buildable: False + exposed-modules: Ide.Plugin.ModuleName + hs-source-dirs: plugins/hls-module-name-plugin/src + build-depends: + , aeson + , containers + , filepath + , ghcide == 2.11.0.0 + , hls-plugin-api == 2.11.0.0 + , lsp + , text + , text-rope + , transformers + + +test-suite hls-module-name-plugin-tests + import: defaults, pedantic, test-defaults, warnings + if !flag(modulename) + buildable: False + type: exitcode-stdio-1.0 + hs-source-dirs: plugins/hls-module-name-plugin/test + main-is: Main.hs + build-depends: + , filepath + , haskell-language-server:hls-module-name-plugin + , hls-test-utils == 2.11.0.0 + +----------------------------- +-- pragmas plugin +----------------------------- + +flag pragmas + description: Enable pragmas plugin + default: True + manual: True + +common pragmas + if flag(pragmas) + build-depends: haskell-language-server:hls-pragmas-plugin + cpp-options: -Dhls_pragmas + +library hls-pragmas-plugin + import: defaults, pedantic, warnings + if !flag(pragmas) + buildable: False + exposed-modules: Ide.Plugin.Pragmas + hs-source-dirs: plugins/hls-pragmas-plugin/src + build-depends: + , aeson + , extra + , fuzzy + , ghcide == 2.11.0.0 + , hls-plugin-api == 2.11.0.0 + , lens + , lens-aeson + , lsp + , text + , transformers + , containers + +test-suite hls-pragmas-plugin-tests + import: defaults, pedantic, test-defaults, warnings + if !flag(pragmas) + buildable: False + type: exitcode-stdio-1.0 + hs-source-dirs: plugins/hls-pragmas-plugin/test + main-is: Main.hs + build-depends: + , aeson + , filepath + , haskell-language-server:hls-pragmas-plugin + , hls-test-utils == 2.11.0.0 + , lens + , lsp-types + , text + +----------------------------- +-- splice plugin +----------------------------- + +flag splice + description: Enable splice plugin + default: True + manual: True + +common splice + if flag(splice) && (impl(ghc < 9.10) || flag(ignore-plugins-ghc-bounds)) + build-depends: haskell-language-server:hls-splice-plugin + cpp-options: -Dhls_splice + +library hls-splice-plugin + import: defaults, pedantic, warnings + if !(flag(splice) && (impl(ghc < 9.10) || flag(ignore-plugins-ghc-bounds))) + buildable: False + exposed-modules: + Ide.Plugin.Splice + Ide.Plugin.Splice.Types + + hs-source-dirs: plugins/hls-splice-plugin/src + build-depends: + , aeson + , extra + , foldl + , ghc + , ghcide == 2.11.0.0 + , hls-plugin-api == 2.11.0.0 + , haskell-language-server:hls-refactor-plugin + , lens + , lsp + , mtl + , syb + , text + , transformers + , unliftio-core + + default-extensions: + DataKinds + +test-suite hls-splice-plugin-tests + import: defaults, pedantic, test-defaults, warnings + if !(flag(splice) && (impl(ghc < 9.10) || flag(ignore-plugins-ghc-bounds))) + buildable: False + type: exitcode-stdio-1.0 + hs-source-dirs: plugins/hls-splice-plugin/test + main-is: Main.hs + build-depends: + , filepath + , haskell-language-server:hls-splice-plugin + , hls-test-utils == 2.11.0.0 + , text + +----------------------------- +-- alternate number format plugin +----------------------------- + +flag alternateNumberFormat + description: Enable Alternate Number Format plugin + default: True + manual: True + +common alternateNumberFormat + if flag(alternateNumberFormat) + build-depends: haskell-language-server:hls-alternate-number-format-plugin + cpp-options: -Dhls_alternateNumberFormat + +library hls-alternate-number-format-plugin + import: defaults, pedantic, warnings + if !flag(alternateNumberFormat) + buildable: False + exposed-modules: Ide.Plugin.AlternateNumberFormat, Ide.Plugin.Conversion + other-modules: Ide.Plugin.Literals + hs-source-dirs: plugins/hls-alternate-number-format-plugin/src + build-depends: + , containers + , extra + , ghcide == 2.11.0.0 + , ghc-boot-th + , hls-graph + , hls-plugin-api == 2.11.0.0 + , lens + , lsp ^>=2.7 + , mtl + , regex-tdfa + , syb + , text + + default-extensions: + LambdaCase + OverloadedStrings + RecordWildCards + +test-suite hls-alternate-number-format-plugin-tests + import: defaults, pedantic, test-defaults, warnings + if !flag(alternateNumberFormat) + buildable: False + type: exitcode-stdio-1.0 + hs-source-dirs: plugins/hls-alternate-number-format-plugin/test + other-modules: Properties.Conversion + main-is: Main.hs + ghc-options: -fno-ignore-asserts + build-depends: + , filepath + , haskell-language-server:hls-alternate-number-format-plugin + , hls-test-utils == 2.11.0.0 + , regex-tdfa + , tasty-quickcheck + , text + + default-extensions: + LambdaCase + OverloadedStrings + RecordWildCards + +----------------------------- +-- qualify imported names plugin +----------------------------- + +flag qualifyImportedNames + description: Enable qualifyImportedNames plugin + default: True + manual: True + +common qualifyImportedNames + if flag(qualifyImportedNames) + build-depends: haskell-language-server:hls-qualify-imported-names-plugin + cpp-options: -Dhls_qualifyImportedNames + +library hls-qualify-imported-names-plugin + import: defaults, pedantic, warnings + if !flag(qualifyImportedNames) + buildable: False + exposed-modules: Ide.Plugin.QualifyImportedNames + hs-source-dirs: plugins/hls-qualify-imported-names-plugin/src + build-depends: + , containers + , ghcide == 2.11.0.0 + , hls-plugin-api == 2.11.0.0 + , lens + , lsp + , text + , text-rope + , dlist + , transformers + + default-extensions: + DataKinds + +test-suite hls-qualify-imported-names-plugin-tests + import: defaults, pedantic, test-defaults, warnings + if !flag(qualifyImportedNames) + buildable: False + type: exitcode-stdio-1.0 + hs-source-dirs: plugins/hls-qualify-imported-names-plugin/test + main-is: Main.hs + build-depends: + , text + , filepath + , haskell-language-server:hls-qualify-imported-names-plugin + , hls-test-utils == 2.11.0.0 + +----------------------------- +-- code range plugin +----------------------------- + +flag codeRange + description: Enable Code Range plugin + default: True + manual: True + +common codeRange + if flag(codeRange) + build-depends: haskell-language-server:hls-code-range-plugin + cpp-options: -Dhls_codeRange + +library hls-code-range-plugin + import: defaults, pedantic, warnings + if !flag(codeRange) + buildable: False + exposed-modules: + Ide.Plugin.CodeRange + Ide.Plugin.CodeRange.Rules + other-modules: + Ide.Plugin.CodeRange.ASTPreProcess + hs-source-dirs: plugins/hls-code-range-plugin/src + build-depends: + , containers + , deepseq + , extra + , ghcide == 2.11.0.0 + , hashable + , hls-plugin-api == 2.11.0.0 + , lens + , lsp + , mtl + , semigroupoids + , transformers + , vector + +test-suite hls-code-range-plugin-tests + import: defaults, pedantic, test-defaults, warnings + if !flag(codeRange) + buildable: False + type: exitcode-stdio-1.0 + hs-source-dirs: plugins/hls-code-range-plugin/test + main-is: Main.hs + other-modules: + Ide.Plugin.CodeRangeTest + Ide.Plugin.CodeRange.RulesTest + build-depends: + , bytestring + , filepath + , haskell-language-server:hls-code-range-plugin + , hls-test-utils == 2.11.0.0 + , lens + , lsp + , lsp-test + , transformers + , vector + +----------------------------- +-- change type signature plugin +----------------------------- + +flag changeTypeSignature + description: Enable changeTypeSignature plugin + default: True + manual: True + +common changeTypeSignature + if flag(changeTypeSignature) + build-depends: haskell-language-server:hls-change-type-signature-plugin + cpp-options: -Dhls_changeTypeSignature + +library hls-change-type-signature-plugin + import: defaults, pedantic, warnings + if !flag(changeTypeSignature) + buildable: False + exposed-modules: Ide.Plugin.ChangeTypeSignature + hs-source-dirs: plugins/hls-change-type-signature-plugin/src + build-depends: + , ghcide == 2.11.0.0 + , hls-plugin-api == 2.11.0.0 + , lsp-types + , regex-tdfa + , syb + , text + , transformers + , containers + default-extensions: + DataKinds + ExplicitNamespaces + OverloadedStrings + RecordWildCards + + +test-suite hls-change-type-signature-plugin-tests + import: defaults, pedantic, test-defaults, warnings + if !flag(changeTypeSignature) + buildable: False + type: exitcode-stdio-1.0 + hs-source-dirs: plugins/hls-change-type-signature-plugin/test + main-is: Main.hs + build-depends: + , filepath + , haskell-language-server:hls-change-type-signature-plugin + , hls-test-utils == 2.11.0.0 + , regex-tdfa + , text + default-extensions: + OverloadedStrings + ViewPatterns + +----------------------------- +-- gadt plugin +----------------------------- + +flag gadt + description: Enable gadt plugin + default: True + manual: True + +common gadt + if flag(gadt) + build-depends: haskell-language-server:hls-gadt-plugin + cpp-options: -Dhls_gadt + +library hls-gadt-plugin + import: defaults, pedantic, warnings + if !flag(gadt) + buildable: False + exposed-modules: Ide.Plugin.GADT + other-modules: Ide.Plugin.GHC + hs-source-dirs: plugins/hls-gadt-plugin/src + build-depends: + , aeson + , containers + , extra + , ghc + , ghcide == 2.11.0.0 + , ghc-exactprint + , hls-plugin-api == 2.11.0.0 + , haskell-language-server:hls-refactor-plugin + , lens + , lsp >=2.7 + , mtl + , text + , transformers + + default-extensions: DataKinds + +test-suite hls-gadt-plugin-tests + import: defaults, pedantic, test-defaults, warnings + if !flag(gadt) + buildable: False + type: exitcode-stdio-1.0 + hs-source-dirs: plugins/hls-gadt-plugin/test + main-is: Main.hs + build-depends: + , filepath + , haskell-language-server:hls-gadt-plugin + , hls-test-utils == 2.11.0.0 + , text + +----------------------------- +-- explicit fixity plugin +----------------------------- + +flag explicitFixity + description: Enable explicitFixity plugin + default: True + manual: True + +common explicitFixity + if flag(explicitFixity) + build-depends: haskell-language-server:hls-explicit-fixity-plugin + cpp-options: -DexplicitFixity + +library hls-explicit-fixity-plugin + import: defaults, pedantic, warnings + if !flag(explicitFixity) + buildable: False + exposed-modules: Ide.Plugin.ExplicitFixity + hs-source-dirs: plugins/hls-explicit-fixity-plugin/src + build-depends: + , containers + , deepseq + , extra + , ghcide == 2.11.0.0 + , hashable + , hls-plugin-api == 2.11.0.0 + , lsp >=2.7 + , text + + default-extensions: DataKinds + +test-suite hls-explicit-fixity-plugin-tests + import: defaults, pedantic, test-defaults, warnings + if !flag(explicitFixity) + buildable: False + type: exitcode-stdio-1.0 + hs-source-dirs: plugins/hls-explicit-fixity-plugin/test + main-is: Main.hs + build-depends: + , filepath + , haskell-language-server:hls-explicit-fixity-plugin + , hls-test-utils == 2.11.0.0 + , text + +----------------------------- +-- explicit fields plugin +----------------------------- + +flag explicitFields + description: Enable explicitFields plugin + default: True + manual: True + +common explicitFields + if flag(explicitFields) + build-depends: haskell-language-server:hls-explicit-record-fields-plugin + cpp-options: -DexplicitFields + +library hls-explicit-record-fields-plugin + import: defaults, pedantic, warnings + if !flag(explicitFields) + buildable: False + exposed-modules: Ide.Plugin.ExplicitFields + build-depends: + , ghcide == 2.11.0.0 + , hls-plugin-api == 2.11.0.0 + , lsp + , lens + , hls-graph + , text + , syb + , transformers + , containers + , aeson + hs-source-dirs: plugins/hls-explicit-record-fields-plugin/src + + if flag(pedantic) + ghc-options: -Wwarn=incomplete-record-updates + +test-suite hls-explicit-record-fields-plugin-tests + import: defaults, pedantic, test-defaults, warnings + if !flag(explicitFields) + buildable: False + type: exitcode-stdio-1.0 + hs-source-dirs: plugins/hls-explicit-record-fields-plugin/test + main-is: Main.hs + build-depends: + , filepath + , text + , ghcide + , haskell-language-server:hls-explicit-record-fields-plugin + , hls-test-utils == 2.11.0.0 + +----------------------------- +-- overloaded record dot plugin +----------------------------- + +flag overloadedRecordDot + description: Enable overloadedRecordDot plugin + default: True + manual: True + +common overloadedRecordDot + if flag(overloadedRecordDot) + build-depends: haskell-language-server:hls-overloaded-record-dot-plugin + cpp-options: -Dhls_overloaded_record_dot + +library hls-overloaded-record-dot-plugin + import: defaults, pedantic, warnings + if !flag(overloadedRecordDot) + buildable: False + exposed-modules: Ide.Plugin.OverloadedRecordDot + build-depends: + , aeson + , ghcide + , hls-plugin-api + , lsp + , lens + , hls-graph + , text + , syb + , transformers + , containers + , deepseq + hs-source-dirs: plugins/hls-overloaded-record-dot-plugin/src + +test-suite hls-overloaded-record-dot-plugin-tests + import: defaults, pedantic, test-defaults, warnings + if !flag(overloadedRecordDot) + buildable: False + type: exitcode-stdio-1.0 + hs-source-dirs: plugins/hls-overloaded-record-dot-plugin/test + main-is: Main.hs + build-depends: + , filepath + , text + , haskell-language-server:hls-overloaded-record-dot-plugin + , hls-test-utils == 2.11.0.0 + + +----------------------------- +-- floskell plugin +----------------------------- + +flag floskell + description: Enable floskell plugin + default: True + manual: True + +common floskell + if flag(floskell) && (impl(ghc < 9.10) || flag(ignore-plugins-ghc-bounds)) + build-depends: haskell-language-server:hls-floskell-plugin + cpp-options: -Dhls_floskell + +library hls-floskell-plugin + import: defaults, pedantic, warnings + -- https://github.com/ennocramer/floskell/pull/82 + if !(flag(floskell) && (impl(ghc < 9.10) || flag(ignore-plugins-ghc-bounds))) + buildable: False + exposed-modules: Ide.Plugin.Floskell + hs-source-dirs: plugins/hls-floskell-plugin/src + build-depends: + , floskell ^>=0.11.0 + , ghcide == 2.11.0.0 + , hls-plugin-api == 2.11.0.0 + , lsp-types ^>=2.3 + , mtl + , text + + +test-suite hls-floskell-plugin-tests + import: defaults, pedantic, test-defaults, warnings + if !(flag(floskell) && (impl(ghc < 9.10) || flag(ignore-plugins-ghc-bounds))) + buildable: False + type: exitcode-stdio-1.0 + hs-source-dirs: plugins/hls-floskell-plugin/test + main-is: Main.hs + build-depends: + , filepath + , haskell-language-server:hls-floskell-plugin + , hls-test-utils == 2.11.0.0 + +----------------------------- +-- fourmolu plugin +----------------------------- + +flag fourmolu + description: Enable fourmolu plugin + default: True + manual: True + +common fourmolu + if flag(fourmolu) + build-depends: haskell-language-server:hls-fourmolu-plugin + cpp-options: -Dhls_fourmolu + +library hls-fourmolu-plugin + import: defaults, pedantic, warnings + if !flag(fourmolu) + buildable: False + exposed-modules: Ide.Plugin.Fourmolu + hs-source-dirs: plugins/hls-fourmolu-plugin/src + build-depends: + , filepath + , fourmolu ^>= 0.14 || ^>= 0.15 || ^>= 0.16 || ^>=0.17 || ^>=0.18 + , ghc-boot-th + , ghcide == 2.11.0.0 + , hls-plugin-api == 2.11.0.0 + , lens + , lsp + , mtl + , process-extras >= 0.7.1 + , text + , transformers + , yaml + +test-suite hls-fourmolu-plugin-tests + import: defaults, pedantic, test-defaults, warnings + if !flag(fourmolu) + buildable: False + type: exitcode-stdio-1.0 + hs-source-dirs: plugins/hls-fourmolu-plugin/test + main-is: Main.hs + -- Work around https://gitlab.haskell.org/ghc/ghc/-/issues/24648 + if os(darwin) + ghc-options: -optl-Wl,-ld_classic + build-tool-depends: + fourmolu:fourmolu + build-depends: + , aeson + , filepath + , haskell-language-server:hls-fourmolu-plugin + , hls-plugin-api + , hls-test-utils == 2.11.0.0 + , lsp-test + +----------------------------- +-- ormolu plugin +----------------------------- + +flag ormolu + description: Enable ormolu plugin + default: True + manual: True + +common ormolu + if flag(ormolu) + build-depends: haskell-language-server:hls-ormolu-plugin + cpp-options: -Dhls_ormolu + +library hls-ormolu-plugin + import: defaults, pedantic, warnings + if !flag(ormolu) + buildable: False + exposed-modules: Ide.Plugin.Ormolu + hs-source-dirs: plugins/hls-ormolu-plugin/src + build-depends: + , extra + , filepath + , ghc-boot-th + , ghcide == 2.11.0.0 + , hls-plugin-api == 2.11.0.0 + , lsp + , mtl + , process-extras >= 0.7.1 + , ormolu ^>=0.5.3 || ^>= 0.6 || ^>= 0.7 || ^>=0.8 + , text + , transformers + + +test-suite hls-ormolu-plugin-tests + import: defaults, pedantic, test-defaults, warnings + if !flag(ormolu) + buildable: False + type: exitcode-stdio-1.0 + hs-source-dirs: plugins/hls-ormolu-plugin/test + main-is: Main.hs + -- Work around https://gitlab.haskell.org/ghc/ghc/-/issues/24648 + if os(darwin) + ghc-options: -optl-Wl,-ld_classic + build-tool-depends: + ormolu:ormolu + build-depends: + , aeson + , filepath + , haskell-language-server:hls-ormolu-plugin + , hls-plugin-api + , hls-test-utils == 2.11.0.0 + , lsp-types + , ormolu + +----------------------------- +-- stylish-haskell plugin +----------------------------- + +flag stylishHaskell + description: Enable stylishHaskell plugin + default: True + manual: True + +common stylishHaskell + if flag(stylishHaskell) + build-depends: haskell-language-server:hls-stylish-haskell-plugin + cpp-options: -Dhls_stylishHaskell -common hlint - if flag(hlint) && (impl(ghc < 9.8.0) || flag(ignore-plugins-ghc-bounds)) - build-depends: hls-hlint-plugin == 2.4.0.0 - cpp-options: -Dhls_hlint +library hls-stylish-haskell-plugin + import: defaults, pedantic, warnings + -- https://github.com/haskell/stylish-haskell/issues/479 + if !flag(stylishHaskell) + buildable: False + exposed-modules: Ide.Plugin.StylishHaskell + hs-source-dirs: plugins/hls-stylish-haskell-plugin/src + build-depends: + , directory + , filepath + , ghc-boot-th + , ghcide == 2.11.0.0 + , hls-plugin-api == 2.11.0.0 + , lsp-types + , mtl + , stylish-haskell >=0.12 && <0.16 + , text -common stan - if flag(stan) && (impl(ghc > 8.8.1) && impl(ghc <= 9.2.3) || impl(ghc >= 9.4.0) && impl(ghc < 9.10.0)) - build-depends: hls-stan-plugin == 2.4.0.0 - cpp-options: -Dhls_stan -common moduleName - if flag(moduleName) - build-depends: hls-module-name-plugin == 2.4.0.0 - cpp-options: -Dhls_moduleName +test-suite hls-stylish-haskell-plugin-tests + import: defaults, pedantic, test-defaults, warnings + if !flag(stylishHaskell) + buildable: False + type: exitcode-stdio-1.0 + hs-source-dirs: plugins/hls-stylish-haskell-plugin/test + main-is: Main.hs + build-depends: + , filepath + , haskell-language-server:hls-stylish-haskell-plugin + , hls-test-utils == 2.11.0.0 -common pragmas - if flag(pragmas) - build-depends: hls-pragmas-plugin == 2.4.0.0 - cpp-options: -Dhls_pragmas +----------------------------- +-- refactor plugin +----------------------------- -common splice - if flag(splice) && (impl(ghc < 9.8.0) || flag(ignore-plugins-ghc-bounds)) - build-depends: hls-splice-plugin == 2.4.0.0 - cpp-options: -Dhls_splice +flag refactor + description: Enable refactor plugin + default: True + manual: True -common alternateNumberFormat - if flag(alternateNumberFormat) - build-depends: hls-alternate-number-format-plugin == 2.4.0.0 - cpp-options: -Dhls_alternateNumberFormat +common refactor + if flag(refactor) + build-depends: haskell-language-server:hls-refactor-plugin + cpp-options: -Dhls_refactor -common qualifyImportedNames - if flag(qualifyImportedNames) - build-depends: hls-qualify-imported-names-plugin == 2.4.0.0 - cpp-options: -Dhls_qualifyImportedNames +library hls-refactor-plugin + import: defaults, pedantic, warnings + if !flag(refactor) + buildable: False + exposed-modules: Development.IDE.GHC.ExactPrint + Development.IDE.GHC.Compat.ExactPrint + Development.IDE.Plugin.CodeAction + Development.IDE.Plugin.CodeAction.Util + Development.IDE.GHC.Dump + other-modules: Development.IDE.Plugin.CodeAction.Args + Development.IDE.Plugin.CodeAction.ExactPrint + Development.IDE.Plugin.CodeAction.PositionIndexed + Development.IDE.Plugin.Plugins.AddArgument + Development.IDE.Plugin.Plugins.Diagnostic + Development.IDE.Plugin.Plugins.FillHole + Development.IDE.Plugin.Plugins.FillTypeWildcard + Development.IDE.Plugin.Plugins.ImportUtils + default-extensions: + CPP + DataKinds + DerivingStrategies + DerivingVia + DuplicateRecordFields + ExplicitNamespaces + FunctionalDependencies + LambdaCase + OverloadedStrings + PatternSynonyms + RecordWildCards + ViewPatterns + hs-source-dirs: plugins/hls-refactor-plugin/src + build-depends: + , ghc + , bytestring + , ghc-boot + , regex-tdfa + , ghcide == 2.11.0.0 + , hls-plugin-api == 2.11.0.0 + , lsp + , text + , text-rope + , transformers + , unordered-containers + , containers + , ghc-exactprint < 1 || >= 1.4 + , extra + , syb + , hls-graph + , dlist + , deepseq + , mtl + , lens + , time + -- FIXME: Only needed to workaround for qualified imports in GHC 9.4 + , regex-applicative + , parser-combinators + if impl(ghc < 9.10) + build-depends: data-default + +test-suite hls-refactor-plugin-tests + import: defaults, pedantic, test-defaults, warnings + if !flag(refactor) + buildable: False + type: exitcode-stdio-1.0 + hs-source-dirs: plugins/hls-refactor-plugin/test + main-is: Main.hs + other-modules: Test.AddArgument + ghc-options: -O0 + build-depends: + , data-default + , directory + , extra + , filepath + , ghcide:ghcide + , haskell-language-server:hls-refactor-plugin + , hls-test-utils == 2.11.0.0 + , lens + , lsp-test + , lsp-types + , parser-combinators + , regex-tdfa + , shake + , tasty + , tasty-expected-failure + , tasty-hunit + , text -common codeRange - if flag(codeRange) - build-depends: hls-code-range-plugin == 2.4.0.0 - cpp-options: -Dhls_codeRange +----------------------------- +-- semantic tokens plugin +----------------------------- -common changeTypeSignature - if flag(changeTypeSignature) - build-depends: hls-change-type-signature-plugin == 2.4.0.0 - cpp-options: -Dhls_changeTypeSignature +flag semanticTokens + description: Enable semantic tokens plugin + default: True + manual: True -common gadt - if flag(gadt) && (impl(ghc < 9.8.0) || flag(ignore-plugins-ghc-bounds)) - build-depends: hls-gadt-plugin == 2.4.0.0 - cpp-options: -Dhls_gadt +common semanticTokens + if flag(semanticTokens) + build-depends: haskell-language-server:hls-semantic-tokens-plugin + cpp-options: -Dhls_semanticTokens -common explicitFixity - if flag(explicitFixity) - build-depends: hls-explicit-fixity-plugin == 2.4.0.0 - cpp-options: -DexplicitFixity +library hls-semantic-tokens-plugin + import: defaults, pedantic, warnings + if !flag(semanticTokens) + buildable: False + exposed-modules: + Ide.Plugin.SemanticTokens + Ide.Plugin.SemanticTokens.Types + Ide.Plugin.SemanticTokens.Mappings + other-modules: + Ide.Plugin.SemanticTokens.Query + Ide.Plugin.SemanticTokens.SemanticConfig + Ide.Plugin.SemanticTokens.Utils + Ide.Plugin.SemanticTokens.Tokenize + Ide.Plugin.SemanticTokens.Internal -common explicitFields - if flag(explicitFields) - build-depends: hls-explicit-record-fields-plugin == 2.4.0.0 - cpp-options: -DexplicitFields + hs-source-dirs: plugins/hls-semantic-tokens-plugin/src + build-depends: + , containers + , extra + , text-rope + , mtl >= 2.2 + , ghcide == 2.11.0.0 + , hls-plugin-api == 2.11.0.0 + , lens + , lsp >=2.6 + , text + , transformers + , bytestring + , syb + , array + , deepseq + , dlist + , hls-graph == 2.11.0.0 + , template-haskell + , data-default + , stm + , stm-containers -common overloadedRecordDot - if flag(overloadedRecordDot) && (impl(ghc >= 9.2.0) || flag(ignore-plugins-ghc-bounds)) - build-depends: hls-overloaded-record-dot-plugin == 2.4.0.0 - cpp-options: -Dhls_overloaded_record_dot + default-extensions: DataKinds --- formatters +test-suite hls-semantic-tokens-plugin-tests + import: defaults, pedantic, test-defaults, warnings + if !flag(semanticTokens) + buildable: False + type: exitcode-stdio-1.0 + hs-source-dirs: plugins/hls-semantic-tokens-plugin/test + main-is: SemanticTokensTest.hs -common floskell - if flag(floskell) && impl(ghc < 9.5) - build-depends: hls-floskell-plugin == 2.4.0.0 - cpp-options: -Dhls_floskell + build-depends: + , aeson + , containers + , data-default + , filepath + , ghcide == 2.11.0.0 + , haskell-language-server:hls-semantic-tokens-plugin + , hls-plugin-api == 2.11.0.0 + , hls-test-utils == 2.11.0.0 + , lens + , lsp + , lsp-test + , text + , text-rope -common fourmolu - if flag(fourmolu) && (impl(ghc < 9.8.0) || flag(ignore-plugins-ghc-bounds)) - build-depends: hls-fourmolu-plugin == 2.4.0.0 - cpp-options: -Dhls_fourmolu +----------------------------- +-- notes plugin +----------------------------- -common ormolu - if flag(ormolu) && impl(ghc < 9.7) - build-depends: hls-ormolu-plugin == 2.4.0.0 - cpp-options: -Dhls_ormolu +flag notes + description: Enable notes plugin + default: True + manual: True -common stylishHaskell - if flag(stylishHaskell) && (impl(ghc < 9.8.0) || flag(ignore-plugins-ghc-bounds)) - build-depends: hls-stylish-haskell-plugin == 2.4.0.0 - cpp-options: -Dhls_stylishHaskell +common notes + if flag(notes) + build-depends: haskell-language-server:hls-notes-plugin + cpp-options: -Dhls_notes -common refactor - if flag(refactor) && (impl(ghc < 9.8.0) || flag(ignore-plugins-ghc-bounds)) - build-depends: hls-refactor-plugin == 2.4.0.0 - cpp-options: -Dhls_refactor +library hls-notes-plugin + import: defaults, pedantic, warnings + if !flag(notes) + buildable: False + exposed-modules: + Ide.Plugin.Notes + hs-source-dirs: plugins/hls-notes-plugin/src + build-depends: + , array + , ghcide == 2.11.0.0 + , hls-graph == 2.11.0.0 + , hls-plugin-api == 2.11.0.0 + , lens + , lsp >=2.7 + , mtl >= 2.2 + , regex-tdfa >= 1.3.1 + , text + , text-rope + , unordered-containers + default-extensions: + DataKinds + , DeriveAnyClass + , DerivingStrategies + , OverloadedStrings + , LambdaCase + , TypeFamilies + +test-suite hls-notes-plugin-tests + import: defaults, pedantic, test-defaults, warnings + if !flag(notes) + buildable: False + type: exitcode-stdio-1.0 + hs-source-dirs: plugins/hls-notes-plugin/test + main-is: NotesTest.hs + build-depends: + , filepath + , haskell-language-server:hls-notes-plugin + , hls-test-utils == 2.11.0.0 + default-extensions: OverloadedStrings + +---------------------------- +---------------------------- +-- HLS +---------------------------- +---------------------------- library - import: common-deps - -- configuration + import: defaults , warnings , pedantic -- plugins , cabal , callHierarchy , cabalfmt + , cabalgild , changeTypeSignature , class , eval @@ -365,6 +1861,8 @@ library , stylishHaskell , refactor , overloadedRecordDot + , semanticTokens + , notes exposed-modules: Ide.Arguments @@ -376,34 +1874,26 @@ library autogen-modules: Paths_haskell_language_server hs-source-dirs: src build-depends: - , async - , base16-bytestring - , bytestring - , containers - , cryptohash-sha1 + , aeson-pretty , data-default + , directory + , extra + , filepath , ghc - , ghcide == 2.4.0.0 + , ghcide == 2.11.0.0 , githash >=0.1.6.1 - , lsp >= 2.3.0.0 , hie-bios - , hiedb - , hls-plugin-api == 2.4.0.0 + , hls-plugin-api == 2.11.0.0 , optparse-applicative , optparse-simple + , prettyprinter >= 1.7 , process - , hls-graph - , safe-exceptions - , sqlite-simple - , unordered-containers - , aeson-pretty + , text - default-language: Haskell2010 - default-extensions: DataKinds, TypeOperators + default-extensions: DataKinds executable haskell-language-server - import: common-deps - -- configuration + import: defaults , warnings , pedantic main-is: Main.hs @@ -417,7 +1907,6 @@ executable haskell-language-server -- increase nursery size -- Enable collection of heap statistics "-with-rtsopts=-I0 -A128M -T" - -Wno-unticked-promoted-constructors if flag(pedantic) ghc-options: -Werror if !os(windows) && flag(dynamic) @@ -431,42 +1920,16 @@ executable haskell-language-server ghc-options: -dynamic build-depends: - , aeson - , async - , base16-bytestring - , binary - , bytestring - , containers - , cryptohash-sha1 - , deepseq - , ghc - , ghc-boot-th - , ghcide - , hashable , haskell-language-server - , lsp - , hie-bios - , hiedb - , lens - , regex-tdfa - , optparse-applicative , hls-plugin-api - , lens - , mtl - , regex-tdfa - , safe-exceptions - , hls-graph - , sqlite-simple - , stm - , temporary - , transformers - , unordered-containers + , lsp + , prettyprinter >= 1.7 + , text - default-language: Haskell2010 - default-extensions: DataKinds, TypeOperators + default-extensions: DataKinds executable haskell-language-server-wrapper - import: common-deps + import: defaults , warnings , pedantic main-is: Wrapper.hs @@ -483,64 +1946,59 @@ executable haskell-language-server-wrapper build-depends: , data-default - , ghc - , ghc-paths + , directory + , extra + , filepath , ghcide - , gitrev , haskell-language-server , hie-bios , hls-plugin-api , lsp , lsp-types - , mtl - , optparse-applicative - , optparse-simple - , process + , text , transformers , unliftio-core if !os(windows) build-depends: - unix + , unix , containers - - default-language: Haskell2010 - + else + build-depends: + , process test-suite func-test - import: common-deps + import: defaults + , test-defaults , warnings , pedantic , refactor type: exitcode-stdio-1.0 - default-language: Haskell2010 build-tool-depends: - haskell-language-server:haskell-language-server -any, - ghcide:ghcide-test-preprocessor -any + haskell-language-server:haskell-language-server, build-depends: + , aeson , bytestring - , data-default + , containers , deepseq + , extra + , filepath + , ghcide:ghcide , hashable - , hspec-expectations - , lens - , lens-aeson - , ghcide - , ghcide-test-utils - , hls-test-utils == 2.4.0.0 - , lsp-types - , aeson , hls-plugin-api + , hls-test-utils == 2.11.0.0 + , lens , lsp-test - , containers + , lsp-types + , text , unordered-containers - , row-types hs-source-dirs: test/functional test/utils main-is: Main.hs other-modules: Config + ConfigSchema Format FunctionalBadProject HieBios @@ -549,14 +2007,12 @@ test-suite func-test Test.Hls.Flags default-extensions: OverloadedStrings - ghc-options: - -threaded -rtsopts -with-rtsopts=-N -- Duplicating inclusion plugin conditions until tests are moved to their own packages if flag(eval) cpp-options: -Dhls_eval -- formatters - if flag(floskell) && (impl(ghc < 9.2.1) || flag(ignore-plugins-ghc-bounds)) + if flag(floskell) && (impl(ghc < 9.10) || flag(ignore-plugins-ghc-bounds)) cpp-options: -Dhls_floskell if flag(fourmolu) cpp-options: -Dhls_fourmolu @@ -564,65 +2020,278 @@ test-suite func-test cpp-options: -Dhls_ormolu test-suite wrapper-test - import: common-deps + import: defaults , warnings , pedantic type: exitcode-stdio-1.0 build-tool-depends: - haskell-language-server:haskell-language-server-wrapper -any, - haskell-language-server:haskell-language-server -any + haskell-language-server:haskell-language-server-wrapper, + haskell-language-server:haskell-language-server - default-language: Haskell2010 build-depends: - process - , hls-test-utils + , extra + , hls-test-utils == 2.11.0.0 + , process hs-source-dirs: test/wrapper main-is: Main.hs benchmark benchmark + import: defaults, warnings -- Depends on shake-bench which is unbuildable after this point - if impl(ghc >= 9.5) - buildable: False type: exitcode-stdio-1.0 - default-language: Haskell2010 - ghc-options: -Wall -Wno-name-shadowing -threaded + ghc-options: -threaded main-is: Main.hs hs-source-dirs: bench build-tool-depends: - ghcide-bench:ghcide-bench, - hp2pretty:hp2pretty, - implicit-hie:gen-hie + haskell-language-server:ghcide-bench, + eventlog2html:eventlog2html, default-extensions: - BangPatterns - DeriveFunctor - DeriveGeneric - FlexibleContexts - GeneralizedNewtypeDeriving LambdaCase - NamedFieldPuns RecordWildCards - ScopedTypeVariables - StandaloneDeriving - TupleSections - TypeApplications ViewPatterns + build-depends: + , aeson + , containers + , data-default + , directory + , extra + , filepath + , haskell-language-server:ghcide-bench-lib + , haskell-language-server + , hls-plugin-api + , lens + , lens-aeson + , shake + , shake-bench == 0.2.* + , text + , yaml + +flag test-exe + description: Build the ghcide-test-preprocessor executable + default: True + +executable ghcide-test-preprocessor + import: warnings + default-language: GHC2021 + hs-source-dirs: ghcide-test/preprocessor + main-is: Main.hs + build-depends: base >=4 && <5 + + if !flag(test-exe) + buildable: False + +test-suite ghcide-tests + import: warnings, defaults + type: exitcode-stdio-1.0 + default-language: GHC2021 + build-tool-depends: + , ghcide:ghcide + , haskell-language-server:ghcide-test-preprocessor + , implicit-hie:gen-hie + + build-depends: + , aeson + , containers + , data-default + , directory + , enummapset + , extra + , filepath + , ghcide + , hls-plugin-api + , lens + , list-t + , lsp + , lsp-test ^>=0.17.1 + , lsp-types + , mtl + , network-uri + , QuickCheck + , random + , regex-tdfa ^>=1.3.1 + , shake + , sqlite-simple + , stm + , stm-containers + , tasty + , tasty-expected-failure + , tasty-hunit >=0.10 + , tasty-quickcheck + , tasty-rerun + , text + , text-rope + , unordered-containers + , hls-test-utils == 2.11.0.0 + + if impl(ghc <9.3) + build-depends: ghc-typelits-knownnat + + hs-source-dirs: ghcide-test/exe + ghc-options: -threaded -O0 + + main-is: Main.hs + other-modules: + Config + AsyncTests + BootTests + ClientSettingsTests + CodeLensTests + CompletionTests + CPPTests + CradleTests + DependentFileTest + DiagnosticTests + ExceptionTests + FindDefinitionAndHoverTests + FindImplementationAndHoverTests + FuzzySearch + GarbageCollectionTests + HaddockTests + HieDbRetry + HighlightTests + IfaceTests + InitializeResponseTests + LogType + NonLspCommandLine + OpenCloseTest + OutlineTests + PluginSimpleTests + PositionMappingTests + PreprocessorTests + Progress + ReferenceTests + ResolveTests + RootUriTests + SafeTests + SymlinkTests + THTests + UnitTests + WatchedFileTests + + -- Tests that have been pulled out of the main file + default-extensions: + LambdaCase + OverloadedStrings + RecordWildCards + ViewPatterns + +flag ghcide-bench + description: Build the ghcide-bench executable + default: True + +executable ghcide-bench + import: defaults + if !flag(ghcide-bench) + buildable: False build-depends: aeson, - base == 4.*, + bytestring, containers, data-default, - directory, extra, filepath, - ghcide-bench, - haskell-language-server, hls-plugin-api, + hls-test-utils, lens, - lens-aeson, + lsp-test, + lsp-types, optparse-applicative, + process, + safe-exceptions, + hls-graph, shake, - shake-bench == 0.2.*, + tasty-hunit >= 0.10, text, - yaml + haskell-language-server:ghcide-bench-lib, + hs-source-dirs: ghcide-bench/exe + ghc-options: -threaded -Wall -Wno-name-shadowing -rtsopts + main-is: Main.hs + default-extensions: + LambdaCase + OverloadedStrings + RecordWildCards + ViewPatterns + +library ghcide-bench-lib + import: defaults + hs-source-dirs: ghcide-bench/src + ghc-options: -Wall -Wno-name-shadowing + exposed-modules: + Experiments.Types + Experiments + build-depends: + aeson, + async, + binary, + bytestring, + deepseq, + directory, + extra, + filepath, + ghcide:{ghcide}, + hashable, + lens, + lsp-test, + lsp-types, + optparse-applicative, + parser-combinators, + process, + safe-exceptions, + shake, + text, + hls-test-utils, + row-types + default-extensions: + LambdaCase + RecordWildCards + ViewPatterns + + +test-suite ghcide-bench-test + import: defaults + type: exitcode-stdio-1.0 + build-tool-depends: + ghcide:ghcide, + main-is: Main.hs + hs-source-dirs: ghcide-bench/test + ghc-options: -Wunused-packages + ghc-options: -threaded -Wall + build-depends: + extra, + haskell-language-server:ghcide-bench-lib, + lsp-test ^>= 0.17, + tasty, + tasty-hunit >= 0.10, + tasty-rerun + default-extensions: + LambdaCase + OverloadedStrings + RecordWildCards + ViewPatterns + +executable plugin-tutorial + import: defaults + -- The plugin tutorial is only compatible with 9.6 and 9.8. + -- No particular reason, just to avoid excessive CPP. + if (impl(ghc >= 9.6) && impl(ghc < 9.10)) + buildable: True + else + buildable: False + ghc-options: -pgmL markdown-unlit + main-is: docs/contributing/plugin-tutorial.lhs + build-tool-depends: markdown-unlit:markdown-unlit + build-depends: + base, + ghcide, + hls-plugin-api, + aeson, + lsp, + lsp-types, + markdown-unlit, + text, + unordered-containers, + containers, + transformers, + ghc, diff --git a/hie-compat/hie-compat.cabal b/hie-compat/hie-compat.cabal index 2b9e78d323..2b361df887 100644 --- a/hie-compat/hie-compat.cabal +++ b/hie-compat/hie-compat.cabal @@ -22,9 +22,9 @@ source-repository head location: https://github.com/haskell/haskell-language-server.git library - default-language: Haskell2010 + default-language: GHC2021 build-depends: - base < 4.20, array, bytestring, containers, directory, filepath, transformers + base < 4.22, array, bytestring, containers, directory, filepath, transformers build-depends: ghc >= 8.10, ghc-boot ghc-options: -Wall -Wno-name-shadowing @@ -35,9 +35,5 @@ library Compat.HieDebug Compat.HieUtils - if (impl(ghc >= 9.0) && impl(ghc < 9.1)) - hs-source-dirs: src-ghc90 src-reexport-ghc9 - if (impl(ghc >= 9.2) && impl(ghc < 9.3)) - hs-source-dirs: src-ghc92 src-reexport-ghc9 if (impl(ghc >= 9.4)) hs-source-dirs: src-reexport-ghc92 diff --git a/hie-compat/src-ghc90/Compat/HieAst.hs b/hie-compat/src-ghc90/Compat/HieAst.hs deleted file mode 100644 index c6d0260f6b..0000000000 --- a/hie-compat/src-ghc90/Compat/HieAst.hs +++ /dev/null @@ -1,3 +0,0 @@ -module Compat.HieAst ( enrichHie ) where - -import GHC.Iface.Ext.Ast (enrichHie) diff --git a/hie-compat/src-ghc92/Compat/HieAst.hs b/hie-compat/src-ghc92/Compat/HieAst.hs index 6d887c46a0..3445ff6213 100644 --- a/hie-compat/src-ghc92/Compat/HieAst.hs +++ b/hie-compat/src-ghc92/Compat/HieAst.hs @@ -1,14 +1,8 @@ {-# LANGUAGE AllowAmbiguousTypes #-} {-# LANGUAGE CPP #-} -{-# LANGUAGE ConstraintKinds #-} {-# LANGUAGE DataKinds #-} -{-# LANGUAGE DeriveDataTypeable #-} -{-# LANGUAGE FlexibleContexts #-} -{-# LANGUAGE FlexibleInstances #-} {-# LANGUAGE GADTs #-} {-# LANGUAGE OverloadedStrings #-} -{-# LANGUAGE ScopedTypeVariables #-} -{-# LANGUAGE TypeApplications #-} {-# LANGUAGE TypeFamilies #-} {-# LANGUAGE UndecidableInstances #-} {-# LANGUAGE UndecidableSuperClasses #-} @@ -78,7 +72,7 @@ import qualified Data.Array as A import qualified Data.ByteString as BS import qualified Data.Map as M import qualified Data.Set as S -import Data.Data ( Data, Typeable ) +import Data.Data ( Data ) import Data.Void ( Void, absurd ) import Control.Monad ( forM_ ) import Control.Monad.Trans.State.Strict @@ -89,6 +83,7 @@ import GHC.HsToCore.Expr import GHC.HsToCore.Monad {- Note [Updating HieAst for changes in the GHC AST] + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ When updating the code in this file for changes in the GHC AST, you need to pay attention to the following things: @@ -224,6 +219,7 @@ type TypecheckedSource = LHsBinds GhcTc {- Note [Name Remapping] + ~~~~~~~~~~~~~~~~~~~~~ The Typechecker introduces new names for mono names in AbsBinds. We don't care about the distinction between mono and poly bindings, so we replace all occurrences of the mono name with the poly name. @@ -431,6 +427,7 @@ concatM :: Monad m => [m [a]] -> m [a] concatM xs = concat <$> sequence xs {- Note [Capturing Scopes and other non local information] + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ toHie is a local transformation, but scopes of bindings cannot be known locally, hence we have to push the relevant info down into the binding nodes. We use the following types (*Context and *Scoped) to wrap things and @@ -472,9 +469,10 @@ data PScoped a = PS (Maybe Span) Scope -- ^ use site of the pattern Scope -- ^ pattern to the right of a, not including a a - deriving (Typeable, Data) -- Pattern Scope + deriving (Data) -- Pattern Scope {- Note [TyVar Scopes] + ~~~~~~~~~~~~~~~~~~~ Due to -XScopedTypeVariables, type variables can be in scope quite far from their original binding. We resolve the scope of these type variables in a separate pass @@ -528,6 +526,7 @@ tvScopes tvScope rhsScope xs = map (\(RS sc a)-> TVS tvScope sc a) $ listScopes rhsScope xs {- Note [Scoping Rules for SigPat] + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Explicitly quantified variables in pattern type signatures are not brought into scope in the rhs, but implicitly quantified variables are (HsWC and HsIB). @@ -1041,10 +1040,6 @@ instance HiePass p => ToHie (PScoped (LocatedA (Pat (GhcPass p)))) where in [ toHie $ L ospan wrap , toHie $ PS rsp scope pscope $ (L ospan pat) ] --- CHANGED: removed preprocessor stuff --- #if __GLASGOW_HASKELL__ < 811 --- HieRn -> [] --- #endif where contextify :: a ~ LPat (GhcPass p) => HsConDetails (HsPatSigType (NoGhcTc (GhcPass p))) a (HsRecFields (GhcPass p) a) -> HsConDetails (TScoped (HsPatSigType (NoGhcTc (GhcPass p)))) (PScoped a) (RContext (HsRecFields (GhcPass p) (PScoped a))) @@ -1929,11 +1924,6 @@ instance HiePass p => ToHie (LocatedA (HsSplice (GhcPass p))) where HsSpliced _ _ _ -> [] XSplice x -> case ghcPass @p of --- CHANGED: removed preprocessor stuff --- #if __GLASGOW_HASKELL__ < 811 --- GhcPs -> noExtCon x --- GhcRn -> noExtCon x --- #endif GhcTc -> case x of HsSplicedT _ -> [] diff --git a/hls-graph/hls-graph.cabal b/hls-graph/hls-graph.cabal index 740baf6227..5eccb4d75e 100644 --- a/hls-graph/hls-graph.cabal +++ b/hls-graph/hls-graph.cabal @@ -1,6 +1,6 @@ cabal-version: 2.4 name: hls-graph -version: 2.4.0.0 +version: 2.11.0.0 synopsis: Haskell Language Server internal graph API description: Please see the README on GitHub at @@ -39,7 +39,16 @@ source-repository head type: git location: https://github.com/haskell/haskell-language-server +common warnings + ghc-options: + -Wall + -Wredundant-constraints + -Wunused-packages + -Wno-name-shadowing + -Wno-unticked-promoted-constructors + library + import: warnings exposed-modules: Control.Concurrent.STM.Stats Development.IDE.Graph @@ -48,6 +57,7 @@ library Development.IDE.Graph.Internal.Action Development.IDE.Graph.Internal.Database Development.IDE.Graph.Internal.Options + Development.IDE.Graph.Internal.Key Development.IDE.Graph.Internal.Paths Development.IDE.Graph.Internal.Profile Development.IDE.Graph.Internal.Rules @@ -66,7 +76,6 @@ library , bytestring , containers , deepseq - , directory , exceptions , extra , filepath @@ -89,26 +98,24 @@ library build-depends: , file-embed >=0.0.11 , template-haskell + else + build-depends: + directory if flag(stm-stats) cpp-options: -DSTM_STATS - ghc-options: - -Wall -Wredundant-constraints -Wno-name-shadowing - -Wno-unticked-promoted-constructors -Wunused-packages - if flag(pedantic) ghc-options: -Werror - default-language: Haskell2010 + default-language: GHC2021 default-extensions: DataKinds - KindSignatures - TypeOperators test-suite tests + import: warnings type: exitcode-stdio-1.0 - default-language: Haskell2010 + default-language: GHC2021 hs-source-dirs: test main-is: Main.hs other-modules: @@ -120,23 +127,16 @@ test-suite tests ghc-options: -threaded -rtsopts -with-rtsopts=-N -fno-ignore-asserts - -Wunused-packages build-depends: , base - , containers - , directory , extra - , filepath , hls-graph , hspec , stm , stm-containers , tasty - , tasty-hspec - , tasty-hunit + , tasty-hspec >= 1.2 , tasty-rerun - , text - , unordered-containers build-tool-depends: hspec-discover:hspec-discover diff --git a/hls-graph/src/Control/Concurrent/STM/Stats.hs b/hls-graph/src/Control/Concurrent/STM/Stats.hs index 1fc920ff2c..a6e7d0459b 100644 --- a/hls-graph/src/Control/Concurrent/STM/Stats.hs +++ b/hls-graph/src/Control/Concurrent/STM/Stats.hs @@ -1,7 +1,6 @@ -{-# LANGUAGE CPP #-} -{-# LANGUAGE ScopedTypeVariables #-} +{-# LANGUAGE CPP #-} #ifdef STM_STATS -{-# LANGUAGE RecordWildCards #-} +{-# LANGUAGE RecordWildCards #-} #endif module Control.Concurrent.STM.Stats ( atomicallyNamed @@ -21,7 +20,6 @@ import Control.Monad import Data.IORef import qualified Data.Map.Strict as M import Data.Time (getCurrentTime) -import Data.Typeable (Typeable) import GHC.Conc (unsafeIOToSTM) import System.IO import System.IO.Unsafe @@ -152,7 +150,6 @@ trackSTMConf (TrackSTMConf {..}) name txm = do -- 'BlockedIndefinitelyOnNamedSTM', carrying the name of the transaction and -- thus giving more helpful error messages. newtype BlockedIndefinitelyOnNamedSTM = BlockedIndefinitelyOnNamedSTM String - deriving (Typeable) instance Show BlockedIndefinitelyOnNamedSTM where showsPrec _ (BlockedIndefinitelyOnNamedSTM name) = diff --git a/hls-graph/src/Development/IDE/Graph.hs b/hls-graph/src/Development/IDE/Graph.hs index 98111080a2..81ad3b3dfd 100644 --- a/hls-graph/src/Development/IDE/Graph.hs +++ b/hls-graph/src/Development/IDE/Graph.hs @@ -3,7 +3,7 @@ module Development.IDE.Graph( shakeOptions, Rules, Action, action, - Key(.., Key), + pattern Key, newKey, renderKey, actionFinally, actionBracket, actionCatch, actionFork, -- * Configuration @@ -15,8 +15,6 @@ module Development.IDE.Graph( ShakeValue, RuleResult, -- * Special rules alwaysRerun, - -- * Batching - reschedule, -- * Actions for inspecting the keys in the database getDirtySet, getKeysAndVisitedAge, @@ -25,9 +23,10 @@ module Development.IDE.Graph( ) where import Development.IDE.Graph.Database -import Development.IDE.Graph.KeyMap -import Development.IDE.Graph.KeySet import Development.IDE.Graph.Internal.Action +import Development.IDE.Graph.Internal.Key import Development.IDE.Graph.Internal.Options import Development.IDE.Graph.Internal.Rules import Development.IDE.Graph.Internal.Types +import Development.IDE.Graph.KeyMap +import Development.IDE.Graph.KeySet diff --git a/hls-graph/src/Development/IDE/Graph/Database.hs b/hls-graph/src/Development/IDE/Graph/Database.hs index 2bed4a2360..bd8601cd16 100644 --- a/hls-graph/src/Development/IDE/Graph/Database.hs +++ b/hls-graph/src/Development/IDE/Graph/Database.hs @@ -1,6 +1,3 @@ - -{-# LANGUAGE ConstraintKinds #-} -{-# LANGUAGE ExistentialQuantification #-} module Development.IDE.Graph.Database( ShakeDatabase, ShakeValue, @@ -19,6 +16,7 @@ import Data.Maybe import Development.IDE.Graph.Classes () import Development.IDE.Graph.Internal.Action import Development.IDE.Graph.Internal.Database +import Development.IDE.Graph.Internal.Key import Development.IDE.Graph.Internal.Options import Development.IDE.Graph.Internal.Profile (writeProfile) import Development.IDE.Graph.Internal.Rules diff --git a/hls-graph/src/Development/IDE/Graph/Internal/Action.hs b/hls-graph/src/Development/IDE/Graph/Internal/Action.hs index 9602f3a10c..6d47d9b511 100644 --- a/hls-graph/src/Development/IDE/Graph/Internal/Action.hs +++ b/hls-graph/src/Development/IDE/Graph/Internal/Action.hs @@ -1,6 +1,4 @@ -{-# LANGUAGE ConstraintKinds #-} -{-# LANGUAGE ScopedTypeVariables #-} -{-# LANGUAGE TypeFamilies #-} +{-# LANGUAGE TypeFamilies #-} module Development.IDE.Graph.Internal.Action ( ShakeValue @@ -13,13 +11,13 @@ module Development.IDE.Graph.Internal.Action , apply , applyWithoutDependency , parallel -, reschedule , runActions , Development.IDE.Graph.Internal.Action.getDirtySet , getKeysAndVisitedAge ) where import Control.Concurrent.Async +import Control.DeepSeq (force) import Control.Exception import Control.Monad.IO.Class import Control.Monad.Trans.Class @@ -29,6 +27,7 @@ import Data.Functor.Identity import Data.IORef import Development.IDE.Graph.Classes import Development.IDE.Graph.Internal.Database +import Development.IDE.Graph.Internal.Key import Development.IDE.Graph.Internal.Rules (RuleResult) import Development.IDE.Graph.Internal.Types import System.Exit @@ -39,11 +38,7 @@ type ShakeValue a = (Show a, Typeable a, Eq a, Hashable a, NFData a) alwaysRerun :: Action () alwaysRerun = do ref <- Action $ asks actionDeps - liftIO $ modifyIORef ref (AlwaysRerunDeps mempty <>) - --- No-op for now -reschedule :: Double -> Action () -reschedule _ = pure () + liftIO $ modifyIORef' ref (AlwaysRerunDeps mempty <>) parallel :: [Action a] -> Action [a] parallel [] = pure [] @@ -121,7 +116,8 @@ apply ks = do stack <- Action $ asks actionStack (is, vs) <- liftIO $ build db stack ks ref <- Action $ asks actionDeps - liftIO $ modifyIORef ref (ResultDeps (fromListKeySet $ toList is) <>) + let !ks = force $ fromListKeySet $ toList is + liftIO $ modifyIORef' ref (ResultDeps [ks] <>) pure vs -- | Evaluate a list of keys without recording any dependencies. diff --git a/hls-graph/src/Development/IDE/Graph/Internal/Database.hs b/hls-graph/src/Development/IDE/Graph/Internal/Database.hs index 2ee8212520..359e5ceb6a 100644 --- a/hls-graph/src/Development/IDE/Graph/Internal/Database.hs +++ b/hls-graph/src/Development/IDE/Graph/Internal/Database.hs @@ -2,16 +2,13 @@ -- has the constraints we need on it when we get it out. {-# OPTIONS_GHC -Wno-redundant-constraints #-} -{-# LANGUAGE DerivingStrategies #-} -{-# LANGUAGE GeneralizedNewtypeDeriving #-} -{-# LANGUAGE RankNTypes #-} -{-# LANGUAGE RecordWildCards #-} -{-# LANGUAGE ScopedTypeVariables #-} -{-# LANGUAGE TupleSections #-} -{-# LANGUAGE TypeFamilies #-} -{-# LANGUAGE ViewPatterns #-} +{-# LANGUAGE CPP #-} +{-# LANGUAGE DerivingStrategies #-} +{-# LANGUAGE LambdaCase #-} +{-# LANGUAGE RecordWildCards #-} +{-# LANGUAGE TypeFamilies #-} -module Development.IDE.Graph.Internal.Database (newDatabase, incDatabase, build, getDirtySet, getKeysAndVisitAge) where +module Development.IDE.Graph.Internal.Database (compute, newDatabase, incDatabase, build, getDirtySet, getKeysAndVisitAge) where import Prelude hiding (unzip) @@ -31,12 +28,12 @@ import Data.Dynamic import Data.Either import Data.Foldable (for_, traverse_) import Data.IORef.Extra -import Data.List.NonEmpty (unzip) import Data.Maybe import Data.Traversable (for) import Data.Tuple.Extra import Debug.Trace (traceM) import Development.IDE.Graph.Classes +import Development.IDE.Graph.Internal.Key import Development.IDE.Graph.Internal.Rules import Development.IDE.Graph.Internal.Types import qualified Focus @@ -45,6 +42,12 @@ import qualified StmContainers.Map as SMap import System.IO.Unsafe import System.Time.Extra (duration, sleep) +#if MIN_VERSION_base(4,19,0) +import Data.Functor (unzip) +#else +import Data.List.NonEmpty (unzip) +#endif + newDatabase :: Dynamic -> TheRules -> IO Database newDatabase databaseExtra databaseRules = do @@ -136,26 +139,44 @@ builder db@Database{..} stack keys = withRunInIO $ \(RunInIO run) -> do waitAll pure results + +-- | isDirty +-- only dirty when it's build time is older than the changed time of one of its dependencies +isDirty :: Foldable t => Result -> t (a, Result) -> Bool +isDirty me = any (\(_,dep) -> resultBuilt me < resultChanged dep) + +-- | Refresh dependencies for a key and compute the key: +-- The refresh the deps linearly(last computed order of the deps for the key). +-- If any of the deps is dirty in the process, we jump to the actual computation of the key +-- and shortcut the refreshing of the rest of the deps. +-- * If no dirty dependencies and we have evaluated the key previously, then we refresh it in the current thread. +-- This assumes that the implementation will be a lookup +-- * Otherwise, we spawn a new thread to refresh the dirty deps (if any) and the key itself +refreshDeps :: KeySet -> Database -> Stack -> Key -> Result -> [KeySet] -> AIO Result +refreshDeps visited db stack key result = \case + -- no more deps to refresh + [] -> liftIO $ compute db stack key RunDependenciesSame (Just result) + (dep:deps) -> do + let newVisited = dep <> visited + res <- builder db stack (toListKeySet (dep `differenceKeySet` visited)) + case res of + Left res -> if isDirty result res + -- restart the computation if any of the deps are dirty + then liftIO $ compute db stack key RunDependenciesChanged (Just result) + -- else kick the rest of the deps + else refreshDeps newVisited db stack key result deps + Right iores -> do + res <- liftIO iores + if isDirty result res + then liftIO $ compute db stack key RunDependenciesChanged (Just result) + else refreshDeps newVisited db stack key result deps + -- | Refresh a key: --- * If no dirty dependencies and we have evaluated the key previously, then we refresh it in the current thread. --- This assumes that the implementation will be a lookup --- * Otherwise, we spawn a new thread to refresh the dirty deps (if any) and the key itself refresh :: Database -> Stack -> Key -> Maybe Result -> AIO (IO Result) -- refresh _ st k _ | traceShow ("refresh", st, k) False = undefined refresh db stack key result = case (addStack key stack, result) of (Left e, _) -> throw e - (Right stack, Just me@Result{resultDeps = ResultDeps (toListKeySet -> deps)}) -> do - res <- builder db stack deps - let isDirty = any (\(_,dep) -> resultBuilt me < resultChanged dep) - case res of - Left res -> - if isDirty res - then asyncWithCleanUp $ liftIO $ compute db stack key RunDependenciesChanged result - else pure $ compute db stack key RunDependenciesSame result - Right iores -> asyncWithCleanUp $ liftIO $ do - res <- iores - let mode = if isDirty res then RunDependenciesChanged else RunDependenciesSame - compute db stack key mode result + (Right stack, Just me@Result{resultDeps = ResultDeps deps}) -> asyncWithCleanUp $ refreshDeps mempty db stack key me (reverse deps) (Right stack, _) -> asyncWithCleanUp $ liftIO $ compute db stack key RunDependenciesChanged result @@ -167,16 +188,24 @@ compute db@Database{..} stack key mode result = do deps <- newIORef UnknownDeps (execution, RunResult{..}) <- duration $ runReaderT (fromAction act) $ SAction db deps stack - built <- readTVarIO databaseStep + curStep <- readTVarIO databaseStep deps <- readIORef deps - let changed = if runChanged == ChangedRecomputeDiff then built else maybe built resultChanged result - built' = if runChanged /= ChangedNothing then built else changed - -- only update the deps when the rule ran with changes + let lastChanged = maybe curStep resultChanged result + let lastBuild = maybe curStep resultBuilt result + -- changed time is always older than or equal to build time + let (changed, built) = case runChanged of + -- some thing changed + ChangedRecomputeDiff -> (curStep, curStep) + -- recomputed is the same + ChangedRecomputeSame -> (lastChanged, curStep) + -- nothing changed + ChangedNothing -> (lastChanged, lastBuild) + let -- only update the deps when the rule ran with changes actualDeps = if runChanged /= ChangedNothing then deps else previousDeps previousDeps= maybe UnknownDeps resultDeps result - let res = Result runValue built' changed built actualDeps execution runStore + let res = Result runValue built changed curStep actualDeps execution runStore case getResultDepsDefault mempty actualDeps of - deps | not(nullKeySet deps) + deps | not (nullKeySet deps) && runChanged /= ChangedNothing -> do -- IMPORTANT: record the reverse deps **before** marking the key Clean. @@ -188,7 +217,9 @@ compute db@Database{..} stack key mode result = do (getResultDepsDefault mempty previousDeps) deps _ -> pure () - atomicallyNamed "compute" $ SMap.focus (updateStatus $ Clean res) key databaseValues + atomicallyNamed "compute and run hook" $ do + runHook + SMap.focus (updateStatus $ Clean res) key databaseValues pure res updateStatus :: Monad m => Status -> Focus.Focus KeyDetails m () diff --git a/hls-graph/src/Development/IDE/Graph/Internal/Key.hs b/hls-graph/src/Development/IDE/Graph/Internal/Key.hs new file mode 100644 index 0000000000..ba303cdb99 --- /dev/null +++ b/hls-graph/src/Development/IDE/Graph/Internal/Key.hs @@ -0,0 +1,174 @@ +{-# LANGUAGE DerivingStrategies #-} +{-# LANGUAGE PatternSynonyms #-} +{-# LANGUAGE ViewPatterns #-} + +module Development.IDE.Graph.Internal.Key + ( Key -- Opaque - don't expose constructor, use newKey to create + , KeyValue (..) + , pattern Key + , newKey + , renderKey + -- * KeyMap + , KeyMap + , mapKeyMap + , insertKeyMap + , lookupKeyMap + , lookupDefaultKeyMap + , fromListKeyMap + , fromListWithKeyMap + , toListKeyMap + , elemsKeyMap + , restrictKeysKeyMap + -- * KeySet + , KeySet + , nullKeySet + , insertKeySet + , memberKeySet + , toListKeySet + , lengthKeySet + , filterKeySet + , singletonKeySet + , fromListKeySet + , deleteKeySet + , differenceKeySet + ) where + +--import Control.Monad.IO.Class () +import Data.Coerce +import Data.Dynamic +import qualified Data.HashMap.Strict as Map +import Data.IntMap (IntMap) +import qualified Data.IntMap.Strict as IM +import Data.IntSet (IntSet) +import qualified Data.IntSet as IS +import Data.IORef +import Data.Text (Text) +import qualified Data.Text as T +import Data.Typeable +import Development.IDE.Graph.Classes +import System.IO.Unsafe + + +newtype Key = UnsafeMkKey Int + +pattern Key :: () => (Typeable a, Hashable a, Show a) => a -> Key +pattern Key a <- (lookupKeyValue -> KeyValue a _) +{-# COMPLETE Key #-} + +data KeyValue = forall a . (Typeable a, Hashable a, Show a) => KeyValue a Text + +instance Eq KeyValue where + KeyValue a _ == KeyValue b _ = Just a == cast b +instance Hashable KeyValue where + hashWithSalt i (KeyValue x _) = hashWithSalt i (typeOf x, x) +instance Show KeyValue where + show (KeyValue _ t) = T.unpack t + +data GlobalKeyValueMap = GlobalKeyValueMap !(Map.HashMap KeyValue Key) !(IntMap KeyValue) {-# UNPACK #-} !Int + +keyMap :: IORef GlobalKeyValueMap +keyMap = unsafePerformIO $ newIORef (GlobalKeyValueMap Map.empty IM.empty 0) + +{-# NOINLINE keyMap #-} + +newKey :: (Typeable a, Hashable a, Show a) => a -> Key +newKey k = unsafePerformIO $ do + let !newKey = KeyValue k (T.pack (show k)) + atomicModifyIORef' keyMap $ \km@(GlobalKeyValueMap hm im n) -> + let new_key = Map.lookup newKey hm + in case new_key of + Just v -> (km, v) + Nothing -> + let !new_index = UnsafeMkKey n + in (GlobalKeyValueMap (Map.insert newKey new_index hm) (IM.insert n newKey im) (n+1), new_index) +{-# NOINLINE newKey #-} + +lookupKeyValue :: Key -> KeyValue +lookupKeyValue (UnsafeMkKey x) = unsafePerformIO $ do + GlobalKeyValueMap _ im _ <- readIORef keyMap + pure $! im IM.! x + +{-# NOINLINE lookupKeyValue #-} + +instance Eq Key where + UnsafeMkKey a == UnsafeMkKey b = a == b +instance Hashable Key where + hashWithSalt i (UnsafeMkKey x) = hashWithSalt i x +instance Show Key where + show (Key x) = show x + +renderKey :: Key -> Text +renderKey (lookupKeyValue -> KeyValue _ t) = t + +newtype KeySet = KeySet IntSet + deriving newtype (Eq, Ord, Semigroup, Monoid, NFData) + +instance Show KeySet where + showsPrec p (KeySet is)= showParen (p > 10) $ + showString "fromList " . shows ks + where ks = coerce (IS.toList is) :: [Key] + +insertKeySet :: Key -> KeySet -> KeySet +insertKeySet = coerce IS.insert + +memberKeySet :: Key -> KeySet -> Bool +memberKeySet = coerce IS.member + +toListKeySet :: KeySet -> [Key] +toListKeySet = coerce IS.toList + +nullKeySet :: KeySet -> Bool +nullKeySet = coerce IS.null + +differenceKeySet :: KeySet -> KeySet -> KeySet +differenceKeySet = coerce IS.difference + +deleteKeySet :: Key -> KeySet -> KeySet +deleteKeySet = coerce IS.delete + +fromListKeySet :: [Key] -> KeySet +fromListKeySet = coerce IS.fromList + +singletonKeySet :: Key -> KeySet +singletonKeySet = coerce IS.singleton + +filterKeySet :: (Key -> Bool) -> KeySet -> KeySet +filterKeySet = coerce IS.filter + +lengthKeySet :: KeySet -> Int +lengthKeySet = coerce IS.size + +newtype KeyMap a = KeyMap (IntMap a) + deriving newtype (Eq, Ord, Semigroup, Monoid) + +instance Show a => Show (KeyMap a) where + showsPrec p (KeyMap im)= showParen (p > 10) $ + showString "fromList " . shows ks + where ks = coerce (IM.toList im) :: [(Key,a)] + +mapKeyMap :: (a -> b) -> KeyMap a -> KeyMap b +mapKeyMap f (KeyMap m) = KeyMap (IM.map f m) + +insertKeyMap :: Key -> a -> KeyMap a -> KeyMap a +insertKeyMap (UnsafeMkKey k) v (KeyMap m) = KeyMap (IM.insert k v m) + +lookupKeyMap :: Key -> KeyMap a -> Maybe a +lookupKeyMap (UnsafeMkKey k) (KeyMap m) = IM.lookup k m + +lookupDefaultKeyMap :: a -> Key -> KeyMap a -> a +lookupDefaultKeyMap a (UnsafeMkKey k) (KeyMap m) = IM.findWithDefault a k m + +fromListKeyMap :: [(Key,a)] -> KeyMap a +fromListKeyMap xs = KeyMap (IM.fromList (coerce xs)) + +fromListWithKeyMap :: (a -> a -> a) -> [(Key,a)] -> KeyMap a +fromListWithKeyMap f xs = KeyMap (IM.fromListWith f (coerce xs)) + +toListKeyMap :: KeyMap a -> [(Key,a)] +toListKeyMap (KeyMap m) = coerce (IM.toList m) + +elemsKeyMap :: KeyMap a -> [a] +elemsKeyMap (KeyMap m) = IM.elems m + +restrictKeysKeyMap :: KeyMap a -> KeySet -> KeyMap a +restrictKeysKeyMap (KeyMap m) (KeySet s) = KeyMap (IM.restrictKeys m s) diff --git a/hls-graph/src/Development/IDE/Graph/Internal/Profile.hs b/hls-graph/src/Development/IDE/Graph/Internal/Profile.hs index 0403e43a5a..5369c578f8 100644 --- a/hls-graph/src/Development/IDE/Graph/Internal/Profile.hs +++ b/hls-graph/src/Development/IDE/Graph/Internal/Profile.hs @@ -13,7 +13,7 @@ import qualified Data.ByteString.Lazy.Char8 as LBS import Data.Char import Data.Dynamic (toDyn) import qualified Data.HashMap.Strict as Map -import Data.List (dropWhileEnd, foldl', +import Data.List (dropWhileEnd, intercalate, partition, sort, sortBy) @@ -21,8 +21,8 @@ import Data.List.Extra (nubOrd) import Data.Maybe import Data.Time (getCurrentTime) import Data.Time.Format.ISO8601 (iso8601Show) -import Development.IDE.Graph.Classes import Development.IDE.Graph.Internal.Database (getDirtySet) +import Development.IDE.Graph.Internal.Key import Development.IDE.Graph.Internal.Paths import Development.IDE.Graph.Internal.Types import qualified Language.Javascript.DGTable as DGTable @@ -33,6 +33,10 @@ import System.FilePath import System.IO.Unsafe (unsafePerformIO) import System.Time.Extra (Seconds) +#if !MIN_VERSION_base(4,20,0) +import Data.List (foldl') +#endif + #ifdef FILE_EMBED import Data.FileEmbed import Language.Haskell.TH.Syntax (runIO) @@ -64,7 +68,7 @@ resultsOnly mp = mapKeyMap (\r -> -- | Given a map of representing a dependency order (with a show for error messages), find an ordering for the items such -- that no item points to an item before itself. -- Raise an error if you end up with a cycle. --- dependencyOrder :: (Eq a, Hashable a) => (a -> String) -> [(a,[a])] -> [a] +-- -- Algorithm: -- Divide everyone up into those who have no dependencies [Id] -- And those who depend on a particular Id, Dep :-> Maybe [(Key,[Dep])] @@ -72,6 +76,7 @@ resultsOnly mp = mapKeyMap (\r -> -- For each with no dependencies, add to list, then take its dep hole and -- promote them either to Nothing (if ds == []) or into a new slot. -- k :-> Nothing means the key has already been freed +dependencyOrder :: (Key -> String) -> [(Key, [Key])] -> [Key] dependencyOrder shw status = f (map fst noDeps) $ mapKeyMap Just $ @@ -88,7 +93,7 @@ dependencyOrder shw status = where (bad,badOverflow) = splitAt 10 [shw i | (i, Just _) <- toListKeyMap mp] f (x:xs) mp = x : f (now++xs) later - where Just free = lookupDefaultKeyMap (Just []) x mp + where free = fromMaybe [] $ lookupDefaultKeyMap (Just []) x mp (now,later) = foldl' g ([], insertKeyMap x Nothing mp) free g (free, mp) (k, []) = (k:free, mp) diff --git a/hls-graph/src/Development/IDE/Graph/Internal/Rules.hs b/hls-graph/src/Development/IDE/Graph/Internal/Rules.hs index 97ea11eff7..9a5f36ca35 100644 --- a/hls-graph/src/Development/IDE/Graph/Internal/Rules.hs +++ b/hls-graph/src/Development/IDE/Graph/Internal/Rules.hs @@ -1,9 +1,8 @@ -- We deliberately want to ensure the function we add to the rule database -- has the constraints we need on it when we get it out. {-# OPTIONS_GHC -Wno-redundant-constraints #-} -{-# LANGUAGE RecordWildCards #-} -{-# LANGUAGE ScopedTypeVariables #-} -{-# LANGUAGE TypeFamilies #-} +{-# LANGUAGE RecordWildCards #-} +{-# LANGUAGE TypeFamilies #-} module Development.IDE.Graph.Internal.Rules where @@ -18,6 +17,7 @@ import Data.IORef import Data.Maybe import Data.Typeable import Development.IDE.Graph.Classes +import Development.IDE.Graph.Internal.Key import Development.IDE.Graph.Internal.Types -- | The type mapping between the @key@ or a rule and the resulting @value@. diff --git a/hls-graph/src/Development/IDE/Graph/Internal/Types.hs b/hls-graph/src/Development/IDE/Graph/Internal/Types.hs index 891b358c7b..34bed42391 100644 --- a/hls-graph/src/Development/IDE/Graph/Internal/Types.hs +++ b/hls-graph/src/Development/IDE/Graph/Internal/Types.hs @@ -1,48 +1,38 @@ -{-# LANGUAGE CPP #-} -{-# LANGUAGE DeriveAnyClass #-} -{-# LANGUAGE DeriveFunctor #-} -{-# LANGUAGE DeriveGeneric #-} -{-# LANGUAGE DerivingStrategies #-} -{-# LANGUAGE ExistentialQuantification #-} -{-# LANGUAGE GeneralizedNewtypeDeriving #-} -{-# LANGUAGE RecordWildCards #-} -{-# LANGUAGE ScopedTypeVariables #-} -{-# LANGUAGE PatternSynonyms #-} -{-# LANGUAGE BangPatterns #-} -{-# LANGUAGE ViewPatterns #-} +{-# LANGUAGE CPP #-} +{-# LANGUAGE DeriveAnyClass #-} +{-# LANGUAGE DerivingStrategies #-} +{-# LANGUAGE RecordWildCards #-} module Development.IDE.Graph.Internal.Types where -import Control.Applicative +import Control.Concurrent.STM (STM) +import Control.Monad ((>=>)) import Control.Monad.Catch import Control.Monad.IO.Class import Control.Monad.Trans.Reader -import Data.Aeson (FromJSON, ToJSON) -import Data.Bifunctor (second) -import qualified Data.ByteString as BS -import Data.Coerce +import Data.Aeson (FromJSON, ToJSON) +import Data.Bifunctor (second) +import qualified Data.ByteString as BS import Data.Dynamic -import qualified Data.HashMap.Strict as Map -import qualified Data.IntMap.Strict as IM -import Data.IntMap (IntMap) -import qualified Data.IntSet as IS -import Data.IntSet (IntSet) -import qualified Data.Text as T -import Data.Text (Text) +import Data.Foldable (fold) +import qualified Data.HashMap.Strict as Map import Data.IORef -import Data.List (intercalate) +import Data.List (intercalate) import Data.Maybe import Data.Typeable import Development.IDE.Graph.Classes -import GHC.Conc (TVar, atomically) -import GHC.Generics (Generic) +import Development.IDE.Graph.Internal.Key +import GHC.Conc (TVar, atomically) +import GHC.Generics (Generic) import qualified ListT -import qualified StmContainers.Map as SMap -import StmContainers.Map (Map) -import System.Time.Extra (Seconds) -import System.IO.Unsafe -import UnliftIO (MonadUnliftIO) +import qualified StmContainers.Map as SMap +import StmContainers.Map (Map) +import System.Time.Extra (Seconds) +import UnliftIO (MonadUnliftIO) +#if !MIN_VERSION_base(4,18,0) +import Control.Applicative (liftA2) +#endif unwrapDynamic :: forall a . Typeable a => Dynamic -> a unwrapDynamic x = fromMaybe (error msg) $ fromDynamic x @@ -68,15 +58,14 @@ data SRules = SRules { rulesMap :: !(IORef TheRules) } - --------------------------------------------------------------------- -- ACTIONS -- | An action representing something that can be run as part of a 'Rule'. --- +-- -- 'Action's can be pure functions but also have access to 'IO' via 'MonadIO' and 'MonadUnliftIO. -- It should be assumed that actions throw exceptions, these can be caught with --- 'Development.IDE.Graph.Internal.Action.actionCatch'. In particular, it is +-- 'Development.IDE.Graph.Internal.Action.actionCatch'. In particular, it is -- permissible to use the 'MonadFail' instance, which will lead to an 'IOException'. newtype Action a = Action {fromAction :: ReaderT SAction IO a} deriving newtype (Monad, Applicative, Functor, MonadIO, MonadFail, MonadThrow, MonadCatch, MonadMask, MonadUnliftIO) @@ -90,138 +79,22 @@ data SAction = SAction { getDatabase :: Action Database getDatabase = Action $ asks actionDatabase +-- | waitForDatabaseRunningKeysAction waits for all keys in the database to finish running. +waitForDatabaseRunningKeysAction :: Action () +waitForDatabaseRunningKeysAction = getDatabase >>= liftIO . waitForDatabaseRunningKeys + --------------------------------------------------------------------- -- DATABASE data ShakeDatabase = ShakeDatabase !Int [Action ()] Database newtype Step = Step Int - deriving newtype (Eq,Ord,Hashable) + deriving newtype (Eq,Ord,Hashable,Show) --------------------------------------------------------------------- -- Keys -data KeyValue = forall a . (Eq a, Typeable a, Hashable a, Show a) => KeyValue a Text - -newtype Key = UnsafeMkKey Int - -pattern Key a <- (lookupKeyValue -> KeyValue a _) - -data GlobalKeyValueMap = GlobalKeyValueMap !(Map.HashMap KeyValue Key) !(IntMap KeyValue) {-# UNPACK #-} !Int - -keyMap :: IORef GlobalKeyValueMap -keyMap = unsafePerformIO $ newIORef (GlobalKeyValueMap Map.empty IM.empty 0) - -{-# NOINLINE keyMap #-} - -newKey :: (Eq a, Typeable a, Hashable a, Show a) => a -> Key -newKey k = unsafePerformIO $ do - let !newKey = KeyValue k (T.pack (show k)) - atomicModifyIORef' keyMap $ \km@(GlobalKeyValueMap hm im n) -> - let new_key = Map.lookup newKey hm - in case new_key of - Just v -> (km, v) - Nothing -> - let !new_index = UnsafeMkKey n - in (GlobalKeyValueMap (Map.insert newKey new_index hm) (IM.insert n newKey im) (n+1), new_index) -{-# NOINLINE newKey #-} - -lookupKeyValue :: Key -> KeyValue -lookupKeyValue (UnsafeMkKey x) = unsafePerformIO $ do - GlobalKeyValueMap _ im _ <- readIORef keyMap - pure $! im IM.! x - -{-# NOINLINE lookupKeyValue #-} - -instance Eq Key where - UnsafeMkKey a == UnsafeMkKey b = a == b -instance Hashable Key where - hashWithSalt i (UnsafeMkKey x) = hashWithSalt i x -instance Show Key where - show (Key x) = show x - -instance Eq KeyValue where - KeyValue a _ == KeyValue b _ = Just a == cast b -instance Hashable KeyValue where - hashWithSalt i (KeyValue x _) = hashWithSalt i (typeOf x, x) -instance Show KeyValue where - show (KeyValue x t) = T.unpack t - -renderKey :: Key -> Text -renderKey (lookupKeyValue -> KeyValue _ t) = t - -newtype KeySet = KeySet IntSet - deriving newtype (Eq, Ord, Semigroup, Monoid) - -instance Show KeySet where - showsPrec p (KeySet is)= showParen (p > 10) $ - showString "fromList " . shows ks - where ks = coerce (IS.toList is) :: [Key] - -insertKeySet :: Key -> KeySet -> KeySet -insertKeySet = coerce IS.insert - -memberKeySet :: Key -> KeySet -> Bool -memberKeySet = coerce IS.member - -toListKeySet :: KeySet -> [Key] -toListKeySet = coerce IS.toList - -nullKeySet :: KeySet -> Bool -nullKeySet = coerce IS.null -differenceKeySet :: KeySet -> KeySet -> KeySet -differenceKeySet = coerce IS.difference - -deleteKeySet :: Key -> KeySet -> KeySet -deleteKeySet = coerce IS.delete - -fromListKeySet :: [Key] -> KeySet -fromListKeySet = coerce IS.fromList - -singletonKeySet :: Key -> KeySet -singletonKeySet = coerce IS.singleton - -filterKeySet :: (Key -> Bool) -> KeySet -> KeySet -filterKeySet = coerce IS.filter - -lengthKeySet :: KeySet -> Int -lengthKeySet = coerce IS.size - -newtype KeyMap a = KeyMap (IntMap a) - deriving newtype (Eq, Ord, Semigroup, Monoid) - -instance Show a => Show (KeyMap a) where - showsPrec p (KeyMap im)= showParen (p > 10) $ - showString "fromList " . shows ks - where ks = coerce (IM.toList im) :: [(Key,a)] - -mapKeyMap :: (a -> b) -> KeyMap a -> KeyMap b -mapKeyMap f (KeyMap m) = KeyMap (IM.map f m) - -insertKeyMap :: Key -> a -> KeyMap a -> KeyMap a -insertKeyMap (UnsafeMkKey k) v (KeyMap m) = KeyMap (IM.insert k v m) - -lookupKeyMap :: Key -> KeyMap a -> Maybe a -lookupKeyMap (UnsafeMkKey k) (KeyMap m) = IM.lookup k m - -lookupDefaultKeyMap :: a -> Key -> KeyMap a -> a -lookupDefaultKeyMap a (UnsafeMkKey k) (KeyMap m) = IM.findWithDefault a k m - -fromListKeyMap :: [(Key,a)] -> KeyMap a -fromListKeyMap xs = KeyMap (IM.fromList (coerce xs)) - -fromListWithKeyMap :: (a -> a -> a) -> [(Key,a)] -> KeyMap a -fromListWithKeyMap f xs = KeyMap (IM.fromListWith f (coerce xs)) - -toListKeyMap :: KeyMap a -> [(Key,a)] -toListKeyMap (KeyMap m) = coerce (IM.toList m) - -elemsKeyMap :: KeyMap a -> [a] -elemsKeyMap (KeyMap m) = IM.elems m - -restrictKeysKeyMap :: KeyMap a -> KeySet -> KeyMap a -restrictKeysKeyMap (KeyMap m) (KeySet s) = KeyMap (IM.restrictKeys m s) newtype Value = Value Dynamic @@ -242,6 +115,9 @@ data Database = Database { databaseValues :: !(Map Key KeyDetails) } +waitForDatabaseRunningKeys :: Database -> IO () +waitForDatabaseRunningKeys = getDatabaseValues >=> mapM_ (waitRunning . snd) + getDatabaseValues :: Database -> IO [(Key, Status)] getDatabaseValues = atomically . (fmap.fmap) (second keyStatus) @@ -268,6 +144,10 @@ getResult (Clean re) = Just re getResult (Dirty m_re) = m_re getResult (Running _ _ _ m_re) = m_re -- watch out: this returns the previous result +waitRunning :: Status -> IO () +waitRunning Running{..} = runningWait +waitRunning _ = return () + data Result = Result { resultValue :: !Value, resultBuilt :: !Step, -- ^ the step when it was last recomputed @@ -278,16 +158,20 @@ data Result = Result { resultData :: !BS.ByteString } -data ResultDeps = UnknownDeps | AlwaysRerunDeps !KeySet | ResultDeps !KeySet +-- Notice, invariant to maintain: +-- the ![KeySet] in ResultDeps need to be stored in reverse order, +-- so that we can append to it efficiently, and we need the ordering +-- so we can do a linear dependency refreshing in refreshDeps. +data ResultDeps = UnknownDeps | AlwaysRerunDeps !KeySet | ResultDeps ![KeySet] deriving (Eq, Show) getResultDepsDefault :: KeySet -> ResultDeps -> KeySet -getResultDepsDefault _ (ResultDeps ids) = ids +getResultDepsDefault _ (ResultDeps ids) = fold ids getResultDepsDefault _ (AlwaysRerunDeps ids) = ids getResultDepsDefault def UnknownDeps = def mapResultDeps :: (KeySet -> KeySet) -> ResultDeps -> ResultDeps -mapResultDeps f (ResultDeps ids) = ResultDeps $ f ids +mapResultDeps f (ResultDeps ids) = ResultDeps $ fmap f ids mapResultDeps f (AlwaysRerunDeps ids) = AlwaysRerunDeps $ f ids mapResultDeps _ UnknownDeps = UnknownDeps @@ -315,7 +199,6 @@ instance NFData RunMode where rnf x = x `seq` () -- | How the output of a rule has changed. data RunChanged = ChangedNothing -- ^ Nothing has changed. - | ChangedStore -- ^ The stored value has changed, but in a way that should be considered identical (used rarely). | ChangedRecomputeSame -- ^ I recomputed the value and it was the same. | ChangedRecomputeDiff -- ^ I recomputed the value and it was different. deriving (Eq,Show,Generic) @@ -331,11 +214,11 @@ data RunResult value = RunResult -- ^ The value to store in the Shake database. ,runValue :: value -- ^ The value to return from 'Development.Shake.Rule.apply'. + ,runHook :: STM () + -- ^ The hook to run at the end of the build in the same transaction + -- when the key is marked as clean. } deriving Functor -instance NFData value => NFData (RunResult value) where - rnf (RunResult x1 x2 x3) = rnf x1 `seq` x2 `seq` rnf x3 - --------------------------------------------------------------------- -- EXCEPTIONS @@ -344,7 +227,7 @@ data GraphException = forall e. Exception e => GraphException { stack :: [String], -- ^ The stack of keys that led to this exception inner :: e -- ^ The underlying exception } - deriving (Typeable, Exception) + deriving (Exception) instance Show GraphException where show GraphException{..} = unlines $ @@ -366,7 +249,7 @@ instance Show Stack where show (Stack kk _) = "Stack: " <> intercalate " -> " (map show kk) newtype StackException = StackException Stack - deriving (Typeable, Show) + deriving (Show) instance Exception StackException where fromException = fromGraphException diff --git a/hls-graph/src/Development/IDE/Graph/KeyMap.hs b/hls-graph/src/Development/IDE/Graph/KeyMap.hs index daa1ae8642..30ff4d6cfa 100644 --- a/hls-graph/src/Development/IDE/Graph/KeyMap.hs +++ b/hls-graph/src/Development/IDE/Graph/KeyMap.hs @@ -12,4 +12,4 @@ module Development.IDE.Graph.KeyMap( restrictKeysKeyMap, ) where -import Development.IDE.Graph.Internal.Types +import Development.IDE.Graph.Internal.Key diff --git a/hls-graph/src/Development/IDE/Graph/KeySet.hs b/hls-graph/src/Development/IDE/Graph/KeySet.hs index ef8c46e6b5..cd0e76e675 100644 --- a/hls-graph/src/Development/IDE/Graph/KeySet.hs +++ b/hls-graph/src/Development/IDE/Graph/KeySet.hs @@ -13,4 +13,4 @@ module Development.IDE.Graph.KeySet( lengthKeySet, ) where -import Development.IDE.Graph.Internal.Types +import Development.IDE.Graph.Internal.Key diff --git a/hls-graph/test/ActionSpec.hs b/hls-graph/test/ActionSpec.hs index 171e90214b..97ab5555ac 100644 --- a/hls-graph/test/ActionSpec.hs +++ b/hls-graph/test/ActionSpec.hs @@ -1,32 +1,68 @@ -{-# LANGUAGE OverloadedStrings #-} -{-# LANGUAGE RecordWildCards #-} -{-# LANGUAGE ScopedTypeVariables #-} -{-# LANGUAGE TypeApplications #-} +{-# LANGUAGE OverloadedStrings #-} +{-# LANGUAGE RecordWildCards #-} module ActionSpec where +import Control.Concurrent (MVar, readMVar) +import qualified Control.Concurrent as C import Control.Concurrent.STM -import qualified Data.HashSet as HashSet -import Development.IDE.Graph (shakeOptions) -import Development.IDE.Graph.Database (shakeNewDatabase, - shakeRunDatabase) -import Development.IDE.Graph.Internal.Action (apply1) +import Control.Monad.IO.Class (MonadIO (..)) +import Development.IDE.Graph (shakeOptions) +import Development.IDE.Graph.Database (shakeNewDatabase, + shakeRunDatabase, + shakeRunDatabaseForKeys) +import Development.IDE.Graph.Internal.Database (build, incDatabase) +import Development.IDE.Graph.Internal.Key import Development.IDE.Graph.Internal.Types import Development.IDE.Graph.Rule import Example -import qualified StmContainers.Map as STM -import System.Time.Extra (timeout) +import qualified StmContainers.Map as STM import Test.Hspec + + spec :: Spec spec = do + describe "apply1" $ it "Test build update, Buggy dirty mechanism in hls-graph #4237" $ do + let ruleStep1 :: MVar Int -> Rules () + ruleStep1 m = addRule $ \CountRule _old mode -> do + -- depends on ruleSubBranch, it always changed if dirty + _ :: Int <- apply1 SubBranchRule + let r = 1 + case mode of + -- it update the built step + RunDependenciesChanged -> do + _ <- liftIO $ C.modifyMVar m $ \x -> return (x+1, x) + return $ RunResult ChangedRecomputeSame "" r (return ()) + -- this won't update the built step + RunDependenciesSame -> + return $ RunResult ChangedNothing "" r (return ()) + count <- C.newMVar 0 + count1 <- C.newMVar 0 + db <- shakeNewDatabase shakeOptions $ do + ruleSubBranch count + ruleStep1 count1 + -- bootstrapping the database + _ <- shakeRunDatabase db $ pure $ apply1 CountRule -- count = 1 + let child = newKey SubBranchRule + let parent = newKey CountRule + -- instruct to RunDependenciesChanged then CountRule should be recomputed + -- result should be changed 0, build 1 + _res1 <- shakeRunDatabaseForKeys (Just [child]) db [apply1 CountRule] -- count = 2 + -- since child changed = parent build + -- instruct to RunDependenciesSame then CountRule should not be recomputed + -- result should be changed 0, build 1 + _res3 <- shakeRunDatabaseForKeys (Just [parent]) db [apply1 CountRule] -- count = 2 + -- invariant child changed = parent build should remains after RunDependenciesSame + -- this used to be a bug, with additional computation, see https://github.com/haskell/haskell-language-server/pull/4238 + _res3 <- shakeRunDatabaseForKeys (Just [parent]) db [apply1 CountRule] -- count = 2 + c1 <- readMVar count1 + c1 `shouldBe` 2 describe "apply1" $ do it "computes a rule with no dependencies" $ do - db <- shakeNewDatabase shakeOptions $ do - ruleUnit + db <- shakeNewDatabase shakeOptions ruleUnit res <- shakeRunDatabase db $ - pure $ do - apply1 (Rule @()) + pure $ apply1 (Rule @()) res `shouldBe` [()] it "computes a rule with one dependency" $ do db <- shakeNewDatabase shakeOptions $ do @@ -40,39 +76,56 @@ spec = do ruleBool let theKey = Rule @Bool res <- shakeRunDatabase db $ - pure $ do - apply1 theKey + pure $ apply1 theKey res `shouldBe` [True] Just (Clean res) <- lookup (newKey theKey) <$> getDatabaseValues theDb - resultDeps res `shouldBe` ResultDeps (singletonKeySet $ newKey (Rule @())) + resultDeps res `shouldBe` ResultDeps [singletonKeySet $ newKey (Rule @())] it "tracks reverse dependencies" $ do db@(ShakeDatabase _ _ Database {..}) <- shakeNewDatabase shakeOptions $ do ruleUnit ruleBool let theKey = Rule @Bool res <- shakeRunDatabase db $ - pure $ do - apply1 theKey + pure $ apply1 theKey res `shouldBe` [True] Just KeyDetails {..} <- atomically $ STM.lookup (newKey (Rule @())) databaseValues - keyReverseDeps `shouldBe` (singletonKeySet $ newKey theKey) + keyReverseDeps `shouldBe` singletonKeySet (newKey theKey) it "rethrows exceptions" $ do - db <- shakeNewDatabase shakeOptions $ do - addRule $ \(Rule :: Rule ()) old mode -> error "boom" + db <- shakeNewDatabase shakeOptions $ addRule $ \(Rule :: Rule ()) _old _mode -> error "boom" let res = shakeRunDatabase db $ pure $ apply1 (Rule @()) res `shouldThrow` anyErrorCall - describe "applyWithoutDependency" $ do - it "does not track dependencies" $ do - db@(ShakeDatabase _ _ theDb) <- shakeNewDatabase shakeOptions $ do + it "computes a rule with branching dependencies does not invoke phantom dependencies #3423" $ do + cond <- C.newMVar True + count <- C.newMVar 0 + (ShakeDatabase _ _ theDb) <- shakeNewDatabase shakeOptions $ do ruleUnit - addRule $ \Rule old mode -> do - [()] <- applyWithoutDependency [Rule] - return $ RunResult ChangedRecomputeDiff "" True + ruleCond cond + ruleSubBranch count + ruleWithCond + -- build the one with the condition True + -- This should call the SubBranchRule once + -- cond rule would return different results each time + res0 <- build theDb emptyStack [BranchedRule] + snd res0 `shouldBe` [1 :: Int] + incDatabase theDb Nothing + -- build the one with the condition False + -- This should not call the SubBranchRule + res1 <- build theDb emptyStack [BranchedRule] + snd res1 `shouldBe` [2 :: Int] + -- SubBranchRule should be recomputed once before this (when the condition was True) + countRes <- build theDb emptyStack [SubBranchRule] + snd countRes `shouldBe` [1 :: Int] - let theKey = Rule @Bool - res <- shakeRunDatabase db $ - pure $ do - applyWithoutDependency [theKey] - res `shouldBe` [[True]] - Just (Clean res) <- lookup (newKey theKey) <$> getDatabaseValues theDb - resultDeps res `shouldBe` UnknownDeps + describe "applyWithoutDependency" $ it "does not track dependencies" $ do + db@(ShakeDatabase _ _ theDb) <- shakeNewDatabase shakeOptions $ do + ruleUnit + addRule $ \Rule _old _mode -> do + [()] <- applyWithoutDependency [Rule] + return $ RunResult ChangedRecomputeDiff "" True $ return () + + let theKey = Rule @Bool + res <- shakeRunDatabase db $ + pure $ applyWithoutDependency [theKey] + res `shouldBe` [[True]] + Just (Clean res) <- lookup (newKey theKey) <$> getDatabaseValues theDb + resultDeps res `shouldBe` UnknownDeps diff --git a/hls-graph/test/DatabaseSpec.hs b/hls-graph/test/DatabaseSpec.hs index 0189a92b9a..9061bfa89d 100644 --- a/hls-graph/test/DatabaseSpec.hs +++ b/hls-graph/test/DatabaseSpec.hs @@ -1,30 +1,49 @@ -{-# LANGUAGE OverloadedLists #-} -{-# LANGUAGE OverloadedStrings #-} -{-# LANGUAGE RecordWildCards #-} -{-# LANGUAGE ScopedTypeVariables #-} -{-# LANGUAGE TypeApplications #-} +{-# LANGUAGE OverloadedStrings #-} + module DatabaseSpec where -import Control.Concurrent.STM -import Development.IDE.Graph (shakeOptions) -import Development.IDE.Graph.Database (shakeNewDatabase, - shakeRunDatabase) -import Development.IDE.Graph.Internal.Action (apply1) +import Development.IDE.Graph (newKey, shakeOptions) +import Development.IDE.Graph.Database (shakeNewDatabase, + shakeRunDatabase) +import Development.IDE.Graph.Internal.Action (apply1) +import Development.IDE.Graph.Internal.Database (compute, incDatabase) +import Development.IDE.Graph.Internal.Rules (addRule) import Development.IDE.Graph.Internal.Types -import Development.IDE.Graph.Rule import Example -import qualified StmContainers.Map as STM -import System.Time.Extra (timeout) +import System.Time.Extra (timeout) import Test.Hspec + spec :: Spec spec = do describe "Evaluation" $ do it "detects cycles" $ do db <- shakeNewDatabase shakeOptions $ do ruleBool - addRule $ \Rule old mode -> do + addRule $ \Rule _old _mode -> do True <- apply1 (Rule @Bool) - return $ RunResult ChangedRecomputeDiff "" () + return $ RunResult ChangedRecomputeDiff "" () (return ()) let res = shakeRunDatabase db $ pure $ apply1 (Rule @()) timeout 1 res `shouldThrow` \StackException{} -> True + + describe "compute" $ do + it "build step and changed step updated correctly" $ do + (ShakeDatabase _ _ theDb) <- shakeNewDatabase shakeOptions $ do + ruleStep + + let k = newKey $ Rule @() + -- ChangedRecomputeSame + r1@Result{resultChanged=rc1, resultBuilt=rb1} <- compute theDb emptyStack k RunDependenciesChanged Nothing + incDatabase theDb Nothing + -- ChangedRecomputeSame + r2@Result{resultChanged=rc2, resultBuilt=rb2} <- compute theDb emptyStack k RunDependenciesChanged (Just r1) + incDatabase theDb Nothing + -- changed Nothing + Result{resultChanged=rc3, resultBuilt=rb3} <- compute theDb emptyStack k RunDependenciesSame (Just r2) + rc1 `shouldBe` Step 0 + rc2 `shouldBe` Step 0 + rc3 `shouldBe` Step 0 + + rb1 `shouldBe` Step 0 + rb2 `shouldBe` Step 1 + rb3 `shouldBe` Step 1 diff --git a/hls-graph/test/Example.hs b/hls-graph/test/Example.hs index 18807bd1c1..c20ea79328 100644 --- a/hls-graph/test/Example.hs +++ b/hls-graph/test/Example.hs @@ -1,11 +1,11 @@ -{-# LANGUAGE DeriveAnyClass #-} -{-# LANGUAGE DeriveGeneric #-} -{-# LANGUAGE OverloadedStrings #-} -{-# LANGUAGE ScopedTypeVariables #-} -{-# LANGUAGE TypeApplications #-} -{-# LANGUAGE TypeFamilies #-} +{-# LANGUAGE DeriveAnyClass #-} +{-# LANGUAGE NoPolyKinds #-} +{-# LANGUAGE OverloadedStrings #-} +{-# LANGUAGE TypeFamilies #-} module Example where +import qualified Control.Concurrent as C +import Control.Monad.IO.Class (liftIO) import Development.IDE.Graph import Development.IDE.Graph.Classes import Development.IDE.Graph.Rule @@ -20,12 +20,55 @@ instance Typeable a => Show (Rule a) where type instance RuleResult (Rule a) = a +ruleStep :: Rules () +ruleStep = addRule $ \(Rule :: Rule ()) _old mode -> do + case mode of + RunDependenciesChanged -> return $ RunResult ChangedRecomputeSame "" () (return ()) + RunDependenciesSame -> return $ RunResult ChangedNothing "" () (return ()) + ruleUnit :: Rules () -ruleUnit = addRule $ \(Rule :: Rule ()) old mode -> do - return $ RunResult ChangedRecomputeDiff "" () +ruleUnit = addRule $ \(Rule :: Rule ()) _old _mode -> do + return $ RunResult ChangedRecomputeDiff "" () (return ()) -- | Depends on Rule @() ruleBool :: Rules () -ruleBool = addRule $ \Rule old mode -> do +ruleBool = addRule $ \Rule _old _mode -> do () <- apply1 Rule - return $ RunResult ChangedRecomputeDiff "" True + return $ RunResult ChangedRecomputeDiff "" True (return ()) + + +data CondRule = CondRule + deriving (Eq, Generic, Hashable, NFData, Show) +type instance RuleResult CondRule = Bool + + +ruleCond :: C.MVar Bool -> Rules () +ruleCond mv = addRule $ \CondRule _old _mode -> do + r <- liftIO $ C.modifyMVar mv $ \x -> return (not x, x) + return $ RunResult ChangedRecomputeDiff "" r (return ()) + +data BranchedRule = BranchedRule + deriving (Eq, Generic, Hashable, NFData, Show) +type instance RuleResult BranchedRule = Int + +ruleWithCond :: Rules () +ruleWithCond = addRule $ \BranchedRule _old _mode -> do + r <- apply1 CondRule + if r then do + _ <- apply1 SubBranchRule + return $ RunResult ChangedRecomputeDiff "" (1 :: Int) (return ()) + else + return $ RunResult ChangedRecomputeDiff "" (2 :: Int) (return ()) + +data SubBranchRule = SubBranchRule + deriving (Eq, Generic, Hashable, NFData, Show) +type instance RuleResult SubBranchRule = Int + +ruleSubBranch :: C.MVar Int -> Rules () +ruleSubBranch mv = addRule $ \SubBranchRule _old _mode -> do + r <- liftIO $ C.modifyMVar mv $ \x -> return (x+1, x) + return $ RunResult ChangedRecomputeDiff "" r (return ()) + +data CountRule = CountRule + deriving (Eq, Generic, Hashable, NFData, Show) +type instance RuleResult CountRule = Int diff --git a/hls-plugin-api/bench/Main.hs b/hls-plugin-api/bench/Main.hs index 0fc64f49f1..52006af16d 100644 --- a/hls-plugin-api/bench/Main.hs +++ b/hls-plugin-api/bench/Main.hs @@ -2,17 +2,17 @@ -- vs RangeMap-based "in-range filtering" approaches module Main (main) where -import Control.DeepSeq (force) -import Control.Exception (evaluate) -import Control.Monad (replicateM) +import Control.DeepSeq (force) +import Control.Exception (evaluate) +import Control.Monad (replicateM) import qualified Criterion import qualified Criterion.Main -import Data.Random (RVar) -import qualified Data.Random as Fu -import qualified Ide.Plugin.RangeMap as RangeMap -import Language.LSP.Types (Position (..), Range (..), UInt, - isSubrangeOf) -import qualified System.Random.Stateful as Random +import Data.Random (RVar) +import qualified Data.Random as Fu +import qualified Ide.Plugin.RangeMap as RangeMap +import Language.LSP.Protocol.Types (Position (..), Range (..), UInt, + isSubrangeOf) +import qualified System.Random.Stateful as Random genRangeList :: Int -> RVar [Range] diff --git a/hls-plugin-api/hls-plugin-api.cabal b/hls-plugin-api/hls-plugin-api.cabal index df60db344c..bad55992bb 100644 --- a/hls-plugin-api/hls-plugin-api.cabal +++ b/hls-plugin-api/hls-plugin-api.cabal @@ -1,6 +1,6 @@ cabal-version: 2.4 name: hls-plugin-api -version: 2.4.0.0 +version: 2.11.0.0 synopsis: Haskell Language Server API for plugin communication description: Please see the README on GitHub at @@ -32,12 +32,19 @@ source-repository head type: git location: https://github.com/haskell/haskell-language-server +common warnings + ghc-options: + -Wall -Wredundant-constraints -Wunused-packages + -Wno-name-shadowing -Wno-unticked-promoted-constructors + library + import: warnings exposed-modules: Ide.Logger Ide.Plugin.Config Ide.Plugin.ConfigUtils Ide.Plugin.Error + Ide.Plugin.HandleRequestTypes Ide.Plugin.Properties Ide.Plugin.RangeMap Ide.Plugin.Resolve @@ -53,23 +60,22 @@ library , data-default , dependent-map , dependent-sum >=0.7 - , Diff ^>=0.4.0 + , Diff ^>=0.5 || ^>=1.0.0 , dlist , extra , filepath , ghc , hashable - , hls-graph ==2.4.0.0 + , hls-graph == 2.11.0.0 , lens , lens-aeson - , lsp ^>=2.3 + , lsp ^>=2.7 , megaparsec >=9.0 , mtl , opentelemetry >=0.4 , optparse-applicative , prettyprinter , regex-tdfa >=1.3.1.0 - , row-types , stm , text , time @@ -83,10 +89,6 @@ library else build-depends: unix - ghc-options: - -Wall -Wredundant-constraints -Wno-name-shadowing - -Wno-unticked-promoted-constructors -Wunused-packages - if flag(pedantic) ghc-options: -Werror @@ -94,15 +96,14 @@ library cpp-options: -DUSE_FINGERTREE build-depends: hw-fingertree - default-language: Haskell2010 + default-language: GHC2021 default-extensions: DataKinds - KindSignatures - TypeOperators test-suite tests + import: warnings type: exitcode-stdio-1.0 - default-language: Haskell2010 + default-language: GHC2021 hs-source-dirs: test main-is: Main.hs ghc-options: -threaded -rtsopts -with-rtsopts=-N @@ -111,6 +112,8 @@ test-suite tests Ide.TypesTests build-depends: + , bytestring + , aeson , base , containers , data-default @@ -118,22 +121,24 @@ test-suite tests , lens , lsp-types , tasty + , tasty-golden , tasty-hunit , tasty-quickcheck , tasty-rerun , text benchmark rangemap-benchmark + import: warnings -- Benchmark doesn't make sense if fingertree implementation -- is not used. if !flag(use-fingertree) buildable: False type: exitcode-stdio-1.0 - default-language: Haskell2010 + default-language: GHC2021 hs-source-dirs: bench main-is: Main.hs - ghc-options: -threaded -Wall + ghc-options: -threaded build-depends: , base , criterion diff --git a/hls-plugin-api/src/Ide/Logger.hs b/hls-plugin-api/src/Ide/Logger.hs index aab41f4e73..d9d1eb95b3 100644 --- a/hls-plugin-api/src/Ide/Logger.hs +++ b/hls-plugin-api/src/Ide/Logger.hs @@ -1,23 +1,16 @@ -- Copyright (c) 2019 The DAML Authors. All rights reserved. -- SPDX-License-Identifier: Apache-2.0 -{-# LANGUAGE DeriveFunctor #-} -{-# LANGUAGE LambdaCase #-} -{-# LANGUAGE NamedFieldPuns #-} -{-# LANGUAGE OverloadedStrings #-} -{-# LANGUAGE RankNTypes #-} -{-# LANGUAGE RecordWildCards #-} -{-# LANGUAGE ScopedTypeVariables #-} +{-# LANGUAGE LambdaCase #-} +{-# LANGUAGE OverloadedStrings #-} +{-# LANGUAGE RecordWildCards #-} -- | This is a compatibility module that abstracts over the -- concrete choice of logging framework so users can plug in whatever -- framework they want to. module Ide.Logger ( Priority(..) - , Logger(..) , Recorder(..) - , logError, logWarning, logInfo, logDebug - , noLogging , WithPriority(..) , logWith , cmap @@ -34,6 +27,7 @@ module Ide.Logger , module PrettyPrinterModule , renderStrict , toCologActionWithPrio + , defaultLoggingColumns ) where import Colog.Core (LogAction (..), Severity, @@ -52,7 +46,6 @@ import Data.Foldable (for_) import Data.Functor.Contravariant (Contravariant (contramap)) import Data.Maybe (fromMaybe) import Data.Text (Text) -import qualified Data.Text as T import qualified Data.Text as Text import qualified Data.Text.IO as Text import Data.Time (defaultTimeLocale, formatTime, @@ -85,32 +78,6 @@ data Priority | Error -- ^ Such log messages must never occur in expected usage. deriving (Eq, Show, Read, Ord, Enum, Bounded) --- | Note that this is logging actions _of the program_, not of the user. --- You shouldn't call warning/error if the user has caused an error, only --- if our code has gone wrong and is itself erroneous (e.g. we threw an exception). -newtype Logger = Logger {logPriority :: Priority -> T.Text -> IO ()} - -instance Semigroup Logger where - l1 <> l2 = Logger $ \p t -> logPriority l1 p t >> logPriority l2 p t - -instance Monoid Logger where - mempty = Logger $ \_ _ -> pure () - -logError :: Logger -> T.Text -> IO () -logError x = logPriority x Error - -logWarning :: Logger -> T.Text -> IO () -logWarning x = logPriority x Warning - -logInfo :: Logger -> T.Text -> IO () -logInfo x = logPriority x Info - -logDebug :: Logger -> T.Text -> IO () -logDebug x = logPriority x Debug - -noLogging :: Logger -noLogging = Logger $ \_ _ -> return () - data WithPriority a = WithPriority { priority :: Priority, callStack_ :: CallStack, payload :: a } deriving Functor -- | Note that this is logging actions _of the program_, not of the user. @@ -178,7 +145,7 @@ withFileRecorder path columns action = do fileHandle :: Either IOException Handle <- liftIO $ try (openFile path AppendMode) case fileHandle of Left e -> action $ Left e - Right fileHandle -> finally ((Right <$> makeHandleRecorder fileHandle) >>= action) (liftIO $ hClose fileHandle) + Right fileHandle -> finally (makeHandleRecorder fileHandle >>= action . Right) (liftIO $ hClose fileHandle) makeDefaultHandleRecorder :: MonadIO m diff --git a/hls-plugin-api/src/Ide/Plugin/Config.hs b/hls-plugin-api/src/Ide/Plugin/Config.hs index 785a7a5a92..4fee92c309 100644 --- a/hls-plugin-api/src/Ide/Plugin/Config.hs +++ b/hls-plugin-api/src/Ide/Plugin/Config.hs @@ -1,5 +1,4 @@ {-# LANGUAGE DerivingStrategies #-} -{-# LANGUAGE FlexibleInstances #-} {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE TypeFamilies #-} {-# LANGUAGE ViewPatterns #-} @@ -43,6 +42,7 @@ parseConfig idePlugins defValue = A.withObject "settings" $ \o -> <*> o .:? "formattingProvider" .!= formattingProvider defValue <*> o .:? "cabalFormattingProvider" .!= cabalFormattingProvider defValue <*> o .:? "maxCompletions" .!= maxCompletions defValue + <*> o .:? "sessionLoading" .!= sessionLoading defValue <*> A.explicitParseFieldMaybe (parsePlugins idePlugins) o "plugin" .!= plugins defValue -- | Parse the 'PluginConfig'. @@ -63,18 +63,20 @@ parsePlugins (IdePlugins plugins) = A.withObject "Config.plugins" $ \o -> do -- --------------------------------------------------------------------- parsePluginConfig :: PluginConfig -> Value -> A.Parser PluginConfig -parsePluginConfig def = A.withObject "PluginConfig" $ \o -> PluginConfig +parsePluginConfig def = A.withObject "PluginConfig" $ \o -> PluginConfig <$> o .:? "globalOn" .!= plcGlobalOn def <*> o .:? "callHierarchyOn" .!= plcCallHierarchyOn def <*> o .:? "codeActionsOn" .!= plcCodeActionsOn def <*> o .:? "codeLensOn" .!= plcCodeLensOn def + <*> o .:? "inlayHintsOn" .!= plcInlayHintsOn def <*> o .:? "diagnosticsOn" .!= plcDiagnosticsOn def -- AZ <*> o .:? "hoverOn" .!= plcHoverOn def <*> o .:? "symbolsOn" .!= plcSymbolsOn def <*> o .:? "completionOn" .!= plcCompletionOn def <*> o .:? "renameOn" .!= plcRenameOn def <*> o .:? "selectionRangeOn" .!= plcSelectionRangeOn def - <*> o .:? "foldingRangeOn" .!= plcFoldingRangeOn def + <*> o .:? "foldingRangeOn" .!= plcFoldingRangeOn def + <*> o .:? "semanticTokensOn" .!= plcSemanticTokensOn def <*> o .:? "config" .!= plcConfig def -- --------------------------------------------------------------------- diff --git a/hls-plugin-api/src/Ide/Plugin/ConfigUtils.hs b/hls-plugin-api/src/Ide/Plugin/ConfigUtils.hs index 6111de4a48..a7350ab344 100644 --- a/hls-plugin-api/src/Ide/Plugin/ConfigUtils.hs +++ b/hls-plugin-api/src/Ide/Plugin/ConfigUtils.hs @@ -3,7 +3,11 @@ {-# LANGUAGE RecordWildCards #-} {-# LANGUAGE ViewPatterns #-} -module Ide.Plugin.ConfigUtils where +module Ide.Plugin.ConfigUtils ( + pluginsToDefaultConfig, + pluginsToVSCodeExtensionSchema, + pluginsCustomConfigToMarkdownTables + ) where import Control.Lens (at, (&), (?~)) import qualified Data.Aeson as A @@ -15,8 +19,15 @@ import qualified Data.Dependent.Sum as DSum import Data.List.Extra (nubOrd) import Data.String (IsString (fromString)) import qualified Data.Text as T +import GHC.TypeLits (symbolVal) import Ide.Plugin.Config -import Ide.Plugin.Properties (toDefaultJSON, +import Ide.Plugin.Properties (KeyNameProxy, MetaData (..), + PluginCustomConfig (..), + PluginCustomConfigParam (..), + Properties (..), + SPropertyKey (..), + SomePropertyKeyWithMetaData (..), + toDefaultJSON, toVSCodeExtensionSchema) import Ide.Types import Language.LSP.Protocol.Message @@ -31,10 +42,10 @@ pluginsToDefaultConfig :: IdePlugins a -> A.Value pluginsToDefaultConfig IdePlugins {..} = -- Use '_Object' and 'at' to get at the "plugin" key -- and actually set it. - A.toJSON defaultConfig & _Object . at "plugin" ?~ elems + A.toJSON defaultConfig & _Object . at "plugin" ?~ pluginSpecificDefaultConfigs where - defaultConfig@Config {} = def - elems = A.object $ mconcat $ singlePlugin <$> ipMap + defaultConfig = def :: Config + pluginSpecificDefaultConfigs = A.object $ mconcat $ singlePlugin <$> ipMap -- Splice genericDefaultConfig and dedicatedDefaultConfig -- Example: -- @@ -48,6 +59,7 @@ pluginsToDefaultConfig IdePlugins {..} = -- } -- } -- } + singlePlugin :: PluginDescriptor ideState -> [A.Pair] singlePlugin PluginDescriptor {pluginConfigDescriptor = ConfigDescriptor {..}, ..} = let x = genericDefaultConfig <> dedicatedDefaultConfig in [fromString (T.unpack pId) A..= A.object x | not $ null x] @@ -66,8 +78,8 @@ pluginsToDefaultConfig IdePlugins {..} = <> nubOrd (mconcat (handlersToGenericDefaultConfig configInitialGenericConfig <$> handlers)) in case x of - -- if the plugin has only one capability, we produce globalOn instead of the specific one; - -- otherwise we don't produce globalOn at all + -- If the plugin has only one capability, we produce globalOn instead of the specific one; + -- otherwise we omit globalOn [_] -> ["globalOn" A..= plcGlobalOn configInitialGenericConfig] _ -> x -- Example: @@ -88,11 +100,14 @@ pluginsToDefaultConfig IdePlugins {..} = handlersToGenericDefaultConfig PluginConfig{..} (IdeMethod m DSum.:=> _) = case m of SMethod_TextDocumentCodeAction -> ["codeActionsOn" A..= plcCodeActionsOn] SMethod_TextDocumentCodeLens -> ["codeLensOn" A..= plcCodeLensOn] + SMethod_TextDocumentInlayHint -> ["inlayHintsOn" A..= plcInlayHintsOn] SMethod_TextDocumentRename -> ["renameOn" A..= plcRenameOn] SMethod_TextDocumentHover -> ["hoverOn" A..= plcHoverOn] SMethod_TextDocumentDocumentSymbol -> ["symbolsOn" A..= plcSymbolsOn] SMethod_TextDocumentCompletion -> ["completionOn" A..= plcCompletionOn] SMethod_TextDocumentPrepareCallHierarchy -> ["callHierarchyOn" A..= plcCallHierarchyOn] + SMethod_TextDocumentSemanticTokensFull -> ["semanticTokensOn" A..= plcSemanticTokensOn] + SMethod_TextDocumentSemanticTokensFullDelta -> ["semanticTokensOn" A..= plcSemanticTokensOn] _ -> [] -- | Generates json schema used in haskell vscode extension @@ -107,29 +122,121 @@ pluginsToVSCodeExtensionSchema IdePlugins {..} = A.object $ mconcat $ singlePlug (PluginId pId) = pluginId genericSchema = let x = - [toKey' "diagnosticsOn" A..= schemaEntry "diagnostics" | configHasDiagnostics] - <> nubOrd (mconcat (handlersToGenericSchema <$> handlers)) + [toKey' "diagnosticsOn" A..= schemaEntry "diagnostics" True | configHasDiagnostics] + <> nubOrd (mconcat (handlersToGenericSchema configInitialGenericConfig <$> handlers)) in case x of -- If the plugin has only one capability, we produce globalOn instead of the specific one; -- otherwise we don't produce globalOn at all - [_] -> [toKey' "globalOn" A..= schemaEntry "plugin"] + [_] -> [toKey' "globalOn" A..= schemaEntry "plugin" (plcGlobalOn configInitialGenericConfig)] _ -> x dedicatedSchema = customConfigToDedicatedSchema configCustomConfig - handlersToGenericSchema (IdeMethod m DSum.:=> _) = case m of - SMethod_TextDocumentCodeAction -> [toKey' "codeActionsOn" A..= schemaEntry "code actions"] - SMethod_TextDocumentCodeLens -> [toKey' "codeLensOn" A..= schemaEntry "code lenses"] - SMethod_TextDocumentRename -> [toKey' "renameOn" A..= schemaEntry "rename"] - SMethod_TextDocumentHover -> [toKey' "hoverOn" A..= schemaEntry "hover"] - SMethod_TextDocumentDocumentSymbol -> [toKey' "symbolsOn" A..= schemaEntry "symbols"] - SMethod_TextDocumentCompletion -> [toKey' "completionOn" A..= schemaEntry "completions"] - SMethod_TextDocumentPrepareCallHierarchy -> [toKey' "callHierarchyOn" A..= schemaEntry "call hierarchy"] - _ -> [] - schemaEntry desc = + handlersToGenericSchema PluginConfig{..} (IdeMethod m DSum.:=> _) = case m of + SMethod_TextDocumentCodeAction -> [toKey' "codeActionsOn" A..= schemaEntry "code actions" plcCodeActionsOn] + SMethod_TextDocumentCodeLens -> [toKey' "codeLensOn" A..= schemaEntry "code lenses" plcCodeLensOn] + SMethod_TextDocumentInlayHint -> [toKey' "inlayHintsOn" A..= schemaEntry "inlay hints" plcInlayHintsOn] + SMethod_TextDocumentRename -> [toKey' "renameOn" A..= schemaEntry "rename" plcRenameOn] + SMethod_TextDocumentHover -> [toKey' "hoverOn" A..= schemaEntry "hover" plcHoverOn] + SMethod_TextDocumentDocumentSymbol -> [toKey' "symbolsOn" A..= schemaEntry "symbols" plcSymbolsOn] + SMethod_TextDocumentCompletion -> [toKey' "completionOn" A..= schemaEntry "completions" plcCompletionOn] + SMethod_TextDocumentPrepareCallHierarchy -> [toKey' "callHierarchyOn" A..= schemaEntry "call hierarchy" plcCallHierarchyOn] + SMethod_TextDocumentSemanticTokensFull -> [toKey' "semanticTokensOn" A..= schemaEntry "semantic tokens" plcSemanticTokensOn] + SMethod_TextDocumentSemanticTokensFullDelta -> [toKey' "semanticTokensOn" A..= schemaEntry "semantic tokens" plcSemanticTokensOn] + _ -> [] + schemaEntry desc defaultVal = A.object [ "scope" A..= A.String "resource", "type" A..= A.String "boolean", - "default" A..= True, + "default" A..= A.Bool defaultVal, "description" A..= A.String ("Enables " <> pId <> " " <> desc) ] withIdPrefix x = "haskell.plugin." <> pId <> "." <> x toKey' = fromString . T.unpack . withIdPrefix + + +-- | Generates markdown tables for custom config +pluginsCustomConfigToMarkdownTables :: IdePlugins a -> T.Text +pluginsCustomConfigToMarkdownTables IdePlugins {..} = T.unlines + $ map renderCfg + $ filter (\(PluginCustomConfig _ params) -> not $ null params) + $ map toPluginCustomConfig ipMap + where + toPluginCustomConfig :: PluginDescriptor ideState -> PluginCustomConfig + toPluginCustomConfig PluginDescriptor {pluginConfigDescriptor = ConfigDescriptor {configCustomConfig = c}, pluginId = PluginId pId} = + PluginCustomConfig { pcc'Name = pId, pcc'Params = toPluginCustomConfigParams c} + toPluginCustomConfigParams :: CustomConfig -> [PluginCustomConfigParam] + toPluginCustomConfigParams (CustomConfig p) = toPluginCustomConfigParams' p + toPluginCustomConfigParams' :: Properties r -> [PluginCustomConfigParam] + toPluginCustomConfigParams' EmptyProperties = [] + toPluginCustomConfigParams' (ConsProperties (keyNameProxy :: KeyNameProxy s) (k :: SPropertyKey k) (m :: MetaData t) xs) = + toEntry (SomePropertyKeyWithMetaData k m) : toPluginCustomConfigParams' xs + where + toEntry :: SomePropertyKeyWithMetaData -> PluginCustomConfigParam + toEntry (SomePropertyKeyWithMetaData SNumber MetaData {..}) = + PluginCustomConfigParam { + pccp'Name = T.pack $ symbolVal keyNameProxy, + pccp'Description = description, + pccp'Default = T.pack $ show defaultValue, + pccp'EnumValues = [] + } + toEntry (SomePropertyKeyWithMetaData SInteger MetaData {..}) = + PluginCustomConfigParam { + pccp'Name = T.pack $ symbolVal keyNameProxy, + pccp'Description = description, + pccp'Default = T.pack $ show defaultValue, + pccp'EnumValues = [] + } + toEntry (SomePropertyKeyWithMetaData SString MetaData {..}) = + PluginCustomConfigParam { + pccp'Name = T.pack $ symbolVal keyNameProxy, + pccp'Description = description, + pccp'Default = T.pack $ show defaultValue, + pccp'EnumValues = [] + } + toEntry (SomePropertyKeyWithMetaData SBoolean MetaData {..}) = + PluginCustomConfigParam { + pccp'Name = T.pack $ symbolVal keyNameProxy, + pccp'Description = description, + pccp'Default = T.pack $ show defaultValue, + pccp'EnumValues = [] + } + toEntry (SomePropertyKeyWithMetaData (SObject _) MetaData {..}) = + PluginCustomConfigParam { + pccp'Name = T.pack $ symbolVal keyNameProxy, + pccp'Description = description, + pccp'Default = "TODO: nested object", -- T.pack $ show defaultValue, + pccp'EnumValues = [] + } + toEntry (SomePropertyKeyWithMetaData (SArray _) MetaData {..}) = + PluginCustomConfigParam { + pccp'Name = T.pack $ symbolVal keyNameProxy, + pccp'Description = description, + pccp'Default = "TODO: Array values", -- T.pack $ show defaultValue, + pccp'EnumValues = [] + } + toEntry (SomePropertyKeyWithMetaData (SEnum _) EnumMetaData {..}) = + PluginCustomConfigParam { + pccp'Name = T.pack $ symbolVal keyNameProxy, + pccp'Description = description, + pccp'Default = T.pack $ show defaultValue, + pccp'EnumValues = map (T.pack . show) enumValues + } + toEntry (SomePropertyKeyWithMetaData SProperties PropertiesMetaData {..}) = + PluginCustomConfigParam { + pccp'Name = T.pack $ symbolVal keyNameProxy, + pccp'Description = description, + pccp'Default = T.pack $ show defaultValue, + pccp'EnumValues = [] + } + renderCfg :: PluginCustomConfig -> T.Text + renderCfg (PluginCustomConfig pId pccParams) = + T.unlines (pluginHeader : tableHeader : rows pccParams) + where + pluginHeader = "## " <> pId + tableHeader = + "| Property | Description | Default | Allowed values |" <> "\n" <> + "| --- | --- | --- | --- |" + rows = map renderRow + renderRow PluginCustomConfigParam {..} = + "| `" <> pccp'Name <> "` | " <> pccp'Description <> " | `" <> pccp'Default <> "` | " <> renderEnum pccp'EnumValues <> " |" + renderEnum [] = "   " -- Placeholder to prevent missing cells + renderEnum vs = "
    " <> (T.intercalate " " $ map (\x -> "
  • " <> x <> "
  • ") vs) <> "
" diff --git a/hls-plugin-api/src/Ide/Plugin/Error.hs b/hls-plugin-api/src/Ide/Plugin/Error.hs index ce874b744a..b323079aff 100644 --- a/hls-plugin-api/src/Ide/Plugin/Error.hs +++ b/hls-plugin-api/src/Ide/Plugin/Error.hs @@ -1,6 +1,5 @@ -{-# LANGUAGE ExistentialQuantification #-} -{-# LANGUAGE LambdaCase #-} -{-# LANGUAGE OverloadedStrings #-} +{-# LANGUAGE LambdaCase #-} +{-# LANGUAGE OverloadedStrings #-} module Ide.Plugin.Error ( -- * Plugin Error Handling API PluginError(..), @@ -11,11 +10,12 @@ module Ide.Plugin.Error ( getNormalizedFilePathE, ) where -import Control.Monad.Extra (maybeM) -import Control.Monad.Trans.Class (lift) -import Control.Monad.Trans.Except (ExceptT (..), throwE) -import qualified Data.Text as T +import Control.Monad.Extra (maybeM) +import Control.Monad.Trans.Class (lift) +import Control.Monad.Trans.Except (ExceptT (..), throwE) +import qualified Data.Text as T import Ide.Logger +import Ide.Plugin.HandleRequestTypes (RejectionReason) import Language.LSP.Protocol.Types -- ---------------------------------------------------------------------------- @@ -79,13 +79,13 @@ data PluginError | PluginInvalidUserState T.Text -- |PluginRequestRefused allows your handler to inspect a request before -- rejecting it. In effect it allows your plugin to act make a secondary - -- `pluginEnabled` decision after receiving the request. This should only be + -- `handlesRequest` decision after receiving the request. This should only be -- used if the decision to accept the request can not be made in - -- `pluginEnabled`. + -- `handlesRequest`. -- -- This error will be with Debug. If it's the only response to a request, - -- HLS will respond as if no plugins passed the `pluginEnabled` stage. - | PluginRequestRefused T.Text + -- HLS will respond as if no plugins passed the `handlesRequest` stage. + | PluginRequestRefused RejectionReason -- |PluginRuleFailed should be thrown when a Rule your response depends on -- fails. -- diff --git a/hls-plugin-api/src/Ide/Plugin/HandleRequestTypes.hs b/hls-plugin-api/src/Ide/Plugin/HandleRequestTypes.hs new file mode 100644 index 0000000000..20b81efa2d --- /dev/null +++ b/hls-plugin-api/src/Ide/Plugin/HandleRequestTypes.hs @@ -0,0 +1,46 @@ +{-# LANGUAGE OverloadedStrings #-} + +module Ide.Plugin.HandleRequestTypes where + +import Data.Text +import Prettyprinter + +-- | Reasons why a plugin could reject a specific request. +data RejectionReason = + -- | The resolve request is not meant for this plugin or handler. The text + -- field should contain the identifier for the plugin who owns this resolve + -- request. + NotResolveOwner Text + -- | The plugin is disabled globally in the users config. + | DisabledGlobally + -- | The feature in the plugin that responds to this request is disabled in + -- the users config + | FeatureDisabled + -- | This plugin is not the formatting provider selected in the users config. + -- The text should be the formatting provider in your config. + | NotFormattingProvider Text + -- | This plugin does not support the file type. The text field here should + -- contain the filetype of the rejected request. + | DoesNotSupportFileType Text + deriving (Eq) + +-- | Whether a plugin will handle a request or not. +data HandleRequestResult = HandlesRequest | DoesNotHandleRequest RejectionReason + deriving (Eq) + +instance Pretty HandleRequestResult where + pretty HandlesRequest = "handles this request" + pretty (DoesNotHandleRequest reason) = pretty reason + +instance Pretty RejectionReason where + pretty (NotResolveOwner s) = "does not handle resolve requests for " <> pretty s <> ")." + pretty DisabledGlobally = "is disabled globally in your config." + pretty FeatureDisabled = "'s feature that handles this request is disabled in your config." + pretty (NotFormattingProvider s) = "is not the formatting provider ("<> pretty s<>") you chose in your config." + pretty (DoesNotSupportFileType s) = "does not support " <> pretty s <> " filetypes)." + +-- We always want to keep the leftmost disabled reason +instance Semigroup HandleRequestResult where + HandlesRequest <> HandlesRequest = HandlesRequest + DoesNotHandleRequest r <> _ = DoesNotHandleRequest r + _ <> DoesNotHandleRequest r = DoesNotHandleRequest r diff --git a/hls-plugin-api/src/Ide/Plugin/Properties.hs b/hls-plugin-api/src/Ide/Plugin/Properties.hs index 9baaf26833..49a45721b4 100644 --- a/hls-plugin-api/src/Ide/Plugin/Properties.hs +++ b/hls-plugin-api/src/Ide/Plugin/Properties.hs @@ -6,23 +6,27 @@ {-# LANGUAGE LambdaCase #-} {-# LANGUAGE MultiParamTypeClasses #-} {-# LANGUAGE OverloadedStrings #-} +{-# LANGUAGE RankNTypes #-} {-# LANGUAGE RecordWildCards #-} {-# LANGUAGE ScopedTypeVariables #-} {-# LANGUAGE TypeFamilies #-} {-# LANGUAGE TypeOperators #-} {-# LANGUAGE UndecidableInstances #-} --- See Note [Constraints] -{-# OPTIONS_GHC -Wno-redundant-constraints #-} + module Ide.Plugin.Properties ( PropertyType (..), ToHsType, + NotElem, MetaData (..), PropertyKey (..), SPropertyKey (..), + SomePropertyKeyWithMetaData (..), KeyNameProxy (..), - Properties, + KeyNamePath (..), + Properties(..), HasProperty, + HasPropertyByPath, emptyProperties, defineNumberProperty, defineIntegerProperty, @@ -31,26 +35,31 @@ module Ide.Plugin.Properties defineObjectProperty, defineArrayProperty, defineEnumProperty, + definePropertiesProperty, toDefaultJSON, toVSCodeExtensionSchema, usePropertyEither, useProperty, + usePropertyByPathEither, + usePropertyByPath, (&), + PluginCustomConfig(..), + PluginCustomConfigParam(..), ) where +import Control.Arrow (first) import qualified Data.Aeson as A import qualified Data.Aeson.Types as A import Data.Either (fromRight) import Data.Function ((&)) import Data.Kind (Constraint, Type) -import qualified Data.Map.Strict as Map import Data.Proxy (Proxy (..)) import Data.String (IsString (fromString)) import qualified Data.Text as T import GHC.OverloadedLabels (IsLabel (..)) import GHC.TypeLits -import Unsafe.Coerce (unsafeCoerce) + -- | Types properties may have data PropertyType @@ -61,6 +70,7 @@ data PropertyType | TObject Type | TArray Type | TEnum Type + | TProperties [PropertyKey] -- ^ A typed TObject, defined in a recursive manner type family ToHsType (t :: PropertyType) where ToHsType 'TNumber = Double -- in js, there are no distinct types for integers and floating-point values @@ -70,13 +80,14 @@ type family ToHsType (t :: PropertyType) where ToHsType ('TObject a) = a ToHsType ('TArray a) = [a] ToHsType ('TEnum a) = a + ToHsType ('TProperties _) = A.Object -- --------------------------------------------------------------------- -- | Metadata of a property data MetaData (t :: PropertyType) where MetaData :: - (IsTEnum t ~ 'False) => + (IsTEnum t ~ 'False, IsProperties t ~ 'False) => { defaultValue :: ToHsType t, description :: T.Text } -> @@ -89,6 +100,15 @@ data MetaData (t :: PropertyType) where enumDescriptions :: [T.Text] } -> MetaData t + PropertiesMetaData :: + (t ~ TProperties rs) => + { + defaultValue :: ToHsType t + , description :: T.Text + , childrenProperties :: Properties rs + } -> + MetaData t + -- | Used at type level for name-type mapping in 'Properties' data PropertyKey = PropertyKey Symbol PropertyType @@ -102,6 +122,7 @@ data SPropertyKey (k :: PropertyKey) where SObject :: (A.ToJSON a, A.FromJSON a) => Proxy a -> SPropertyKey ('PropertyKey s ('TObject a)) SArray :: (A.ToJSON a, A.FromJSON a) => Proxy a -> SPropertyKey ('PropertyKey s ('TArray a)) SEnum :: (A.ToJSON a, A.FromJSON a, Eq a, Show a) => Proxy a -> SPropertyKey ('PropertyKey s ('TEnum a)) + SProperties :: SPropertyKey ('PropertyKey s ('TProperties pp)) -- | Existential wrapper of 'SPropertyKey', with an extra 'MetaData' data SomePropertyKeyWithMetaData @@ -110,11 +131,14 @@ data SomePropertyKeyWithMetaData SomePropertyKeyWithMetaData (SPropertyKey k) (MetaData t) -- | 'Properties' is a partial implementation of json schema, without supporting union types and validation. --- In hls, it defines a set of properties which used in dedicated configuration of a plugin. +-- In hls, it defines a set of properties used in dedicated configuration of a plugin. -- A property is an immediate child of the json object in each plugin's "config" section. -- It was designed to be compatible with vscode's settings UI. -- Use 'emptyProperties' and 'useProperty' to create and consume 'Properties'. -newtype Properties (r :: [PropertyKey]) = Properties (Map.Map String SomePropertyKeyWithMetaData) +data Properties (r :: [PropertyKey]) where + ConsProperties :: (k ~ 'PropertyKey s t, KnownSymbol s, NotElem s ks) + => KeyNameProxy s -> (SPropertyKey k) -> (MetaData t) -> Properties ks -> Properties (k : ks) + EmptyProperties :: Properties '[] -- | A proxy type in order to allow overloaded labels as properties' names at the call site data KeyNameProxy (s :: Symbol) = KnownSymbol s => KeyNameProxy @@ -122,16 +146,61 @@ data KeyNameProxy (s :: Symbol) = KnownSymbol s => KeyNameProxy instance (KnownSymbol s', s ~ s') => IsLabel s (KeyNameProxy s') where fromLabel = KeyNameProxy +data NonEmptyList a = + a :| NonEmptyList a | NE a + +-- | a path to a property in a json object +data KeyNamePath (r :: NonEmptyList Symbol) where + SingleKey :: KeyNameProxy s -> KeyNamePath (NE s) + ConsKeysPath :: KeyNameProxy s1 -> KeyNamePath ss -> KeyNamePath (s1 :| ss) + +class ParsePropertyPath (rs :: [PropertyKey]) (r :: NonEmptyList Symbol) where + usePropertyByPathEither :: KeyNamePath r -> Properties rs -> A.Object -> Either String (ToHsType (FindByKeyPath r rs)) + useDefault :: KeyNamePath r -> Properties rs -> ToHsType (FindByKeyPath r rs) + usePropertyByPath :: KeyNamePath r -> Properties rs -> A.Object -> ToHsType (FindByKeyPath r rs) + usePropertyByPath p ps x = fromRight (useDefault p ps) $ usePropertyByPathEither p ps x + +instance (HasProperty s k t r) => ParsePropertyPath r (NE s) where + usePropertyByPathEither (SingleKey kn) sm x = parseProperty kn (find kn sm) x + useDefault (SingleKey kn) sm = defaultValue metadata + where (_, metadata) = find kn sm + +instance ( ToHsType (FindByKeyPath ss r2) ~ ToHsType (FindByKeyPath (s :| ss) r) + ,HasProperty s ('PropertyKey s ('TProperties r2)) t2 r + , ParsePropertyPath r2 ss) + => ParsePropertyPath r (s :| ss) where + usePropertyByPathEither (ConsKeysPath kn p) sm x = do + let (key, meta) = find kn sm + interMedia <- parseProperty kn (key, meta) x + case meta of + PropertiesMetaData {..} + -> usePropertyByPathEither p childrenProperties interMedia + useDefault (ConsKeysPath kn p) sm = case find kn sm of + (_, PropertiesMetaData {..}) -> useDefault p childrenProperties + -- --------------------------------------------------------------------- +type family IsProperties (t :: PropertyType) :: Bool where + IsProperties ('TProperties pp) = 'True + IsProperties _ = 'False + type family IsTEnum (t :: PropertyType) :: Bool where IsTEnum ('TEnum _) = 'True IsTEnum _ = 'False +type family FindByKeyPath (ne :: NonEmptyList Symbol) (r :: [PropertyKey]) :: PropertyType where + FindByKeyPath (s :| xs) ('PropertyKey s ('TProperties rs) ': _) = FindByKeyPath xs rs + FindByKeyPath (s :| xs) (_ ': ys) = FindByKeyPath (s :| xs) ys + FindByKeyPath (NE s) ys = FindByKeyName s ys + type family FindByKeyName (s :: Symbol) (r :: [PropertyKey]) :: PropertyType where FindByKeyName s ('PropertyKey s t ': _) = t FindByKeyName s (_ ': xs) = FindByKeyName s xs +type family IsPropertySymbol (s :: Symbol) (r :: PropertyKey) :: Bool where + IsPropertySymbol s ('PropertyKey s _) = 'True + IsPropertySymbol s _ = 'False + type family Elem (s :: Symbol) (r :: [PropertyKey]) :: Constraint where Elem s ('PropertyKey s _ ': _) = () Elem s (_ ': xs) = Elem s xs @@ -142,8 +211,21 @@ type family NotElem (s :: Symbol) (r :: [PropertyKey]) :: Constraint where NotElem s (_ ': xs) = NotElem s xs NotElem s '[] = () + -- | In row @r@, there is a 'PropertyKey' @k@, which has name @s@ and carries haskell type @t@ -type HasProperty s k t r = (k ~ 'PropertyKey s t, Elem s r, FindByKeyName s r ~ t, KnownSymbol s) +type HasProperty s k t r = (k ~ 'PropertyKey s t, Elem s r, FindByKeyPath (NE s) r ~ t, FindByKeyName s r ~ t, KnownSymbol s, FindPropertyMeta s r t) +-- similar to HasProperty, but the path is given as a type-level list of symbols +type HasPropertyByPath props path t = (t ~ FindByKeyPath path props, ParsePropertyPath props path) +class FindPropertyMeta (s :: Symbol) (r :: [PropertyKey]) t where + findSomePropertyKeyWithMetaData :: KeyNameProxy s -> Properties r -> (SPropertyKey ('PropertyKey s t), MetaData t) +instance (FindPropertyMetaIf (IsPropertySymbol symbol k) symbol k ks t) => FindPropertyMeta symbol (k : ks) t where + findSomePropertyKeyWithMetaData = findSomePropertyKeyWithMetaDataIf +class (bool ~ IsPropertySymbol symbol k) => FindPropertyMetaIf bool symbol k ks t where + findSomePropertyKeyWithMetaDataIf :: KeyNameProxy symbol -> Properties (k : ks) -> (SPropertyKey ('PropertyKey symbol t), MetaData t) +instance (k ~ 'PropertyKey s t) => FindPropertyMetaIf 'True s k ks t where + findSomePropertyKeyWithMetaDataIf _ (ConsProperties _ k m _) = (k, m) +instance ('False ~ IsPropertySymbol s k, FindPropertyMeta s ks t) => FindPropertyMetaIf 'False s k ks t where + findSomePropertyKeyWithMetaDataIf s (ConsProperties _ _ _ ks) = findSomePropertyKeyWithMetaData s ks -- --------------------------------------------------------------------- @@ -164,7 +246,7 @@ type HasProperty s k t r = (k ~ 'PropertyKey s t, Elem s r, FindByKeyName s r ~ -- @ emptyProperties :: Properties '[] -emptyProperties = Properties Map.empty +emptyProperties = EmptyProperties insert :: (k ~ 'PropertyKey s t, NotElem s r, KnownSymbol s) => @@ -173,30 +255,14 @@ insert :: MetaData t -> Properties r -> Properties (k ': r) -insert kn key metadata (Properties old) = - Properties - ( Map.insert - (symbolVal kn) - (SomePropertyKeyWithMetaData key metadata) - old - ) +insert = ConsProperties find :: (HasProperty s k t r) => KeyNameProxy s -> Properties r -> (SPropertyKey k, MetaData t) -find kn (Properties p) = case p Map.! symbolVal kn of - (SomePropertyKeyWithMetaData sing metadata) -> - -- Note [Constraints] - -- It's safe to use unsafeCoerce here: - -- Since each property name is unique that the redefinition will be prevented by predication on the type level list, - -- the value we get from the name-indexed map must be exactly the singleton and metadata corresponding to the type. - -- We drop this information at type level: some of the above type families return '() :: Constraint', - -- so GHC will consider them as redundant. - -- But we encode it using semantically identical 'Map' at term level, - -- which avoids inducting on the list by defining a new type class. - unsafeCoerce (sing, metadata) +find = findSomePropertyKeyWithMetaData -- --------------------------------------------------------------------- @@ -227,6 +293,7 @@ parseProperty :: A.Object -> Either String (ToHsType t) parseProperty kn k x = case k of + (SProperties, _) -> parseEither (SNumber, _) -> parseEither (SInteger, _) -> parseEither (SString, _) -> parseEither @@ -346,11 +413,24 @@ defineEnumProperty :: defineEnumProperty kn description enums defaultValue = insert kn (SEnum Proxy) $ EnumMetaData defaultValue description (fst <$> enums) (snd <$> enums) +definePropertiesProperty :: + (KnownSymbol s, NotElem s r) => + KeyNameProxy s -> + T.Text -> + Properties childrenProps -> + Properties r -> + Properties ('PropertyKey s ('TProperties childrenProps) : r) +definePropertiesProperty kn description ps rs = + insert kn SProperties (PropertiesMetaData mempty description ps) rs + -- --------------------------------------------------------------------- -- | Converts a properties definition into kv pairs with default values from 'MetaData' toDefaultJSON :: Properties r -> [A.Pair] -toDefaultJSON (Properties p) = [toEntry s v | (s, v) <- Map.toList p] +toDefaultJSON pr = case pr of + EmptyProperties -> [] + ConsProperties keyNameProxy k m xs -> + toEntry (symbolVal keyNameProxy) (SomePropertyKeyWithMetaData k m) : toDefaultJSON xs where toEntry :: String -> SomePropertyKeyWithMetaData -> A.Pair toEntry s = \case @@ -368,58 +448,68 @@ toDefaultJSON (Properties p) = [toEntry s v | (s, v) <- Map.toList p] fromString s A..= defaultValue (SomePropertyKeyWithMetaData (SEnum _) EnumMetaData {..}) -> fromString s A..= defaultValue + (SomePropertyKeyWithMetaData SProperties PropertiesMetaData {..}) -> + fromString s A..= A.object (toDefaultJSON childrenProperties) -- | Converts a properties definition into kv pairs as vscode schema toVSCodeExtensionSchema :: T.Text -> Properties r -> [A.Pair] -toVSCodeExtensionSchema prefix (Properties p) = - [fromString (T.unpack prefix <> k) A..= toEntry v | (k, v) <- Map.toList p] +toVSCodeExtensionSchema prefix p = [fromString (T.unpack prefix <> fromString k) A..= v | (k, v) <- toVSCodeExtensionSchema' p] +toVSCodeExtensionSchema' :: Properties r -> [(String, A.Value)] +toVSCodeExtensionSchema' ps = case ps of + EmptyProperties -> [] + ConsProperties (keyNameProxy :: KeyNameProxy s) (k :: SPropertyKey k) (m :: MetaData t) xs -> + [(symbolVal keyNameProxy <> maybe "" ((<>) ".") k1, v) + | (k1, v) <- toEntry (SomePropertyKeyWithMetaData k m) ] + ++ toVSCodeExtensionSchema' xs where - toEntry :: SomePropertyKeyWithMetaData -> A.Value + wrapEmpty :: A.Value -> [(Maybe String, A.Value)] + wrapEmpty v = [(Nothing, v)] + toEntry :: SomePropertyKeyWithMetaData -> [(Maybe String, A.Value)] toEntry = \case (SomePropertyKeyWithMetaData SNumber MetaData {..}) -> - A.object + wrapEmpty $ A.object [ "type" A..= A.String "number", "markdownDescription" A..= description, "default" A..= defaultValue, "scope" A..= A.String "resource" ] (SomePropertyKeyWithMetaData SInteger MetaData {..}) -> - A.object + wrapEmpty $ A.object [ "type" A..= A.String "integer", "markdownDescription" A..= description, "default" A..= defaultValue, "scope" A..= A.String "resource" ] (SomePropertyKeyWithMetaData SString MetaData {..}) -> - A.object + wrapEmpty $ A.object [ "type" A..= A.String "string", "markdownDescription" A..= description, "default" A..= defaultValue, "scope" A..= A.String "resource" ] (SomePropertyKeyWithMetaData SBoolean MetaData {..}) -> - A.object + wrapEmpty $ A.object [ "type" A..= A.String "boolean", "markdownDescription" A..= description, "default" A..= defaultValue, "scope" A..= A.String "resource" ] (SomePropertyKeyWithMetaData (SObject _) MetaData {..}) -> - A.object + wrapEmpty $ A.object [ "type" A..= A.String "object", "markdownDescription" A..= description, "default" A..= defaultValue, "scope" A..= A.String "resource" ] (SomePropertyKeyWithMetaData (SArray _) MetaData {..}) -> - A.object + wrapEmpty $ A.object [ "type" A..= A.String "array", "markdownDescription" A..= description, "default" A..= defaultValue, "scope" A..= A.String "resource" ] (SomePropertyKeyWithMetaData (SEnum _) EnumMetaData {..}) -> - A.object + wrapEmpty $ A.object [ "type" A..= A.String "string", "description" A..= description, "enum" A..= enumValues, @@ -427,3 +517,17 @@ toVSCodeExtensionSchema prefix (Properties p) = "default" A..= defaultValue, "scope" A..= A.String "resource" ] + (SomePropertyKeyWithMetaData SProperties PropertiesMetaData {..}) -> + map (first Just) $ toVSCodeExtensionSchema' childrenProperties + +data PluginCustomConfig = PluginCustomConfig { + pcc'Name :: T.Text, + pcc'Params :: [PluginCustomConfigParam] +} +data PluginCustomConfigParam = PluginCustomConfigParam { + pccp'Name :: T.Text, + pccp'Description :: T.Text, + pccp'Default :: T.Text, + pccp'EnumValues :: [T.Text] +} + diff --git a/hls-plugin-api/src/Ide/Plugin/RangeMap.hs b/hls-plugin-api/src/Ide/Plugin/RangeMap.hs index 11d7ebe29e..6c4b4041c9 100644 --- a/hls-plugin-api/src/Ide/Plugin/RangeMap.hs +++ b/hls-plugin-api/src/Ide/Plugin/RangeMap.hs @@ -1,9 +1,5 @@ -{-# LANGUAGE CPP #-} -{-# LANGUAGE DeriveFoldable #-} -{-# LANGUAGE DeriveFunctor #-} -{-# LANGUAGE DeriveTraversable #-} -{-# LANGUAGE DerivingStrategies #-} -{-# LANGUAGE GeneralizedNewtypeDeriving #-} +{-# LANGUAGE CPP #-} +{-# LANGUAGE DerivingStrategies #-} -- | A map that allows fast \"in-range\" filtering. 'RangeMap' is meant -- to be constructed once and cached as part of a Shake rule. If @@ -17,16 +13,22 @@ module Ide.Plugin.RangeMap fromList, fromList', filterByRange, + elementsInRange, ) where -import Data.Bifunctor (first) -import Data.Foldable (foldl') import Development.IDE.Graph.Classes (NFData) -import Language.LSP.Protocol.Types (Position, - Range (Range), - isSubrangeOf) + #ifdef USE_FINGERTREE +import Data.Bifunctor (first) import qualified HaskellWorks.Data.IntervalMap.FingerTree as IM +import Language.LSP.Protocol.Types (Position, + Range (Range)) +#else +import Language.LSP.Protocol.Types (Range, isSubrangeOf) +#endif + +#if USE_FINGERTREE && !MIN_VERSION_base(4,20,0) +import Data.List (foldl') #endif -- | A map from code ranges to values. @@ -66,6 +68,14 @@ filterByRange range = map snd . IM.dominators (rangeToInterval range) . unRangeM filterByRange range = map snd . filter (isSubrangeOf range . fst) . unRangeMap #endif +-- | Extracts all elements from a 'RangeMap' that fall within a given 'Range'. +elementsInRange :: Range -> RangeMap a -> [a] +#ifdef USE_FINGERTREE +elementsInRange range = map snd . IM.intersections (rangeToInterval range) . unRangeMap +#else +elementsInRange range = map snd . filter (flip isSubrangeOf range . fst) . unRangeMap +#endif + #ifdef USE_FINGERTREE -- NOTE(ozkutuk): In itself, this conversion is wrong. As Michael put it: -- "LSP Ranges have exclusive upper bounds, whereas the intervals here are diff --git a/hls-plugin-api/src/Ide/Plugin/Resolve.hs b/hls-plugin-api/src/Ide/Plugin/Resolve.hs index 511b5f0a61..36c61baaff 100644 --- a/hls-plugin-api/src/Ide/Plugin/Resolve.hs +++ b/hls-plugin-api/src/Ide/Plugin/Resolve.hs @@ -1,11 +1,6 @@ -{-# LANGUAGE DeriveGeneric #-} {-# LANGUAGE DisambiguateRecordFields #-} {-# LANGUAGE LambdaCase #-} -{-# LANGUAGE NamedFieldPuns #-} -{-# LANGUAGE OverloadedLabels #-} {-# LANGUAGE OverloadedStrings #-} -{-# LANGUAGE RankNTypes #-} -{-# LANGUAGE ScopedTypeVariables #-} {-| This module currently includes helper functions to provide fallback support to code actions that use resolve in HLS. The difference between the two functions for code actions that don't support resolve is that @@ -26,11 +21,10 @@ import Control.Lens (_Just, (&), (.~), (?~), (^.), (^?)) import Control.Monad.Error.Class (MonadError (throwError)) import Control.Monad.Trans.Class (lift) -import Control.Monad.Trans.Except (ExceptT (..), runExceptT) +import Control.Monad.Trans.Except (ExceptT (..)) import qualified Data.Aeson as A import Data.Maybe (catMaybes) -import Data.Row ((.!)) import qualified Data.Text as T import GHC.Generics (Generic) import Ide.Logger @@ -39,15 +33,10 @@ import Ide.Types import qualified Language.LSP.Protocol.Lens as L import Language.LSP.Protocol.Message import Language.LSP.Protocol.Types -import Language.LSP.Server (LspT, - ProgressCancellable (Cancellable), - getClientCapabilities, - sendRequest, - withIndefiniteProgress) data Log = DoesNotSupportResolve T.Text - | ApplyWorkspaceEditFailed ResponseError + | forall m . A.ToJSON (ErrorData m) => ApplyWorkspaceEditFailed (TResponseError m) instance Pretty Log where pretty = \case DoesNotSupportResolve fallback-> @@ -69,7 +58,7 @@ mkCodeActionHandlerWithResolve mkCodeActionHandlerWithResolve recorder codeActionMethod codeResolveMethod = let newCodeActionMethod ideState pid params = do codeActionReturn <- codeActionMethod ideState pid params - caps <- lift getClientCapabilities + caps <- lift pluginGetClientCapabilities case codeActionReturn of r@(InR Null) -> pure r (InL ls) | -- We don't need to do anything if the client supports @@ -83,7 +72,7 @@ mkCodeActionHandlerWithResolve recorder codeActionMethod codeResolveMethod = <> mkResolveHandler SMethod_CodeActionResolve codeResolveMethod) where dropData :: CodeAction -> CodeAction dropData ca = ca & L.data_ .~ Nothing - resolveCodeAction :: Uri -> ideState -> PluginId -> (Command |? CodeAction) -> ExceptT PluginError (LspT Config IO) (Command |? CodeAction) + resolveCodeAction :: Uri -> ideState -> PluginId -> (Command |? CodeAction) -> ExceptT PluginError (HandlerM Config) (Command |? CodeAction) resolveCodeAction _uri _ideState _plId c@(InL _) = pure c resolveCodeAction uri ideState pid (InR codeAction@CodeAction{_data_=Just value}) = do case A.fromJSON value of @@ -114,7 +103,7 @@ mkCodeActionWithResolveAndCommand mkCodeActionWithResolveAndCommand recorder plId codeActionMethod codeResolveMethod = let newCodeActionMethod ideState pid params = do codeActionReturn <- codeActionMethod ideState pid params - caps <- lift getClientCapabilities + caps <- lift pluginGetClientCapabilities case codeActionReturn of r@(InR Null) -> pure r (InL ls) | -- We don't need to do anything if the client supports @@ -144,25 +133,24 @@ mkCodeActionWithResolveAndCommand recorder plId codeActionMethod codeResolveMeth codeAction & L.data_ .~ (A.toJSON .WithURI uri <$> data_) where data_ = codeAction ^? L.data_ . _Just executeResolveCmd :: ResolveFunction ideState a 'Method_CodeActionResolve -> CommandFunction ideState CodeAction - executeResolveCmd resolveProvider ideState ca@CodeAction{_data_=Just value} = do - ExceptT $ withIndefiniteProgress "Applying edits for code action..." Cancellable $ runExceptT $ do - case A.fromJSON value of - A.Error err -> throwError $ parseError (Just value) (T.pack err) - A.Success (WithURI uri innerValue) -> do - case A.fromJSON innerValue of - A.Error err -> throwError $ parseError (Just value) (T.pack err) - A.Success innerValueDecoded -> do - resolveResult <- resolveProvider ideState plId ca uri innerValueDecoded - case resolveResult of - ca2@CodeAction {_edit = Just wedits } | diffCodeActions ca ca2 == ["edit"] -> do - _ <- ExceptT $ Right <$> sendRequest SMethod_WorkspaceApplyEdit (ApplyWorkspaceEditParams Nothing wedits) handleWEditCallback - pure $ InR Null - ca2@CodeAction {_edit = Just _ } -> - throwError $ internalError $ - "The resolve provider unexpectedly returned a code action with the following differing fields: " - <> (T.pack $ show $ diffCodeActions ca ca2) - _ -> throwError $ internalError "The resolve provider unexpectedly returned a result with no data field" - executeResolveCmd _ _ CodeAction{_data_= value} = throwError $ invalidParamsError ("The code action to resolve has an illegal data field: " <> (T.pack $ show value)) + executeResolveCmd resolveProvider ideState _token ca@CodeAction{_data_=Just value} = do + case A.fromJSON value of + A.Error err -> throwError $ parseError (Just value) (T.pack err) + A.Success (WithURI uri innerValue) -> do + case A.fromJSON innerValue of + A.Error err -> throwError $ parseError (Just value) (T.pack err) + A.Success innerValueDecoded -> do + resolveResult <- resolveProvider ideState plId ca uri innerValueDecoded + case resolveResult of + ca2@CodeAction {_edit = Just wedits } | diffCodeActions ca ca2 == ["edit"] -> do + _ <- ExceptT $ Right <$> pluginSendRequest SMethod_WorkspaceApplyEdit (ApplyWorkspaceEditParams Nothing wedits) handleWEditCallback + pure $ InR Null + ca2@CodeAction {_edit = Just _ } -> + throwError $ internalError $ + "The resolve provider unexpectedly returned a code action with the following differing fields: " + <> (T.pack $ show $ diffCodeActions ca ca2) + _ -> throwError $ internalError "The resolve provider unexpectedly returned a result with no data field" + executeResolveCmd _ _ _ CodeAction{_data_= value} = throwError $ invalidParamsError ("The code action to resolve has an illegal data field: " <> (T.pack $ show value)) handleWEditCallback (Left err ) = do logWith recorder Warning (ApplyWorkspaceEditFailed err) pure () @@ -198,7 +186,7 @@ supportsCodeActionResolve :: ClientCapabilities -> Bool supportsCodeActionResolve caps = caps ^? L.textDocument . _Just . L.codeAction . _Just . L.dataSupport . _Just == Just True && case caps ^? L.textDocument . _Just . L.codeAction . _Just . L.resolveSupport . _Just of - Just row -> "edit" `elem` row .! #properties + Just ClientCodeActionResolveOptions{_properties} -> "edit" `elem` _properties _ -> False internalError :: T.Text -> PluginError @@ -211,6 +199,7 @@ parseError :: Maybe A.Value -> T.Text -> PluginError parseError value errMsg = PluginInternalError ("Ide.Plugin.Resolve: Error parsing value:"<> (T.pack $ show value) <> " Error: "<> errMsg) {- Note [Code action resolve fallback to commands] + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ To make supporting code action resolve easy for plugins, we want to let them provide one implementation that can be used both when clients support resolve, and when they don't. diff --git a/hls-plugin-api/src/Ide/PluginUtils.hs b/hls-plugin-api/src/Ide/PluginUtils.hs index 817c96ed9c..e34d19f8b0 100644 --- a/hls-plugin-api/src/Ide/PluginUtils.hs +++ b/hls-plugin-api/src/Ide/PluginUtils.hs @@ -1,5 +1,3 @@ -{-# LANGUAGE FlexibleContexts #-} -{-# LANGUAGE LambdaCase #-} {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE TypeFamilies #-} @@ -20,7 +18,7 @@ module Ide.PluginUtils getClientConfig, getPluginConfig, configForPlugin, - pluginEnabled, + handlesRequest, extractTextInRange, fullRange, mkLspCommand, @@ -30,10 +28,13 @@ module Ide.PluginUtils allLspCmdIds', installSigUsr1Handler, subRange, + rangesOverlap, positionInRange, usePropertyLsp, -- * Escape unescape, + -- * toAbsolute + toAbsolute ) where @@ -52,6 +53,7 @@ import Ide.Types import qualified Language.LSP.Protocol.Lens as L import Language.LSP.Protocol.Types import Language.LSP.Server +import System.FilePath (()) import qualified Text.Megaparsec as P import qualified Text.Megaparsec.Char as P import qualified Text.Megaparsec.Char.Lexer as P @@ -276,6 +278,21 @@ fullRange s = Range startPos endPos subRange :: Range -> Range -> Bool subRange = isSubrangeOf + +-- | Check whether the two 'Range's overlap in any way. +-- +-- >>> rangesOverlap (mkRange 1 0 1 4) (mkRange 1 2 1 5) +-- True +-- >>> rangesOverlap (mkRange 1 2 1 5) (mkRange 1 0 1 4) +-- True +-- >>> rangesOverlap (mkRange 1 0 1 6) (mkRange 1 2 1 4) +-- True +-- >>> rangesOverlap (mkRange 1 2 1 4) (mkRange 1 0 1 6) +-- True +rangesOverlap :: Range -> Range -> Bool +rangesOverlap r1 r2 = + r1 ^. L.start <= r2 ^. L.end && r2 ^. L.start <= r1 ^. L.end + -- --------------------------------------------------------------------- allLspCmdIds' :: T.Text -> IdePlugins ideState -> [T.Text] @@ -318,3 +335,12 @@ escapedTextParser = concat <$> P.many (outsideStringLiteral P.<|> stringLiteral) inside' = concatMap f inside pure $ "\"" <> inside' <> "\"" + +-- --------------------------------------------------------------------- + +-- | toAbsolute +-- use `toAbsolute` to state our intention that we are actually make a path absolute +-- the first argument should be the root directory +-- the second argument should be the relative path +toAbsolute :: FilePath -> FilePath -> FilePath +toAbsolute = () diff --git a/hls-plugin-api/src/Ide/Types.hs b/hls-plugin-api/src/Ide/Types.hs index ab9f30f611..3a06656a77 100644 --- a/hls-plugin-api/src/Ide/Types.hs +++ b/hls-plugin-api/src/Ide/Types.hs @@ -1,44 +1,37 @@ -{-# LANGUAGE BangPatterns #-} -{-# LANGUAGE BlockArguments #-} -{-# LANGUAGE CPP #-} -{-# LANGUAGE ConstraintKinds #-} -{-# LANGUAGE DefaultSignatures #-} -{-# LANGUAGE DeriveAnyClass #-} -{-# LANGUAGE DeriveGeneric #-} -{-# LANGUAGE DerivingStrategies #-} -{-# LANGUAGE DuplicateRecordFields #-} -{-# LANGUAGE FlexibleContexts #-} -{-# LANGUAGE FlexibleInstances #-} -{-# LANGUAGE GADTs #-} -{-# LANGUAGE GeneralizedNewtypeDeriving #-} -{-# LANGUAGE MonadComprehensions #-} -{-# LANGUAGE MultiParamTypeClasses #-} -{-# LANGUAGE NamedFieldPuns #-} -{-# LANGUAGE OverloadedStrings #-} -{-# LANGUAGE PatternSynonyms #-} -{-# LANGUAGE PolyKinds #-} -{-# LANGUAGE RecordWildCards #-} -{-# LANGUAGE ScopedTypeVariables #-} -{-# LANGUAGE TypeFamilies #-} -{-# LANGUAGE UndecidableInstances #-} -{-# LANGUAGE ViewPatterns #-} +{-# LANGUAGE BlockArguments #-} +{-# LANGUAGE CPP #-} +{-# LANGUAGE CUSKs #-} +{-# LANGUAGE DefaultSignatures #-} +{-# LANGUAGE DeriveAnyClass #-} +{-# LANGUAGE DerivingStrategies #-} +{-# LANGUAGE DuplicateRecordFields #-} +{-# LANGUAGE GADTs #-} +{-# LANGUAGE MonadComprehensions #-} +{-# LANGUAGE OverloadedStrings #-} +{-# LANGUAGE PatternSynonyms #-} +{-# LANGUAGE RecordWildCards #-} +{-# LANGUAGE TypeFamilies #-} +{-# LANGUAGE UndecidableInstances #-} +{-# LANGUAGE ViewPatterns #-} module Ide.Types ( PluginDescriptor(..), defaultPluginDescriptor, defaultCabalPluginDescriptor , defaultPluginPriority +, describePlugin , IdeCommand(..) , IdeMethod(..) , IdeNotification(..) , IdePlugins(IdePlugins, ipMap) , DynFlagsModifications(..) -, Config(..), PluginConfig(..), CheckParents(..) -, ConfigDescriptor(..), defaultConfigDescriptor, configForPlugin, pluginEnabledConfig +, Config(..), PluginConfig(..), CheckParents(..), SessionLoadingPreferenceConfig(..) +, ConfigDescriptor(..), defaultConfigDescriptor, configForPlugin , CustomConfig(..), mkCustomConfig , FallbackCodeActionParams(..) -, FormattingType(..), FormattingMethod, FormattingHandler, mkFormattingHandlers +, FormattingType(..), FormattingMethod, FormattingHandler , HasTracing(..) , PluginCommand(..), CommandId(..), CommandFunction, mkLspCommand, mkLspCmdId , PluginId(..) , PluginHandler(..), mkPluginHandler +, HandlerM, runHandlerM, pluginGetClientCapabilities, pluginSendNotification, pluginSendRequest, pluginWithIndefiniteProgress , PluginHandlers(..) , PluginMethod(..) , PluginMethodHandler @@ -66,17 +59,18 @@ import System.Posix.Signals import Control.Applicative ((<|>)) import Control.Arrow ((&&&)) -import Control.Lens (_Just, (.~), (?~), (^.), (^?)) +import Control.Lens (_Just, view, (.~), (?~), (^.), + (^?)) import Control.Monad (void) import Control.Monad.Error.Class (MonadError (throwError)) -import Control.Monad.Trans.Class (MonadTrans (lift)) +import Control.Monad.IO.Class (MonadIO) import Control.Monad.Trans.Except (ExceptT, runExceptT) import Data.Aeson hiding (Null, defaultOptions) +import qualified Data.Aeson.Types as A import Data.Default import Data.Dependent.Map (DMap) import qualified Data.Dependent.Map as DMap import qualified Data.DList as DList -import Data.Foldable (foldl') import Data.GADT.Compare import Data.Hashable (Hashable) import Data.HashMap.Strict (HashMap) @@ -95,18 +89,26 @@ import Development.IDE.Graph import GHC (DynFlags) import GHC.Generics import Ide.Plugin.Error +import Ide.Plugin.HandleRequestTypes import Ide.Plugin.Properties import qualified Language.LSP.Protocol.Lens as L import Language.LSP.Protocol.Message import Language.LSP.Protocol.Types -import Language.LSP.Server (LspM, LspT, getVirtualFile) +import Language.LSP.Server import Language.LSP.VFS import Numeric.Natural import OpenTelemetry.Eventlog import Options.Applicative (ParserInfo) +import Prettyprinter as PP import System.FilePath import System.IO.Unsafe import Text.Regex.TDFA.Text () +import UnliftIO (MonadUnliftIO) + +#if !MIN_VERSION_base(4,20,0) +import Data.Foldable (foldl') +#endif + -- --------------------------------------------------------------------- data IdePlugins ideState = IdePlugins_ @@ -175,6 +177,7 @@ data Config = , formattingProvider :: !T.Text , cabalFormattingProvider :: !T.Text , maxCompletions :: !Int + , sessionLoading :: !SessionLoadingPreferenceConfig , plugins :: !(Map.Map PluginId PluginConfig) } deriving (Show,Eq) @@ -183,7 +186,9 @@ instance ToJSON Config where object [ "checkParents" .= checkParents , "checkProject" .= checkProject , "formattingProvider" .= formattingProvider + , "cabalFormattingProvider" .= cabalFormattingProvider , "maxCompletions" .= maxCompletions + , "sessionLoading" .= sessionLoading , "plugin" .= Map.mapKeysMonotonic (\(PluginId p) -> p) plugins ] @@ -194,9 +199,11 @@ instance Default Config where , formattingProvider = "ormolu" -- , formattingProvider = "floskell" -- , formattingProvider = "stylish-haskell" - , cabalFormattingProvider = "cabal-fmt" + , cabalFormattingProvider = "cabal-gild" + -- , cabalFormattingProvider = "cabal-fmt" -- this string value needs to kept in sync with the value provided in HlsPlugins , maxCompletions = 40 + , sessionLoading = PreferSingleComponentLoading , plugins = mempty } @@ -209,6 +216,39 @@ data CheckParents deriving stock (Eq, Ord, Show, Generic) deriving anyclass (FromJSON, ToJSON) + +data SessionLoadingPreferenceConfig + = PreferSingleComponentLoading + -- ^ Always load only a singleComponent when a new component + -- is discovered. + | PreferMultiComponentLoading + -- ^ Always prefer loading multiple components in the cradle + -- at once. This might not be always possible, if the tool doesn't + -- support multiple components loading. + -- + -- The cradle can decide how to handle these situations, and whether + -- to honour the preference at all. + deriving stock (Eq, Ord, Show, Generic) + +instance Pretty SessionLoadingPreferenceConfig where + pretty PreferSingleComponentLoading = "Prefer Single Component Loading" + pretty PreferMultiComponentLoading = "Prefer Multiple Components Loading" + +instance ToJSON SessionLoadingPreferenceConfig where + toJSON PreferSingleComponentLoading = + String "singleComponent" + toJSON PreferMultiComponentLoading = + String "multipleComponents" + +instance FromJSON SessionLoadingPreferenceConfig where + parseJSON (String val) = case val of + "singleComponent" -> pure PreferSingleComponentLoading + "multipleComponents" -> pure PreferMultiComponentLoading + _ -> A.prependFailure "parsing SessionLoadingPreferenceConfig failed, " + (A.parseFail $ "Expected one of \"singleComponent\" or \"multipleComponents\" but got " <> T.unpack val ) + parseJSON o = A.prependFailure "parsing SessionLoadingPreferenceConfig failed, " + (A.typeMismatch "String" o) + -- | A PluginConfig is a generic configuration for a given HLS plugin. It -- provides a "big switch" to turn it on or off as a whole, as well as small -- switches per feature, and a slot for custom config. @@ -219,6 +259,7 @@ data PluginConfig = , plcCallHierarchyOn :: !Bool , plcCodeActionsOn :: !Bool , plcCodeLensOn :: !Bool + , plcInlayHintsOn :: !Bool , plcDiagnosticsOn :: !Bool , plcHoverOn :: !Bool , plcSymbolsOn :: !Bool @@ -226,6 +267,7 @@ data PluginConfig = , plcRenameOn :: !Bool , plcSelectionRangeOn :: !Bool , plcFoldingRangeOn :: !Bool + , plcSemanticTokensOn :: !Bool , plcConfig :: !Object } deriving (Show,Eq) @@ -235,23 +277,26 @@ instance Default PluginConfig where , plcCallHierarchyOn = True , plcCodeActionsOn = True , plcCodeLensOn = True + , plcInlayHintsOn = True , plcDiagnosticsOn = True , plcHoverOn = True , plcSymbolsOn = True , plcCompletionOn = True , plcRenameOn = True , plcSelectionRangeOn = True - , plcFoldingRangeOn = True + , plcFoldingRangeOn = True + , plcSemanticTokensOn = True , plcConfig = mempty } instance ToJSON PluginConfig where - toJSON (PluginConfig g ch ca cl d h s c rn sr fr cfg) = r + toJSON (PluginConfig g ch ca ih cl d h s c rn sr fr st cfg) = r where r = object [ "globalOn" .= g , "callHierarchyOn" .= ch , "codeActionsOn" .= ca , "codeLensOn" .= cl + , "inlayHintsOn" .= ih , "diagnosticsOn" .= d , "hoverOn" .= h , "symbolsOn" .= s @@ -259,6 +304,7 @@ instance ToJSON PluginConfig where , "renameOn" .= rn , "selectionRangeOn" .= sr , "foldingRangeOn" .= fr + , "semanticTokensOn" .= st , "config" .= cfg ] @@ -266,6 +312,7 @@ instance ToJSON PluginConfig where data PluginDescriptor (ideState :: Type) = PluginDescriptor { pluginId :: !PluginId + , pluginDescription :: !T.Text -- ^ Unique identifier of the plugin. , pluginPriority :: Natural -- ^ Plugin handlers are called in priority order, higher priority first @@ -283,16 +330,13 @@ data PluginDescriptor (ideState :: Type) = -- The file extension must have a leading '.'. } --- | Check whether the given plugin descriptor is responsible for the file with the given path. --- Compares the file extension of the file at the given path with the file extension --- the plugin is responsible for. -pluginResponsible :: Uri -> PluginDescriptor c -> Bool -pluginResponsible uri pluginDesc - | Just fp <- mfp - , T.pack (takeExtension fp) `elem` pluginFileType pluginDesc = True - | otherwise = False - where - mfp = uriToFilePath uri +describePlugin :: PluginDescriptor c -> Doc ann +describePlugin p = + let + PluginId pid = pluginId p + pdesc = pluginDescription p + in pretty pid <> ":" <> nest 4 (PP.line <> pretty pdesc) + -- | An existential wrapper of 'Properties' data CustomConfig = forall r. CustomConfig (Properties r) @@ -334,26 +378,72 @@ defaultConfigDescriptor :: ConfigDescriptor defaultConfigDescriptor = ConfigDescriptor Data.Default.def False (mkCustomConfig emptyProperties) +-- | Lookup the current config for a plugin +configForPlugin :: Config -> PluginDescriptor c -> PluginConfig +configForPlugin config PluginDescriptor{..} + = Map.findWithDefault (configInitialGenericConfig pluginConfigDescriptor) pluginId (plugins config) + +-- | Checks that a specific plugin is globally enabled in order to respond to +-- requests +pluginEnabledGlobally :: PluginDescriptor c -> Config -> HandleRequestResult +pluginEnabledGlobally desc conf = if plcGlobalOn (configForPlugin conf desc) + then HandlesRequest + else DoesNotHandleRequest DisabledGlobally + +-- | Checks that a specific feature for a given plugin is enabled in order +-- to respond to requests +pluginFeatureEnabled :: (PluginConfig -> Bool) -> PluginDescriptor c -> Config -> HandleRequestResult +pluginFeatureEnabled f desc conf = if f (configForPlugin conf desc) + then HandlesRequest + else DoesNotHandleRequest FeatureDisabled + +-- |Determine whether this request should be routed to the plugin. Fails closed +-- if we can't determine which plugin it should be routed to. +pluginResolverResponsible :: L.HasData_ m (Maybe Value) => m -> PluginDescriptor c -> HandleRequestResult +pluginResolverResponsible + (view L.data_ -> (Just (fromJSON -> (Success (PluginResolveData o@(PluginId ot) _ _))))) + pluginDesc = + if pluginId pluginDesc == o + then HandlesRequest + else DoesNotHandleRequest $ NotResolveOwner ot +-- If we can't determine who this request belongs to, then we don't want any plugin +-- to handle it. +pluginResolverResponsible _ _ = DoesNotHandleRequest $ NotResolveOwner "(unable to determine resolve owner)" + +-- | Check whether the given plugin descriptor supports the file with +-- the given path. Compares the file extension from the msgParams with the +-- file extension the plugin is responsible for. +-- We are passing the msgParams here even though we only need the URI URI here. +-- If in the future we need to be able to provide only an URI it can be +-- separated again. +pluginSupportsFileType :: (L.HasTextDocument m doc, L.HasUri doc Uri) => m -> PluginDescriptor c -> HandleRequestResult +pluginSupportsFileType msgParams pluginDesc = + case mfp of + Just fp | T.pack (takeExtension fp) `elem` pluginFileType pluginDesc -> HandlesRequest + _ -> DoesNotHandleRequest $ DoesNotSupportFileType (maybe "(unable to determine file type)" (T.pack . takeExtension) mfp) + where + mfp = uriToFilePath uri + uri = msgParams ^. L.textDocument . L.uri + -- | Methods that can be handled by plugins. -- 'ExtraParams' captures any extra data the IDE passes to the handlers for this method -- Only methods for which we know how to combine responses can be instances of 'PluginMethod' class HasTracing (MessageParams m) => PluginMethod (k :: MessageKind) (m :: Method ClientToServer k) where - -- | Parse the configuration to check if this plugin is enabled. - -- Perform sanity checks on the message to see whether the plugin is enabled - -- for this message in particular. - -- If a plugin is not enabled, its handlers, commands, etc. will not be - -- run for the given message. + -- | Parse the configuration to check if this plugin is globally enabled, and + -- if the feature which handles this method is enabled. Perform sanity checks + -- on the message to see whether the plugin handles this message in particular. + -- This class is only used to determine whether a plugin can handle a specific + -- request. Commands and rules do not use this logic to determine whether or + -- not they are run. -- - -- Semantically, this method describes whether a plugin is enabled configuration wise - -- and is allowed to respond to the message. This might depend on the URI that is - -- associated to the Message Parameters. There are requests - -- with no associated URI that, consequentially, cannot inspect the URI. -- - -- A common reason why a plugin might not be allowed to respond although it is enabled: + -- A common reason why a plugin won't handle a request even though it is enabled: -- * The plugin cannot handle requests associated with the specific URI -- * Since the implementation of [cabal plugins](https://github.com/haskell/haskell-language-server/issues/2940) -- HLS knows plugins specific to Haskell and specific to [Cabal file descriptions](https://cabal.readthedocs.io/en/3.6/cabal-package.html) + -- * The resolve request is not routed to that specific plugin. Each resolve + -- request needs to be routed to only one plugin. -- -- Strictly speaking, we are conflating two concepts here: -- * Dynamically enabled (e.g. on a per-message basis) @@ -361,7 +451,7 @@ class HasTracing (MessageParams m) => PluginMethod (k :: MessageKind) (m :: Meth -- * Strictly speaking, this might also change dynamically -- -- But there is no use to split it up into two different methods for now. - pluginEnabled + handlesRequest :: SMethod m -- ^ Method type. -> MessageParams m @@ -373,173 +463,203 @@ class HasTracing (MessageParams m) => PluginMethod (k :: MessageKind) (m :: Meth -> Config -- ^ Generic config description, expected to contain 'PluginConfig' configuration -- for this plugin - -> Bool + -> HandleRequestResult -- ^ Is this plugin enabled and allowed to respond to the given request -- with the given parameters? - default pluginEnabled :: (L.HasTextDocument (MessageParams m) doc, L.HasUri doc Uri) - => SMethod m -> MessageParams m -> PluginDescriptor c -> Config -> Bool - pluginEnabled _ params desc conf = pluginResponsible uri desc && plcGlobalOn (configForPlugin conf desc) - where - uri = params ^. L.textDocument . L.uri - --- --------------------------------------------------------------------- --- Plugin Requests --- --------------------------------------------------------------------- - -class PluginMethod Request m => PluginRequestMethod (m :: Method ClientToServer Request) where - -- | How to combine responses from different plugins. - -- - -- For example, for Hover requests, we might have multiple producers of - -- Hover information. We do not want to decide which one to display to the user - -- but instead allow to define how to merge two hover request responses into one - -- glorious hover box. - -- - -- However, as sometimes only one handler of a request can realistically exist - -- (such as TextDocumentFormatting), it is safe to just unconditionally report - -- back one arbitrary result (arbitrary since it should only be one anyway). - combineResponses - :: SMethod m - -> Config -- ^ IDE Configuration - -> ClientCapabilities - -> MessageParams m - -> NonEmpty (MessageResult m) -> MessageResult m - - default combineResponses :: Semigroup (MessageResult m) - => SMethod m -> Config -> ClientCapabilities -> MessageParams m -> NonEmpty (MessageResult m) -> MessageResult m - combineResponses _method _config _caps _params = sconcat + default handlesRequest :: (L.HasTextDocument (MessageParams m) doc, L.HasUri doc Uri) + => SMethod m -> MessageParams m -> PluginDescriptor c -> Config -> HandleRequestResult + handlesRequest _ params desc conf = + pluginEnabledGlobally desc conf <> pluginSupportsFileType params desc + +-- | Check if a plugin is enabled, if one of it's specific config's is enabled, +-- and if it supports the file +pluginEnabledWithFeature :: (L.HasTextDocument (MessageParams m) doc, L.HasUri doc Uri) + => (PluginConfig -> Bool) -> SMethod m -> MessageParams m + -> PluginDescriptor c -> Config -> HandleRequestResult +pluginEnabledWithFeature feature _ msgParams pluginDesc config = + pluginEnabledGlobally pluginDesc config + <> pluginFeatureEnabled feature pluginDesc config + <> pluginSupportsFileType msgParams pluginDesc + +-- | Check if a plugin is enabled, if one of it's specific configs is enabled, +-- and if it's the plugin responsible for a resolve request. +pluginEnabledResolve :: L.HasData_ s (Maybe Value) => (PluginConfig -> Bool) -> p -> s -> PluginDescriptor c -> Config -> HandleRequestResult +pluginEnabledResolve feature _ msgParams pluginDesc config = + pluginEnabledGlobally pluginDesc config + <> pluginFeatureEnabled feature pluginDesc config + <> pluginResolverResponsible msgParams pluginDesc instance PluginMethod Request Method_TextDocumentCodeAction where - pluginEnabled _ msgParams pluginDesc config = - pluginResponsible uri pluginDesc && pluginEnabledConfig plcCodeActionsOn (configForPlugin config pluginDesc) - where - uri = msgParams ^. L.textDocument . L.uri + handlesRequest = pluginEnabledWithFeature plcCodeActionsOn instance PluginMethod Request Method_CodeActionResolve where -- See Note [Resolve in PluginHandlers] - pluginEnabled _ msgParams pluginDesc config = - pluginResolverResponsible (msgParams ^. L.data_) pluginDesc - && pluginEnabledConfig plcCodeActionsOn (configForPlugin config pluginDesc) + handlesRequest = pluginEnabledResolve plcCodeActionsOn instance PluginMethod Request Method_TextDocumentDefinition where - pluginEnabled _ msgParams pluginDesc _ = - pluginResponsible uri pluginDesc - where - uri = msgParams ^. L.textDocument . L.uri + handlesRequest _ msgParams pluginDesc _ = pluginSupportsFileType msgParams pluginDesc instance PluginMethod Request Method_TextDocumentTypeDefinition where - pluginEnabled _ msgParams pluginDesc _ = - pluginResponsible uri pluginDesc - where - uri = msgParams ^. L.textDocument . L.uri + handlesRequest _ msgParams pluginDesc _ = pluginSupportsFileType msgParams pluginDesc + +instance PluginMethod Request Method_TextDocumentImplementation where + handlesRequest _ msgParams pluginDesc _ = pluginSupportsFileType msgParams pluginDesc instance PluginMethod Request Method_TextDocumentDocumentHighlight where - pluginEnabled _ msgParams pluginDesc _ = - pluginResponsible uri pluginDesc - where - uri = msgParams ^. L.textDocument . L.uri + handlesRequest _ msgParams pluginDesc _ = pluginSupportsFileType msgParams pluginDesc instance PluginMethod Request Method_TextDocumentReferences where - pluginEnabled _ msgParams pluginDesc _ = - pluginResponsible uri pluginDesc - where - uri = msgParams ^. L.textDocument . L.uri + handlesRequest _ msgParams pluginDesc _ = pluginSupportsFileType msgParams pluginDesc instance PluginMethod Request Method_WorkspaceSymbol where -- Unconditionally enabled, but should it really be? - pluginEnabled _ _ _ _ = True + handlesRequest _ _ _ _ = HandlesRequest + +instance PluginMethod Request Method_TextDocumentInlayHint where + handlesRequest = pluginEnabledWithFeature plcInlayHintsOn + +instance PluginMethod Request Method_InlayHintResolve where + handlesRequest = pluginEnabledResolve plcInlayHintsOn instance PluginMethod Request Method_TextDocumentCodeLens where - pluginEnabled _ msgParams pluginDesc config = pluginResponsible uri pluginDesc - && pluginEnabledConfig plcCodeLensOn (configForPlugin config pluginDesc) - where - uri = msgParams ^. L.textDocument . L.uri + handlesRequest = pluginEnabledWithFeature plcCodeLensOn instance PluginMethod Request Method_CodeLensResolve where -- See Note [Resolve in PluginHandlers] - pluginEnabled _ msgParams pluginDesc config = - pluginResolverResponsible (msgParams ^. L.data_) pluginDesc - && pluginEnabledConfig plcCodeActionsOn (configForPlugin config pluginDesc) + handlesRequest = pluginEnabledResolve plcCodeLensOn instance PluginMethod Request Method_TextDocumentRename where - pluginEnabled _ msgParams pluginDesc config = pluginResponsible uri pluginDesc - && pluginEnabledConfig plcRenameOn (configForPlugin config pluginDesc) - where - uri = msgParams ^. L.textDocument . L.uri + handlesRequest = pluginEnabledWithFeature plcRenameOn + +instance PluginMethod Request Method_TextDocumentPrepareRename where + handlesRequest = pluginEnabledWithFeature plcRenameOn + instance PluginMethod Request Method_TextDocumentHover where - pluginEnabled _ msgParams pluginDesc config = pluginResponsible uri pluginDesc - && pluginEnabledConfig plcHoverOn (configForPlugin config pluginDesc) - where - uri = msgParams ^. L.textDocument . L.uri + handlesRequest = pluginEnabledWithFeature plcHoverOn instance PluginMethod Request Method_TextDocumentDocumentSymbol where - pluginEnabled _ msgParams pluginDesc config = pluginResponsible uri pluginDesc - && pluginEnabledConfig plcSymbolsOn (configForPlugin config pluginDesc) - where - uri = msgParams ^. L.textDocument . L.uri + handlesRequest = pluginEnabledWithFeature plcSymbolsOn instance PluginMethod Request Method_CompletionItemResolve where -- See Note [Resolve in PluginHandlers] - pluginEnabled _ msgParams pluginDesc config = pluginResolverResponsible (msgParams ^. L.data_) pluginDesc - && pluginEnabledConfig plcCompletionOn (configForPlugin config pluginDesc) + handlesRequest = pluginEnabledResolve plcCompletionOn instance PluginMethod Request Method_TextDocumentCompletion where - pluginEnabled _ msgParams pluginDesc config = pluginResponsible uri pluginDesc - && pluginEnabledConfig plcCompletionOn (configForPlugin config pluginDesc) - where - uri = msgParams ^. L.textDocument . L.uri + handlesRequest = pluginEnabledWithFeature plcCompletionOn instance PluginMethod Request Method_TextDocumentFormatting where - pluginEnabled SMethod_TextDocumentFormatting msgParams pluginDesc conf = - pluginResponsible uri pluginDesc - && (PluginId (formattingProvider conf) == pid || PluginId (cabalFormattingProvider conf) == pid) + handlesRequest _ msgParams pluginDesc conf = + (if PluginId (formattingProvider conf) == pid + || PluginId (cabalFormattingProvider conf) == pid + then HandlesRequest + else DoesNotHandleRequest (NotFormattingProvider (formattingProvider conf)) ) + <> pluginSupportsFileType msgParams pluginDesc where - uri = msgParams ^. L.textDocument . L.uri pid = pluginId pluginDesc instance PluginMethod Request Method_TextDocumentRangeFormatting where - pluginEnabled _ msgParams pluginDesc conf = pluginResponsible uri pluginDesc - && (PluginId (formattingProvider conf) == pid || PluginId (cabalFormattingProvider conf) == pid) + handlesRequest _ msgParams pluginDesc conf = + (if PluginId (formattingProvider conf) == pid + || PluginId (cabalFormattingProvider conf) == pid + then HandlesRequest + else DoesNotHandleRequest (NotFormattingProvider (formattingProvider conf))) + <> pluginSupportsFileType msgParams pluginDesc where - uri = msgParams ^. L.textDocument . L.uri pid = pluginId pluginDesc +instance PluginMethod Request Method_TextDocumentSemanticTokensFull where + handlesRequest = pluginEnabledWithFeature plcSemanticTokensOn + +instance PluginMethod Request Method_TextDocumentSemanticTokensFullDelta where + handlesRequest = pluginEnabledWithFeature plcSemanticTokensOn + instance PluginMethod Request Method_TextDocumentPrepareCallHierarchy where - pluginEnabled _ msgParams pluginDesc conf = pluginResponsible uri pluginDesc - && pluginEnabledConfig plcCallHierarchyOn (configForPlugin conf pluginDesc) - where - uri = msgParams ^. L.textDocument . L.uri + handlesRequest = pluginEnabledWithFeature plcCallHierarchyOn instance PluginMethod Request Method_TextDocumentSelectionRange where - pluginEnabled _ msgParams pluginDesc conf = pluginResponsible uri pluginDesc - && pluginEnabledConfig plcSelectionRangeOn (configForPlugin conf pluginDesc) - where - uri = msgParams ^. L.textDocument . L.uri + handlesRequest = pluginEnabledWithFeature plcSelectionRangeOn instance PluginMethod Request Method_TextDocumentFoldingRange where - pluginEnabled _ msgParams pluginDesc conf = pluginResponsible uri pluginDesc - && pluginEnabledConfig plcFoldingRangeOn (configForPlugin conf pluginDesc) - where - uri = msgParams ^. L.textDocument . L.uri + handlesRequest = pluginEnabledWithFeature plcFoldingRangeOn instance PluginMethod Request Method_CallHierarchyIncomingCalls where -- This method has no URI parameter, thus no call to 'pluginResponsible' - pluginEnabled _ _ pluginDesc conf = pluginEnabledConfig plcCallHierarchyOn (configForPlugin conf pluginDesc) + handlesRequest _ _ pluginDesc conf = + pluginEnabledGlobally pluginDesc conf + <> pluginFeatureEnabled plcCallHierarchyOn pluginDesc conf instance PluginMethod Request Method_CallHierarchyOutgoingCalls where -- This method has no URI parameter, thus no call to 'pluginResponsible' - pluginEnabled _ _ pluginDesc conf = pluginEnabledConfig plcCallHierarchyOn (configForPlugin conf pluginDesc) + handlesRequest _ _ pluginDesc conf = + pluginEnabledGlobally pluginDesc conf + <> pluginFeatureEnabled plcCallHierarchyOn pluginDesc conf instance PluginMethod Request Method_WorkspaceExecuteCommand where - pluginEnabled _ _ _ _= True + handlesRequest _ _ _ _= HandlesRequest instance PluginMethod Request (Method_CustomMethod m) where - pluginEnabled _ _ _ _ = True + handlesRequest _ _ _ _ = HandlesRequest + +-- Plugin Notifications + +instance PluginMethod Notification Method_TextDocumentDidOpen where + +instance PluginMethod Notification Method_TextDocumentDidChange where + +instance PluginMethod Notification Method_TextDocumentDidSave where + +instance PluginMethod Notification Method_TextDocumentDidClose where + +instance PluginMethod Notification Method_WorkspaceDidChangeWatchedFiles where + -- This method has no URI parameter, thus no call to 'pluginResponsible'. + handlesRequest _ _ desc conf = pluginEnabledGlobally desc conf + +instance PluginMethod Notification Method_WorkspaceDidChangeWorkspaceFolders where + -- This method has no URI parameter, thus no call to 'pluginResponsible'. + handlesRequest _ _ desc conf = pluginEnabledGlobally desc conf + +instance PluginMethod Notification Method_WorkspaceDidChangeConfiguration where + -- This method has no URI parameter, thus no call to 'pluginResponsible'. + handlesRequest _ _ desc conf = pluginEnabledGlobally desc conf + +instance PluginMethod Notification Method_Initialized where + -- This method has no URI parameter, thus no call to 'pluginResponsible'. + handlesRequest _ _ desc conf = pluginEnabledGlobally desc conf + + +-- --------------------------------------------------------------------- +-- Plugin Requests +-- --------------------------------------------------------------------- + +class PluginMethod Request m => PluginRequestMethod (m :: Method ClientToServer Request) where + -- | How to combine responses from different plugins. + -- + -- For example, for Hover requests, we might have multiple producers of + -- Hover information. We do not want to decide which one to display to the user + -- but instead allow to define how to merge two hover request responses into one + -- glorious hover box. + -- + -- However, as sometimes only one handler of a request can realistically exist + -- (such as TextDocumentFormatting), it is safe to just unconditionally report + -- back one arbitrary result (arbitrary since it should only be one anyway). + combineResponses + :: SMethod m + -> Config -- ^ IDE Configuration + -> ClientCapabilities + -> MessageParams m + -> NonEmpty (MessageResult m) -> MessageResult m + + default combineResponses :: Semigroup (MessageResult m) + => SMethod m -> Config -> ClientCapabilities -> MessageParams m -> NonEmpty (MessageResult m) -> MessageResult m + combineResponses _method _config _caps _params = sconcat + + --- instance PluginRequestMethod Method_TextDocumentCodeAction where combineResponses _method _config (ClientCapabilities _ textDocCaps _ _ _ _) (CodeActionParams _ _ _ _ context) resps = - InL $ fmap compat $ filter wasRequested $ concat $ mapMaybe nullToMaybe $ toList resps + InL $ fmap compat $ concatMap (filter wasRequested) $ mapMaybe nullToMaybe $ toList resps where compat :: (Command |? CodeAction) -> (Command |? CodeAction) compat x@(InL _) = x @@ -579,6 +699,11 @@ instance PluginRequestMethod Method_TextDocumentTypeDefinition where | Just (Just True) <- caps ^? (L.textDocument . _Just . L.typeDefinition . _Just . L.linkSupport) = foldl' mergeDefinitions x xs | otherwise = downgradeLinks $ foldl' mergeDefinitions x xs +instance PluginRequestMethod Method_TextDocumentImplementation where + combineResponses _ _ caps _ (x :| xs) + | Just (Just True) <- caps ^? (L.textDocument . _Just . L.implementation . _Just . L.linkSupport) = foldl' mergeDefinitions x xs + | otherwise = downgradeLinks $ foldl' mergeDefinitions x xs + instance PluginRequestMethod Method_TextDocumentDocumentHighlight where instance PluginRequestMethod Method_TextDocumentReferences where @@ -597,8 +722,12 @@ instance PluginRequestMethod Method_CodeLensResolve where instance PluginRequestMethod Method_TextDocumentRename where +instance PluginRequestMethod Method_TextDocumentPrepareRename where + -- TODO more intelligent combining? + combineResponses _ _ _ _ (x :| _) = x + instance PluginRequestMethod Method_TextDocumentHover where - combineResponses _ _ _ _ (mapMaybe nullToMaybe . toList -> hs :: [Hover]) = + combineResponses _ _ _ _ (mapMaybe nullToMaybe . toList -> (hs :: [Hover])) = if null hs then InR Null else InL $ Hover (InL mcontent) r @@ -691,6 +820,15 @@ instance PluginRequestMethod Method_CallHierarchyOutgoingCalls where instance PluginRequestMethod (Method_CustomMethod m) where combineResponses _ _ _ _ (x :| _) = x +instance PluginRequestMethod Method_TextDocumentSemanticTokensFull where + combineResponses _ _ _ _ (x :| _) = x + +instance PluginRequestMethod Method_TextDocumentSemanticTokensFullDelta where + combineResponses _ _ _ _ (x :| _) = x + +instance PluginRequestMethod Method_TextDocumentInlayHint where + combineResponses _ _ _ _ x = sconcat x + takeLefts :: [a |? b] -> [a] takeLefts = mapMaybe (\x -> [res | (InL res) <- Just x]) @@ -746,31 +884,6 @@ downgradeLinks defs = defs class PluginMethod Notification m => PluginNotificationMethod (m :: Method ClientToServer Notification) where -instance PluginMethod Notification Method_TextDocumentDidOpen where - -instance PluginMethod Notification Method_TextDocumentDidChange where - -instance PluginMethod Notification Method_TextDocumentDidSave where - -instance PluginMethod Notification Method_TextDocumentDidClose where - -instance PluginMethod Notification Method_WorkspaceDidChangeWatchedFiles where - -- This method has no URI parameter, thus no call to 'pluginResponsible'. - pluginEnabled _ _ desc conf = plcGlobalOn $ configForPlugin conf desc - -instance PluginMethod Notification Method_WorkspaceDidChangeWorkspaceFolders where - -- This method has no URI parameter, thus no call to 'pluginResponsible'. - pluginEnabled _ _ desc conf = plcGlobalOn $ configForPlugin conf desc - -instance PluginMethod Notification Method_WorkspaceDidChangeConfiguration where - -- This method has no URI parameter, thus no call to 'pluginResponsible'. - pluginEnabled _ _ desc conf = plcGlobalOn $ configForPlugin conf desc - -instance PluginMethod Notification Method_Initialized where - -- This method has no URI parameter, thus no call to 'pluginResponsible'. - pluginEnabled _ _ desc conf = plcGlobalOn $ configForPlugin conf desc - - instance PluginNotificationMethod Method_TextDocumentDidOpen where instance PluginNotificationMethod Method_TextDocumentDidChange where @@ -803,9 +916,43 @@ instance GEq IdeNotification where instance GCompare IdeNotification where gcompare (IdeNotification a) (IdeNotification b) = gcompare a b +-- | Restricted version of 'LspM' specific to plugins. +-- +-- We use this monad for running plugins instead of 'LspM', since there are +-- parts of the LSP server state which plugins should not access directly, but +-- instead only via the build system. +newtype HandlerM config a = HandlerM { _runHandlerM :: LspM config a } + deriving newtype (Applicative, Functor, Monad, MonadIO, MonadUnliftIO) + +runHandlerM :: HandlerM config a -> LspM config a +runHandlerM = _runHandlerM + +-- | Wrapper of 'getClientCapabilities' for HandlerM +pluginGetClientCapabilities :: HandlerM config ClientCapabilities +pluginGetClientCapabilities = HandlerM getClientCapabilities + +-- | Wrapper of 'sendNotification for HandlerM +-- +-- TODO: Return notification in result instead of calling `sendNotification` directly +pluginSendNotification :: forall (m :: Method ServerToClient Notification) config. SServerMethod m -> MessageParams m -> HandlerM config () +pluginSendNotification smethod params = HandlerM $ sendNotification smethod params + +-- | Wrapper of 'sendRequest' for HandlerM +-- +-- TODO: Return request in result instead of calling `sendRequest` directly +pluginSendRequest :: forall (m :: Method ServerToClient Request) config. SServerMethod m -> MessageParams m -> (Either (TResponseError m) (MessageResult m) -> HandlerM config ()) -> HandlerM config (LspId m) +pluginSendRequest smethod params action = HandlerM $ sendRequest smethod params (runHandlerM . action) + +-- | Wrapper of 'withIndefiniteProgress' for HandlerM +pluginWithIndefiniteProgress :: T.Text -> Maybe ProgressToken -> ProgressCancellable -> ((T.Text -> HandlerM config ()) -> HandlerM config a) -> HandlerM config a +pluginWithIndefiniteProgress title progressToken cancellable updateAction = + HandlerM $ + withIndefiniteProgress title progressToken cancellable $ \putUpdate -> + runHandlerM $ updateAction (HandlerM . putUpdate) + -- | Combine handlers for the newtype PluginHandler a (m :: Method ClientToServer Request) - = PluginHandler (PluginId -> a -> MessageParams m -> LspM Config (NonEmpty (Either PluginError (MessageResult m)))) + = PluginHandler (PluginId -> a -> MessageParams m -> HandlerM Config (NonEmpty (Either PluginError (MessageResult m)))) newtype PluginNotificationHandler a (m :: Method ClientToServer Notification) = PluginNotificationHandler (PluginId -> a -> VFS -> MessageParams m -> LspM Config ()) @@ -830,7 +977,7 @@ instance Semigroup (PluginNotificationHandlers a) where instance Monoid (PluginNotificationHandlers a) where mempty = PluginNotificationHandlers mempty -type PluginMethodHandler a m = a -> PluginId -> MessageParams m -> ExceptT PluginError (LspM Config) (MessageResult m) +type PluginMethodHandler a m = a -> PluginId -> MessageParams m -> ExceptT PluginError (HandlerM Config) (MessageResult m) type PluginNotificationMethodHandler a m = a -> VFS -> PluginId -> MessageParams m -> LspM Config () @@ -843,7 +990,7 @@ mkPluginHandler -> PluginHandlers ideState mkPluginHandler m f = PluginHandlers $ DMap.singleton (IdeMethod m) (PluginHandler (f' m)) where - f' :: SMethod m -> PluginId -> ideState -> MessageParams m -> LspT Config IO (NonEmpty (Either PluginError (MessageResult m))) + f' :: SMethod m -> PluginId -> ideState -> MessageParams m -> HandlerM Config (NonEmpty (Either PluginError (MessageResult m))) -- We need to have separate functions for each method that supports resolve, so far we only support CodeActions -- CodeLens, and Completion methods. f' SMethod_TextDocumentCodeAction pid ide params@CodeActionParams{_textDocument=TextDocumentIdentifier {_uri}} = @@ -894,10 +1041,11 @@ defaultPluginPriority = 1000 -- -- and handlers will be enabled for files with the appropriate file -- extensions. -defaultPluginDescriptor :: PluginId -> PluginDescriptor ideState -defaultPluginDescriptor plId = +defaultPluginDescriptor :: PluginId -> T.Text -> PluginDescriptor ideState +defaultPluginDescriptor plId desc = PluginDescriptor plId + desc defaultPluginPriority mempty mempty @@ -914,10 +1062,11 @@ defaultPluginDescriptor plId = -- -- Handles files with the following extensions: -- * @.cabal@ -defaultCabalPluginDescriptor :: PluginId -> PluginDescriptor ideState -defaultCabalPluginDescriptor plId = +defaultCabalPluginDescriptor :: PluginId -> T.Text -> PluginDescriptor ideState +defaultCabalPluginDescriptor plId desc = PluginDescriptor plId + desc defaultPluginPriority mempty mempty @@ -943,8 +1092,9 @@ data PluginCommand ideState = forall a. (FromJSON a) => type CommandFunction ideState a = ideState + -> Maybe ProgressToken -> a - -> ExceptT PluginError (LspM Config) (Value |? Null) + -> ExceptT PluginError (HandlerM Config) (Value |? Null) -- --------------------------------------------------------------------- @@ -954,7 +1104,7 @@ type ResolveFunction ideState a (m :: Method ClientToServer Request) = -> MessageParams m -> Uri -> a - -> ExceptT PluginError (LspM Config) (MessageResult m) + -> ExceptT PluginError (HandlerM Config) (MessageResult m) -- | Make a handler for resolve methods. In here we take your provided ResolveFunction -- and turn it into a PluginHandlers. See Note [Resolve in PluginHandlers] @@ -965,7 +1115,7 @@ mkResolveHandler -> PluginHandlers ideState mkResolveHandler m f = mkPluginHandler m $ \ideState plId params -> do case fromJSON <$> (params ^. L.data_) of - (Just (Success (PluginResolveData owner uri value) )) -> do + (Just (Success (PluginResolveData owner@(PluginId ownerName) uri value) )) -> do if owner == plId then case fromJSON value of @@ -975,7 +1125,8 @@ mkResolveHandler m f = mkPluginHandler m $ \ideState plId params -> do Error msg -> -- We are assuming that if we can't decode the data, that this -- request belongs to another resolve handler for this plugin. - throwError (PluginRequestRefused (T.pack ("Unable to decode payload for handler, assuming that it's for a different handler" <> msg))) + throwError (PluginRequestRefused + (NotResolveOwner (ownerName <> ": error decoding payload:" <> T.pack msg))) -- If we are getting an owner that isn't us, this means that there is an -- error, as we filter these our in `pluginEnabled` else throwError $ PluginInternalError invalidRequest @@ -1011,15 +1162,6 @@ newtype PluginId = PluginId T.Text instance IsString PluginId where fromString = PluginId . T.pack --- | Lookup the current config for a plugin -configForPlugin :: Config -> PluginDescriptor c -> PluginConfig -configForPlugin config PluginDescriptor{..} - = Map.findWithDefault (configInitialGenericConfig pluginConfigDescriptor) pluginId (plugins config) - --- | Checks that a given plugin is both enabled and the specific feature is --- enabled -pluginEnabledConfig :: (PluginConfig -> Bool) -> PluginConfig -> Bool -pluginEnabledConfig f pluginConfig = plcGlobalOn pluginConfig && f pluginConfig -- --------------------------------------------------------------------- @@ -1039,37 +1181,15 @@ type FormattingMethod m = type FormattingHandler a = a + -> Maybe ProgressToken -> FormattingType -> T.Text -> NormalizedFilePath -> FormattingOptions - -> ExceptT PluginError (LspM Config) ([TextEdit] |? Null) - -mkFormattingHandlers :: forall a. FormattingHandler a -> PluginHandlers a -mkFormattingHandlers f = mkPluginHandler SMethod_TextDocumentFormatting ( provider SMethod_TextDocumentFormatting) - <> mkPluginHandler SMethod_TextDocumentRangeFormatting (provider SMethod_TextDocumentRangeFormatting) - where - provider :: forall m. FormattingMethod m => SMethod m -> PluginMethodHandler a m - provider m ide _pid params - | Just nfp <- uriToNormalizedFilePath $ toNormalizedUri uri = do - mf <- lift $ getVirtualFile $ toNormalizedUri uri - case mf of - Just vf -> do - let typ = case m of - SMethod_TextDocumentFormatting -> FormatText - SMethod_TextDocumentRangeFormatting -> FormatRange (params ^. L.range) - _ -> Prelude.error "mkFormattingHandlers: impossible" - f ide typ (virtualFileText vf) nfp opts - Nothing -> throwError $ PluginInvalidParams $ T.pack $ "Formatter plugin: could not get file contents for " ++ show uri - - | otherwise = throwError $ PluginInvalidParams $ T.pack $ "Formatter plugin: uriToFilePath failed for: " ++ show uri - where - uri = params ^. L.textDocument . L.uri - opts = params ^. L.options + -> ExceptT PluginError (HandlerM Config) ([TextEdit] |? Null) -- --------------------------------------------------------------------- - data FallbackCodeActionParams = FallbackCodeActionParams { fallbackWorkspaceEdit :: Maybe WorkspaceEdit @@ -1144,15 +1264,8 @@ getProcessID = fromIntegral <$> P.getProcessID installSigUsr1Handler h = void $ installHandler sigUSR1 (Catch h) Nothing #endif --- |Determine whether this request should be routed to the plugin. Fails closed --- if we can't determine which plugin it should be routed to. -pluginResolverResponsible :: Maybe Value -> PluginDescriptor c -> Bool -pluginResolverResponsible (Just (fromJSON -> (Success (PluginResolveData o _ _)))) pluginDesc = - pluginId pluginDesc == o --- We want to fail closed -pluginResolverResponsible _ _ = False - {- Note [Resolve in PluginHandlers] + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Resolve methods have a few guarantees that need to be made by HLS, specifically they need to only be called once, as neither their errors nor their responses can be easily combined. Whereas commands, which similarly have diff --git a/hls-plugin-api/test/Ide/PluginUtilsTest.hs b/hls-plugin-api/test/Ide/PluginUtilsTest.hs index a4f16a4491..1fa1ace39b 100644 --- a/hls-plugin-api/test/Ide/PluginUtilsTest.hs +++ b/hls-plugin-api/test/Ide/PluginUtilsTest.hs @@ -1,19 +1,30 @@ +{-# LANGUAGE OverloadedLabels #-} {-# LANGUAGE OverloadedStrings #-} -{-# LANGUAGE TypeApplications #-} +{-# OPTIONS_GHC -Wno-orphans #-} module Ide.PluginUtilsTest ( tests ) where -import Data.Char (isPrint) +import qualified Data.Aeson as A +import qualified Data.Aeson.Types as A +import Data.ByteString.Lazy (ByteString) +import Data.Function ((&)) import qualified Data.Set as Set import qualified Data.Text as T +import Ide.Plugin.Properties (KeyNamePath (..), + definePropertiesProperty, + defineStringProperty, + emptyProperties, toDefaultJSON, + toVSCodeExtensionSchema, + usePropertyByPath, + usePropertyByPathEither) import qualified Ide.Plugin.RangeMap as RangeMap -import Ide.PluginUtils (extractTextInRange, - positionInRange, unescape) +import Ide.PluginUtils (extractTextInRange, unescape) import Language.LSP.Protocol.Types (Position (..), Range (Range), UInt, isSubrangeOf) import Test.Tasty +import Test.Tasty.Golden (goldenVsStringDiff) import Test.Tasty.HUnit import Test.Tasty.QuickCheck @@ -24,6 +35,7 @@ tests = testGroup "PluginUtils" , localOption (QuickCheckMaxSize 10000) $ testProperty "RangeMap-List filtering identical" $ prop_rangemapListEq @Int + , propertyTest ] unescapeTest :: TestTree @@ -106,7 +118,7 @@ genRangeInline = do pure $ Range x1 x2 where genRangeLength :: Gen UInt - genRangeLength = fromInteger <$> chooseInteger (5, 50) + genRangeLength = uInt (5, 50) genRangeMultiline :: Gen Range genRangeMultiline = do @@ -119,17 +131,20 @@ genRangeMultiline = do pure $ Range x1 x2 where genSecond :: Gen UInt - genSecond = fromInteger <$> chooseInteger (0, 10) + genSecond = uInt (0, 10) genPosition :: Gen Position genPosition = Position - <$> (fromInteger <$> chooseInteger (0, 1000)) - <*> (fromInteger <$> chooseInteger (0, 150)) + <$> uInt (0, 1000) + <*> uInt (0, 150) + +uInt :: (Integer, Integer) -> Gen UInt +uInt (a, b) = fromInteger <$> chooseInteger (a, b) instance Arbitrary Range where arbitrary = genRange -prop_rangemapListEq :: (Show a, Eq a, Ord a) => Range -> [(Range, a)] -> Property +prop_rangemapListEq :: (Show a, Ord a) => Range -> [(Range, a)] -> Property prop_rangemapListEq r xs = let filteredList = (map snd . filter (isSubrangeOf r . fst)) xs filteredRangeMap = RangeMap.filterByRange r (RangeMap.fromList' xs) @@ -137,3 +152,54 @@ prop_rangemapListEq r xs = cover 5 (length filteredList == 1) "1 match" $ cover 2 (length filteredList > 1) ">1 matches" $ Set.fromList filteredList === Set.fromList filteredRangeMap + + +gitDiff :: FilePath -> FilePath -> [String] +gitDiff fRef fNew = ["git", "-c", "core.fileMode=false", "diff", "-w", "--no-index", "--text", "--exit-code", fRef, fNew] + +goldenGitDiff :: TestName -> FilePath -> IO ByteString -> TestTree +goldenGitDiff name = goldenVsStringDiff name gitDiff + +testDir :: FilePath +testDir = "test/testdata/Property" + +propertyTest :: TestTree +propertyTest = testGroup "property api tests" [ + goldenGitDiff "property toVSCodeExtensionSchema" (testDir <> "/NestedPropertyVscode.json") (return $ A.encode $ A.object $ toVSCodeExtensionSchema "top." nestedPropertiesExample) + , goldenGitDiff "property toDefaultJSON" (testDir <> "/NestedPropertyDefault.json") (return $ A.encode $ A.object $ toDefaultJSON nestedPropertiesExample) + , testCase "parsePropertyPath single key path" $ do + let obj = A.object (toDefaultJSON nestedPropertiesExample) + let key1 = A.parseEither (A.withObject "test parsePropertyPath" $ \o -> do + let key1 = usePropertyByPathEither examplePath1 nestedPropertiesExample o + return key1) obj + key1 @?= Right (Right "baz") + , testCase "parsePropertyPath two key path" $ do + let obj = A.object (toDefaultJSON nestedPropertiesExample) + let key1 = A.parseEither (A.withObject "test parsePropertyPath" $ \o -> do + let key1 = usePropertyByPathEither examplePath2 nestedPropertiesExample o + return key1) obj + key1 @?= Right (Right "foo") + , testCase "parsePropertyPath two key path default" $ do + let obj = A.object [] + let key1 = A.parseEither (A.withObject "test parsePropertyPath" $ \o -> do + let key1 = usePropertyByPath examplePath2 nestedPropertiesExample o + return key1) obj + key1 @?= Right "foo" + , testCase "parsePropertyPath two key path not default" $ do + let obj = A.object (toDefaultJSON nestedPropertiesExample2) + let key1 = A.parseEither (A.withObject "test parsePropertyPath" $ \o -> do + let key1 = usePropertyByPathEither examplePath2 nestedPropertiesExample o + return key1) obj + key1 @?= Right (Right "xxx") + ] + where + nestedPropertiesExample = emptyProperties + & definePropertiesProperty #parent "parent" (emptyProperties & defineStringProperty #foo "foo" "foo" & defineStringProperty #boo "boo" "boo") + & defineStringProperty #baz "baz" "baz" + + nestedPropertiesExample2 = emptyProperties + & definePropertiesProperty #parent "parent" (emptyProperties & defineStringProperty #foo "foo" "xxx") + & defineStringProperty #baz "baz" "baz" + + examplePath1 = SingleKey #baz + examplePath2 = ConsKeysPath #parent (SingleKey #foo) diff --git a/hls-plugin-api/test/Ide/TypesTests.hs b/hls-plugin-api/test/Ide/TypesTests.hs index c5ceab7ed2..07556d625c 100644 --- a/hls-plugin-api/test/Ide/TypesTests.hs +++ b/hls-plugin-api/test/Ide/TypesTests.hs @@ -1,23 +1,19 @@ -{-# LANGUAGE DerivingStrategies #-} -{-# LANGUAGE DuplicateRecordFields #-} -{-# LANGUAGE GeneralizedNewtypeDeriving #-} -{-# LANGUAGE TypeFamilies #-} -{-# LANGUAGE TypeOperators #-} +{-# LANGUAGE DerivingStrategies #-} +{-# LANGUAGE DuplicateRecordFields #-} +{-# LANGUAGE TypeFamilies #-} module Ide.TypesTests ( tests ) where -import Control.Lens (preview, (?~), (^?)) -import Control.Monad ((>=>)) +import Control.Lens ((?~), (^?)) import Data.Default (Default (def)) import Data.Function ((&)) -import Data.List.NonEmpty (NonEmpty ((:|)), nonEmpty) +import Data.List.NonEmpty (NonEmpty ((:|))) import Data.Maybe (isJust) import qualified Data.Text as Text -import Ide.Types (Config (Config), - PluginRequestMethod (combineResponses)) +import Ide.Types (PluginRequestMethod (combineResponses)) import qualified Language.LSP.Protocol.Lens as L -import Language.LSP.Protocol.Message (Method (Method_TextDocumentDefinition), +import Language.LSP.Protocol.Message (MessageParams, MessageResult, SMethod (..)) import Language.LSP.Protocol.Types (ClientCapabilities, Definition (Definition), @@ -29,18 +25,17 @@ import Language.LSP.Protocol.Types (ClientCapabilities, Null (Null), Position (Position), Range (Range), - TextDocumentClientCapabilities (TextDocumentClientCapabilities, _definition), + TextDocumentClientCapabilities, TextDocumentIdentifier (TextDocumentIdentifier), TypeDefinitionClientCapabilities (TypeDefinitionClientCapabilities, _dynamicRegistration, _linkSupport), TypeDefinitionParams (..), - Uri (Uri), _L, _R, + Uri (Uri), _L, _R, _definition, _typeDefinition, filePathToUri, type (|?) (..)) import Test.Tasty (TestTree, testGroup) -import Test.Tasty.HUnit (assertBool, testCase, (@=?)) +import Test.Tasty.HUnit (testCase, (@=?)) import Test.Tasty.QuickCheck (ASCIIString (ASCIIString), Arbitrary (arbitrary), Gen, - NonEmptyList (NonEmpty), arbitraryBoundedEnum, cover, listOf1, oneof, testProperty, (===)) @@ -63,6 +58,11 @@ combineResponsesTextDocumentTypeDefinitionTests :: TestTree combineResponsesTextDocumentTypeDefinitionTests = testGroup "TextDocumentTypeDefinition" $ defAndTypeDefSharedTests SMethod_TextDocumentTypeDefinition typeDefinitionParams +defAndTypeDefSharedTests :: + ( MessageResult m ~ (Definition |? ([DefinitionLink] |? Null)) + , PluginRequestMethod m + ) + => SMethod m -> MessageParams m -> [TestTree] defAndTypeDefSharedTests message params = [ testCase "merges all single location responses into one response with all locations (without upgrading to links)" $ do let pluginResponses :: NonEmpty (Definition |? ([DefinitionLink] |? Null)) @@ -177,7 +177,11 @@ defAndTypeDefSharedTests message params = (isJust (result ^? _L) || isJust (result ^? _R >>= (^? _R))) === True ] -(range1, range2, range3) = (Range (Position 3 0) $ Position 3 5, Range (Position 5 7) $ Position 5 13, Range (Position 24 30) $ Position 24 40) + +range1, range2, range3 :: Range +range1 = Range (Position 3 0) $ Position 3 5 +range2 = Range (Position 5 7) $ Position 5 13 +range3 = Range (Position 24 30) $ Position 24 40 supportsLinkInAllDefinitionCaps :: ClientCapabilities supportsLinkInAllDefinitionCaps = def & L.textDocument ?~ textDocumentCaps diff --git a/hls-plugin-api/test/testdata/Property/NestedPropertyDefault.json b/hls-plugin-api/test/testdata/Property/NestedPropertyDefault.json new file mode 100644 index 0000000000..0d8f57656c --- /dev/null +++ b/hls-plugin-api/test/testdata/Property/NestedPropertyDefault.json @@ -0,0 +1 @@ +{"baz":"baz","parent":{"boo":"boo","foo":"foo"}} \ No newline at end of file diff --git a/hls-plugin-api/test/testdata/Property/NestedPropertyVscode.json b/hls-plugin-api/test/testdata/Property/NestedPropertyVscode.json new file mode 100644 index 0000000000..4c9e721c4d --- /dev/null +++ b/hls-plugin-api/test/testdata/Property/NestedPropertyVscode.json @@ -0,0 +1 @@ +{"top.baz":{"default":"baz","markdownDescription":"baz","scope":"resource","type":"string"},"top.parent.boo":{"default":"boo","markdownDescription":"boo","scope":"resource","type":"string"},"top.parent.foo":{"default":"foo","markdownDescription":"foo","scope":"resource","type":"string"}} diff --git a/hls-test-utils/hls-test-utils.cabal b/hls-test-utils/hls-test-utils.cabal index 8e822d380a..084de98534 100644 --- a/hls-test-utils/hls-test-utils.cabal +++ b/hls-test-utils/hls-test-utils.cabal @@ -1,6 +1,6 @@ cabal-version: 2.4 name: hls-test-utils -version: 2.4.0.0 +version: 2.11.0.0 synopsis: Utilities used in the tests of Haskell Language Server description: Please see the README on GitHub at @@ -29,26 +29,28 @@ library Test.Hls Test.Hls.Util Test.Hls.FileSystem + Development.IDE.Test + Development.IDE.Test.Diagnostic hs-source-dirs: src build-depends: , aeson , async , base >=4.12 && <5 - , blaze-markup , bytestring , containers , data-default , directory , extra , filepath - , ghcide == 2.4.0.0 - , hls-graph - , hls-plugin-api == 2.4.0.0 + , ghcide == 2.11.0.0 + , hls-plugin-api == 2.11.0.0 , lens - , lsp ^>=2.3 - , lsp-test ^>=0.16 - , lsp-types ^>=2.1 + , lsp + , lsp-test ^>=0.17 + , lsp-types ^>=2.3 + , neat-interpolation + , safe-exceptions , tasty , tasty-expected-failure , tasty-golden @@ -56,11 +58,15 @@ library , tasty-rerun , temporary , text - , unordered-containers - , row-types - ghc-options: -Wall + , text-rope + + ghc-options: + -Wall + -Wunused-packages + -Wno-name-shadowing + -Wno-unticked-promoted-constructors if flag(pedantic) ghc-options: -Werror - default-language: Haskell2010 + default-language: GHC2021 diff --git a/ghcide/test/src/Development/IDE/Test.hs b/hls-test-utils/src/Development/IDE/Test.hs similarity index 89% rename from ghcide/test/src/Development/IDE/Test.hs rename to hls-test-utils/src/Development/IDE/Test.hs index e92e7a43d9..a1bd2dec0e 100644 --- a/ghcide/test/src/Development/IDE/Test.hs +++ b/hls-test-utils/src/Development/IDE/Test.hs @@ -4,7 +4,8 @@ {-# LANGUAGE DataKinds #-} {-# LANGUAGE DuplicateRecordFields #-} {-# LANGUAGE GADTs #-} -{-# LANGUAGE PolyKinds #-} +{-# LANGUAGE LambdaCase #-} +{-# LANGUAGE OverloadedStrings #-} module Development.IDE.Test ( Cursor @@ -13,6 +14,8 @@ module Development.IDE.Test , diagnostic , expectDiagnostics , expectDiagnosticsWithTags + , ExpectedDiagnostic + , ExpectedDiagnosticWithTag , expectNoMoreDiagnostics , expectMessages , expectCurrentDiagnostics @@ -62,10 +65,13 @@ import System.FilePath (equalFilePath) import System.Time.Extra import Test.Tasty.HUnit +expectedDiagnosticWithNothing :: ExpectedDiagnostic -> ExpectedDiagnosticWithTag +expectedDiagnosticWithNothing (ds, c, t, code) = (ds, c, t, code, Nothing) + requireDiagnosticM :: (Foldable f, Show (f Diagnostic), HasCallStack) => f Diagnostic - -> (DiagnosticSeverity, Cursor, T.Text, Maybe DiagnosticTag) + -> ExpectedDiagnosticWithTag -> Assertion requireDiagnosticM actuals expected = case requireDiagnostic actuals expected of Nothing -> pure () @@ -78,7 +84,7 @@ expectNoMoreDiagnostics timeout = expectMessages SMethod_TextDocumentPublishDiagnostics timeout $ \diagsNot -> do let fileUri = diagsNot ^. L.params . L.uri actual = diagsNot ^. L.params . L.diagnostics - unless (actual == []) $ liftIO $ + unless (null actual) $ liftIO $ assertFailure $ "Got unexpected diagnostics for " <> show fileUri <> " got " @@ -113,25 +119,25 @@ flushMessages = do -- -- Rather than trying to assert the absence of diagnostics, introduce an -- expected diagnostic (e.g. a redundant import) and assert the singleton diagnostic. -expectDiagnostics :: HasCallStack => [(FilePath, [(DiagnosticSeverity, Cursor, T.Text)])] -> Session () +expectDiagnostics :: HasCallStack => [(FilePath, [ExpectedDiagnostic])] -> Session () expectDiagnostics = expectDiagnosticsWithTags - . map (second (map (\(ds, c, t) -> (ds, c, t, Nothing)))) + . map (second (map expectedDiagnosticWithNothing)) unwrapDiagnostic :: TServerMessage Method_TextDocumentPublishDiagnostics -> (Uri, [Diagnostic]) unwrapDiagnostic diagsNot = (diagsNot^. L.params . L.uri, diagsNot^. L.params . L.diagnostics) -expectDiagnosticsWithTags :: HasCallStack => [(String, [(DiagnosticSeverity, Cursor, T.Text, Maybe DiagnosticTag)])] -> Session () +expectDiagnosticsWithTags :: HasCallStack => [(String, [ExpectedDiagnosticWithTag])] -> Session () expectDiagnosticsWithTags expected = do - let f = getDocUri >=> liftIO . canonicalizeUri >=> pure . toNormalizedUri + let toSessionPath = getDocUri >=> liftIO . canonicalizeUri >=> pure . toNormalizedUri next = unwrapDiagnostic <$> skipManyTill anyMessage diagnostic - expected' <- Map.fromListWith (<>) <$> traverseOf (traverse . _1) f expected + expected' <- Map.fromListWith (<>) <$> traverseOf (traverse . _1) toSessionPath expected expectDiagnosticsWithTags' next expected' expectDiagnosticsWithTags' :: (HasCallStack, MonadIO m) => m (Uri, [Diagnostic]) -> - Map.Map NormalizedUri [(DiagnosticSeverity, Cursor, T.Text, Maybe DiagnosticTag)] -> + Map.Map NormalizedUri [ExpectedDiagnosticWithTag] -> m () expectDiagnosticsWithTags' next m | null m = do (_,actual) <- next @@ -169,14 +175,14 @@ expectDiagnosticsWithTags' next expected = go expected <> show actual go $ Map.delete canonUri m -expectCurrentDiagnostics :: HasCallStack => TextDocumentIdentifier -> [(DiagnosticSeverity, Cursor, T.Text)] -> Session () +expectCurrentDiagnostics :: HasCallStack => TextDocumentIdentifier -> [ExpectedDiagnostic] -> Session () expectCurrentDiagnostics doc expected = do diags <- getCurrentDiagnostics doc checkDiagnosticsForDoc doc expected diags -checkDiagnosticsForDoc :: HasCallStack => TextDocumentIdentifier -> [(DiagnosticSeverity, Cursor, T.Text)] -> [Diagnostic] -> Session () +checkDiagnosticsForDoc :: HasCallStack => TextDocumentIdentifier -> [ExpectedDiagnostic] -> [Diagnostic] -> Session () checkDiagnosticsForDoc TextDocumentIdentifier {_uri} expected obtained = do - let expected' = Map.fromList [(nuri, map (\(ds, c, t) -> (ds, c, t, Nothing)) expected)] + let expected' = Map.singleton nuri (map expectedDiagnosticWithNothing expected) nuri = toNormalizedUri _uri expectDiagnosticsWithTags' (return (_uri, obtained)) expected' @@ -186,7 +192,7 @@ canonicalizeUri uri = filePathToUri <$> canonicalizePath (fromJust (uriToFilePat diagnostic :: Session (TNotificationMessage Method_TextDocumentPublishDiagnostics) diagnostic = LspTest.message SMethod_TextDocumentPublishDiagnostics -tryCallTestPlugin :: (A.FromJSON b) => TestRequest -> Session (Either ResponseError b) +tryCallTestPlugin :: (A.FromJSON b) => TestRequest -> Session (Either (TResponseError @ClientToServer (Method_CustomMethod "test")) b) tryCallTestPlugin cmd = do let cm = SMethod_CustomMethod (Proxy @"test") waitId <- sendRequest cm (A.toJSON cmd) @@ -201,8 +207,8 @@ callTestPlugin :: (A.FromJSON b) => TestRequest -> Session b callTestPlugin cmd = do res <- tryCallTestPlugin cmd case res of - Left (ResponseError t err _) -> error $ show t <> ": " <> T.unpack err - Right a -> pure a + Left (TResponseError t err _) -> error $ show t <> ": " <> T.unpack err + Right a -> pure a waitForAction :: String -> TextDocumentIdentifier -> Session WaitForIdeRuleResult diff --git a/ghcide/test/src/Development/IDE/Test/Diagnostic.hs b/hls-test-utils/src/Development/IDE/Test/Diagnostic.hs similarity index 57% rename from ghcide/test/src/Development/IDE/Test/Diagnostic.hs rename to hls-test-utils/src/Development/IDE/Test/Diagnostic.hs index 86c1b8bb9d..e64ab34876 100644 --- a/ghcide/test/src/Development/IDE/Test/Diagnostic.hs +++ b/hls-test-utils/src/Development/IDE/Test/Diagnostic.hs @@ -1,7 +1,9 @@ +{-# LANGUAGE CPP #-} module Development.IDE.Test.Diagnostic where import Control.Lens ((^.)) import qualified Data.Text as T +import Development.IDE.GHC.Compat (GhcVersion (..), ghcVersion) import GHC.Stack (HasCallStack) import Language.LSP.Protocol.Lens import Language.LSP.Protocol.Types @@ -14,12 +16,41 @@ cursorPosition (line, col) = Position line col type ErrorMsg = String + +-- | Expected diagnostics have the following components: +-- +-- 1. severity +-- 2. cursor (line and column numbers) +-- 3. infix of the message +-- 4. code (e.g. GHC-87543) +type ExpectedDiagnostic = + ( DiagnosticSeverity + , Cursor + , T.Text + , Maybe T.Text + ) + +-- | Expected diagnostics with a tag have the following components: +-- +-- 1. severity +-- 2. cursor (line and column numbers) +-- 3. infix of the message +-- 4. code (e.g. GHC-87543) +-- 5. tag (unnecessary or deprecated) +type ExpectedDiagnosticWithTag = + ( DiagnosticSeverity + , Cursor + , T.Text + , Maybe T.Text + , Maybe DiagnosticTag + ) + requireDiagnostic :: (Foldable f, Show (f Diagnostic), HasCallStack) => f Diagnostic - -> (DiagnosticSeverity, Cursor, T.Text, Maybe DiagnosticTag) + -> ExpectedDiagnosticWithTag -> Maybe ErrorMsg -requireDiagnostic actuals expected@(severity, cursor, expectedMsg, expectedTag) +requireDiagnostic actuals expected@(severity, cursor, expectedMsg, mbExpectedCode, expectedTag) | any match actuals = Nothing | otherwise = Just $ "Could not find " <> show expected <> @@ -32,6 +63,15 @@ requireDiagnostic actuals expected@(severity, cursor, expectedMsg, expectedTag) && standardizeQuotes (T.toLower expectedMsg) `T.isInfixOf` standardizeQuotes (T.toLower $ d ^. message) && hasTag expectedTag (d ^. tags) + && codeMatches d + + codeMatches d + | ghcVersion >= GHC96 = + case (mbExpectedCode, _code d) of + (Nothing, _) -> True + (Just expectedCode, Nothing) -> False + (Just expectedCode, Just actualCode) -> InR expectedCode == actualCode + | otherwise = True hasTag :: Maybe DiagnosticTag -> Maybe [DiagnosticTag] -> Bool hasTag Nothing _ = True diff --git a/hls-test-utils/src/Test/Hls.hs b/hls-test-utils/src/Test/Hls.hs index 9320e3b300..1193b2dd19 100644 --- a/hls-test-utils/src/Test/Hls.hs +++ b/hls-test-utils/src/Test/Hls.hs @@ -1,14 +1,12 @@ -{-# LANGUAGE DataKinds #-} -{-# LANGUAGE DisambiguateRecordFields #-} -{-# LANGUAGE DuplicateRecordFields #-} -{-# LANGUAGE GADTs #-} -{-# LANGUAGE LambdaCase #-} -{-# LANGUAGE NamedFieldPuns #-} -{-# LANGUAGE OverloadedLists #-} -{-# LANGUAGE OverloadedStrings #-} -{-# LANGUAGE PolyKinds #-} -{-# LANGUAGE RankNTypes #-} -{-# LANGUAGE TypeApplications #-} +{-# LANGUAGE DataKinds #-} +{-# LANGUAGE DuplicateRecordFields #-} +{-# LANGUAGE GADTs #-} +{-# LANGUAGE LambdaCase #-} +{-# LANGUAGE OverloadedLists #-} +{-# LANGUAGE OverloadedStrings #-} +{-# LANGUAGE RecordWildCards #-} +{-# LANGUAGE ScopedTypeVariables #-} +{-# LANGUAGE TypeApplications #-} module Test.Hls ( module Test.Tasty.HUnit, module Test.Tasty, @@ -30,21 +28,26 @@ module Test.Hls goldenWithHaskellDocFormatterInTmpDir, goldenWithCabalDocFormatter, goldenWithCabalDocFormatterInTmpDir, + goldenWithTestConfig, def, -- * Running HLS for integration tests runSessionWithServer, - runSessionWithServerAndCaps, runSessionWithServerInTmpDir, - runSessionWithServerAndCapsInTmpDir, - runSessionWithServer', - runSessionWithServerInTmpDir', + runSessionWithTestConfig, + -- * Running parameterised tests for a set of test configurations + parameterisedCursorTest, -- * Helpful re-exports PluginDescriptor, IdeState, + -- * Helpers for expected test case failuers + BrokenBehavior(..), + ExpectBroken(..), + unCurrent, -- * Assertion helper functions waitForProgressDone, waitForAllProgressDone, waitForBuildQueue, + waitForProgressBegin, waitForTypecheck, waitForAction, hlsConfigToClientConfig, @@ -54,7 +57,7 @@ module Test.Hls waitForKickStart, -- * Plugin descriptor helper functions for tests PluginTestDescriptor, - pluginTestRecorder, + hlsPluginTestRecorder, mkPluginTestDescriptor, mkPluginTestDescriptor', -- * Re-export logger types @@ -62,72 +65,89 @@ module Test.Hls WithPriority(..), Recorder, Priority(..), + captureKickDiagnostics, + kick, + TestConfig(..) ) where import Control.Applicative.Combinators -import Control.Concurrent.Async (async, cancel, wait) +import Control.Concurrent.Async (async, cancel, wait) import Control.Concurrent.Extra -import Control.Exception.Base -import Control.Lens.Extras (is) -import Control.Monad (guard, unless, void) -import Control.Monad.Extra (forM) +import Control.Exception.Safe +import Control.Lens ((^.)) +import Control.Lens.Extras (is) +import Control.Monad (guard, unless, void) +import Control.Monad.Extra (forM) import Control.Monad.IO.Class -import Data.Aeson (Result (Success), - Value (Null), fromJSON, - toJSON) -import qualified Data.Aeson as A -import Data.ByteString.Lazy (ByteString) -import Data.Default (def) -import qualified Data.Map as M -import Data.Maybe (fromMaybe) -import Data.Proxy (Proxy (Proxy)) -import qualified Data.Text as T -import qualified Data.Text.Lazy as TL -import qualified Data.Text.Lazy.Encoding as TL -import Development.IDE (IdeState) -import Development.IDE.Main hiding (Log) -import qualified Development.IDE.Main as Ghcide -import qualified Development.IDE.Main as IDEMain -import Development.IDE.Plugin.Test (TestRequest (GetBuildKeysBuilt, WaitForIdeRule, WaitForShakeQueue), - WaitForIdeRuleResult (ideResultSuccess)) -import qualified Development.IDE.Plugin.Test as Test +import Data.Aeson (Result (Success), + Value (Null), + fromJSON, toJSON) +import qualified Data.Aeson as A +import Data.ByteString.Lazy (ByteString) +import Data.Default (Default, def) +import qualified Data.Map as M +import Data.Maybe (fromMaybe, mapMaybe) +import Data.Proxy (Proxy (Proxy)) +import qualified Data.Text as T +import qualified Data.Text.Lazy as TL +import qualified Data.Text.Lazy.Encoding as TL +import Development.IDE (IdeState, + LoggingColumn (ThreadIdColumn), + defaultLayoutOptions, + layoutPretty, + renderStrict) +import Development.IDE.Main hiding (Log) +import qualified Development.IDE.Main as IDEMain +import Development.IDE.Plugin.Completions.Types (PosPrefixInfo) +import Development.IDE.Plugin.Test (TestRequest (GetBuildKeysBuilt, WaitForIdeRule, WaitForShakeQueue), + WaitForIdeRuleResult (ideResultSuccess)) +import qualified Development.IDE.Plugin.Test as Test import Development.IDE.Types.Options import GHC.IO.Handle -import GHC.Stack (emptyCallStack) import GHC.TypeLits -import Ide.Logger (Doc, Logger (Logger), - Pretty (pretty), - Priority (..), - Recorder (Recorder, logger_), - WithPriority (WithPriority, priority), - cfilter, cmapWithPrio, - logWith, - makeDefaultStderrRecorder, - (<+>)) +import Ide.Logger (Pretty (pretty), + Priority (..), + Recorder, + WithPriority (WithPriority, priority), + cfilter, + cmapWithPrio, + defaultLoggingColumns, + logWith, + makeDefaultStderrRecorder, + (<+>)) +import qualified Ide.Logger as Logger +import Ide.PluginUtils (idePluginsToPluginDesc, + pluginDescToIdePlugins) import Ide.Types import Language.LSP.Protocol.Capabilities +import qualified Language.LSP.Protocol.Lens as L import Language.LSP.Protocol.Message -import Language.LSP.Protocol.Types hiding (Null) +import qualified Language.LSP.Protocol.Message as LSP +import Language.LSP.Protocol.Types hiding (Null) +import qualified Language.LSP.Server as LSP import Language.LSP.Test -import Prelude hiding (log) -import System.Directory (getCurrentDirectory, - setCurrentDirectory) -import System.Environment (lookupEnv) +import Prelude hiding (log) +import System.Directory (canonicalizePath, + createDirectoryIfMissing, + getCurrentDirectory, + getTemporaryDirectory, + makeAbsolute, + setCurrentDirectory) +import System.Environment (lookupEnv, setEnv) import System.FilePath -import System.IO.Extra (newTempDir, withTempDir) -import System.IO.Unsafe (unsafePerformIO) -import System.Process.Extra (createPipe) +import System.IO.Extra (newTempDirWithin) +import System.IO.Unsafe (unsafePerformIO) +import System.Process.Extra (createPipe) import System.Time.Extra -import qualified Test.Hls.FileSystem as FS +import qualified Test.Hls.FileSystem as FS import Test.Hls.FileSystem import Test.Hls.Util -import Test.Tasty hiding (Timeout) +import Test.Tasty hiding (Timeout) import Test.Tasty.ExpectedFailure import Test.Tasty.Golden import Test.Tasty.HUnit import Test.Tasty.Ingredients.Rerun -import Test.Tasty.Runners (NumThreads (..)) data Log = LogIDEMain IDEMain.Log @@ -150,9 +170,18 @@ instance Pretty LogTestHarness where LogCleanup -> "Cleaned up temporary directory" LogNoCleanup -> "No cleanup of temporary directory" +data BrokenBehavior = Current | Ideal + +data ExpectBroken (k :: BrokenBehavior) a where + BrokenCurrent :: a -> ExpectBroken 'Current a + BrokenIdeal :: a -> ExpectBroken 'Ideal a + +unCurrent :: ExpectBroken 'Current a -> a +unCurrent (BrokenCurrent a) = a + -- | Run 'defaultMainWithRerun', limiting each single test case running at most 10 minutes defaultTestRunner :: TestTree -> IO () -defaultTestRunner = defaultMainWithRerun . adjustOption (const $ NumThreads 1) . adjustOption (const $ mkTimeout 600000000) +defaultTestRunner = defaultMainWithRerun . adjustOption (const $ mkTimeout 600000000) gitDiff :: FilePath -> FilePath -> [String] gitDiff fRef fNew = ["git", "-c", "core.fileMode=false", "diff", "--no-index", "--text", "--exit-code", fRef, fNew] @@ -171,7 +200,7 @@ goldenWithHaskellDoc -> FilePath -> (TextDocumentIdentifier -> Session ()) -> TestTree -goldenWithHaskellDoc = goldenWithDoc "haskell" +goldenWithHaskellDoc = goldenWithDoc LanguageKind_Haskell goldenWithHaskellDocInTmpDir :: Pretty b @@ -184,7 +213,7 @@ goldenWithHaskellDocInTmpDir -> FilePath -> (TextDocumentIdentifier -> Session ()) -> TestTree -goldenWithHaskellDocInTmpDir = goldenWithDocInTmpDir "haskell" +goldenWithHaskellDocInTmpDir = goldenWithDocInTmpDir LanguageKind_Haskell goldenWithHaskellAndCaps :: Pretty b @@ -200,7 +229,34 @@ goldenWithHaskellAndCaps -> TestTree goldenWithHaskellAndCaps config clientCaps plugin title testDataDir path desc ext act = goldenGitDiff title (testDataDir path <.> desc <.> ext) - $ runSessionWithServerAndCaps config plugin clientCaps testDataDir + $ runSessionWithTestConfig def { + testDirLocation = Left testDataDir, + testConfigCaps = clientCaps, + testLspConfig = config, + testPluginDescriptor = plugin + } + $ const +-- runSessionWithServerAndCaps config plugin clientCaps testDataDir + $ TL.encodeUtf8 . TL.fromStrict + <$> do + doc <- openDoc (path <.> ext) "haskell" + void waitForBuildQueue + act doc + documentContents doc + +goldenWithTestConfig + :: Pretty b + => TestConfig b + -> TestName + -> VirtualFileTree + -> FilePath + -> FilePath + -> FilePath + -> (TextDocumentIdentifier -> Session ()) + -> TestTree +goldenWithTestConfig config title tree path desc ext act = + goldenGitDiff title (vftOriginalRoot tree path <.> desc <.> ext) + $ runSessionWithTestConfig config $ const $ TL.encodeUtf8 . TL.fromStrict <$> do doc <- openDoc (path <.> ext) "haskell" @@ -222,7 +278,13 @@ goldenWithHaskellAndCapsInTmpDir -> TestTree goldenWithHaskellAndCapsInTmpDir config clientCaps plugin title tree path desc ext act = goldenGitDiff title (vftOriginalRoot tree path <.> desc <.> ext) - $ runSessionWithServerAndCapsInTmpDir config plugin clientCaps tree + $ + runSessionWithTestConfig def { + testDirLocation = Right tree, + testConfigCaps = clientCaps, + testLspConfig = config, + testPluginDescriptor = plugin + } $ const $ TL.encodeUtf8 . TL.fromStrict <$> do doc <- openDoc (path <.> ext) "haskell" @@ -241,11 +303,11 @@ goldenWithCabalDoc -> FilePath -> (TextDocumentIdentifier -> Session ()) -> TestTree -goldenWithCabalDoc = goldenWithDoc "cabal" +goldenWithCabalDoc = goldenWithDoc (LanguageKind_Custom "cabal") goldenWithDoc :: Pretty b - => T.Text + => LanguageKind -> Config -> PluginTestDescriptor b -> TestName @@ -255,19 +317,19 @@ goldenWithDoc -> FilePath -> (TextDocumentIdentifier -> Session ()) -> TestTree -goldenWithDoc fileType config plugin title testDataDir path desc ext act = +goldenWithDoc languageKind config plugin title testDataDir path desc ext act = goldenGitDiff title (testDataDir path <.> desc <.> ext) $ runSessionWithServer config plugin testDataDir $ TL.encodeUtf8 . TL.fromStrict <$> do - doc <- openDoc (path <.> ext) fileType + doc <- openDoc (path <.> ext) languageKind void waitForBuildQueue act doc documentContents doc goldenWithDocInTmpDir :: Pretty b - => T.Text + => LanguageKind -> Config -> PluginTestDescriptor b -> TestName @@ -277,16 +339,66 @@ goldenWithDocInTmpDir -> FilePath -> (TextDocumentIdentifier -> Session ()) -> TestTree -goldenWithDocInTmpDir fileType config plugin title tree path desc ext act = +goldenWithDocInTmpDir languageKind config plugin title tree path desc ext act = goldenGitDiff title (vftOriginalRoot tree path <.> desc <.> ext) $ runSessionWithServerInTmpDir config plugin tree $ TL.encodeUtf8 . TL.fromStrict <$> do - doc <- openDoc (path <.> ext) fileType + doc <- openDoc (path <.> ext) languageKind void waitForBuildQueue act doc documentContents doc +-- | A parameterised test is similar to a normal test case but allows to run +-- the same test case multiple times with different inputs. +-- A 'parameterisedCursorTest' allows to define a test case based on an input file +-- that specifies one or many cursor positions via the identification value '^'. +-- +-- For example: +-- +-- @ +-- parameterisedCursorTest "Cursor Test" [trimming| +-- foo = 2 +-- ^ +-- bar = 3 +-- baz = foo + bar +-- ^ +-- |] +-- ["foo", "baz"] +-- (\input cursor -> findFunctionNameUnderCursor input cursor) +-- @ +-- +-- Assuming a fitting implementation for 'findFunctionNameUnderCursor'. +-- +-- This test definition will run the test case 'findFunctionNameUnderCursor' for +-- each cursor position, each in its own isolated 'testCase'. +-- Cursor positions are identified via the character '^', which points to the +-- above line as the actual cursor position. +-- Lines containing '^' characters, are removed from the final text, that is +-- passed to the testing function. +-- +-- TODO: Many Haskell and Cabal source may contain '^' characters for good reasons. +-- We likely need a way to change the character for certain test cases in the future. +-- +-- The quasi quoter 'trimming' is very helpful to define such tests, as it additionally +-- allows to interpolate haskell values and functions. We reexport this quasi quoter +-- for easier usage. +parameterisedCursorTest :: (Show a, Eq a) => String -> T.Text -> [a] -> (T.Text -> PosPrefixInfo -> IO a) -> TestTree +parameterisedCursorTest title content expectations act + | lenPrefs /= lenExpected = error $ "parameterisedCursorTest: Expected " <> show lenExpected <> " cursors but found: " <> show lenPrefs + | otherwise = testGroup title $ + map singleTest testCaseSpec + where + lenPrefs = length prefInfos + lenExpected = length expectations + (cleanText, prefInfos) = extractCursorPositions content + + testCaseSpec = zip [1 ::Int ..] (zip expectations prefInfos) + + singleTest (n, (expected, info)) = testCase (title <> " " <> show n) $ do + actual <- act cleanText info + assertEqual (mkParameterisedLabel info) expected actual + -- ------------------------------------------------------------ -- Helper function for initialising plugins under test -- ------------------------------------------------------------ @@ -325,9 +437,28 @@ mkPluginTestDescriptor' -> PluginTestDescriptor b mkPluginTestDescriptor' pluginDesc plId _recorder = IdePlugins [pluginDesc plId] --- | Initialise a recorder that can be instructed to write to stderr by --- setting the environment variable "HLS_TEST_PLUGIN_LOG_STDERR=1" before --- running the tests. +-- | Initialize a recorder that can be instructed to write to stderr by +-- setting one of the environment variables: +-- +-- * HLS_TEST_HARNESS_STDERR=1 +-- * HLS_TEST_LOG_STDERR=1 +-- +-- "HLS_TEST_LOG_STDERR" is intended to enable all logging for the server and the plugins +-- under test. +hlsHelperTestRecorder :: Pretty a => IO (Recorder (WithPriority a)) +hlsHelperTestRecorder = initializeTestRecorder ["HLS_TEST_HARNESS_STDERR", "HLS_TEST_LOG_STDERR"] + + +-- | Initialize a recorder that can be instructed to write to stderr by +-- setting one of the environment variables: +-- +-- * HLS_TEST_PLUGIN_LOG_STDERR=1 +-- * HLS_TEST_LOG_STDERR=1 +-- +-- before running the tests. +-- +-- "HLS_TEST_LOG_STDERR" is intended to enable all logging for the server and the plugins +-- under test. -- -- On the cli, use for example: -- @@ -340,12 +471,10 @@ mkPluginTestDescriptor' pluginDesc plId _recorder = IdePlugins [pluginDesc plId] -- @ -- HLS_TEST_LOG_STDERR=1 cabal test -- @ -pluginTestRecorder :: Pretty a => IO (Recorder (WithPriority a)) -pluginTestRecorder = do - (recorder, _) <- initialiseTestRecorder ["HLS_TEST_PLUGIN_LOG_STDERR", "HLS_TEST_LOG_STDERR"] - pure recorder +hlsPluginTestRecorder :: Pretty a => IO (Recorder (WithPriority a)) +hlsPluginTestRecorder = initializeTestRecorder ["HLS_TEST_PLUGIN_LOG_STDERR", "HLS_TEST_LOG_STDERR"] --- | Generic recorder initialisation for plugins and the HLS server for test-cases. +-- | Generic recorder initialization for plugins and the HLS server for test-cases. -- -- The created recorder writes to stderr if any of the given environment variables -- have been set to a value different to @0@. @@ -354,104 +483,100 @@ pluginTestRecorder = do -- -- We have to return the base logger function for HLS server logging initialisation. -- See 'runSessionWithServer'' for details. -initialiseTestRecorder :: Pretty a => [String] -> IO (Recorder (WithPriority a), WithPriority (Doc ann) -> IO ()) -initialiseTestRecorder envVars = do - docWithPriorityRecorder <- makeDefaultStderrRecorder Nothing +initializeTestRecorder :: Pretty a => [String] -> IO (Recorder (WithPriority a)) +initializeTestRecorder envVars = do + docWithPriorityRecorder <- makeDefaultStderrRecorder (Just $ ThreadIdColumn : defaultLoggingColumns) + -- lspClientLogRecorder -- There are potentially multiple environment variables that enable this logger - definedEnvVars <- forM envVars (\var -> fromMaybe "0" <$> lookupEnv var) + definedEnvVars <- forM envVars (fmap (fromMaybe "0") . lookupEnv) let logStdErr = any (/= "0") definedEnvVars docWithFilteredPriorityRecorder = if logStdErr then cfilter (\WithPriority{ priority } -> priority >= Debug) docWithPriorityRecorder else mempty - Recorder {logger_} = docWithFilteredPriorityRecorder - - pure (cmapWithPrio pretty docWithFilteredPriorityRecorder, logger_) + pure (cmapWithPrio pretty docWithFilteredPriorityRecorder) -- ------------------------------------------------------------ -- Run an HLS server testing a specific plugin -- ------------------------------------------------------------ -runSessionWithServer :: Pretty b => Config -> PluginTestDescriptor b -> FilePath -> Session a -> IO a -runSessionWithServer config plugin fp act = do - recorder <- pluginTestRecorder - runSessionWithServer' (plugin recorder) config def fullCaps fp act - -runSessionWithServerAndCaps :: Pretty b => Config -> PluginTestDescriptor b -> ClientCapabilities -> FilePath -> Session a -> IO a -runSessionWithServerAndCaps config plugin caps fp act = do - recorder <- pluginTestRecorder - runSessionWithServer' (plugin recorder) config def caps fp act - runSessionWithServerInTmpDir :: Pretty b => Config -> PluginTestDescriptor b -> VirtualFileTree -> Session a -> IO a -runSessionWithServerInTmpDir config plugin tree act = do - recorder <- pluginTestRecorder - runSessionWithServerInTmpDir' (plugin recorder) config def fullCaps tree act - -runSessionWithServerAndCapsInTmpDir :: Pretty b => Config -> PluginTestDescriptor b -> ClientCapabilities -> VirtualFileTree -> Session a -> IO a -runSessionWithServerAndCapsInTmpDir config plugin caps tree act = do - recorder <- pluginTestRecorder - runSessionWithServerInTmpDir' (plugin recorder) config def caps tree act +runSessionWithServerInTmpDir config plugin tree act = + runSessionWithTestConfig def + {testLspConfig=config, testPluginDescriptor = plugin, testDirLocation=Right tree} + (const act) + +runWithLockInTempDir :: VirtualFileTree -> (FileSystem -> IO a) -> IO a +runWithLockInTempDir tree act = withLock lockForTempDirs $ do + testRoot <- setupTestEnvironment + helperRecorder <- hlsHelperTestRecorder + -- Do not clean up the temporary directory if this variable is set to anything but '0'. + -- Aids debugging. + cleanupTempDir <- lookupEnv "HLS_TEST_HARNESS_NO_TESTDIR_CLEANUP" + let runTestInDir action = case cleanupTempDir of + Just val | val /= "0" -> do + (tempDir, _) <- newTempDirWithin testRoot + a <- action tempDir + logWith helperRecorder Debug LogNoCleanup + pure a + + _ -> do + (tempDir, cleanup) <- newTempDirWithin testRoot + a <- action tempDir `finally` cleanup + logWith helperRecorder Debug LogCleanup + pure a + runTestInDir $ \tmpDir' -> do + -- we canonicalize the path, so that we do not need to do + -- cannibalization during the test when we compare two paths + tmpDir <- canonicalizePath tmpDir' + logWith helperRecorder Info $ LogTestDir tmpDir + fs <- FS.materialiseVFT tmpDir tree + act fs --- | Host a server, and run a test session on it. --- --- Creates a temporary directory, and materializes the VirtualFileTree --- in the temporary directory. --- --- To debug test cases and verify the file system is correctly set up, --- you should set the environment variable 'HLS_TEST_HARNESS_NO_TESTDIR_CLEANUP=1'. --- Further, we log the temporary directory location on startup. To view --- the logs, set the environment variable 'HLS_TEST_HARNESS_STDERR=1'. --- --- Example invocation to debug test cases: --- --- @ --- HLS_TEST_HARNESS_NO_TESTDIR_CLEANUP=1 HLS_TEST_HARNESS_STDERR=1 cabal test --- @ --- --- Don't forget to use 'TASTY_PATTERN' to debug only a subset of tests. +runSessionWithServer :: Pretty b => Config -> PluginTestDescriptor b -> FilePath -> Session a -> IO a +runSessionWithServer config plugin fp act = + runSessionWithTestConfig def { + testLspConfig=config + , testPluginDescriptor=plugin + , testDirLocation = Left fp + } (const act) + + +instance Default (TestConfig b) where + def = TestConfig { + testDirLocation = Right $ VirtualFileTree [] "", + testClientRoot = Nothing, + testServerRoot = Nothing, + testShiftRoot = False, + testDisableKick = False, + testDisableDefaultPlugin = False, + testPluginDescriptor = mempty, + testLspConfig = def, + testConfigSession = def, + testConfigCaps = fullLatestClientCaps, + testCheckProject = False + } + +-- | Setup the test environment for isolated tests. -- --- For plugin test logs, look at the documentation of 'mkPluginTestDescriptor'. +-- This creates a directory in the temporary directory that will be +-- reused for running isolated tests. +-- It returns the root to the testing directory that tests should use. +-- This directory is not fully cleaned between reruns. +-- However, it is totally safe to delete the directory between runs. -- --- Note: cwd will be shifted into a temporary directory in @Session a@ -runSessionWithServerInTmpDir' :: - -- | Plugins to load on the server. - -- - -- For improved logging, make sure these plugins have been initalised with - -- the recorder produced by @pluginTestRecorder@. - IdePlugins IdeState -> - -- | lsp config for the server - Config -> - -- | config for the test session - SessionConfig -> - ClientCapabilities -> - VirtualFileTree -> - Session a -> - IO a -runSessionWithServerInTmpDir' plugins conf sessConf caps tree act = withLock lockForTempDirs $ do - (recorder, _) <- initialiseTestRecorder - ["LSP_TEST_LOG_STDERR", "HLS_TEST_HARNESS_STDERR", "HLS_TEST_LOG_STDERR"] - - -- Do not clean up the temporary directory if this variable is set to anything but '0'. - -- Aids debugging. - cleanupTempDir <- lookupEnv "HLS_TEST_HARNESS_NO_TESTDIR_CLEANUP" - let runTestInDir = case cleanupTempDir of - Just val - | val /= "0" -> \action -> do - (tempDir, _) <- newTempDir - a <- action tempDir - logWith recorder Debug $ LogNoCleanup - pure a - - _ -> \action -> do - a <- withTempDir action - logWith recorder Debug $ LogCleanup - pure a - - runTestInDir $ \tmpDir -> do - logWith recorder Info $ LogTestDir tmpDir - _fs <- FS.materialiseVFT tmpDir tree - runSessionWithServer' plugins conf sessConf caps tmpDir act +-- Additionally, this overwrites the 'XDG_CACHE_HOME' variable to isolate +-- the tests from existing caches. 'hie-bios' and 'ghcide' honour the +-- 'XDG_CACHE_HOME' environment variable and generate their caches there. +setupTestEnvironment :: IO FilePath +setupTestEnvironment = do + tmpDirRoot <- getTemporaryDirectory + let testRoot = tmpDirRoot "hls-test-root" + testCacheDir = testRoot ".cache" + createDirectoryIfMissing True testCacheDir + setEnv "XDG_CACHE_HOME" testCacheDir + pure testRoot goldenWithHaskellDocFormatter :: Pretty b @@ -564,64 +689,93 @@ lock = unsafePerformIO newLock lockForTempDirs :: Lock lockForTempDirs = unsafePerformIO newLock --- | Host a server, and run a test session on it --- Note: cwd will be shifted into @root@ in @Session a@ -runSessionWithServer' :: - -- | Plugins to load on the server. - -- - -- For improved logging, make sure these plugins have been initalised with - -- the recorder produced by @pluginTestRecorder@. - IdePlugins IdeState -> - -- | lsp config for the server - Config -> - -- | config for the test session - SessionConfig -> - ClientCapabilities -> - FilePath -> - Session a -> - IO a -runSessionWithServer' plugins conf sconf caps root s = withLock lock $ keepCurrentDirectory $ do +data TestConfig b = TestConfig + { + testDirLocation :: Either FilePath VirtualFileTree + -- ^ Client capabilities + -- ^ The file tree to use for the test, either a directory or a virtual file tree + -- if using a virtual file tree, + -- Creates a temporary directory, and materializes the VirtualFileTree + -- in the temporary directory. + -- + -- To debug test cases and verify the file system is correctly set up, + -- you should set the environment variable 'HLS_TEST_HARNESS_NO_TESTDIR_CLEANUP=1'. + -- Further, we log the temporary directory location on startup. To view + -- the logs, set the environment variable 'HLS_TEST_HARNESS_STDERR=1'. + -- Example invocation to debug test cases: + -- + -- @ + -- HLS_TEST_HARNESS_NO_TESTDIR_CLEANUP=1 HLS_TEST_HARNESS_STDERR=1 cabal test + -- @ + -- + -- Don't forget to use 'TASTY_PATTERN' to debug only a subset of tests. + -- + -- For plugin test logs, look at the documentation of 'mkPluginTestDescriptor'. + , testShiftRoot :: Bool + -- ^ Whether to shift the current directory to the root of the project + , testClientRoot :: Maybe FilePath + -- ^ Specify the root of (the client or LSP context), + -- if Nothing it is the same as the testDirLocation + -- if Just, it is subdirectory of the testDirLocation + , testServerRoot :: Maybe FilePath + -- ^ Specify root of the server, in exe, it can be specify in command line --cwd, + -- or just the server start directory + -- if Nothing it is the same as the testDirLocation + -- if Just, it is subdirectory of the testDirLocation + , testDisableKick :: Bool + -- ^ Whether to disable the kick action + , testDisableDefaultPlugin :: Bool + -- ^ Whether to disable the default plugin comes with ghcide + , testCheckProject :: Bool + -- ^ Whether to typecheck check the project after the session is loaded + , testPluginDescriptor :: PluginTestDescriptor b + -- ^ Plugin to load on the server. + , testLspConfig :: Config + -- ^ lsp config for the server + , testConfigSession :: SessionConfig + -- ^ config for the test session + , testConfigCaps :: ClientCapabilities + -- ^ Client capabilities + } + + +wrapClientLogger :: Pretty a => Recorder (WithPriority a) -> + IO (Recorder (WithPriority a), LSP.LanguageContextEnv Config -> IO ()) +wrapClientLogger logger = do + (lspLogRecorder', cb1) <- Logger.withBacklog Logger.lspClientLogRecorder + let lspLogRecorder = cmapWithPrio (renderStrict . layoutPretty defaultLayoutOptions. pretty) lspLogRecorder' + return (lspLogRecorder <> logger, cb1) + +-- | Host a server, and run a test session on it. +-- For setting custom timeout, set the environment variable 'LSP_TIMEOUT' +-- * LSP_TIMEOUT=10 cabal test +-- For more detail of the test configuration, see 'TestConfig' +runSessionWithTestConfig :: Pretty b => TestConfig b -> (FilePath -> Session a) -> IO a +runSessionWithTestConfig TestConfig{..} session = + runSessionInVFS testDirLocation $ \root -> shiftRoot root $ do (inR, inW) <- createPipe (outR, outW) <- createPipe - - -- Allow three environment variables, because "LSP_TEST_LOG_STDERR" has been used before, - -- (thus, backwards compatibility) and "HLS_TEST_SERVER_LOG_STDERR" because it - -- uses a more descriptive name. - -- It is also in better accordance with 'pluginTestRecorder' which uses "HLS_TEST_PLUGIN_LOG_STDERR". - -- At last, "HLS_TEST_LOG_STDERR" is intended to enable all logging for the server and the plugins - -- under test. - (recorder, logger_) <- initialiseTestRecorder - ["LSP_TEST_LOG_STDERR", "HLS_TEST_SERVER_LOG_STDERR", "HLS_TEST_LOG_STDERR"] - - let - sconf' = sconf { lspConfig = hlsConfigToClientConfig conf } - -- exists until old logging style is phased out - logger = Logger $ \p m -> logger_ (WithPriority p emptyCallStack (pretty m)) - - hlsPlugins = IdePlugins [Test.blockCommandDescriptor "block-command"] <> plugins - - arguments@Arguments{ argsIdeOptions, argsLogger } = - testing (cmapWithPrio LogIDEMain recorder) logger hlsPlugins - - ideOptions config ghcSession = - let defIdeOptions = argsIdeOptions config ghcSession - in defIdeOptions - { optTesting = IdeTesting True - , optCheckProject = pure False - } - + let serverRoot = fromMaybe root testServerRoot + let clientRoot = fromMaybe root testClientRoot + + (recorder, cb1) <- wrapClientLogger =<< hlsPluginTestRecorder + (recorderIde, cb2) <- wrapClientLogger =<< hlsHelperTestRecorder + -- This plugin just installs a handler for the `initialized` notification, which then + -- picks up the LSP environment and feeds it to our recorders + let lspRecorderPlugin = pluginDescToIdePlugins [(defaultPluginDescriptor "LSPRecorderCallback" "Internal plugin") + { pluginNotificationHandlers = mkPluginNotificationHandler LSP.SMethod_Initialized $ \_ _ _ _ -> do + env <- LSP.getLspEnv + liftIO $ (cb1 <> cb2) env + }] + + let plugins = testPluginDescriptor recorder <> lspRecorderPlugin + timeoutOverride <- fmap read <$> lookupEnv "LSP_TIMEOUT" + let sconf' = testConfigSession { lspConfig = hlsConfigToClientConfig testLspConfig, messageTimeout = fromMaybe (messageTimeout defaultConfig) timeoutOverride} + arguments = testingArgs serverRoot recorderIde plugins server <- async $ - Ghcide.defaultMain (cmapWithPrio LogIDEMain recorder) - arguments - { argsHandleIn = pure inR - , argsHandleOut = pure outW - , argsDefaultHlsConfig = conf - , argsLogger = argsLogger - , argsIdeOptions = ideOptions - , argsProjectRoot = Just root - } - - x <- runSessionWithHandles inW outR sconf' caps root s + IDEMain.defaultMain (cmapWithPrio LogIDEMain recorderIde) + arguments { argsHandleIn = pure inR , argsHandleOut = pure outW } + result <- runSessionWithHandles inW outR sconf' testConfigCaps clientRoot (session root) hClose inW timeout 3 (wait server) >>= \case Just () -> pure () @@ -629,7 +783,44 @@ runSessionWithServer' plugins conf sconf caps root s = withLock lock $ keepCurr putStrLn "Server does not exit in 3s, canceling the async task..." (t, _) <- duration $ cancel server putStrLn $ "Finishing canceling (took " <> showDuration t <> "s)" - pure x + pure result + + where + shiftRoot shiftTarget f = + if testShiftRoot + then withLock lock $ keepCurrentDirectory $ setCurrentDirectory shiftTarget >> f + else f + runSessionInVFS (Left testConfigRoot) act = do + root <- makeAbsolute testConfigRoot + act root + runSessionInVFS (Right vfs) act = runWithLockInTempDir vfs $ \fs -> act (fsRoot fs) + testingArgs prjRoot recorderIde plugins = + let + arguments@Arguments{ argsHlsPlugins, argsIdeOptions, argsLspOptions } = defaultArguments (cmapWithPrio LogIDEMain recorderIde) prjRoot plugins + argsHlsPlugins' = if testDisableDefaultPlugin + then plugins + else argsHlsPlugins + hlsPlugins = pluginDescToIdePlugins $ idePluginsToPluginDesc argsHlsPlugins' + ++ [Test.blockCommandDescriptor "block-command", Test.plugin] + ideOptions config sessionLoader = (argsIdeOptions config sessionLoader){ + optTesting = IdeTesting True + , optCheckProject = pure testCheckProject + } + in + arguments + { argsHlsPlugins = hlsPlugins + , argsIdeOptions = ideOptions + , argsLspOptions = argsLspOptions { LSP.optProgressStartDelay = 0, LSP.optProgressUpdateDelay = 0 } + , argsDefaultHlsConfig = testLspConfig + , argsProjectRoot = prjRoot + , argsDisableKick = testDisableKick + } + +-- | Wait for the next progress begin step +waitForProgressBegin :: Session () +waitForProgressBegin = skipManyTill anyMessage $ satisfyMaybe $ \case + FromServerMess SMethod_Progress (TNotificationMessage _ _ (ProgressParams _ v)) | is _workDoneProgressBegin v-> Just () + _ -> Nothing -- | Wait for the next progress end step waitForProgressDone :: Session () @@ -660,7 +851,7 @@ waitForBuildQueue = do -- assume a ghcide binary lacking the WaitForShakeQueue method _ -> return 0 -callTestPlugin :: (A.FromJSON b) => TestRequest -> Session (Either ResponseError b) +callTestPlugin :: (A.FromJSON b) => TestRequest -> Session (Either (TResponseError @ClientToServer (Method_CustomMethod "test")) b) callTestPlugin cmd = do let cm = SMethod_CustomMethod (Proxy @"test") waitId <- sendRequest cm (A.toJSON cmd) @@ -668,17 +859,17 @@ callTestPlugin cmd = do return $ do e <- _result case A.fromJSON e of - A.Error err -> Left $ ResponseError (InR ErrorCodes_InternalError) (T.pack err) Nothing + A.Error err -> Left $ TResponseError (InR ErrorCodes_InternalError) (T.pack err) Nothing A.Success a -> pure a -waitForAction :: String -> TextDocumentIdentifier -> Session (Either ResponseError WaitForIdeRuleResult) +waitForAction :: String -> TextDocumentIdentifier -> Session (Either (TResponseError @ClientToServer (Method_CustomMethod "test")) WaitForIdeRuleResult) waitForAction key TextDocumentIdentifier{_uri} = callTestPlugin (WaitForIdeRule key _uri) -waitForTypecheck :: TextDocumentIdentifier -> Session (Either ResponseError Bool) +waitForTypecheck :: TextDocumentIdentifier -> Session (Either (TResponseError @ClientToServer (Method_CustomMethod "test")) Bool) waitForTypecheck tid = fmap ideResultSuccess <$> waitForAction "typecheck" tid -getLastBuildKeys :: Session (Either ResponseError [T.Text]) +getLastBuildKeys :: Session (Either (TResponseError @ClientToServer (Method_CustomMethod "test")) [T.Text]) getLastBuildKeys = callTestPlugin GetBuildKeysBuilt hlsConfigToClientConfig :: Config -> A.Object @@ -695,6 +886,17 @@ setHlsConfig config = do -- requests! skipManyTill anyMessage (void configurationRequest) +captureKickDiagnostics :: Session () -> Session () -> Session [Diagnostic] +captureKickDiagnostics start done = do + _ <- skipManyTill anyMessage start + messages <- manyTill anyMessage done + pure $ concat $ mapMaybe diagnostics messages + where + diagnostics :: FromServerMessage' a -> Maybe [Diagnostic] + diagnostics = \msg -> case msg of + FromServerMess SMethod_TextDocumentPublishDiagnostics diags -> Just (diags ^. L.params . L.diagnostics) + _ -> Nothing + waitForKickDone :: Session () waitForKickDone = void $ skipManyTill anyMessage nonTrivialKickDone @@ -707,6 +909,7 @@ nonTrivialKickDone = kick (Proxy @"kick/done") >>= guard . not . null nonTrivialKickStart :: Session () nonTrivialKickStart = kick (Proxy @"kick/start") >>= guard . not . null + kick :: KnownSymbol k => Proxy k -> Session [FilePath] kick proxyMsg = do NotMess TNotificationMessage{_params} <- customNotification proxyMsg diff --git a/hls-test-utils/src/Test/Hls/FileSystem.hs b/hls-test-utils/src/Test/Hls/FileSystem.hs index b6742c4b83..c93643badd 100644 --- a/hls-test-utils/src/Test/Hls/FileSystem.hs +++ b/hls-test-utils/src/Test/Hls/FileSystem.hs @@ -20,6 +20,7 @@ module Test.Hls.FileSystem , directory , text , ref + , copyDir -- * Cradle helpers , directCradle , simpleCabalCradle @@ -37,6 +38,7 @@ import Development.IDE (NormalizedFilePath) import Language.LSP.Protocol.Types (toNormalizedFilePath) import System.Directory import System.FilePath as FP +import System.Process.Extra (readProcess) -- ---------------------------------------------------------------------------- -- Top Level definitions @@ -64,8 +66,9 @@ data VirtualFileTree = } deriving (Eq, Ord, Show) data FileTree - = File FilePath Content - | Directory FilePath [FileTree] + = File FilePath Content -- ^ Create a file with the given content. + | Directory FilePath [FileTree] -- ^ Create a directory with the given files. + | CopiedDirectory FilePath -- ^ Copy a directory from the test data dir. deriving (Show, Eq, Ord) data Content @@ -99,12 +102,21 @@ materialise rootDir' fileTree testDataDir' = do rootDir = FP.normalise rootDir' persist :: FilePath -> FileTree -> IO () - persist fp (File name cts) = case cts of - Inline txt -> T.writeFile (fp name) txt - Ref path -> copyFile (testDataDir FP.normalise path) (fp takeFileName name) - persist fp (Directory name nodes) = do - createDirectory (fp name) - mapM_ (persist (fp name)) nodes + persist root (File name cts) = case cts of + Inline txt -> T.writeFile (root name) txt + Ref path -> copyFile (testDataDir FP.normalise path) (root takeFileName name) + persist root (Directory name nodes) = do + createDirectory (root name) + mapM_ (persist (root name)) nodes + persist root (CopiedDirectory name) = do + copyDir' root name + + copyDir' :: FilePath -> FilePath -> IO () + copyDir' root dir = do + files <- fmap FP.normalise . lines <$> withCurrentDirectory (testDataDir dir) (readProcess "git" ["ls-files", "--cached", "--modified", "--others"] "") + mapM_ (createDirectoryIfMissing True . ((root ) . takeDirectory)) files + mapM_ (\f -> copyFile (testDataDir dir f) (root f)) files + return () traverse_ (persist rootDir) fileTree pure $ FileSystem rootDir fileTree testDataDir @@ -115,8 +127,7 @@ materialise rootDir' fileTree testDataDir' = do -- -- File references in 'virtualFileTree' are resolved relative to the @vftOriginalRoot@. materialiseVFT :: FilePath -> VirtualFileTree -> IO FileSystem -materialiseVFT root fs = - materialise root (vftTree fs) (vftOriginalRoot fs) +materialiseVFT root fs = materialise root (vftTree fs) (vftOriginalRoot fs) -- ---------------------------------------------------------------------------- -- Test definition helpers @@ -154,6 +165,11 @@ file fp cts = File fp cts copy :: FilePath -> FileTree copy fp = File fp (Ref fp) +-- | Copy a directory into a test project. +-- The filepath is always resolved to the root of the test data dir. +copyDir :: FilePath -> FileTree +copyDir dir = CopiedDirectory dir + directory :: FilePath -> [FileTree] -> FileTree directory name nodes = Directory name nodes diff --git a/hls-test-utils/src/Test/Hls/Util.hs b/hls-test-utils/src/Test/Hls/Util.hs index 4f0c400a18..98c795f8e0 100644 --- a/hls-test-utils/src/Test/Hls/Util.hs +++ b/hls-test-utils/src/Test/Hls/Util.hs @@ -1,17 +1,13 @@ {-# LANGUAGE CPP #-} +{-# LANGUAGE DataKinds #-} {-# LANGUAGE DuplicateRecordFields #-} -{-# LANGUAGE FlexibleContexts #-} {-# LANGUAGE GADTs #-} -{-# LANGUAGE MultiParamTypeClasses #-} {-# LANGUAGE OverloadedStrings #-} -{-# LANGUAGE TypeOperators #-} -{-# LANGUAGE OverloadedLabels #-} -{-# LANGUAGE TypeApplications #-} -{-# LANGUAGE DataKinds #-} module Test.Hls.Util ( -- * Test Capabilities codeActionResolveCaps , codeActionNoResolveCaps + , codeActionNoInlayHintsCaps , codeActionSupportCaps , expectCodeAction -- * Environment specifications @@ -34,14 +30,13 @@ module Test.Hls.Util , dontExpectCodeAction , expectDiagnostic , expectNoMoreDiagnostics - , expectSameLocations , failIfSessionTimeout , getCompletionByLabel , noLiteralCaps , inspectCodeAction , inspectCommand , inspectDiagnostic - , SymbolLocation + , inspectDiagnosticAny , waitForDiagnosticsFrom , waitForDiagnosticsFromSource , waitForDiagnosticsFromSourceWithTimeout @@ -49,38 +44,48 @@ module Test.Hls.Util , withCurrentDirectoryInTmp , withCurrentDirectoryInTmp' , withCanonicalTempDir + -- * Extract positions from input file. + , extractCursorPositions + , mkParameterisedLabel + , trimming ) where -import Control.Applicative.Combinators (skipManyTill, (<|>)) -import Control.Exception (catch, throwIO) -import Control.Lens ((&), (?~), (^.), _Just, (.~)) +import Control.Applicative.Combinators (skipManyTill, (<|>)) +import Control.Exception (catch, throwIO) +import Control.Lens (_Just, (&), (.~), + (?~), (^.)) import Control.Monad import Control.Monad.IO.Class -import qualified Data.Aeson as A -import Data.Bool (bool) +import qualified Data.Aeson as A +import Data.Bool (bool) import Data.Default -import Data.Row +import Data.List.Extra (find) import Data.Proxy -import Data.List.Extra (find) -import qualified Data.Set as Set -import qualified Data.Text as T -import Development.IDE (GhcVersion (..), ghcVersion) -import qualified Language.LSP.Test as Test -import Language.LSP.Protocol.Types +import qualified Data.Text as T +import Development.IDE (GhcVersion (..), + ghcVersion) +import qualified Language.LSP.Protocol.Lens as L import Language.LSP.Protocol.Message -import qualified Language.LSP.Protocol.Lens as L +import Language.LSP.Protocol.Types +import qualified Language.LSP.Test as Test import System.Directory import System.FilePath -import System.Info.Extra (isMac, isWindows) +import System.Info.Extra (isMac, isWindows) import qualified System.IO.Extra import System.IO.Temp -import System.Time.Extra (Seconds, sleep) -import Test.Tasty (TestTree) -import Test.Tasty.ExpectedFailure (expectFailBecause, - ignoreTestBecause) -import Test.Tasty.HUnit (Assertion, assertFailure, - (@?=)) +import System.Time.Extra (Seconds, sleep) +import Test.Tasty (TestTree) +import Test.Tasty.ExpectedFailure (expectFailBecause, + ignoreTestBecause) +import Test.Tasty.HUnit (assertFailure) + +import qualified Data.List as List +import qualified Data.Text.Internal.Search as T +import qualified Data.Text.Utf16.Rope.Mixed as Rope +import Development.IDE.Plugin.Completions.Logic (getCompletionPrefixFromRope) +import Development.IDE.Plugin.Completions.Types (PosPrefixInfo (..)) +import NeatInterpolation (trimming) noLiteralCaps :: ClientCapabilities noLiteralCaps = def & L.textDocument ?~ textDocumentCaps @@ -93,17 +98,23 @@ codeActionSupportCaps = def & L.textDocument ?~ textDocumentCaps where textDocumentCaps = def { _codeAction = Just codeActionCaps } codeActionCaps = CodeActionClientCapabilities (Just True) (Just literalSupport) (Just True) Nothing Nothing Nothing Nothing - literalSupport = #codeActionKind .== (#valueSet .== []) + literalSupport = ClientCodeActionLiteralOptions (ClientCodeActionKindOptions []) codeActionResolveCaps :: ClientCapabilities -codeActionResolveCaps = Test.fullCaps - & (L.textDocument . _Just . L.codeAction . _Just . L.resolveSupport . _Just) .~ (#properties .== ["edit"]) +codeActionResolveCaps = Test.fullLatestClientCaps + & (L.textDocument . _Just . L.codeAction . _Just . L.resolveSupport . _Just) .~ ClientCodeActionResolveOptions {_properties= ["edit"]} & (L.textDocument . _Just . L.codeAction . _Just . L.dataSupport . _Just) .~ True codeActionNoResolveCaps :: ClientCapabilities -codeActionNoResolveCaps = Test.fullCaps +codeActionNoResolveCaps = Test.fullLatestClientCaps + & (L.textDocument . _Just . L.codeAction . _Just . L.resolveSupport) .~ Nothing + & (L.textDocument . _Just . L.codeAction . _Just . L.dataSupport . _Just) .~ False + +codeActionNoInlayHintsCaps :: ClientCapabilities +codeActionNoInlayHintsCaps = Test.fullLatestClientCaps & (L.textDocument . _Just . L.codeAction . _Just . L.resolveSupport) .~ Nothing & (L.textDocument . _Just . L.codeAction . _Just . L.dataSupport . _Just) .~ False + & (L.textDocument . _Just . L.inlayHint) .~ Nothing -- --------------------------------------------------------------------- -- Environment specification for ignoring tests -- --------------------------------------------------------------------- @@ -237,6 +248,10 @@ inspectDiagnostic :: [Diagnostic] -> [T.Text] -> IO Diagnostic inspectDiagnostic diags s = onMatch diags (\ca -> all (`T.isInfixOf` (ca ^. L.message)) s) err where err = "expected diagnostic matching '" ++ show s ++ "' but did not find one" +inspectDiagnosticAny :: [Diagnostic] -> [T.Text] -> IO Diagnostic +inspectDiagnosticAny diags s = onMatch diags (\ca -> any (`T.isInfixOf` (ca ^. L.message)) s) err + where err = "expected diagnostic matching one of'" ++ show s ++ "' but did not find one" + expectDiagnostic :: [Diagnostic] -> [T.Text] -> IO () expectDiagnostic diags s = void $ inspectDiagnostic diags s @@ -304,7 +319,7 @@ waitForDiagnosticsFromSourceWithTimeout timeout document source = do handleDiagnostic testId = do diagsNot <- Test.message SMethod_TextDocumentPublishDiagnostics let fileUri = diagsNot ^. L.params . L.uri - ( diags) = diagsNot ^. L.params . L.diagnostics + diags = diagsNot ^. L.params . L.diagnostics res = filter matches diags if fileUri == document ^. L.uri && not (null res) then return res else handleMessages testId @@ -320,23 +335,6 @@ failIfSessionTimeout action = action `catch` errorHandler errorHandler e@(Test.Timeout _) = assertFailure $ show e errorHandler e = throwIO e --- | To locate a symbol, we provide a path to the file from the HLS root --- directory, the line number, and the column number. (0 indexed.) -type SymbolLocation = (FilePath, UInt, UInt) - -expectSameLocations :: [Location] -> [SymbolLocation] -> Assertion -actual `expectSameLocations` expected = do - let actual' = - Set.map (\location -> (location ^. L.uri - , location ^. L.range . L.start . L.line - , location ^. L.range . L.start . L.character)) - $ Set.fromList actual - expected' <- Set.fromList <$> - (forM expected $ \(file, l, c) -> do - fp <- canonicalizePath file - return (filePathToUri fp, l, c)) - actual' @?= expected' - -- --------------------------------------------------------------------- getCompletionByLabel :: MonadIO m => T.Text -> [CompletionItem] -> m CompletionItem getCompletionByLabel desiredLabel compls = @@ -352,3 +350,119 @@ withCanonicalTempDir :: (FilePath -> IO a) -> IO a withCanonicalTempDir f = System.IO.Extra.withTempDir $ \dir -> do dir' <- canonicalizePath dir f dir' + +-- ---------------------------------------------------------------------------- +-- Extract Position data from the source file itself. +-- ---------------------------------------------------------------------------- + +-- | Pretty labelling for tests that use the parameterised test helpers. +mkParameterisedLabel :: PosPrefixInfo -> String +mkParameterisedLabel posPrefixInfo = unlines + [ "Full Line: \"" <> T.unpack (fullLine posPrefixInfo) <> "\"" + , "Cursor Column: \"" <> replicate (fromIntegral $ cursorPos posPrefixInfo ^. L.character) ' ' ++ "^" <> "\"" + , "Prefix Text: \"" <> T.unpack (prefixText posPrefixInfo) <> "\"" + ] + +-- | Given a in-memory representation of a file, where a user can specify the +-- current cursor position using a '^' in the next line. +-- +-- This function allows to generate multiple tests for a single input file, without +-- the hassle of calculating by hand where there cursor is supposed to be. +-- +-- Example (line number has been added for readability): +-- +-- @ +-- 0: foo = 2 +-- 1: ^ +-- 2: bar = +-- 3: ^ +-- @ +-- +-- This example input file contains two cursor positions (y, x), at +-- +-- * (1, 1), and +-- * (3, 5). +-- +-- 'extractCursorPositions' will search for '^' characters, and determine there are +-- two cursor positions in the text. +-- First, it will normalise the text to: +-- +-- @ +-- 0: foo = 2 +-- 1: bar = +-- @ +-- +-- stripping away the '^' characters. Then, the actual cursor positions are: +-- +-- * (0, 1) and +-- * (2, 5). +-- +extractCursorPositions :: T.Text -> (T.Text, [PosPrefixInfo]) +extractCursorPositions t = + let + textLines = T.lines t + foldState = List.foldl' go emptyFoldState textLines + finalText = foldStateToText foldState + reconstructCompletionPrefix pos = getCompletionPrefixFromRope pos (Rope.fromText finalText) + cursorPositions = reverse . fmap reconstructCompletionPrefix $ foldStatePositions foldState + in + (finalText, cursorPositions) + + where + go foldState l = case T.indices "^" l of + [] -> addTextLine foldState l + xs -> List.foldl' addTextCursor foldState xs + +-- | 'FoldState' is an implementation detail used to parse some file contents, +-- extracting the cursor positions identified by '^' and producing a cleaned +-- representation of the file contents. +data FoldState = FoldState + { foldStateRows :: !Int + -- ^ The row index of the cleaned file contents. + -- + -- For example, the file contents + -- + -- @ + -- 0: foo + -- 1: ^ + -- 2: bar + -- @ + -- will report that 'bar' is actually occurring in line '1', as '^' is + -- a cursor position. + -- Lines containing cursor positions are removed. + , foldStatePositions :: ![Position] + -- ^ List of cursors positions found in the file contents. + -- + -- List is stored in reverse for efficient 'cons'ing + , foldStateFinalText :: ![T.Text] + -- ^ Final file contents with all lines containing cursor positions removed. + -- + -- List is stored in reverse for efficient 'cons'ing + } + +emptyFoldState :: FoldState +emptyFoldState = FoldState + { foldStateRows = 0 + , foldStatePositions = [] + , foldStateFinalText = [] + } + +-- | Produce the final file contents, without any lines containing cursor positions. +foldStateToText :: FoldState -> T.Text +foldStateToText state = T.unlines $ reverse $ foldStateFinalText state + +-- | We found a '^' at some location! Add it to the list of known cursor positions. +-- +-- If the row index is '0', we throw an error, as there can't be a cursor position above the first line. +addTextCursor :: FoldState -> Int -> FoldState +addTextCursor state col + | foldStateRows state <= 0 = error $ "addTextCursor: Invalid '^' found at: " <> show (col, foldStateRows state) + | otherwise = state + { foldStatePositions = Position (fromIntegral (foldStateRows state) - 1) (fromIntegral col) : foldStatePositions state + } + +addTextLine :: FoldState -> T.Text -> FoldState +addTextLine state l = state + { foldStateFinalText = l : foldStateFinalText state + , foldStateRows = foldStateRows state + 1 + } diff --git a/plugins/hls-alternate-number-format-plugin/hls-alternate-number-format-plugin.cabal b/plugins/hls-alternate-number-format-plugin/hls-alternate-number-format-plugin.cabal deleted file mode 100644 index beca02f17d..0000000000 --- a/plugins/hls-alternate-number-format-plugin/hls-alternate-number-format-plugin.cabal +++ /dev/null @@ -1,76 +0,0 @@ -cabal-version: 2.4 -name: hls-alternate-number-format-plugin -version: 2.4.0.0 -synopsis: Provide Alternate Number Formats plugin for Haskell Language Server -description: - Please see the README on GitHub at - -license: Apache-2.0 -license-file: LICENSE -author: Nick Suchecki -maintainer: nicksuchecki@gmail.com -category: Development -build-type: Simple -extra-source-files: - LICENSE - README.md - test/testdata/*.hs - test/testdata/*.yaml - -source-repository head - type: git - location: https://github.com/haskell/haskell-language-server.git - -library - exposed-modules: Ide.Plugin.AlternateNumberFormat, Ide.Plugin.Conversion - other-modules: Ide.Plugin.Literals - hs-source-dirs: src - ghc-options: -Wall - build-depends: - aeson - , base >=4.12 && < 5 - , containers - , extra - , ghcide == 2.4.0.0 - , ghc-boot-th - , hls-graph - , hls-plugin-api == 2.4.0.0 - , hie-compat - , lens - , lsp ^>=2.3.0.0 - , mtl - , regex-tdfa - , syb - , text - , unordered-containers - - default-language: Haskell2010 - default-extensions: - LambdaCase - NamedFieldPuns - OverloadedStrings - RecordWildCards - -test-suite tests - type: exitcode-stdio-1.0 - default-language: Haskell2010 - hs-source-dirs: test - other-modules: Properties.Conversion - main-is: Main.hs - ghc-options: -threaded -rtsopts -with-rtsopts=-N -fno-ignore-asserts - build-depends: - , base >=4.12 && < 5 - , filepath - , hls-alternate-number-format-plugin - , hls-test-utils == 2.4.0.0 - , lsp - , QuickCheck - , regex-tdfa - , tasty-quickcheck - , text - - default-extensions: - LambdaCase - NamedFieldPuns - OverloadedStrings - RecordWildCards diff --git a/plugins/hls-alternate-number-format-plugin/src/Ide/Plugin/AlternateNumberFormat.hs b/plugins/hls-alternate-number-format-plugin/src/Ide/Plugin/AlternateNumberFormat.hs index 3986ad835b..3b00d79d1b 100644 --- a/plugins/hls-alternate-number-format-plugin/src/Ide/Plugin/AlternateNumberFormat.hs +++ b/plugins/hls-alternate-number-format-plugin/src/Ide/Plugin/AlternateNumberFormat.hs @@ -1,19 +1,17 @@ -{-# LANGUAGE DataKinds #-} -{-# LANGUAGE DeriveGeneric #-} -{-# LANGUAGE TypeFamilies #-} -{-# LANGUAGE TypeOperators #-} +{-# LANGUAGE DataKinds #-} +{-# LANGUAGE TypeFamilies #-} module Ide.Plugin.AlternateNumberFormat (descriptor, Log(..)) where import Control.Lens ((^.)) import Control.Monad.Except (ExceptT) -import Control.Monad.IO.Class (MonadIO, liftIO) +import Control.Monad.IO.Class (MonadIO) import qualified Data.Map as Map import Data.Text (Text, unpack) import qualified Data.Text as T import Development.IDE (GetParsedModule (GetParsedModule), IdeState, RuleResult, Rules, define, realSrcSpanToRange, - runAction, use) + use) import Development.IDE.Core.PluginUtils import qualified Development.IDE.Core.Shake as Shake import Development.IDE.GHC.Compat hiding (getSrcSpan) @@ -36,15 +34,14 @@ import qualified Language.LSP.Protocol.Lens as L import Language.LSP.Protocol.Message import Language.LSP.Protocol.Types - newtype Log = LogShake Shake.Log deriving Show instance Pretty Log where pretty = \case - LogShake log -> pretty log + LogShake msg -> pretty msg descriptor :: Recorder (WithPriority Log) -> PluginId -> PluginDescriptor IdeState -descriptor recorder pId = (defaultPluginDescriptor pId) +descriptor recorder pId = (defaultPluginDescriptor pId "Provides code actions to convert numeric literals to different formats") { pluginHandlers = mkPluginHandler SMethod_TextDocumentCodeAction codeActionHandler , pluginRules = collectLiteralsRule recorder } @@ -93,7 +90,7 @@ codeActionHandler state pId (CodeActionParams _ _ docId currRange _) = do literalPairs = map (\lit -> (lit, alternateFormat lit)) litsInRange -- make a code action for every literal and its' alternates (then flatten the result) actions = concatMap (\(lit, alts) -> map (mkCodeAction nfp lit enabledExtensions pragma) alts) literalPairs - pure $ InL $ actions + pure $ InL actions where mkCodeAction :: NormalizedFilePath -> Literal -> [GhcExtension] -> NextPragmaInfo -> AlternateFormat -> Command |? CodeAction mkCodeAction nfp lit enabled npi af@(alt, ext) = InR CodeAction { @@ -115,7 +112,7 @@ codeActionHandler state pId (CodeActionParams _ _ docId currRange _) = do mkWorkspaceEdit :: NormalizedFilePath -> [TextEdit] -> WorkspaceEdit mkWorkspaceEdit nfp edits = WorkspaceEdit changes Nothing Nothing where - changes = Just $ Map.fromList [(filePathToUri $ fromNormalizedFilePath nfp, edits)] + changes = Just $ Map.singleton (filePathToUri $ fromNormalizedFilePath nfp) edits mkCodeActionTitle :: Literal -> AlternateFormat -> [GhcExtension] -> Text mkCodeActionTitle lit (alt, ext) ghcExts diff --git a/plugins/hls-alternate-number-format-plugin/src/Ide/Plugin/Conversion.hs b/plugins/hls-alternate-number-format-plugin/src/Ide/Plugin/Conversion.hs index a6872121af..cbfaa30140 100644 --- a/plugins/hls-alternate-number-format-plugin/src/Ide/Plugin/Conversion.hs +++ b/plugins/hls-alternate-number-format-plugin/src/Ide/Plugin/Conversion.hs @@ -1,6 +1,5 @@ -{-# LANGUAGE DeriveGeneric #-} -{-# LANGUAGE TypeApplications #-} -{-# LANGUAGE ViewPatterns #-} +{-# LANGUAGE CPP #-} +{-# LANGUAGE ViewPatterns #-} module Ide.Plugin.Conversion ( alternateFormat , hexRegex @@ -21,7 +20,6 @@ module Ide.Plugin.Conversion ( , ExtensionNeeded(..) ) where -import Data.Char (toUpper) import Data.List (delete) import Data.List.Extra (enumerate, upper) import Data.Maybe (mapMaybe) @@ -161,21 +159,24 @@ toBase conv header n | n < 0 = '-' : header <> upper (conv (abs n) "") | otherwise = header <> upper (conv n "") -toOctal :: (Integral a, Show a) => a -> String -toOctal = toBase showOct "0o" +#if MIN_VERSION_base(4,17,0) +toOctal, toBinary, toHex :: Integral a => a -> String +#else +toOctal, toBinary, toHex:: (Integral a, Show a) => a -> String +#endif -toDecimal :: Integral a => a -> String -toDecimal = toBase showInt "" - -toBinary :: (Integral a, Show a) => a -> String -toBinary = toBase showBin "0b" +toBinary = toBase showBin_ "0b" where - -- this is not defined in versions of Base < 4.16-ish - showBin = showIntAtBase 2 intToDigit + -- this is not defined in base < 4.16 + showBin_ = showIntAtBase 2 intToDigit + +toOctal = toBase showOct "0o" -toHex :: (Integral a, Show a) => a -> String toHex = toBase showHex "0x" +toDecimal :: Integral a => a -> String +toDecimal = toBase showInt "" + toFloatDecimal :: RealFloat a => a -> String toFloatDecimal val = showFFloat Nothing val "" diff --git a/plugins/hls-alternate-number-format-plugin/src/Ide/Plugin/Literals.hs b/plugins/hls-alternate-number-format-plugin/src/Ide/Plugin/Literals.hs index 21b9cd4699..c26227d933 100644 --- a/plugins/hls-alternate-number-format-plugin/src/Ide/Plugin/Literals.hs +++ b/plugins/hls-alternate-number-format-plugin/src/Ide/Plugin/Literals.hs @@ -1,9 +1,5 @@ -{-# LANGUAGE CPP #-} -{-# LANGUAGE DeriveDataTypeable #-} -{-# LANGUAGE DeriveGeneric #-} -{-# LANGUAGE FlexibleInstances #-} -{-# LANGUAGE RankNTypes #-} -{-# LANGUAGE ViewPatterns #-} +{-# LANGUAGE CPP #-} +{-# LANGUAGE ViewPatterns #-} module Ide.Plugin.Literals ( collectLiterals , Literal(..) @@ -13,14 +9,14 @@ module Ide.Plugin.Literals ( import Data.Maybe (maybeToList) import Data.Text (Text) -import qualified Data.Text as T #if __GLASGOW_HASKELL__ >= 908 import qualified Data.Text.Encoding as T +#else +import qualified Data.Text as T #endif import Development.IDE.GHC.Compat hiding (getSrcSpan) import Development.IDE.Graph.Classes (NFData (rnf)) -import Generics.SYB (Data, Typeable, everything, - extQ) +import Generics.SYB (Data, everything, extQ) import qualified GHC.Generics as GHC -- data type to capture what type of literal we are dealing with @@ -53,7 +49,7 @@ getSrcSpan = \case FracLiteral ss _ _ -> unLit ss -- | Find all literals in a Parsed Source File -collectLiterals :: (Data ast, Typeable ast) => ast -> [Literal] +collectLiterals :: Data ast => ast -> [Literal] collectLiterals = everything (<>) (maybeToList . (const Nothing `extQ` getLiteral `extQ` getPattern)) @@ -72,13 +68,8 @@ getPattern (L (locA -> (RealSrcSpan patSpan _)) pat) = case pat of HsInt _ val -> fromIntegralLit patSpan val HsRat _ val _ -> fromFractionalLit patSpan val _ -> Nothing -#if __GLASGOW_HASKELL__ == 902 - NPat _ (L (RealSrcSpan sSpan _) overLit) _ _ -> fromOverLit overLit sSpan - NPlusKPat _ _ (L (RealSrcSpan sSpan _) overLit1) _ _ _ -> fromOverLit overLit1 sSpan -#else NPat _ (L (locA -> (RealSrcSpan sSpan _)) overLit) _ _ -> fromOverLit overLit sSpan NPlusKPat _ _ (L (locA -> (RealSrcSpan sSpan _)) overLit1) _ _ _ -> fromOverLit overLit1 sSpan -#endif _ -> Nothing getPattern _ = Nothing diff --git a/plugins/hls-alternate-number-format-plugin/test/Main.hs b/plugins/hls-alternate-number-format-plugin/test/Main.hs index 6eedae82ce..3a5f205e5a 100644 --- a/plugins/hls-alternate-number-format-plugin/test/Main.hs +++ b/plugins/hls-alternate-number-format-plugin/test/Main.hs @@ -1,6 +1,4 @@ -{-# LANGUAGE NamedFieldPuns #-} -{-# LANGUAGE TypeOperators #-} -{-# LANGUAGE ViewPatterns #-} +{-# LANGUAGE ViewPatterns #-} module Main ( main ) where import Data.Either (rights) @@ -9,8 +7,6 @@ import Data.Text (Text) import qualified Data.Text as T import qualified Ide.Plugin.AlternateNumberFormat as AlternateNumberFormat import qualified Ide.Plugin.Conversion as Conversion -import Language.LSP.Protocol.Lens (kind) -import Language.LSP.Protocol.Types (toEither) import Properties.Conversion (conversions) import System.FilePath ((<.>), ()) import Test.Hls @@ -72,7 +68,7 @@ findAlternateNumberActions = pure . filter isAlternateNumberCodeAction . rights -- most helpers derived from explicit-imports-plugin Main Test file testDataDir :: FilePath -testDataDir = "test" "testdata" +testDataDir = "plugins" "hls-alternate-number-format-plugin" "test" "testdata" goldenAlternateFormat :: FilePath -> (TextDocumentIdentifier -> Session ()) -> TestTree goldenAlternateFormat fp = goldenWithHaskellDoc def alternateNumberFormatPlugin (fp <> " (golden)") testDataDir fp "expected" "hs" @@ -110,21 +106,12 @@ codeActionTitle :: (Command |? CodeAction) -> Maybe Text codeActionTitle (InR CodeAction {_title}) = Just _title codeActionTitle _ = Nothing -codeActionTitle' :: CodeAction -> Text -codeActionTitle' CodeAction{_title} = _title - pointRange :: Int -> Int -> Range pointRange (subtract 1 -> fromIntegral -> line) (subtract 1 -> fromIntegral -> col) = Range (Position line col) (Position line $ col + 1) -contains :: [CodeAction] -> Text -> Bool -acts `contains` regex = any (\action -> codeActionTitle' action =~ regex) acts - -doesNotContain :: [CodeAction] -> Text -> Bool -acts `doesNotContain` regex = not $ acts `contains` regex - convertPrefix, intoInfix, maybeExtension, hexRegex, hexFloatRegex, binaryRegex, octalRegex, numDecimalRegex, decimalRegex :: Text convertPrefix = "Convert (" <> T.intercalate "|" [Conversion.hexRegex, Conversion.hexFloatRegex, Conversion.binaryRegex, Conversion.octalRegex, Conversion.numDecimalRegex, Conversion.decimalRegex] <> ")" intoInfix = " into " diff --git a/plugins/hls-alternate-number-format-plugin/test/Properties/Conversion.hs b/plugins/hls-alternate-number-format-plugin/test/Properties/Conversion.hs index a1a1dfe660..07e4617bde 100644 --- a/plugins/hls-alternate-number-format-plugin/test/Properties/Conversion.hs +++ b/plugins/hls-alternate-number-format-plugin/test/Properties/Conversion.hs @@ -1,4 +1,3 @@ -{-# LANGUAGE TypeApplications #-} module Properties.Conversion where import Ide.Plugin.Conversion @@ -7,32 +6,37 @@ import Test.Tasty.QuickCheck (testProperty) import Text.Regex.TDFA ((=~)) conversions :: TestTree -conversions = testGroup "Conversions" $ map (uncurry testProperty) [("Match NumDecimal", prop_regexMatchesNumDecimal) +conversions = testGroup "Conversions" $ + map (uncurry testProperty) + [ ("Match NumDecimal", prop_regexMatchesNumDecimal) , ("Match Hex", prop_regexMatchesHex) , ("Match Octal", prop_regexMatchesOctal) , ("Match Binary", prop_regexMatchesBinary) - ] <> map (uncurry testProperty) [("Match HexFloat", prop_regexMatchesHexFloat @Double) + ] + <> + map (uncurry testProperty) + [ ("Match HexFloat", prop_regexMatchesHexFloat) , ("Match FloatDecimal", prop_regexMatchesFloatDecimal) , ("Match FloatExpDecimal", prop_regexMatchesFloatExpDecimal) ] prop_regexMatchesNumDecimal :: Integer -> Bool -prop_regexMatchesNumDecimal = (=~ numDecimalRegex) . toFloatExpDecimal . fromInteger +prop_regexMatchesNumDecimal = (=~ numDecimalRegex) . toFloatExpDecimal @Double . fromInteger -prop_regexMatchesHex :: (Integral a, Show a) => a -> Bool +prop_regexMatchesHex :: Integer -> Bool prop_regexMatchesHex = (=~ hexRegex ) . toHex -prop_regexMatchesOctal :: (Integral a, Show a) => a -> Bool +prop_regexMatchesOctal :: Integer -> Bool prop_regexMatchesOctal = (=~ octalRegex) . toOctal -prop_regexMatchesBinary :: (Integral a, Show a) => a -> Bool +prop_regexMatchesBinary :: Integer -> Bool prop_regexMatchesBinary = (=~ binaryRegex) . toBinary -prop_regexMatchesHexFloat :: (RealFloat a) => a -> Bool +prop_regexMatchesHexFloat :: Double -> Bool prop_regexMatchesHexFloat = (=~ hexFloatRegex) . toHexFloat -prop_regexMatchesFloatDecimal :: (RealFloat a) => a -> Bool +prop_regexMatchesFloatDecimal :: Double -> Bool prop_regexMatchesFloatDecimal = (=~ decimalRegex ) . toFloatDecimal -prop_regexMatchesFloatExpDecimal :: (RealFloat a) => a -> Bool +prop_regexMatchesFloatExpDecimal :: Double -> Bool prop_regexMatchesFloatExpDecimal = (=~ numDecimalRegex ) . toFloatExpDecimal diff --git a/plugins/hls-cabal-fmt-plugin/LICENSE b/plugins/hls-cabal-fmt-plugin/LICENSE deleted file mode 100644 index 16502c47e2..0000000000 --- a/plugins/hls-cabal-fmt-plugin/LICENSE +++ /dev/null @@ -1,201 +0,0 @@ - Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ - - TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - - 1. Definitions. - - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. - - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. - - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. - - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. - - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. - - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. - - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). - - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. - - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution." - - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. - - 2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. - - 3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. - - 4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: - - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and - - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and - - (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. - - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. - - 5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. - - 6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. - - 7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. - - 8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. - - 9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. - - END OF TERMS AND CONDITIONS - - APPENDIX: How to apply the Apache License to your work. - - To apply the Apache License to your work, attach the following - boilerplate notice, with the fields enclosed by brackets "[]" - replaced with your own identifying information. (Don't include - the brackets!) The text should be enclosed in the appropriate - comment syntax for the file format. We also recommend that a - file or class name and description of purpose be included on the - same "printed page" as the copyright notice for easier - identification within third-party archives. - - Copyright 2021 The Haskell IDE team - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. diff --git a/plugins/hls-cabal-fmt-plugin/hls-cabal-fmt-plugin.cabal b/plugins/hls-cabal-fmt-plugin/hls-cabal-fmt-plugin.cabal deleted file mode 100644 index 7a002bbf49..0000000000 --- a/plugins/hls-cabal-fmt-plugin/hls-cabal-fmt-plugin.cabal +++ /dev/null @@ -1,62 +0,0 @@ -cabal-version: 2.4 -name: hls-cabal-fmt-plugin -version: 2.4.0.0 -synopsis: Integration with the cabal-fmt code formatter -description: - Please see the README on GitHub at - -license: Apache-2.0 -license-file: LICENSE -author: The Haskell IDE Team -copyright: The Haskell IDE Team -maintainer: jana.chadt@nets.at -category: Development -build-type: Simple -extra-source-files: - LICENSE - test/testdata/**/*.hs - -flag isolateTests - description: Should tests search for 'cabal-fmt' on the $PATH or shall we install it via build-tool-depends? - -- By default, search on the PATH - default: False - manual: True - -common warnings - ghc-options: -Wall - -library - import: warnings - exposed-modules: Ide.Plugin.CabalFmt - hs-source-dirs: src - build-depends: - , base >=4.12 && <5 - , directory - , filepath - , ghcide == 2.4.0.0 - , hls-plugin-api == 2.4.0.0 - , lens - , lsp-types - , mtl - , process-extras - , text - , transformers - - default-language: Haskell2010 - -test-suite tests - import: warnings - type: exitcode-stdio-1.0 - default-language: Haskell2010 - hs-source-dirs: test - main-is: Main.hs - ghc-options: -threaded -rtsopts -with-rtsopts=-N - build-depends: - , base - , directory - , filepath - , hls-cabal-fmt-plugin - , hls-test-utils == 2.4.0.0 - - if flag(isolateTests) - build-tool-depends: cabal-fmt:cabal-fmt ^>=0.1.6 diff --git a/plugins/hls-cabal-fmt-plugin/src/Ide/Plugin/CabalFmt.hs b/plugins/hls-cabal-fmt-plugin/src/Ide/Plugin/CabalFmt.hs index d51c25678a..8c49f379d7 100644 --- a/plugins/hls-cabal-fmt-plugin/src/Ide/Plugin/CabalFmt.hs +++ b/plugins/hls-cabal-fmt-plugin/src/Ide/Plugin/CabalFmt.hs @@ -1,30 +1,34 @@ +{-# LANGUAGE DataKinds #-} {-# LANGUAGE LambdaCase #-} +{-# LANGUAGE OverloadedLabels #-} {-# LANGUAGE OverloadedStrings #-} module Ide.Plugin.CabalFmt where import Control.Lens -import Control.Monad.Except (throwError) +import Control.Monad.Except (throwError) import Control.Monad.IO.Class -import qualified Data.Text as T -import Development.IDE hiding (pluginHandlers) -import Ide.Plugin.Error (PluginError (PluginInternalError, PluginInvalidParams)) +import qualified Data.Text as T +import Development.IDE hiding (pluginHandlers) +import Development.IDE.Core.PluginUtils (mkFormattingHandlers) +import Ide.Plugin.Error (PluginError (PluginInternalError, PluginInvalidParams)) +import Ide.Plugin.Properties import Ide.PluginUtils import Ide.Types -import qualified Language.LSP.Protocol.Lens as L +import qualified Language.LSP.Protocol.Lens as L import Language.LSP.Protocol.Types -import Prelude hiding (log) +import Prelude hiding (log) import System.Directory import System.Exit import System.FilePath import System.Process.ListLike -import qualified System.Process.Text as Process +import qualified System.Process.Text as Process data Log = LogProcessInvocationFailure Int | LogReadCreateProcessInfo T.Text [String] | LogInvalidInvocationInfo - | LogCabalFmtNotFound + | LogFormatterBinNotFound FilePath deriving (Show) instance Pretty Log where @@ -35,29 +39,39 @@ instance Pretty Log where ["Invocation of cabal-fmt with arguments" <+> pretty args] ++ ["failed with standard error:" <+> pretty stdErrorOut | not (T.null stdErrorOut)] LogInvalidInvocationInfo -> "Invocation of cabal-fmt with range was called but is not supported." - LogCabalFmtNotFound -> "Couldn't find executable 'cabal-fmt'" + LogFormatterBinNotFound fp -> "Couldn't find formatter executable 'cabal-fmt' at:" <+> pretty fp descriptor :: Recorder (WithPriority Log) -> PluginId -> PluginDescriptor IdeState descriptor recorder plId = - (defaultCabalPluginDescriptor plId) - { pluginHandlers = mkFormattingHandlers (provider recorder) + (defaultCabalPluginDescriptor plId "Provides formatting of cabal files with cabal-fmt") + { pluginHandlers = mkFormattingHandlers (provider recorder plId) + , pluginConfigDescriptor = defaultConfigDescriptor{configCustomConfig = mkCustomConfig properties} } +properties :: Properties '[ 'PropertyKey "path" 'TString] +properties = + emptyProperties + & defineStringProperty + #path + "Set path to 'cabal-fmt' executable" + "cabal-fmt" + -- | Formatter provider of cabal fmt. -- Formats the given source in either a given Range or the whole Document. -- If the provider fails an error is returned that can be displayed to the user. -provider :: Recorder (WithPriority Log) -> FormattingHandler IdeState -provider recorder _ (FormatRange _) _ _ _ = do +provider :: Recorder (WithPriority Log) -> PluginId -> FormattingHandler IdeState +provider recorder _ _ _ (FormatRange _) _ _ _ = do logWith recorder Info LogInvalidInvocationInfo throwError $ PluginInvalidParams "You cannot format a text-range using cabal-fmt." -provider recorder _ide FormatText contents nfp opts = do +provider recorder plId ideState _ FormatText contents nfp opts = do let cabalFmtArgs = [ "--indent", show tabularSize] - x <- liftIO $ findExecutable "cabal-fmt" + cabalFmtExePath <- fmap T.unpack $ liftIO $ runAction "cabal-fmt" ideState $ usePropertyAction #path plId properties + x <- liftIO $ findExecutable cabalFmtExePath case x of Just _ -> do (exitCode, out, err) <- liftIO $ Process.readCreateProcessWithExitCode - ( proc "cabal-fmt" cabalFmtArgs + ( proc cabalFmtExePath cabalFmtArgs ) { cwd = Just $ takeDirectory fp } @@ -71,8 +85,8 @@ provider recorder _ide FormatText contents nfp opts = do let fmtDiff = makeDiffTextEdit contents out pure $ InL fmtDiff Nothing -> do - log Error LogCabalFmtNotFound - throwError (PluginInternalError "No installation of cabal-fmt could be found. Please install it into your global environment.") + log Error $ LogFormatterBinNotFound cabalFmtExePath + throwError (PluginInternalError "No installation of cabal-fmt could be found. Please install it globally, or provide the full path to the executable") where fp = fromNormalizedFilePath nfp tabularSize = opts ^. L.tabSize diff --git a/plugins/hls-cabal-fmt-plugin/test/Main.hs b/plugins/hls-cabal-fmt-plugin/test/Main.hs index d2e0b9c0f1..0e458b2163 100644 --- a/plugins/hls-cabal-fmt-plugin/test/Main.hs +++ b/plugins/hls-cabal-fmt-plugin/test/Main.hs @@ -1,18 +1,30 @@ {-# LANGUAGE CPP #-} +{-# LANGUAGE LambdaCase #-} {-# LANGUAGE OverloadedStrings #-} module Main ( main ) where +import Ide.Logger +import qualified Ide.Plugin.Cabal as Cabal import qualified Ide.Plugin.CabalFmt as CabalFmt import System.Directory (findExecutable) import System.FilePath import Test.Hls +data TestLog + = LogCabalFmt CabalFmt.Log + | LogCabal Cabal.Log + +instance Pretty TestLog where + pretty = \case + LogCabalFmt msg -> pretty msg + LogCabal msg -> pretty msg + data CabalFmtFound = Found | NotFound isTestIsolated :: Bool -#if isolateTests +#if hls_isolate_cabalfmt_tests isTestIsolated = True #else isTestIsolated = False @@ -21,7 +33,7 @@ isTestIsolated = False isCabalFmtFound :: IO CabalFmtFound isCabalFmtFound = case isTestIsolated of True -> pure Found - False-> do + False -> do cabalFmt <- findExecutable "cabal-fmt" pure $ maybe NotFound (const Found) cabalFmt @@ -30,8 +42,11 @@ main = do foundCabalFmt <- isCabalFmtFound defaultTestRunner (tests foundCabalFmt) -cabalFmtPlugin :: PluginTestDescriptor CabalFmt.Log -cabalFmtPlugin = mkPluginTestDescriptor CabalFmt.descriptor "cabal-fmt" +cabalFmtPlugin :: PluginTestDescriptor TestLog +cabalFmtPlugin = mconcat + [ mkPluginTestDescriptor (CabalFmt.descriptor . cmapWithPrio LogCabalFmt) "cabal-fmt" + , mkPluginTestDescriptor (Cabal.descriptor . cmapWithPrio LogCabal) "cabal" + ] tests :: CabalFmtFound -> TestTree tests found = testGroup "cabal-fmt" @@ -39,8 +54,9 @@ tests found = testGroup "cabal-fmt" cabalFmtGolden found "formats a simple document" "simple_testdata" "formatted_document" $ \doc -> do formatDoc doc (FormattingOptions 2 True Nothing Nothing Nothing) - , expectFailBecause "cabal-fmt can't expand modules if .cabal file is read from stdin. Tracking issue: https://github.com/phadej/cabal-fmt/pull/82" $ - cabalFmtGolden found "formats a document with expand:src comment" "commented_testdata" "formatted_document" $ \doc -> do + -- TODO: cabal-fmt can't expand modules if .cabal file is read from stdin. Tracking + -- issue: https://github.com/phadej/cabal-fmt/pull/82 + , cabalFmtGolden found "formats a document with expand:src comment" "commented_testdata" "formatted_document" $ \doc -> do formatDoc doc (FormattingOptions 2 True Nothing Nothing Nothing) , cabalFmtGolden found "formats a document with lib information" "lib_testdata" "formatted_document" $ \doc -> do @@ -51,10 +67,10 @@ cabalFmtGolden :: CabalFmtFound -> TestName -> FilePath -> FilePath -> (TextDocu cabalFmtGolden NotFound title _ _ _ = testCase title $ assertFailure $ "Couldn't find cabal-fmt on PATH or this is not an isolated run. " - <> "Use cabal flag 'isolateTests' to make it isolated or install cabal-fmt locally." + <> "Use cabal flag 'isolateCabalFmtTests' to make it isolated or install cabal-fmt locally." cabalFmtGolden Found title path desc act = goldenWithCabalDocFormatter def cabalFmtPlugin "cabal-fmt" conf title testDataDir path desc "cabal" act where conf = def testDataDir :: FilePath -testDataDir = "test" "testdata" +testDataDir = "plugins" "hls-cabal-fmt-plugin" "test" "testdata" diff --git a/plugins/hls-cabal-fmt-plugin/test/testdata/commented_testdata.formatted_document.cabal b/plugins/hls-cabal-fmt-plugin/test/testdata/commented_testdata.formatted_document.cabal index 28f8e040cf..933669a483 100644 --- a/plugins/hls-cabal-fmt-plugin/test/testdata/commented_testdata.formatted_document.cabal +++ b/plugins/hls-cabal-fmt-plugin/test/testdata/commented_testdata.formatted_document.cabal @@ -6,10 +6,7 @@ extra-source-files: CHANGELOG.md library -- cabal-fmt: expand src - exposed-modules: - MyLib - MyOtherLib - + exposed-modules: MyLib build-depends: base ^>=4.14.1.0 hs-source-dirs: src default-language: Haskell2010 diff --git a/plugins/hls-cabal-gild-plugin/src/Ide/Plugin/CabalGild.hs b/plugins/hls-cabal-gild-plugin/src/Ide/Plugin/CabalGild.hs new file mode 100644 index 0000000000..1d698d637b --- /dev/null +++ b/plugins/hls-cabal-gild-plugin/src/Ide/Plugin/CabalGild.hs @@ -0,0 +1,93 @@ +{-# LANGUAGE DataKinds #-} +{-# LANGUAGE LambdaCase #-} +{-# LANGUAGE OverloadedLabels #-} +{-# LANGUAGE OverloadedStrings #-} + +module Ide.Plugin.CabalGild where + +import Control.Monad.Except (throwError) +import Control.Monad.IO.Class +import qualified Data.Text as T +import Development.IDE hiding (pluginHandlers) +import Development.IDE.Core.PluginUtils (mkFormattingHandlers) +import Ide.Plugin.Error (PluginError (PluginInternalError, PluginInvalidParams)) +import Ide.Plugin.Properties +import Ide.PluginUtils +import Ide.Types +import Language.LSP.Protocol.Types +import Prelude hiding (log) +import System.Directory +import System.Exit +import System.FilePath +import System.Process.ListLike +import qualified System.Process.Text as Process + +data Log + = LogProcessInvocationFailure Int T.Text + | LogReadCreateProcessInfo [String] + | LogInvalidInvocationInfo + | LogFormatterBinNotFound FilePath + deriving (Show) + +instance Pretty Log where + pretty = \case + LogProcessInvocationFailure exitCode err -> + vcat + [ "Invocation of cabal-gild failed with code" <+> pretty exitCode + , "Stderr:" <+> pretty err + ] + LogReadCreateProcessInfo args -> + "Formatter invocation: cabal-gild " <+> pretty args + LogInvalidInvocationInfo -> "Invocation of cabal-gild with range was called but is not supported." + LogFormatterBinNotFound fp -> "Couldn't find formatter executable 'cabal-gild' at:" <+> pretty fp + +descriptor :: Recorder (WithPriority Log) -> PluginId -> PluginDescriptor IdeState +descriptor recorder plId = + (defaultCabalPluginDescriptor plId "Provides formatting of cabal files with cabal-gild") + { pluginHandlers = mkFormattingHandlers (provider recorder plId) + , pluginConfigDescriptor = defaultConfigDescriptor{configCustomConfig = mkCustomConfig properties} + } + +properties :: Properties '[ 'PropertyKey "path" 'TString] +properties = + emptyProperties + & defineStringProperty + #path + "Set path to 'cabal-gild' executable" + "cabal-gild" + +-- | Formatter provider of cabal gild. +-- Formats the given source in either a given Range or the whole Document. +-- If the provider fails an error is returned that can be displayed to the user. +provider :: Recorder (WithPriority Log) -> PluginId -> FormattingHandler IdeState +provider recorder _ _ _ (FormatRange _) _ _ _ = do + logWith recorder Info LogInvalidInvocationInfo + throwError $ PluginInvalidParams "You cannot format a text-range using cabal-gild." +provider recorder plId ideState _ FormatText contents nfp _ = do + let cabalGildArgs = ["--stdin=" <> fp, "--input=-"] -- < Read from stdin + + cabalGildExePath <- fmap T.unpack $ liftIO $ runAction "cabal-gild" ideState $ usePropertyAction #path plId properties + x <- liftIO $ findExecutable cabalGildExePath + case x of + Just _ -> do + log Debug $ LogReadCreateProcessInfo cabalGildArgs + (exitCode, out, err) <- + liftIO $ Process.readCreateProcessWithExitCode + ( proc cabalGildExePath cabalGildArgs + ) + { cwd = Just $ takeDirectory fp + } + contents + case exitCode of + ExitFailure code -> do + log Error $ LogProcessInvocationFailure code err + throwError (PluginInternalError "Failed to invoke cabal-gild") + ExitSuccess -> do + let fmtDiff = makeDiffTextEdit contents out + pure $ InL fmtDiff + Nothing -> do + log Error $ LogFormatterBinNotFound cabalGildExePath + throwError (PluginInternalError "No installation of cabal-gild could be found. Please install it globally, or provide the full path to the executable.") + where + fp = fromNormalizedFilePath nfp + log = logWith recorder diff --git a/plugins/hls-cabal-gild-plugin/test/Main.hs b/plugins/hls-cabal-gild-plugin/test/Main.hs new file mode 100644 index 0000000000..5aa5ba9fba --- /dev/null +++ b/plugins/hls-cabal-gild-plugin/test/Main.hs @@ -0,0 +1,73 @@ +{-# LANGUAGE CPP #-} +{-# LANGUAGE LambdaCase #-} +{-# LANGUAGE OverloadedStrings #-} +module Main + ( main + ) where + +import Ide.Logger +import qualified Ide.Plugin.Cabal as Cabal +import qualified Ide.Plugin.CabalGild as CabalGild +import System.Directory (findExecutable) +import System.FilePath +import Test.Hls + +data TestLog + = LogCabalGild CabalGild.Log + | LogCabal Cabal.Log + +instance Pretty TestLog where + pretty = \case + LogCabalGild msg -> pretty msg + LogCabal msg -> pretty msg + +data CabalGildFound = Found | NotFound + +isTestIsolated :: Bool +#if hls_isolate_cabalgild_tests +isTestIsolated = True +#else +isTestIsolated = False +#endif + +isCabalFmtFound :: IO CabalGildFound +isCabalFmtFound = case isTestIsolated of + True -> pure Found + False -> do + cabalGild <- findExecutable "cabal-gild" + pure $ maybe NotFound (const Found) cabalGild + +main :: IO () +main = do + foundCabalFmt <- isCabalFmtFound + defaultTestRunner (tests foundCabalFmt) + +cabalGildPlugin :: PluginTestDescriptor TestLog +cabalGildPlugin = mconcat + [ mkPluginTestDescriptor (CabalGild.descriptor . cmapWithPrio LogCabalGild) "cabal-gild" + , mkPluginTestDescriptor (Cabal.descriptor . cmapWithPrio LogCabal) "cabal" + ] + +tests :: CabalGildFound -> TestTree +tests found = testGroup "cabal-gild" + [ cabalGildGolden found "formats a simple document" "simple_testdata" "formatted_document" $ \doc -> do + formatDoc doc (FormattingOptions 2 True Nothing Nothing Nothing) + + , cabalGildGolden found "formats a document with expand:src comment" "commented_testdata" "formatted_document" $ \doc -> do + formatDoc doc (FormattingOptions 2 True Nothing Nothing Nothing) + + , cabalGildGolden found "formats a document with lib information" "lib_testdata" "formatted_document" $ \doc -> do + formatDoc doc (FormattingOptions 10 True Nothing Nothing Nothing) + ] + +cabalGildGolden :: CabalGildFound -> TestName -> FilePath -> FilePath -> (TextDocumentIdentifier -> Session ()) -> TestTree +cabalGildGolden NotFound title _ _ _ = + testCase title $ + assertFailure $ "Couldn't find cabal-gild on PATH or this is not an isolated run. " + <> "Use cabal flag 'isolateCabalGildTests' to make it isolated or install cabal-gild locally." +cabalGildGolden Found title path desc act = goldenWithCabalDocFormatter def cabalGildPlugin "cabal-gild" conf title testDataDir path desc "cabal" act + where + conf = def + +testDataDir :: FilePath +testDataDir = "plugins" "hls-cabal-gild-plugin" "test" "testdata" diff --git a/plugins/hls-cabal-gild-plugin/test/testdata/commented_testdata.cabal b/plugins/hls-cabal-gild-plugin/test/testdata/commented_testdata.cabal new file mode 100644 index 0000000000..ed2f1d701e --- /dev/null +++ b/plugins/hls-cabal-gild-plugin/test/testdata/commented_testdata.cabal @@ -0,0 +1,12 @@ +cabal-version: 2.4 +name: testdata +version: 0.1.0.0 +author: Banana +extra-source-files: CHANGELOG.md + +library + -- cabal-gild: discover src + exposed-modules: MyLib + build-depends: base ^>=4.14.1.0 + hs-source-dirs: src + default-language: Haskell2010 diff --git a/plugins/hls-cabal-gild-plugin/test/testdata/commented_testdata.formatted_document.cabal b/plugins/hls-cabal-gild-plugin/test/testdata/commented_testdata.formatted_document.cabal new file mode 100644 index 0000000000..3c88b4a823 --- /dev/null +++ b/plugins/hls-cabal-gild-plugin/test/testdata/commented_testdata.formatted_document.cabal @@ -0,0 +1,15 @@ +cabal-version: 2.4 +name: testdata +version: 0.1.0.0 +author: Banana +extra-source-files: CHANGELOG.md + +library + -- cabal-gild: discover src + exposed-modules: + MyLib + MyOtherLib + + build-depends: base ^>=4.14.1.0 + hs-source-dirs: src + default-language: Haskell2010 diff --git a/plugins/hls-cabal-gild-plugin/test/testdata/hie.yaml b/plugins/hls-cabal-gild-plugin/test/testdata/hie.yaml new file mode 100644 index 0000000000..824558147d --- /dev/null +++ b/plugins/hls-cabal-gild-plugin/test/testdata/hie.yaml @@ -0,0 +1,3 @@ +cradle: + direct: + arguments: [] diff --git a/plugins/hls-cabal-gild-plugin/test/testdata/lib_testdata.cabal b/plugins/hls-cabal-gild-plugin/test/testdata/lib_testdata.cabal new file mode 100644 index 0000000000..0f07af1d70 --- /dev/null +++ b/plugins/hls-cabal-gild-plugin/test/testdata/lib_testdata.cabal @@ -0,0 +1,19 @@ +cabal-version: 2.4 +name: testdata +version: 0.1.0.0 +author: Gregg +extra-source-files: CHANGELOG.md + +library + exposed-modules: MyLib + build-depends: base ^>=4.14.1.0 + hs-source-dirs: src + default-language: Haskell2010 + +executable testdata + main-is: Main.hs + build-depends: + base ^>=4.14.1.0,testdata + hs-source-dirs: app + default-language: + Haskell2010 diff --git a/plugins/hls-cabal-gild-plugin/test/testdata/lib_testdata.formatted_document.cabal b/plugins/hls-cabal-gild-plugin/test/testdata/lib_testdata.formatted_document.cabal new file mode 100644 index 0000000000..a29e590238 --- /dev/null +++ b/plugins/hls-cabal-gild-plugin/test/testdata/lib_testdata.formatted_document.cabal @@ -0,0 +1,21 @@ +cabal-version: 2.4 +name: testdata +version: 0.1.0.0 +author: Gregg +extra-source-files: CHANGELOG.md + +library + exposed-modules: MyLib + build-depends: base ^>=4.14.1.0 + hs-source-dirs: src + default-language: Haskell2010 + +executable testdata + main-is: Main.hs + build-depends: + base ^>=4.14.1.0, + testdata, + + hs-source-dirs: app + default-language: + Haskell2010 diff --git a/plugins/hls-cabal-gild-plugin/test/testdata/simple_testdata.cabal b/plugins/hls-cabal-gild-plugin/test/testdata/simple_testdata.cabal new file mode 100644 index 0000000000..0421a27ddb --- /dev/null +++ b/plugins/hls-cabal-gild-plugin/test/testdata/simple_testdata.cabal @@ -0,0 +1,36 @@ +cabal-version: 2.4 +name: testdata +version: 0.1.0.0 + +-- A short (one-line) description of the package. +-- synopsis: + +-- A longer description of the package. +-- description: + +-- A URL where users can report bugs. +-- bug-reports: + +-- The license under which the package is released. +-- license: +author: Milky + +-- An email address to which users can send suggestions, bug reports, and patches. +-- maintainer: + +-- A copyright notice. +-- copyright: +-- category: +extra-source-files: CHANGELOG.md + +executable testdata + main-is: Main.hs + + -- Modules included in this executable, other than Main. + -- other-modules: + + -- LANGUAGE extensions used by modules in this package. + -- other-extensions: + build-depends: base ^>=4.14.1.0 + hs-source-dirs: app + default-language: Haskell2010 diff --git a/plugins/hls-cabal-gild-plugin/test/testdata/simple_testdata.formatted_document.cabal b/plugins/hls-cabal-gild-plugin/test/testdata/simple_testdata.formatted_document.cabal new file mode 100644 index 0000000000..f79cba396e --- /dev/null +++ b/plugins/hls-cabal-gild-plugin/test/testdata/simple_testdata.formatted_document.cabal @@ -0,0 +1,28 @@ +cabal-version: 2.4 +name: testdata +version: 0.1.0.0 +-- A short (one-line) description of the package. +-- synopsis: +-- A longer description of the package. +-- description: +-- A URL where users can report bugs. +-- bug-reports: +-- The license under which the package is released. +-- license: +author: Milky +-- An email address to which users can send suggestions, bug reports, and patches. +-- maintainer: +-- A copyright notice. +-- copyright: +-- category: +extra-source-files: CHANGELOG.md + +executable testdata + main-is: Main.hs + -- Modules included in this executable, other than Main. + -- other-modules: + -- LANGUAGE extensions used by modules in this package. + -- other-extensions: + build-depends: base ^>=4.14.1.0 + hs-source-dirs: app + default-language: Haskell2010 diff --git a/plugins/hls-cabal-gild-plugin/test/testdata/src/MyLib.hs b/plugins/hls-cabal-gild-plugin/test/testdata/src/MyLib.hs new file mode 100644 index 0000000000..e657c4403f --- /dev/null +++ b/plugins/hls-cabal-gild-plugin/test/testdata/src/MyLib.hs @@ -0,0 +1,4 @@ +module MyLib (someFunc) where + +someFunc :: IO () +someFunc = putStrLn "someFunc" diff --git a/plugins/hls-cabal-gild-plugin/test/testdata/src/MyOtherLib.hs b/plugins/hls-cabal-gild-plugin/test/testdata/src/MyOtherLib.hs new file mode 100644 index 0000000000..15450b43b3 --- /dev/null +++ b/plugins/hls-cabal-gild-plugin/test/testdata/src/MyOtherLib.hs @@ -0,0 +1,3 @@ +module MyOtherLib where + +bar = 2 diff --git a/plugins/hls-cabal-plugin/CHANGELOG.md b/plugins/hls-cabal-plugin/CHANGELOG.md deleted file mode 100644 index 809439f0a8..0000000000 --- a/plugins/hls-cabal-plugin/CHANGELOG.md +++ /dev/null @@ -1,6 +0,0 @@ -# Revision history for hls-cabal-plugin - -## 0.1.0.0 -- YYYY-mm-dd - -* Provide Diagnostics on parse errors and warnings for .cabal files -* Provide CodeAction for the common SPDX License mistake "BSD3" instead of "BSD-3-Clause" diff --git a/plugins/hls-cabal-plugin/LICENSE b/plugins/hls-cabal-plugin/LICENSE deleted file mode 100644 index 6d34465ea5..0000000000 --- a/plugins/hls-cabal-plugin/LICENSE +++ /dev/null @@ -1,20 +0,0 @@ -Copyright (c) 2022 Fendor - -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. diff --git a/plugins/hls-cabal-plugin/hls-cabal-plugin.cabal b/plugins/hls-cabal-plugin/hls-cabal-plugin.cabal deleted file mode 100644 index a59001eb35..0000000000 --- a/plugins/hls-cabal-plugin/hls-cabal-plugin.cabal +++ /dev/null @@ -1,95 +0,0 @@ -cabal-version: 3.0 -name: hls-cabal-plugin -version: 2.4.0.0 -synopsis: Cabal integration plugin with Haskell Language Server -description: - Please see the README on GitHub at - -homepage: -license: MIT -license-file: LICENSE -author: Fendor -maintainer: fendor@posteo.de -category: Development -extra-source-files: - CHANGELOG.md - test/testdata/*.cabal - test/testdata/simple-cabal/A.hs - test/testdata/simple-cabal/cabal.project - test/testdata/simple-cabal/hie.yaml - test/testdata/simple-cabal/simple-cabal.cabal - -common warnings - ghc-options: -Wall - -library - import: warnings - exposed-modules: - Ide.Plugin.Cabal - Ide.Plugin.Cabal.Diagnostics - Ide.Plugin.Cabal.Completion.Completer.FilePath - Ide.Plugin.Cabal.Completion.Completer.Module - Ide.Plugin.Cabal.Completion.Completer.Paths - Ide.Plugin.Cabal.Completion.Completer.Simple - Ide.Plugin.Cabal.Completion.Completer.Snippet - Ide.Plugin.Cabal.Completion.Completer.Types - Ide.Plugin.Cabal.Completion.Completions - Ide.Plugin.Cabal.Completion.Data - Ide.Plugin.Cabal.Completion.Types - Ide.Plugin.Cabal.LicenseSuggest - Ide.Plugin.Cabal.Parse - - - build-depends: - , base >=4.12 && <5 - , bytestring - , Cabal-syntax >= 3.7 - , containers - , deepseq - , directory - , filepath - , extra >=1.7.4 - , ghcide == 2.4.0.0 - , hashable - , hls-plugin-api == 2.4.0.0 - , hls-graph == 2.4.0.0 - , lens - , lsp ^>=2.3 - , lsp-types ^>=2.1 - , regex-tdfa ^>=1.3.1 - , stm - , text - , text-rope - , transformers - , unordered-containers >=0.2.10.0 - , containers - hs-source-dirs: src - default-language: Haskell2010 - -test-suite tests - import: warnings - default-language: Haskell2010 - type: exitcode-stdio-1.0 - hs-source-dirs: test - main-is: Main.hs - other-modules: - Completer - Context - Utils - build-depends: - , base - , bytestring - , Cabal-syntax >= 3.7 - , directory - , filepath - , ghcide - , hls-cabal-plugin - , hls-test-utils == 2.4.0.0 - , lens - , lsp - , lsp-types - , tasty-hunit - , text - , text-rope - , transformers - , row-types diff --git a/plugins/hls-cabal-plugin/src/Ide/Plugin/Cabal.hs b/plugins/hls-cabal-plugin/src/Ide/Plugin/Cabal.hs index be0db5ffbe..9a56467f3f 100644 --- a/plugins/hls-cabal-plugin/src/Ide/Plugin/Cabal.hs +++ b/plugins/hls-cabal-plugin/src/Ide/Plugin/Cabal.hs @@ -1,48 +1,73 @@ {-# LANGUAGE DataKinds #-} -{-# LANGUAGE DeriveGeneric #-} {-# LANGUAGE DuplicateRecordFields #-} -{-# LANGUAGE FlexibleContexts #-} -{-# LANGUAGE FlexibleInstances #-} {-# LANGUAGE LambdaCase #-} -{-# LANGUAGE NamedFieldPuns #-} {-# LANGUAGE OverloadedStrings #-} -{-# LANGUAGE TupleSections #-} {-# LANGUAGE TypeFamilies #-} -module Ide.Plugin.Cabal (descriptor, Log (..)) where +module Ide.Plugin.Cabal (descriptor, haskellInteractionDescriptor, Log (..)) where -import Control.Concurrent.STM import Control.Concurrent.Strict import Control.DeepSeq -import Control.Lens ((^.)) +import Control.Lens ((^.)) import Control.Monad.Extra import Control.Monad.IO.Class -import Control.Monad.Trans.Class (lift) -import Control.Monad.Trans.Maybe (runMaybeT) -import qualified Data.ByteString as BS +import Control.Monad.Trans.Class (lift) +import Control.Monad.Trans.Maybe (runMaybeT) +import qualified Data.ByteString as BS import Data.Hashable -import Data.HashMap.Strict (HashMap) -import qualified Data.HashMap.Strict as HashMap -import qualified Data.List.NonEmpty as NE -import qualified Data.Text.Encoding as Encoding -import Data.Typeable -import Development.IDE as D -import Development.IDE.Core.Shake (restartShakeSession) -import qualified Development.IDE.Core.Shake as Shake -import Development.IDE.Graph (alwaysRerun) +import Data.HashMap.Strict (HashMap) +import qualified Data.HashMap.Strict as HashMap +import qualified Data.List as List +import qualified Data.List.NonEmpty as NE +import qualified Data.Maybe as Maybe +import Data.Proxy +import qualified Data.Text () +import qualified Data.Text as T +import qualified Data.Text.Encoding as Encoding +import Data.Text.Utf16.Rope.Mixed as Rope +import Development.IDE as D +import Development.IDE.Core.FileStore (getVersionedTextDoc) +import Development.IDE.Core.PluginUtils +import Development.IDE.Core.Shake (restartShakeSession) +import qualified Development.IDE.Core.Shake as Shake +import Development.IDE.Graph (Key, + alwaysRerun) +import Development.IDE.LSP.HoverDefinition (foundHover) +import qualified Development.IDE.Plugin.Completions.Logic as Ghcide +import Development.IDE.Types.Shake (toKey) +import qualified Distribution.CabalSpecVersion as Cabal +import qualified Distribution.Fields as Syntax +import Distribution.Package (Dependency) +import Distribution.PackageDescription (allBuildDepends, + depPkgName, + unPackageName) +import Distribution.PackageDescription.Configuration (flattenPackageDescription) +import Distribution.Parsec.Error +import qualified Distribution.Parsec.Position as Syntax import GHC.Generics -import qualified Ide.Plugin.Cabal.Completion.Completer.Types as CompleterTypes -import qualified Ide.Plugin.Cabal.Completion.Completions as Completions -import qualified Ide.Plugin.Cabal.Completion.Types as Types -import qualified Ide.Plugin.Cabal.Diagnostics as Diagnostics -import qualified Ide.Plugin.Cabal.LicenseSuggest as LicenseSuggest -import qualified Ide.Plugin.Cabal.Parse as Parse +import qualified Ide.Plugin.Cabal.CabalAdd as CabalAdd +import Ide.Plugin.Cabal.Completion.CabalFields as CabalFields +import qualified Ide.Plugin.Cabal.Completion.Completer.Types as CompleterTypes +import qualified Ide.Plugin.Cabal.Completion.Completions as Completions +import qualified Ide.Plugin.Cabal.Completion.Data as Data +import Ide.Plugin.Cabal.Completion.Types (ParseCabalCommonSections (ParseCabalCommonSections), + ParseCabalFields (..), + ParseCabalFile (..)) +import qualified Ide.Plugin.Cabal.Completion.Types as Types +import Ide.Plugin.Cabal.Definition (gotoDefinition) +import qualified Ide.Plugin.Cabal.Diagnostics as Diagnostics +import qualified Ide.Plugin.Cabal.FieldSuggest as FieldSuggest +import qualified Ide.Plugin.Cabal.LicenseSuggest as LicenseSuggest +import Ide.Plugin.Cabal.Orphans () +import Ide.Plugin.Cabal.Outline +import qualified Ide.Plugin.Cabal.Parse as Parse +import Ide.Plugin.Error import Ide.Types -import qualified Language.LSP.Protocol.Lens as JL -import qualified Language.LSP.Protocol.Message as LSP +import qualified Language.LSP.Protocol.Lens as JL +import qualified Language.LSP.Protocol.Message as LSP import Language.LSP.Protocol.Types -import Language.LSP.Server (getVirtualFile) -import qualified Language.LSP.VFS as VFS +import qualified Language.LSP.VFS as VFS +import Text.Regex.TDFA data Log = LogModificationTime NormalizedFilePath FileVersion @@ -54,6 +79,7 @@ data Log | LogFOI (HashMap NormalizedFilePath FileOfInterestStatus) | LogCompletionContext Types.Context Position | LogCompletions Types.Log + | LogCabalAdd CabalAdd.Log deriving (Show) instance Pretty Log where @@ -73,19 +99,42 @@ instance Pretty Log where "Set files of interest to:" <+> viaShow files LogCompletionContext context position -> "Determined completion context:" - <+> viaShow context + <+> pretty context <+> "for cursor position:" <+> pretty position LogCompletions logs -> pretty logs + LogCabalAdd logs -> pretty logs + +-- | Some actions with cabal files originate from haskell files. +-- This descriptor allows to hook into the diagnostics of haskell source files, and +-- allows us to provide code actions and commands that interact with `.cabal` files. +haskellInteractionDescriptor :: Recorder (WithPriority Log) -> PluginId -> PluginDescriptor IdeState +haskellInteractionDescriptor recorder plId = + (defaultPluginDescriptor plId "Provides the cabal-add code action in haskell files") + { pluginHandlers = + mconcat + [ mkPluginHandler LSP.SMethod_TextDocumentCodeAction cabalAddCodeAction + ] + , pluginCommands = [PluginCommand CabalAdd.cabalAddCommand "add a dependency to a cabal file" (CabalAdd.command cabalAddRecorder)] + , pluginRules = pure () + , pluginNotificationHandlers = mempty + } + where + cabalAddRecorder = cmapWithPrio LogCabalAdd recorder + descriptor :: Recorder (WithPriority Log) -> PluginId -> PluginDescriptor IdeState descriptor recorder plId = - (defaultCabalPluginDescriptor plId) - { pluginRules = cabalRules recorder + (defaultCabalPluginDescriptor plId "Provides a variety of IDE features in cabal files") + { pluginRules = cabalRules recorder plId , pluginHandlers = mconcat [ mkPluginHandler LSP.SMethod_TextDocumentCodeAction licenseSuggestCodeAction , mkPluginHandler LSP.SMethod_TextDocumentCompletion $ completion recorder + , mkPluginHandler LSP.SMethod_TextDocumentDocumentSymbol moduleOutline + , mkPluginHandler LSP.SMethod_TextDocumentCodeAction $ fieldSuggestCodeAction recorder + , mkPluginHandler LSP.SMethod_TextDocumentDefinition gotoDefinition + , mkPluginHandler LSP.SMethod_TextDocumentHover hover ] , pluginNotificationHandlers = mconcat @@ -93,27 +142,30 @@ descriptor recorder plId = \ide vfs _ (DidOpenTextDocumentParams TextDocumentItem{_uri, _version}) -> liftIO $ do whenUriFile _uri $ \file -> do log' Debug $ LogDocOpened _uri - addFileOfInterest recorder ide file Modified{firstOpen = True} - restartCabalShakeSession (shakeExtras ide) vfs file "(opened)" + restartCabalShakeSession (shakeExtras ide) vfs file "(opened)" $ + addFileOfInterest recorder ide file Modified{firstOpen = True} , mkPluginNotificationHandler LSP.SMethod_TextDocumentDidChange $ \ide vfs _ (DidChangeTextDocumentParams VersionedTextDocumentIdentifier{_uri} _) -> liftIO $ do whenUriFile _uri $ \file -> do log' Debug $ LogDocModified _uri - addFileOfInterest recorder ide file Modified{firstOpen = False} - restartCabalShakeSession (shakeExtras ide) vfs file "(changed)" + restartCabalShakeSession (shakeExtras ide) vfs file "(changed)" $ + addFileOfInterest recorder ide file Modified{firstOpen = False} , mkPluginNotificationHandler LSP.SMethod_TextDocumentDidSave $ \ide vfs _ (DidSaveTextDocumentParams TextDocumentIdentifier{_uri} _) -> liftIO $ do whenUriFile _uri $ \file -> do log' Debug $ LogDocSaved _uri - addFileOfInterest recorder ide file OnDisk - restartCabalShakeSession (shakeExtras ide) vfs file "(saved)" + restartCabalShakeSession (shakeExtras ide) vfs file "(saved)" $ + addFileOfInterest recorder ide file OnDisk , mkPluginNotificationHandler LSP.SMethod_TextDocumentDidClose $ \ide vfs _ (DidCloseTextDocumentParams TextDocumentIdentifier{_uri}) -> liftIO $ do whenUriFile _uri $ \file -> do log' Debug $ LogDocClosed _uri - deleteFileOfInterest recorder ide file - restartCabalShakeSession (shakeExtras ide) vfs file "(closed)" + restartCabalShakeSession (shakeExtras ide) vfs file "(closed)" $ + deleteFileOfInterest recorder ide file ] + , pluginConfigDescriptor = defaultConfigDescriptor + { configHasDiagnostics = True + } } where log' = logWith recorder @@ -130,40 +182,109 @@ needs to be re-parsed. That's what we do when we record the dirty key that our p rule depends on. Then we restart the shake session, so that changes to our virtual files are actually picked up. -} -restartCabalShakeSession :: ShakeExtras -> VFS.VFS -> NormalizedFilePath -> String -> IO () -restartCabalShakeSession shakeExtras vfs file actionMsg = do - join $ atomically $ Shake.recordDirtyKeys shakeExtras GetModificationTime [file] - restartShakeSession shakeExtras (VFSModified vfs) (fromNormalizedFilePath file ++ " " ++ actionMsg) [] +restartCabalShakeSession :: ShakeExtras -> VFS.VFS -> NormalizedFilePath -> String -> IO [Key] -> IO () +restartCabalShakeSession shakeExtras vfs file actionMsg actionBetweenSession = do + restartShakeSession shakeExtras (VFSModified vfs) (fromNormalizedFilePath file ++ " " ++ actionMsg) [] $ do + keys <- actionBetweenSession + return (toKey GetModificationTime file:keys) -- ---------------------------------------------------------------- -- Plugin Rules -- ---------------------------------------------------------------- -cabalRules :: Recorder (WithPriority Log) -> Rules () -cabalRules recorder = do +cabalRules :: Recorder (WithPriority Log) -> PluginId -> Rules () +cabalRules recorder plId = do -- Make sure we initialise the cabal files-of-interest. ofInterestRules recorder -- Rule to produce diagnostics for cabal files. - define (cmapWithPrio LogShake recorder) $ \Types.ParseCabal file -> do - -- whenever this key is marked as dirty (e.g., when a user writes stuff to it), - -- we rerun this rule because this rule *depends* on GetModificationTime. - (t, mCabalSource) <- use_ GetFileContents file - log' Debug $ LogModificationTime file t - contents <- case mCabalSource of - Just sources -> - pure $ Encoding.encodeUtf8 sources - Nothing -> do - liftIO $ BS.readFile $ fromNormalizedFilePath file - - (pWarnings, pm) <- liftIO $ Parse.parseCabalFileContents contents - let warningDiags = fmap (Diagnostics.warningDiagnostic file) pWarnings - case pm of - Left (_cabalVersion, pErrorNE) -> do - let errorDiags = NE.toList $ NE.map (Diagnostics.errorDiagnostic file) pErrorNE - allDiags = errorDiags <> warningDiags - pure (allDiags, Nothing) - Right gpd -> do - pure (warningDiags, Just gpd) + define (cmapWithPrio LogShake recorder) $ \ParseCabalFields file -> do + config <- getPluginConfigAction plId + if not (plcGlobalOn config && plcDiagnosticsOn config) + then pure ([], Nothing) + else do + -- whenever this key is marked as dirty (e.g., when a user writes stuff to it), + -- we rerun this rule because this rule *depends* on GetModificationTime. + (t, mCabalSource) <- use_ GetFileContents file + log' Debug $ LogModificationTime file t + contents <- case mCabalSource of + Just sources -> + pure $ Encoding.encodeUtf8 $ Rope.toText sources + Nothing -> do + liftIO $ BS.readFile $ fromNormalizedFilePath file + + case Parse.readCabalFields file contents of + Left _ -> + pure ([], Nothing) + Right fields -> + pure ([], Just fields) + + define (cmapWithPrio LogShake recorder) $ \ParseCabalCommonSections file -> do + fields <- use_ ParseCabalFields file + let commonSections = Maybe.mapMaybe (\case + commonSection@(Syntax.Section (Syntax.Name _ "common") _ _) -> Just commonSection + _ -> Nothing) + fields + pure ([], Just commonSections) + + define (cmapWithPrio LogShake recorder) $ \ParseCabalFile file -> do + config <- getPluginConfigAction plId + if not (plcGlobalOn config && plcDiagnosticsOn config) + then pure ([], Nothing) + else do + -- whenever this key is marked as dirty (e.g., when a user writes stuff to it), + -- we rerun this rule because this rule *depends* on GetModificationTime. + (t, mCabalSource) <- use_ GetFileContents file + log' Debug $ LogModificationTime file t + contents <- case mCabalSource of + Just sources -> + pure $ Encoding.encodeUtf8 $ Rope.toText sources + Nothing -> do + liftIO $ BS.readFile $ fromNormalizedFilePath file + + -- Instead of fully reparsing the sources to get a 'GenericPackageDescription', + -- we would much rather re-use the already parsed results of 'ParseCabalFields'. + -- Unfortunately, Cabal-syntax doesn't expose the function 'parseGenericPackageDescription'' + -- which allows us to resume the parsing pipeline with '[Field Position]'. + (pWarnings, pm) <- liftIO $ Parse.parseCabalFileContents contents + let warningDiags = fmap (Diagnostics.warningDiagnostic file) pWarnings + case pm of + Left (_cabalVersion, pErrorNE) -> do + let regexUnknownCabalBefore310 :: T.Text + -- We don't support the cabal version, this should not be an error, as the + -- user did not do anything wrong. Instead we cast it to a warning + regexUnknownCabalBefore310 = "Unsupported cabal-version [0-9]+.[0-9]*" + regexUnknownCabalVersion :: T.Text + regexUnknownCabalVersion = "Unsupported cabal format version in cabal-version field: [0-9]+.[0-9]+" + unsupportedCabalHelpText = unlines + [ "The used `cabal-version` is not fully supported by this `HLS` binary." + , "Either the `cabal-version` is unknown, or too new for this executable." + , "This means that some functionality might not work as expected." + , "If you face any issues, try downgrading to a supported `cabal-version` or upgrading `HLS` if possible." + , "" + , "Supported versions are: " <> + List.intercalate ", " + (fmap Cabal.showCabalSpecVersion Data.supportedCabalVersions) + ] + errorDiags = + NE.toList $ + NE.map + ( \pe@(PError pos text) -> + if any (text =~) + [ regexUnknownCabalBefore310 + , regexUnknownCabalVersion + ] + then Diagnostics.warningDiagnostic file (Syntax.PWarning Syntax.PWTOther pos $ + unlines + [ text + , unsupportedCabalHelpText + ]) + else Diagnostics.errorDiagnostic file pe + ) + pErrorNE + allDiags = errorDiags <> warningDiags + pure (allDiags, Nothing) + Right gpd -> do + pure (warningDiags, Just gpd) action $ do -- Run the cabal kick. This code always runs when 'shakeRestart' is run. @@ -183,15 +304,127 @@ function invocation. kick :: Action () kick = do files <- HashMap.keys <$> getCabalFilesOfInterestUntracked - void $ uses Types.ParseCabal files + Shake.runWithSignal (Proxy @"kick/start/cabal") (Proxy @"kick/done/cabal") files Types.ParseCabalFile -- ---------------------------------------------------------------- -- Code Actions -- ---------------------------------------------------------------- licenseSuggestCodeAction :: PluginMethodHandler IdeState 'LSP.Method_TextDocumentCodeAction -licenseSuggestCodeAction _ _ (CodeActionParams _ _ (TextDocumentIdentifier uri) _range CodeActionContext{_diagnostics=diags}) = - pure $ InL $ diags >>= (fmap InR . LicenseSuggest.licenseErrorAction uri) +licenseSuggestCodeAction ideState _ (CodeActionParams _ _ (TextDocumentIdentifier uri) _range CodeActionContext{_diagnostics=diags}) = do + maxCompls <- fmap maxCompletions . liftIO $ runAction "cabal-plugin.suggestLicense" ideState getClientConfigAction + pure $ InL $ diags >>= (fmap InR . LicenseSuggest.licenseErrorAction maxCompls uri) + +-- | CodeActions for correcting field names with typos in them. +-- +-- Provides CodeActions that fix typos in both stanzas and top-level field names. +-- The suggestions are computed based on the completion context, where we "move" a fake cursor +-- to the end of the field name and trigger cabal file completions. The completions are then +-- suggested to the user. +-- +-- TODO: Relying on completions here often does not produce the desired results, we should +-- use some sort of fuzzy matching in the future, see issue #4357. +fieldSuggestCodeAction :: Recorder (WithPriority Log) -> PluginMethodHandler IdeState 'LSP.Method_TextDocumentCodeAction +fieldSuggestCodeAction recorder ide _ (CodeActionParams _ _ (TextDocumentIdentifier uri) _ CodeActionContext{_diagnostics=diags}) = do + mContents <- liftIO $ runAction "cabal-plugin.getUriContents" ide $ getUriContents $ toNormalizedUri uri + case (,) <$> mContents <*> uriToFilePath' uri of + Nothing -> pure $ InL [] + Just (fileContents, path) -> do + -- We decide on `useWithStale` here, since `useWithStaleFast` often leads to the wrong completions being suggested. + -- In case it fails, we still will get some completion results instead of an error. + mFields <- liftIO $ runAction "cabal-plugin.fields" ide $ useWithStale ParseCabalFields $ toNormalizedFilePath path + case mFields of + Nothing -> + pure $ InL [] + Just (cabalFields, _) -> do + let fields = Maybe.mapMaybe FieldSuggest.fieldErrorName diags + results <- forM fields (getSuggestion fileContents path cabalFields) + pure $ InL $ map InR $ concat results + where + getSuggestion fileContents fp cabalFields (fieldName,Diagnostic{ _range=_range@(Range (Position lineNr col) _) }) = do + let -- Compute where we would anticipate the cursor to be. + fakeLspCursorPosition = Position lineNr (col + fromIntegral (T.length fieldName)) + lspPrefixInfo = Ghcide.getCompletionPrefixFromRope fakeLspCursorPosition fileContents + cabalPrefixInfo = Completions.getCabalPrefixInfo fp lspPrefixInfo + completions <- liftIO $ computeCompletionsAt recorder ide cabalPrefixInfo fp cabalFields + let completionTexts = fmap (^. JL.label) completions + pure $ FieldSuggest.fieldErrorAction uri fieldName completionTexts _range + +cabalAddCodeAction :: PluginMethodHandler IdeState 'LSP.Method_TextDocumentCodeAction +cabalAddCodeAction state plId (CodeActionParams _ _ (TextDocumentIdentifier uri) _ CodeActionContext{_diagnostics=diags}) = do + maxCompls <- fmap maxCompletions . liftIO $ runAction "cabal.cabal-add" state getClientConfigAction + let suggestions = take maxCompls $ concatMap CabalAdd.hiddenPackageSuggestion diags + case suggestions of + [] -> pure $ InL [] + _ -> + case uriToFilePath uri of + Nothing -> pure $ InL [] + Just haskellFilePath -> do + mbCabalFile <- liftIO $ CabalAdd.findResponsibleCabalFile haskellFilePath + case mbCabalFile of + Nothing -> pure $ InL [] + Just cabalFilePath -> do + verTxtDocId <- runActionE "cabalAdd.getVersionedTextDoc" state $ + lift $ getVersionedTextDoc $ TextDocumentIdentifier (filePathToUri cabalFilePath) + mbGPD <- liftIO $ runAction "cabal.cabal-add" state $ useWithStale ParseCabalFile $ toNormalizedFilePath cabalFilePath + case mbGPD of + Nothing -> pure $ InL [] + Just (gpd, _) -> do + actions <- liftIO $ CabalAdd.addDependencySuggestCodeAction plId verTxtDocId + suggestions + haskellFilePath cabalFilePath + gpd + pure $ InL $ fmap InR actions + +-- | Handler for hover messages. +-- +-- Provides a Handler for displaying message on hover. +-- If found that the filtered hover message is a dependency, +-- adds a Documentation link. +hover :: PluginMethodHandler IdeState LSP.Method_TextDocumentHover +hover ide _ msgParam = do + nfp <- getNormalizedFilePathE uri + cabalFields <- runActionE "cabal.cabal-hover" ide $ useE ParseCabalFields nfp + case CabalFields.findTextWord cursor cabalFields of + Nothing -> + pure $ InR Null + Just cursorText -> do + gpd <- runActionE "cabal.GPD" ide $ useE ParseCabalFile nfp + let depsNames = map dependencyName $ allBuildDepends $ flattenPackageDescription gpd + case filterVersion cursorText of + Nothing -> pure $ InR Null + Just txt -> + if txt `elem` depsNames + then pure $ foundHover (Nothing, [txt <> "\n", documentationText txt]) + else pure $ InR Null + where + cursor = Types.lspPositionToCabalPosition (msgParam ^. JL.position) + uri = msgParam ^. JL.textDocument . JL.uri + + dependencyName :: Dependency -> T.Text + dependencyName dep = T.pack $ unPackageName $ depPkgName dep + + -- | Removes version requirements like + -- `==1.0.0.0`, `>= 2.1.1` that could be included in + -- hover message. Assumes that the dependency consists + -- of alphanums with dashes in between. Ends with an alphanum. + -- + -- Examples: + -- >>> filterVersion "imp-deps>=2.1.1" + -- "imp-deps" + filterVersion :: T.Text -> Maybe T.Text + filterVersion msg = getMatch (msg =~ regex) + where + regex :: T.Text + regex = "([a-zA-Z0-9-]*[a-zA-Z0-9])" + + getMatch :: (T.Text, T.Text, T.Text, [T.Text]) -> Maybe T.Text + getMatch (_, _, _, [dependency]) = Just dependency + getMatch (_, _, _, _) = Nothing -- impossible case + + documentationText :: T.Text -> T.Text + documentationText package = "[Documentation](https://hackage.haskell.org/package/" <> package <> ")" + -- ---------------------------------------------------------------- -- Cabal file of Interest rules and global variable @@ -209,14 +442,14 @@ newtype OfInterestCabalVar = OfInterestCabalVar (Var (HashMap NormalizedFilePath instance Shake.IsIdeGlobal OfInterestCabalVar data IsCabalFileOfInterest = IsCabalFileOfInterest - deriving (Eq, Show, Typeable, Generic) + deriving (Eq, Show, Generic) instance Hashable IsCabalFileOfInterest instance NFData IsCabalFileOfInterest type instance RuleResult IsCabalFileOfInterest = CabalFileOfInterestResult data CabalFileOfInterestResult = NotCabalFOI | IsCabalFOI FileOfInterestStatus - deriving (Eq, Show, Typeable, Generic) + deriving (Eq, Show, Generic) instance Hashable CabalFileOfInterestResult instance NFData CabalFileOfInterestResult @@ -245,24 +478,26 @@ getCabalFilesOfInterestUntracked = do OfInterestCabalVar var <- Shake.getIdeGlobalAction liftIO $ readVar var -addFileOfInterest :: Recorder (WithPriority Log) -> IdeState -> NormalizedFilePath -> FileOfInterestStatus -> IO () +addFileOfInterest :: Recorder (WithPriority Log) -> IdeState -> NormalizedFilePath -> FileOfInterestStatus -> IO [Key] addFileOfInterest recorder state f v = do OfInterestCabalVar var <- Shake.getIdeGlobalState state (prev, files) <- modifyVar var $ \dict -> do let (prev, new) = HashMap.alterF (,Just v) f dict pure (new, (prev, new)) - when (prev /= Just v) $ do - join $ atomically $ Shake.recordDirtyKeys (shakeExtras state) IsFileOfInterest [f] - log' Debug $ LogFOI files + if prev /= Just v + then do + log' Debug $ LogFOI files + return [toKey IsCabalFileOfInterest f] + else return [] where log' = logWith recorder -deleteFileOfInterest :: Recorder (WithPriority Log) -> IdeState -> NormalizedFilePath -> IO () +deleteFileOfInterest :: Recorder (WithPriority Log) -> IdeState -> NormalizedFilePath -> IO [Key] deleteFileOfInterest recorder state f = do OfInterestCabalVar var <- Shake.getIdeGlobalState state files <- modifyVar' var $ HashMap.delete f - join $ atomically $ Shake.recordDirtyKeys (shakeExtras state) IsFileOfInterest [f] log' Debug $ LogFOI files + return [toKey IsFileOfInterest f] where log' = logWith recorder @@ -272,38 +507,48 @@ deleteFileOfInterest recorder state f = do completion :: Recorder (WithPriority Log) -> PluginMethodHandler IdeState 'LSP.Method_TextDocumentCompletion completion recorder ide _ complParams = do - let (TextDocumentIdentifier uri) = complParams ^. JL.textDocument + let TextDocumentIdentifier uri = complParams ^. JL.textDocument position = complParams ^. JL.position - contents <- lift $ getVirtualFile $ toNormalizedUri uri - case (contents, uriToFilePath' uri) of - (Just cnts, Just path) -> do - pref <- VFS.getCompletionPrefix position cnts - let res = result pref path cnts - liftIO $ fmap (InL) res - _ -> pure . InR $ InR Null - where - result :: Maybe VFS.PosPrefixInfo -> FilePath -> VFS.VirtualFile -> IO [CompletionItem] - result Nothing _ _ = pure [] - result (Just prefix) fp cnts = do - runMaybeT context >>= \case - Nothing -> pure [] - Just ctx -> do - logWith recorder Debug $ LogCompletionContext ctx pos - let completer = Completions.contextToCompleter ctx - let completerData = CompleterTypes.CompleterData - { getLatestGPD = do - mGPD <- runIdeAction "cabal-plugin.modulesCompleter.gpd" (shakeExtras ide) $ useWithStaleFast Types.ParseCabal $ toNormalizedFilePath fp - pure $ fmap fst mGPD - , cabalPrefixInfo = prefInfo - , stanzaName = - case fst ctx of - Types.Stanza _ name -> name - _ -> Nothing - } - completions <- completer completerRecorder completerData - pure completions - where + mContents <- liftIO $ runAction "cabal-plugin.getUriContents" ide $ getUriContents $ toNormalizedUri uri + case (,) <$> mContents <*> uriToFilePath' uri of + Just (cnts, path) -> do + -- We decide on `useWithStale` here, since `useWithStaleFast` often leads to the wrong completions being suggested. + -- In case it fails, we still will get some completion results instead of an error. + mFields <- liftIO $ runAction "cabal-plugin.fields" ide $ useWithStale ParseCabalFields $ toNormalizedFilePath path + case mFields of + Nothing -> + pure . InR $ InR Null + Just (fields, _) -> do + let lspPrefInfo = Ghcide.getCompletionPrefixFromRope position cnts + cabalPrefInfo = Completions.getCabalPrefixInfo path lspPrefInfo + let res = computeCompletionsAt recorder ide cabalPrefInfo path fields + liftIO $ fmap InL res + Nothing -> pure . InR $ InR Null + +computeCompletionsAt :: Recorder (WithPriority Log) -> IdeState -> Types.CabalPrefixInfo -> FilePath -> [Syntax.Field Syntax.Position] -> IO [CompletionItem] +computeCompletionsAt recorder ide prefInfo fp fields = do + runMaybeT (context fields) >>= \case + Nothing -> pure [] + Just ctx -> do + logWith recorder Debug $ LogCompletionContext ctx pos + let completer = Completions.contextToCompleter ctx + let completerData = CompleterTypes.CompleterData + { getLatestGPD = do + -- We decide on useWithStaleFast here, since we mostly care about the file's meta information, + -- thus, a quick response gives us the desired result most of the time. + -- The `withStale` option is very important here, since we often call this rule with invalid cabal files. + mGPD <- runAction "cabal-plugin.modulesCompleter.gpd" ide $ useWithStale ParseCabalFile $ toNormalizedFilePath fp + pure $ fmap fst mGPD + , getCabalCommonSections = runAction "cabal-plugin.commonSections" ide $ use ParseCabalCommonSections $ toNormalizedFilePath fp + , cabalPrefixInfo = prefInfo + , stanzaName = + case fst ctx of + Types.Stanza _ name -> name + _ -> Nothing + } + completions <- completer completerRecorder completerData + pure completions + where + pos = Types.completionCursorPosition prefInfo + context fields = Completions.getContext completerRecorder prefInfo fields completerRecorder = cmapWithPrio LogCompletions recorder - pos = VFS.cursorPos prefix - context = Completions.getContext completerRecorder prefInfo (cnts ^. VFS.file_text) - prefInfo = Completions.getCabalPrefixInfo fp prefix diff --git a/plugins/hls-cabal-plugin/src/Ide/Plugin/Cabal/CabalAdd.hs b/plugins/hls-cabal-plugin/src/Ide/Plugin/Cabal/CabalAdd.hs new file mode 100644 index 0000000000..3b46eec128 --- /dev/null +++ b/plugins/hls-cabal-plugin/src/Ide/Plugin/Cabal/CabalAdd.hs @@ -0,0 +1,326 @@ +{-# LANGUAGE AllowAmbiguousTypes #-} +{-# LANGUAGE DeriveAnyClass #-} +{-# LANGUAGE DerivingStrategies #-} +{-# LANGUAGE ExplicitNamespaces #-} +{-# LANGUAGE LambdaCase #-} +{-# LANGUAGE OverloadedStrings #-} +{-# LANGUAGE PartialTypeSignatures #-} +{-# LANGUAGE RecordWildCards #-} + +module Ide.Plugin.Cabal.CabalAdd +( findResponsibleCabalFile + , addDependencySuggestCodeAction + , hiddenPackageSuggestion + , cabalAddCommand + , command + , Log +) +where + +import Control.Monad (filterM, void) +import Control.Monad.IO.Class (MonadIO, liftIO) +import Control.Monad.Trans.Class (lift) +import Control.Monad.Trans.Except +import Data.Aeson.Types (FromJSON, + ToJSON, toJSON) +import Data.ByteString (ByteString) +import qualified Data.ByteString.Char8 as B +import Data.List.NonEmpty (NonEmpty (..), + fromList) +import Data.String (IsString) +import qualified Data.Text as T +import Data.Text.Encoding (encodeUtf8) +import qualified Data.Text.Encoding as T +import Data.Text.Utf16.Rope.Mixed as Rope +import Development.IDE (IdeState, + getFileContents, + useWithStale) +import Development.IDE.Core.Rules (runAction) +import Distribution.Client.Add as Add +import Distribution.Compat.Prelude (Generic) +import Distribution.PackageDescription (GenericPackageDescription, + packageDescription, + specVersion) +import Distribution.PackageDescription.Configuration (flattenPackageDescription) +import Distribution.PackageDescription.Quirks (patchQuirks) +import qualified Distribution.Pretty as Pretty +import Distribution.Simple.BuildTarget (BuildTarget, + buildTargetComponentName, + readBuildTargets) +import Distribution.Simple.Utils (safeHead) +import Distribution.Verbosity (silent, + verboseNoStderr) +import Ide.Logger +import Ide.Plugin.Cabal.Completion.Types (ParseCabalFields (..), + ParseCabalFile (..)) +import Ide.Plugin.Cabal.Orphans () +import Ide.Plugin.Error +import Ide.PluginUtils (WithDeletions (SkipDeletions), + diffText, + mkLspCommand) +import Ide.Types (CommandFunction, + CommandId (CommandId), + PluginId, + pluginGetClientCapabilities, + pluginSendRequest) +import Language.LSP.Protocol.Message (SMethod (SMethod_WorkspaceApplyEdit)) +import Language.LSP.Protocol.Types (ApplyWorkspaceEditParams (ApplyWorkspaceEditParams), + ClientCapabilities, + CodeAction (CodeAction), + CodeActionKind (CodeActionKind_QuickFix), + Diagnostic (..), + Null (Null), + VersionedTextDocumentIdentifier, + WorkspaceEdit, + toNormalizedFilePath, + type (|?) (InR)) +import System.Directory (doesFileExist, + listDirectory) +import System.FilePath (dropFileName, + makeRelative, + splitPath, + takeExtension, + ()) +import Text.PrettyPrint (render) +import Text.Regex.TDFA + +data Log + = LogFoundResponsibleCabalFile FilePath + | LogCalledCabalAddCommand CabalAddCommandParams + | LogCreatedEdit WorkspaceEdit + | LogExecutedCommand + deriving (Show) + +instance Pretty Log where + pretty = \case + LogFoundResponsibleCabalFile fp -> "Located the responsible cabal file at " <+> pretty fp + LogCalledCabalAddCommand params -> "Called CabalAdd command with:\n" <+> pretty params + LogCreatedEdit edit -> "Created inplace edit:\n" <+> pretty edit + LogExecutedCommand -> "Executed CabalAdd command" + +cabalAddCommand :: IsString p => p +cabalAddCommand = "cabalAdd" + +data CabalAddCommandParams = + CabalAddCommandParams { cabalPath :: FilePath + , verTxtDocId :: VersionedTextDocumentIdentifier + , buildTarget :: Maybe String + , dependency :: T.Text + , version :: Maybe T.Text + } + deriving (Generic, Show) + deriving anyclass (FromJSON, ToJSON) + +instance Pretty CabalAddCommandParams where + pretty CabalAddCommandParams{..} = + "CabalAdd parameters:" <+> vcat + [ "cabal path:" <+> pretty cabalPath + , "target:" <+> pretty buildTarget + , "dependendency:" <+> pretty dependency + , "version:" <+> pretty version + ] + +-- | Creates a code action that calls the `cabalAddCommand`, +-- using dependency-version suggestion pairs as input. +-- +-- Returns disabled action if no cabal files given. +-- +-- Takes haskell file and cabal file paths to create a relative path +-- to the haskell file, which is used to get a `BuildTarget`. +-- +-- In current implementation the dependency is being added to the main found +-- build target, but if there will be a way to get all build targets from a file +-- it will be possible to support addition to a build target of choice. +addDependencySuggestCodeAction + :: PluginId + -> VersionedTextDocumentIdentifier -- ^ Cabal's versioned text identifier + -> [(T.Text, T.Text)] -- ^ A dependency-version suggestion pairs + -> FilePath -- ^ Path to the haskell file (source of diagnostics) + -> FilePath -- ^ Path to the cabal file (that will be edited) + -> GenericPackageDescription + -> IO [CodeAction] +addDependencySuggestCodeAction plId verTxtDocId suggestions haskellFilePath cabalFilePath gpd = do + buildTargets <- liftIO $ getBuildTargets gpd cabalFilePath haskellFilePath + case buildTargets of + -- If there are no build targets found, run `cabal-add` command with default behaviour + [] -> pure $ mkCodeAction cabalFilePath Nothing <$> suggestions + -- Otherwise provide actions for all found targets + targets -> pure $ concat [mkCodeAction cabalFilePath (Just $ buildTargetToStringRepr target) <$> + suggestions | target <- targets] + where + -- | Note the use of `pretty` function. + -- It converts the `BuildTarget` to an acceptable string representation. + -- It will be used in as the input for `cabal-add`'s `executeConfig`. + buildTargetToStringRepr target = render $ Pretty.pretty $ buildTargetComponentName target + + -- | Gives the build targets that are used in the `CabalAdd`. + -- Note the unorthodox usage of `readBuildTargets`: + -- If the relative path to the haskell file is provided, + -- the `readBuildTargets` will return build targets, where this + -- module is mentioned (in exposed-modules or other-modules). + getBuildTargets :: GenericPackageDescription -> FilePath -> FilePath -> IO [BuildTarget] + getBuildTargets gpd cabalFilePath haskellFilePath = do + let haskellFileRelativePath = makeRelative (dropFileName cabalFilePath) haskellFilePath + readBuildTargets (verboseNoStderr silent) (flattenPackageDescription gpd) [haskellFileRelativePath] + + mkCodeAction :: FilePath -> Maybe String -> (T.Text, T.Text) -> CodeAction + mkCodeAction cabalFilePath target (suggestedDep, suggestedVersion) = + let + versionTitle = if T.null suggestedVersion then T.empty else "-" <> suggestedVersion + targetTitle = case target of + Nothing -> T.empty + Just t -> " at " <> T.pack t + title = "Add dependency " <> suggestedDep <> versionTitle <> targetTitle + version = if T.null suggestedVersion then Nothing else Just suggestedVersion + + params = CabalAddCommandParams {cabalPath = cabalFilePath + , verTxtDocId = verTxtDocId + , buildTarget = target + , dependency = suggestedDep + , version=version} + command = mkLspCommand plId (CommandId cabalAddCommand) "Add missing dependency" (Just [toJSON params]) + in CodeAction title (Just CodeActionKind_QuickFix) (Just []) Nothing Nothing Nothing (Just command) Nothing + +-- | Gives a mentioned number of @(dependency, version)@ pairs +-- found in the "hidden package" diagnostic message. +-- +-- For example, if a ghc error looks like this: +-- +-- > "Could not load module ‘Data.List.Split’ +-- > It is a member of the hidden package ‘split-0.2.5’. +-- > Perhaps you need to add ‘split’ to the build-depends in your .cabal file." +-- +-- or this if PackageImports extension is used: +-- +-- > "Could not find module ‘Data.List.Split’ +-- > Perhaps you meant +-- > Data.List.Split (needs flag -package-id split-0.2.5)" +-- +-- It extracts mentioned package names and version numbers. +-- In this example, it will be @[("split", "0.2.5")]@ +-- +-- Also supports messages without a version. +-- +-- > "Perhaps you need to add ‘split’ to the build-depends in your .cabal file." +-- +-- Will turn into @[("split", "")]@ +hiddenPackageSuggestion :: Diagnostic -> [(T.Text, T.Text)] +hiddenPackageSuggestion diag = getMatch (msg =~ regex) + where + msg :: T.Text + msg = _message diag + regex :: T.Text -- TODO: Support multiple packages suggestion + regex = + let regex' = "([a-zA-Z0-9-]*[a-zA-Z0-9])(-([0-9\\.]*))?" + in "It is a member of the hidden package [\8216']" <> regex' <> "[\8217']" + <> "|" + <> "needs flag -package-id " <> regex' + -- Have to do this matching because `Regex.TDFA` doesn't(?) support + -- not-capturing groups like (?:message) + getMatch :: (T.Text, T.Text, T.Text, [T.Text]) -> [(T.Text, T.Text)] + getMatch (_, _, _, []) = [] + getMatch (_, _, _, [dependency, _, cleanVersion, "", "", ""]) = [(dependency, cleanVersion)] + getMatch (_, _, _, ["", "", "", dependency, _, cleanVersion]) = [(dependency, cleanVersion)] + getMatch (_, _, _, _) = [] + +command :: Recorder (WithPriority Log) -> CommandFunction IdeState CabalAddCommandParams +command recorder state _ params@(CabalAddCommandParams {cabalPath = path, verTxtDocId = verTxtDocId, buildTarget = target, dependency = dep, version = mbVer}) = do + logWith recorder Debug $ LogCalledCabalAddCommand params + let specifiedDep = case mbVer of + Nothing -> dep + Just ver -> dep <> " ^>=" <> ver + caps <- lift pluginGetClientCapabilities + let env = (state, caps, verTxtDocId) + edit <- getDependencyEdit recorder env path target (fromList [T.unpack specifiedDep]) + void $ lift $ pluginSendRequest SMethod_WorkspaceApplyEdit (ApplyWorkspaceEditParams Nothing edit) (\_ -> pure ()) + logWith recorder Debug LogExecutedCommand + pure $ InR Null + +-- | Constructs prerequisites for the @executeConfig@ +-- and runs it, given path to the cabal file and a dependency message. +-- Given the new contents of the cabal file constructs and returns the @edit@. +-- Inspired by @main@ in cabal-add, +-- Distribution.Client.Main +getDependencyEdit :: MonadIO m => Recorder (WithPriority Log) -> (IdeState, ClientCapabilities, VersionedTextDocumentIdentifier) -> + FilePath -> Maybe String -> NonEmpty String -> ExceptT PluginError m WorkspaceEdit +getDependencyEdit recorder env cabalFilePath buildTarget dependency = do + let (state, caps, verTxtDocId) = env + (mbCnfOrigContents, mbFields, mbPackDescr) <- liftIO $ runAction "cabal.cabal-add" state $ do + contents <- getFileContents $ toNormalizedFilePath cabalFilePath + inFields <- useWithStale ParseCabalFields $ toNormalizedFilePath cabalFilePath + inPackDescr <- useWithStale ParseCabalFile $ toNormalizedFilePath cabalFilePath + let mbCnfOrigContents = case contents of + (Just txt) -> Just $ encodeUtf8 $ Rope.toText txt + _ -> Nothing + let mbFields = fst <$> inFields + let mbPackDescr = fst <$> inPackDescr + pure (mbCnfOrigContents, mbFields, mbPackDescr) + + -- Check if required info was received, + -- otherwise fall back on other options. + (cnfOrigContents, fields, packDescr) <- do + cnfOrigContents <- case mbCnfOrigContents of + (Just cnfOrigContents) -> pure cnfOrigContents + Nothing -> readCabalFile cabalFilePath + (fields, packDescr) <- case (mbFields, mbPackDescr) of + (Just fields, Just packDescr) -> pure (fields, packDescr) + (_, _) -> case parseCabalFile cabalFilePath cnfOrigContents of + Left err -> throwE $ PluginInternalError $ T.pack err + Right (f ,gpd) -> pure (f, gpd) + pure (cnfOrigContents, fields, packDescr) + + let inputs = do + let rcnfComponent = buildTarget + let specVer = specVersion $ packageDescription packDescr + cmp <- resolveComponent cabalFilePath (fields, packDescr) rcnfComponent + deps <- traverse (validateDependency specVer) dependency + pure (fields, packDescr, cmp, deps) + + (cnfFields, origPackDescr, cnfComponent, cnfDependencies) <- case inputs of + Left err -> throwE $ PluginInternalError $ T.pack err + Right pair -> pure pair + + case executeConfig (validateChanges origPackDescr) (Config {..}) of + Nothing -> throwE $ PluginInternalError $ T.pack $ "Cannot extend build-depends in " ++ cabalFilePath + Just newContents -> do + let edit = diffText caps (verTxtDocId, T.decodeUtf8 cnfOrigContents) (T.decodeUtf8 newContents) SkipDeletions + logWith recorder Debug $ LogCreatedEdit edit + pure edit + +-- | Given a path to a haskell file, returns the closest cabal file. +-- If a package.yaml is present in same directory as the .cabal file, returns nothing, because adding a dependency to a generated cabal file +-- will break propagation of changes from package.yaml to cabal files in stack projects. +-- If cabal file wasn't found, gives Nothing. +findResponsibleCabalFile :: FilePath -> IO (Maybe FilePath) +findResponsibleCabalFile haskellFilePath = do + let dirPath = dropFileName haskellFilePath + allDirPaths = reverse $ scanl1 () (splitPath dirPath) -- sorted from most to least specific + go allDirPaths + where + go [] = pure Nothing + go (path:ps) = do + objects <- listDirectory path + let objectsWithPaths = map (\obj -> path <> obj) objects + objectsCabalExtension = filter (\c -> takeExtension c == ".cabal") objectsWithPaths + cabalFiles <- filterM (\c -> doesFileExist c) objectsCabalExtension + case safeHead cabalFiles of + Nothing -> go ps + Just cabalFile -> guardAgainstHpack path cabalFile + where + guardAgainstHpack :: FilePath -> FilePath -> IO (Maybe FilePath) + guardAgainstHpack path cabalFile = do + exists <- doesFileExist $ path "package.yaml" + if exists then pure Nothing else pure $ Just cabalFile + +-- | Gives cabal file's contents or throws error. +-- Inspired by @readCabalFile@ in cabal-add, +-- Distribution.Client.Main +-- +-- This is a fallback option! +-- Use only if the `GetFileContents` fails. +readCabalFile :: MonadIO m => FilePath -> ExceptT PluginError m ByteString +readCabalFile fileName = do + cabalFileExists <- liftIO $ doesFileExist fileName + if cabalFileExists + then snd . patchQuirks <$> liftIO (B.readFile fileName) + else throwE $ PluginInternalError $ T.pack ("Failed to read cabal file at " <> fileName) diff --git a/plugins/hls-cabal-plugin/src/Ide/Plugin/Cabal/Completion/CabalFields.hs b/plugins/hls-cabal-plugin/src/Ide/Plugin/Cabal/Completion/CabalFields.hs new file mode 100644 index 0000000000..b8cb7ce0d6 --- /dev/null +++ b/plugins/hls-cabal-plugin/src/Ide/Plugin/Cabal/Completion/CabalFields.hs @@ -0,0 +1,303 @@ +module Ide.Plugin.Cabal.Completion.CabalFields + ( findStanzaForColumn + , getModulesNames + , getFieldLSPRange + , findFieldSection + , findTextWord + , findFieldLine + , getOptionalSectionName + , getAnnotation + , getFieldName + , onelineSectionArgs + , getFieldEndPosition + , getSectionArgEndPosition + , getNameEndPosition + , getFieldLineEndPosition + ) + where + +import qualified Data.ByteString as BS +import Data.List (find) +import Data.List.Extra (groupSort) +import Data.List.NonEmpty (NonEmpty) +import qualified Data.List.NonEmpty as NE +import qualified Data.Text as T +import qualified Data.Text.Encoding as T +import Data.Tuple (swap) +import qualified Distribution.Fields as Syntax +import qualified Distribution.Parsec.Position as Syntax +import Ide.Plugin.Cabal.Completion.Types +import qualified Language.LSP.Protocol.Types as LSP + +-- ---------------------------------------------------------------- +-- Cabal-syntax utilities I don't really want to write myself +-- ---------------------------------------------------------------- + +-- | Determine the context of a cursor position within a stack of stanza contexts +-- +-- If the cursor is indented more than one of the stanzas in the stack +-- the respective stanza is returned if this is never the case, the toplevel stanza +-- in the stack is returned. +findStanzaForColumn :: Int -> NonEmpty (Int, StanzaContext) -> (StanzaContext, FieldContext) +findStanzaForColumn col ctx = case NE.uncons ctx of + ((_, stanza), Nothing) -> (stanza, None) + ((indentation, stanza), Just res) + | col < indentation -> findStanzaForColumn col res + | otherwise -> (stanza, None) + +-- | Determine the field the cursor is currently a part of. +-- +-- The result is said field and its starting position +-- or Nothing if the passed list of fields is empty. +-- +-- This only looks at the row of the cursor and not at the cursor's +-- position within the row. +-- +-- TODO: we do not handle braces correctly. Add more tests! +findFieldSection :: Syntax.Position -> [Syntax.Field Syntax.Position] -> Maybe (Syntax.Field Syntax.Position) +findFieldSection _cursor [] = Nothing +findFieldSection _cursor [x] = + -- Last field. We decide later, whether we are starting + -- a new section. + Just x +findFieldSection cursor (x:y:ys) + | Syntax.positionRow (getAnnotation x) <= cursorLine && cursorLine < Syntax.positionRow (getAnnotation y) + = Just x + | otherwise = findFieldSection cursor (y:ys) + where + cursorLine = Syntax.positionRow cursor + +-- | Determine the field line the cursor is currently a part of. +-- +-- The result is said field line and its starting position +-- or Nothing if the passed list of fields is empty. +-- +-- This function assumes that elements in a field's @FieldLine@ list +-- do not share the same row. +findFieldLine :: Syntax.Position -> [Syntax.Field Syntax.Position] -> Maybe (Syntax.FieldLine Syntax.Position) +findFieldLine _cursor [] = Nothing +findFieldLine cursor fields = + case findFieldSection cursor fields of + Nothing -> Nothing + Just (Syntax.Field _ fieldLines) -> find filterLineFields fieldLines + Just (Syntax.Section _ _ fields) -> findFieldLine cursor fields + where + cursorLine = Syntax.positionRow cursor + -- In contrast to `Field` or `Section`, `FieldLine` must have the exact + -- same line position as the cursor. + filterLineFields (Syntax.FieldLine pos _) = Syntax.positionRow pos == cursorLine + +-- | Determine the exact word at the current cursor position. +-- +-- The result is said word or Nothing if the passed list is empty +-- or the cursor position is not next to, or on a word. +-- For this function, a word is a sequence of consecutive characters +-- that are not a space or column. +-- +-- This function currently only considers words inside of a @FieldLine@. +findTextWord :: Syntax.Position -> [Syntax.Field Syntax.Position] -> Maybe T.Text +findTextWord _cursor [] = Nothing +findTextWord cursor fields = + case findFieldLine cursor fields of + Nothing -> Nothing + Just (Syntax.FieldLine pos byteString) -> + let decodedText = T.decodeUtf8 byteString + lineFieldCol = Syntax.positionCol pos + lineFieldLen = T.length decodedText + offset = cursorCol - lineFieldCol in + -- Range check if cursor is inside or or next to found line. + -- The latter comparison includes the length of the line as offset, + -- which is done to also include cursors that are at the end of a line. + -- e.g. "foo,bar|" + -- ^ + -- cursor + -- + -- Having an offset which is outside of the line is possible because of `splitAt`. + if offset >= 0 && lineFieldLen >= offset + then + let (lhs, rhs) = T.splitAt offset decodedText + strippedLhs = T.takeWhileEnd isAllowedChar lhs + strippedRhs = T.takeWhile isAllowedChar rhs + resultText = T.concat [strippedLhs, strippedRhs] in + -- It could be possible that the cursor was in-between separators, in this + -- case the resulting text would be empty, which should result in `Nothing`. + -- e.g. " foo ,| bar" + -- ^ + -- cursor + if not $ T.null resultText then Just resultText else Nothing + else + Nothing + where + cursorCol = Syntax.positionCol cursor + separators = [',', ' '] + isAllowedChar = (`notElem` separators) + +type FieldName = T.Text + +getAnnotation :: Syntax.Field ann -> ann +getAnnotation (Syntax.Field (Syntax.Name ann _) _) = ann +getAnnotation (Syntax.Section (Syntax.Name ann _) _ _) = ann + +getFieldName :: Syntax.Field ann -> FieldName +getFieldName (Syntax.Field (Syntax.Name _ fn) _) = T.decodeUtf8 fn +getFieldName (Syntax.Section (Syntax.Name _ fn) _ _) = T.decodeUtf8 fn + +getFieldLineName :: Syntax.FieldLine ann -> FieldName +getFieldLineName (Syntax.FieldLine _ fn) = T.decodeUtf8 fn + +-- | Returns the name of a section if it has a name. +-- +-- This assumes that the given section args belong to named stanza +-- in which case the stanza name is returned. +getOptionalSectionName :: [Syntax.SectionArg ann] -> Maybe T.Text +getOptionalSectionName [] = Nothing +getOptionalSectionName (x:xs) = case x of + Syntax.SecArgName _ name -> Just (T.decodeUtf8 name) + _ -> getOptionalSectionName xs + +type BuildTargetName = T.Text +type ModuleName = T.Text + +-- | Given a cabal AST returns pairs of all respective target names +-- and the module name bound to them. If a target is a main library gives +-- @Nothing@, otherwise @Just target-name@ +-- +-- Examples of input cabal files and the outputs: +-- +-- * Target is a main library module: +-- +-- > library +-- > exposed-modules: +-- > MyLib +-- +-- * @getModulesNames@ output: +-- +-- > [([Nothing], "MyLib")] +-- +-- * Same module names in different targets: +-- +-- > test-suite first-target +-- > other-modules: +-- > Config +-- > test-suite second-target +-- > other-modules: +-- > Config +-- +-- * @getModulesNames@ output: +-- +-- > [([Just "first-target", Just "second-target"], "Config")] +getModulesNames :: [Syntax.Field any] -> [([Maybe BuildTargetName], ModuleName)] +getModulesNames fields = map swap $ groupSort rawModuleTargetPairs + where + rawModuleTargetPairs = concatMap getSectionModuleNames sections + sections = getSectionsWithModules fields + + getSectionModuleNames :: Syntax.Field any -> [(ModuleName, Maybe BuildTargetName)] + getSectionModuleNames (Syntax.Section _ secArgs fields) = map (, getArgsName secArgs) $ concatMap getFieldModuleNames fields + getSectionModuleNames _ = [] + + getArgsName [Syntax.SecArgName _ name] = Just $ T.decodeUtf8 name + getArgsName _ = Nothing -- Can be only a main library, that has no name + -- since it's impossible to have multiple names for a build target + + getFieldModuleNames field@(Syntax.Field _ modules) = if getFieldName field == T.pack "exposed-modules" || + getFieldName field == T.pack "other-modules" + then map getFieldLineName modules + else [] + getFieldModuleNames _ = [] + +-- | Trims a given cabal AST leaving only targets and their +-- @exposed-modules@ and @other-modules@ sections. +-- +-- For example: +-- +-- * Given a cabal file like this: +-- +-- > library +-- > import: extra +-- > hs-source-dirs: source/directory +-- > ... +-- > exposed-modules: +-- > Important.Exposed.Module +-- > other-modules: +-- > Important.Other.Module +-- > +-- > test-suite tests +-- > type: type +-- > build-tool-depends: tool +-- > other-modules: +-- > Important.Other.Module +-- +-- * @getSectionsWithModules@ gives output: +-- +-- > library +-- > exposed-modules: +-- > Important.Exposed.Module +-- > other-modules: +-- > Important.Other.Module +-- > test-suite tests +-- > other-modules: +-- > Important.Other.Module +getSectionsWithModules :: [Syntax.Field any] -> [Syntax.Field any] +getSectionsWithModules fields = concatMap go fields + where + go :: Syntax.Field any -> [Syntax.Field any] + go (Syntax.Field _ _) = [] + go section@(Syntax.Section _ _ fields) = concatMap onlySectionsWithModules (section:fields) + + onlySectionsWithModules :: Syntax.Field any -> [Syntax.Field any] + onlySectionsWithModules (Syntax.Field _ _) = [] + onlySectionsWithModules (Syntax.Section name secArgs fields) + | (not . null) newFields = [Syntax.Section name secArgs newFields] + | otherwise = [] + where newFields = filter subfieldHasModule fields + + subfieldHasModule :: Syntax.Field any -> Bool + subfieldHasModule field@(Syntax.Field _ _) = getFieldName field == T.pack "exposed-modules" || + getFieldName field == T.pack "other-modules" + subfieldHasModule (Syntax.Section _ _ _) = False + +-- | Makes a single text line out of multiple +-- @SectionArg@s. Allows to display conditions, +-- flags, etc in one line, which is easier to read. +-- +-- For example, @flag@ @(@ @pedantic@ @)@ will be joined in +-- one line, instead of four @SectionArg@s separately. +onelineSectionArgs :: [Syntax.SectionArg ann] -> T.Text +onelineSectionArgs sectionArgs = joinedName + where + joinedName = T.unwords $ map getName sectionArgs + + getName :: Syntax.SectionArg ann -> T.Text + getName (Syntax.SecArgName _ identifier) = T.decodeUtf8 identifier + getName (Syntax.SecArgStr _ quotedString) = T.decodeUtf8 quotedString + getName (Syntax.SecArgOther _ string) = T.decodeUtf8 string + +-- | Returns the end position of a provided field +getFieldEndPosition :: Syntax.Field Syntax.Position -> Syntax.Position +getFieldEndPosition (Syntax.Field name []) = getNameEndPosition name +getFieldEndPosition (Syntax.Field _ (x:xs)) = getFieldLineEndPosition $ NE.last (x NE.:| xs) +getFieldEndPosition (Syntax.Section name [] []) = getNameEndPosition name +getFieldEndPosition (Syntax.Section _ (x:xs) []) = getSectionArgEndPosition $ NE.last (x NE.:| xs) +getFieldEndPosition (Syntax.Section _ _ (x:xs)) = getFieldEndPosition $ NE.last (x NE.:| xs) + +-- | Returns the end position of a provided section arg +getSectionArgEndPosition :: Syntax.SectionArg Syntax.Position -> Syntax.Position +getSectionArgEndPosition (Syntax.SecArgName (Syntax.Position row col) byteString) = Syntax.Position row (col + BS.length byteString) +getSectionArgEndPosition (Syntax.SecArgStr (Syntax.Position row col) byteString) = Syntax.Position row (col + BS.length byteString) +getSectionArgEndPosition (Syntax.SecArgOther (Syntax.Position row col) byteString) = Syntax.Position row (col + BS.length byteString) + +-- | Returns the end position of a provided name +getNameEndPosition :: Syntax.Name Syntax.Position -> Syntax.Position +getNameEndPosition (Syntax.Name (Syntax.Position row col) byteString) = Syntax.Position row (col + BS.length byteString) + +-- | Returns the end position of a provided field line +getFieldLineEndPosition :: Syntax.FieldLine Syntax.Position -> Syntax.Position +getFieldLineEndPosition (Syntax.FieldLine (Syntax.Position row col) byteString) = Syntax.Position row (col + BS.length byteString) + +-- | Returns an LSP compatible range for a provided field +getFieldLSPRange :: Syntax.Field Syntax.Position -> LSP.Range +getFieldLSPRange field = LSP.Range startLSPPos endLSPPos + where + startLSPPos = cabalPositionToLSPPosition $ getAnnotation field + endLSPPos = cabalPositionToLSPPosition $ getFieldEndPosition field diff --git a/plugins/hls-cabal-plugin/src/Ide/Plugin/Cabal/Completion/Completer/FilePath.hs b/plugins/hls-cabal-plugin/src/Ide/Plugin/Cabal/Completion/Completer/FilePath.hs index c7aa59f125..a63777416b 100644 --- a/plugins/hls-cabal-plugin/src/Ide/Plugin/Cabal/Completion/Completer/FilePath.hs +++ b/plugins/hls-cabal-plugin/src/Ide/Plugin/Cabal/Completion/Completer/FilePath.hs @@ -1,5 +1,4 @@ -{-# LANGUAGE LambdaCase #-} -{-# LANGUAGE ScopedTypeVariables #-} +{-# LANGUAGE LambdaCase #-} module Ide.Plugin.Cabal.Completion.Completer.FilePath where diff --git a/plugins/hls-cabal-plugin/src/Ide/Plugin/Cabal/Completion/Completer/Paths.hs b/plugins/hls-cabal-plugin/src/Ide/Plugin/Cabal/Completion/Completer/Paths.hs index b067fa9e49..0e1053453b 100644 --- a/plugins/hls-cabal-plugin/src/Ide/Plugin/Cabal/Completion/Completer/Paths.hs +++ b/plugins/hls-cabal-plugin/src/Ide/Plugin/Cabal/Completion/Completer/Paths.hs @@ -1,11 +1,13 @@ module Ide.Plugin.Cabal.Completion.Completer.Paths where import qualified Data.List as List +import Data.List.Extra (dropPrefix) import qualified Data.Text as T import Distribution.PackageDescription (Benchmark (..), BuildInfo (..), CondTree (condTreeData), Executable (..), + ForeignLib (..), GenericPackageDescription (..), Library (..), UnqualComponentName, @@ -45,6 +47,32 @@ data PathCompletionInfo = PathCompletionInfo } deriving (Eq, Show) + +{- | Posix.splitFileName modification, that drops trailing ./ if + if wasn't present in the original path. + + Fix for the issue #3774 + Examples: + + >>> splitFileNameNoTrailingSlash "" + ("", "") + >>> splitFileNameNoTrailingSlash "./" + ("./", "") + >>> splitFileNameNoTrailingSlash "dir" + ("", "dir") + >>> splitFileNameNoTrailingSlash "./dir" + ("./", "dir") + >>> splitFileNameNoTrailingSlash "dir1/dir2" + ("dir1/","dir2") + >>> splitFileNameNoTrailingSlash "./dir1/dir2" + ("./dir1/","dir2") +-} +splitFileNameNoTrailingSlash :: FilePath -> (String, String) +splitFileNameNoTrailingSlash prefix = rmTrailingSlash ("./" `List.isPrefixOf` prefix) (Posix.splitFileName prefix) + where rmTrailingSlash hadTrailingSlash (queryDirectory', pathSegment') + | hadTrailingSlash = (queryDirectory', pathSegment') + | otherwise = ("./" `dropPrefix` queryDirectory', pathSegment') + {- | Takes an optional source subdirectory and a prefix info and creates a path completion info accordingly. @@ -64,7 +92,7 @@ pathCompletionInfoFromCabalPrefixInfo srcDir prefInfo = } where prefix = T.unpack $ completionPrefix prefInfo - (queryDirectory', pathSegment') = Posix.splitFileName prefix + (queryDirectory', pathSegment') = splitFileNameNoTrailingSlash prefix -- | Extracts the source directories of the library stanza. sourceDirsExtractionLibrary :: Maybe StanzaName -> GenericPackageDescription -> [FilePath] @@ -91,6 +119,10 @@ sourceDirsExtractionTestSuite name gpd = extractRelativeDirsFromStanza name gpd sourceDirsExtractionBenchmark :: Maybe StanzaName -> GenericPackageDescription -> [FilePath] sourceDirsExtractionBenchmark name gpd = extractRelativeDirsFromStanza name gpd condBenchmarks benchmarkBuildInfo +-- | Extracts the source directories of foreign-lib stanza with the given name. +sourceDirsExtractionForeignLib :: Maybe StanzaName -> GenericPackageDescription -> [FilePath] +sourceDirsExtractionForeignLib name gpd = extractRelativeDirsFromStanza name gpd condForeignLibs foreignLibBuildInfo + {- | Takes a possible stanza name, a GenericPackageDescription, a function to access the stanza information we are interested in and a function to access the build info from the specific stanza. diff --git a/plugins/hls-cabal-plugin/src/Ide/Plugin/Cabal/Completion/Completer/Simple.hs b/plugins/hls-cabal-plugin/src/Ide/Plugin/Cabal/Completion/Completer/Simple.hs index d4fb54bb5c..b097af5cd2 100644 --- a/plugins/hls-cabal-plugin/src/Ide/Plugin/Cabal/Completion/Completer/Simple.hs +++ b/plugins/hls-cabal-plugin/src/Ide/Plugin/Cabal/Completion/Completer/Simple.hs @@ -1,5 +1,5 @@ -{-# LANGUAGE OverloadedStrings #-} -{-# LANGUAGE ScopedTypeVariables #-} +{-# LANGUAGE LambdaCase #-} +{-# LANGUAGE OverloadedStrings #-} module Ide.Plugin.Cabal.Completion.Completer.Simple where @@ -8,11 +8,14 @@ import Data.Function ((&)) import qualified Data.List as List import Data.Map (Map) import qualified Data.Map as Map -import Data.Maybe (fromMaybe) +import Data.Maybe (fromMaybe, + mapMaybe) import Data.Ord (Down (Down)) import qualified Data.Text as T +import qualified Distribution.Fields as Syntax import Ide.Logger (Priority (..), logWith) +import Ide.Plugin.Cabal.Completion.CabalFields import Ide.Plugin.Cabal.Completion.Completer.Types import Ide.Plugin.Cabal.Completion.Types (CabalPrefixInfo (..), Log) @@ -42,6 +45,22 @@ constantCompleter completions _ cData = do range = completionRange prefInfo pure $ map (mkSimpleCompletionItem range . Fuzzy.original) scored +-- | Completer to be used for import fields. +-- +-- TODO: Does not exclude imports, defined after the current cursor position +-- which are not allowed according to the cabal specification +importCompleter :: Completer +importCompleter l cData = do + cabalCommonsM <- getCabalCommonSections cData + case cabalCommonsM of + Just cabalCommons -> do + let commonNames = mapMaybe (\case + Syntax.Section (Syntax.Name _ "common") commonNames _ -> getOptionalSectionName commonNames + _ -> Nothing) + cabalCommons + constantCompleter commonNames l cData + Nothing -> noopCompleter l cData + -- | Completer to be used for the field @name:@ value. -- -- This is almost always the name of the cabal file. However, diff --git a/plugins/hls-cabal-plugin/src/Ide/Plugin/Cabal/Completion/Completer/Types.hs b/plugins/hls-cabal-plugin/src/Ide/Plugin/Cabal/Completion/Completer/Types.hs index c39ad2d953..968b68919b 100644 --- a/plugins/hls-cabal-plugin/src/Ide/Plugin/Cabal/Completion/Completer/Types.hs +++ b/plugins/hls-cabal-plugin/src/Ide/Plugin/Cabal/Completion/Completer/Types.hs @@ -1,10 +1,11 @@ -{-# LANGUAGE ScopedTypeVariables #-} -{-# LANGUAGE TypeFamilies #-} +{-# LANGUAGE TypeFamilies #-} module Ide.Plugin.Cabal.Completion.Completer.Types where import Development.IDE as D +import qualified Distribution.Fields as Syntax import Distribution.PackageDescription (GenericPackageDescription) +import qualified Distribution.Parsec.Position as Syntax import Ide.Plugin.Cabal.Completion.Types import Language.LSP.Protocol.Types (CompletionItem) @@ -17,9 +18,11 @@ data CompleterData = CompleterData { -- | Access to the latest available generic package description for the handled cabal file, -- relevant for some completion actions which require the file's meta information -- such as the module completers which require access to source directories - getLatestGPD :: IO (Maybe GenericPackageDescription), + getLatestGPD :: IO (Maybe GenericPackageDescription), + -- | Access to the entries of the handled cabal file as parsed by ParseCabalFields + getCabalCommonSections :: IO (Maybe [Syntax.Field Syntax.Position]), -- | Prefix info to be used for constructing completion items - cabalPrefixInfo :: CabalPrefixInfo, + cabalPrefixInfo :: CabalPrefixInfo, -- | The name of the stanza in which the completer is applied - stanzaName :: Maybe StanzaName + stanzaName :: Maybe StanzaName } diff --git a/plugins/hls-cabal-plugin/src/Ide/Plugin/Cabal/Completion/Completions.hs b/plugins/hls-cabal-plugin/src/Ide/Plugin/Cabal/Completion/Completions.hs index 69c5fa6598..83e809fb0f 100644 --- a/plugins/hls-cabal-plugin/src/Ide/Plugin/Cabal/Completion/Completions.hs +++ b/plugins/hls-cabal-plugin/src/Ide/Plugin/Cabal/Completion/Completions.hs @@ -1,26 +1,24 @@ -{-# LANGUAGE OverloadedStrings #-} -{-# LANGUAGE ScopedTypeVariables #-} +{-# LANGUAGE OverloadedStrings #-} module Ide.Plugin.Cabal.Completion.Completions (contextToCompleter, getContext, getCabalPrefixInfo) where import Control.Lens ((^.)) import Control.Monad.IO.Class (MonadIO) -import Control.Monad.Trans.Maybe -import Data.Foldable (asum) -import qualified Data.List as List -import Data.Map (Map) +import Data.List.NonEmpty (NonEmpty) +import qualified Data.List.NonEmpty as NE import qualified Data.Map as Map import qualified Data.Text as T -import Data.Text.Utf16.Rope (Rope) -import qualified Data.Text.Utf16.Rope as Rope import Development.IDE as D +import qualified Development.IDE.Plugin.Completions.Types as Ghcide +import qualified Distribution.Fields as Syntax +import qualified Distribution.Parsec.Position as Syntax +import Ide.Plugin.Cabal.Completion.CabalFields import Ide.Plugin.Cabal.Completion.Completer.Simple import Ide.Plugin.Cabal.Completion.Completer.Snippet import Ide.Plugin.Cabal.Completion.Completer.Types (Completer) import Ide.Plugin.Cabal.Completion.Data import Ide.Plugin.Cabal.Completion.Types import qualified Language.LSP.Protocol.Lens as JL -import qualified Language.LSP.VFS as VFS import qualified System.FilePath as FP import System.FilePath (takeBaseName) @@ -64,32 +62,13 @@ contextToCompleter (Stanza s _, KeyWord kw) = -- Can return Nothing if an error occurs. -- -- TODO: first line can only have cabal-version: keyword -getContext :: (MonadIO m) => Recorder (WithPriority Log) -> CabalPrefixInfo -> Rope -> MaybeT m Context -getContext recorder prefInfo ls = - case prevLinesM of - Just prevLines -> do - let lvlContext = - if completionIndentation prefInfo == 0 - then TopLevel - else currentLevel prevLines - case lvlContext of - TopLevel -> do - kwContext <- MaybeT . pure $ getKeyWordContext prefInfo prevLines (cabalVersionKeyword <> cabalKeywords) - pure (TopLevel, kwContext) - Stanza s n -> - case Map.lookup s stanzaKeywordMap of - Nothing -> do - pure (Stanza s n, None) - Just m -> do - kwContext <- MaybeT . pure $ getKeyWordContext prefInfo prevLines m - pure (Stanza s n, kwContext) - Nothing -> do - logWith recorder Warning $ LogFileSplitError pos - -- basically returns nothing - fail "Abort computation" +getContext :: (MonadIO m) => Recorder (WithPriority Log) -> CabalPrefixInfo -> [Syntax.Field Syntax.Position] -> m Context +getContext recorder prefInfo fields = do + let ctx = findCursorContext cursor (NE.singleton (0, TopLevel)) (completionPrefix prefInfo) fields + logWith recorder Debug $ LogCompletionContext ctx + pure ctx where - pos = completionCursorPosition prefInfo - prevLinesM = splitAtPosition pos ls + cursor = lspPositionToCabalPosition (completionCursorPosition prefInfo) -- | Takes information about the current file's file path, -- and the cursor position in the file; and builds a CabalPrefixInfo @@ -97,23 +76,23 @@ getContext recorder prefInfo ls = -- Checks whether a suffix needs to be completed -- and calculates the range in the document -- where the completion action should be applied. -getCabalPrefixInfo :: FilePath -> VFS.PosPrefixInfo -> CabalPrefixInfo +getCabalPrefixInfo :: FilePath -> Ghcide.PosPrefixInfo -> CabalPrefixInfo getCabalPrefixInfo fp prefixInfo = CabalPrefixInfo { completionPrefix = completionPrefix', isStringNotation = mkIsStringNotation separator afterCursorText, - completionCursorPosition = VFS.cursorPos prefixInfo, + completionCursorPosition = Ghcide.cursorPos prefixInfo, completionRange = Range completionStart completionEnd, completionWorkingDir = FP.takeDirectory fp, completionFileName = T.pack $ takeBaseName fp } where - completionEnd = VFS.cursorPos prefixInfo + completionEnd = Ghcide.cursorPos prefixInfo completionStart = Position (_line completionEnd) (_character completionEnd - (fromIntegral $ T.length completionPrefix')) - (beforeCursorText, afterCursorText) = T.splitAt cursorColumn $ VFS.fullLine prefixInfo + (beforeCursorText, afterCursorText) = T.splitAt cursorColumn $ Ghcide.fullLine prefixInfo completionPrefix' = T.takeWhileEnd (not . (`elem` stopConditionChars)) beforeCursorText separator = -- if there is an opening apostrophe before the cursor in the line somewhere, @@ -121,7 +100,7 @@ getCabalPrefixInfo fp prefixInfo = if odd $ T.count "\"" beforeCursorText then '\"' else ' ' - cursorColumn = fromIntegral $ VFS.cursorPos prefixInfo ^. JL.character + cursorColumn = fromIntegral $ Ghcide.cursorPos prefixInfo ^. JL.character stopConditionChars = separator : [',', ':'] -- \| Takes the character occurring exactly before, @@ -144,84 +123,59 @@ getCabalPrefixInfo fp prefixInfo = -- Implementation Details -- ---------------------------------------------------------------- --- | Takes prefix info about the previously written text, --- a list of lines (representing a file) and a map of --- keywords and returns a keyword context if the --- previously written keyword matches one in the map. --- --- From a cursor position, we traverse the cabal file upwards to --- find the latest written keyword if there is any. --- Values may be written on subsequent lines, --- in order to allow for this we take the indentation of the current --- word to be completed into account to find the correct keyword context. -getKeyWordContext :: CabalPrefixInfo -> [T.Text] -> Map KeyWordName a -> Maybe FieldContext -getKeyWordContext prefInfo ls keywords = do - case lastNonEmptyLineM of - Nothing -> Just None - Just lastLine' -> do - let (whiteSpaces, lastLine) = T.span (== ' ') lastLine' - let keywordIndentation = T.length whiteSpaces - let cursorIndentation = completionIndentation prefInfo - -- in order to be in a keyword context the cursor needs - -- to be indented more than the keyword - if cursorIndentation > keywordIndentation - then -- if the last thing written was a keyword without a value - case List.find (`T.isPrefixOf` lastLine) (Map.keys keywords) of - Nothing -> Just None - Just kw -> Just $ KeyWord kw - else Just None - where - lastNonEmptyLineM :: Maybe T.Text - lastNonEmptyLineM = do - (curLine, rest) <- List.uncons ls - -- represents the current line while disregarding the - -- currently written text we want to complete - let cur = stripPartiallyWritten curLine - List.find (not . T.null . T.stripEnd) $ - cur : rest +findCursorContext :: + Syntax.Position -> + -- ^ The cursor position we look for in the fields + NonEmpty (Int, StanzaContext) -> + -- ^ A stack of current stanza contexts and their starting line numbers + T.Text -> + -- ^ The cursor's prefix text + [Syntax.Field Syntax.Position] -> + -- ^ The fields to traverse + Context +findCursorContext cursor parentHistory prefixText fields = + case findFieldSection cursor fields of + Nothing -> (snd $ NE.head parentHistory, None) + -- We found the most likely section. Now, are we starting a new section or are we completing an existing one? + Just field@(Syntax.Field _ _) -> classifyFieldContext parentHistory cursor field + Just section@(Syntax.Section _ args sectionFields) + | inSameLineAsSectionName section -> (stanzaCtx, None) -- TODO: test whether keyword in same line is parsed correctly + | getFieldName section `elem` conditionalKeywords -> findCursorContext cursor parentHistory prefixText sectionFields -- Ignore if conditionals, they are not real sections + | otherwise -> + findCursorContext cursor + (NE.cons (Syntax.positionCol (getAnnotation section) + 1, Stanza (getFieldName section) (getOptionalSectionName args)) parentHistory) + prefixText sectionFields + where + inSameLineAsSectionName section = Syntax.positionRow (getAnnotation section) == Syntax.positionRow cursor + stanzaCtx = snd $ NE.head parentHistory + conditionalKeywords = ["if", "elif", "else"] --- | Traverse the given lines (starting before current cursor position --- up to the start of the file) to find the nearest stanza declaration, --- if none is found we are in the top level context. +-- | Finds the cursor's context, where the cursor is already found to be in a specific field -- --- TODO: this could be merged with getKeyWordContext in order to increase --- performance by reducing the number of times we have to traverse the cabal file. -currentLevel :: [T.Text] -> StanzaContext -currentLevel [] = TopLevel -currentLevel (cur : xs) - | Just (s, n) <- stanza = Stanza s n - | otherwise = currentLevel xs +-- Due to the way the field context is recognised for incomplete cabal files, +-- an incomplete keyword is also recognised as a field, therefore we need to determine +-- the specific context as we could still be in a stanza context in this case. +classifyFieldContext :: NonEmpty (Int, StanzaContext) -> Syntax.Position -> Syntax.Field Syntax.Position -> Context +classifyFieldContext ctx cursor field + -- the cursor is not indented enough to be within the field + -- but still indented enough to be within the stanza + | cursorColumn <= fieldColumn && minIndent <= cursorColumn = (stanzaCtx, None) + -- the cursor is not in the current stanza's context as it is not indented enough + | cursorColumn < minIndent = findStanzaForColumn cursorColumn ctx + | cursorIsInFieldName = (stanzaCtx, None) + | cursorIsBeforeFieldName = (stanzaCtx, None) + | otherwise = (stanzaCtx, KeyWord (getFieldName field <> ":")) where - stanza = asum $ map checkStanza (Map.keys stanzaKeywordMap) - checkStanza :: StanzaType -> Maybe (StanzaType, Maybe StanzaName) - checkStanza t = - case T.stripPrefix t (T.strip cur) of - Just n - | T.null n -> Just (t, Nothing) - | otherwise -> Just (t, Just $ T.strip n) - Nothing -> Nothing + (minIndent, stanzaCtx) = NE.head ctx --- | Get all lines before the given cursor position in the given file --- and reverse their order to traverse backwards starting from the given position. -splitAtPosition :: Position -> Rope -> Maybe [T.Text] -splitAtPosition pos ls = do - split <- splitFile - pure $ reverse $ Rope.lines $ fst split - where - splitFile = Rope.splitAtPosition ropePos ls - ropePos = - Rope.Position - { Rope.posLine = fromIntegral $ pos ^. JL.line, - Rope.posColumn = fromIntegral $ pos ^. JL.character - } + cursorIsInFieldName = inSameLineAsFieldName && + fieldColumn <= cursorColumn && + cursorColumn <= fieldColumn + T.length (getFieldName field) --- | Takes a line of text and removes the last partially --- written word to be completed. -stripPartiallyWritten :: T.Text -> T.Text -stripPartiallyWritten = T.dropWhileEnd (\y -> (y /= ' ') && (y /= ':')) + cursorIsBeforeFieldName = inSameLineAsFieldName && + cursorColumn < fieldColumn --- | Calculates how many spaces the currently completed item is indented. -completionIndentation :: CabalPrefixInfo -> Int -completionIndentation prefInfo = fromIntegral (pos ^. JL.character) - (T.length $ completionPrefix prefInfo) - where - pos = completionCursorPosition prefInfo + inSameLineAsFieldName = Syntax.positionRow (getAnnotation field) == Syntax.positionRow cursor + + cursorColumn = Syntax.positionCol cursor + fieldColumn = Syntax.positionCol (getAnnotation field) diff --git a/plugins/hls-cabal-plugin/src/Ide/Plugin/Cabal/Completion/Data.hs b/plugins/hls-cabal-plugin/src/Ide/Plugin/Cabal/Completion/Data.hs index 24badfcfc5..03e517eae2 100644 --- a/plugins/hls-cabal-plugin/src/Ide/Plugin/Cabal/Completion/Data.hs +++ b/plugins/hls-cabal-plugin/src/Ide/Plugin/Cabal/Completion/Data.hs @@ -1,8 +1,4 @@ -{-# LANGUAGE OverloadedStrings #-} -{-# LANGUAGE ScopedTypeVariables #-} -{-# OPTIONS_GHC -Wno-unrecognised-pragmas #-} - -{-# HLINT ignore "Redundant bracket" #-} +{-# LANGUAGE OverloadedStrings #-} module Ide.Plugin.Cabal.Completion.Data where @@ -20,10 +16,24 @@ import Ide.Plugin.Cabal.Completion.Completer.Types (Completer) import Ide.Plugin.Cabal.Completion.Types import Ide.Plugin.Cabal.LicenseSuggest (licenseNames) +-- | Ad-hoc data type for modelling the available top-level stanzas. +-- Not intended right now for anything else but to avoid string +-- comparisons in 'stanzaKeywordMap' and 'libExecTestBenchCommons'. +data TopLevelStanza + = Library + | Executable + | TestSuite + | Benchmark + | ForeignLib + | Common + -- ---------------------------------------------------------------- -- Completion Data -- ---------------------------------------------------------------- +supportedCabalVersions :: [CabalSpecVersion] +supportedCabalVersions = [CabalSpecV2_2 .. maxBound] + -- | Keyword for cabal version; required to be the top line in a cabal file cabalVersionKeyword :: Map KeyWordName Completer cabalVersionKeyword = @@ -31,7 +41,7 @@ cabalVersionKeyword = constantCompleter $ -- We only suggest cabal versions newer than 2.2 -- since we don't recommend using older ones. - map (T.pack . showCabalSpecVersion) [CabalSpecV2_2 .. maxBound] + map (T.pack . showCabalSpecVersion) supportedCabalVersions -- | Top level keywords of a cabal file. -- @@ -64,16 +74,18 @@ cabalKeywords = ("extra-tmp-files:", filePathCompleter) ] --- | Map, containing all stanzas in a cabal file as keys +-- | Map, containing all stanzas in a cabal file as keys, -- and lists of their possible nested keywords as values. stanzaKeywordMap :: Map StanzaType (Map KeyWordName Completer) stanzaKeywordMap = Map.fromList - [ ("library", libraryFields <> libExecTestBenchCommons), - ("executable", executableFields <> libExecTestBenchCommons), - ("test-suite", testSuiteFields <> libExecTestBenchCommons), - ("benchmark", benchmarkFields <> libExecTestBenchCommons), - ("foreign-library", foreignLibraryFields <> libExecTestBenchCommons), + [ ("library", libraryFields <> libExecTestBenchCommons Library), + ("executable", executableFields <> libExecTestBenchCommons Executable), + ("test-suite", testSuiteFields <> libExecTestBenchCommons TestSuite), + ("benchmark", benchmarkFields <> libExecTestBenchCommons Benchmark), + ("foreign-library", foreignLibraryFields <> libExecTestBenchCommons ForeignLib), + ("common", libExecTestBenchCommons Library), + ("common", libExecTestBenchCommons Common), ("flag", flagFields), ("source-repository", sourceRepositoryFields) ] @@ -159,10 +171,11 @@ flagFields = ("lib-version-linux:", noopCompleter) ] -libExecTestBenchCommons :: Map KeyWordName Completer -libExecTestBenchCommons = +libExecTestBenchCommons :: TopLevelStanza -> Map KeyWordName Completer +libExecTestBenchCommons st = Map.fromList - [ ("build-depends:", noopCompleter), + [ ("import:", importCompleter), + ("build-depends:", noopCompleter), ("hs-source-dirs:", directoryCompleter), ("default-extensions:", noopCompleter), ("other-extensions:", noopCompleter), @@ -179,6 +192,8 @@ libExecTestBenchCommons = ("includes:", filePathCompleter), ("install-includes:", filePathCompleter), ("include-dirs:", directoryCompleter), + ("autogen-includes:", filePathCompleter), + ("autogen-modules:", moduleCompleterByTopLevelStanza), ("c-sources:", filePathCompleter), ("cxx-sources:", filePathCompleter), ("asm-sources:", filePathCompleter), @@ -199,6 +214,26 @@ libExecTestBenchCommons = ("extra-framework-dirs:", directoryCompleter), ("mixins:", noopCompleter) ] + where + -- + moduleCompleterByTopLevelStanza = case st of + Library -> modulesCompleter sourceDirsExtractionLibrary + Executable -> modulesCompleter sourceDirsExtractionExecutable + TestSuite -> modulesCompleter sourceDirsExtractionTestSuite + Benchmark -> modulesCompleter sourceDirsExtractionBenchmark + ForeignLib -> modulesCompleter sourceDirsExtractionForeignLib + Common -> + -- TODO: We can't provide a module completer because we provide + -- module completions based on the "hs-source-dirs" after parsing the file, + -- i.e. based on the 'PackageDescription'. + -- "common" stanzas are erased in the 'PackageDescription' representation, + -- thus we can't provide accurate module completers right now, as we don't + -- know what the 'hs-source-dirs' in the "common" stanza are. + -- + -- A potential fix would be to introduce an intermediate representation that + -- parses the '.cabal' file s.t. that we have access to the 'hs-source-dirs', + -- but not have erased the "common" stanza. + noopCompleter -- | Contains a map of the most commonly used licenses, weighted by their popularity. -- diff --git a/plugins/hls-cabal-plugin/src/Ide/Plugin/Cabal/Completion/Types.hs b/plugins/hls-cabal-plugin/src/Ide/Plugin/Cabal/Completion/Types.hs index 6a8512d093..59796afe2b 100644 --- a/plugins/hls-cabal-plugin/src/Ide/Plugin/Cabal/Completion/Types.hs +++ b/plugins/hls-cabal-plugin/src/Ide/Plugin/Cabal/Completion/Types.hs @@ -1,18 +1,19 @@ -{-# LANGUAGE DeriveGeneric #-} -{-# LANGUAGE LambdaCase #-} -{-# LANGUAGE OverloadedStrings #-} -{-# LANGUAGE ScopedTypeVariables #-} -{-# LANGUAGE TypeFamilies #-} +{-# LANGUAGE LambdaCase #-} +{-# LANGUAGE OverloadedStrings #-} +{-# LANGUAGE TypeFamilies #-} module Ide.Plugin.Cabal.Completion.Types where -import Control.DeepSeq (NFData) +import Control.DeepSeq (NFData) +import Control.Lens ((^.)) import Data.Hashable -import qualified Data.Text as T -import Data.Typeable -import Development.IDE as D +import qualified Data.Text as T +import Development.IDE as D +import qualified Distribution.Fields as Syntax +import qualified Distribution.PackageDescription as PD +import qualified Distribution.Parsec.Position as Syntax import GHC.Generics -import qualified Ide.Plugin.Cabal.Parse as Parse +import qualified Language.LSP.Protocol.Lens as JL data Log = LogFileSplitError Position @@ -23,28 +24,48 @@ data Log | LogFilePathCompleterIOError FilePath IOError | LogUseWithStaleFastNoResult | LogMapLookUpOfKnownKeyFailed T.Text + | LogCompletionContext Context deriving (Show) instance Pretty Log where pretty = \case - LogFileSplitError pos -> "An error occured when trying to separate the lines of the cabal file at position:" <+> pretty pos + LogFileSplitError pos -> "An error occurred when trying to separate the lines of the cabal file at position:" <+> pretty pos LogUnknownKeyWordInContextError kw -> "Lookup of key word failed for:" <+> viaShow kw LogUnknownStanzaNameInContextError sn -> "Lookup of stanza name failed for:" <+> viaShow sn LogFilePathCompleterIOError fp ioErr -> - "When trying to complete the file path:" <+> pretty fp <+> "the following unexpected IO error occured" <+> viaShow ioErr + "When trying to complete the file path:" <+> pretty fp <+> "the following unexpected IO error occurred" <+> viaShow ioErr LogUseWithStaleFastNoResult -> "Package description couldn't be read" LogMapLookUpOfKnownKeyFailed key -> "Lookup of key in map failed even though it should exist" <+> pretty key + LogCompletionContext ctx -> "Completion context is:" <+> pretty ctx -type instance RuleResult ParseCabal = Parse.GenericPackageDescription +type instance RuleResult ParseCabalFile = PD.GenericPackageDescription -data ParseCabal = ParseCabal - deriving (Eq, Show, Typeable, Generic) +data ParseCabalFile = ParseCabalFile + deriving (Eq, Show, Generic) -instance Hashable ParseCabal +instance Hashable ParseCabalFile -instance NFData ParseCabal +instance NFData ParseCabalFile + +type instance RuleResult ParseCabalFields = [Syntax.Field Syntax.Position] + +data ParseCabalFields = ParseCabalFields + deriving (Eq, Show, Generic) + +instance Hashable ParseCabalFields + +instance NFData ParseCabalFields + +type instance RuleResult ParseCabalCommonSections = [Syntax.Field Syntax.Position] + +data ParseCabalCommonSections = ParseCabalCommonSections + deriving (Eq, Show, Generic) + +instance Hashable ParseCabalCommonSections + +instance NFData ParseCabalCommonSections -- | The context a cursor can be in within a cabal file. -- @@ -63,9 +84,13 @@ data StanzaContext -- Stanzas have their own fields which differ from top-level fields. -- Each stanza must be named, such as 'executable exe', -- except for the main library. - Stanza StanzaType (Maybe StanzaName) + Stanza !StanzaType !(Maybe StanzaName) deriving (Eq, Show, Read) +instance Pretty StanzaContext where + pretty TopLevel = "TopLevel" + pretty (Stanza t ms) = "Stanza" <+> pretty t <+> (maybe mempty pretty ms) + -- | Keyword context in a cabal file. -- -- Used to decide whether to suggest values or keywords. @@ -73,12 +98,16 @@ data FieldContext = -- | Key word context, where a keyword -- occurs right before the current word -- to be completed - KeyWord KeyWordName + KeyWord !KeyWordName | -- | Keyword context where no keyword occurs -- right before the current word to be completed None deriving (Eq, Show, Read) +instance Pretty FieldContext where + pretty (KeyWord kw) = "KeyWord" <+> pretty kw + pretty None = "No Keyword" + type KeyWordName = T.Text type StanzaName = T.Text @@ -141,3 +170,19 @@ applyStringNotation (Just LeftSide) compl = compl <> "\"" applyStringNotation Nothing compl | Just _ <- T.find (== ' ') compl = "\"" <> compl <> "\"" | otherwise = compl + +-- | Convert an LSP 'Position' to a 'Syntax.Position'. +-- +-- Cabal Positions start their indexing at 1 while LSP starts at 0. +-- This helper makes sure, the translation is done properly. +lspPositionToCabalPosition :: Position -> Syntax.Position +lspPositionToCabalPosition pos = Syntax.Position + (fromIntegral (pos ^. JL.line) + 1) + (fromIntegral (pos ^. JL.character) + 1) + +-- | Convert an 'Syntax.Position' to a LSP 'Position'. +-- +-- Cabal Positions start their indexing at 1 while LSP starts at 0. +-- This helper makes sure, the translation is done properly. +cabalPositionToLSPPosition :: Syntax.Position -> Position +cabalPositionToLSPPosition (Syntax.Position start end) = Position (toEnum start -1) (toEnum end -1) diff --git a/plugins/hls-cabal-plugin/src/Ide/Plugin/Cabal/Definition.hs b/plugins/hls-cabal-plugin/src/Ide/Plugin/Cabal/Definition.hs new file mode 100644 index 0000000000..5f85151199 --- /dev/null +++ b/plugins/hls-cabal-plugin/src/Ide/Plugin/Cabal/Definition.hs @@ -0,0 +1,206 @@ +{-# LANGUAGE DataKinds #-} +{-# LANGUAGE DuplicateRecordFields #-} +{-# LANGUAGE OverloadedStrings #-} +{-# LANGUAGE RecordWildCards #-} +{-# LANGUAGE TypeFamilies #-} + +module Ide.Plugin.Cabal.Definition where + +import Control.Lens ((^.)) +import Control.Monad.Extra +import Control.Monad.IO.Class +import Data.List (find) +import qualified Data.Maybe as Maybe +import qualified Data.Text as T +import Development.IDE as D +import Development.IDE.Core.PluginUtils +import qualified Distribution.Fields as Syntax +import Distribution.PackageDescription (Benchmark (..), + BuildInfo (..), + Executable (..), + ForeignLib (..), + GenericPackageDescription, + Library (..), + LibraryName (LMainLibName, LSubLibName), + PackageDescription (..), + TestSuite (..), + library, + unUnqualComponentName) +import Distribution.PackageDescription.Configuration (flattenPackageDescription) +import qualified Distribution.Parsec.Position as Syntax +import Distribution.Utils.Generic (safeHead) +import Distribution.Utils.Path (getSymbolicPath) +import Ide.Plugin.Cabal.Completion.CabalFields as CabalFields +import Ide.Plugin.Cabal.Completion.Types (ParseCabalCommonSections (ParseCabalCommonSections), + ParseCabalFields (..), + ParseCabalFile (..)) +import qualified Ide.Plugin.Cabal.Completion.Types as Types +import Ide.Plugin.Cabal.Orphans () +import Ide.Plugin.Error +import Ide.Types +import qualified Language.LSP.Protocol.Lens as JL +import qualified Language.LSP.Protocol.Message as LSP +import Language.LSP.Protocol.Types +import System.Directory (doesFileExist) +import System.FilePath (joinPath, + takeDirectory, + (<.>), ()) + +-- | Handler for going to definitions. +-- +-- Provides a handler for going to the definition in a cabal file, +-- gathering all possible definitions by calling subfunctions. + +-- TODO: Resolve more cases for go-to definition. +gotoDefinition :: PluginMethodHandler IdeState LSP.Method_TextDocumentDefinition +gotoDefinition ide _ msgParam = do + nfp <- getNormalizedFilePathE uri + cabalFields <- runActionE "cabal-plugin.commonSections" ide $ useE ParseCabalFields nfp + -- Trim the AST tree, so multiple passes in subfunctions won't hurt the performance. + let fieldsOfInterest = maybe cabalFields (:[] ) $ CabalFields.findFieldSection cursor cabalFields + + commonSections <- runActionE "cabal-plugin.commonSections" ide $ useE ParseCabalCommonSections nfp + let mCommonSectionsDef = gotoCommonSectionDefinition uri commonSections cursor fieldsOfInterest + + mModuleDef <- do + mGPD <- liftIO $ runAction "cabal.GPD" ide $ useWithStale ParseCabalFile nfp + case mGPD of + Nothing -> pure Nothing + Just (gpd, _) -> liftIO $ gotoModulesDefinition nfp gpd cursor fieldsOfInterest + + let defs = Maybe.catMaybes [ mCommonSectionsDef + , mModuleDef + ] + -- Take first found definition. + -- We assume, that there can't be multiple definitions, + -- or the most specific definitions come first. + case safeHead defs of + Nothing -> pure $ InR $ InR Null + Just def -> pure $ InL def + where + cursor = Types.lspPositionToCabalPosition (msgParam ^. JL.position) + uri = msgParam ^. JL.textDocument . JL.uri + +-- | Definitions for Sections. +-- +-- Provides a Definition if cursor is pointed at an identifier, +-- otherwise gives Nothing. +-- The definition is found by traversing the sections and comparing their name to +-- the clicked identifier. +gotoCommonSectionDefinition + :: Uri -- ^ Cabal file URI + -> [Syntax.Field Syntax.Position] -- ^ Found common sections + -> Syntax.Position -- ^ Cursor position + -> [Syntax.Field Syntax.Position] -- ^ Trimmed cabal AST on a cursor + -> Maybe Definition +gotoCommonSectionDefinition uri commonSections cursor fieldsOfInterest = do + cursorText <- CabalFields.findTextWord cursor fieldsOfInterest + commonSection <- find (isSectionArgName cursorText) commonSections + Just $ Definition $ InL $ Location uri $ CabalFields.getFieldLSPRange commonSection + where + isSectionArgName name (Syntax.Section _ sectionArgName _) = name == CabalFields.onelineSectionArgs sectionArgName + isSectionArgName _ _ = False + +-- | Definitions for Modules. +-- +-- Provides a Definition if cursor is pointed at a +-- exposed-module or other-module field, otherwise gives Nothing +-- +-- Definition is found by looking for a module name, +-- the cursor is pointing to and looking for it in @BuildInfo@s. +-- Note that since a trimmed ast is provided, a @Definition@ to +-- a module with the same name as the target one, +-- but in another build target can't be given. +-- +-- See resolving @Config@ module in tests. +gotoModulesDefinition + :: NormalizedFilePath -- ^ Normalized FilePath to the cabal file + -> GenericPackageDescription + -> Syntax.Position -- ^ Cursor position + -> [Syntax.Field Syntax.Position] -- ^ Trimmed cabal AST on a cursor + -> IO (Maybe Definition) +gotoModulesDefinition nfp gpd cursor fieldsOfInterest = do + let mCursorText = CabalFields.findTextWord cursor fieldsOfInterest + moduleNames = CabalFields.getModulesNames fieldsOfInterest + mModuleName = find (isModuleName mCursorText) moduleNames + + case mModuleName of + Nothing -> pure Nothing + Just (mBuildTargetNames, moduleName) -> do + let buildInfos = foldMap (lookupBuildTargetPackageDescription + (flattenPackageDescription gpd)) + mBuildTargetNames + sourceDirs = map getSymbolicPath $ concatMap hsSourceDirs buildInfos + potentialPaths = map (\dir -> takeDirectory (fromNormalizedFilePath nfp) dir toHaskellFile moduleName) sourceDirs + allPaths <- liftIO $ filterM doesFileExist potentialPaths + -- Don't provide the range, since there is little benefit for it + let locations = map (\pth -> Location (filePathToUri pth) (mkRange 0 0 0 0)) allPaths + case safeHead locations of -- We assume there could be only one source location + Nothing -> pure Nothing + Just location -> pure $ Just $ Definition $ InL location + where + isModuleName (Just name) (_, moduleName) = name == moduleName + isModuleName _ _ = False + +-- | Gives all `buildInfo`s given a target name. +-- +-- `Maybe buildTargetName` is provided, and if it's +-- Nothing we assume, that it's a main library. +-- Otherwise looks for the provided name. +lookupBuildTargetPackageDescription :: PackageDescription -> Maybe T.Text -> [BuildInfo] +lookupBuildTargetPackageDescription (PackageDescription {..}) Nothing = + case library of + Nothing -> [] -- Target is a main library but no main library was found + Just (Library {libBuildInfo}) -> [libBuildInfo] +lookupBuildTargetPackageDescription (PackageDescription {..}) (Just buildTargetName) = + Maybe.catMaybes $ + map executableNameLookup executables <> + map subLibraryNameLookup subLibraries <> + map foreignLibsNameLookup foreignLibs <> + map testSuiteNameLookup testSuites <> + map benchmarkNameLookup benchmarks + where + executableNameLookup :: Executable -> Maybe BuildInfo + executableNameLookup (Executable {exeName, buildInfo}) = + if T.pack (unUnqualComponentName exeName) == buildTargetName + then Just buildInfo + else Nothing + subLibraryNameLookup :: Library -> Maybe BuildInfo + subLibraryNameLookup (Library {libName, libBuildInfo}) = + case libName of + (LSubLibName name) -> + if T.pack (unUnqualComponentName name) == buildTargetName + then Just libBuildInfo + else Nothing + LMainLibName -> Nothing + foreignLibsNameLookup :: ForeignLib -> Maybe BuildInfo + foreignLibsNameLookup (ForeignLib {foreignLibName, foreignLibBuildInfo}) = + if T.pack (unUnqualComponentName foreignLibName) == buildTargetName + then Just foreignLibBuildInfo + else Nothing + testSuiteNameLookup :: TestSuite -> Maybe BuildInfo + testSuiteNameLookup (TestSuite {testName, testBuildInfo}) = + if T.pack (unUnqualComponentName testName) == buildTargetName + then Just testBuildInfo + else Nothing + benchmarkNameLookup :: Benchmark -> Maybe BuildInfo + benchmarkNameLookup (Benchmark {benchmarkName, benchmarkBuildInfo}) = + if T.pack (unUnqualComponentName benchmarkName) == buildTargetName + then Just benchmarkBuildInfo + else Nothing + +-- | Converts a name of a module to a FilePath. +-- Is needed to guess the relative path to a file +-- using the name of the module. +-- We assume, that correct module naming is guaranteed. +-- +-- Warning: Generally not advised to use, if there are +-- better ways to get the path. +-- +-- Examples: (output is system dependent) +-- >>> toHaskellFile "My.Module.Lib" +-- "My/Module/Lib.hs" +-- >>> toHaskellFile "Main" +-- "Main.hs" +toHaskellFile :: T.Text -> FilePath +toHaskellFile moduleName = joinPath (map T.unpack $ T.splitOn "." moduleName) <.> ".hs" diff --git a/plugins/hls-cabal-plugin/src/Ide/Plugin/Cabal/Diagnostics.hs b/plugins/hls-cabal-plugin/src/Ide/Plugin/Cabal/Diagnostics.hs index 78ca21f236..5429ac0bb9 100644 --- a/plugins/hls-cabal-plugin/src/Ide/Plugin/Cabal/Diagnostics.hs +++ b/plugins/hls-cabal-plugin/src/Ide/Plugin/Cabal/Diagnostics.hs @@ -1,39 +1,47 @@ {-# LANGUAGE DuplicateRecordFields #-} {-# LANGUAGE OverloadedStrings #-} -{-# LANGUAGE TupleSections #-} module Ide.Plugin.Cabal.Diagnostics ( errorDiagnostic , warningDiagnostic , positionFromCabalPosition +, fatalParseErrorDiagnostic -- * Re-exports , FileDiagnostic , Diagnostic(..) ) where -import qualified Data.Text as T -import Development.IDE (FileDiagnostic, - ShowDiagnostic (ShowDiag)) -import Distribution.Fields (showPError, showPWarning) -import qualified Ide.Plugin.Cabal.Parse as Lib -import Ide.PluginUtils (extendNextLine) -import Language.LSP.Protocol.Types (Diagnostic (..), - DiagnosticSeverity (..), - NormalizedFilePath, - Position (Position), - Range (Range), - fromNormalizedFilePath) +import Control.Lens ((&), (.~)) +import qualified Data.Text as T +import Development.IDE (FileDiagnostic) +import Development.IDE.Types.Diagnostics (fdLspDiagnosticL, + ideErrorWithSource) +import Distribution.Fields (showPError, showPWarning) +import qualified Distribution.Parsec as Syntax +import Ide.PluginUtils (extendNextLine) +import Language.LSP.Protocol.Lens (range) +import Language.LSP.Protocol.Types (Diagnostic (..), + DiagnosticSeverity (..), + NormalizedFilePath, + Position (Position), + Range (Range), + fromNormalizedFilePath) + +-- | Produce a diagnostic for a fatal Cabal parser error. +fatalParseErrorDiagnostic :: NormalizedFilePath -> T.Text -> FileDiagnostic +fatalParseErrorDiagnostic fp msg = + mkDiag fp "cabal" DiagnosticSeverity_Error (toBeginningOfNextLine Syntax.zeroPos) msg -- | Produce a diagnostic from a Cabal parser error -errorDiagnostic :: NormalizedFilePath -> Lib.PError -> FileDiagnostic -errorDiagnostic fp err@(Lib.PError pos _) = +errorDiagnostic :: NormalizedFilePath -> Syntax.PError -> FileDiagnostic +errorDiagnostic fp err@(Syntax.PError pos _) = mkDiag fp "cabal" DiagnosticSeverity_Error (toBeginningOfNextLine pos) msg where msg = T.pack $ showPError (fromNormalizedFilePath fp) err -- | Produce a diagnostic from a Cabal parser warning -warningDiagnostic :: NormalizedFilePath -> Lib.PWarning -> FileDiagnostic -warningDiagnostic fp warning@(Lib.PWarning _ pos _) = +warningDiagnostic :: NormalizedFilePath -> Syntax.PWarning -> FileDiagnostic +warningDiagnostic fp warning@(Syntax.PWarning _ pos _) = mkDiag fp "cabal" DiagnosticSeverity_Warning (toBeginningOfNextLine pos) msg where msg = T.pack $ showPWarning (fromNormalizedFilePath fp) warning @@ -42,7 +50,7 @@ warningDiagnostic fp warning@(Lib.PWarning _ pos _) = -- only a single source code 'Lib.Position'. -- We define the range to be _from_ this position -- _to_ the first column of the next line. -toBeginningOfNextLine :: Lib.Position -> Range +toBeginningOfNextLine :: Syntax.Position -> Range toBeginningOfNextLine cabalPos = extendNextLine $ Range pos pos where pos = positionFromCabalPosition cabalPos @@ -54,12 +62,13 @@ toBeginningOfNextLine cabalPos = extendNextLine $ Range pos pos -- -- >>> positionFromCabalPosition $ Lib.Position 1 1 -- Position 0 0 -positionFromCabalPosition :: Lib.Position -> Position -positionFromCabalPosition (Lib.Position line column) = Position (fromIntegral line') (fromIntegral col') +positionFromCabalPosition :: Syntax.Position -> Position +positionFromCabalPosition (Syntax.Position line column) = Position (fromIntegral line') (fromIntegral col') where -- LSP is zero-based, Cabal is one-based - line' = line-1 - col' = column-1 + -- Cabal can return line 0 for errors in the first line + line' = if line <= 0 then 0 else line-1 + col' = if column <= 0 then 0 else column-1 -- | Create a 'FileDiagnostic' mkDiag @@ -74,15 +83,11 @@ mkDiag -> T.Text -- ^ The message displayed by the editor -> FileDiagnostic -mkDiag file diagSource sev loc msg = (file, ShowDiag,) - Diagnostic - { _range = loc - , _severity = Just sev - , _source = Just diagSource - , _message = msg - , _code = Nothing - , _tags = Nothing - , _relatedInformation = Nothing - , _codeDescription = Nothing - , _data_ = Nothing - } +mkDiag file diagSource sev loc msg = + ideErrorWithSource + (Just diagSource) + (Just sev) + file + msg + Nothing + & fdLspDiagnosticL . range .~ loc diff --git a/plugins/hls-cabal-plugin/src/Ide/Plugin/Cabal/FieldSuggest.hs b/plugins/hls-cabal-plugin/src/Ide/Plugin/Cabal/FieldSuggest.hs new file mode 100644 index 0000000000..2e77ccb193 --- /dev/null +++ b/plugins/hls-cabal-plugin/src/Ide/Plugin/Cabal/FieldSuggest.hs @@ -0,0 +1,70 @@ +{-# LANGUAGE AllowAmbiguousTypes #-} +{-# LANGUAGE ExplicitNamespaces #-} +{-# LANGUAGE FlexibleContexts #-} +{-# LANGUAGE LambdaCase #-} +{-# LANGUAGE OverloadedStrings #-} + +module Ide.Plugin.Cabal.FieldSuggest + ( fieldErrorName, + fieldErrorAction, + -- * Re-exports + T.Text, + Diagnostic (..), + ) +where + +import qualified Data.Map.Strict as Map +import qualified Data.Text as T +import Language.LSP.Protocol.Types (CodeAction (..), + CodeActionKind (..), + Diagnostic (..), Position (..), + Range (..), TextEdit (..), Uri, + WorkspaceEdit (..)) +import Text.Regex.TDFA + +-- | Generate all code actions for given file, erroneous/unknown field and suggestions +fieldErrorAction + :: Uri + -- ^ File for which the diagnostic was generated + -> T.Text + -- ^ Original (unknown) field + -> [T.Text] + -- ^ Suggestions for the given file + -> Range + -- ^ Location of diagnostic + -> [CodeAction] +fieldErrorAction uri original suggestions range = + fmap mkCodeAction suggestions + where + mkCodeAction suggestion = + let + -- Range returned by cabal here represents fragment from start of offending identifier + -- to end of line, we modify this range to be to the end of the identifier + adjustRange (Range rangeFrom@(Position lineNr col) _) = + Range rangeFrom (Position lineNr (col + fromIntegral (T.length original))) + title = "Replace with " <> suggestion' + tedit = [TextEdit (adjustRange range ) suggestion'] + edit = WorkspaceEdit (Just $ Map.singleton uri tedit) Nothing Nothing + in CodeAction title (Just CodeActionKind_QuickFix) (Just []) Nothing Nothing (Just edit) Nothing Nothing + where + -- dropping colon from the end of suggestion + suggestion' = T.dropEnd 1 suggestion + +-- | Given a diagnostic returned by 'Ide.Plugin.Cabal.Diag.errorDiagnostic', +-- if it represents an "Unknown field"-error with incorrect identifier +-- then return the incorrect identifier together with original diagnostics. +fieldErrorName :: + Diagnostic -> + -- ^ Output of 'Ide.Plugin.Cabal.Diag.errorDiagnostic' + Maybe (T.Text, Diagnostic) + -- ^ Original (incorrect) field name with the suggested replacement +fieldErrorName diag = + mSuggestion (_message diag) >>= \case + [original] -> Just (original, diag) + _ -> Nothing + where + regex :: T.Text + regex = "Unknown field: \"(.*)\"" + mSuggestion msg = getMatch <$> (msg :: T.Text) =~~ regex + getMatch :: (T.Text, T.Text, T.Text, [T.Text]) -> [T.Text] + getMatch (_, _, _, results) = results diff --git a/plugins/hls-cabal-plugin/src/Ide/Plugin/Cabal/LicenseSuggest.hs b/plugins/hls-cabal-plugin/src/Ide/Plugin/Cabal/LicenseSuggest.hs index 8ff0f9e988..7da1277289 100644 --- a/plugins/hls-cabal-plugin/src/Ide/Plugin/Cabal/LicenseSuggest.hs +++ b/plugins/hls-cabal-plugin/src/Ide/Plugin/Cabal/LicenseSuggest.hs @@ -1,6 +1,4 @@ {-# LANGUAGE AllowAmbiguousTypes #-} -{-# LANGUAGE ExplicitNamespaces #-} -{-# LANGUAGE FlexibleContexts #-} {-# LANGUAGE LambdaCase #-} {-# LANGUAGE OverloadedStrings #-} module Ide.Plugin.Cabal.LicenseSuggest @@ -33,13 +31,12 @@ import qualified Text.Fuzzy.Parallel as Fuzzy -- with a suggestion, then return a 'CodeAction' for replacing the -- the incorrect license identifier with the suggestion. licenseErrorAction - :: Uri - -- ^ File for which the diagnostic was generated - -> Diagnostic - -- ^ Output of 'Ide.Plugin.Cabal.Diag.errorDiagnostic' + :: Int -- ^ Maximum number of suggestions to return + -> Uri -- ^ File for which the diagnostic was generated + -> Diagnostic -- ^ Output of 'Ide.Plugin.Cabal.Diag.errorDiagnostic' -> [CodeAction] -licenseErrorAction uri diag = - mkCodeAction <$> licenseErrorSuggestion (_message diag) +licenseErrorAction maxCompletions uri diag = + mkCodeAction <$> licenseErrorSuggestion maxCompletions (_message diag) where mkCodeAction (original, suggestion) = let @@ -68,22 +65,22 @@ licenseNames = map (T.pack . licenseId) [minBound .. maxBound] -- Results are sorted by best fit, and prefer solutions that have smaller -- length distance to the original word. -- --- >>> take 2 $ licenseErrorSuggestion (T.pack "Unknown SPDX license identifier: 'BSD3'") +-- >>> licenseErrorSuggestion 2 (T.pack "Unknown SPDX license identifier: 'BSD3'") -- [("BSD3","BSD-3-Clause"),("BSD3","BSD-3-Clause-LBNL")] licenseErrorSuggestion :: - T.Text - -- ^ Output of 'Ide.Plugin.Cabal.Diag.errorDiagnostic' + Int -- ^ Maximum number of suggestions to return + -> T.Text -- ^ Output of 'Ide.Plugin.Cabal.Diag.errorDiagnostic' -> [(T.Text, T.Text)] -- ^ (Original (incorrect) license identifier, suggested replacement) -licenseErrorSuggestion msg = +licenseErrorSuggestion maxCompletions msg = (getMatch <$> msg =~~ regex) >>= \case [original] -> - let matches = map Fuzzy.original $ Fuzzy.simpleFilter Fuzzy.defChunkSize Fuzzy.defMaxResults original licenseNames - in [(original,candidate) | candidate <- List.sortBy (lengthDistance original) matches] + let matches = map Fuzzy.original $ Fuzzy.simpleFilter Fuzzy.defChunkSize maxCompletions original licenseNames + in [(original,candidate) | candidate <- List.sortOn (lengthDistance original) matches] _ -> [] where regex :: T.Text regex = "Unknown SPDX license identifier: '(.*)'" getMatch :: (T.Text, T.Text, T.Text, [T.Text]) -> [T.Text] getMatch (_, _, _, results) = results - lengthDistance original x1 x2 = abs (T.length original - T.length x1) `compare` abs (T.length original - T.length x2) + lengthDistance original x = abs $ T.length original - T.length x diff --git a/plugins/hls-cabal-plugin/src/Ide/Plugin/Cabal/Orphans.hs b/plugins/hls-cabal-plugin/src/Ide/Plugin/Cabal/Orphans.hs new file mode 100644 index 0000000000..2264d5390f --- /dev/null +++ b/plugins/hls-cabal-plugin/src/Ide/Plugin/Cabal/Orphans.hs @@ -0,0 +1,24 @@ +{-# OPTIONS_GHC -Wno-orphans #-} +module Ide.Plugin.Cabal.Orphans where +import Control.DeepSeq +import Distribution.Fields.Field +import Distribution.Parsec.Position + +-- ---------------------------------------------------------------- +-- Cabal-syntax orphan instances we need sometimes +-- ---------------------------------------------------------------- + +instance NFData (Field Position) where + rnf (Field name fieldLines) = rnf name `seq` rnf fieldLines + rnf (Section name sectionArgs fields) = rnf name `seq` rnf sectionArgs `seq` rnf fields + +instance NFData (Name Position) where + rnf (Name ann fName) = rnf ann `seq` rnf fName + +instance NFData (FieldLine Position) where + rnf (FieldLine ann bs) = rnf ann `seq` rnf bs + +instance NFData (SectionArg Position) where + rnf (SecArgName ann bs) = rnf ann `seq` rnf bs + rnf (SecArgStr ann bs) = rnf ann `seq` rnf bs + rnf (SecArgOther ann bs) = rnf ann `seq` rnf bs diff --git a/plugins/hls-cabal-plugin/src/Ide/Plugin/Cabal/Outline.hs b/plugins/hls-cabal-plugin/src/Ide/Plugin/Cabal/Outline.hs new file mode 100644 index 0000000000..40f348f88c --- /dev/null +++ b/plugins/hls-cabal-plugin/src/Ide/Plugin/Cabal/Outline.hs @@ -0,0 +1,119 @@ +{-# LANGUAGE CPP #-} +{-# LANGUAGE DataKinds #-} +{-# LANGUAGE DuplicateRecordFields #-} +{-# LANGUAGE GADTs #-} +{-# LANGUAGE OverloadedStrings #-} +{-# LANGUAGE ViewPatterns #-} + +module Ide.Plugin.Cabal.Outline where + +import Control.Monad.IO.Class +import Data.Maybe +import qualified Data.Text as T +import Data.Text.Encoding (decodeUtf8) +import Development.IDE.Core.Rules +import Development.IDE.Core.Shake (IdeState (shakeExtras), + runIdeAction, + useWithStaleFast) +import Development.IDE.Types.Location (toNormalizedFilePath') +import Distribution.Fields.Field (Field (Field, Section), + Name (Name)) +import Distribution.Parsec.Position (Position) +import Ide.Plugin.Cabal.Completion.CabalFields (onelineSectionArgs) +import Ide.Plugin.Cabal.Completion.Types (ParseCabalFields (..), + cabalPositionToLSPPosition) +import Ide.Plugin.Cabal.Orphans () +import Ide.Types (PluginMethodHandler) +import Language.LSP.Protocol.Message (Method (..)) +import Language.LSP.Protocol.Types (DocumentSymbol (..)) +import qualified Language.LSP.Protocol.Types as LSP + + +moduleOutline :: PluginMethodHandler IdeState Method_TextDocumentDocumentSymbol +moduleOutline ideState _ LSP.DocumentSymbolParams {_textDocument = LSP.TextDocumentIdentifier uri} = + case LSP.uriToFilePath uri of + Just (toNormalizedFilePath' -> fp) -> do + mFields <- liftIO $ runIdeAction "cabal-plugin.fields" (shakeExtras ideState) (useWithStaleFast ParseCabalFields fp) + case fmap fst mFields of + Just fieldPositions -> pure $ LSP.InR (LSP.InL allSymbols) + where + allSymbols = mapMaybe documentSymbolForField fieldPositions + Nothing -> pure $ LSP.InL [] + Nothing -> pure $ LSP.InL [] + +-- | Creates a @DocumentSymbol@ object for the +-- cabal AST, without displaying @fieldLines@ and +-- displaying @Section Name@ and @SectionArgs@ in one line. +-- +-- @fieldLines@ are leaves of a cabal AST, so they are omitted +-- in the outline. Sections have to be displayed in one line, because +-- the AST representation looks unnatural. See examples: +-- +-- * part of a cabal file: +-- +-- > if impl(ghc >= 9.8) +-- > ghc-options: -Wall +-- +-- * AST representation: +-- +-- > if +-- > impl +-- > ( +-- > ghc >= 9.8 +-- > ) +-- > +-- > ghc-options: +-- > -Wall +-- +-- * resulting @DocumentSymbol@: +-- +-- > if impl(ghc >= 9.8) +-- > ghc-options: +-- > +documentSymbolForField :: Field Position -> Maybe DocumentSymbol +documentSymbolForField (Field (Name pos fieldName) _) = + Just + (defDocumentSymbol range) + { _name = decodeUtf8 fieldName, + _kind = LSP.SymbolKind_Field, + _children = Nothing + } + where + range = cabalPositionToLSPRange pos `addNameLengthToLSPRange` decodeUtf8 fieldName +documentSymbolForField (Section (Name pos fieldName) sectionArgs fields) = + Just + (defDocumentSymbol range) + { _name = joinedName, + _kind = LSP.SymbolKind_Object, + _children = + Just + (mapMaybe documentSymbolForField fields) + } + where + joinedName = decodeUtf8 fieldName <> " " <> onelineSectionArgs sectionArgs + range = cabalPositionToLSPRange pos `addNameLengthToLSPRange` joinedName + +-- | Creates a single point LSP range +-- using cabal position +cabalPositionToLSPRange :: Position -> LSP.Range +cabalPositionToLSPRange pos = LSP.Range lspPos lspPos + where + lspPos = cabalPositionToLSPPosition pos + +addNameLengthToLSPRange :: LSP.Range -> T.Text -> LSP.Range +addNameLengthToLSPRange (LSP.Range pos1 (LSP.Position line char)) name = + LSP.Range + pos1 + (LSP.Position line (char + fromIntegral (T.length name))) + +defDocumentSymbol :: LSP.Range -> DocumentSymbol +defDocumentSymbol range = DocumentSymbol + { _detail = Nothing + , _deprecated = Nothing + , _name = "" + , _kind = LSP.SymbolKind_File + , _range = range + , _selectionRange = range + , _children = Nothing + , _tags = Nothing + } diff --git a/plugins/hls-cabal-plugin/src/Ide/Plugin/Cabal/Parse.hs b/plugins/hls-cabal-plugin/src/Ide/Plugin/Cabal/Parse.hs index 28700c5104..e949af1b1d 100644 --- a/plugins/hls-cabal-plugin/src/Ide/Plugin/Cabal/Parse.hs +++ b/plugins/hls-cabal-plugin/src/Ide/Plugin/Cabal/Parse.hs @@ -1,13 +1,7 @@ +{-# LANGUAGE OverloadedStrings #-} module Ide.Plugin.Cabal.Parse ( parseCabalFileContents - -- * Re-exports -, FilePath -, NonEmpty(..) -, PWarning(..) -, Version -, PError(..) -, Position(..) -, GenericPackageDescription(..) +, readCabalFields ) where import qualified Data.ByteString as BS @@ -16,12 +10,31 @@ import Distribution.Fields (PError (..), PWarning (..)) import Distribution.Fields.ParseResult (runParseResult) import Distribution.PackageDescription.Parsec (parseGenericPackageDescription) -import Distribution.Parsec.Position (Position (..)) import Distribution.Types.GenericPackageDescription (GenericPackageDescription (..)) import Distribution.Types.Version (Version) +import qualified Ide.Plugin.Cabal.Diagnostics as Diagnostics + +import qualified Data.Text as T +import Development.IDE +import qualified Distribution.Fields.Parser as Syntax +import qualified Distribution.Parsec.Position as Syntax + parseCabalFileContents :: BS.ByteString -- ^ UTF-8 encoded bytestring -> IO ([PWarning], Either (Maybe Version, NonEmpty PError) GenericPackageDescription) parseCabalFileContents bs = pure $ runParseResult (parseGenericPackageDescription bs) + +readCabalFields :: + NormalizedFilePath -> + BS.ByteString -> + Either FileDiagnostic [Syntax.Field Syntax.Position] +readCabalFields file contents = do + case Syntax.readFields' contents of + Left parseError -> + Left $ Diagnostics.fatalParseErrorDiagnostic file + $ "Failed to parse cabal file: " <> T.pack (show parseError) + Right (fields, _warnings) -> do + -- we don't want to double report diagnostics, all diagnostics are produced by 'ParseCabalFile'. + Right fields diff --git a/plugins/hls-cabal-plugin/test/CabalAdd.hs b/plugins/hls-cabal-plugin/test/CabalAdd.hs new file mode 100644 index 0000000000..6517c811fe --- /dev/null +++ b/plugins/hls-cabal-plugin/test/CabalAdd.hs @@ -0,0 +1,185 @@ +{-# LANGUAGE OverloadedStrings #-} + +module CabalAdd ( + cabalAddTests, +) where + +import Control.Lens ((^.)) +import Control.Lens.Fold ((^?)) +import qualified Data.Maybe as Maybe +import qualified Data.Text as T +import qualified Data.Text.Internal.Search as T +import Distribution.Utils.Generic (safeHead) +import Ide.Plugin.Cabal.CabalAdd (hiddenPackageSuggestion) +import qualified Language.LSP.Protocol.Lens as L +import Language.LSP.Protocol.Types (Diagnostic (..), mkRange) +import System.FilePath +import Test.Hls (Session, TestTree, _R, anyMessage, + assertEqual, documentContents, + executeCodeAction, + getAllCodeActions, + getDocumentEdit, liftIO, openDoc, + skipManyTill, testCase, testGroup, + waitForDiagnosticsFrom, (@?=)) +import Utils + +cabalAddTests :: TestTree +cabalAddTests = + testGroup + "CabalAdd Tests" + [ runHaskellTestCaseSession "Code Actions - Can add hidden package to an executable" ("cabal-add-testdata" "cabal-add-exe") + (generateAddDependencyTestSession "cabal-add-exe.cabal" ("src" "Main.hs") "split" [253]) + , runHaskellTestCaseSession "Code Actions - Can add hidden package to a library" ("cabal-add-testdata" "cabal-add-lib") + (generateAddDependencyTestSession "cabal-add-lib.cabal" ("src" "MyLib.hs") "split" [348]) + , runHaskellTestCaseSession "Code Actions - Can add hidden package to a test" ("cabal-add-testdata" "cabal-add-tests") + (generateAddDependencyTestSession "cabal-add-tests.cabal" ("test" "Main.hs") "split" [478]) + , runHaskellTestCaseSession "Code Actions - Can add hidden package to a test with PackageImports" ("cabal-add-testdata" "cabal-add-tests") + (generateAddDependencyTestSession "cabal-add-tests.cabal" ("test" "MainPackageImports.hs") "split" [731]) + , runHaskellTestCaseSession "Code Actions - Can add hidden package to a benchmark" ("cabal-add-testdata" "cabal-add-bench") + (generateAddDependencyTestSession "cabal-add-bench.cabal" ("bench" "Main.hs") "split" [403]) + + , runHaskellTestCaseSession "Code Actions - Can add hidden package to an executable, multiple targets" ("cabal-add-testdata" "cabal-add-multitarget") + (generateAddDependencyTestSession "cabal-add-multitarget.cabal" ("src" "Main.hs") "split" [269]) + , runHaskellTestCaseSession "Code Actions - Can add hidden package to a library, multiple targets" ("cabal-add-testdata" "cabal-add-multitarget") + (generateAddDependencyTestSession "cabal-add-multitarget.cabal" ("lib" "MyLib.hs") "split" [413]) + , runHaskellTestCaseSession "Code Actions - Can add hidden package to an internal library, multiple targets" ("cabal-add-testdata" "cabal-add-multitarget") + (generateAddDependencyTestSession "cabal-add-multitarget.cabal" ("lib" "InternalLib.hs") "split" [413]) + , runHaskellTestCaseSession "Code Actions - Can add hidden package to a test, multiple targets" ("cabal-add-testdata" "cabal-add-multitarget") + (generateAddDependencyTestSession "cabal-add-multitarget.cabal" ("test" "Main.hs") "split" [655]) + , runHaskellTestCaseSession "Code Actions - Can add hidden package to a benchmark, multiple targets" ("cabal-add-testdata" "cabal-add-multitarget") + (generateAddDependencyTestSession "cabal-add-multitarget.cabal" ("bench" "Main.hs") "split" [776]) + + + , runHaskellTestCaseSession "Code Actions - Guard against HPack" ("cabal-add-testdata" "cabal-add-packageYaml") + (generatePackageYAMLTestSession ("src" "Main.hs")) + + , testHiddenPackageSuggestions "Check CabalAdd's parser, no version" + [ "It is a member of the hidden package 'base'" + , "It is a member of the hidden package 'Blammo-wai'" + , "It is a member of the hidden package 'BlastHTTP'" + , "It is a member of the hidden package 'CC-delcont-ref-tf'" + , "It is a member of the hidden package '3d-graphics-examples'" + , "It is a member of the hidden package 'AAI'" + , "It is a member of the hidden package 'AWin32Console'" + ] + [ ("base", T.empty) + , ("Blammo-wai", T.empty) + , ("BlastHTTP", T.empty) + , ("CC-delcont-ref-tf", T.empty) + , ("3d-graphics-examples", T.empty) + , ("AAI", T.empty) + , ("AWin32Console", T.empty) + ] + , testHiddenPackageSuggestions "Check CabalAdd's parser, with version" + [ "It is a member of the hidden package 'base-0.1.0.0'" + , "It is a member of the hidden package 'Blammo-wai-0.11.0'" + , "It is a member of the hidden package 'BlastHTTP-2.6.4.3'" + , "It is a member of the hidden package 'CC-delcont-ref-tf-0.0.0.2'" + , "It is a member of the hidden package '3d-graphics-examples-1.1.6'" + , "It is a member of the hidden package 'AAI-0.1'" + , "It is a member of the hidden package 'AWin32Console-1.19.1'" + ] + [ ("base","0.1.0.0") + , ("Blammo-wai", "0.11.0") + , ("BlastHTTP", "2.6.4.3") + , ("CC-delcont-ref-tf", "0.0.0.2") + , ("3d-graphics-examples", "1.1.6") + , ("AAI", "0.1") + , ("AWin32Console", "1.19.1") + ] + , testHiddenPackageSuggestions "Check CabalAdd's parser, no version, unicode comma" + [ "It is a member of the hidden package \8216base\8217" + , "It is a member of the hidden package \8216Blammo-wai\8217" + , "It is a member of the hidden package \8216BlastHTTP\8217" + , "It is a member of the hidden package \8216CC-delcont-ref-tf\8217" + , "It is a member of the hidden package \8216AAI\8217" + , "It is a member of the hidden package \8216AWin32Console\8217" + ] + [ ("base", T.empty) + , ("Blammo-wai", T.empty) + , ("BlastHTTP", T.empty) + , ("CC-delcont-ref-tf", T.empty) + , ("AAI", T.empty) + , ("AWin32Console", T.empty) + ] + , testHiddenPackageSuggestions "Check CabalAdd's parser, with version, unicode comma" + [ "It is a member of the hidden package \8216base-0.1.0.0\8217" + , "It is a member of the hidden package \8216Blammo-wai-0.11.0\8217" + , "It is a member of the hidden package \8216BlastHTTP-2.6.4.3\8217" + , "It is a member of the hidden package \8216CC-delcont-ref-tf-0.0.0.2\8217" + , "It is a member of the hidden package \8216AAI-0.1\8217" + , "It is a member of the hidden package \8216AWin32Console-1.19.1\8217" + ] + [ ("base","0.1.0.0") + , ("Blammo-wai", "0.11.0") + , ("BlastHTTP", "2.6.4.3") + , ("CC-delcont-ref-tf", "0.0.0.2") + , ("AAI", "0.1") + , ("AWin32Console", "1.19.1") + ] + , testHiddenPackageSuggestions "Check CabalAdd's parser, with version, unicode comma" + [ "It is a member of the hidden package \8216\&3d-graphics-examples\8217" + , "It is a member of the hidden package \8216\&3d-graphics-examples-1.1.6\8217" + ] + [ ("3d-graphics-examples", T.empty) + , ("3d-graphics-examples", "1.1.6") + ] + , testHiddenPackageSuggestions "Check CabalAdd's parser, with version, with PackageImports" + [ "(needs flag -package-id base-0.1.0.0)" + , "(needs flag -package-id Blammo-wai-0.11.0)" + , "(needs flag -package-id BlastHTTP-2.6.4.3)" + , "(needs flag -package-id CC-delcont-ref-tf-0.0.0.2)" + , "(needs flag -package-id 3d-graphics-examples-1.1.6)" + , "(needs flag -package-id AAI-0.1)" + , "(needs flag -package-id AWin32Console-1.19.1)" + ] + [ ("base","0.1.0.0") + , ("Blammo-wai", "0.11.0") + , ("BlastHTTP", "2.6.4.3") + , ("CC-delcont-ref-tf", "0.0.0.2") + , ("3d-graphics-examples", "1.1.6") + , ("AAI", "0.1") + , ("AWin32Console", "1.19.1") + ] + ] + where + generateAddDependencyTestSession :: FilePath -> FilePath -> T.Text -> [Int] -> Session () + generateAddDependencyTestSession cabalFile haskellFile dependency indicesRes = do + hsdoc <- openDoc haskellFile "haskell" + cabDoc <- openDoc cabalFile "cabal" + _ <- waitForDiagnosticsFrom hsdoc + cas <- Maybe.mapMaybe (^? _R) <$> getAllCodeActions hsdoc + let selectedCas = filter (\ca -> "Add dependency" `T.isPrefixOf` (ca ^. L.title)) cas + mapM_ executeCodeAction selectedCas + _ <- skipManyTill anyMessage $ getDocumentEdit cabDoc -- Wait for the changes in cabal file + contents <- documentContents cabDoc + liftIO $ assertEqual (T.unpack dependency <> " isn't found in the cabal file") indicesRes (T.indices dependency contents) + testHiddenPackageSuggestions :: String -> [T.Text] -> [(T.Text, T.Text)] -> TestTree + testHiddenPackageSuggestions testTitle messages suggestions = + let diags = map (\msg -> messageToDiagnostic msg ) messages + suggestions' = map (safeHead . hiddenPackageSuggestion) diags + assertions = zipWith (@?=) suggestions' (map Just suggestions) + testNames = map (\(f, s) -> "Check if " ++ T.unpack f ++ (if s == "" then "" else "-") ++ T.unpack s ++ " was parsed correctly") suggestions + test = testGroup testTitle $ zipWith testCase testNames assertions + in test + messageToDiagnostic :: T.Text -> Diagnostic + messageToDiagnostic msg = Diagnostic { + _range = mkRange 0 0 0 0 + , _severity = Nothing + , _code = Nothing + , _source = Nothing + , _message = msg + , _relatedInformation = Nothing + , _tags = Nothing + , _codeDescription = Nothing + , _data_ = Nothing + } + + + generatePackageYAMLTestSession :: FilePath -> Session () + generatePackageYAMLTestSession haskellFile = do + hsdoc <- openDoc haskellFile "haskell" + _ <- waitForDiagnosticsFrom hsdoc + cas <- Maybe.mapMaybe (^? _R) <$> getAllCodeActions hsdoc + let selectedCas = filter (\ca -> "Add dependency" `T.isPrefixOf` (ca ^. L.title)) cas + liftIO $ assertEqual "PackageYAML" [] selectedCas diff --git a/plugins/hls-cabal-plugin/test/Completer.hs b/plugins/hls-cabal-plugin/test/Completer.hs index 594678ad71..ab7165b1ac 100644 --- a/plugins/hls-cabal-plugin/test/Completer.hs +++ b/plugins/hls-cabal-plugin/test/Completer.hs @@ -1,24 +1,31 @@ {-# LANGUAGE DisambiguateRecordFields #-} {-# LANGUAGE OverloadedStrings #-} +{-# LANGUAGE QuasiQuotes #-} + module Completer where import Control.Lens ((^.), (^?)) import Control.Lens.Prism +import Control.Monad (forM_) import qualified Data.ByteString as ByteString +import qualified Data.ByteString.Char8 as BS8 import Data.Maybe (mapMaybe) import qualified Data.Text as T +import qualified Development.IDE.Plugin.Completions.Types as Ghcide +import qualified Distribution.Fields as Syntax +import Distribution.PackageDescription (GenericPackageDescription) import Distribution.PackageDescription.Parsec (parseGenericPackageDescriptionMaybe) +import qualified Distribution.Parsec.Position as Syntax import Ide.Plugin.Cabal.Completion.Completer.FilePath import Ide.Plugin.Cabal.Completion.Completer.Module import Ide.Plugin.Cabal.Completion.Completer.Paths +import Ide.Plugin.Cabal.Completion.Completer.Simple (importCompleter) import Ide.Plugin.Cabal.Completion.Completer.Types (CompleterData (..)) import Ide.Plugin.Cabal.Completion.Completions import Ide.Plugin.Cabal.Completion.Types (CabalPrefixInfo (..), StanzaName) -import Ide.Plugin.Cabal.Parse (GenericPackageDescription) import qualified Language.LSP.Protocol.Lens as L -import qualified Language.LSP.VFS as VFS import System.FilePath import Test.Hls import Utils @@ -33,7 +40,9 @@ completerTests = directoryCompleterTests, completionHelperTests, filePathExposedModulesTests, - exposedModuleCompleterTests + exposedModuleCompleterTests, + importCompleterTests, + autogenFieldCompletionTests ] basicCompleterTests :: TestTree @@ -51,12 +60,17 @@ basicCompleterTests = compls <- getCompletions doc (Position 8 2) let complTexts = getTextEditTexts compls liftIO $ assertBool "suggests benchmark" $ "benchmark" `elem` complTexts + , runCabalTestCaseSession "In top level context - stanza should be suggested" "" $ do + doc <- openDoc "completer.cabal" "cabal" + compls <- getCompletions doc (Position 13 2) + let complTexts = getTextEditTexts compls + liftIO $ assertBool "suggests common" $ "common" `elem` complTexts , runCabalTestCaseSession "Main-is completions should be relative to hs-source-dirs of same stanza" "filepath-completions" $ do doc <- openDoc "main-is.cabal" "cabal" compls <- getCompletions doc (Position 10 12) let complTexts = getTextEditTexts compls - liftIO $ assertBool "suggests f2" $ "./f2.hs" `elem` complTexts - liftIO $ assertBool "does not suggest" $ "./Content.hs" `notElem` complTexts + liftIO $ assertBool "suggests f2" $ "f2.hs" `elem` complTexts + liftIO $ assertBool "does not suggest" $ "Content.hs" `notElem` complTexts ] where getTextEditTexts :: [CompletionItem] -> [T.Text] @@ -66,21 +80,21 @@ fileCompleterTests :: TestTree fileCompleterTests = testGroup "File Completer Tests" - [ testCase "Current Directory" $ do + [ testCase "Current Directory - no leading ./ by default" $ do completions <- completeFilePath "" filePathComplTestDir - completions @?== ["./.hidden", "./Content.hs", "./dir1/", "./dir2/", "./textfile.txt", "./main-is.cabal"], + completions @?== [".hidden", "Content.hs", "dir1/", "dir2/", "textfile.txt", "main-is.cabal"], testCase "Current Directory - alternative writing" $ do completions <- completeFilePath "./" filePathComplTestDir completions @?== ["./.hidden", "./Content.hs", "./dir1/", "./dir2/", "./textfile.txt", "./main-is.cabal"], testCase "Current Directory - hidden file start" $ do completions <- completeFilePath "." filePathComplTestDir - completions @?== ["./Content.hs", "./.hidden", "./textfile.txt", "./main-is.cabal"], + completions @?== ["Content.hs", ".hidden", "textfile.txt", "main-is.cabal"], testCase "Current Directory - incomplete directory path written" $ do completions <- completeFilePath "di" filePathComplTestDir - completions @?== ["./dir1/", "./dir2/"], + completions @?== ["dir1/", "dir2/"], testCase "Current Directory - incomplete filepath written" $ do completions <- completeFilePath "te" filePathComplTestDir - completions @?== ["./Content.hs", "./textfile.txt"], + completions @?== ["Content.hs", "textfile.txt"], testCase "Subdirectory" $ do completions <- completeFilePath "dir1/" filePathComplTestDir completions @?== ["dir1/f1.txt", "dir1/f2.hs"], @@ -152,28 +166,28 @@ filePathCompletionContextTests = compls @?== ["f1.txt", "f2.hs"] ] where - simplePosPrefixInfo :: T.Text -> UInt -> UInt -> VFS.PosPrefixInfo + simplePosPrefixInfo :: T.Text -> UInt -> UInt -> Ghcide.PosPrefixInfo simplePosPrefixInfo lineString linePos charPos = - VFS.PosPrefixInfo - { VFS.fullLine = lineString, - VFS.prefixModule = "", - VFS.prefixText = "", - VFS.cursorPos = Position linePos charPos + Ghcide.PosPrefixInfo + { Ghcide.fullLine = lineString, + Ghcide.prefixScope = "", + Ghcide.prefixText = "", + Ghcide.cursorPos = Position linePos charPos } directoryCompleterTests :: TestTree directoryCompleterTests = testGroup "Directory Completer Tests" - [ testCase "Current Directory" $ do + [ testCase "Current Directory - no leading ./ by default" $ do completions <- completeDirectory "" filePathComplTestDir - completions @?== ["./dir1/", "./dir2/"], + completions @?== ["dir1/", "dir2/"], testCase "Current Directory - alternative writing" $ do completions <- completeDirectory "./" filePathComplTestDir completions @?== ["./dir1/", "./dir2/"], testCase "Current Directory - incomplete directory path written" $ do completions <- completeDirectory "di" filePathComplTestDir - completions @?== ["./dir1/", "./dir2/"], + completions @?== ["dir1/", "dir2/"], testCase "Current Directory - incomplete filepath written" $ do completions <- completeDirectory "te" filePathComplTestDir completions @?== [], @@ -228,11 +242,11 @@ completionHelperTests = getFilePathCursorPrefix :: T.Text -> UInt -> UInt -> T.Text getFilePathCursorPrefix lineString linePos charPos = completionPrefix . getCabalPrefixInfo "" $ - VFS.PosPrefixInfo - { VFS.fullLine = lineString, - VFS.prefixModule = "", - VFS.prefixText = "", - VFS.cursorPos = Position linePos charPos + Ghcide.PosPrefixInfo + { Ghcide.fullLine = lineString, + Ghcide.prefixScope = "", + Ghcide.prefixText = "", + Ghcide.cursorPos = Position linePos charPos } filePathExposedModulesTests :: TestTree @@ -285,23 +299,78 @@ exposedModuleCompleterTests = completions @?== [] ] where - simpleCompleterData :: Maybe StanzaName -> FilePath -> T.Text -> CompleterData - simpleCompleterData sName dir pref = do - CompleterData - { cabalPrefixInfo = simpleExposedCabalPrefixInfo pref dir, - getLatestGPD = do - cabalContents <- ByteString.readFile $ testDataDir "exposed.cabal" - pure $ parseGenericPackageDescriptionMaybe cabalContents, - stanzaName = sName - } callModulesCompleter :: Maybe StanzaName -> (Maybe StanzaName -> GenericPackageDescription -> [FilePath]) -> T.Text -> IO [T.Text] callModulesCompleter sName func prefix = do let cData = simpleCompleterData sName testDataDir prefix completer <- modulesCompleter func mempty cData pure $ fmap extract completer +-- TODO: These tests are a bit barebones at the moment, +-- since we do not take cursorposition into account at this point. +importCompleterTests :: TestTree +importCompleterTests = + testGroup + "Import Completer Tests" + [ testCase "All above common sections are suggested" $ do + completions <- callImportCompleter + ("defaults" `elem` completions) @? "defaults contained" + ("test-defaults" `elem` completions) @? "test-defaults contained" + -- TODO: Only common sections defined before the current stanza may be imported + , testCase "Common sections occuring below are not suggested" $ do + completions <- callImportCompleter + ("notForLib" `elem` completions) @? "notForLib contained, this needs to be fixed" + , testCase "All common sections are suggested when curser is below them" $ do + completions <- callImportCompleter + completions @?== ["defaults", "notForLib" ,"test-defaults"] + ] + where + callImportCompleter :: IO [T.Text] + callImportCompleter = do + let cData' = simpleCompleterData Nothing testDataDir "" + let cabalCommonSections = [makeCommonSection 13 0 "defaults", makeCommonSection 18 0 "test-defaults", makeCommonSection 27 0 "notForLib"] + let cData = cData' {getCabalCommonSections = pure $ Just cabalCommonSections} + completer <- importCompleter mempty cData + pure $ fmap extract completer + makeCommonSection :: Int -> Int -> String -> Syntax.Field Syntax.Position + makeCommonSection row col name = + Syntax.Section + (Syntax.Name (Syntax.Position row col) "common") + [Syntax.SecArgName (Syntax.Position row (col + 7)) (BS8.pack name)] + [] + +autogenFieldCompletionTests :: TestTree +autogenFieldCompletionTests = + testGroup "Autogen Field Completer Tests" + [ testAutogenField "library" "completion/autogen-completion.cabal" (Position 6 9) ["autogen-modules:", "autogen-includes:"] + , testAutogenField "executable" "completion/autogen-completion.cabal" (Position 11 9) ["autogen-modules:", "autogen-includes:"] + , testAutogenField "test-suite" "completion/autogen-completion.cabal" (Position 16 9) ["autogen-modules:", "autogen-includes:"] + , testAutogenField "benchmark" "completion/autogen-completion.cabal" (Position 21 9) ["autogen-modules:", "autogen-includes:"] + , testAutogenField "common" "completion/autogen-completion.cabal" (Position 24 9) ["autogen-modules:", "autogen-includes:"] + ] + + where + testAutogenField :: String -> FilePath -> Position -> [T.Text] -> TestTree + testAutogenField section file pos expected = runCabalTestCaseSession ("autogen-modules completion in " <> section) "" $ do + doc <- openDoc file "cabal" + items <- getCompletions doc pos + let labels = map (^. L.label) items + liftIO $ forM_ expected $ \expect -> + assertBool (T.unpack expect <> " not found in " <> section) $ + any (expect `T.isInfixOf`) labels + +simpleCompleterData :: Maybe StanzaName -> FilePath -> T.Text -> CompleterData +simpleCompleterData sName dir pref = do + CompleterData + { cabalPrefixInfo = simpleExposedCabalPrefixInfo pref dir, + getLatestGPD = do + cabalContents <- ByteString.readFile $ testDataDir "exposed.cabal" + pure $ parseGenericPackageDescriptionMaybe cabalContents, + getCabalCommonSections = undefined, + stanzaName = sName + } + mkCompleterData :: CabalPrefixInfo -> CompleterData -mkCompleterData prefInfo = CompleterData {getLatestGPD = undefined, cabalPrefixInfo = prefInfo, stanzaName = Nothing} +mkCompleterData prefInfo = CompleterData {getLatestGPD = undefined, getCabalCommonSections = undefined, cabalPrefixInfo = prefInfo, stanzaName = Nothing} exposedTestDir :: FilePath exposedTestDir = addTrailingPathSeparator $ testDataDir "src-modules" @@ -321,3 +390,41 @@ extract :: CompletionItem -> T.Text extract item = case item ^. L.textEdit of Just (InL v) -> v ^. L.newText _ -> error "" + +importTestData :: T.Text +importTestData = [trimming| +cabal-version: 3.0 +name: hls-cabal-plugin +version: 0.1.0.0 +synopsis: +homepage: +license: MIT +license-file: LICENSE +author: Fendor +maintainer: fendor@posteo.de +category: Development +extra-source-files: CHANGELOG.md + +common defaults + default-language: GHC2021 + -- Should have been in GHC2021, an oversight + default-extensions: ExplicitNamespaces + +common test-defaults + ghc-options: -threaded -rtsopts -with-rtsopts=-N + +library + import: + ^ + exposed-modules: IDE.Plugin.Cabal + build-depends: base ^>=4.14.3.0 + hs-source-dirs: src + default-language: Haskell2010 + +common notForLib + default-language: GHC2021 + +test-suite tests + import: + ^ +|] diff --git a/plugins/hls-cabal-plugin/test/Context.hs b/plugins/hls-cabal-plugin/test/Context.hs index e2a7b0290e..8e6176bc5b 100644 --- a/plugins/hls-cabal-plugin/test/Context.hs +++ b/plugins/hls-cabal-plugin/test/Context.hs @@ -1,18 +1,20 @@ {-# LANGUAGE DisambiguateRecordFields #-} {-# LANGUAGE LambdaCase #-} {-# LANGUAGE OverloadedStrings #-} +{-# LANGUAGE QuasiQuotes #-} module Context where -import Control.Monad.Trans.Maybe (runMaybeT) import qualified Data.Text as T -import qualified Data.Text.Utf16.Rope as Rope +import qualified Data.Text.Encoding as Text +import Development.IDE.Plugin.Completions.Types (PosPrefixInfo (..)) import Ide.Plugin.Cabal import Ide.Plugin.Cabal.Completion.Completer.Paths import Ide.Plugin.Cabal.Completion.Completions import Ide.Plugin.Cabal.Completion.Types (Context, FieldContext (KeyWord, None), StanzaContext (Stanza, TopLevel)) +import qualified Ide.Plugin.Cabal.Parse as Parse import Test.Hls import Utils as T @@ -22,7 +24,7 @@ cabalPlugin = mkPluginTestDescriptor descriptor "cabal context" contextTests :: TestTree contextTests = testGroup - "Context Tests " + "Context Tests" [ pathCompletionInfoFromCompletionContextTests , getContextTests ] @@ -31,12 +33,12 @@ pathCompletionInfoFromCompletionContextTests :: TestTree pathCompletionInfoFromCompletionContextTests = testGroup "Completion Info to Completion Context Tests" - [ testCase "Current Directory" $ do + [ testCase "Current Directory - no leading ./ by default" $ do let complInfo = pathCompletionInfoFromCabalPrefixInfo "" $ simpleCabalPrefixInfoFromFp "" testDataDir - queryDirectory complInfo @?= "./" + queryDirectory complInfo @?= "" , testCase "Current Directory - partly written next" $ do let complInfo = pathCompletionInfoFromCabalPrefixInfo "" $ simpleCabalPrefixInfoFromFp "di" testDataDir - queryDirectory complInfo @?= "./" + queryDirectory complInfo @?= "" pathSegment complInfo @?= "di" , testCase "Current Directory - alternative writing" $ do let complInfo = pathCompletionInfoFromCabalPrefixInfo "" $ simpleCabalPrefixInfoFromFp "./" testDataDir @@ -58,39 +60,39 @@ pathCompletionInfoFromCompletionContextTests = getContextTests :: TestTree getContextTests = testGroup - "Context Tests" + "Context Tests Real" [ testCase "Empty File - Start" $ do -- for a completely empty file, the context needs to -- be top level without a specified keyword - ctx <- callGetContext (Position 0 0) "" [""] + ctx <- callGetContext (Position 0 0) "" "" ctx @?= (TopLevel, None) , testCase "Cabal version keyword - no value, no space after :" $ do -- on a file, where the keyword is already written -- the context should still be toplevel but the keyword should be recognized - ctx <- callGetContext (Position 0 14) "" ["cabal-version:"] + ctx <- callGetContext (Position 0 14) "" "cabal-version:\n" ctx @?= (TopLevel, KeyWord "cabal-version:") , testCase "Cabal version keyword - cursor in keyword" $ do -- on a file, where the keyword is already written -- but the cursor is in the middle of the keyword, -- we are not in a keyword context - ctx <- callGetContext (Position 0 5) "cabal" ["cabal-version:"] + ctx <- callGetContext (Position 0 5) "cabal" "cabal-version:\n" ctx @?= (TopLevel, None) , testCase "Cabal version keyword - no value, many spaces" $ do -- on a file, where the "cabal-version:" keyword is already written -- the context should still be top level but the keyword should be recognized - ctx <- callGetContext (Position 0 45) ("") ["cabal-version:" <> T.replicate 50 " "] + ctx <- callGetContext (Position 0 45) "" ("cabal-version:" <> T.replicate 50 " " <> "\n") ctx @?= (TopLevel, KeyWord "cabal-version:") , testCase "Cabal version keyword - keyword partly written" $ do -- in the first line of the file, if the keyword -- has not been written completely, the keyword context -- should still be None - ctx <- callGetContext (Position 0 5) "cabal" ["cabal"] + ctx <- callGetContext (Position 0 5) "cabal" "cabal" ctx @?= (TopLevel, None) , testCase "Cabal version keyword - value partly written" $ do -- in the first line of the file, if the keyword -- has not been written completely, the keyword context -- should still be None - ctx <- callGetContext (Position 0 17) "1." ["cabal-version: 1."] + ctx <- callGetContext (Position 0 17) "1." "cabal-version: 1." ctx @?= (TopLevel, KeyWord "cabal-version:") , testCase "Inside Stanza - no keyword" $ do -- on a file, where the library stanza has been defined @@ -102,14 +104,15 @@ getContextTests = -- has been defined, the keyword and stanza should be recognized ctx <- callGetContext (Position 4 21) "" libraryStanzaData ctx @?= (Stanza "library" Nothing, KeyWord "build-depends:") - , expectFailBecause "While not valid, it is not that important to make the code more complicated for this" $ - testCase "Cabal version keyword - no value, next line" $ do - -- if the cabal version keyword has been written but without a value, - -- in the next line we still should be in top level context with no keyword - -- since the cabal version keyword and value pair need to be in the same line - ctx <- callGetContext (Position 1 2) "" ["cabal-version:", ""] - ctx @?= (TopLevel, None) - , testCase "Non-cabal-version keyword - no value, next line indentented position" $ do + , testCase "Cabal version keyword - no value, next line" $ do + -- if the cabal version keyword has been written but without a value, + -- in the next line we still should be in top level context with no keyword + -- since the cabal version keyword and value pair need to be in the same line. + -- However, that's too much work to implement for virtually no benefit, so we + -- test here the status-quo is satisfied. + ctx <- callGetContext (Position 1 2) "" "cabal-version:\n\n" + ctx @?= (TopLevel, KeyWord "cabal-version:") + , testCase "Non-cabal-version keyword - no value, next line indented position" $ do -- if a keyword, other than the cabal version keyword has been written -- with no value, in the next line we still should be in top level keyword context -- of the keyword with no value, since its value may be written in the next line @@ -151,48 +154,156 @@ getContextTests = , testCase "Top level - cursor in later line with partially written value" $ do ctx <- callGetContext (Position 5 13) "eee" topLevelData ctx @?= (TopLevel, KeyWord "name:") + , testCase "If is ignored" $ do + ctx <- callGetContext (Position 5 18) "" conditionalData + ctx @?= (Stanza "library" Nothing, None) + , testCase "Elif is ignored" $ do + ctx <- callGetContext (Position 7 18) "" conditionalData + ctx @?= (Stanza "library" Nothing, None) + , testCase "Else is ignored" $ do + ctx <- callGetContext (Position 9 18) "" conditionalData + ctx @?= (Stanza "library" Nothing, KeyWord "buildable:") , testCase "Named Stanza" $ do ctx <- callGetContext (Position 2 18) "" executableStanzaData - ctx @?= (Stanza "executable" (Just "exeName"), None) + ctx @?= (TopLevel, None) + , testCase "Multi line, finds context in same line" $ do + ctx <- callGetContext (Position 5 18) "" multiLineOptsData + ctx @?= (Stanza "library" Nothing, KeyWord "build-depends:") + , testCase "Multi line, in the middle of option" $ do + ctx <- callGetContext (Position 6 11) "" multiLineOptsData + ctx @?= (Stanza "library" Nothing, KeyWord "build-depends:") + , testCase "Multi line, finds context in between lines" $ do + ctx <- callGetContext (Position 7 8) "" multiLineOptsData + ctx @?= (Stanza "library" Nothing, KeyWord "build-depends:") + , testCase "Multi line, finds context in between lines, start if line" $ do + ctx <- callGetContext (Position 7 0) "" multiLineOptsData + ctx @?= (TopLevel, None) + , testCase "Multi line, end of option" $ do + ctx <- callGetContext (Position 8 14) "" multiLineOptsData + ctx @?= (Stanza "library" Nothing, KeyWord "build-depends:") + , parameterisedCursorTest "Contexts in large testfile" multiPositionTestData + [ (TopLevel, None) + , (TopLevel, KeyWord "cabal-version:") + , (TopLevel, None) + , (TopLevel, KeyWord "description:") + , (TopLevel, KeyWord "extra-source-files:") + , (TopLevel, None) + -- this might not be what we want, maybe add another Context + , (TopLevel, None) + -- this might not be what we want, maybe add another Context + , (TopLevel, None) + , (Stanza "source-repository" (Just "head"), None) + , (Stanza "source-repository" (Just "head"), KeyWord "type:") + , (Stanza "source-repository" (Just "head"), KeyWord "type:") + , (Stanza "source-repository" (Just "head"), KeyWord "type:") + , (Stanza "source-repository" (Just "head"), None) + , (Stanza "common" (Just "cabalfmt"), None) + , (Stanza "common" (Just "cabalfmt"), None) + , (Stanza "common" (Just "cabalfmt"), KeyWord "build-depends:") + ] + $ \fileContent posPrefInfo -> + callGetContext (cursorPos posPrefInfo) (prefixText posPrefInfo) fileContent ] where - callGetContext :: Position -> T.Text -> [T.Text] -> IO Context + callGetContext :: Position -> T.Text -> T.Text -> IO Context callGetContext pos pref ls = do - runMaybeT (getContext mempty (simpleCabalPrefixInfoFromPos pos pref) (Rope.fromText $ T.unlines ls)) - >>= \case - Nothing -> assertFailure "Context must be found" - Just ctx -> pure ctx + case Parse.readCabalFields "not-real" (Text.encodeUtf8 ls) of + Left err -> fail $ show err + Right fields -> do + getContext mempty (simpleCabalPrefixInfoFromPos pos pref) fields -- ------------------------------------------------------------------------ -- Test Data -- ------------------------------------------------------------------------ -libraryStanzaData :: [T.Text] -libraryStanzaData = - [ "cabal-version: 3.0" - , "name: simple-cabal" - , "library " - , " default-language: Haskell98" - , " build-depends: " - , " " - , "ma " - ] - -executableStanzaData :: [T.Text] -executableStanzaData = - [ "cabal-version: 3.0" - , "name: simple-cabal" - , "executable exeName" - , " default-language: Haskell2010" - , " hs-source-dirs: test/preprocessor" - ] - -topLevelData :: [T.Text] -topLevelData = - [ "cabal-version: 3.0" - , "name:" - , "" - , "" - , "" - , " eee" - ] +libraryStanzaData :: T.Text +libraryStanzaData = [trimming| +cabal-version: 3.0 +name: simple-cabal +library + default-language: Haskell98 + build-depends: + +ma +|] + +executableStanzaData :: T.Text +executableStanzaData = [trimming| +cabal-version: 3.0 +name: simple-cabal +executable exeName + default-language: Haskell2010 + hs-source-dirs: test/preprocessor +|] + +topLevelData :: T.Text +topLevelData = [trimming| +cabal-version: 3.0 +name: + + + + eee +|] + +conditionalData :: T.Text +conditionalData = [trimming| +cabal-version: 3.0 +name: simple-cabal +library + if os(windows) + buildable: + elif os(linux) + buildable: + else + buildable: +|] +multiLineOptsData :: T.Text +multiLineOptsData = [trimming| +cabal-version: 3.0 +name: + + +library + build-depends: + base, + + text , +|] + +multiPositionTestData :: T.Text +multiPositionTestData = [trimming| +cabal-version: 3.4 + ^ ^ +category: Development +^ +name: haskell-language-server +description: + Please see the README on GitHub at + ^ +extra-source-files: + README.md + ChangeLog.md + test/testdata/**/*.project + test/testdata/**/*.cabal + test/testdata/**/*.yaml + test/testdata/**/*.hs + test/testdata/**/*.json + ^ + -- These globs should only match test/testdata + plugins/**/*.project + +source-repository head + ^ ^ ^ + type: git + ^ ^ ^ ^ + location: https://github.com/haskell/haskell-language-server + + ^ +common cabalfmt + + ^ + build-depends: haskell-language-server:hls-cabal-fmt-plugin + ^ ^ + cpp-options: -Dhls_cabalfmt +|] diff --git a/plugins/hls-cabal-plugin/test/Definition.hs b/plugins/hls-cabal-plugin/test/Definition.hs new file mode 100644 index 0000000000..33163c03eb --- /dev/null +++ b/plugins/hls-cabal-plugin/test/Definition.hs @@ -0,0 +1,126 @@ +{-# LANGUAGE OverloadedStrings #-} + +module Definition ( + gotoDefinitionTests, +) where + +import Control.Lens ((^.)) +import Data.List.Extra (isSuffixOf) +import qualified Data.Text as T +import Ide.Plugin.Cabal.Definition (toHaskellFile) +import qualified Language.LSP.Protocol.Lens as L +import qualified Language.LSP.Protocol.Types as LSP +import System.FilePath +import Test.Hls +import Utils + + +gotoDefinitionTests :: TestTree +gotoDefinitionTests = testGroup "Goto Definition" + [ gotoCommonSectionDefinitionTests + , gotoModuleDefinitionTests + ] + +gotoModuleDefinitionTests :: TestTree +gotoModuleDefinitionTests = testGroup "Goto Module Definition" + [ testGoToDefinitionLink "simple cabal test" "simple-cabal" "simple-cabal.cabal" + (Position 8 23) (toTestHaskellPath "" "A") + + , testGoToDefinitionLink "library start of exposed-modules" ("goto-definition" "modules") "module-examples.cabal" + (Position 6 22) (toTestHaskellPath "src" "Library.Lib") + , testGoToDefinitionLink "library middle of exposed-modules" ("goto-definition" "modules") "module-examples.cabal" + (Position 6 29) (toTestHaskellPath "src" "Library.Lib") + , testGoToDefinitionLink "library end of exposed-modules" ("goto-definition" "modules") "module-examples.cabal" + (Position 6 33) (toTestHaskellPath "src" "Library.Lib") + , testGoToDefinitionLink "library start of other-modules" ("goto-definition" "modules") "module-examples.cabal" + (Position 9 22) (toTestHaskellPath "src" "Library.Other.OtherLib") + , testGoToDefinitionLink "library end of other-modules" ("goto-definition" "modules") "module-examples.cabal" + (Position 9 44) (toTestHaskellPath "src" "Library.Other.OtherLib") + + , testGoToDefinitionLink "executable other-modules" ("goto-definition" "modules") "module-examples.cabal" + (Position 22 10) (toTestHaskellPath ("src" "exe") "Config") + + , testGoToDefinitionLink "test-suite other-modules Config" ("goto-definition" "modules") "module-examples.cabal" + (Position 31 10) (toTestHaskellPath ("src" "test") "Config") + , testGoToDefinitionLink "test-suite other-modules Library" ("goto-definition" "modules") "module-examples.cabal" + (Position 34 10) (toTestHaskellPath ("src" "test") "Library") + + , testGoToDefinitionLink "benchmark other-modules Config" ("goto-definition" "modules") "module-examples.cabal" + (Position 45 30) (toTestHaskellPath ("src" "bench") "Config") + + , testGoToDefinitionLinkNoLocation "not existent module" ("goto-definition" "modules") "module-examples.cabal" (Position 48 25) + , testGoToDefinitionLinkNoLocation "behind module" ("goto-definition" "modules") "module-examples.cabal" (Position 9 20) + , testGoToDefinitionLinkNoLocation "after module" ("goto-definition" "modules") "module-examples.cabal" (Position 9 50) + ] + where + toTestHaskellPath :: FilePath -> T.Text -> FilePath + toTestHaskellPath dir moduleName = dir toHaskellFile moduleName + + getUriFromDefinition :: Show b => (Definition |? b) -> Uri + getUriFromDefinition (InL (Definition (InL loc))) = loc^.L.uri + getUriFromDefinition unk = error $ "Unexpected pattern '" ++ show unk ++ "' , expected '(InL (Definition (InL loc))'" + + testGoToDefinitionLink :: TestName -> FilePath -> FilePath -> Position -> FilePath -> TestTree + testGoToDefinitionLink testName testDir cabalFile cursorPos expectedFilePath = + runCabalTestCaseSession testName testDir $ do + doc <- openDoc cabalFile "cabal" + definitions <- getDefinitions doc cursorPos + let uri = getUriFromDefinition definitions + mFilePath = (testDir ) <$> uriToFilePath uri + case mFilePath of + Nothing -> error $ "Not possible to convert Uri " <> show uri <> " to FilePath" + Just filePath -> do + let filePathWithDir = testDir expectedFilePath + isCorrectPath = filePathWithDir `isSuffixOf` filePath + liftIO $ isCorrectPath @? ("Absolute path expected to end on " <> filePathWithDir <> + " but " <> filePath <> " was given.") + + testGoToDefinitionLinkNoLocation :: TestName -> FilePath -> FilePath -> Position -> TestTree + testGoToDefinitionLinkNoLocation testName testDir cabalFile cursorPos = + runCabalTestCaseSession testName testDir $ do + doc <- openDoc cabalFile "cabal" + empty <- getDefinitions doc cursorPos + liftIO $ empty @?= (InR $ InR LSP.Null) + +gotoCommonSectionDefinitionTests :: TestTree +gotoCommonSectionDefinitionTests = testGroup "Goto Common Section Definition" + [ positiveTest "middle of identifier" (Position 27 16) (mkRange 6 0 7 22) + , positiveTest "left of identifier" (Position 30 12) (mkRange 10 0 17 40) + , positiveTest "right of identifier" (Position 33 22) (mkRange 20 0 23 34) + , positiveTest "left of '-' in identifier" (Position 36 20) (mkRange 6 0 7 22) + , positiveTest "right of '-' in identifier" (Position 39 19) (mkRange 10 0 17 40) + , positiveTest "identifier in identifier list" (Position 42 16) (mkRange 20 0 23 34) + , positiveTest "left of ',' right of identifier" (Position 45 33) (mkRange 10 0 17 40) + , positiveTest "right of ',' left of identifier" (Position 48 34) (mkRange 6 0 7 22) + + , negativeTest "right of ',' left of space" (Position 51 23) + , negativeTest "right of ':' left of space" (Position 54 11) + , negativeTest "not a definition" (Position 57 8) + , negativeTest "empty space" (Position 59 7) + ] + where + getRangeFromDefinition :: Show b => (Definition |? b) -> Range + getRangeFromDefinition (InL (Definition (InL loc))) = loc^.L.range + getRangeFromDefinition unk = error $ "Unexpected pattern '" ++ show unk ++ "' , expected '(InL (Definition (InL loc))'" + + -- A positive test checks if the provided range is equal + -- to the expected range from the definition in the test file. + -- The test emulates a goto-definition request of an actual definition. + positiveTest :: TestName -> Position -> Range -> TestTree + positiveTest testName cursorPos expectedRange = + runCabalTestCaseSession testName ("goto-definition" "common-section") $ do + doc <- openDoc "simple-with-common.cabal" "cabal" + definitions <- getDefinitions doc cursorPos + let range = getRangeFromDefinition definitions + liftIO $ range @?= expectedRange + + -- A negative test checks if the request failed and + -- the provided result is empty, i.e. `InR $ InR Null`. + -- The test emulates a goto-definition request of anything but an + -- actual definition. + negativeTest :: TestName -> Position -> TestTree + negativeTest testName cursorPos = + runCabalTestCaseSession testName ("goto-definition" "common-section") $ do + doc <- openDoc "simple-with-common.cabal" "cabal" + empty <- getDefinitions doc cursorPos + liftIO $ empty @?= (InR $ InR LSP.Null) diff --git a/plugins/hls-cabal-plugin/test/Main.hs b/plugins/hls-cabal-plugin/test/Main.hs index 4ee8afac28..fcb85a081e 100644 --- a/plugins/hls-cabal-plugin/test/Main.hs +++ b/plugins/hls-cabal-plugin/test/Main.hs @@ -1,25 +1,27 @@ +{-# LANGUAGE CPP #-} {-# LANGUAGE DisambiguateRecordFields #-} -{-# LANGUAGE NamedFieldPuns #-} -{-# LANGUAGE OverloadedLabels #-} {-# LANGUAGE OverloadedStrings #-} -{-# LANGUAGE TypeOperators #-} module Main ( main, ) where +import CabalAdd (cabalAddTests) import Completer (completerTests) import Context (contextTests) import Control.Lens ((^.)) +import Control.Lens.Fold ((^?)) import Control.Monad (guard) import qualified Data.ByteString as BS import Data.Either (isRight) -import Data.Row +import Data.List.Extra (nubOrdOn) +import qualified Data.Maybe as Maybe import qualified Data.Text as T -import qualified Data.Text as Text +import Definition (gotoDefinitionTests) import Ide.Plugin.Cabal.LicenseSuggest (licenseErrorSuggestion) import qualified Ide.Plugin.Cabal.Parse as Lib import qualified Language.LSP.Protocol.Lens as L +import Outline (outlineTests) import System.FilePath import Test.Hls import Utils @@ -33,6 +35,10 @@ main = do , pluginTests , completerTests , contextTests + , outlineTests + , codeActionTests + , gotoDefinitionTests + , hoverTests ] -- ------------------------------------------------------------------------ @@ -64,15 +70,24 @@ codeActionUnitTests = "Code Action Tests" [ testCase "Unknown format" $ do -- the message has the wrong format - licenseErrorSuggestion "Unknown license identifier: 'BSD3' Do you mean BSD-3-Clause?" @?= [] + licenseErrorSuggestion maxCompletions "Unknown license identifier: 'BSD3' Do you mean BSD-3-Clause?" @?= [] , testCase "BSD-3-Clause" $ do - take 2 (licenseErrorSuggestion "Unknown SPDX license identifier: 'BSD3' Do you mean BSD-3-Clause?") - @?= [("BSD3", "BSD-3-Clause"), ("BSD3", "BSD-3-Clause-LBNL")] + take 2 (licenseErrorSuggestion maxCompletions "Unknown SPDX license identifier: 'BSD3' Do you mean BSD-3-Clause?") + @?= +-- Cabal-syntax 3.12.0.0 added bunch of new licenses, so now more licenses match "BSD3" pattern +#if MIN_VERSION_Cabal_syntax(3,12,0) + [("BSD3", "BSD-4.3RENO"), ("BSD3", "BSD-3-Clause")] +#else + [("BSD3", "BSD-3-Clause"), ("BSD3", "BSD-3-Clause-LBNL")] +#endif , testCase "MiT" $ do -- contains no suggestion - take 2 (licenseErrorSuggestion "Unknown SPDX license identifier: 'MiT'") + take 2 (licenseErrorSuggestion maxCompletions "Unknown SPDX license identifier: 'MiT'") @?= [("MiT", "MIT"), ("MiT", "MIT-0")] ] + where + maxCompletions = 100 + -- ------------------------ ------------------------------------------------ -- Integration Tests @@ -85,110 +100,162 @@ pluginTests = [ testGroup "Diagnostics" [ runCabalTestCaseSession "Publishes Diagnostics on Error" "" $ do - doc <- openDoc "invalid.cabal" "cabal" - diags <- waitForDiagnosticsFromSource doc "cabal" + _ <- openDoc "invalid.cabal" "cabal" + diags <- cabalCaptureKick unknownLicenseDiag <- liftIO $ inspectDiagnostic diags ["Unknown SPDX license identifier: 'BSD3'"] liftIO $ do length diags @?= 1 unknownLicenseDiag ^. L.range @?= Range (Position 3 24) (Position 4 0) unknownLicenseDiag ^. L.severity @?= Just DiagnosticSeverity_Error + , runCabalTestCaseSession "Publishes Diagnostics on unsupported cabal version as Warning" "" $ do + _ <- openDoc "unsupportedVersion.cabal" "cabal" + diags <- cabalCaptureKick + unknownVersionDiag <- liftIO $ inspectDiagnosticAny diags ["Unsupported cabal-version 99999.0", "Unsupported cabal format version in cabal-version field: 99999.0"] + liftIO $ do + length diags @?= 1 + unknownVersionDiag ^. L.range @?= Range (Position 0 0) (Position 1 0) + unknownVersionDiag ^. L.severity @?= Just DiagnosticSeverity_Warning , runCabalTestCaseSession "Clears diagnostics" "" $ do doc <- openDoc "invalid.cabal" "cabal" - diags <- waitForDiagnosticsFrom doc + diags <- cabalCaptureKick unknownLicenseDiag <- liftIO $ inspectDiagnostic diags ["Unknown SPDX license identifier: 'BSD3'"] liftIO $ do length diags @?= 1 unknownLicenseDiag ^. L.range @?= Range (Position 3 24) (Position 4 0) unknownLicenseDiag ^. L.severity @?= Just DiagnosticSeverity_Error _ <- applyEdit doc $ TextEdit (Range (Position 3 20) (Position 4 0)) "BSD-3-Clause\n" - newDiags <- waitForDiagnosticsFrom doc + newDiags <- cabalCaptureKick liftIO $ newDiags @?= [] , runCabalTestCaseSession "No Diagnostics in .hs files from valid .cabal file" "simple-cabal" $ do hsDoc <- openDoc "A.hs" "haskell" expectNoMoreDiagnostics 1 hsDoc "typechecking" cabalDoc <- openDoc "simple-cabal.cabal" "cabal" expectNoMoreDiagnostics 1 cabalDoc "parsing" - , ignoreTestBecause "Testcase is flaky for certain GHC versions (e.g. 9.2.5). See #3333 for details." $ do - runCabalTestCaseSession "Diagnostics in .hs files from invalid .cabal file" "simple-cabal" $ do - hsDoc <- openDoc "A.hs" "haskell" - expectNoMoreDiagnostics 1 hsDoc "typechecking" - cabalDoc <- openDoc "simple-cabal.cabal" "cabal" - expectNoMoreDiagnostics 1 cabalDoc "parsing" - let theRange = Range (Position 3 20) (Position 3 23) - -- Invalid license - changeDoc - cabalDoc - [ TextDocumentContentChangeEvent $ - InL $ - #range - .== theRange - .+ #rangeLength - .== Nothing - .+ #text - .== "MIT3" - ] - cabalDiags <- waitForDiagnosticsFrom cabalDoc - unknownLicenseDiag <- liftIO $ inspectDiagnostic cabalDiags ["Unknown SPDX license identifier: 'MIT3'"] - expectNoMoreDiagnostics 1 hsDoc "typechecking" - liftIO $ do - length cabalDiags @?= 1 - unknownLicenseDiag ^. L.range @?= Range (Position 3 24) (Position 4 0) - unknownLicenseDiag ^. L.severity @?= Just DiagnosticSeverity_Error - ] - , testGroup - "Code Actions" - [ runCabalTestCaseSession "BSD-3" "" $ do - doc <- openDoc "licenseCodeAction.cabal" "cabal" - diags <- waitForDiagnosticsFromSource doc "cabal" - reduceDiag <- liftIO $ inspectDiagnostic diags ["Unknown SPDX license identifier: 'BSD3'"] - liftIO $ do - length diags @?= 1 - reduceDiag ^. L.range @?= Range (Position 3 24) (Position 4 0) - reduceDiag ^. L.severity @?= Just DiagnosticSeverity_Error - [codeAction] <- getLicenseAction "BSD-3-Clause" <$> getCodeActions doc (Range (Position 3 24) (Position 4 0)) - executeCodeAction codeAction - contents <- documentContents doc - liftIO $ - contents - @?= Text.unlines - [ "cabal-version: 3.0" - , "name: licenseCodeAction" - , "version: 0.1.0.0" - , "license: BSD-3-Clause" - , "" - , "library" - , " build-depends: base" - , " default-language: Haskell2010" - ] - , runCabalTestCaseSession "Apache-2.0" "" $ do - doc <- openDoc "licenseCodeAction2.cabal" "cabal" - diags <- waitForDiagnosticsFromSource doc "cabal" - -- test if it supports typos in license name, here 'apahe' - reduceDiag <- liftIO $ inspectDiagnostic diags ["Unknown SPDX license identifier: 'APAHE'"] - liftIO $ do - length diags @?= 1 - reduceDiag ^. L.range @?= Range (Position 3 25) (Position 4 0) - reduceDiag ^. L.severity @?= Just DiagnosticSeverity_Error - [codeAction] <- getLicenseAction "Apache-2.0" <$> getCodeActions doc (Range (Position 3 24) (Position 4 0)) - executeCodeAction codeAction - contents <- documentContents doc - liftIO $ - contents - @?= Text.unlines - [ "cabal-version: 3.0" - , "name: licenseCodeAction2" - , "version: 0.1.0.0" - , "license: Apache-2.0" - , "" - , "library" - , " build-depends: base" - , " default-language: Haskell2010" - ] ] ] +-- ---------------------------------------------------------------------------- +-- Code Action Tests +-- ---------------------------------------------------------------------------- + +codeActionTests :: TestTree +codeActionTests = testGroup "Code Actions" + [ runCabalTestCaseSession "BSD-3" "" $ do + doc <- openDoc "licenseCodeAction.cabal" "cabal" + diags <- waitForDiagnosticsFromSource doc "cabal" + reduceDiag <- liftIO $ inspectDiagnostic diags ["Unknown SPDX license identifier: 'BSD3'"] + liftIO $ do + length diags @?= 1 + reduceDiag ^. L.range @?= Range (Position 3 24) (Position 4 0) + reduceDiag ^. L.severity @?= Just DiagnosticSeverity_Error + [codeAction] <- getLicenseAction "BSD-3-Clause" <$> getCodeActions doc (Range (Position 3 24) (Position 4 0)) + executeCodeAction codeAction + contents <- documentContents doc + liftIO $ + contents + @?= T.unlines + [ "cabal-version: 3.0" + , "name: licenseCodeAction" + , "version: 0.1.0.0" + , "license: BSD-3-Clause" + , "" + , "library" + , " build-depends: base" + , " default-language: Haskell2010" + ] + , runCabalTestCaseSession "Apache-2.0" "" $ do + doc <- openDoc "licenseCodeAction2.cabal" "cabal" + diags <- waitForDiagnosticsFromSource doc "cabal" + -- test if it supports typos in license name, here 'apahe' + reduceDiag <- liftIO $ inspectDiagnostic diags ["Unknown SPDX license identifier: 'APAHE'"] + liftIO $ do + length diags @?= 1 + reduceDiag ^. L.range @?= Range (Position 3 25) (Position 4 0) + reduceDiag ^. L.severity @?= Just DiagnosticSeverity_Error + [codeAction] <- getLicenseAction "Apache-2.0" <$> getCodeActions doc (Range (Position 3 24) (Position 4 0)) + executeCodeAction codeAction + contents <- documentContents doc + liftIO $ + contents + @?= T.unlines + [ "cabal-version: 3.0" + , "name: licenseCodeAction2" + , "version: 0.1.0.0" + , "license: Apache-2.0" + , "" + , "library" + , " build-depends: base" + , " default-language: Haskell2010" + ] + , runCabalGoldenSession "Code Actions - Can fix field names" "code-actions" "FieldSuggestions" $ \doc -> do + _ <- waitForDiagnosticsFrom doc + cas <- Maybe.mapMaybe (^? _R) <$> getAllCodeActions doc + -- Filter out the code actions we want to invoke. + -- We only want to invoke Code Actions with certain titles, and + -- we want to invoke them only once, not once for each cursor request. + -- 'getAllCodeActions' iterates over each cursor position and requests code actions. + let selectedCas = nubOrdOn (^. L.title) $ filter + (\ca -> (ca ^. L.title) `elem` + [ "Replace with license" + , "Replace with build-type" + , "Replace with extra-doc-files" + , "Replace with ghc-options" + , "Replace with location" + , "Replace with default-language" + , "Replace with import" + , "Replace with build-depends" + , "Replace with main-is" + , "Replace with hs-source-dirs" + ]) cas + mapM_ executeCodeAction selectedCas + pure () + , cabalAddTests + ] where getLicenseAction :: T.Text -> [Command |? CodeAction] -> [CodeAction] getLicenseAction license codeActions = do InR action@CodeAction{_title} <- codeActions guard (_title == "Replace with " <> license) pure action + +-- ---------------------------------------------------------------------------- +-- Hover Tests +-- ---------------------------------------------------------------------------- + +hoverTests :: TestTree +hoverTests = testGroup "Hover" + [ hoverOnDependencyTests + ] + +hoverOnDependencyTests :: TestTree +hoverOnDependencyTests = testGroup "Hover Dependency" + [ hoverContainsTest "base with separated version" "hover-deps.cabal" (Position 6 25) "[Documentation](https://hackage.haskell.org/package/base)" + , hoverContainsTest "aeson with not separated version " "hover-deps.cabal" (Position 7 25) "[Documentation](https://hackage.haskell.org/package/aeson)" + , hoverContainsTest "lens no version" "hover-deps.cabal" (Position 7 42) "[Documentation](https://hackage.haskell.org/package/lens)" + + , hoverIsNullTest "name has no documentation" "hover-deps.cabal" (Position 1 25) + , hoverIsNullTest "exposed-modules has no documentation" "hover-deps.cabal" (Position 5 25) + , hoverIsNullTest "hs-source-dirs has no documentation" "hover-deps.cabal" (Position 8 25) + ] + where + hoverContainsTest :: TestName -> FilePath -> Position -> T.Text -> TestTree + hoverContainsTest testName cabalFile pos containedText = + runCabalTestCaseSession testName "hover" $ do + doc <- openDoc cabalFile "cabal" + h <- getHover doc pos + case h of + Nothing -> liftIO $ assertFailure "No hover" + Just (Hover contents _) -> case contents of + InL (MarkupContent _ txt) -> do + liftIO + $ assertBool ("Failed to find `" <> T.unpack containedText <> "` in hover message: " <> T.unpack txt) + $ containedText `T.isInfixOf` txt + _ -> liftIO $ assertFailure "Unexpected content type" + closeDoc doc + + hoverIsNullTest :: TestName -> FilePath -> Position -> TestTree + hoverIsNullTest testName cabalFile pos = + runCabalTestCaseSession testName "hover" $ do + doc <- openDoc cabalFile "cabal" + h <- getHover doc pos + liftIO $ assertBool ("Found hover `" <> show h <> "`") $ Maybe.isNothing h + closeDoc doc diff --git a/plugins/hls-cabal-plugin/test/Outline.hs b/plugins/hls-cabal-plugin/test/Outline.hs new file mode 100644 index 0000000000..cb7279e387 --- /dev/null +++ b/plugins/hls-cabal-plugin/test/Outline.hs @@ -0,0 +1,103 @@ +{-# LANGUAGE OverloadedStrings #-} + +module Outline ( + outlineTests, +) where + +import Language.LSP.Protocol.Types (DocumentSymbol (..), + Position (..), Range (..)) +import qualified Test.Hls as T +import Utils + +testSymbols :: (T.HasCallStack) => T.TestName -> FilePath -> [DocumentSymbol] -> T.TestTree +testSymbols testName path expectedSymbols = + runCabalTestCaseSession testName "outline-cabal" $ do + docId <- T.openDoc path "cabal" + symbols <- T.getDocumentSymbols docId + T.liftIO $ symbols T.@?= Right expectedSymbols + +outlineTests :: T.TestTree +outlineTests = + T.testGroup + "Cabal Outline Tests" + [ testSymbols + "cabal Field outline test" + "field.cabal" + [fieldDocumentSymbol] + , testSymbols + "cabal FieldLine outline test" + "fieldline.cabal" + [fieldLineDocumentSymbol] + , testSymbols + "cabal Section outline test" + "section.cabal" + [sectionDocumentSymbol] + , testSymbols + "cabal SectionArg outline test" + "sectionarg.cabal" + [sectionArgDocumentSymbol] + ] + where + fieldDocumentSymbol :: DocumentSymbol + fieldDocumentSymbol = + ( defDocumentSymbol + ( Range { _start = Position{_line = 0, _character = 0} + , _end = Position{_line = 0, _character = 8} }) + ) + { _name = "homepage" + , _kind = T.SymbolKind_Field + , _children = Nothing + } + fieldLineDocumentSymbol :: DocumentSymbol + fieldLineDocumentSymbol = + ( defDocumentSymbol + ( Range { _start = Position{_line = 0, _character = 0} + , _end = Position{_line = 0, _character = 13} }) + ) + { _name = "cabal-version" + , _kind = T.SymbolKind_Field + , _children = Nothing -- the values of fieldLine are removed from the outline + } + sectionDocumentSymbol :: DocumentSymbol + sectionDocumentSymbol = + ( defDocumentSymbol + ( Range { _start = Position{_line = 0, _character = 2} + , _end = Position{_line = 0, _character = 15} }) + ) + { _name = "build-depends" + , _kind = T.SymbolKind_Field + , _children = Nothing -- the values of fieldLine are removed from the outline + } + sectionArgDocumentSymbol :: DocumentSymbol + sectionArgDocumentSymbol = + ( defDocumentSymbol + ( Range { _start = Position{_line = 0, _character = 2} + , _end = Position{_line = 0, _character = 19} }) + ) + { _name = "if os ( windows )" + , _kind = T.SymbolKind_Object + , _children = Just $ [sectionArgChildrenDocumentSymbol] + } + sectionArgChildrenDocumentSymbol :: DocumentSymbol + sectionArgChildrenDocumentSymbol = + ( defDocumentSymbol + ( Range { _start = Position{_line = 1, _character = 4} + , _end = Position{_line = 1, _character = 17} }) + ) + { _name = "build-depends" + , _kind = T.SymbolKind_Field + , _children = Nothing + } + +defDocumentSymbol :: Range -> DocumentSymbol +defDocumentSymbol range = + DocumentSymbol + { _detail = Nothing + , _deprecated = Nothing + , _name = "" + , _kind = T.SymbolKind_File + , _range = range + , _selectionRange = range + , _children = Nothing + , _tags = Nothing + } diff --git a/plugins/hls-cabal-plugin/test/Utils.hs b/plugins/hls-cabal-plugin/test/Utils.hs index f6df79cc8b..2733f94fd0 100644 --- a/plugins/hls-cabal-plugin/test/Utils.hs +++ b/plugins/hls-cabal-plugin/test/Utils.hs @@ -1,19 +1,27 @@ +{-# LANGUAGE DataKinds #-} {-# LANGUAGE DisambiguateRecordFields #-} {-# LANGUAGE OverloadedStrings #-} module Utils where +import Control.Monad (guard) import Data.List (sort) +import Data.Proxy (Proxy (Proxy)) import qualified Data.Text as T -import Ide.Plugin.Cabal (descriptor) +import Ide.Plugin.Cabal (descriptor, + haskellInteractionDescriptor) import qualified Ide.Plugin.Cabal import Ide.Plugin.Cabal.Completion.Types import System.FilePath import Test.Hls + cabalPlugin :: PluginTestDescriptor Ide.Plugin.Cabal.Log cabalPlugin = mkPluginTestDescriptor descriptor "cabal" +cabalHaskellPlugin :: PluginTestDescriptor Ide.Plugin.Cabal.Log +cabalHaskellPlugin = mkPluginTestDescriptor haskellInteractionDescriptor "cabal-haskell" + simpleCabalPrefixInfoFromPos :: Position -> T.Text -> CabalPrefixInfo simpleCabalPrefixInfoFromPos pos prefix = CabalPrefixInfo @@ -42,12 +50,34 @@ filePathComplTestDir = addTrailingPathSeparator $ testDataDir "filepath-comp runCabalTestCaseSession :: TestName -> FilePath -> Session () -> TestTree runCabalTestCaseSession title subdir = testCase title . runCabalSession subdir +runHaskellTestCaseSession :: TestName -> FilePath -> Session () -> TestTree +runHaskellTestCaseSession title subdir = testCase title . runHaskellAndCabalSession subdir + runCabalSession :: FilePath -> Session a -> IO a runCabalSession subdir = failIfSessionTimeout . runSessionWithServer def cabalPlugin (testDataDir subdir) +runHaskellAndCabalSession :: FilePath -> Session a -> IO a +runHaskellAndCabalSession subdir = + failIfSessionTimeout . runSessionWithServer def (cabalPlugin <> cabalHaskellPlugin) (testDataDir subdir) + +runCabalGoldenSession :: TestName -> FilePath -> FilePath -> (TextDocumentIdentifier -> Session ()) -> TestTree +runCabalGoldenSession title subdir fp act = goldenWithCabalDoc def cabalPlugin title testDataDir (subdir fp) "golden" "cabal" act + testDataDir :: FilePath -testDataDir = "test" "testdata" +testDataDir = "plugins" "hls-cabal-plugin" "test" "testdata" + +-- | these functions are used to detect cabal kicks +-- and look at diagnostics for cabal files +-- kicks are run everytime there is a shake session run/restart +cabalKickDone :: Session () +cabalKickDone = kick (Proxy @"kick/done/cabal") >>= guard . not . null + +cabalKickStart :: Session () +cabalKickStart = kick (Proxy @"kick/start/cabal") >>= guard . not . null + +cabalCaptureKick :: Session [Diagnostic] +cabalCaptureKick = captureKickDiagnostics cabalKickStart cabalKickDone -- | list comparison where the order in the list is irrelevant (@?==) :: (HasCallStack, Ord a, Show a) => [a] -> [a] -> Assertion diff --git a/plugins/hls-cabal-plugin/test/testdata/cabal-add-testdata/cabal-add-bench/bench/Main.hs b/plugins/hls-cabal-plugin/test/testdata/cabal-add-testdata/cabal-add-bench/bench/Main.hs new file mode 100644 index 0000000000..e5c42398f2 --- /dev/null +++ b/plugins/hls-cabal-plugin/test/testdata/cabal-add-testdata/cabal-add-bench/bench/Main.hs @@ -0,0 +1,6 @@ +module Main (main) where + +import Data.List.Split + +main :: IO () +main = putStrLn "Test suite not yet implemented." diff --git a/plugins/hls-cabal-plugin/test/testdata/cabal-add-testdata/cabal-add-bench/cabal-add-bench.cabal b/plugins/hls-cabal-plugin/test/testdata/cabal-add-testdata/cabal-add-bench/cabal-add-bench.cabal new file mode 100644 index 0000000000..b58a6d3302 --- /dev/null +++ b/plugins/hls-cabal-plugin/test/testdata/cabal-add-testdata/cabal-add-bench/cabal-add-bench.cabal @@ -0,0 +1,17 @@ +cabal-version: 2.4 +name: cabal-add-bench +version: 0.1.0.0 +license: NONE +author: George Gerasev +maintainer: george30032002@gmail.com +build-type: Simple + +common warnings + ghc-options: -Wall + +benchmark benchmark + type: exitcode-stdio-1.0 + ghc-options: -threaded + main-is: Main.hs + hs-source-dirs: bench + build-depends: base diff --git a/plugins/hls-cabal-plugin/test/testdata/cabal-add-testdata/cabal-add-exe/cabal-add-exe.cabal b/plugins/hls-cabal-plugin/test/testdata/cabal-add-testdata/cabal-add-exe/cabal-add-exe.cabal new file mode 100644 index 0000000000..a3499bbf97 --- /dev/null +++ b/plugins/hls-cabal-plugin/test/testdata/cabal-add-testdata/cabal-add-exe/cabal-add-exe.cabal @@ -0,0 +1,15 @@ +cabal-version: 2.4 +name: cabal-add-exe +version: 0.1.0.0 +build-type: Simple + +executable cabal-add-exe + main-is: Main.hs + hs-source-dirs: src + ghc-options: -Wall + build-depends: base + default-language: Haskell2010 + +library + build-depends: base >= 4 && < 5 + ghc-options: -Wall diff --git a/plugins/hls-cabal-plugin/test/testdata/cabal-add-testdata/cabal-add-exe/src/Main.hs b/plugins/hls-cabal-plugin/test/testdata/cabal-add-testdata/cabal-add-exe/src/Main.hs new file mode 100644 index 0000000000..0bf3e99dae --- /dev/null +++ b/plugins/hls-cabal-plugin/test/testdata/cabal-add-testdata/cabal-add-exe/src/Main.hs @@ -0,0 +1,5 @@ +module Main where + +import Data.List.Split + +main = putStrLn "Hello, Haskell!" \ No newline at end of file diff --git a/plugins/hls-cabal-plugin/test/testdata/cabal-add-testdata/cabal-add-lib/cabal-add-lib.cabal b/plugins/hls-cabal-plugin/test/testdata/cabal-add-testdata/cabal-add-lib/cabal-add-lib.cabal new file mode 100644 index 0000000000..b00b45bb6b --- /dev/null +++ b/plugins/hls-cabal-plugin/test/testdata/cabal-add-testdata/cabal-add-lib/cabal-add-lib.cabal @@ -0,0 +1,17 @@ +cabal-version: 2.4 +name: cabal-add-lib +version: 0.1.0.0 +license: NONE +author: George Gerasev +maintainer: george30032002@gmail.com +build-type: Simple + +common warnings + ghc-options: -Wall + +library + import: warnings + exposed-modules: MyLib + build-depends: base + hs-source-dirs: src + default-language: Haskell2010 diff --git a/plugins/hls-cabal-plugin/test/testdata/cabal-add-testdata/cabal-add-lib/src/MyLib.hs b/plugins/hls-cabal-plugin/test/testdata/cabal-add-testdata/cabal-add-lib/src/MyLib.hs new file mode 100644 index 0000000000..bbb506d001 --- /dev/null +++ b/plugins/hls-cabal-plugin/test/testdata/cabal-add-testdata/cabal-add-lib/src/MyLib.hs @@ -0,0 +1,6 @@ +module MyLib (someFunc) where + +import Data.List.Split + +someFunc :: IO () +someFunc = putStrLn "someFunc" diff --git a/plugins/hls-cabal-plugin/test/testdata/cabal-add-testdata/cabal-add-multitarget/bench/Main.hs b/plugins/hls-cabal-plugin/test/testdata/cabal-add-testdata/cabal-add-multitarget/bench/Main.hs new file mode 100644 index 0000000000..e5c42398f2 --- /dev/null +++ b/plugins/hls-cabal-plugin/test/testdata/cabal-add-testdata/cabal-add-multitarget/bench/Main.hs @@ -0,0 +1,6 @@ +module Main (main) where + +import Data.List.Split + +main :: IO () +main = putStrLn "Test suite not yet implemented." diff --git a/plugins/hls-cabal-plugin/test/testdata/cabal-add-testdata/cabal-add-multitarget/cabal-add-multitarget.cabal b/plugins/hls-cabal-plugin/test/testdata/cabal-add-testdata/cabal-add-multitarget/cabal-add-multitarget.cabal new file mode 100644 index 0000000000..677986768e --- /dev/null +++ b/plugins/hls-cabal-plugin/test/testdata/cabal-add-testdata/cabal-add-multitarget/cabal-add-multitarget.cabal @@ -0,0 +1,33 @@ +cabal-version: 2.4 +name: cabal-add-multitarget +version: 0.1.0.0 +build-type: Simple + +executable cabal-add-exe + main-is: Main.hs + hs-source-dirs: src + ghc-options: -Wall + build-depends: base + default-language: Haskell2010 + +library + exposed-modules: MyLib + other-modules: InternalLib + build-depends: base >= 4 && < 5 + hs-source-dirs: lib + ghc-options: -Wall + +test-suite cabal-add-tests-test + main-is: Main.hs + hs-source-dirs: test + type: exitcode-stdio-1.0 + build-depends: base + default-language: Haskell2010 + +benchmark benchmark + main-is: Main.hs + build-depends: base + hs-source-dirs: bench + type: exitcode-stdio-1.0 + ghc-options: -threaded + diff --git a/plugins/hls-cabal-plugin/test/testdata/cabal-add-testdata/cabal-add-multitarget/lib/InternalLib.hs b/plugins/hls-cabal-plugin/test/testdata/cabal-add-testdata/cabal-add-multitarget/lib/InternalLib.hs new file mode 100644 index 0000000000..5a3dd79258 --- /dev/null +++ b/plugins/hls-cabal-plugin/test/testdata/cabal-add-testdata/cabal-add-multitarget/lib/InternalLib.hs @@ -0,0 +1,6 @@ +module InternalLib (internalFunc) where + +import Data.List.Split + +internalFunc :: IO () +internalFunc = putStrLn "internalFunc" diff --git a/plugins/hls-cabal-plugin/test/testdata/cabal-add-testdata/cabal-add-multitarget/lib/MyLib.hs b/plugins/hls-cabal-plugin/test/testdata/cabal-add-testdata/cabal-add-multitarget/lib/MyLib.hs new file mode 100644 index 0000000000..bbb506d001 --- /dev/null +++ b/plugins/hls-cabal-plugin/test/testdata/cabal-add-testdata/cabal-add-multitarget/lib/MyLib.hs @@ -0,0 +1,6 @@ +module MyLib (someFunc) where + +import Data.List.Split + +someFunc :: IO () +someFunc = putStrLn "someFunc" diff --git a/plugins/hls-cabal-plugin/test/testdata/cabal-add-testdata/cabal-add-multitarget/src/Main.hs b/plugins/hls-cabal-plugin/test/testdata/cabal-add-testdata/cabal-add-multitarget/src/Main.hs new file mode 100644 index 0000000000..0bf3e99dae --- /dev/null +++ b/plugins/hls-cabal-plugin/test/testdata/cabal-add-testdata/cabal-add-multitarget/src/Main.hs @@ -0,0 +1,5 @@ +module Main where + +import Data.List.Split + +main = putStrLn "Hello, Haskell!" \ No newline at end of file diff --git a/plugins/hls-cabal-plugin/test/testdata/cabal-add-testdata/cabal-add-multitarget/test/Main.hs b/plugins/hls-cabal-plugin/test/testdata/cabal-add-testdata/cabal-add-multitarget/test/Main.hs new file mode 100644 index 0000000000..e5c42398f2 --- /dev/null +++ b/plugins/hls-cabal-plugin/test/testdata/cabal-add-testdata/cabal-add-multitarget/test/Main.hs @@ -0,0 +1,6 @@ +module Main (main) where + +import Data.List.Split + +main :: IO () +main = putStrLn "Test suite not yet implemented." diff --git a/plugins/hls-cabal-plugin/test/testdata/cabal-add-testdata/cabal-add-packageYaml/cabal-add-packageYaml.cabal b/plugins/hls-cabal-plugin/test/testdata/cabal-add-testdata/cabal-add-packageYaml/cabal-add-packageYaml.cabal new file mode 100644 index 0000000000..3ac549aa60 --- /dev/null +++ b/plugins/hls-cabal-plugin/test/testdata/cabal-add-testdata/cabal-add-packageYaml/cabal-add-packageYaml.cabal @@ -0,0 +1,17 @@ +cabal-version: 2.4 +name: cabal-add-packageYaml +version: 0.1.0.0 +license: NONE +author: George Gerasev +maintainer: george30032002@gmail.com +build-type: Simple + +common warnings + ghc-options: -Wall + +benchmark benchmark-packageYaml + type: exitcode-stdio-1.0 + ghc-options: -threaded + main-is: Main.hs + hs-source-dirs: bench + build-depends: base diff --git a/plugins/hls-cabal-plugin/test/testdata/cabal-add-testdata/cabal-add-packageYaml/package.yaml b/plugins/hls-cabal-plugin/test/testdata/cabal-add-testdata/cabal-add-packageYaml/package.yaml new file mode 100644 index 0000000000..e69de29bb2 diff --git a/plugins/hls-cabal-plugin/test/testdata/cabal-add-testdata/cabal-add-packageYaml/src/Main.hs b/plugins/hls-cabal-plugin/test/testdata/cabal-add-testdata/cabal-add-packageYaml/src/Main.hs new file mode 100644 index 0000000000..e5c42398f2 --- /dev/null +++ b/plugins/hls-cabal-plugin/test/testdata/cabal-add-testdata/cabal-add-packageYaml/src/Main.hs @@ -0,0 +1,6 @@ +module Main (main) where + +import Data.List.Split + +main :: IO () +main = putStrLn "Test suite not yet implemented." diff --git a/plugins/hls-cabal-plugin/test/testdata/cabal-add-testdata/cabal-add-tests/cabal-add-tests.cabal b/plugins/hls-cabal-plugin/test/testdata/cabal-add-testdata/cabal-add-tests/cabal-add-tests.cabal new file mode 100644 index 0000000000..9adc498231 --- /dev/null +++ b/plugins/hls-cabal-plugin/test/testdata/cabal-add-testdata/cabal-add-tests/cabal-add-tests.cabal @@ -0,0 +1,26 @@ +cabal-version: 2.4 +name: cabal-add-tests +version: 0.1.0.0 +license: NONE +author: George Gerasev +maintainer: george30032002@gmail.com +build-type: Simple + +common warnings + ghc-options: -Wall + +test-suite cabal-add-tests-test + import: warnings + default-language: Haskell2010 + type: exitcode-stdio-1.0 + hs-source-dirs: test + main-is: Main.hs + build-depends: base + +test-suite cabal-add-tests-test-package-imports + import: warnings + default-language: Haskell2010 + type: exitcode-stdio-1.0 + hs-source-dirs: test + main-is: MainPackageImports.hs + build-depends: base diff --git a/plugins/hls-cabal-plugin/test/testdata/cabal-add-testdata/cabal-add-tests/test/Main.hs b/plugins/hls-cabal-plugin/test/testdata/cabal-add-testdata/cabal-add-tests/test/Main.hs new file mode 100644 index 0000000000..e5c42398f2 --- /dev/null +++ b/plugins/hls-cabal-plugin/test/testdata/cabal-add-testdata/cabal-add-tests/test/Main.hs @@ -0,0 +1,6 @@ +module Main (main) where + +import Data.List.Split + +main :: IO () +main = putStrLn "Test suite not yet implemented." diff --git a/plugins/hls-cabal-plugin/test/testdata/cabal-add-testdata/cabal-add-tests/test/MainPackageImports.hs b/plugins/hls-cabal-plugin/test/testdata/cabal-add-testdata/cabal-add-tests/test/MainPackageImports.hs new file mode 100644 index 0000000000..753dd165dd --- /dev/null +++ b/plugins/hls-cabal-plugin/test/testdata/cabal-add-testdata/cabal-add-tests/test/MainPackageImports.hs @@ -0,0 +1,8 @@ +{-# LANGUAGE PackageImports #-} + +module Main (main) where + +import "split" Data.List.Split + +main :: IO () +main = putStrLn "Test suite not yet implemented." diff --git a/plugins/hls-cabal-plugin/test/testdata/cabal-add-testdata/cabal.project b/plugins/hls-cabal-plugin/test/testdata/cabal-add-testdata/cabal.project new file mode 100644 index 0000000000..21eb1f63eb --- /dev/null +++ b/plugins/hls-cabal-plugin/test/testdata/cabal-add-testdata/cabal.project @@ -0,0 +1,6 @@ +packages: cabal-add-exe + cabal-add-lib + cabal-add-tests + cabal-add-bench + cabal-add-multitarget + cabal-add-packageYaml diff --git a/plugins/hls-cabal-plugin/test/testdata/cabal-add-testdata/hie.yaml b/plugins/hls-cabal-plugin/test/testdata/cabal-add-testdata/hie.yaml new file mode 100644 index 0000000000..f0c7014d7f --- /dev/null +++ b/plugins/hls-cabal-plugin/test/testdata/cabal-add-testdata/hie.yaml @@ -0,0 +1,2 @@ +cradle: + cabal: \ No newline at end of file diff --git a/plugins/hls-cabal-plugin/test/testdata/code-actions/FieldSuggestions.cabal b/plugins/hls-cabal-plugin/test/testdata/code-actions/FieldSuggestions.cabal new file mode 100644 index 0000000000..e32f77b614 --- /dev/null +++ b/plugins/hls-cabal-plugin/test/testdata/code-actions/FieldSuggestions.cabal @@ -0,0 +1,36 @@ +cabal-version: 3.0 +name: FieldSuggestions +version: 0.1.0 +licens: BSD-3-Clause + +buil-type: Simple + +extra-doc-fils: + ChangeLog + +-- Default warnings in HLS +common warnings + ghc-option: -Wall + -Wredundant-constraints + -Wunused-packages + -Wno-name-shadowing + -Wno-unticked-promoted-constructors + +source-repository head + type: git + loc: fake + +library + default-lang: Haskell2010 + -- Import isn't supported right now. + impor: warnings + build-dep: base + +executable my-exe + mains: Main.hs + +test-suite Test + type: exitcode-stdio-1.0 + main-is: Test.hs + hs-source-drs: + diff --git a/plugins/hls-cabal-plugin/test/testdata/code-actions/FieldSuggestions.golden.cabal b/plugins/hls-cabal-plugin/test/testdata/code-actions/FieldSuggestions.golden.cabal new file mode 100644 index 0000000000..99bf84dfd7 --- /dev/null +++ b/plugins/hls-cabal-plugin/test/testdata/code-actions/FieldSuggestions.golden.cabal @@ -0,0 +1,36 @@ +cabal-version: 3.0 +name: FieldSuggestions +version: 0.1.0 +license: BSD-3-Clause + +build-type: Simple + +extra-doc-files: + ChangeLog + +-- Default warnings in HLS +common warnings + ghc-options: -Wall + -Wredundant-constraints + -Wunused-packages + -Wno-name-shadowing + -Wno-unticked-promoted-constructors + +source-repository head + type: git + location: fake + +library + default-language: Haskell2010 + -- Import isn't supported right now. + import: warnings + build-depends: base + +executable my-exe + main-is: Main.hs + +test-suite Test + type: exitcode-stdio-1.0 + main-is: Test.hs + hs-source-dirs: + diff --git a/plugins/hls-cabal-plugin/test/testdata/completer.cabal b/plugins/hls-cabal-plugin/test/testdata/completer.cabal index cd7c697026..141bdd7d2d 100644 --- a/plugins/hls-cabal-plugin/test/testdata/completer.cabal +++ b/plugins/hls-cabal-plugin/test/testdata/completer.cabal @@ -10,3 +10,5 @@ be library lib + +co \ No newline at end of file diff --git a/plugins/hls-cabal-plugin/test/testdata/completion/autogen-completion.cabal b/plugins/hls-cabal-plugin/test/testdata/completion/autogen-completion.cabal new file mode 100644 index 0000000000..dd5c86d339 --- /dev/null +++ b/plugins/hls-cabal-plugin/test/testdata/completion/autogen-completion.cabal @@ -0,0 +1,25 @@ +cabal-version: 3.0 +name: autogen-completion +version: 0.1.0.0 + +library + hs-source-dirs: src + autogen- + +executable autoexe + main-is: Main.hs + hs-source-dirs: src + autogen- + +test-suite autotest + type: exitcode-stdio-1.0 + hs-source-dirs: src + autogen- + +benchmark autobench + type: exitcode-stdio-1.0 + hs-source-dirs: src + autogen- + +common defaults + autogen- diff --git a/plugins/hls-cabal-plugin/test/testdata/goto-definition/common-section/simple-with-common.cabal b/plugins/hls-cabal-plugin/test/testdata/goto-definition/common-section/simple-with-common.cabal new file mode 100644 index 0000000000..c71e369b30 --- /dev/null +++ b/plugins/hls-cabal-plugin/test/testdata/goto-definition/common-section/simple-with-common.cabal @@ -0,0 +1,62 @@ +cabal-version: 3.0 +name: simple-cabal +version: 0.1.0.0 +license: MIT + +-- Range : (6, 0) - (7, 22) +common warnings-0 + ghc-options: -Wall + +-- Range : (10, 0) - (17, 40) +common warnings-1 + ghc-options: -Wall + -Wredundant-constraints + -Wunused-packages + + -Wno-name-shadowing + + -Wno-unticked-promoted-constructors + +-- Range : (20, 0) - (23, 34) +common warnings-2 + ghc-options: -Wall + -Wredundant-constraints + -Wunused-packages + +library + + import: warnings-0 +-- ^ Position: (27, 16), middle of identifier + + import: warnings-1 +-- ^ Position: (30, 12), left of identifier + + import: warnings-2 +-- ^ Position: (33, 22), right of identifier + + import: warnings-0 +-- ^ Position: (36, 20), left of '-' in identifier + + import: warnings-1 +-- ^ Position: (39, 19), right of "-" in identifier + + import: warnings-2,warnings-1,warnings-0 +-- ^ Position: (42, 16), identifier in identifier list + + import: warnings-2,warnings-1,warnings-0 +-- ^ Position: (45, 33), left of ',' right of identifier + + import: warnings-2,warnings-1,warnings-0 +-- ^ Position: (48, 34), right of ',' left of identifier + + import: warnings-2, warnings-1,warnings-0 +-- ^ Position: (51, 37), right of ',' left of space + + import: warnings-0 +-- ^ Position: (54, 11), right of ':' left of space + + import: warnings-0 +-- ^ Position: (57, 8), not a definition + + -- EOL +-- ^ Position: (59, 7), empty space \ No newline at end of file diff --git a/plugins/hls-cabal-plugin/test/testdata/goto-definition/modules/module-examples.cabal b/plugins/hls-cabal-plugin/test/testdata/goto-definition/modules/module-examples.cabal new file mode 100644 index 0000000000..24c2bb854e --- /dev/null +++ b/plugins/hls-cabal-plugin/test/testdata/goto-definition/modules/module-examples.cabal @@ -0,0 +1,51 @@ +cabal-version: 3.0 +name: module-examples +version: 0.1.0.0 + + +library + exposed-modules: Library.Lib +-- ^ Position: (6, 22) +-- ^ Position: (6, 33) + other-modules: Library.Other.OtherLib +-- ^ Position: (9, 22) +-- ^ Position: (9, 44) + + build-depends: base + hs-source-dirs: src + default-language: Haskell2010 + +executable exec + hs-source-dirs: src/exe + main-is: Main.hs + build-depends: base + other-modules: + Config +-- ^ Position: (22, 8) +-- ^ Position: (22, 14) + +test-suite module-examples-test + type: exitcode-stdio-1.0 + hs-source-dirs: src/test + main-is: Main.hs + other-modules: + Config +-- ^ Position: (31, 8) +-- ^ Position: (31, 14) + Library +-- ^ Position: (34, 8) +-- ^ Position: (34, 15) + build-depends: base + +benchmark benchmark + type: exitcode-stdio-1.0 + main-is: Main.hs + hs-source-dirs: src/bench + build-depends: base + other-modules: + Config +-- ^ Position: (45, 28) +-- ^ Position: (45, 34) + NotExistent +-- ^ Position: (48, 19) +-- ^ Position: (48, 30) diff --git a/plugins/hls-cabal-plugin/test/testdata/goto-definition/modules/src/Library/Lib.hs b/plugins/hls-cabal-plugin/test/testdata/goto-definition/modules/src/Library/Lib.hs new file mode 100644 index 0000000000..e2cde3780b --- /dev/null +++ b/plugins/hls-cabal-plugin/test/testdata/goto-definition/modules/src/Library/Lib.hs @@ -0,0 +1 @@ +module Library.Lib where diff --git a/plugins/hls-cabal-plugin/test/testdata/goto-definition/modules/src/Library/Other/OtherLib.hs b/plugins/hls-cabal-plugin/test/testdata/goto-definition/modules/src/Library/Other/OtherLib.hs new file mode 100644 index 0000000000..625be777dc --- /dev/null +++ b/plugins/hls-cabal-plugin/test/testdata/goto-definition/modules/src/Library/Other/OtherLib.hs @@ -0,0 +1 @@ +module Library.Other.OtherLib where diff --git a/plugins/hls-cabal-plugin/test/testdata/goto-definition/modules/src/bench/Config.hs b/plugins/hls-cabal-plugin/test/testdata/goto-definition/modules/src/bench/Config.hs new file mode 100644 index 0000000000..6ea268c214 --- /dev/null +++ b/plugins/hls-cabal-plugin/test/testdata/goto-definition/modules/src/bench/Config.hs @@ -0,0 +1 @@ +module Config where diff --git a/plugins/hls-cabal-plugin/test/testdata/goto-definition/modules/src/bench/Main.hs b/plugins/hls-cabal-plugin/test/testdata/goto-definition/modules/src/bench/Main.hs new file mode 100644 index 0000000000..6640b61707 --- /dev/null +++ b/plugins/hls-cabal-plugin/test/testdata/goto-definition/modules/src/bench/Main.hs @@ -0,0 +1,3 @@ +module Main where + +main = undefined \ No newline at end of file diff --git a/plugins/hls-cabal-plugin/test/testdata/goto-definition/modules/src/exe/Config.hs b/plugins/hls-cabal-plugin/test/testdata/goto-definition/modules/src/exe/Config.hs new file mode 100644 index 0000000000..3a2489708e --- /dev/null +++ b/plugins/hls-cabal-plugin/test/testdata/goto-definition/modules/src/exe/Config.hs @@ -0,0 +1 @@ +module Confing where \ No newline at end of file diff --git a/plugins/hls-cabal-plugin/test/testdata/goto-definition/modules/src/exe/Main.hs b/plugins/hls-cabal-plugin/test/testdata/goto-definition/modules/src/exe/Main.hs new file mode 100644 index 0000000000..6640b61707 --- /dev/null +++ b/plugins/hls-cabal-plugin/test/testdata/goto-definition/modules/src/exe/Main.hs @@ -0,0 +1,3 @@ +module Main where + +main = undefined \ No newline at end of file diff --git a/plugins/hls-cabal-plugin/test/testdata/goto-definition/modules/src/test/Config.hs b/plugins/hls-cabal-plugin/test/testdata/goto-definition/modules/src/test/Config.hs new file mode 100644 index 0000000000..39e39fc16a --- /dev/null +++ b/plugins/hls-cabal-plugin/test/testdata/goto-definition/modules/src/test/Config.hs @@ -0,0 +1 @@ +module Config where \ No newline at end of file diff --git a/plugins/hls-cabal-plugin/test/testdata/goto-definition/modules/src/test/Library.hs b/plugins/hls-cabal-plugin/test/testdata/goto-definition/modules/src/test/Library.hs new file mode 100644 index 0000000000..7899749de8 --- /dev/null +++ b/plugins/hls-cabal-plugin/test/testdata/goto-definition/modules/src/test/Library.hs @@ -0,0 +1 @@ +module Library where \ No newline at end of file diff --git a/plugins/hls-cabal-plugin/test/testdata/goto-definition/modules/src/test/Main.hs b/plugins/hls-cabal-plugin/test/testdata/goto-definition/modules/src/test/Main.hs new file mode 100644 index 0000000000..e69de29bb2 diff --git a/plugins/hls-cabal-plugin/test/testdata/goto-definition/simple-with-common.cabal b/plugins/hls-cabal-plugin/test/testdata/goto-definition/simple-with-common.cabal new file mode 100644 index 0000000000..c71e369b30 --- /dev/null +++ b/plugins/hls-cabal-plugin/test/testdata/goto-definition/simple-with-common.cabal @@ -0,0 +1,62 @@ +cabal-version: 3.0 +name: simple-cabal +version: 0.1.0.0 +license: MIT + +-- Range : (6, 0) - (7, 22) +common warnings-0 + ghc-options: -Wall + +-- Range : (10, 0) - (17, 40) +common warnings-1 + ghc-options: -Wall + -Wredundant-constraints + -Wunused-packages + + -Wno-name-shadowing + + -Wno-unticked-promoted-constructors + +-- Range : (20, 0) - (23, 34) +common warnings-2 + ghc-options: -Wall + -Wredundant-constraints + -Wunused-packages + +library + + import: warnings-0 +-- ^ Position: (27, 16), middle of identifier + + import: warnings-1 +-- ^ Position: (30, 12), left of identifier + + import: warnings-2 +-- ^ Position: (33, 22), right of identifier + + import: warnings-0 +-- ^ Position: (36, 20), left of '-' in identifier + + import: warnings-1 +-- ^ Position: (39, 19), right of "-" in identifier + + import: warnings-2,warnings-1,warnings-0 +-- ^ Position: (42, 16), identifier in identifier list + + import: warnings-2,warnings-1,warnings-0 +-- ^ Position: (45, 33), left of ',' right of identifier + + import: warnings-2,warnings-1,warnings-0 +-- ^ Position: (48, 34), right of ',' left of identifier + + import: warnings-2, warnings-1,warnings-0 +-- ^ Position: (51, 37), right of ',' left of space + + import: warnings-0 +-- ^ Position: (54, 11), right of ':' left of space + + import: warnings-0 +-- ^ Position: (57, 8), not a definition + + -- EOL +-- ^ Position: (59, 7), empty space \ No newline at end of file diff --git a/plugins/hls-cabal-plugin/test/testdata/hie.yaml b/plugins/hls-cabal-plugin/test/testdata/hie.yaml new file mode 100644 index 0000000000..95d800026a --- /dev/null +++ b/plugins/hls-cabal-plugin/test/testdata/hie.yaml @@ -0,0 +1,3 @@ +cradle: + direct: + arguments: [] \ No newline at end of file diff --git a/plugins/hls-cabal-plugin/test/testdata/hover/hover-deps.cabal b/plugins/hls-cabal-plugin/test/testdata/hover/hover-deps.cabal new file mode 100644 index 0000000000..ddc4a6107a --- /dev/null +++ b/plugins/hls-cabal-plugin/test/testdata/hover/hover-deps.cabal @@ -0,0 +1,10 @@ +cabal-version: 3.0 +name: hover-deps +version: 0.1.0.0 + +library + exposed-modules: Module + build-depends: base ^>=4.14.3.0 + , aeson==1.0.0.0 , lens + hs-source-dirs: src + default-language: Haskell2010 diff --git a/plugins/hls-cabal-plugin/test/testdata/outline-cabal/field.cabal b/plugins/hls-cabal-plugin/test/testdata/outline-cabal/field.cabal new file mode 100644 index 0000000000..c3e3d80df2 --- /dev/null +++ b/plugins/hls-cabal-plugin/test/testdata/outline-cabal/field.cabal @@ -0,0 +1 @@ +homepage: \ No newline at end of file diff --git a/plugins/hls-cabal-plugin/test/testdata/outline-cabal/fieldline.cabal b/plugins/hls-cabal-plugin/test/testdata/outline-cabal/fieldline.cabal new file mode 100644 index 0000000000..998369e5f1 --- /dev/null +++ b/plugins/hls-cabal-plugin/test/testdata/outline-cabal/fieldline.cabal @@ -0,0 +1 @@ +cabal-version: 3.0 diff --git a/plugins/hls-cabal-plugin/test/testdata/outline-cabal/section.cabal b/plugins/hls-cabal-plugin/test/testdata/outline-cabal/section.cabal new file mode 100644 index 0000000000..8a140c7517 --- /dev/null +++ b/plugins/hls-cabal-plugin/test/testdata/outline-cabal/section.cabal @@ -0,0 +1,2 @@ + build-depends: + base >=4.16 && <5 \ No newline at end of file diff --git a/plugins/hls-cabal-plugin/test/testdata/outline-cabal/sectionarg.cabal b/plugins/hls-cabal-plugin/test/testdata/outline-cabal/sectionarg.cabal new file mode 100644 index 0000000000..060d067377 --- /dev/null +++ b/plugins/hls-cabal-plugin/test/testdata/outline-cabal/sectionarg.cabal @@ -0,0 +1,2 @@ + if os(windows) + build-depends: Win32 \ No newline at end of file diff --git a/plugins/hls-cabal-plugin/test/testdata/unsupportedVersion.cabal b/plugins/hls-cabal-plugin/test/testdata/unsupportedVersion.cabal new file mode 100644 index 0000000000..328d373cd8 --- /dev/null +++ b/plugins/hls-cabal-plugin/test/testdata/unsupportedVersion.cabal @@ -0,0 +1,3 @@ +cabal-version: 99999.0 +name: invalid +version: 0.1.0.0 \ No newline at end of file diff --git a/plugins/hls-call-hierarchy-plugin/LICENSE b/plugins/hls-call-hierarchy-plugin/LICENSE deleted file mode 100644 index 261eeb9e9f..0000000000 --- a/plugins/hls-call-hierarchy-plugin/LICENSE +++ /dev/null @@ -1,201 +0,0 @@ - Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ - - TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - - 1. Definitions. - - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. - - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. - - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. - - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. - - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. - - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. - - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). - - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. - - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution." - - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. - - 2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. - - 3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. - - 4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: - - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and - - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and - - (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. - - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. - - 5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. - - 6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. - - 7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. - - 8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. - - 9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. - - END OF TERMS AND CONDITIONS - - APPENDIX: How to apply the Apache License to your work. - - To apply the Apache License to your work, attach the following - boilerplate notice, with the fields enclosed by brackets "[]" - replaced with your own identifying information. (Don't include - the brackets!) The text should be enclosed in the appropriate - comment syntax for the file format. We also recommend that a - file or class name and description of purpose be included on the - same "printed page" as the copyright notice for easier - identification within third-party archives. - - Copyright [yyyy] [name of copyright owner] - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. diff --git a/plugins/hls-call-hierarchy-plugin/hls-call-hierarchy-plugin.cabal b/plugins/hls-call-hierarchy-plugin/hls-call-hierarchy-plugin.cabal deleted file mode 100644 index 90990ca538..0000000000 --- a/plugins/hls-call-hierarchy-plugin/hls-call-hierarchy-plugin.cabal +++ /dev/null @@ -1,66 +0,0 @@ -cabal-version: 2.4 -name: hls-call-hierarchy-plugin -version: 2.4.0.0 -synopsis: Call hierarchy plugin for Haskell Language Server -description: - Please see the README on GitHub at - -license: Apache-2.0 -license-file: LICENSE -author: Lei Zhu -maintainer: julytreee@gmail.com -category: Development -build-type: Simple -extra-source-files: - LICENSE - test/testdata/*.hs - -source-repository head - type: git - location: https://github.com/haskell/haskell-language-server.git - -library - buildable: True - exposed-modules: Ide.Plugin.CallHierarchy - other-modules: - Ide.Plugin.CallHierarchy.Internal - Ide.Plugin.CallHierarchy.Query - Ide.Plugin.CallHierarchy.Types - - hs-source-dirs: src - build-depends: - , aeson - , base >=4.12 && <5 - , containers - , extra - , ghcide == 2.4.0.0 - , hiedb - , hls-plugin-api == 2.4.0.0 - , lens - , lsp >=2.3 - , sqlite-simple - , text - , unordered-containers - - default-language: Haskell2010 - default-extensions: DataKinds - -test-suite tests - type: exitcode-stdio-1.0 - default-language: Haskell2010 - hs-source-dirs: test - main-is: Main.hs - ghc-options: -threaded -rtsopts -with-rtsopts=-N - build-depends: - , aeson - , base - , containers - , extra - , filepath - , hls-call-hierarchy-plugin - , hls-test-utils == 2.4.0.0 - , ghcide-test-utils - , lens - , lsp - , lsp-test - , text diff --git a/plugins/hls-call-hierarchy-plugin/src/Ide/Plugin/CallHierarchy.hs b/plugins/hls-call-hierarchy-plugin/src/Ide/Plugin/CallHierarchy.hs index de5dac99d8..165a51013a 100644 --- a/plugins/hls-call-hierarchy-plugin/src/Ide/Plugin/CallHierarchy.hs +++ b/plugins/hls-call-hierarchy-plugin/src/Ide/Plugin/CallHierarchy.hs @@ -1,3 +1,4 @@ +{-# LANGUAGE OverloadedStrings #-} module Ide.Plugin.CallHierarchy (descriptor) where import Development.IDE @@ -6,7 +7,7 @@ import Ide.Types import Language.LSP.Protocol.Message descriptor :: PluginId -> PluginDescriptor IdeState -descriptor plId = (defaultPluginDescriptor plId) +descriptor plId = (defaultPluginDescriptor plId "Provides call-hierarchy support in Haskell") { Ide.Types.pluginHandlers = mkPluginHandler SMethod_TextDocumentPrepareCallHierarchy X.prepareCallHierarchy <> mkPluginHandler SMethod_CallHierarchyIncomingCalls X.incomingCalls diff --git a/plugins/hls-call-hierarchy-plugin/src/Ide/Plugin/CallHierarchy/Internal.hs b/plugins/hls-call-hierarchy-plugin/src/Ide/Plugin/CallHierarchy/Internal.hs index dcae70b249..06e9d99679 100644 --- a/plugins/hls-call-hierarchy-plugin/src/Ide/Plugin/CallHierarchy/Internal.hs +++ b/plugins/hls-call-hierarchy-plugin/src/Ide/Plugin/CallHierarchy/Internal.hs @@ -1,11 +1,7 @@ -{-# LANGUAGE CPP #-} -{-# LANGUAGE FlexibleContexts #-} -{-# LANGUAGE LambdaCase #-} -{-# LANGUAGE NamedFieldPuns #-} -{-# LANGUAGE OverloadedStrings #-} -{-# LANGUAGE RecordWildCards #-} -{-# LANGUAGE ScopedTypeVariables #-} -{-# LANGUAGE StandaloneDeriving #-} +{-# LANGUAGE CPP #-} +{-# LANGUAGE LambdaCase #-} +{-# LANGUAGE OverloadedStrings #-} +{-# LANGUAGE RecordWildCards #-} module Ide.Plugin.CallHierarchy.Internal ( prepareCallHierarchy @@ -13,43 +9,45 @@ module Ide.Plugin.CallHierarchy.Internal ( , outgoingCalls ) where -import Control.Lens ((^.)) +import Control.Lens (Lens', (^.)) import Control.Monad.IO.Class -import Data.Aeson as A -import Data.List (groupBy, sortBy) -import qualified Data.Map as M +import Data.Aeson as A +import Data.Functor ((<&>)) +import Data.List (groupBy, sortBy) +import qualified Data.Map as M import Data.Maybe -import qualified Data.Set as S -import qualified Data.Text as T +import Data.Ord (comparing) +import qualified Data.Set as S +import qualified Data.Text as T import Data.Tuple.Extra import Development.IDE -import qualified Development.IDE.Core.PluginUtils as PluginUtils import Development.IDE.Core.Shake -import Development.IDE.GHC.Compat as Compat +import Development.IDE.GHC.Compat as Compat import Development.IDE.Spans.AtPoint -import HieDb (Symbol (Symbol)) -import qualified Ide.Plugin.CallHierarchy.Query as Q +import HieDb (Symbol (Symbol)) +import qualified Ide.Plugin.CallHierarchy.Query as Q import Ide.Plugin.CallHierarchy.Types import Ide.Plugin.Error import Ide.Types -import qualified Language.LSP.Protocol.Lens as L +import qualified Language.LSP.Protocol.Lens as L import Language.LSP.Protocol.Message import Language.LSP.Protocol.Types -import Text.Read (readMaybe) +import Prelude hiding (mod, span) +import Text.Read (readMaybe) -- | Render prepare call hierarchy request. prepareCallHierarchy :: PluginMethodHandler IdeState Method_TextDocumentPrepareCallHierarchy prepareCallHierarchy state _ param = do - nfp <- getNormalizedFilePathE (param ^. L.textDocument ^. L.uri) + nfp <- getNormalizedFilePathE (param ^. (L.textDocument . L.uri)) items <- liftIO $ runAction "CallHierarchy.prepareHierarchy" state $ prepareCallHierarchyItem nfp (param ^. L.position) pure $ InL items prepareCallHierarchyItem :: NormalizedFilePath -> Position -> Action [CallHierarchyItem] -prepareCallHierarchyItem nfp pos = use GetHieAst nfp >>= \case - Nothing -> pure mempty - Just (HAR _ hf _ _ _) -> pure $ prepareByAst hf pos nfp +prepareCallHierarchyItem nfp pos = use GetHieAst nfp <&> \case + Nothing -> mempty + Just (HAR _ hf _ _ _) -> prepareByAst hf pos nfp prepareByAst :: HieASTs a -> Position -> NormalizedFilePath -> [CallHierarchyItem] prepareByAst hf pos nfp = @@ -167,13 +165,9 @@ mkSymbol = \case -------------- Incoming calls and outgoing calls --------------------- ---------------------------------------------------------------------- -#if !MIN_VERSION_aeson(1,5,2) -deriving instance Ord Value -#endif - -- | Render incoming calls request. incomingCalls :: PluginMethodHandler IdeState Method_CallHierarchyIncomingCalls -incomingCalls state pluginId param = do +incomingCalls state _pluginId param = do calls <- liftIO $ runAction "CallHierarchy.incomingCalls" state $ queryCalls @@ -181,14 +175,14 @@ incomingCalls state pluginId param = do Q.incomingCalls mkCallHierarchyIncomingCall (mergeCalls CallHierarchyIncomingCall L.from) - pure $ InL $ calls + pure $ InL calls where mkCallHierarchyIncomingCall :: Vertex -> Action (Maybe CallHierarchyIncomingCall) mkCallHierarchyIncomingCall = mkCallHierarchyCall CallHierarchyIncomingCall -- | Render outgoing calls request. outgoingCalls :: PluginMethodHandler IdeState Method_CallHierarchyOutgoingCalls -outgoingCalls state pluginId param = do +outgoingCalls state _pluginId param = do calls <- liftIO $ runAction "CallHierarchy.outgoingCalls" state $ queryCalls @@ -196,15 +190,22 @@ outgoingCalls state pluginId param = do Q.outgoingCalls mkCallHierarchyOutgoingCall (mergeCalls CallHierarchyOutgoingCall L.to) - pure $ InL $ calls + pure $ InL calls where mkCallHierarchyOutgoingCall :: Vertex -> Action (Maybe CallHierarchyOutgoingCall) mkCallHierarchyOutgoingCall = mkCallHierarchyCall CallHierarchyOutgoingCall + -- | Merge calls from the same place +mergeCalls :: + L.HasFromRanges s [Range] + => (CallHierarchyItem -> [Range] -> s) + -> Lens' s CallHierarchyItem + -> [s] + -> [s] mergeCalls constructor target = concatMap merge . groupBy (\a b -> a ^. target == b ^. target) - . sortBy (\a b -> (a ^. target) `compare` (b ^. target)) + . sortBy (comparing (^. target)) where merge [] = [] merge calls@(call:_) = @@ -235,11 +236,11 @@ mkCallHierarchyCall mk v@Vertex{..} = do case items of [item] -> pure $ Just $ mk item [range] _ -> pure Nothing - _ -> pure Nothing + [] -> pure Nothing -- | Unified queries include incoming calls and outgoing calls. -queryCalls :: (Show a) - => CallHierarchyItem +queryCalls :: + CallHierarchyItem -> (HieDb -> Symbol -> IO [Vertex]) -> (Vertex -> Action (Maybe a)) -> ([a] -> [a]) @@ -257,7 +258,6 @@ queryCalls item queryFunc makeFunc merge | otherwise = pure mempty where uri = item ^. L.uri - xdata = item ^. L.data_ pos = item ^. (L.selectionRange . L.start) getSymbol nfp = case item ^. L.data_ of @@ -267,9 +267,9 @@ queryCalls item queryFunc makeFunc merge Nothing -> getSymbolFromAst nfp pos -- Fallback if xdata lost, some editor(VSCode) will drop it getSymbolFromAst :: NormalizedFilePath -> Position -> Action (Maybe Symbol) - getSymbolFromAst nfp pos = use GetHieAst nfp >>= \case - Nothing -> pure Nothing + getSymbolFromAst nfp pos_ = use GetHieAst nfp <&> \case + Nothing -> Nothing Just (HAR _ hf _ _ _) -> do - case listToMaybe $ pointCommand hf pos extract of - Just infos -> maybe (pure Nothing) pure $ mkSymbol . fst3 <$> listToMaybe infos - Nothing -> pure Nothing + case listToMaybe $ pointCommand hf pos_ extract of + Just infos -> mkSymbol . fst3 =<< listToMaybe infos + Nothing -> Nothing diff --git a/plugins/hls-call-hierarchy-plugin/src/Ide/Plugin/CallHierarchy/Query.hs b/plugins/hls-call-hierarchy-plugin/src/Ide/Plugin/CallHierarchy/Query.hs index 9a855958c1..2303aa94b9 100644 --- a/plugins/hls-call-hierarchy-plugin/src/Ide/Plugin/CallHierarchy/Query.hs +++ b/plugins/hls-call-hierarchy-plugin/src/Ide/Plugin/CallHierarchy/Query.hs @@ -1,6 +1,5 @@ -{-# LANGUAGE FlexibleContexts #-} -{-# LANGUAGE RecordWildCards #-} -{-# LANGUAGE ViewPatterns #-} +{-# LANGUAGE RecordWildCards #-} +{-# LANGUAGE ViewPatterns #-} module Ide.Plugin.CallHierarchy.Query ( incomingCalls @@ -11,9 +10,9 @@ module Ide.Plugin.CallHierarchy.Query ( import qualified Data.Text as T import Database.SQLite.Simple import Development.IDE.GHC.Compat -import HieDb (HieDb (getConn), Symbol (..), - toNsChar) +import HieDb (HieDb (getConn), Symbol (..)) import Ide.Plugin.CallHierarchy.Types +import Prelude hiding (mod) incomingCalls :: HieDb -> Symbol -> IO [Vertex] incomingCalls (getConn -> conn) symbol = do @@ -73,9 +72,9 @@ getSymbolPosition (getConn -> conn) Vertex{..} = do ] ) (occ, sl, sc, sl, el, ec, el) -parseSymbol :: Symbol -> (String, String, String) +parseSymbol :: Symbol -> (OccName, ModuleName, Unit) parseSymbol Symbol{..} = - let o = toNsChar (occNameSpace symName) : occNameString symName - m = moduleNameString $ moduleName symModule - u = unitString $ moduleUnit symModule + let o = symName + m = moduleName symModule + u = moduleUnit symModule in (o, m, u) diff --git a/plugins/hls-call-hierarchy-plugin/src/Ide/Plugin/CallHierarchy/Types.hs b/plugins/hls-call-hierarchy-plugin/src/Ide/Plugin/CallHierarchy/Types.hs index d71b60e292..a31f85fd45 100644 --- a/plugins/hls-call-hierarchy-plugin/src/Ide/Plugin/CallHierarchy/Types.hs +++ b/plugins/hls-call-hierarchy-plugin/src/Ide/Plugin/CallHierarchy/Types.hs @@ -1,5 +1,4 @@ {-# LANGUAGE DeriveAnyClass #-} -{-# LANGUAGE DeriveGeneric #-} {-# LANGUAGE DuplicateRecordFields #-} module Ide.Plugin.CallHierarchy.Types where diff --git a/plugins/hls-call-hierarchy-plugin/test/Main.hs b/plugins/hls-call-hierarchy-plugin/test/Main.hs index af51fdd04c..31dad633e6 100644 --- a/plugins/hls-call-hierarchy-plugin/test/Main.hs +++ b/plugins/hls-call-hierarchy-plugin/test/Main.hs @@ -1,8 +1,5 @@ -{-# LANGUAGE LambdaCase #-} -{-# LANGUAGE OverloadedStrings #-} -{-# LANGUAGE RankNTypes #-} -{-# LANGUAGE StandaloneDeriving #-} -{-# LANGUAGE TupleSections #-} +{-# LANGUAGE LambdaCase #-} +{-# LANGUAGE OverloadedStrings #-} module Main (main) where @@ -17,11 +14,8 @@ import Development.IDE.Test import Ide.Plugin.CallHierarchy import qualified Language.LSP.Protocol.Lens as L import qualified Language.LSP.Test as Test -import System.Directory.Extra import System.FilePath -import qualified System.IO.Extra import Test.Hls -import Test.Hls.Util (withCanonicalTempDir) plugin :: PluginTestDescriptor () plugin = mkPluginTestDescriptor' descriptor "call-hierarchy" @@ -119,13 +113,14 @@ prepareCallHierarchyTests = , testGroup "data family" [ testCase "1" $ do let contents = T.unlines ["{-# LANGUAGE TypeFamilies #-}", "data family A"] - range = mkRange 1 0 1 11 + -- Since GHC 9.10 the range also includes the family name (and its parameters if any) + range = mkRange 1 0 1 (if ghcVersion >= GHC910 then 13 else 11) selRange = mkRange 1 12 1 13 expected = mkCallHierarchyItemT "A" SymbolKind_Function range selRange oneCaseWithCreate contents 1 12 expected , testCase "2" $ do let contents = T.unlines [ "{-# LANGUAGE TypeFamilies #-}" , "data family A a"] - range = mkRange 1 0 1 11 + range = mkRange 1 0 1 (if ghcVersion >= GHC910 then 15 else 11) selRange = mkRange 1 12 1 13 expected = mkCallHierarchyItemT "A" SymbolKind_Function range selRange oneCaseWithCreate contents 1 12 expected @@ -196,20 +191,16 @@ incomingCallsTests :: TestTree incomingCallsTests = testGroup "Incoming Calls" [ testGroup "single file" - [ - testCase "xdata unavailable" $ + [ testCase "xdata unavailable" $ runSessionWithServer def plugin testDataDir $ do doc <- createDoc "A.hs" "haskell" $ T.unlines ["a=3", "b=a"] waitForIndex (testDataDir "A.hs") - [item] <- Test.prepareCallHierarchy (mkPrepareCallHierarchyParam doc 1 0) + item <- expectOneElement =<< Test.prepareCallHierarchy (mkPrepareCallHierarchyParam doc 1 0) let expected = [CallHierarchyIncomingCall item [mkRange 1 2 1 3]] - Test.prepareCallHierarchy (mkPrepareCallHierarchyParam doc 0 0) >>= - \case - [item] -> do - let itemNoData = set L.data_ Nothing item - Test.incomingCalls (mkIncomingCallsParam itemNoData) >>= - \res -> liftIO $ sort expected @=? sort res - _ -> liftIO $ assertFailure "Not exactly one element" + item' <- expectOneElement =<< Test.prepareCallHierarchy (mkPrepareCallHierarchyParam doc 0 0) + let itemNoData = set L.data_ Nothing item' + res <- Test.incomingCalls (mkIncomingCallsParam itemNoData) + liftIO $ sort expected @=? sort res closeDoc doc , testCase "xdata available" $ do let contents = T.unlines ["a=3","b=a"] @@ -321,20 +312,16 @@ outgoingCallsTests :: TestTree outgoingCallsTests = testGroup "Outgoing Calls" [ testGroup "single file" - [ - testCase "xdata unavailable" $ withCanonicalTempDir $ \dir -> + [ testCase "xdata unavailable" $ withCanonicalTempDir $ \dir -> runSessionWithServer def plugin dir $ do doc <- createDoc "A.hs" "haskell" $ T.unlines ["a=3", "b=a"] waitForIndex (dir "A.hs") - [item] <- Test.prepareCallHierarchy (mkPrepareCallHierarchyParam doc 0 1) + item <- expectOneElement =<< Test.prepareCallHierarchy (mkPrepareCallHierarchyParam doc 0 1) let expected = [CallHierarchyOutgoingCall item [mkRange 1 2 1 3]] - Test.prepareCallHierarchy (mkPrepareCallHierarchyParam doc 1 0) >>= - \case - [item] -> do - let itemNoData = set L.data_ Nothing item - Test.outgoingCalls (mkOutgoingCallsParam itemNoData) >>= - \res -> liftIO $ sort expected @=? sort res - _ -> liftIO $ assertFailure "Not exactly one element" + item' <- expectOneElement =<< Test.prepareCallHierarchy (mkPrepareCallHierarchyParam doc 1 0) + let itemNoData = set L.data_ Nothing item' + res <- Test.outgoingCalls (mkOutgoingCallsParam itemNoData) + liftIO $ sort expected @=? sort res closeDoc doc , testCase "xdata available" $ do let contents = T.unlines ["a=3", "b=a"] @@ -434,13 +421,9 @@ incomingCallTestCase contents queryX queryY positions ranges = withCanonicalTemp ) (zip positions ranges) let expected = map mkCallHierarchyIncomingCall items - -- liftIO delay - Test.prepareCallHierarchy (mkPrepareCallHierarchyParam doc queryX queryY) >>= - \case - [item] -> do - Test.incomingCalls (mkIncomingCallsParam item) >>= - \res -> liftIO $ sort expected @=? sort res - _ -> liftIO $ assertFailure "Not one element" + item <- expectOneElement =<< Test.prepareCallHierarchy (mkPrepareCallHierarchyParam doc queryX queryY) + res <- Test.incomingCalls (mkIncomingCallsParam item) + liftIO $ sort expected @=? sort res closeDoc doc incomingCallMultiFileTestCase :: FilePath -> Int -> Int -> M.Map FilePath [((Int, Int), Range)] -> Assertion @@ -456,13 +439,9 @@ incomingCallMultiFileTestCase filepath queryX queryY mp = <&> map (, range) ) pr) mp let expected = map mkCallHierarchyIncomingCall items - -- liftIO delay - Test.prepareCallHierarchy (mkPrepareCallHierarchyParam doc queryX queryY) >>= - \case - [item] -> do - Test.incomingCalls (mkIncomingCallsParam item) >>= - \res -> liftIO $ sort expected @=? sort res - _ -> liftIO $ assertFailure "Not one element" + item <- expectOneElement =<< Test.prepareCallHierarchy (mkPrepareCallHierarchyParam doc queryX queryY) + res <- Test.incomingCalls (mkIncomingCallsParam item) + liftIO $ sort expected @=? sort res closeDoc doc outgoingCallTestCase :: T.Text -> Int -> Int -> [(Int, Int)] -> [Range] -> Assertion @@ -476,12 +455,9 @@ outgoingCallTestCase contents queryX queryY positions ranges = withCanonicalTemp ) (zip positions ranges) let expected = map mkCallHierarchyOutgoingCall items - Test.prepareCallHierarchy (mkPrepareCallHierarchyParam doc queryX queryY) >>= - \case - [item] -> do - Test.outgoingCalls (mkOutgoingCallsParam item) >>= - \res -> liftIO $ sort expected @=? sort res - _ -> liftIO $ assertFailure "Not one element" + item <- expectOneElement =<< Test.prepareCallHierarchy (mkPrepareCallHierarchyParam doc queryX queryY) + res <- Test.outgoingCalls (mkOutgoingCallsParam item) + liftIO $ sort expected @=? sort res closeDoc doc outgoingCallMultiFileTestCase :: FilePath -> Int -> Int -> M.Map FilePath [((Int, Int), Range)] -> Assertion @@ -497,12 +473,9 @@ outgoingCallMultiFileTestCase filepath queryX queryY mp = <&> map (, range) ) pr) mp let expected = map mkCallHierarchyOutgoingCall items - Test.prepareCallHierarchy (mkPrepareCallHierarchyParam doc queryX queryY) >>= - \case - [item] -> do - Test.outgoingCalls (mkOutgoingCallsParam item) >>= - \res -> liftIO $ sort expected @=? sort res - _ -> liftIO $ assertFailure "Not one element" + item <- expectOneElement =<< Test.prepareCallHierarchy (mkPrepareCallHierarchyParam doc queryX queryY) + res <- Test.outgoingCalls (mkOutgoingCallsParam item) + liftIO $ sort expected @=? sort res closeDoc doc oneCaseWithCreate :: T.Text -> Int -> Int -> (Uri -> CallHierarchyItem -> Assertion) -> Assertion @@ -510,12 +483,15 @@ oneCaseWithCreate contents queryX queryY expected = withCanonicalTempDir $ \dir runSessionWithServer def plugin dir $ do doc <- createDoc "A.hs" "haskell" contents waitForIndex (dir "A.hs") - Test.prepareCallHierarchy (mkPrepareCallHierarchyParam doc queryX queryY) >>= - \case - [item] -> liftIO $ expected (doc ^. L.uri) item - res -> liftIO $ assertFailure "Not one element" + item <- expectOneElement =<< Test.prepareCallHierarchy (mkPrepareCallHierarchyParam doc queryX queryY) + liftIO $ expected (doc ^. L.uri) item closeDoc doc +expectOneElement :: [a] -> Session a +expectOneElement = \case + [x] -> pure x + xs -> liftIO . assertFailure $ "Expecting exactly one element, but got " ++ show (length xs) + mkCallHierarchyItem' :: String -> T.Text -> SymbolKind -> Range -> Range -> Uri -> CallHierarchyItem -> Assertion mkCallHierarchyItem' prefix name kind range selRange uri c@(CallHierarchyItem name' kind' tags' detail' uri' range' selRange' xdata') = do assertHierarchyItem name name' @@ -528,7 +504,7 @@ mkCallHierarchyItem' prefix name kind range selRange uri c@(CallHierarchyItem na case xdata' of Nothing -> assertFailure ("In " ++ show c ++ ", got Nothing for data but wanted " ++ show xdata) Just v -> case Aeson.fromJSON v of - Aeson.Success v -> assertBool ("In " ++ show c ++ " wanted data prefix: " ++ show xdata) (xdata `T.isPrefixOf` v) + Aeson.Success v' -> assertBool ("In " ++ show c ++ " wanted data prefix: " ++ show xdata) (xdata `T.isPrefixOf` v') Aeson.Error err -> assertFailure ("In " ++ show c ++ " wanted data prefix: " ++ show xdata ++ " but json parsing failed with " ++ show err) where tags = Nothing @@ -550,7 +526,7 @@ mkCallHierarchyOutgoingCall :: (CallHierarchyItem, Range) -> CallHierarchyOutgoi mkCallHierarchyOutgoingCall (item, range) = CallHierarchyOutgoingCall item [range] testDataDir :: FilePath -testDataDir = "test" "testdata" +testDataDir = "plugins" "hls-call-hierarchy-plugin" "test" "testdata" mkPrepareCallHierarchyParam :: TextDocumentIdentifier -> Int -> Int -> CallHierarchyPrepareParams mkPrepareCallHierarchyParam doc x y = CallHierarchyPrepareParams doc (Position (fromIntegral x) (fromIntegral y)) Nothing @@ -570,6 +546,6 @@ waitForIndex fp1 = skipManyTill anyMessage $ void $ referenceReady lenientEquals -- filepath from the message lenientEquals :: FilePath -> Bool lenientEquals fp2 - | isRelative fp1 = any (equalFilePath fp1) (map (foldr () "") $ tails $ splitDirectories fp2) + | isRelative fp1 = any (equalFilePath fp1 . joinPath) $ tails $ splitDirectories fp2 | otherwise = equalFilePath fp1 fp2 diff --git a/plugins/hls-call-hierarchy-plugin/test/testdata/hie.yaml b/plugins/hls-call-hierarchy-plugin/test/testdata/hie.yaml new file mode 100644 index 0000000000..1909df7d79 --- /dev/null +++ b/plugins/hls-call-hierarchy-plugin/test/testdata/hie.yaml @@ -0,0 +1 @@ +cradle: {direct: {arguments: ["A", "B", "C"]}} diff --git a/plugins/hls-change-type-signature-plugin/LICENSE b/plugins/hls-change-type-signature-plugin/LICENSE deleted file mode 100644 index 261eeb9e9f..0000000000 --- a/plugins/hls-change-type-signature-plugin/LICENSE +++ /dev/null @@ -1,201 +0,0 @@ - Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ - - TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - - 1. Definitions. - - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. - - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. - - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. - - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. - - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. - - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. - - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). - - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. - - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution." - - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. - - 2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. - - 3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. - - 4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: - - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and - - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and - - (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. - - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. - - 5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. - - 6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. - - 7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. - - 8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. - - 9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. - - END OF TERMS AND CONDITIONS - - APPENDIX: How to apply the Apache License to your work. - - To apply the Apache License to your work, attach the following - boilerplate notice, with the fields enclosed by brackets "[]" - replaced with your own identifying information. (Don't include - the brackets!) The text should be enclosed in the appropriate - comment syntax for the file format. We also recommend that a - file or class name and description of purpose be included on the - same "printed page" as the copyright notice for easier - identification within third-party archives. - - Copyright [yyyy] [name of copyright owner] - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. diff --git a/plugins/hls-change-type-signature-plugin/hls-change-type-signature-plugin.cabal b/plugins/hls-change-type-signature-plugin/hls-change-type-signature-plugin.cabal deleted file mode 100644 index a13d396f3a..0000000000 --- a/plugins/hls-change-type-signature-plugin/hls-change-type-signature-plugin.cabal +++ /dev/null @@ -1,71 +0,0 @@ -cabal-version: 2.4 -name: hls-change-type-signature-plugin -version: 2.4.0.0 -synopsis: Change a declarations type signature with a Code Action -description: - Please see the README on GitHub at - -license: Apache-2.0 -license-file: LICENSE -author: Nick Suchecki -maintainer: nicksuchecki@gmail.com -category: Development -build-type: Simple -extra-source-files: - LICENSE - README.md - test/testdata/*.hs - test/testdata/*.txt - test/testdata/*.yaml - -source-repository head - type: git - location: https://github.com/haskell/haskell-language-server.git - -library - exposed-modules: Ide.Plugin.ChangeTypeSignature - hs-source-dirs: src - build-depends: - , base >=4.12 && < 5 - , ghcide == 2.4.0.0 - , hls-plugin-api == 2.4.0.0 - , lsp-types - , regex-tdfa - , syb - , text - , transformers - , unordered-containers - , containers - ghc-options: -Wall - default-language: Haskell2010 - default-extensions: - ConstraintKinds - DataKinds - ExplicitNamespaces - FlexibleContexts - NamedFieldPuns - OverloadedStrings - RecordWildCards - TypeOperators - - -test-suite tests - type: exitcode-stdio-1.0 - default-language: Haskell2010 - hs-source-dirs: test - main-is: Main.hs - ghc-options: -threaded -rtsopts -with-rtsopts=-N -fno-ignore-asserts -Wall - build-depends: - , base >=4.12 && < 5 - , filepath - , hls-change-type-signature-plugin - , hls-test-utils == 2.4.0.0 - , lsp - , QuickCheck - , regex-tdfa - , text - default-extensions: - NamedFieldPuns - OverloadedStrings - TypeOperators - ViewPatterns diff --git a/plugins/hls-change-type-signature-plugin/src/Ide/Plugin/ChangeTypeSignature.hs b/plugins/hls-change-type-signature-plugin/src/Ide/Plugin/ChangeTypeSignature.hs index a0933fc25b..df776e6d15 100644 --- a/plugins/hls-change-type-signature-plugin/src/Ide/Plugin/ChangeTypeSignature.hs +++ b/plugins/hls-change-type-signature-plugin/src/Ide/Plugin/ChangeTypeSignature.hs @@ -33,7 +33,8 @@ import Language.LSP.Protocol.Types import Text.Regex.TDFA ((=~)) descriptor :: PluginId -> PluginDescriptor IdeState -descriptor plId = (defaultPluginDescriptor plId) { pluginHandlers = mkPluginHandler SMethod_TextDocumentCodeAction (codeActionHandler plId) } +descriptor plId = (defaultPluginDescriptor plId "Provides a code action to change the type signature of a binding if it is wrong") + { pluginHandlers = mkPluginHandler SMethod_TextDocumentCodeAction (codeActionHandler plId) } codeActionHandler :: PluginId -> PluginMethodHandler IdeState 'Method_TextDocumentCodeAction codeActionHandler plId ideState _ CodeActionParams {_textDocument = TextDocumentIdentifier uri, _context = CodeActionContext diags _ _} = do @@ -45,7 +46,7 @@ codeActionHandler plId ideState _ CodeActionParams {_textDocument = TextDocument getDecls :: MonadIO m => PluginId -> IdeState -> NormalizedFilePath -> ExceptT PluginError m [LHsDecl GhcPs] getDecls (PluginId changeTypeSignatureId) state = runActionE (T.unpack changeTypeSignatureId <> ".GetParsedModule") state - . (fmap (hsmodDecls . unLoc . pm_parsed_source)) + . fmap (hsmodDecls . unLoc . pm_parsed_source) . useE GetParsedModule -- | Text representing a Declaration's Name @@ -69,15 +70,12 @@ data ChangeSignature = ChangeSignature { , diagnostic :: Diagnostic } --- | Constraint needed to trackdown OccNames in signatures -type SigName = (HasOccName (IdP GhcPs)) - -- | Create a CodeAction from a Diagnostic -generateAction :: SigName => PluginId -> Uri -> [LHsDecl GhcPs] -> Diagnostic -> Maybe (Command |? CodeAction) +generateAction :: PluginId -> Uri -> [LHsDecl GhcPs] -> Diagnostic -> Maybe (Command |? CodeAction) generateAction plId uri decls diag = changeSigToCodeAction plId uri <$> diagnosticToChangeSig decls diag -- | Convert a diagnostic into a ChangeSignature and add the proper SrcSpan -diagnosticToChangeSig :: SigName => [LHsDecl GhcPs] -> Diagnostic -> Maybe ChangeSignature +diagnosticToChangeSig :: [LHsDecl GhcPs] -> Diagnostic -> Maybe ChangeSignature diagnosticToChangeSig decls diagnostic = do -- regex match on the GHC Error Message (expectedType, actualType, declName) <- matchingDiagnostic diagnostic @@ -106,7 +104,7 @@ errorMessageRegexes = [ -- be sure to add new Error Messages Regexes at the bott -- | Given a String with the name of a declaration, GHC's "Expected Type", find the declaration that matches -- both the name given and the Expected Type, and return the type signature location -findSigLocOfStringDecl :: SigName => [LHsDecl GhcPs] -> ExpectedSig -> String -> Maybe RealSrcSpan +findSigLocOfStringDecl :: [LHsDecl GhcPs] -> ExpectedSig -> String -> Maybe RealSrcSpan findSigLocOfStringDecl decls expectedType declName = something (const Nothing `extQ` findSig `extQ` findLocalSig) decls where -- search for Top Level Signatures diff --git a/plugins/hls-change-type-signature-plugin/test/Main.hs b/plugins/hls-change-type-signature-plugin/test/Main.hs index 98f45f3929..cd1b152c0b 100644 --- a/plugins/hls-change-type-signature-plugin/test/Main.hs +++ b/plugins/hls-change-type-signature-plugin/test/Main.hs @@ -25,8 +25,7 @@ import Test.Hls (CodeAction (..), Command, mkPluginTestDescriptor', openDoc, runSessionWithServer, testCase, testGroup, toEither, - type (|?), - waitForAllProgressDone, + type (|?), waitForBuildQueue, waitForDiagnostics, (@?=)) import Text.Regex.TDFA ((=~)) @@ -40,7 +39,8 @@ test :: TestTree test = testGroup "changeTypeSignature" [ testRegexes , codeActionTest "TExpectedActual" 4 11 - , knownBrokenForGhcVersions [GHC92, GHC94, GHC96, GHC98] "Error Message in 9.2/9.4 does not provide enough info" $ codeActionTest "TRigidType" 4 14 + , knownBrokenForGhcVersions [GHC96 .. GHC912] "Error Message in 9.2+ does not provide enough info" $ + codeActionTest "TRigidType" 4 14 , codeActionTest "TRigidType2" 4 6 , codeActionTest "TLocalBinding" 7 22 , codeActionTest "TLocalBindingShadow1" 11 8 @@ -88,15 +88,15 @@ testRegex921One = testGroup "Regex One" [ regex = errorMessageRegexes !! 2 testDataDir :: FilePath -testDataDir = "test" "testdata" +testDataDir = "plugins" "hls-change-type-signature-plugin" "test" "testdata" goldenChangeSignature :: FilePath -> (TextDocumentIdentifier -> Session ()) -> TestTree goldenChangeSignature fp = goldenWithHaskellDoc def changeTypeSignaturePlugin (fp <> " (golden)") testDataDir fp "expected" "hs" codeActionTest :: FilePath -> Int -> Int -> TestTree codeActionTest fp line col = goldenChangeSignature fp $ \doc -> do - void $ waitForDiagnostics -- code actions are triggered from Diagnostics - void $ waitForAllProgressDone -- apparently some tests need this to get the CodeAction to show up + void waitForDiagnostics -- code actions are triggered from Diagnostics + void waitForBuildQueue -- apparently some tests need this to get the CodeAction to show up actions <- getCodeActions doc (pointRange line col) foundActions <- findChangeTypeActions actions liftIO $ length foundActions @?= 1 diff --git a/plugins/hls-change-type-signature-plugin/test/testdata/TErrorGivenPartialSignature.hs b/plugins/hls-change-type-signature-plugin/test/testdata/TErrorGivenPartialSignature.hs index caa595242a..da45222d93 100644 --- a/plugins/hls-change-type-signature-plugin/test/testdata/TErrorGivenPartialSignature.hs +++ b/plugins/hls-change-type-signature-plugin/test/testdata/TErrorGivenPartialSignature.hs @@ -1,4 +1,4 @@ -module ErrorGivenPartialSignature where +module TErrorGivenPartialSignature where partial :: Int -> Int partial x = init x diff --git a/plugins/hls-class-plugin/LICENSE b/plugins/hls-class-plugin/LICENSE deleted file mode 100644 index 261eeb9e9f..0000000000 --- a/plugins/hls-class-plugin/LICENSE +++ /dev/null @@ -1,201 +0,0 @@ - Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ - - TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - - 1. Definitions. - - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. - - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. - - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. - - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. - - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. - - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. - - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). - - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. - - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution." - - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. - - 2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. - - 3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. - - 4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: - - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and - - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and - - (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. - - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. - - 5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. - - 6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. - - 7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. - - 8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. - - 9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. - - END OF TERMS AND CONDITIONS - - APPENDIX: How to apply the Apache License to your work. - - To apply the Apache License to your work, attach the following - boilerplate notice, with the fields enclosed by brackets "[]" - replaced with your own identifying information. (Don't include - the brackets!) The text should be enclosed in the appropriate - comment syntax for the file format. We also recommend that a - file or class name and description of purpose be included on the - same "printed page" as the copyright notice for easier - identification within third-party archives. - - Copyright [yyyy] [name of copyright owner] - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. diff --git a/plugins/hls-class-plugin/hls-class-plugin.cabal b/plugins/hls-class-plugin/hls-class-plugin.cabal deleted file mode 100644 index 035b2f554c..0000000000 --- a/plugins/hls-class-plugin/hls-class-plugin.cabal +++ /dev/null @@ -1,91 +0,0 @@ -cabal-version: 2.4 -name: hls-class-plugin -version: 2.4.0.0 -synopsis: - Class/instance management plugin for Haskell Language Server - -description: - Class/instance management plugin for Haskell Language Server. - For usage, please see README of HLS on GitHub at - -license: Apache-2.0 -license-file: LICENSE -author: Junyoung Clare Jang -maintainer: jjc9310@gmail.com -homepage: https://github.com/haskell/haskell-language-server#readme -bug-reports: https://github.com/haskell/haskell-language-server/issues -category: Development -build-type: Simple -extra-source-files: - LICENSE - test/testdata/*.hs - -source-repository head - type: git - location: https://github.com/haskell/haskell-language-server.git - -library - -- Plugins that need exactprint have not been updated for 9.8 yet - if impl(ghc >= 9.8) - buildable: False - else - buildable: True - exposed-modules: Ide.Plugin.Class - other-modules: Ide.Plugin.Class.CodeAction - , Ide.Plugin.Class.CodeLens - , Ide.Plugin.Class.ExactPrint - , Ide.Plugin.Class.Types - , Ide.Plugin.Class.Utils - hs-source-dirs: src - build-depends: - , aeson - , base >=4.12 && <5 - , containers - , deepseq - , extra - , ghc - , ghcide == 2.4.0.0 - , ghc-boot-th - , hls-graph - , hls-plugin-api == 2.4.0.0 - , lens - , lsp - , mtl - , text - , transformers - - if impl(ghc >=9.2.1) - build-depends: ghc-exactprint >= 1.5 - else - build-depends: ghc-exactprint >= 0.6.4 && <1.1 - - default-language: Haskell2010 - default-extensions: - DataKinds - TypeOperators - OverloadedStrings - - ghc-options: -Wall -Wno-unticked-promoted-constructors -Wno-name-shadowing - -test-suite tests - if impl(ghc >= 9.8) - buildable: False - else - buildable: True - type: exitcode-stdio-1.0 - default-language: Haskell2010 - hs-source-dirs: test - main-is: Main.hs - ghc-options: -threaded -rtsopts -with-rtsopts=-N - build-depends: - , aeson - , base - , filepath - , ghcide - , hls-class-plugin - , hls-plugin-api - , hls-test-utils == 2.4.0.0 - , lens - , lsp-types - , row-types - , text diff --git a/plugins/hls-class-plugin/src/Ide/Plugin/Class.hs b/plugins/hls-class-plugin/src/Ide/Plugin/Class.hs index 49abbe9710..15a9fe0f02 100644 --- a/plugins/hls-class-plugin/src/Ide/Plugin/Class.hs +++ b/plugins/hls-class-plugin/src/Ide/Plugin/Class.hs @@ -8,7 +8,7 @@ import Ide.Plugin.Class.Types import Ide.Types import Language.LSP.Protocol.Message descriptor :: Recorder (WithPriority Log) -> PluginId -> PluginDescriptor IdeState -descriptor recorder plId = (defaultPluginDescriptor plId) +descriptor recorder plId = (defaultPluginDescriptor plId "Provides code actions and lenses for working with typeclasses") { pluginCommands = commands plId , pluginRules = getInstanceBindTypeSigsRule recorder >> getInstanceBindLensRule recorder , pluginHandlers = mkPluginHandler SMethod_TextDocumentCodeAction (codeAction recorder) diff --git a/plugins/hls-class-plugin/src/Ide/Plugin/Class/CodeAction.hs b/plugins/hls-class-plugin/src/Ide/Plugin/Class/CodeAction.hs index a3d75465bd..ecbd495246 100644 --- a/plugins/hls-class-plugin/src/Ide/Plugin/Class/CodeAction.hs +++ b/plugins/hls-class-plugin/src/Ide/Plugin/Class/CodeAction.hs @@ -1,10 +1,15 @@ {-# LANGUAGE GADTs #-} +{-# LANGUAGE LambdaCase #-} {-# LANGUAGE OverloadedLists #-} {-# LANGUAGE RecordWildCards #-} {-# LANGUAGE ViewPatterns #-} -module Ide.Plugin.Class.CodeAction where +module Ide.Plugin.Class.CodeAction ( + addMethodPlaceholders, + codeAction, +) where +import Control.Arrow ((>>>)) import Control.Lens hiding (List, use) import Control.Monad.Error.Class (MonadError (throwError)) import Control.Monad.Extra @@ -13,8 +18,6 @@ import Control.Monad.Trans.Class (lift) import Control.Monad.Trans.Except (ExceptT) import Control.Monad.Trans.Maybe import Data.Aeson hiding (Null) -import Data.Bifunctor (second) -import Data.Either.Extra (rights) import Data.List import Data.List.Extra (nubOrdOn) import qualified Data.Map.Strict as Map @@ -23,10 +26,14 @@ import Data.Maybe (isNothing, listToMaybe, import qualified Data.Set as Set import qualified Data.Text as T import Development.IDE -import Development.IDE.Core.Compile (sourceTypecheck) +import Development.IDE.Core.FileStore (getVersionedTextDoc) import Development.IDE.Core.PluginUtils import Development.IDE.Core.PositionMapping (fromCurrentRange) import Development.IDE.GHC.Compat +import Development.IDE.GHC.Compat.Error (TcRnMessage (..), + _TcRnMessage, + msgEnvelopeErrorL, + stripTcRnMessageContext) import Development.IDE.GHC.Compat.Util import Development.IDE.Spans.AtPoint (pointCommand) import Ide.Plugin.Class.ExactPrint @@ -39,11 +46,10 @@ import Ide.Types import qualified Language.LSP.Protocol.Lens as L import Language.LSP.Protocol.Message import Language.LSP.Protocol.Types -import Language.LSP.Server addMethodPlaceholders :: PluginId -> CommandFunction IdeState AddMinimalMethodsParams -addMethodPlaceholders _ state param@AddMinimalMethodsParams{..} = do - caps <- lift $ getClientCapabilities +addMethodPlaceholders _ state _ param@AddMinimalMethodsParams{..} = do + caps <- lift pluginGetClientCapabilities nfp <- getNormalizedFilePathE (verTxtDocId ^. L.uri) pm <- runActionE "classplugin.addMethodPlaceholders.GetParsedModule" state $ useE GetParsedModule nfp @@ -58,7 +64,7 @@ addMethodPlaceholders _ state param@AddMinimalMethodsParams{..} = do then mergeEdit (workspaceEdit caps old new) pragmaInsertion else workspaceEdit caps old new - void $ lift $ sendRequest SMethod_WorkspaceApplyEdit (ApplyWorkspaceEditParams Nothing edit) (\_ -> pure ()) + void $ lift $ pluginSendRequest SMethod_WorkspaceApplyEdit (ApplyWorkspaceEditParams Nothing edit) (\_ -> pure ()) pure $ InR Null where @@ -80,23 +86,25 @@ addMethodPlaceholders _ state param@AddMinimalMethodsParams{..} = do -- This implementation is ad-hoc in a sense that the diagnostic detection mechanism is -- sensitive to the format of diagnostic messages from GHC. codeAction :: Recorder (WithPriority Log) -> PluginMethodHandler IdeState Method_TextDocumentCodeAction -codeAction recorder state plId (CodeActionParams _ _ docId _ context) = do - verTxtDocId <- lift $ getVersionedTextDoc docId +codeAction recorder state plId (CodeActionParams _ _ docId caRange _) = do + verTxtDocId <- liftIO $ runAction "classplugin.codeAction.getVersionedTextDoc" state $ getVersionedTextDoc docId nfp <- getNormalizedFilePathE (verTxtDocId ^. L.uri) - actions <- join <$> mapM (mkActions nfp verTxtDocId) methodDiags - pure $ InL actions + activeDiagnosticsInRange (shakeExtras state) nfp caRange + >>= \case + Nothing -> pure $ InL [] + Just fileDiags -> do + actions <- join <$> mapM (mkActions nfp verTxtDocId) (methodDiags fileDiags) + pure $ InL actions where - diags = context ^. L.diagnostics - - ghcDiags = filter (\d -> d ^. L.source == Just sourceTypecheck) diags - methodDiags = filter (\d -> isClassMethodWarning (d ^. L.message)) ghcDiags + methodDiags fileDiags = + mapMaybe (\d -> (d,) <$> isClassMethodWarning (d ^. fdStructuredMessageL)) fileDiags mkActions :: NormalizedFilePath -> VersionedTextDocumentIdentifier - -> Diagnostic - -> ExceptT PluginError (LspT Ide.Plugin.Config.Config IO) [Command |? CodeAction] - mkActions docPath verTxtDocId diag = do + -> (FileDiagnostic, ClassMinimalDef) + -> ExceptT PluginError (HandlerM Ide.Plugin.Config.Config) [Command |? CodeAction] + mkActions docPath verTxtDocId (diag, classMinDef) = do (HAR {hieAst = ast}, pmap) <- runActionE "classplugin.findClassIdentifier.GetHieAst" state $ useWithStaleE GetHieAst docPath instancePosition <- handleMaybe (PluginInvalidUserState "fromCurrentRange") $ @@ -108,21 +116,19 @@ codeAction recorder state plId (CodeActionParams _ _ docId _ context) = do $ useE GetInstanceBindTypeSigs docPath (tmrTypechecked -> gblEnv ) <- runActionE "classplugin.codeAction.TypeCheck" state $ useE TypeCheck docPath (hscEnv -> hsc) <- runActionE "classplugin.codeAction.GhcSession" state $ useE GhcSession docPath - implemented <- findImplementedMethods ast instancePosition - logWith recorder Info (LogImplementedMethods cls implemented) + logWith recorder Debug (LogImplementedMethods (hsc_dflags hsc) cls classMinDef) pure $ concatMap mkAction $ nubOrdOn snd $ filter ((/=) mempty . snd) - $ fmap (second (filter (\(bind, _) -> bind `notElem` implemented))) - $ mkMethodGroups hsc gblEnv range sigs cls + $ mkMethodGroups hsc gblEnv range sigs classMinDef where - range = diag ^. L.range + range = diag ^. fdLspDiagnosticL . L.range - mkMethodGroups :: HscEnv -> TcGblEnv -> Range -> [InstanceBindTypeSig] -> Class -> [MethodGroup] - mkMethodGroups hsc gblEnv range sigs cls = minimalDef <> [allClassMethods] + mkMethodGroups :: HscEnv -> TcGblEnv -> Range -> [InstanceBindTypeSig] -> ClassMinimalDef -> [MethodGroup] + mkMethodGroups hsc gblEnv range sigs classMinDef = minimalDef <> [allClassMethods] where - minimalDef = minDefToMethodGroups hsc gblEnv range sigs $ classMinimalDef cls + minimalDef = minDefToMethodGroups hsc gblEnv range sigs classMinDef allClassMethods = ("all missing methods", makeMethodDefinitions hsc gblEnv range sigs) mkAction :: MethodGroup -> [Command |? CodeAction] @@ -159,29 +165,10 @@ codeAction recorder state plId (CodeActionParams _ _ docId _ context) = do $ listToMaybe $ mapMaybe listToMaybe $ pointCommand hf instancePosition - ( (Map.keys . Map.filter isClassNodeIdentifier . getNodeIds) + ( (Map.keys . Map.filterWithKey isClassNodeIdentifier . getNodeIds) <=< nodeChildren ) - findImplementedMethods - :: HieASTs a - -> Position - -> ExceptT PluginError (LspT Ide.Plugin.Config.Config IO) [T.Text] - findImplementedMethods asts instancePosition = do - pure - $ concat - $ pointCommand asts instancePosition - $ map (T.pack . getOccString) . rights . findInstanceValBindIdentifiers - - -- | Recurses through the given AST to find identifiers which are - -- 'InstanceValBind's. - findInstanceValBindIdentifiers :: HieAST a -> [Identifier] - findInstanceValBindIdentifiers ast = - let valBindIds = Map.keys - . Map.filter (any isInstanceValBind . identInfo) - $ getNodeIds ast - in valBindIds <> concatMap findInstanceValBindIdentifiers (nodeChildren ast) - findClassFromIdentifier docPath (Right name) = do (hscEnv -> hscenv, _) <- runActionE "classplugin.findClassFromIdentifier.GhcSessionDeps" state $ useWithStaleE GhcSessionDeps docPath @@ -198,15 +185,20 @@ codeAction recorder state plId (CodeActionParams _ _ docId _ context) = do _ -> fail "Ide.Plugin.Class.findClassFromIdentifier" findClassFromIdentifier _ (Left _) = throwError (PluginInternalError "Ide.Plugin.Class.findClassIdentifier") -isClassNodeIdentifier :: IdentifierDetails a -> Bool -isClassNodeIdentifier ident = (isNothing . identType) ident && Use `Set.member` identInfo ident +-- see https://hackage.haskell.org/package/ghc-9.8.1/docs/src/GHC.Types.Name.Occurrence.html#mkClassDataConOcc +isClassNodeIdentifier :: Identifier -> IdentifierDetails a -> Bool +isClassNodeIdentifier (Right i) ident | 'C':':':_ <- unpackFS $ occNameFS $ occName i = (isNothing . identType) ident && Use `Set.member` identInfo ident +isClassNodeIdentifier _ _ = False -isClassMethodWarning :: T.Text -> Bool -isClassMethodWarning = T.isPrefixOf "• No explicit implementation for" +isClassMethodWarning :: StructuredMessage -> Maybe ClassMinimalDef +isClassMethodWarning message = case message ^? _SomeStructuredMessage . msgEnvelopeErrorL . _TcRnMessage of + Nothing -> Nothing + Just tcRnMessage -> isUnsatisfiedMinimalDefWarning tcRnMessage -isInstanceValBind :: ContextInfo -> Bool -isInstanceValBind (ValBind InstanceBind _ _) = True -isInstanceValBind _ = False +isUnsatisfiedMinimalDefWarning :: TcRnMessage -> Maybe ClassMinimalDef +isUnsatisfiedMinimalDefWarning = stripTcRnMessageContext >>> \case + TcRnUnsatisfiedMinimalDef classMinDef -> Just classMinDef + _ -> Nothing type MethodSignature = T.Text type MethodName = T.Text @@ -239,6 +231,6 @@ minDefToMethodGroups hsc gblEnv range sigs minDef = makeMethodGroup <$> go minDe go (Var mn) = pure $ makeMethodDefinitions hsc gblEnv range $ filter ((==) (printOutputable mn) . signatureToName) sigs go (Or ms) = concatMap (go . unLoc) ms - go (And ms) = foldr (liftA2 (<>)) [[]] (fmap (go . unLoc) ms) + go (And ms) = foldr (liftA2 (<>) . go . unLoc) [[]] ms go (Parens m) = go (unLoc m) diff --git a/plugins/hls-class-plugin/src/Ide/Plugin/Class/CodeLens.hs b/plugins/hls-class-plugin/src/Ide/Plugin/Class/CodeLens.hs index ab345b2171..9410469516 100644 --- a/plugins/hls-class-plugin/src/Ide/Plugin/Class/CodeLens.hs +++ b/plugins/hls-class-plugin/src/Ide/Plugin/Class/CodeLens.hs @@ -1,5 +1,4 @@ {-# LANGUAGE GADTs #-} -{-# LANGUAGE NamedFieldPuns #-} {-# LANGUAGE OverloadedLists #-} {-# LANGUAGE ViewPatterns #-} module Ide.Plugin.Class.CodeLens where @@ -24,7 +23,6 @@ import Ide.Types import qualified Language.LSP.Protocol.Lens as L import Language.LSP.Protocol.Message import Language.LSP.Protocol.Types -import Language.LSP.Server (sendRequest) -- The code lens method is only responsible for providing the ranges of the code -- lenses matched to a unique id @@ -69,7 +67,7 @@ codeLensResolve state plId cl uri uniqueID = do -- Finally the command actually generates and applies the workspace edit for the -- specified unique id. codeLensCommandHandler :: PluginId -> CommandFunction IdeState InstanceBindLensCommand -codeLensCommandHandler plId state InstanceBindLensCommand{commandUri, commandEdit} = do +codeLensCommandHandler plId state _ InstanceBindLensCommand{commandUri, commandEdit} = do nfp <- getNormalizedFilePathE commandUri (InstanceBindLensResult (InstanceBindLens{lensEnabledExtensions}), _) <- runActionE "classplugin.GetInstanceBindLens" state @@ -84,7 +82,7 @@ codeLensCommandHandler plId state InstanceBindLensCommand{commandUri, commandEdi pragmaInsertion = maybeToList $ flip insertNewPragma InstanceSigs <$> mbPragma wEdit = workspaceEdit pragmaInsertion - _ <- lift $ sendRequest SMethod_WorkspaceApplyEdit (ApplyWorkspaceEditParams Nothing wEdit) (\_ -> pure ()) + _ <- lift $ pluginSendRequest SMethod_WorkspaceApplyEdit (ApplyWorkspaceEditParams Nothing wEdit) (\_ -> pure ()) pure $ InR Null where workspaceEdit pragmaInsertion= diff --git a/plugins/hls-class-plugin/src/Ide/Plugin/Class/ExactPrint.hs b/plugins/hls-class-plugin/src/Ide/Plugin/Class/ExactPrint.hs index fd4a5305d2..bb0994442a 100644 --- a/plugins/hls-class-plugin/src/Ide/Plugin/Class/ExactPrint.hs +++ b/plugins/hls-class-plugin/src/Ide/Plugin/Class/ExactPrint.hs @@ -5,23 +5,36 @@ module Ide.Plugin.Class.ExactPrint where import Control.Monad.Trans.Maybe +import Data.Either.Extra (eitherToMaybe) +import Data.Functor.Identity (Identity) import qualified Data.Text as T import Development.IDE.GHC.Compat +import GHC.Parser.Annotation import Ide.Plugin.Class.Types import Ide.Plugin.Class.Utils import Language.Haskell.GHC.ExactPrint import Language.Haskell.GHC.ExactPrint.Parsers +import Language.LSP.Protocol.Types (Range) -import Data.Either.Extra (eitherToMaybe) -import GHC.Parser.Annotation +#if MIN_VERSION_ghc(9,9,0) +import Control.Lens (_head, over) +#endif makeEditText :: Monad m => ParsedModule -> DynFlags -> AddMinimalMethodsParams -> MaybeT m (T.Text, T.Text) --- addMethodDecls :: ParsedSource -> [(LHsDecl GhcPs, LHsDecl GhcPs)] -> Range -> Bool -> TransformT Identity (Located HsModule) makeEditText pm df AddMinimalMethodsParams{..} = do mDecls <- MaybeT . pure $ traverse (makeMethodDecl df) methodGroup - let ps = makeDeltaAst $ pm_parsed_source pm + let ps = +#if !MIN_VERSION_ghc(9,9,0) + makeDeltaAst $ +#endif + pm_parsed_source pm + old = T.pack $ exactPrint ps +#if MIN_VERSION_ghc_exactprint(1,10,0) + ps' = addMethodDecls ps mDecls range withSig +#else (ps', _, _) = runTransform (addMethodDecls ps mDecls range withSig) +#endif new = T.pack $ exactPrint ps' pure (old, new) @@ -31,14 +44,53 @@ makeMethodDecl df (mName, sig) = do sig' <- eitherToMaybe $ parseDecl df (T.unpack sig) $ T.unpack sig pure (name, sig') +#if MIN_VERSION_ghc_exactprint(1,10,0) +addMethodDecls :: ParsedSource -> [(LHsDecl GhcPs, LHsDecl GhcPs)] -> Range -> Bool -> Located (HsModule GhcPs) +#else +addMethodDecls :: ParsedSource -> [(LHsDecl GhcPs, LHsDecl GhcPs)] -> Range -> Bool -> TransformT Identity (Located (HsModule GhcPs)) +#endif addMethodDecls ps mDecls range withSig | withSig = go (concatMap (\(decl, sig) -> [sig, decl]) mDecls) | otherwise = go (map fst mDecls) where go inserting = do +#if MIN_VERSION_ghc_exactprint(1,10,0) + let allDecls = hsDecls ps +#else allDecls <- hsDecls ps - let (before, ((L l inst): after)) = break (inRange range . getLoc) allDecls - replaceDecls ps (before ++ (L l (addWhere inst)): (map newLine inserting ++ after)) +#endif + case break (inRange range . getLoc) allDecls of + (before, L l inst : after) -> + let + instSpan = realSrcSpan $ getLoc l +#if MIN_VERSION_ghc(9,11,0) + instCol = srcSpanStartCol instSpan - 1 +#else + instCol = srcSpanStartCol instSpan +#endif +#if MIN_VERSION_ghc(9,9,0) + instRow = srcSpanEndLine instSpan + methodEpAnn = noAnnSrcSpanDP $ deltaPos 1 (instCol + defaultIndent) + -- Put each TyCl method/type signature on separate line, indented by 2 spaces relative to instance decl + newLine (L _ e) = L methodEpAnn e + + -- Set DeltaPos for following declarations so they don't move undesirably + resetFollowing = + over _head (\followingDecl -> + let followingDeclRow = srcSpanStartLine $ realSrcSpan $ getLoc followingDecl + delta = DifferentLine (followingDeclRow - instRow) instCol + in setEntryDP followingDecl delta) +#else + newLine (L l e) = + let dp = deltaPos 1 (instCol + defaultIndent - 1) + in L (noAnnSrcSpanDP (getLoc l) dp <> l) e + + resetFollowing = id +#endif + in replaceDecls ps (before ++ L l (addWhere inst):(map newLine inserting ++ resetFollowing after)) + (before, []) -> + replaceDecls ps before + -- Add `where` keyword for `instance X where` if `where` is missing. -- -- The `where` in ghc-9.2 is now stored in the instance declaration @@ -48,18 +100,39 @@ addMethodDecls ps mDecls range withSig -- -- See the link for the original definition: -- https://hackage.haskell.org/package/ghc-9.2.1/docs/Language-Haskell-Syntax-Extension.html#t:XCClsInstDecl - addWhere (InstD xInstD (ClsInstD ext decl@ClsInstDecl{..})) = - let (EpAnn entry anns comments, key) = cid_ext - in InstD xInstD (ClsInstD ext decl { - cid_ext = (EpAnn - entry - (AddEpAnn AnnWhere (EpaDelta (SameLine 1) []) : anns) - comments - , key) - }) + addWhere :: HsDecl GhcPs -> HsDecl GhcPs + addWhere instd@(InstD xInstD (ClsInstD ext decl@ClsInstDecl{..})) = + case cid_ext of +#if MIN_VERSION_ghc(9,11,0) + (warnings, anns, key) + | EpTok _ <- acid_where anns -> instd + | otherwise -> + InstD xInstD (ClsInstD ext decl { + cid_ext = ( warnings + , anns { acid_where = EpTok d1 } + , key + ) + }) +#elif MIN_VERSION_ghc(9,9,0) + (warnings, anns, key) + | any (\(AddEpAnn kw _ )-> kw == AnnWhere) anns -> instd + | otherwise -> + InstD xInstD (ClsInstD ext decl { + cid_ext = ( warnings + , AddEpAnn AnnWhere d1 : anns + , key + ) + }) +#else + (EpAnn entry anns comments, key) -> + InstD xInstD (ClsInstD ext decl { + cid_ext = (EpAnn + entry + (AddEpAnn AnnWhere d1 : anns) + comments + , key + ) + }) + _ -> instd +#endif addWhere decl = decl - - newLine (L l e) = - let dp = deltaPos 1 defaultIndent - in L (noAnnSrcSpanDP (getLoc l) dp <> l) e - diff --git a/plugins/hls-class-plugin/src/Ide/Plugin/Class/Types.hs b/plugins/hls-class-plugin/src/Ide/Plugin/Class/Types.hs index 9f4e5185a8..1669aba43d 100644 --- a/plugins/hls-class-plugin/src/Ide/Plugin/Class/Types.hs +++ b/plugins/hls-class-plugin/src/Ide/Plugin/Class/Types.hs @@ -1,10 +1,9 @@ -{-# LANGUAGE DeriveAnyClass #-} -{-# LANGUAGE DeriveGeneric #-} -{-# LANGUAGE FlexibleContexts #-} -{-# LANGUAGE LambdaCase #-} -{-# LANGUAGE RecordWildCards #-} -{-# LANGUAGE TypeFamilies #-} -{-# LANGUAGE ViewPatterns #-} +{-# LANGUAGE CPP #-} +{-# LANGUAGE DeriveAnyClass #-} +{-# LANGUAGE LambdaCase #-} +{-# LANGUAGE RecordWildCards #-} +{-# LANGUAGE TypeFamilies #-} +{-# LANGUAGE ViewPatterns #-} module Ide.Plugin.Class.Types where @@ -113,15 +112,15 @@ instance NFData InstanceBindLensResult where type instance RuleResult GetInstanceBindLens = InstanceBindLensResult data Log - = LogImplementedMethods Class [T.Text] + = LogImplementedMethods DynFlags Class ClassMinimalDef | LogShake Shake.Log instance Pretty Log where pretty = \case - LogImplementedMethods cls methods -> - pretty ("Detected implemented methods for class" :: String) + LogImplementedMethods dflags cls methods -> + pretty ("The following methods are missing" :: String) <+> pretty (show (getOccString cls) <> ":") -- 'show' is used here to add quotes around the class name - <+> pretty methods + <+> pretty (showSDoc dflags $ ppr methods) LogShake log -> pretty log data BindInfo = BindInfo @@ -134,7 +133,11 @@ data BindInfo = BindInfo getInstanceBindLensRule :: Recorder (WithPriority Log) -> Rules () getInstanceBindLensRule recorder = do defineNoDiagnostics (cmapWithPrio LogShake recorder) $ \GetInstanceBindLens nfp -> runMaybeT $ do +#if MIN_VERSION_ghc(9,9,0) + tmr@(tmrRenamed -> (hs_tyclds -> tycls, _, _, _, _)) <- useMT TypeCheck nfp +#else tmr@(tmrRenamed -> (hs_tyclds -> tycls, _, _, _)) <- useMT TypeCheck nfp +#endif (InstanceBindTypeSigsResult allBinds) <- useMT GetInstanceBindTypeSigs nfp let -- declared instance methods without signatures @@ -173,7 +176,11 @@ getInstanceBindLensRule recorder = do getBindSpanWithoutSig :: ClsInstDecl GhcRn -> [BindInfo] getBindSpanWithoutSig ClsInstDecl{..} = - let bindNames = mapMaybe go (bagToList cid_binds) + let bindNames = mapMaybe go $ +#if !MIN_VERSION_ghc(9,11,0) + bagToList +#endif + cid_binds go (L l bind) = case bind of FunBind{..} -- `Generated` tagged for Template Haskell, @@ -190,7 +197,6 @@ getInstanceBindLensRule recorder = do (locA l) -- bindSpan (locA l') -- bindNameSpan in toBindInfo <$> filter (\(L _ name) -> unLoc name `notElem` sigNames) bindNames - getBindSpanWithoutSig _ = [] -- Get bind definition range with its rendered signature text getRangeWithSig :: Maybe (InstanceBindTypeSig, SrcSpan) -> IO (Maybe (Range, Int, Name, Type)) @@ -208,12 +214,21 @@ getInstanceBindTypeSigsRule recorder = do (hscEnv -> hsc) <- useMT GhcSession nfp let binds = collectHsBindsBinders $ tcg_binds gblEnv (_, maybe [] catMaybes -> instanceBinds) <- liftIO $ - initTcWithGbl hsc gblEnv ghostSpan $ traverse bindToSig binds + initTcWithGbl hsc gblEnv ghostSpan +#if MIN_VERSION_ghc(9,7,0) + $ liftZonkM +#endif + $ traverse bindToSig binds pure $ InstanceBindTypeSigsResult instanceBinds where bindToSig id = do let name = idName id whenMaybe (isBindingName name) $ do env <- tcInitTidyEnv - let (_, ty) = tidyOpenType env (idType id) +#if MIN_VERSION_ghc(9,11,0) + let ty = +#else + let (_, ty) = +#endif + tidyOpenType env (idType id) pure $ InstanceBindTypeSig name ty diff --git a/plugins/hls-class-plugin/src/Ide/Plugin/Class/Utils.hs b/plugins/hls-class-plugin/src/Ide/Plugin/Class/Utils.hs index 129251ffe5..e73344c341 100644 --- a/plugins/hls-class-plugin/src/Ide/Plugin/Class/Utils.hs +++ b/plugins/hls-class-plugin/src/Ide/Plugin/Class/Utils.hs @@ -64,7 +64,7 @@ insertPragmaIfNotPresent :: (MonadIO m) insertPragmaIfNotPresent state nfp pragma = do (hscEnv -> hsc_dflags -> sessionDynFlags, _) <- runActionE "classplugin.insertPragmaIfNotPresent.GhcSession" state $ useWithStaleE GhcSession nfp - (_, fileContents) <- liftIO $ runAction "classplugin.insertPragmaIfNotPresent.GetFileContents" state + fileContents <- liftIO $ runAction "classplugin.insertPragmaIfNotPresent.GetFileContents" state $ getFileContents nfp (pm, _) <- runActionE "classplugin.insertPragmaIfNotPresent.GetParsedModuleWithComments" state $ useWithStaleE GetParsedModuleWithComments nfp diff --git a/plugins/hls-class-plugin/test/Main.hs b/plugins/hls-class-plugin/test/Main.hs index 7274381544..7f1feddc11 100644 --- a/plugins/hls-class-plugin/test/Main.hs +++ b/plugins/hls-class-plugin/test/Main.hs @@ -1,22 +1,18 @@ {-# LANGUAGE LambdaCase #-} -{-# LANGUAGE OverloadedLabels #-} {-# LANGUAGE OverloadedLists #-} {-# LANGUAGE OverloadedStrings #-} -{-# LANGUAGE TypeOperators #-} -{-# OPTIONS_GHC -Wall #-} -{-# OPTIONS_GHC -Wno-incomplete-uni-patterns #-} module Main ( main ) where +import Control.Exception (catch) import Control.Lens (Prism', prism', view, (^.), (^..), (^?)) import Control.Monad (void) +import Data.Foldable (find) import Data.Maybe -import Data.Row ((.==)) import qualified Data.Text as T -import Development.IDE.Core.Compile (sourceTypecheck) import qualified Ide.Plugin.Class as Class import qualified Language.LSP.Protocol.Lens as L import Language.LSP.Protocol.Message @@ -47,35 +43,36 @@ codeActionTests = testGroup , "Add placeholders for all missing methods" , "Add placeholders for all missing methods with signature(s)" ] - , goldenWithClass "Creates a placeholder for '=='" "T1" "eq" $ \(eqAction:_) -> do - executeCodeAction eqAction - , goldenWithClass "Creates a placeholder for '/='" "T1" "ne" $ \(_:_:neAction:_) -> do - executeCodeAction neAction - , goldenWithClass "Creates a placeholder for both '==' and '/='" "T1" "all" $ \(_:_:_:_:allMethodsAction:_) -> do - executeCodeAction allMethodsAction - , goldenWithClass "Creates a placeholder for 'fmap'" "T2" "fmap" $ \(_:_:_:_:_:_:fmapAction:_) -> do - executeCodeAction fmapAction - , goldenWithClass "Creates a placeholder for multiple methods 1" "T3" "1" $ \(mmAction:_) -> do - executeCodeAction mmAction - , goldenWithClass "Creates a placeholder for multiple methods 2" "T3" "2" $ \(_:_:mmAction:_) -> do - executeCodeAction mmAction - , goldenWithClass "Creates a placeholder for a method starting with '_'" "T4" "" $ \(_fAction:_) -> do - executeCodeAction _fAction - , goldenWithClass "Creates a placeholder for '==' with extra lines" "T5" "" $ \(eqAction:_) -> do - executeCodeAction eqAction - , goldenWithClass "Creates a placeholder for only the unimplemented methods of multiple methods" "T6" "1" $ \(gAction:_) -> do - executeCodeAction gAction - , goldenWithClass "Creates a placeholder for other two methods" "T6" "2" $ \(_:_:ghAction:_) -> do - executeCodeAction ghAction - , onlyRunForGhcVersions [GHC92, GHC94] "Only ghc-9.2+ enabled GHC2021 implicitly" $ - goldenWithClass "Don't insert pragma with GHC2021" "InsertWithGHC2021Enabled" "" $ \(_:eqWithSig:_) -> do - executeCodeAction eqWithSig - , goldenWithClass "Insert pragma if not exist" "InsertWithoutPragma" "" $ \(_:eqWithSig:_) -> do - executeCodeAction eqWithSig - , goldenWithClass "Don't insert pragma if exist" "InsertWithPragma" "" $ \(_:eqWithSig:_) -> do - executeCodeAction eqWithSig - , goldenWithClass "Only insert pragma once" "InsertPragmaOnce" "" $ \(_:multi:_) -> do - executeCodeAction multi + , goldenWithClass "Creates a placeholder for '=='" "T1" "eq" $ + getActionByTitle "Add placeholders for '=='" + , goldenWithClass "Creates a placeholder for '/='" "T1" "ne" $ + getActionByTitle "Add placeholders for '/='" + , goldenWithClass "Creates a placeholder for both '==' and '/='" "T1" "all" $ + getActionByTitle "Add placeholders for all missing methods" + , goldenWithClass "Creates a placeholder for 'fmap'" "T2" "fmap" $ + getActionByTitle "Add placeholders for 'fmap'" + , goldenWithClass "Creates a placeholder for multiple methods 1" "T3" "1" $ + getActionByTitle "Add placeholders for 'f','g'" + , goldenWithClass "Creates a placeholder for multiple methods 2" "T3" "2" $ + getActionByTitle "Add placeholders for 'g','h'" + , goldenWithClass "Creates a placeholder for a method starting with '_'" "T4" "" $ + getActionByTitle "Add placeholders for '_f'" + , goldenWithClass "Creates a placeholder for '==' with extra lines" "T5" "" $ + getActionByTitle "Add placeholders for '=='" + , goldenWithClass "Creates a placeholder for only the unimplemented methods of multiple methods" "T6" "1" $ + getActionByTitle "Add placeholders for 'g'" + , goldenWithClass "Creates a placeholder for other two methods" "T6" "2" $ + getActionByTitle "Add placeholders for 'g','h'" + , goldenWithClass "Creates a placeholder when all top-level decls are indented" "T7" "" $ + getActionByTitle "Add placeholders for 'g','h','i'" + , goldenWithClass "Don't insert pragma with GHC2021" "InsertWithGHC2021Enabled" "" $ + getActionByTitle "Add placeholders for '==' with signature(s)" + , goldenWithClass "Insert pragma if not exist" "InsertWithoutPragma" "" $ + getActionByTitle "Add placeholders for '==' with signature(s)" + , goldenWithClass "Don't insert pragma if exist" "InsertWithPragma" "" $ + getActionByTitle "Add placeholders for '==' with signature(s)" + , goldenWithClass "Only insert pragma once" "InsertPragmaOnce" "" $ + getActionByTitle "Add placeholders for 'pure','<*>' with signature(s)" , expectCodeActionsAvailable "No code action available when minimal requirements meet" "MinimalDefinitionMeet" [] , expectCodeActionsAvailable "Add placeholders for all missing methods is unavailable when all methods are required" "AllMethodsRequired" [ "Add placeholders for 'f','g'" @@ -88,7 +85,7 @@ codeActionTests = testGroup -- Change the doc to ensure the version is not 0 changeDoc doc - [ TextDocumentContentChangeEvent . InR . (.==) #text $ + [ TextDocumentContentChangeEvent . InR . TextDocumentContentChangeWholeDocument $ T.unlines ["module Version where", "data A a = A a", "instance Functor A where"] ] ver2 <- (^. L.version) <$> getVersionedDoc doc @@ -99,9 +96,8 @@ codeActionTests = testGroup action <- head . concatMap (^.. _CACodeAction) <$> getAllCodeActions doc executeCodeAction action _ <- waitForDiagnostics - -- TODO: uncomment this after lsp-test fixed - -- ver3 <- (^.J.version) <$> getVersionedDoc doc - -- liftIO $ ver3 @?= Just 3 + ver3 <- (^. L.version) <$> getVersionedDoc doc + liftIO $ ver3 @?= 2 pure mempty ] @@ -122,12 +118,22 @@ codeLensTests = testGroup doc <- openDoc "TH.hs" "haskell" lens <- getAndResolveCodeLenses doc liftIO $ length lens @?= 0 + , testCase "Do not construct error action!, Ticket3942one" $ do + runSessionWithServer def classPlugin testDataDir $ do + doc <- openDoc "Ticket3942one.hs" "haskell" + _ <- waitForDiagnosticsFrom doc + lens <- getAllCodeActions doc + -- should switch to `liftIO $ length lens @?= 2, when Ticket3942 is entirely fixed` + -- current fix is just to make sure the code does not throw an exception that would mess up + -- the client UI. + liftIO $ length lens > 0 @?= True + `catch` \(e :: SessionException) -> do + liftIO $ assertFailure $ "classPluginTestError: "++ show e , goldenCodeLens "Apply code lens" "CodeLensSimple" 1 , goldenCodeLens "Apply code lens for local class" "LocalClassDefine" 0 , goldenCodeLens "Apply code lens on the same line" "Inline" 0 , goldenCodeLens "Don't insert pragma while existing" "CodeLensWithPragma" 0 - , onlyRunForGhcVersions [GHC92, GHC94] "Only ghc-9.2+ enabled GHC2021 implicitly" $ - goldenCodeLens "Don't insert pragma while GHC2021 enabled" "CodeLensWithGHC2021" 0 + , goldenCodeLens "Don't insert pragma while GHC2021 enabled" "CodeLensWithGHC2021" 0 , goldenCodeLens "Qualified name" "Qualified" 0 , goldenCodeLens "Type family" "TypeFamily" 0 , testCase "keep stale lens" $ do @@ -152,20 +158,26 @@ goldenCodeLens title path idx = executeCommand $ fromJust $ (lens !! idx) ^. L.command void $ skipManyTill anyMessage (message SMethod_WorkspaceApplyEdit) -goldenWithClass ::TestName -> FilePath -> FilePath -> ([CodeAction] -> Session ()) -> TestTree -goldenWithClass title path desc act = +goldenWithClass ::TestName -> FilePath -> FilePath -> ([CodeAction] -> Session CodeAction) -> TestTree +goldenWithClass title path desc findAction = goldenWithHaskellDoc def classPlugin title testDataDir path (desc <.> "expected") "hs" $ \doc -> do - _ <- waitForDiagnosticsFromSource doc (T.unpack sourceTypecheck) + _ <- waitForDiagnosticsFrom doc actions <- concatMap (^.. _CACodeAction) <$> getAllCodeActions doc - act actions + action <- findAction actions + executeCodeAction action void $ skipManyTill anyMessage (getDocumentEdit doc) +getActionByTitle :: T.Text -> [CodeAction] -> Session CodeAction +getActionByTitle title actions = case find (\a -> a ^. L.title == title) actions of + Just a -> pure a + Nothing -> liftIO $ assertFailure $ "Action " <> show title <> " not found in " <> show [a ^. L.title | a <- actions] + expectCodeActionsAvailable :: TestName -> FilePath -> [T.Text] -> TestTree expectCodeActionsAvailable title path actionTitles = testCase title $ do runSessionWithServer def classPlugin testDataDir $ do doc <- openDoc (path <.> "hs") "haskell" - _ <- waitForDiagnosticsFromSource doc (T.unpack sourceTypecheck) + _ <- waitForDiagnosticsFrom doc caResults <- getAllCodeActions doc liftIO $ map (^? _CACodeAction . L.title) caResults @?= expectedActions @@ -173,4 +185,4 @@ expectCodeActionsAvailable title path actionTitles = expectedActions = Just <$> actionTitles testDataDir :: FilePath -testDataDir = "test" "testdata" +testDataDir = "plugins" "hls-class-plugin" "test" "testdata" diff --git a/plugins/hls-class-plugin/test/testdata/T5.expected.hs b/plugins/hls-class-plugin/test/testdata/T5.expected.hs index 6c26425f34..fcc51c0787 100644 --- a/plugins/hls-class-plugin/test/testdata/T5.expected.hs +++ b/plugins/hls-class-plugin/test/testdata/T5.expected.hs @@ -1,4 +1,4 @@ -module T1 where +module T5 where data X = X diff --git a/plugins/hls-class-plugin/test/testdata/T5.hs b/plugins/hls-class-plugin/test/testdata/T5.hs index e7dc1d4da3..d33dd8b17c 100644 --- a/plugins/hls-class-plugin/test/testdata/T5.hs +++ b/plugins/hls-class-plugin/test/testdata/T5.hs @@ -1,4 +1,4 @@ -module T1 where +module T5 where data X = X diff --git a/plugins/hls-class-plugin/test/testdata/T7.expected.hs b/plugins/hls-class-plugin/test/testdata/T7.expected.hs new file mode 100644 index 0000000000..5bf716c900 --- /dev/null +++ b/plugins/hls-class-plugin/test/testdata/T7.expected.hs @@ -0,0 +1,20 @@ +module T7 where + + data X = X + + class Test a where + f :: a -> a + g :: a + h :: a -> a + i :: a + + instance Test X where + f X = X + g = _ + h = _ + i = _ + + + + + whiteSpaceBeforeAndIndentationOfThisShouldBePreserved = () diff --git a/plugins/hls-class-plugin/test/testdata/T7.hs b/plugins/hls-class-plugin/test/testdata/T7.hs new file mode 100644 index 0000000000..2f9a1b67f6 --- /dev/null +++ b/plugins/hls-class-plugin/test/testdata/T7.hs @@ -0,0 +1,17 @@ +module T7 where + + data X = X + + class Test a where + f :: a -> a + g :: a + h :: a -> a + i :: a + + instance Test X where + f X = X + + + + + whiteSpaceBeforeAndIndentationOfThisShouldBePreserved = () diff --git a/plugins/hls-class-plugin/test/testdata/Ticket3942one.hs b/plugins/hls-class-plugin/test/testdata/Ticket3942one.hs new file mode 100644 index 0000000000..d620fc2ebb --- /dev/null +++ b/plugins/hls-class-plugin/test/testdata/Ticket3942one.hs @@ -0,0 +1,13 @@ +{-# LANGUAGE DeriveAnyClass #-} + +module Ticket3942one where + +class C a where + foo :: a -> Int + +newtype Foo = MkFoo Int deriving (C) +instance Show Foo where + + +main :: IO () +main = return () diff --git a/plugins/hls-code-range-plugin/LICENSE b/plugins/hls-code-range-plugin/LICENSE deleted file mode 100644 index 261eeb9e9f..0000000000 --- a/plugins/hls-code-range-plugin/LICENSE +++ /dev/null @@ -1,201 +0,0 @@ - Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ - - TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - - 1. Definitions. - - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. - - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. - - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. - - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. - - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. - - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. - - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). - - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. - - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution." - - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. - - 2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. - - 3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. - - 4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: - - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and - - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and - - (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. - - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. - - 5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. - - 6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. - - 7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. - - 8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. - - 9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. - - END OF TERMS AND CONDITIONS - - APPENDIX: How to apply the Apache License to your work. - - To apply the Apache License to your work, attach the following - boilerplate notice, with the fields enclosed by brackets "[]" - replaced with your own identifying information. (Don't include - the brackets!) The text should be enclosed in the appropriate - comment syntax for the file format. We also recommend that a - file or class name and description of purpose be included on the - same "printed page" as the copyright notice for easier - identification within third-party archives. - - Copyright [yyyy] [name of copyright owner] - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. diff --git a/plugins/hls-code-range-plugin/hls-code-range-plugin.cabal b/plugins/hls-code-range-plugin/hls-code-range-plugin.cabal deleted file mode 100644 index 7b226668b5..0000000000 --- a/plugins/hls-code-range-plugin/hls-code-range-plugin.cabal +++ /dev/null @@ -1,75 +0,0 @@ -cabal-version: 2.4 -name: hls-code-range-plugin -version: 2.4.0.0 -synopsis: - HLS Plugin to support smart selection range and Folding range - -description: - Please see the README on GitHub at - -license: Apache-2.0 -license-file: LICENSE -author: kokobd -maintainer: kokobd - -category: Development -build-type: Simple -extra-source-files: - LICENSE - test/testdata/**/*.hs - -source-repository head - type: git - location: https://github.com/haskell/haskell-language-server.git - -library - exposed-modules: - Ide.Plugin.CodeRange - Ide.Plugin.CodeRange.Rules - other-modules: - Ide.Plugin.CodeRange.ASTPreProcess - ghc-options: -Wall - hs-source-dirs: src - default-language: Haskell2010 - build-depends: - , aeson - , base >=4.12 && <5 - , containers - , deepseq - , extra - , ghcide == 2.4.0.0 - , hashable - , hls-plugin-api == 2.4.0.0 - , lens - , lsp - , mtl - , semigroupoids - , text - , transformers - , vector - -test-suite tests - type: exitcode-stdio-1.0 - default-language: Haskell2010 - hs-source-dirs: test - main-is: Main.hs - other-modules: - Ide.Plugin.CodeRangeTest - Ide.Plugin.CodeRange.RulesTest - ghc-options: -threaded -rtsopts -with-rtsopts=-N - build-depends: - , base - , bytestring - , containers - , filepath - , ghcide == 2.4.0.0 - , hls-code-range-plugin - , hls-plugin-api - , hls-test-utils == 2.4.0.0 - , lens - , lsp - , lsp-test - , tasty-hunit - , text - , transformers - , vector diff --git a/plugins/hls-code-range-plugin/src/Ide/Plugin/CodeRange.hs b/plugins/hls-code-range-plugin/src/Ide/Plugin/CodeRange.hs index e5c1123a13..52bcc2226b 100644 --- a/plugins/hls-code-range-plugin/src/Ide/Plugin/CodeRange.hs +++ b/plugins/hls-code-range-plugin/src/Ide/Plugin/CodeRange.hs @@ -1,10 +1,6 @@ -{-# LANGUAGE DataKinds #-} -{-# LANGUAGE ExistentialQuantification #-} -{-# LANGUAGE LambdaCase #-} -{-# LANGUAGE OverloadedStrings #-} -{-# LANGUAGE RecordWildCards #-} -{-# LANGUAGE ScopedTypeVariables #-} -{-# LANGUAGE TypeOperators #-} +{-# LANGUAGE DataKinds #-} +{-# LANGUAGE OverloadedStrings #-} +{-# LANGUAGE RecordWildCards #-} module Ide.Plugin.CodeRange ( descriptor , Log @@ -57,17 +53,17 @@ import Language.LSP.Protocol.Types (FoldingRange (..), import Prelude hiding (log, span) descriptor :: Recorder (WithPriority Log) -> PluginId -> PluginDescriptor IdeState -descriptor recorder plId = (defaultPluginDescriptor plId) +descriptor recorder plId = (defaultPluginDescriptor plId "Provides selection and folding ranges for Haskell") { pluginHandlers = mkPluginHandler SMethod_TextDocumentSelectionRange (selectionRangeHandler recorder) <> mkPluginHandler SMethod_TextDocumentFoldingRange (foldingRangeHandler recorder) , pluginRules = codeRangeRule (cmapWithPrio LogRules recorder) } -data Log = LogRules Rules.Log +newtype Log = LogRules Rules.Log instance Pretty Log where - pretty log = case log of - LogRules codeRangeLog -> pretty codeRangeLog + pretty (LogRules codeRangeLog) = pretty codeRangeLog + foldingRangeHandler :: Recorder (WithPriority Log) -> PluginMethodHandler IdeState 'Method_TextDocumentFoldingRange foldingRangeHandler _ ide _ FoldingRangeParams{..} = diff --git a/plugins/hls-code-range-plugin/src/Ide/Plugin/CodeRange/ASTPreProcess.hs b/plugins/hls-code-range-plugin/src/Ide/Plugin/CodeRange/ASTPreProcess.hs index d2ee4c1c02..6fa799b8d5 100644 --- a/plugins/hls-code-range-plugin/src/Ide/Plugin/CodeRange/ASTPreProcess.hs +++ b/plugins/hls-code-range-plugin/src/Ide/Plugin/CodeRange/ASTPreProcess.hs @@ -1,5 +1,4 @@ -{-# LANGUAGE OverloadedStrings #-} -{-# LANGUAGE ScopedTypeVariables #-} +{-# LANGUAGE OverloadedStrings #-} module Ide.Plugin.CodeRange.ASTPreProcess ( preProcessAST @@ -175,7 +174,7 @@ isIdentADef outerSpan (span, detail) = && isDef where isDef :: Bool - isDef = any isContextInfoDef . toList . identInfo $ detail + isDef = any isContextInfoDef $ identInfo detail -- Determines if the 'ContextInfo' represents a variable/function definition isContextInfoDef :: ContextInfo -> Bool diff --git a/plugins/hls-code-range-plugin/src/Ide/Plugin/CodeRange/Rules.hs b/plugins/hls-code-range-plugin/src/Ide/Plugin/CodeRange/Rules.hs index ffcbc75e7d..86d5923011 100644 --- a/plugins/hls-code-range-plugin/src/Ide/Plugin/CodeRange/Rules.hs +++ b/plugins/hls-code-range-plugin/src/Ide/Plugin/CodeRange/Rules.hs @@ -1,12 +1,7 @@ -{-# LANGUAGE DeriveAnyClass #-} -{-# LANGUAGE DeriveGeneric #-} -{-# LANGUAGE ExistentialQuantification #-} -{-# LANGUAGE InstanceSigs #-} -{-# LANGUAGE NamedFieldPuns #-} -{-# LANGUAGE OverloadedStrings #-} -{-# LANGUAGE ScopedTypeVariables #-} -{-# LANGUAGE TemplateHaskell #-} -{-# LANGUAGE TypeFamilies #-} +{-# LANGUAGE DeriveAnyClass #-} +{-# LANGUAGE OverloadedStrings #-} +{-# LANGUAGE TemplateHaskell #-} +{-# LANGUAGE TypeFamilies #-} module Ide.Plugin.CodeRange.Rules ( CodeRange (..) @@ -34,7 +29,6 @@ import Control.Monad.Trans.Maybe (MaybeT (MaybeT), maybeToExceptT) import Control.Monad.Trans.Writer.CPS import Data.Coerce (coerce) -import Data.Data (Typeable) import Data.Foldable (traverse_) import Data.Function (on, (&)) import Data.Hashable @@ -163,7 +157,7 @@ simplify r = withChildrenSimplified = r { _codeRange_children = simplify <$> _codeRange_children r } data GetCodeRange = GetCodeRange - deriving (Eq, Show, Typeable, Generic) + deriving (Eq, Show, Generic) instance Hashable GetCodeRange instance NFData GetCodeRange diff --git a/plugins/hls-code-range-plugin/test/Ide/Plugin/CodeRange/RulesTest.hs b/plugins/hls-code-range-plugin/test/Ide/Plugin/CodeRange/RulesTest.hs index 473d5b7f77..4dee5e039c 100644 --- a/plugins/hls-code-range-plugin/test/Ide/Plugin/CodeRange/RulesTest.hs +++ b/plugins/hls-code-range-plugin/test/Ide/Plugin/CodeRange/RulesTest.hs @@ -3,11 +3,10 @@ module Ide.Plugin.CodeRange.RulesTest (testTree) where import Control.Monad.Trans.Writer.CPS -import Data.Bifunctor (Bifunctor (first, second)) +import Data.Bifunctor (Bifunctor (second)) import qualified Data.Vector as V import Ide.Plugin.CodeRange.Rules import Test.Hls -import Test.Tasty.HUnit testTree :: TestTree testTree = @@ -78,3 +77,4 @@ instance Eq LogEq where LogEq LogNoAST == LogEq LogNoAST = True LogEq (LogFoundInterleaving left right) == LogEq (LogFoundInterleaving left' right') = left == left' && right == right' + LogEq _ == LogEq _ = False diff --git a/plugins/hls-code-range-plugin/test/Ide/Plugin/CodeRangeTest.hs b/plugins/hls-code-range-plugin/test/Ide/Plugin/CodeRangeTest.hs index 627dc28493..4db8e41d7b 100644 --- a/plugins/hls-code-range-plugin/test/Ide/Plugin/CodeRangeTest.hs +++ b/plugins/hls-code-range-plugin/test/Ide/Plugin/CodeRangeTest.hs @@ -6,7 +6,6 @@ import qualified Data.Vector as V import Ide.Plugin.CodeRange import Ide.Plugin.CodeRange.Rules import Test.Hls -import Test.Tasty.HUnit testTree :: TestTree testTree = diff --git a/plugins/hls-code-range-plugin/test/Main.hs b/plugins/hls-code-range-plugin/test/Main.hs index aebc68ca7e..da32deed51 100644 --- a/plugins/hls-code-range-plugin/test/Main.hs +++ b/plugins/hls-code-range-plugin/test/Main.hs @@ -6,15 +6,10 @@ import Control.Lens hiding (List, (<.>)) import Data.ByteString.Lazy (ByteString) import qualified Data.ByteString.Lazy.Char8 as LBSChar8 import Data.String (fromString) -import Ide.Logger (Priority (Debug), - Recorder (Recorder), - WithPriority (WithPriority), - makeDefaultStderrRecorder, - pretty) import Ide.Plugin.CodeRange (Log, descriptor) import qualified Ide.Plugin.CodeRange.RulesTest import qualified Ide.Plugin.CodeRangeTest -import Language.LSP.Protocol.Lens +import Language.LSP.Protocol.Lens (result) import Language.LSP.Protocol.Message import Language.LSP.Protocol.Types import System.FilePath ((<.>), ()) @@ -40,7 +35,7 @@ main = do ] selectionRangeGoldenTest :: TestName -> [(UInt, UInt)] -> TestTree -selectionRangeGoldenTest testName positions = goldenGitDiff testName (testDataDir testName <.> "golden" <.> "txt") $ do +selectionRangeGoldenTest testName positions = goldenGitDiff testName (testDataDir testName <.> "golden" <.> "txt" <> ghcSuffix) $ do res <- runSessionWithServer def plugin testDataDir $ do doc <- openDoc (testName <.> "hs") "haskell" resp <- request SMethod_TextDocumentSelectionRange $ SelectionRangeParams Nothing Nothing doc @@ -48,12 +43,12 @@ selectionRangeGoldenTest testName positions = goldenGitDiff testName (testDataDi let res = resp ^. result pure $ fmap (showSelectionRangesForTest . absorbNull) res case res of - Left (ResponseError (InL LSPErrorCodes_RequestFailed) _ _) -> pure "" + Left (TResponseError (InL LSPErrorCodes_RequestFailed) _ _) -> pure "" Left err -> assertFailure (show err) Right golden -> pure golden where testDataDir :: FilePath - testDataDir = "test" "testdata" "selection-range" + testDataDir = "plugins" "hls-code-range-plugin" "test" "testdata" "selection-range" showSelectionRangesForTest :: [SelectionRange] -> ByteString showSelectionRangesForTest selectionRanges = LBSChar8.intercalate "\n" $ fmap showSelectionRangeForTest selectionRanges @@ -70,7 +65,7 @@ selectionRangeGoldenTest testName positions = goldenGitDiff testName (testDataDi showLBS = fromString . show foldingRangeGoldenTest :: TestName -> TestTree -foldingRangeGoldenTest testName = goldenGitDiff testName (testDataDir testName <.> "golden" <.> "txt") $ do +foldingRangeGoldenTest testName = goldenGitDiff testName (testDataDir testName <.> "golden" <.> "txt" <> ghcSuffix) $ do res <- runSessionWithServer def plugin testDataDir $ do doc <- openDoc (testName <.> "hs") "haskell" resp <- request SMethod_TextDocumentFoldingRange $ FoldingRangeParams Nothing Nothing doc @@ -83,13 +78,19 @@ foldingRangeGoldenTest testName = goldenGitDiff testName (testDataDir testN where testDataDir :: FilePath - testDataDir = "test" "testdata" "folding-range" + testDataDir = "plugins" "hls-code-range-plugin" "test" "testdata" "folding-range" showFoldingRangesForTest :: [FoldingRange] -> ByteString showFoldingRangesForTest foldingRanges = (LBSChar8.intercalate "\n" $ fmap showFoldingRangeForTest foldingRanges) `LBSChar8.snoc` '\n' showFoldingRangeForTest :: FoldingRange -> ByteString - showFoldingRangeForTest f@(FoldingRange sl (Just sc) el (Just ec) (Just frk) _) = "((" <> showLBS sl <>", "<> showLBS sc <> ")" <> " : " <> "(" <> showLBS el <>", "<> showLBS ec<> ")) : " <> showFRK frk + showFoldingRangeForTest (FoldingRange sl (Just sc) el (Just ec) (Just frk) _) = + "((" <> showLBS sl <> ", " <> showLBS sc <> ") : (" <> showLBS el <> ", " <> showLBS ec <> ")) : " <> showFRK frk + showFoldingRangeForTest fr = + "unexpected FoldingRange: " <> fromString (show fr) showLBS = fromString . show showFRK = fromString . show + +ghcSuffix :: String +ghcSuffix = if ghcVersion >= GHC910 then ".ghc910" else "" diff --git a/plugins/hls-code-range-plugin/test/testdata/folding-range/Function.golden.txt.ghc910 b/plugins/hls-code-range-plugin/test/testdata/folding-range/Function.golden.txt.ghc910 new file mode 100644 index 0000000000..937654b5b7 --- /dev/null +++ b/plugins/hls-code-range-plugin/test/testdata/folding-range/Function.golden.txt.ghc910 @@ -0,0 +1,42 @@ +((2, 7) : (2, 15)) : FoldingRangeKind_Region +((2, 16) : (2, 22)) : FoldingRangeKind_Region +((4, 0) : (7, 21)) : FoldingRangeKind_Region +((4, 0) : (4, 25)) : FoldingRangeKind_Region +((4, 0) : (4, 6)) : FoldingRangeKind_Region +((4, 10) : (4, 25)) : FoldingRangeKind_Region +((4, 10) : (4, 17)) : FoldingRangeKind_Region +((4, 21) : (4, 25)) : FoldingRangeKind_Region +((5, 0) : (7, 21)) : FoldingRangeKind_Region +((5, 0) : (5, 6)) : FoldingRangeKind_Region +((5, 7) : (5, 8)) : FoldingRangeKind_Region +((5, 9) : (7, 21)) : FoldingRangeKind_Region +((5, 11) : (7, 21)) : FoldingRangeKind_Region +((5, 14) : (5, 28)) : FoldingRangeKind_Region +((5, 14) : (5, 23)) : FoldingRangeKind_Region +((5, 14) : (5, 15)) : FoldingRangeKind_Region +((5, 16) : (5, 21)) : FoldingRangeKind_Region +((5, 22) : (5, 23)) : FoldingRangeKind_Region +((5, 24) : (5, 26)) : FoldingRangeKind_Region +((5, 27) : (5, 28)) : FoldingRangeKind_Region +((6, 16) : (6, 20)) : FoldingRangeKind_Region +((7, 16) : (7, 21)) : FoldingRangeKind_Region +((9, 0) : (12, 20)) : FoldingRangeKind_Region +((9, 0) : (9, 24)) : FoldingRangeKind_Region +((9, 0) : (9, 5)) : FoldingRangeKind_Region +((9, 9) : (9, 24)) : FoldingRangeKind_Region +((9, 9) : (9, 16)) : FoldingRangeKind_Region +((9, 20) : (9, 24)) : FoldingRangeKind_Region +((10, 0) : (12, 20)) : FoldingRangeKind_Region +((10, 0) : (10, 5)) : FoldingRangeKind_Region +((10, 6) : (10, 7)) : FoldingRangeKind_Region +((10, 8) : (12, 20)) : FoldingRangeKind_Region +((10, 10) : (12, 20)) : FoldingRangeKind_Region +((10, 13) : (10, 27)) : FoldingRangeKind_Region +((10, 13) : (10, 22)) : FoldingRangeKind_Region +((10, 13) : (10, 14)) : FoldingRangeKind_Region +((10, 15) : (10, 20)) : FoldingRangeKind_Region +((10, 21) : (10, 22)) : FoldingRangeKind_Region +((10, 23) : (10, 25)) : FoldingRangeKind_Region +((10, 26) : (10, 27)) : FoldingRangeKind_Region +((11, 16) : (11, 21)) : FoldingRangeKind_Region +((12, 16) : (12, 20)) : FoldingRangeKind_Region diff --git a/plugins/hls-code-range-plugin/test/testdata/selection-range/Empty.golden.txt.ghc910 b/plugins/hls-code-range-plugin/test/testdata/selection-range/Empty.golden.txt.ghc910 new file mode 100644 index 0000000000..7689c89086 --- /dev/null +++ b/plugins/hls-code-range-plugin/test/testdata/selection-range/Empty.golden.txt.ghc910 @@ -0,0 +1 @@ +(1,5) (1,5) \ No newline at end of file diff --git a/plugins/hls-code-range-plugin/test/testdata/selection-range/Function.golden.txt.ghc910 b/plugins/hls-code-range-plugin/test/testdata/selection-range/Function.golden.txt.ghc910 new file mode 100644 index 0000000000..eb359fb12b --- /dev/null +++ b/plugins/hls-code-range-plugin/test/testdata/selection-range/Function.golden.txt.ghc910 @@ -0,0 +1,4 @@ +(5,16) (5,20) => (5,16) (5,40) => (5,14) (5,40) => (5,1) (11,20) => (4,1) (11,20) => (3,1) (11,20) => (1,8) (14,15) +(5,12) (5,13) => (5,1) (11,20) => (4,1) (11,20) => (3,1) (11,20) => (1,8) (14,15) +(4,1) (4,9) => (4,1) (4,29) => (4,1) (11,20) => (3,1) (11,20) => (1,8) (14,15) +(3,1) (3,9) => (3,1) (3,61) => (3,1) (11,20) => (1,8) (14,15) \ No newline at end of file diff --git a/plugins/hls-code-range-plugin/test/testdata/selection-range/Import.golden.txt.ghc910 b/plugins/hls-code-range-plugin/test/testdata/selection-range/Import.golden.txt.ghc910 new file mode 100644 index 0000000000..4011ddb913 --- /dev/null +++ b/plugins/hls-code-range-plugin/test/testdata/selection-range/Import.golden.txt.ghc910 @@ -0,0 +1,2 @@ +(4,33) (4,38) => (4,32) (4,47) => (4,1) (4,47) => (3,1) (4,47) => (1,8) (4,47) +(1,8) (1,22) => (1,8) (4,47) \ No newline at end of file diff --git a/plugins/hls-eval-plugin/LICENSE b/plugins/hls-eval-plugin/LICENSE deleted file mode 100644 index 261eeb9e9f..0000000000 --- a/plugins/hls-eval-plugin/LICENSE +++ /dev/null @@ -1,201 +0,0 @@ - Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ - - TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - - 1. Definitions. - - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. - - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. - - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. - - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. - - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. - - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. - - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). - - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. - - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution." - - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. - - 2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. - - 3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. - - 4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: - - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and - - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and - - (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. - - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. - - 5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. - - 6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. - - 7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. - - 8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. - - 9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. - - END OF TERMS AND CONDITIONS - - APPENDIX: How to apply the Apache License to your work. - - To apply the Apache License to your work, attach the following - boilerplate notice, with the fields enclosed by brackets "[]" - replaced with your own identifying information. (Don't include - the brackets!) The text should be enclosed in the appropriate - comment syntax for the file format. We also recommend that a - file or class name and description of purpose be included on the - same "printed page" as the copyright notice for easier - identification within third-party archives. - - Copyright [yyyy] [name of copyright owner] - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. diff --git a/plugins/hls-eval-plugin/README.md b/plugins/hls-eval-plugin/README.md index b1a50f0705..d2b39498cb 100644 --- a/plugins/hls-eval-plugin/README.md +++ b/plugins/hls-eval-plugin/README.md @@ -40,7 +40,7 @@ A test is composed by a sequence of contiguous lines, the result of their evalua "CDAB" ``` -You execute a test by clicking on the _Evaluate_ code lens that appears above it (or _Refresh_, if the test has been run previously). +You execute a test by clicking on the _Evaluate_ code lens that appears above it (or _Refresh_, if the test has been run previously). A code action is also provided. All tests in the same comment block are executed together. @@ -334,14 +334,7 @@ prop> \(l::[Int]) -> reverse (reverse l) == l ### Multiline Expressions -``` - >>> :{ - let - x = 1 - y = 2 - in x + y + multiline - :} -``` +Multiline expressions are not supported, see https://github.com/haskell/haskell-language-server/issues/1817 # Acknowledgments diff --git a/plugins/hls-eval-plugin/hls-eval-plugin.cabal b/plugins/hls-eval-plugin/hls-eval-plugin.cabal deleted file mode 100644 index 163681016b..0000000000 --- a/plugins/hls-eval-plugin/hls-eval-plugin.cabal +++ /dev/null @@ -1,119 +0,0 @@ -cabal-version: 2.4 -name: hls-eval-plugin -version: 2.4.0.0 -synopsis: Eval plugin for Haskell Language Server -description: - Please see the README on GitHub at - -category: Development -bug-reports: https://github.com/haskell/haskell-language-server/issues -license: Apache-2.0 -license-file: LICENSE -author: - https://github.com/haskell/haskell-language-server/contributors - -maintainer: - https://github.com/haskell/haskell-language-server/contributors - -build-type: Simple -extra-source-files: - LICENSE - README.md - test/cabal.project - test/testdata/info-util/*.cabal - test/testdata/info-util/*.hs - test/testdata/*.cabal - test/testdata/*.hs - test/testdata/*.lhs - test/testdata/*.yaml - -flag pedantic - description: Enable -Werror - default: False - manual: True - -source-repository head - type: git - location: https://github.com/haskell/haskell-language-server - -library - exposed-modules: - Ide.Plugin.Eval - Ide.Plugin.Eval.Types - - hs-source-dirs: src - other-modules: - Ide.Plugin.Eval.Code - Ide.Plugin.Eval.CodeLens - Ide.Plugin.Eval.Config - Ide.Plugin.Eval.GHC - Ide.Plugin.Eval.Parse.Comments - Ide.Plugin.Eval.Parse.Option - Ide.Plugin.Eval.Rules - Ide.Plugin.Eval.Util - - build-depends: - , aeson - , base >=4.12 && <5 - , bytestring - , containers - , data-default - , deepseq - , Diff ^>=0.4.0 - , directory - , dlist - , extra - , filepath - , ghc - , ghc-boot-th - , ghc-paths - , ghcide == 2.4.0.0 - , hashable - , hls-graph - , hls-plugin-api == 2.4.0.0 - , lens - , lsp - , lsp-types - , megaparsec >=9.0 - , mtl - , parser-combinators >=1.2 - , pretty-simple - , QuickCheck - , safe-exceptions - , text - , time - , transformers - , unliftio - , unordered-containers - - ghc-options: - -Wall -Wno-name-shadowing -Wno-unticked-promoted-constructors -fno-ignore-asserts - - if flag(pedantic) - ghc-options: -Werror - - default-language: Haskell2010 - default-extensions: - DataKinds - TypeOperators - -test-suite tests - type: exitcode-stdio-1.0 - default-language: Haskell2010 - hs-source-dirs: test - main-is: Main.hs - ghc-options: -threaded -rtsopts -with-rtsopts=-N -fno-ignore-asserts - build-depends: - , aeson - , base - , containers - , directory - , extra - , filepath - , hls-eval-plugin - , hls-plugin-api - , hls-test-utils == 2.4.0.0 - , lens - , lsp-types - , text - , row-types diff --git a/plugins/hls-eval-plugin/src/Ide/Plugin/Eval.hs b/plugins/hls-eval-plugin/src/Ide/Plugin/Eval.hs index 5084e9750f..30d43de005 100644 --- a/plugins/hls-eval-plugin/src/Ide/Plugin/Eval.hs +++ b/plugins/hls-eval-plugin/src/Ide/Plugin/Eval.hs @@ -1,5 +1,5 @@ {-# LANGUAGE DuplicateRecordFields #-} -{-# LANGUAGE ScopedTypeVariables #-} +{-# LANGUAGE OverloadedStrings #-} {-# OPTIONS_GHC -Wwarn #-} {-# LANGUAGE LambdaCase #-} @@ -8,16 +8,15 @@ Eval Plugin entry point. -} module Ide.Plugin.Eval ( descriptor, - Log(..) + Eval.Log(..) ) where import Development.IDE (IdeState) -import Ide.Logger (Pretty (pretty), Recorder, - WithPriority, cmapWithPrio) -import qualified Ide.Plugin.Eval.CodeLens as CL +import Ide.Logger (Recorder, WithPriority) import Ide.Plugin.Eval.Config +import qualified Ide.Plugin.Eval.Handlers as Handlers import Ide.Plugin.Eval.Rules (rules) -import qualified Ide.Plugin.Eval.Rules as EvalRules +import qualified Ide.Plugin.Eval.Types as Eval import Ide.Types (ConfigDescriptor (..), PluginDescriptor (..), PluginId, defaultConfigDescriptor, @@ -25,19 +24,16 @@ import Ide.Types (ConfigDescriptor (..), mkCustomConfig, mkPluginHandler) import Language.LSP.Protocol.Message -newtype Log = LogEvalRules EvalRules.Log deriving Show - -instance Pretty Log where - pretty = \case - LogEvalRules log -> pretty log - -- |Plugin descriptor -descriptor :: Recorder (WithPriority Log) -> PluginId -> PluginDescriptor IdeState +descriptor :: Recorder (WithPriority Eval.Log) -> PluginId -> PluginDescriptor IdeState descriptor recorder plId = - (defaultPluginDescriptor plId) - { pluginHandlers = mkPluginHandler SMethod_TextDocumentCodeLens CL.codeLens - , pluginCommands = [CL.evalCommand plId] - , pluginRules = rules (cmapWithPrio LogEvalRules recorder) + (defaultPluginDescriptor plId "Provides code action and lens to evaluate expressions in doctest comments") + { pluginHandlers = mconcat + [ mkPluginHandler SMethod_TextDocumentCodeAction (Handlers.codeAction recorder) + , mkPluginHandler SMethod_TextDocumentCodeLens (Handlers.codeLens recorder) + ] + , pluginCommands = [Handlers.evalCommand recorder plId] + , pluginRules = rules recorder , pluginConfigDescriptor = defaultConfigDescriptor { configCustomConfig = mkCustomConfig properties } diff --git a/plugins/hls-eval-plugin/src/Ide/Plugin/Eval/Code.hs b/plugins/hls-eval-plugin/src/Ide/Plugin/Eval/Code.hs index 846d8ce160..e8b7428b10 100644 --- a/plugins/hls-eval-plugin/src/Ide/Plugin/Eval/Code.hs +++ b/plugins/hls-eval-plugin/src/Ide/Plugin/Eval/Code.hs @@ -1,7 +1,7 @@ {-# LANGUAGE LambdaCase #-} {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE ViewPatterns #-} -{-# OPTIONS_GHC -Wwarn -fno-warn-orphans #-} +{-# OPTIONS_GHC -Wwarn #-} -- | Expression execution module Ide.Plugin.Eval.Code (Statement, testRanges, resultRange, propSetup, testCheck, asStatements,myExecStmt) where @@ -85,7 +85,7 @@ asStmts (Property t _ _) = myExecStmt :: String -> ExecOptions -> Ghc (Either String (Maybe String)) myExecStmt stmt opts = do (temp, purge) <- liftIO newTempFile - evalPrint <- head <$> runDecls ("evalPrint x = P.writeFile "<> show temp <> " (P.show x)") + evalPrint <- head <$> runDecls ("evalPrint x = P.writeFile " <> show temp <> " (P.show x)") modifySession $ \hsc -> hsc {hsc_IC = setInteractivePrintName (hsc_IC hsc) evalPrint} result <- execStmt stmt opts >>= \case ExecComplete (Left err) _ -> pure $ Left $ show err diff --git a/plugins/hls-eval-plugin/src/Ide/Plugin/Eval/Config.hs b/plugins/hls-eval-plugin/src/Ide/Plugin/Eval/Config.hs index 5a340f049a..4b789c37ee 100644 --- a/plugins/hls-eval-plugin/src/Ide/Plugin/Eval/Config.hs +++ b/plugins/hls-eval-plugin/src/Ide/Plugin/Eval/Config.hs @@ -1,4 +1,3 @@ -{-# LANGUAGE FlexibleContexts #-} {-# LANGUAGE OverloadedLabels #-} {-# LANGUAGE OverloadedStrings #-} module Ide.Plugin.Eval.Config diff --git a/plugins/hls-eval-plugin/src/Ide/Plugin/Eval/GHC.hs b/plugins/hls-eval-plugin/src/Ide/Plugin/Eval/GHC.hs index 68ea0a4050..f0b01fca92 100644 --- a/plugins/hls-eval-plugin/src/Ide/Plugin/Eval/GHC.hs +++ b/plugins/hls-eval-plugin/src/Ide/Plugin/Eval/GHC.hs @@ -1,7 +1,6 @@ -{-# LANGUAGE LambdaCase #-} -{-# LANGUAGE ScopedTypeVariables #-} -{-# LANGUAGE StandaloneDeriving #-} -{-# OPTIONS_GHC -fno-warn-unused-imports -Wno-orphans #-} +{-# LANGUAGE CPP #-} +{-# LANGUAGE LambdaCase #-} +{-# OPTIONS_GHC -Wno-orphans #-} -- |GHC API utilities module Ide.Plugin.Eval.GHC ( @@ -11,6 +10,7 @@ module Ide.Plugin.Eval.GHC ( addPackages, modifyFlags, showDynFlags, + setSessionAndInteractiveDynFlags, ) where import Data.List (isPrefixOf) @@ -25,6 +25,11 @@ import Development.IDE.GHC.Util (printOutputable) import GHC.LanguageExtensions.Type (Extension (..)) import Ide.Plugin.Eval.Util (gStrictTry) +import GHC (setTopSessionDynFlags, + setUnitDynFlags) +import GHC.Driver.Env +import GHC.Driver.Session (getDynFlags) + {- $setup >>> import GHC >>> import GHC.Paths @@ -164,3 +169,12 @@ showDynFlags df = vList :: [String] -> SDoc vList = vcat . map text + +setSessionAndInteractiveDynFlags :: DynFlags -> Ghc () +setSessionAndInteractiveDynFlags df = do + _ <- setUnitDynFlags (homeUnitId_ df) df + modifySession (hscUpdateLoggerFlags . hscSetActiveUnitId (homeUnitId_ df)) + df' <- getDynFlags + setTopSessionDynFlags df' + sessDyns <- getSessionDynFlags + setInteractiveDynFlags sessDyns diff --git a/plugins/hls-eval-plugin/src/Ide/Plugin/Eval/CodeLens.hs b/plugins/hls-eval-plugin/src/Ide/Plugin/Eval/Handlers.hs similarity index 78% rename from plugins/hls-eval-plugin/src/Ide/Plugin/Eval/CodeLens.hs rename to plugins/hls-eval-plugin/src/Ide/Plugin/Eval/Handlers.hs index 356c2079f7..1f19b5b476 100644 --- a/plugins/hls-eval-plugin/src/Ide/Plugin/Eval/CodeLens.hs +++ b/plugins/hls-eval-plugin/src/Ide/Plugin/Eval/Handlers.hs @@ -1,34 +1,28 @@ {-# LANGUAGE CPP #-} {-# LANGUAGE DuplicateRecordFields #-} {-# LANGUAGE ExtendedDefaultRules #-} -{-# LANGUAGE FlexibleContexts #-} -{-# LANGUAGE FlexibleInstances #-} -{-# LANGUAGE NamedFieldPuns #-} {-# LANGUAGE NoMonomorphismRestriction #-} {-# LANGUAGE OverloadedStrings #-} -{-# LANGUAGE RankNTypes #-} {-# LANGUAGE RecordWildCards #-} -{-# LANGUAGE ScopedTypeVariables #-} -{-# LANGUAGE TypeApplications #-} {-# LANGUAGE ViewPatterns #-} -{-# OPTIONS_GHC -fno-warn-type-defaults -Wno-unused-imports #-} +{-# OPTIONS_GHC -Wno-type-defaults #-} {- | A plugin inspired by the REPLoid feature of , 's Examples and Properties and . For a full example see the "Ide.Plugin.Eval.Tutorial" module. -} -module Ide.Plugin.Eval.CodeLens ( +module Ide.Plugin.Eval.Handlers ( + codeAction, codeLens, evalCommand, ) where import Control.Applicative (Alternative ((<|>))) -import Control.Arrow (second, (>>>)) -import Control.Exception (bracket_, try) +import Control.Arrow (second) +import Control.Exception (bracket_) import qualified Control.Exception as E -import Control.Lens (_1, _3, ix, (%~), - (<&>), (^.)) +import Control.Lens (ix, (%~), (^.)) import Control.Monad (guard, void, when) import Control.Monad.IO.Class (MonadIO (liftIO)) @@ -46,28 +40,19 @@ import Data.Maybe (catMaybes) import Data.String (IsString) import Data.Text (Text) import qualified Data.Text as T -import Data.Typeable (Typeable) +import qualified Data.Text.Utf16.Rope.Mixed as Rope +import Development.IDE.Core.FileStore (getUriContents, setSomethingModified) import Development.IDE.Core.Rules (IdeState, runAction) -import Development.IDE.Core.RuleTypes (LinkableResult (linkableHomeMod), - NeedsCompilation (NeedsCompilation), - TypeCheck (..), - tmrTypechecked) -import Development.IDE.Core.Shake (useNoFile_, - useWithStale_, - use_, uses_) +import Development.IDE.Core.Shake (use_, uses_, VFSModified (VFSUnmodified), useWithSeparateFingerprintRule_) import Development.IDE.GHC.Compat hiding (typeKind, unitState) -import Development.IDE.GHC.Compat.Util (GhcException, - OverridingBool (..), - bagToList) +import Development.IDE.GHC.Compat.Util (OverridingBool (..)) import Development.IDE.GHC.Util (evalGhcEnv, - modifyDynFlags, - printOutputable) + modifyDynFlags) import Development.IDE.Import.DependencyInformation (transitiveDeps, transitiveModuleDeps) -import Development.IDE.Types.Location (toNormalizedFilePath', - uriToFilePath') +import Development.IDE.Types.Location (toNormalizedFilePath') import GHC (ClsInst, ExecOptions (execLineNumber, execSourceFile), FamInst, @@ -87,22 +72,26 @@ import GHC (ClsInst, import Development.IDE.Core.RuleTypes (GetLinkable (GetLinkable), GetModSummary (GetModSummary), - GetModuleGraph (GetModuleGraph), + GetModuleGraphTransDepsFingerprints (GetModuleGraphTransDepsFingerprints), GhcSessionDeps (GhcSessionDeps), - ModSummaryResult (msrModSummary)) -import Development.IDE.Core.Shake (VFSModified (VFSUnmodified)) + ModSummaryResult (msrModSummary), + LinkableResult (linkableHomeMod), + TypeCheck (..), + tmrTypechecked, GetModuleGraphTransDepsFingerprints(..), GetModuleGraph(..)) import qualified Development.IDE.GHC.Compat.Core as Compat (InteractiveImport (IIModule)) -import qualified Development.IDE.GHC.Compat.Core as SrcLoc (HasSrcSpan (getLoc), - unLoc) +import qualified Development.IDE.GHC.Compat.Core as SrcLoc (unLoc) import Development.IDE.Types.HscEnvEq (HscEnvEq (hscEnv)) import qualified GHC.LanguageExtensions.Type as LangExt (Extension (..)) -import Development.IDE.Core.FileStore (setSomethingModified) +import Data.List.Extra (unsnoc) import Development.IDE.Core.PluginUtils import Development.IDE.Types.Shake (toKey) import GHC.Types.SrcLoc (UnhelpfulSpanReason (UnhelpfulInteractive)) +import Ide.Logger (Priority (..), + Recorder, + WithPriority, + logWith) import Ide.Plugin.Error (PluginError (PluginInternalError), - handleMaybe, handleMaybeM) import Ide.Plugin.Eval.Code (Statement, asStatements, @@ -116,7 +105,7 @@ import Ide.Plugin.Eval.Config (EvalConfig (..), import Ide.Plugin.Eval.GHC (addImport, addPackages, hasPackage, - showDynFlags) + setSessionAndInteractiveDynFlags) import Ide.Plugin.Eval.Parse.Comments (commentsToSections) import Ide.Plugin.Eval.Parse.Option (parseSetFlags) import Ide.Plugin.Eval.Rules (queueForEvaluation, @@ -124,50 +113,63 @@ import Ide.Plugin.Eval.Rules (queueForEvaluatio import Ide.Plugin.Eval.Types import Ide.Plugin.Eval.Util (gStrictTry, isLiterate, - logWith, + prettyWarnings, response', timed) import Ide.Types import qualified Language.LSP.Protocol.Lens as L import Language.LSP.Protocol.Message import Language.LSP.Protocol.Types import Language.LSP.Server -import Language.LSP.VFS (virtualFileText) +#if MIN_VERSION_ghc(9,11,0) +import GHC.Unit.Module.ModIface (IfaceTopEnv (..)) +#endif + +codeAction :: Recorder (WithPriority Log) -> PluginMethodHandler IdeState Method_TextDocumentCodeAction +codeAction recorder st plId CodeActionParams{_textDocument,_range} = do + rangeCommands <- mkRangeCommands recorder st plId _textDocument + pure + $ InL + [ InL command + | (testRange, command) <- rangeCommands + , _range `isSubrangeOf` testRange + ] {- | Code Lens provider NOTE: Invoked every time the document is modified, not just when the document is saved. -} -codeLens :: PluginMethodHandler IdeState Method_TextDocumentCodeLens -codeLens st plId CodeLensParams{_textDocument} = - let dbg = logWith st - perf = timed dbg - in perf "codeLens" $ +codeLens :: Recorder (WithPriority Log) -> PluginMethodHandler IdeState Method_TextDocumentCodeLens +codeLens recorder st plId CodeLensParams{_textDocument} = do + rangeCommands <- mkRangeCommands recorder st plId _textDocument + pure + $ InL + [ CodeLens range (Just command) Nothing + | (range, command) <- rangeCommands + ] + +mkRangeCommands :: Recorder (WithPriority Log) -> IdeState -> PluginId -> TextDocumentIdentifier -> ExceptT PluginError (HandlerM Config) [(Range, Command)] +mkRangeCommands recorder st plId textDocument = + let dbg = logWith recorder Debug + perf = timed (\lbl duration -> dbg $ LogExecutionTime lbl duration) + in perf "evalMkRangeCommands" $ do - let TextDocumentIdentifier uri = _textDocument + let TextDocumentIdentifier uri = textDocument fp <- uriToFilePathE uri let nfp = toNormalizedFilePath' fp isLHS = isLiterate fp - dbg "fp" fp + dbg $ LogCodeLensFp fp (comments, _) <- runActionE "eval.GetParsedModuleWithComments" st $ useWithStaleE GetEvalComments nfp - -- dbg "excluded comments" $ show $ DL.toList $ - -- foldMap (\(L a b) -> - -- case b of - -- AnnLineComment{} -> mempty - -- AnnBlockComment{} -> mempty - -- _ -> DL.singleton (a, b) - -- ) - -- $ apiAnnComments' pm_annotations - dbg "comments" $ show comments + dbg $ LogCodeLensComments comments -- Extract tests from source code let Sections{..} = commentsToSections isLHS comments tests = testsBySection nonSetupSections cmd = mkLspCommand plId evalCommandName "Evaluate=..." (Just []) - let lenses = - [ CodeLens testRange (Just cmd') Nothing + let rangeCommands = + [ (testRange, cmd') | (section, ident, test) <- tests , let (testRange, resultRange) = testRanges test - args = EvalParams (setupSections ++ [section]) _textDocument ident + args = EvalParams (setupSections ++ [section]) textDocument ident cmd' = (cmd :: Command) { _arguments = Just [toJSON args] @@ -179,49 +181,47 @@ codeLens st plId CodeLensParams{_textDocument} = ] perf "tests" $ - dbg "Tests" $ - unwords - [ show (length tests) - , "tests in" - , show (length nonSetupSections) - , "sections" - , show (length setupSections) - , "setups" - , show (length lenses) - , "lenses." - ] - - return $ InL lenses + dbg $ LogTests + (length tests) + (length nonSetupSections) + (length setupSections) + (length rangeCommands) + + pure rangeCommands where trivial (Range p p') = p == p' evalCommandName :: CommandId evalCommandName = "evalCommand" -evalCommand :: PluginId -> PluginCommand IdeState -evalCommand plId = PluginCommand evalCommandName "evaluate" (runEvalCmd plId) +evalCommand :: Recorder (WithPriority Log) -> PluginId -> PluginCommand IdeState +evalCommand recorder plId = PluginCommand evalCommandName "evaluate" (runEvalCmd recorder plId) type EvalId = Int -runEvalCmd :: PluginId -> CommandFunction IdeState EvalParams -runEvalCmd plId st EvalParams{..} = - let dbg = logWith st - perf = timed dbg - cmd :: ExceptT PluginError (LspM Config) WorkspaceEdit +runEvalCmd :: Recorder (WithPriority Log) -> PluginId -> CommandFunction IdeState EvalParams +runEvalCmd recorder plId st mtoken EvalParams{..} = + let dbg = logWith recorder Debug + perf = timed (\lbl duration -> dbg $ LogExecutionTime lbl duration) + cmd :: ExceptT PluginError (HandlerM Config) WorkspaceEdit cmd = do let tests = map (\(a,_,b) -> (a,b)) $ testsBySection sections let TextDocumentIdentifier{_uri} = module_ fp <- uriToFilePathE _uri let nfp = toNormalizedFilePath' fp - mdlText <- moduleText _uri + mdlText <- moduleText st _uri -- enable codegen for the module which we need to evaluate. final_hscEnv <- liftIO $ bracket_ - (do queueForEvaluation st nfp - setSomethingModified VFSUnmodified st [toKey IsEvaluating nfp] "Eval") - (do unqueueForEvaluation st nfp - setSomethingModified VFSUnmodified st [toKey IsEvaluating nfp] "Eval") + (setSomethingModified VFSUnmodified st "Eval" $ do + queueForEvaluation st nfp + return [toKey IsEvaluating nfp] + ) + (setSomethingModified VFSUnmodified st "Eval" $ do + unqueueForEvaluation st nfp + return [toKey IsEvaluating nfp] + ) (initialiseSessionForEval (needsQuickCheck tests) st nfp) evalCfg <- liftIO $ runAction "eval: config" st $ getEvalConfig plId @@ -231,14 +231,14 @@ runEvalCmd plId st EvalParams{..} = perf "edits" $ liftIO $ evalGhcEnv final_hscEnv $ do - runTests evalCfg (st, fp) tests + runTests recorder evalCfg fp tests - let workspaceEditsMap = Map.fromList [(_uri, addFinalReturn mdlText edits)] + let workspaceEditsMap = Map.singleton _uri (addFinalReturn mdlText edits) let workspaceEdits = WorkspaceEdit (Just workspaceEditsMap) Nothing Nothing return workspaceEdits in perf "evalCmd" $ ExceptT $ - withIndefiniteProgress "Evaluating" Cancellable $ + pluginWithIndefiniteProgress "Evaluating" mtoken Cancellable $ \_updater -> runExceptT $ response' cmd -- | Create an HscEnv which is suitable for performing interactive evaluation. @@ -253,24 +253,29 @@ initialiseSessionForEval needs_quickcheck st nfp = do ms <- msrModSummary <$> use_ GetModSummary nfp deps_hsc <- hscEnv <$> use_ GhcSessionDeps nfp - linkables_needed <- transitiveDeps <$> useNoFile_ GetModuleGraph <*> pure nfp + linkables_needed <- transitiveDeps <$> useWithSeparateFingerprintRule_ GetModuleGraphTransDepsFingerprints GetModuleGraph nfp <*> pure nfp linkables <- uses_ GetLinkable (nfp : maybe [] transitiveModuleDeps linkables_needed) -- We unset the global rdr env in mi_globals when we generate interfaces -- See Note [Clearing mi_globals after generating an iface] -- However, the eval plugin (setContext specifically) requires the rdr_env -- for the current module - so get it from the Typechecked Module and add -- it back to the iface for the current module. - rdr_env <- tcg_rdr_env . tmrTypechecked <$> use_ TypeCheck nfp + tm <- tmrTypechecked <$> use_ TypeCheck nfp + let rdr_env = tcg_rdr_env tm let linkable_hsc = loadModulesHome (map (addRdrEnv . linkableHomeMod) linkables) deps_hsc addRdrEnv hmi | iface <- hm_iface hmi , ms_mod ms == mi_module iface +#if MIN_VERSION_ghc(9,11,0) + = hmi { hm_iface = set_mi_top_env (Just $ IfaceTopEnv (forceGlobalRdrEnv (globalRdrEnvLocal rdr_env)) (mkIfaceImports $ tcg_import_decls tm)) iface} +#else = hmi { hm_iface = iface { mi_globals = Just $! #if MIN_VERSION_ghc(9,8,0) forceGlobalRdrEnv #endif rdr_env }} +#endif | otherwise = hmi return (ms, linkable_hsc) @@ -282,6 +287,7 @@ initialiseSessionForEval needs_quickcheck st nfp = do . flip xopt_unset LangExt.MonomorphismRestriction . flip gopt_set Opt_ImplicitImportQualified . flip gopt_unset Opt_DiagnosticsShowCaret + . setBackend ghciBackend $ (ms_hspp_opts ms) { useColor = Never , canUseColor = False } @@ -290,6 +296,15 @@ initialiseSessionForEval needs_quickcheck st nfp = do getSession return env2 +#if MIN_VERSION_ghc(9,11,0) +mkIfaceImports :: [ImportUserSpec] -> [IfaceImport] +mkIfaceImports = map go + where + go (ImpUserSpec decl ImpUserAll) = IfaceImport decl ImpIfaceAll + go (ImpUserSpec decl (ImpUserExplicit env)) = IfaceImport decl (ImpIfaceExplicit (forceGlobalRdrEnv env)) + go (ImpUserSpec decl (ImpUserEverythingBut ns)) = IfaceImport decl (ImpIfaceEverythingBut ns) +#endif + addFinalReturn :: Text -> [TextEdit] -> [TextEdit] addFinalReturn mdlText edits | not (null edits) && not (T.null mdlText) && T.last mdlText /= '\n' = @@ -300,16 +315,19 @@ finalReturn :: Text -> TextEdit finalReturn txt = let ls = T.lines txt l = fromIntegral $ length ls -1 - c = fromIntegral $ T.length . last $ ls + c = fromIntegral $ T.length $ maybe T.empty snd (unsnoc ls) p = Position l c in TextEdit (Range p p) "\n" -moduleText :: MonadLsp c m => Uri -> ExceptT PluginError m Text -moduleText uri = - handleMaybeM (PluginInternalError "mdlText") $ - (virtualFileText <$>) - <$> getVirtualFile - (toNormalizedUri uri) +moduleText :: IdeState -> Uri -> ExceptT PluginError (HandlerM config) Text +moduleText state uri = do + contents <- + handleMaybeM (PluginInternalError "mdlText") $ + liftIO $ + runAction "eval.getUriContents" state $ + getUriContents $ + toNormalizedUri uri + pure $ Rope.toText contents testsBySection :: [Section] -> [(Section, EvalId, Test)] testsBySection sections = @@ -318,7 +336,7 @@ testsBySection sections = , test <- sectionTests section ] -type TEnv = (IdeState, String) +type TEnv = String -- |GHC declarations required for expression evaluation evalSetup :: Ghc () evalSetup = do @@ -326,26 +344,26 @@ evalSetup = do context <- getContext setContext (IIDecl preludeAsP : context) -runTests :: EvalConfig -> TEnv -> [(Section, Test)] -> Ghc [TextEdit] -runTests EvalConfig{..} e@(_st, _) tests = do +runTests :: Recorder (WithPriority Log) -> EvalConfig -> TEnv -> [(Section, Test)] -> Ghc [TextEdit] +runTests recorder EvalConfig{..} e tests = do df <- getInteractiveDynFlags evalSetup - when (hasQuickCheck df && needsQuickCheck tests) $ void $ evals True e df propSetup + when (hasQuickCheck df && needsQuickCheck tests) $ void $ evals recorder True e df propSetup mapM (processTest e df) tests where processTest :: TEnv -> DynFlags -> (Section, Test) -> Ghc TextEdit - processTest e@(st, fp) df (section, test) = do - let dbg = logWith st + processTest fp df (section, test) = do + let dbg = logWith recorder Debug let pad = pad_ $ (if isLiterate fp then ("> " `T.append`) else id) $ padPrefix (sectionFormat section) rs <- runTest e df test - dbg "TEST RESULTS" rs + dbg $ LogRunTestResults rs let checkedResult = testCheck eval_cfg_diff (section, test) rs let resultLines = concatMap T.lines checkedResult let edit = asEdit (sectionFormat section) test (map pad resultLines) - dbg "TEST EDIT" edit + dbg $ LogRunTestEdits edit return edit -- runTest :: String -> DynFlags -> Loc Test -> Ghc [Text] @@ -354,7 +372,7 @@ runTests EvalConfig{..} e@(_st, _) tests = do return $ singleLine "Add QuickCheck to your cabal dependencies to run this test." - runTest e df test = evals (eval_cfg_exception && not (isProperty test)) e df (asStatements test) + runTest e df test = evals recorder (eval_cfg_exception && not (isProperty test)) e df (asStatements test) asEdit :: Format -> Test -> [Text] -> TextEdit asEdit (MultiLine commRange) test resultLines @@ -430,27 +448,26 @@ Or for a value that does not have a Show instance and can therefore not be displ >>> V No instance for (Show V) arising from a use of ‘evalPrint’ -} -evals :: Bool -> TEnv -> DynFlags -> [Statement] -> Ghc [Text] -evals mark_exception (st, fp) df stmts = do +evals :: Recorder (WithPriority Log) -> Bool -> TEnv -> DynFlags -> [Statement] -> Ghc [Text] +evals recorder mark_exception fp df stmts = do er <- gStrictTry $ mapM eval stmts return $ case er of Left err -> errorLines err Right rs -> concat . catMaybes $ rs where - dbg = logWith st + dbg = logWith recorder Debug eval :: Statement -> Ghc (Maybe [Text]) eval (Located l stmt) | -- GHCi flags Just (words -> flags) <- parseSetFlags stmt = do - dbg "{:SET" flags + dbg $ LogEvalFlags flags ndf <- getInteractiveDynFlags - dbg "pre set" $ showDynFlags ndf + dbg $ LogEvalPreSetDynFlags ndf eans <- liftIO $ try @GhcException $ parseDynamicFlagsCmdLine ndf (map (L $ UnhelpfulSpan unhelpfulReason) flags) - dbg "parsed flags" $ eans - <&> (_1 %~ showDynFlags >>> _3 %~ prettyWarnings) + dbg $ LogEvalParsedFlags eans case eans of Left err -> pure $ Just $ errorLines $ show err Right (df', ignoreds, warns) -> do @@ -464,10 +481,8 @@ evals mark_exception (st, fp) df stmts = do ["Some flags have not been recognized: " <> T.pack (intercalate ", " $ map SrcLoc.unLoc ignoreds) ] - dbg "post set" $ showDynFlags df' - _ <- setSessionDynFlags df' - sessDyns <- getSessionDynFlags - setInteractiveDynFlags sessDyns + dbg $ LogEvalPostSetDynFlags df' + setSessionAndInteractiveDynFlags df' pure $ warnings <> igns | -- A type/kind command Just (cmd, arg) <- parseGhciLikeCmd $ T.pack stmt = @@ -475,23 +490,23 @@ evals mark_exception (st, fp) df stmts = do | -- A statement isStmt pf stmt = do - dbg "{STMT " stmt + dbg $ LogEvalStmtStart stmt res <- exec stmt l let r = case res of Left err -> Just . (if mark_exception then exceptionLines else errorLines) $ err Right x -> singleLine <$> x - dbg "STMT} -> " r + dbg $ LogEvalStmtResult r return r | -- An import isImport pf stmt = do - dbg "{IMPORT " stmt + dbg $ LogEvalImport stmt _ <- addImport stmt return Nothing | -- A declaration otherwise = do - dbg "{DECL " stmt + dbg $ LogEvalDeclaration stmt void $ runDecls stmt return Nothing pf = initParserOpts df @@ -500,19 +515,6 @@ evals mark_exception (st, fp) df stmts = do let opts = execOptions{execSourceFile = fp, execLineNumber = l} in myExecStmt stmt opts -#if MIN_VERSION_ghc(9,8,0) -prettyWarnings :: Messages DriverMessage -> String -prettyWarnings = printWithoutUniques . pprMessages (defaultDiagnosticOpts @DriverMessage) -#else -prettyWarnings :: [Warn] -> String -prettyWarnings = unlines . map prettyWarn - -prettyWarn :: Warn -> String -prettyWarn Warn{..} = - T.unpack (printOutputable $ SrcLoc.getLoc warnMsg) <> ": warning:\n" - <> " " <> SrcLoc.unLoc warnMsg -#endif - needsQuickCheck :: [(Section, Test)] -> Bool needsQuickCheck = any (isProperty . snd) @@ -529,7 +531,7 @@ singleLine s = [T.pack s] errorLines :: String -> [Text] errorLines = dropWhileEnd T.null - . takeWhile (not . ("CallStack" `T.isPrefixOf`)) + . takeWhile (not . (\x -> "CallStack" `T.isPrefixOf` x || "HasCallStack" `T.isPrefixOf` x)) . T.lines . T.pack @@ -669,7 +671,6 @@ data GhciLikeCmdException = GhciLikeCmdNotImplemented { ghciCmdName :: Text , ghciCmdArg :: Text } - deriving (Typeable) instance Show GhciLikeCmdException where showsPrec _ GhciLikeCmdNotImplemented{..} = @@ -689,4 +690,3 @@ parseGhciLikeCmd :: Text -> Maybe (Text, Text) parseGhciLikeCmd input = do (':', rest) <- T.uncons $ T.stripStart input pure $ second T.strip $ T.break isSpace rest - diff --git a/plugins/hls-eval-plugin/src/Ide/Plugin/Eval/Parse/Comments.hs b/plugins/hls-eval-plugin/src/Ide/Plugin/Eval/Parse/Comments.hs index b638c159bd..6f8b303302 100644 --- a/plugins/hls-eval-plugin/src/Ide/Plugin/Eval/Parse/Comments.hs +++ b/plugins/hls-eval-plugin/src/Ide/Plugin/Eval/Parse/Comments.hs @@ -1,11 +1,8 @@ -{-# LANGUAGE DeriveGeneric #-} +{-# LANGUAGE CPP #-} {-# LANGUAGE DerivingStrategies #-} -{-# LANGUAGE FlexibleInstances #-} {-# LANGUAGE OverloadedStrings #-} -{-# LANGUAGE RankNTypes #-} {-# LANGUAGE RecordWildCards #-} {-# LANGUAGE TemplateHaskell #-} -{-# LANGUAGE TupleSections #-} module Ide.Plugin.Eval.Parse.Comments where @@ -28,7 +25,7 @@ import Data.Function ((&)) import Data.Functor ((<&>)) import Data.Functor.Identity import Data.List.NonEmpty (NonEmpty ((:|))) -import qualified Data.List.NonEmpty as NE +import qualified Data.List.NonEmpty as NE hiding (unzip) import Data.Map.Strict (Map) import qualified Data.Map.Strict as Map import qualified Data.Text as T @@ -44,6 +41,12 @@ import Text.Megaparsec.Char (alphaNumChar, char, eol, hspace, letterChar) +#if MIN_VERSION_base(4,19,0) +import qualified Data.Functor as NE (unzip) +#else +import qualified Data.List.NonEmpty as NE (unzip) +#endif + {- We build parsers combining the following three kinds of them: @@ -61,7 +64,7 @@ We build parsers combining the following three kinds of them: -} -- | Line parser -type LineParser a = forall m. Monad m => ParsecT Void String m a +type LineParser a = forall m. ParsecT Void String m a -- | Line comment group parser type LineGroupParser = Parsec Void [(Range, RawLineComment)] @@ -126,7 +129,7 @@ commentsToSections isLHS Comments {..} = in case parseMaybe lineGroupP $ NE.toList lcs of Nothing -> mempty Just (mls, rs) -> - ( maybe mempty (uncurry Map.singleton) ((theRan,) <$> mls) + ( maybe mempty (Map.singleton theRan) mls , -- orders setup sections in ascending order if null rs then mempty @@ -305,7 +308,7 @@ blockProp = do AProp ran prop <$> resultBlockP withRange :: - (TraversableStream s, Stream s, Monad m, Ord v, Traversable t) => + (TraversableStream s, Ord v, Traversable t) => ParsecT v s m (t (a, Position)) -> ParsecT v s m (Range, t a) withRange p = do @@ -493,7 +496,7 @@ consume style = Line -> (,) <$> takeRest <*> getPosition Block {} -> manyTill_ anySingle (getPosition <* eob) -getPosition :: (Monad m, Ord v, TraversableStream s) => ParsecT v s m Position +getPosition :: (Ord v, TraversableStream s) => ParsecT v s m Position getPosition = sourcePosToPosition <$> getSourcePos -- | Parses example test line. diff --git a/plugins/hls-eval-plugin/src/Ide/Plugin/Eval/Rules.hs b/plugins/hls-eval-plugin/src/Ide/Plugin/Eval/Rules.hs index 14c1d0b0b9..d01ddbc55c 100644 --- a/plugins/hls-eval-plugin/src/Ide/Plugin/Eval/Rules.hs +++ b/plugins/hls-eval-plugin/src/Ide/Plugin/Eval/Rules.hs @@ -1,57 +1,45 @@ {-# LANGUAGE CPP #-} {-# LANGUAGE LambdaCase #-} {-# LANGUAGE PatternSynonyms #-} -{-# LANGUAGE RecordWildCards #-} --- To avoid warning "Pattern match has inaccessible right hand side" -{-# OPTIONS_GHC -Wno-overlapping-patterns #-} module Ide.Plugin.Eval.Rules (GetEvalComments(..), rules,queueForEvaluation, unqueueForEvaluation, Log) where +import Control.Lens (toListOf) import Control.Monad.IO.Class (MonadIO (liftIO)) +import qualified Data.ByteString as BS +import Data.Data.Lens (biplate) import Data.HashSet (HashSet) import qualified Data.HashSet as Set import Data.IORef import qualified Data.Map.Strict as Map import Data.String (fromString) -import Development.IDE (GetModSummaryWithoutTimestamps (GetModSummaryWithoutTimestamps), - GetParsedModuleWithComments (GetParsedModuleWithComments), +import Development.IDE (GetParsedModuleWithComments (GetParsedModuleWithComments), IdeState, + LinkableType (BCOLinkable), NeedsCompilation (NeedsCompilation), NormalizedFilePath, RuleBody (RuleNoDiagnostics), Rules, defineEarlyCutoff, encodeLinkableType, fromNormalizedFilePath, - msrModSummary, realSrcSpanToRange, - useWithStale_, - use_) + useWithStale_, use_) import Development.IDE.Core.PositionMapping (toCurrentRange) -import Development.IDE.Core.Rules (computeLinkableTypeForDynFlags, - needsCompilationRule) +import Development.IDE.Core.Rules (needsCompilationRule) import Development.IDE.Core.Shake (IsIdeGlobal, RuleBody (RuleWithCustomNewnessCheck), addIdeGlobal, getIdeGlobalAction, getIdeGlobalState) -import qualified Development.IDE.Core.Shake as Shake import Development.IDE.GHC.Compat import qualified Development.IDE.GHC.Compat as SrcLoc import qualified Development.IDE.GHC.Compat.Util as FastString import Development.IDE.Graph (alwaysRerun) -import Ide.Logger (Pretty (pretty), - Recorder, WithPriority, - cmapWithPrio) import GHC.Parser.Annotation +import Ide.Logger (Recorder, WithPriority, + cmapWithPrio) import Ide.Plugin.Eval.Types -import qualified Data.ByteString as BS - -newtype Log = LogShake Shake.Log deriving Show - -instance Pretty Log where - pretty = \case - LogShake shakeLog -> pretty shakeLog rules :: Recorder (WithPriority Log) -> Rules () rules recorder = do @@ -74,28 +62,19 @@ unqueueForEvaluation ide nfp = do -- remove the module from the Evaluating state, so that next time it won't evaluate to True atomicModifyIORef' var $ \fs -> (Set.delete nfp fs, ()) -#if MIN_VERSION_ghc(9,5,0) -getAnnotations :: Development.IDE.GHC.Compat.Located (HsModule GhcPs) -> [LEpaComment] -getAnnotations (L _ m@(HsModule { hsmodExt = XModulePs {hsmodAnn = anns'}})) = -#else -getAnnotations :: Development.IDE.GHC.Compat.Located HsModule -> [LEpaComment] -getAnnotations (L _ m@(HsModule { hsmodAnn = anns'})) = -#endif - priorComments annComments <> getFollowingComments annComments - <> concatMap getCommentsForDecl (hsmodImports m) - <> concatMap getCommentsForDecl (hsmodDecls m) - where - annComments = epAnnComments anns' - -getCommentsForDecl :: GenLocated (SrcSpanAnn' (EpAnn ann)) e - -> [LEpaComment] -getCommentsForDecl (L (SrcSpanAnn (EpAnn _ _ cs) _) _) = priorComments cs <> getFollowingComments cs -getCommentsForDecl (L (SrcSpanAnn (EpAnnNotUsed) _) _) = [] - apiAnnComments' :: ParsedModule -> [SrcLoc.RealLocated EpaCommentTok] apiAnnComments' pm = do - L span (EpaComment c _) <- getAnnotations $ pm_parsed_source pm - pure (L (anchor span) c) + L span (EpaComment c _) <- getEpaComments $ pm_parsed_source pm + pure (L ( +#if MIN_VERSION_ghc(9,11,0) + epaLocationRealSrcSpan +#else + anchor +#endif + span) c) + where + getEpaComments :: Development.IDE.GHC.Compat.Located (HsModule GhcPs) -> [LEpaComment] + getEpaComments = toListOf biplate pattern RealSrcSpanAlready :: SrcLoc.RealSrcSpan -> SrcLoc.RealSrcSpan pattern RealSrcSpanAlready x = x @@ -142,11 +121,10 @@ isEvaluatingRule recorder = defineEarlyCutoff (cmapWithPrio LogShake recorder) $ redefinedNeedsCompilation :: Recorder (WithPriority Log) -> Rules () redefinedNeedsCompilation recorder = defineEarlyCutoff (cmapWithPrio LogShake recorder) $ RuleWithCustomNewnessCheck (<=) $ \NeedsCompilation f -> do isEvaluating <- use_ IsEvaluating f - - if not isEvaluating then needsCompilationRule f else do - ms <- msrModSummary . fst <$> useWithStale_ GetModSummaryWithoutTimestamps f - let df' = ms_hspp_opts ms - linkableType = computeLinkableTypeForDynFlags df' + if isEvaluating then do + let linkableType = BCOLinkable fp = encodeLinkableType $ Just linkableType - pure (Just fp, Just (Just linkableType)) + else + needsCompilationRule f + diff --git a/plugins/hls-eval-plugin/src/Ide/Plugin/Eval/Types.hs b/plugins/hls-eval-plugin/src/Ide/Plugin/Eval/Types.hs index 104c1b4615..1753ab4e6c 100644 --- a/plugins/hls-eval-plugin/src/Ide/Plugin/Eval/Types.hs +++ b/plugins/hls-eval-plugin/src/Ide/Plugin/Eval/Types.hs @@ -1,15 +1,16 @@ -{-# LANGUAGE DeriveAnyClass #-} -{-# LANGUAGE DeriveFunctor #-} -{-# LANGUAGE DeriveGeneric #-} -{-# LANGUAGE DerivingStrategies #-} -{-# LANGUAGE GeneralizedNewtypeDeriving #-} -{-# LANGUAGE TypeFamilies #-} -{-# LANGUAGE UndecidableInstances #-} +{-# LANGUAGE DeriveAnyClass #-} +{-# LANGUAGE DerivingStrategies #-} +{-# LANGUAGE DuplicateRecordFields #-} +{-# LANGUAGE LambdaCase #-} +{-# LANGUAGE OverloadedStrings #-} +{-# LANGUAGE RecordWildCards #-} +{-# LANGUAGE TypeFamilies #-} +{-# LANGUAGE UndecidableInstances #-} {-# OPTIONS_GHC -Wwarn #-} -{-# LANGUAGE RecordWildCards #-} module Ide.Plugin.Eval.Types - ( locate, + ( Log(..), + locate, locate0, Test (..), isProperty, @@ -33,17 +34,75 @@ module Ide.Plugin.Eval.Types nullComments) where -import Control.DeepSeq (deepseq) -import Data.Aeson (FromJSON, ToJSON) -import Data.List (partition) -import Data.List.NonEmpty (NonEmpty) -import Data.Map.Strict (Map) -import Data.String (IsString (..)) -import Development.IDE (Range, RuleResult) +import Control.Arrow ((>>>)) +import Control.DeepSeq (deepseq) +import Control.Lens +import Data.Aeson (FromJSON, ToJSON) +import Data.List (partition) +import Data.List.NonEmpty (NonEmpty) +import Data.Map.Strict (Map) +import Data.String (IsString (..)) +import qualified Data.Text as T +import Development.IDE (Range, RuleResult) +import qualified Development.IDE.Core.Shake as Shake +import qualified Development.IDE.GHC.Compat.Core as Core import Development.IDE.Graph.Classes -import GHC.Generics (Generic) -import Language.LSP.Protocol.Types (TextDocumentIdentifier) -import qualified Text.Megaparsec as P +import GHC.Generics (Generic) +import Ide.Logger +import Ide.Plugin.Eval.GHC (showDynFlags) +import Ide.Plugin.Eval.Util +import Language.LSP.Protocol.Types (TextDocumentIdentifier, + TextEdit) +import qualified System.Time.Extra as Extra +import qualified Text.Megaparsec as P + +data Log + = LogShake Shake.Log + | LogCodeLensFp FilePath + | LogCodeLensComments Comments + | LogExecutionTime T.Text Extra.Seconds + | LogTests !Int !Int !Int !Int + | LogRunTestResults [T.Text] + | LogRunTestEdits TextEdit + | LogEvalFlags [String] + | LogEvalPreSetDynFlags Core.DynFlags + | LogEvalParsedFlags + (Either + Core.GhcException + (Core.DynFlags, [Core.Located String], DynFlagsParsingWarnings)) + | LogEvalPostSetDynFlags Core.DynFlags + | LogEvalStmtStart String + | LogEvalStmtResult (Maybe [T.Text]) + | LogEvalImport String + | LogEvalDeclaration String + +instance Pretty Log where + pretty = \case + LogShake shakeLog -> pretty shakeLog + LogCodeLensFp fp -> "fp" <+> pretty fp + LogCodeLensComments comments -> "comments" <+> viaShow comments + LogExecutionTime lbl duration -> pretty lbl <> ":" <+> pretty (Extra.showDuration duration) + LogTests nTests nNonSetupSections nSetupSections nLenses -> "Tests" <+> fillSep + [ pretty nTests + , "tests in" + , pretty nNonSetupSections + , "sections" + , pretty nSetupSections + , "setups" + , pretty nLenses + , "lenses." + ] + LogRunTestResults results -> "TEST RESULTS" <+> viaShow results + LogRunTestEdits edits -> "TEST EDIT" <+> viaShow edits + LogEvalFlags flags -> "{:SET" <+> pretty flags + LogEvalPreSetDynFlags dynFlags -> "pre set" <+> pretty (showDynFlags dynFlags) + LogEvalParsedFlags eans -> "parsed flags" <+> viaShow (eans + <&> (_1 %~ showDynFlags >>> _3 %~ prettyWarnings)) + LogEvalPostSetDynFlags dynFlags -> "post set" <+> pretty (showDynFlags dynFlags) + LogEvalStmtStart stmt -> "{STMT" <+> pretty stmt + LogEvalStmtResult result -> "STMT}" <+> pretty result + LogEvalImport stmt -> "{IMPORT" <+> pretty stmt + LogEvalDeclaration stmt -> "{DECL" <+> pretty stmt -- | A thing with a location attached. data Located l a = Located {location :: l, located :: a} @@ -98,14 +157,14 @@ data Test deriving (Eq, Show, Generic, FromJSON, ToJSON, NFData) data IsEvaluating = IsEvaluating - deriving (Eq, Show, Typeable, Generic) + deriving (Eq, Show, Generic) instance Hashable IsEvaluating instance NFData IsEvaluating type instance RuleResult IsEvaluating = Bool data GetEvalComments = GetEvalComments - deriving (Eq, Show, Typeable, Generic) + deriving (Eq, Show, Generic) instance Hashable GetEvalComments instance NFData GetEvalComments diff --git a/plugins/hls-eval-plugin/src/Ide/Plugin/Eval/Util.hs b/plugins/hls-eval-plugin/src/Ide/Plugin/Eval/Util.hs index 86adf2cb56..9498076511 100644 --- a/plugins/hls-eval-plugin/src/Ide/Plugin/Eval/Util.hs +++ b/plugins/hls-eval-plugin/src/Ide/Plugin/Eval/Util.hs @@ -1,85 +1,60 @@ -{-# LANGUAGE CPP #-} -{-# LANGUAGE NoMonomorphismRestriction #-} -{-# LANGUAGE ScopedTypeVariables #-} -{-# LANGUAGE TypeApplications #-} -{-# OPTIONS_GHC -Wno-orphans -Wno-unused-imports #-} +{-# LANGUAGE CPP #-} +{-# LANGUAGE RecordWildCards #-} +{-# OPTIONS_GHC -Wno-orphans #-} --- |Debug utilities +-- | Debug utilities module Ide.Plugin.Eval.Util ( timed, isLiterate, response', gStrictTry, - logWith, + DynFlagsParsingWarnings, + prettyWarnings, ) where import Control.Exception (SomeException, evaluate, fromException) -import Control.Monad.Error.Class (MonadError (throwError)) import Control.Monad.IO.Class (MonadIO (liftIO)) import Control.Monad.Trans.Class (MonadTrans (lift)) import Control.Monad.Trans.Except (ExceptT (..), runExceptT) import Data.Aeson (Value) -import Data.Bifunctor (second) import Data.String (IsString (fromString)) -import qualified Data.Text as T -import Development.IDE (IdeState, Priority (..), - ideLogger, logPriority) -import qualified Development.IDE.Core.PluginUtils as PluginUtils import Development.IDE.GHC.Compat.Outputable import Development.IDE.GHC.Compat.Util (MonadCatch, bagToList, catch) -import GHC.Exts (toList) -import GHC.Stack (HasCallStack, callStack, - srcLocFile, - srcLocStartCol, - srcLocStartLine) import Ide.Plugin.Error +import Ide.Types (HandlerM, + pluginSendRequest) import Language.LSP.Protocol.Message import Language.LSP.Protocol.Types -import Language.LSP.Server import System.FilePath (takeExtension) -import System.Time.Extra (duration, showDuration) +import qualified System.Time.Extra as Extra +import System.Time.Extra (duration) import UnliftIO.Exception (catchAny) -timed :: MonadIO m => (t -> String -> m a) -> t -> m b -> m b +#if !MIN_VERSION_ghc(9,8,0) +import qualified Data.Text as T +import Development.IDE (printOutputable) +import qualified Development.IDE.GHC.Compat.Core as Core +#endif + +timed :: MonadIO m => (t -> Extra.Seconds -> m a) -> t -> m b -> m b timed out name op = do (secs, r) <- duration op - _ <- out name (showDuration secs) + _ <- out name secs return r --- | Log using hie logger, reports source position of logging statement -logWith :: (HasCallStack, MonadIO m, Show a1, Show a2) => IdeState -> a1 -> a2 -> m () -logWith state key val = - liftIO . logPriority (ideLogger state) logLevel $ - T.unwords - [T.pack logWithPos, asT key, asT val] - where - logWithPos = - let stk = toList callStack - pr pos = concat [srcLocFile pos, ":", show . srcLocStartLine $ pos, ":", show . srcLocStartCol $ pos] - in case stk of - [] -> "" - (x:_) -> pr $ snd x - - asT :: Show a => a -> T.Text - asT = T.pack . show - --- | Set to Info to see extensive debug info in hie log, set to Debug in production -logLevel :: Priority -logLevel = Debug -- Info - isLiterate :: FilePath -> Bool isLiterate x = takeExtension x `elem` [".lhs", ".lhs-boot"] -response' :: ExceptT PluginError (LspM c) WorkspaceEdit -> ExceptT PluginError (LspM c) (Value |? Null) +response' :: ExceptT PluginError (HandlerM c) WorkspaceEdit -> ExceptT PluginError (HandlerM c) (Value |? Null) response' act = do res <- ExceptT (runExceptT act `catchAny` \e -> do res <- showErr e pure . Left . PluginInternalError $ fromString res) - _ <- lift $ sendRequest SMethod_WorkspaceApplyEdit (ApplyWorkspaceEditParams Nothing res) (\_ -> pure ()) + _ <- lift $ pluginSendRequest SMethod_WorkspaceApplyEdit (ApplyWorkspaceEditParams Nothing res) (\_ -> pure ()) pure $ InR Null gStrictTry :: (MonadIO m, MonadCatch m) => m b -> m (Either String b) @@ -93,7 +68,6 @@ gevaluate = liftIO . evaluate showErr :: Monad m => SomeException -> m String showErr e = -#if MIN_VERSION_ghc(9,3,0) case fromException e of -- On GHC 9.4+, the show instance adds the error message span -- We don't want this for the plugin @@ -103,11 +77,25 @@ showErr e = $ bagToList $ fmap (vcat . unDecorated . diagnosticMessage -#if MIN_VERSION_ghc(9,5,0) (defaultDiagnosticOpts @GhcMessage) -#endif . errMsgDiagnostic) $ getMessages msgs _ -> -#endif return . show $ e + +#if MIN_VERSION_ghc(9,8,0) +type DynFlagsParsingWarnings = Messages DriverMessage + +prettyWarnings :: DynFlagsParsingWarnings -> String +prettyWarnings = printWithoutUniques . pprMessages (defaultDiagnosticOpts @DriverMessage) +#else +type DynFlagsParsingWarnings = [Core.Warn] + +prettyWarnings :: DynFlagsParsingWarnings -> String +prettyWarnings = unlines . map prettyWarn + +prettyWarn :: Core.Warn -> String +prettyWarn Core.Warn{..} = + T.unpack (printOutputable $ Core.getLoc warnMsg) <> ": warning:\n" + <> " " <> Core.unLoc warnMsg +#endif diff --git a/plugins/hls-eval-plugin/test/Main.hs b/plugins/hls-eval-plugin/test/Main.hs index 18f718633b..03416c6902 100644 --- a/plugins/hls-eval-plugin/test/Main.hs +++ b/plugins/hls-eval-plugin/test/Main.hs @@ -1,35 +1,30 @@ -{-# LANGUAGE MultiWayIf #-} -{-# LANGUAGE OverloadedLabels #-} {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE RecordWildCards #-} -{-# LANGUAGE TypeApplications #-} {-# LANGUAGE ViewPatterns #-} module Main ( main ) where -import Control.Lens (_Just, folded, preview, - toListOf, view, (^..)) -import Data.Aeson (Value (Object), fromJSON, - object, toJSON, (.=)) -import Data.Aeson.Types (Pair, Result (Success)) -import Data.List (isInfixOf) -import Data.List.Extra (nubOrdOn) -import qualified Data.Map as Map -import Data.Row -import qualified Data.Text as T -import Ide.Plugin.Config (Config) -import qualified Ide.Plugin.Config as Plugin -import qualified Ide.Plugin.Eval as Eval -import Ide.Plugin.Eval.Types (EvalParams (..), Section (..), - testOutput) -import Ide.Types (IdePlugins (IdePlugins)) -import Language.LSP.Protocol.Lens (arguments, command, range, - title) -import Language.LSP.Protocol.Message hiding (error) -import System.FilePath ((<.>), ()) +import Control.Lens (_Just, folded, preview, view, (^.), + (^..), (^?)) +import Control.Monad (join) +import Data.Aeson (Value (Object), fromJSON, object, + (.=)) +import Data.Aeson.Types (Pair, Result (Success)) +import Data.List (isInfixOf) +import Data.List.Extra (nubOrdOn) +import qualified Data.Map as Map +import qualified Data.Maybe as Maybe +import qualified Data.Text as T +import Ide.Plugin.Config (Config) +import qualified Ide.Plugin.Config as Plugin +import qualified Ide.Plugin.Eval as Eval +import Ide.Plugin.Eval.Types (EvalParams (..), Section (..), + testOutput) +import Language.LSP.Protocol.Lens (command, range, title) +import System.FilePath ((<.>), ()) import Test.Hls -import qualified Test.Hls.FileSystem as FS +import qualified Test.Hls.FileSystem as FS main :: IO () main = defaultTestRunner tests @@ -66,6 +61,9 @@ tests = lenses <- getCodeLenses doc liftIO $ map (view range) lenses @?= [Range (Position 4 0) (Position 5 0)] + , goldenWithEvalForCodeAction "Evaluation of expressions via code action" "T1" "hs" + , goldenWithEvalForCodeAction "Reevaluation of expressions via code action" "T2" "hs" + , goldenWithEval "Evaluation of expressions" "T1" "hs" , goldenWithEval "Reevaluation of expressions" "T2" "hs" , goldenWithEval "Evaluation of expressions w/ imports" "T3" "hs" @@ -77,47 +75,37 @@ tests = , testCase "Semantic and Lexical errors are reported" $ do evalInFile "T8.hs" "-- >>> noFunctionWithThisName" "-- Variable not in scope: noFunctionWithThisName" evalInFile "T8.hs" "-- >>> res = \"a\" + \"bc\"" $ - if - | ghcVersion >= GHC96 -> "-- No instance for `Num String' arising from a use of `+'\n-- In the expression: \"a\" + \"bc\"\n-- In an equation for `res': res = \"a\" + \"bc\"" - | ghcVersion >= GHC92 -> "-- No instance for (Num String) arising from a use of `+'\n-- In the expression: \"a\" + \"bc\"\n-- In an equation for `res': res = \"a\" + \"bc\"" - | ghcVersion == GHC90 -> "-- No instance for (Num String) arising from a use of ‘+’" - | otherwise -> "-- No instance for (Num [Char]) arising from a use of ‘+’" - evalInFile "T8.hs" "-- >>> \"" "-- lexical error in string/character literal at end of input" + if ghcVersion >= GHC96 then + "-- No instance for `Num String' arising from a use of `+'\n-- In the expression: \"a\" + \"bc\"\n-- In an equation for `res': res = \"a\" + \"bc\"" + else + "-- No instance for (Num String) arising from a use of `+'\n-- In the expression: \"a\" + \"bc\"\n-- In an equation for `res': res = \"a\" + \"bc\"" + + evalInFile "T8.hs" "-- >>> \"" (if ghcVersion >= GHC912 then "-- lexical error at end of input" else "-- lexical error in string/character literal at end of input") evalInFile "T8.hs" "-- >>> 3 `div` 0" "-- divide by zero" -- The default for marking exceptions is False , goldenWithEval "Applies file LANGUAGE extensions" "T9" "hs" - , goldenWithEval' "Evaluate a type with :kind!" "T10" "hs" (if ghcVersion >= GHC92 then "ghc92.expected" else "expected") - , goldenWithEval' "Reports an error for an incorrect type with :kind!" "T11" "hs" ( - if ghcVersion >= GHC94 then "ghc94.expected" - else if ghcVersion >= GHC92 then "ghc92.expected" - else "expected" - ) - , goldenWithEval' "Shows a kind with :kind" "T12" "hs" (if ghcVersion >= GHC92 then "ghc92.expected" else "expected") - , goldenWithEval' "Reports an error for an incorrect type with :kind" "T13" "hs" (if ghcVersion >= GHC92 then "ghc92.expected" else "expected") + , goldenWithEval "Evaluate a type with :kind!" "T10" "hs" + , goldenWithEval "Reports an error for an incorrect type with :kind!" "T11" "hs" + , goldenWithEval "Shows a kind with :kind" "T12" "hs" + , goldenWithEval "Reports an error for an incorrect type with :kind" "T13" "hs" , goldenWithEval' "Returns a fully-instantiated type for :type" "T14" "hs" (if ghcVersion >= GHC98 then "ghc98.expected" else "expected") -- See https://gitlab.haskell.org/ghc/ghc/-/issues/24069 - , knownBrokenForGhcVersions [GHC92, GHC94, GHC96, GHC98] "type +v does not work anymore with 9.2" $ goldenWithEval "Returns an uninstantiated type for :type +v, admitting multiple whitespaces around arguments" "T15" "hs" + , goldenWithEval "Doesn't break in module containing main function" "T4139" "hs" , goldenWithEval "Returns defaulted type for :type +d, admitting multiple whitespaces around arguments" "T16" "hs" - , goldenWithEval' ":type reports an error when given with unknown +x option" "T17" "hs" (if ghcVersion >= GHC92 then "ghc92.expected" else "expected") + , goldenWithEval ":type reports an error when given with unknown +x option" "T17" "hs" , goldenWithEval "Reports an error when given with unknown command" "T18" "hs" , goldenWithEval "Returns defaulted type for :type +d reflecting the default declaration specified in the >>> prompt" "T19" "hs" - , expectFailBecause "known issue - see a note in P.R. #361" $ - goldenWithEval' ":type +d reflects the `default' declaration of the module" "T20" "hs" (if ghcVersion >= GHC92 then "ghc92.expected" else "expected") + -- TODO: known issue - see a note in P.R. #361 + , goldenWithEval ":type +d reflects the `default' declaration of the module" "T20" "hs" , testCase ":type handles a multilined result properly" $ evalInFile "T21.hs" "-- >>> :type fun" $ T.unlines [ "-- fun", - if - | ghcVersion >= GHC92 -> "-- :: forall {k1} (k2 :: Nat) (n :: Nat) (a :: k1)." - | ghcVersion == GHC90 -> "-- :: forall {k1} {k2 :: Nat} {n :: Nat} {a :: k1}." - | otherwise -> "-- :: forall k1 (k2 :: Nat) (n :: Nat) (a :: k1).", + "-- :: forall {k1} (k2 :: Nat) (n :: Nat) (a :: k1).", "-- (KnownNat k2, KnownNat n, Typeable a) =>", "-- Proxy k2 -> Proxy n -> Proxy a -> ()" ] , goldenWithEval ":t behaves exactly the same as :type" "T22" "hs" , testCase ":type does \"dovetails\" for short identifiers" $ evalInFile "T23.hs" "-- >>> :type f" $ T.unlines [ - if - | ghcVersion >= GHC92 -> "-- f :: forall {k1} (k2 :: Nat) (n :: Nat) (a :: k1)." - | ghcVersion == GHC90 -> "-- f :: forall {k1} {k2 :: Nat} {n :: Nat} {a :: k1}." - | otherwise -> "-- f :: forall k1 (k2 :: Nat) (n :: Nat) (a :: k1).", + "-- f :: forall {k1} (k2 :: Nat) (n :: Nat) (a :: k1).", "-- (KnownNat k2, KnownNat n, Typeable a) =>", "-- Proxy k2 -> Proxy n -> Proxy a -> ()" ] @@ -125,6 +113,7 @@ tests = , goldenWithEval ":kind treats a multilined result properly" "T25" "hs" , goldenWithEvalAndFs "local imports" (FS.directProjectMulti ["T26.hs", "Util.hs"]) "T26" "hs" , goldenWithEval "Preserves one empty comment line after prompt" "T27" "hs" + , goldenWithEval "Evaluate comment after multiline function definition" "T28" "hs" , goldenWithEval "Multi line comments" "TMulti" "hs" , goldenWithEval "Multi line comments, with the last test line ends without newline" "TEndingMulti" "hs" , goldenWithEval "Evaluate expressions in Plain comments in both single line and multi line format" "TPlainComment" "hs" @@ -134,26 +123,20 @@ tests = , goldenWithEvalAndFs "Transitive local dependency" (FS.directProjectMulti ["TTransitive.hs", "TLocalImport.hs", "Util.hs"]) "TTransitive" "hs" -- , goldenWithEval "Local Modules can be imported in a test" "TLocalImportInTest" "hs" , goldenWithEval "Setting language option TupleSections" "TLanguageOptionsTupleSections" "hs" - , goldenWithEval' ":set accepts ghci flags" "TFlags" "hs" (if ghcVersion >= GHC92 then "ghc98.expected" else if ghcVersion >= GHC92 then "ghc92.expected" else "expected") + , goldenWithEval' ":set accepts ghci flags" "TFlags" "hs" (if ghcVersion >= GHC98 then "ghc98.expected" else "expected") , testCase ":set -fprint-explicit-foralls works" $ do evalInFile "T8.hs" "-- >>> :t id" "-- id :: a -> a" - evalInFile "T8.hs" "-- >>> :set -fprint-explicit-foralls\n-- >>> :t id" - (if ghcVersion >= GHC92 - then "-- id :: forall a. a -> a" - else "-- id :: forall {a}. a -> a") + evalInFile "T8.hs" "-- >>> :set -fprint-explicit-foralls\n-- >>> :t id" "-- id :: forall a. a -> a" , goldenWithEval "The default language extensions for the eval plugin are the same as those for ghci" "TSameDefaultLanguageExtensionsAsGhci" "hs" , goldenWithEval "IO expressions are supported, stdout/stderr output is ignored" "TIO" "hs" , goldenWithEvalAndFs "Property checking" cabalProjectFS "TProperty" "hs" - , goldenWithEvalAndFs' "Property checking with exception" cabalProjectFS "TPropertyError" "hs" ( - if ghcVersion >= GHC96 then - "ghc96.expected" - else if ghcVersion >= GHC94 && hostOS == Windows then - "windows-ghc94.expected" - else if ghcVersion >= GHC94 then - "ghc94.expected" - else - "expected" - ) + , knownBrokenInWindowsBeforeGHC912 "The output has path separators in it, which on Windows look different. Just skip it there" $ + goldenWithEvalAndFs' "Property checking with exception" cabalProjectFS "TPropertyError" "hs" $ + case ghcVersion of + GHC912 -> "ghc912.expected" + GHC910 -> "ghc910.expected" + GHC98 -> "ghc98.expected" + GHC96 -> "ghc96.expected" , goldenWithEval "Prelude has no special treatment, it is imported as stated in the module" "TPrelude" "hs" , goldenWithEval "Don't panic on {-# UNPACK #-} pragma" "TUNPACK" "hs" , goldenWithEval "Can handle eval inside nested comment properly" "TNested" "hs" @@ -215,25 +198,36 @@ tests = , testCase "Interfaces are reused after Eval" $ do runSessionWithServerInTmpDir def evalPlugin (mkFs $ FS.directProjectMulti ["TLocalImport.hs", "Util.hs"]) $ do doc <- openDoc "TLocalImport.hs" "haskell" - waitForTypecheck doc + _ <- waitForTypecheck doc lenses <- getCodeLenses doc - let ~cmds@[cmd] = lenses^..folded.command._Just - liftIO $ cmds^..folded.title @?= ["Evaluate..."] + cmd <- liftIO $ case lenses^..folded.command._Just of + [cmd] -> (cmd^.title @?= "Evaluate...") >> pure cmd + cmds -> assertFailure $ "Expected a single command, got " <> show (length cmds) executeCmd cmd -- trigger a rebuild and check that dependency interfaces are not rebuilt changeDoc doc [] - waitForTypecheck doc + _ <- waitForTypecheck doc Right keys <- getLastBuildKeys let ifaceKeys = filter ("GetModIface" `T.isPrefixOf`) keys liftIO $ ifaceKeys @?= [] ] + where + knownBrokenInWindowsBeforeGHC912 msg = + foldl (.) id + [ knownBrokenInSpecificEnv [GhcVer ghcVer, HostOS Windows] msg + | ghcVer <- [GHC96 .. GHC910] + ] goldenWithEval :: TestName -> FilePath -> FilePath -> TestTree goldenWithEval title path ext = goldenWithHaskellDocInTmpDir def evalPlugin title (mkFs $ FS.directProject (path <.> ext)) path "expected" ext executeLensesBackwards +goldenWithEvalForCodeAction :: TestName -> FilePath -> FilePath -> TestTree +goldenWithEvalForCodeAction title path ext = + goldenWithHaskellDocInTmpDir def evalPlugin title (mkFs $ FS.directProject (path <.> ext)) path "expected" ext executeCodeActionsBackwards + goldenWithEvalAndFs :: TestName -> [FS.FileTree] -> FilePath -> FilePath -> TestTree goldenWithEvalAndFs title tree path ext = goldenWithHaskellDocInTmpDir def evalPlugin title (mkFs tree) path "expected" ext executeLensesBackwards @@ -252,14 +246,24 @@ goldenWithEvalAndFs' title tree path ext expected = -- | Execute lenses backwards, to avoid affecting their position in the source file executeLensesBackwards :: TextDocumentIdentifier -> Session () executeLensesBackwards doc = do - codeLenses <- reverse <$> getCodeLenses doc + codeLenses <- getCodeLenses doc -- liftIO $ print codeLenses + executeCmdsBackwards [c | CodeLens{_command = Just c} <- codeLenses] + +executeCodeActionsBackwards :: TextDocumentIdentifier -> Session () +executeCodeActionsBackwards doc = do + codeLenses <- getCodeLenses doc + let ranges = [_range | CodeLens{_range} <- codeLenses] + -- getAllCodeActions cannot get our code actions because they have no diagnostics + codeActions <- join <$> traverse (getCodeActions doc) ranges + let cmds = Maybe.mapMaybe (^? _L) codeActions + executeCmdsBackwards cmds - -- Execute sequentially, nubbing elements to avoid - -- evaluating the same section with multiple tests - -- more than twice - mapM_ executeCmd $ - nubOrdOn actSectionId [c | CodeLens{_command = Just c} <- codeLenses] +-- Execute commands backwards, nubbing elements to avoid +-- evaluating the same section with multiple tests +-- more than twice +executeCmdsBackwards :: [Command] -> Session () +executeCmdsBackwards = mapM_ executeCmd . nubOrdOn actSectionId . reverse actSectionId :: Command -> Int actSectionId Command{_arguments = Just [fromJSON -> Success EvalParams{..}]} = evalId @@ -288,7 +292,7 @@ codeLensTestOutput codeLens = do testOutput =<< sectionTests testDataDir :: FilePath -testDataDir = "test" "testdata" +testDataDir = "plugins" "hls-eval-plugin" "test" "testdata" changeConfig :: [Pair] -> Config changeConfig conf = @@ -316,7 +320,7 @@ evalInFile fp e expected = runSessionWithServerInTmpDir def evalPlugin (mkFs $ F doc <- openDoc fp "haskell" origin <- documentContents doc let withEval = origin <> e - changeDoc doc [TextDocumentContentChangeEvent . InR . (.==) #text $ withEval] + changeDoc doc [TextDocumentContentChangeEvent . InR . TextDocumentContentChangeWholeDocument $ withEval] executeLensesBackwards doc result <- fmap T.strip . T.stripPrefix withEval <$> documentContents doc liftIO $ result @?= Just (T.strip expected) diff --git a/plugins/hls-eval-plugin/test/cabal.project b/plugins/hls-eval-plugin/test/cabal.project index f0e29ace6b..3fae89fe02 100644 --- a/plugins/hls-eval-plugin/test/cabal.project +++ b/plugins/hls-eval-plugin/test/cabal.project @@ -1,3 +1,3 @@ packages: testdata/ - info-util/ + testdata/info-util/ diff --git a/plugins/hls-eval-plugin/test/testdata/T10.expected.hs b/plugins/hls-eval-plugin/test/testdata/T10.expected.hs index 2c50750981..776c970591 100644 --- a/plugins/hls-eval-plugin/test/testdata/T10.expected.hs +++ b/plugins/hls-eval-plugin/test/testdata/T10.expected.hs @@ -7,5 +7,5 @@ type Dummy = 1 + 1 -- >>> type N = 1 -- >>> type M = 40 -- >>> :kind! N + M + 1 --- N + M + 1 :: Nat +-- N + M + 1 :: Natural -- = 42 diff --git a/plugins/hls-eval-plugin/test/testdata/T10.ghc92.expected b/plugins/hls-eval-plugin/test/testdata/T10.ghc92.expected deleted file mode 100644 index 776c970591..0000000000 --- a/plugins/hls-eval-plugin/test/testdata/T10.ghc92.expected +++ /dev/null @@ -1,11 +0,0 @@ -{-# LANGUAGE DataKinds, TypeOperators #-} -module T10 where -import GHC.TypeNats ( type (+) ) - -type Dummy = 1 + 1 - --- >>> type N = 1 --- >>> type M = 40 --- >>> :kind! N + M + 1 --- N + M + 1 :: Natural --- = 42 diff --git a/plugins/hls-eval-plugin/test/testdata/T10.ghc92.expected.hs b/plugins/hls-eval-plugin/test/testdata/T10.ghc92.expected.hs deleted file mode 100644 index 776c970591..0000000000 --- a/plugins/hls-eval-plugin/test/testdata/T10.ghc92.expected.hs +++ /dev/null @@ -1,11 +0,0 @@ -{-# LANGUAGE DataKinds, TypeOperators #-} -module T10 where -import GHC.TypeNats ( type (+) ) - -type Dummy = 1 + 1 - --- >>> type N = 1 --- >>> type M = 40 --- >>> :kind! N + M + 1 --- N + M + 1 :: Natural --- = 42 diff --git a/plugins/hls-eval-plugin/test/testdata/T11.expected.hs b/plugins/hls-eval-plugin/test/testdata/T11.expected.hs index eb472f9002..63d0ed8a07 100644 --- a/plugins/hls-eval-plugin/test/testdata/T11.expected.hs +++ b/plugins/hls-eval-plugin/test/testdata/T11.expected.hs @@ -1,4 +1,4 @@ module T11 where -- >>> :kind! A --- Not in scope: type constructor or class ‘A’ +-- Not in scope: type constructor or class `A' diff --git a/plugins/hls-eval-plugin/test/testdata/T11.ghc94.expected.hs b/plugins/hls-eval-plugin/test/testdata/T11.ghc94.expected.hs deleted file mode 100644 index 63d0ed8a07..0000000000 --- a/plugins/hls-eval-plugin/test/testdata/T11.ghc94.expected.hs +++ /dev/null @@ -1,4 +0,0 @@ -module T11 where - --- >>> :kind! A --- Not in scope: type constructor or class `A' diff --git a/plugins/hls-eval-plugin/test/testdata/T12.expected.hs b/plugins/hls-eval-plugin/test/testdata/T12.expected.hs index 81bf5c30c2..4f0dd67b82 100644 --- a/plugins/hls-eval-plugin/test/testdata/T12.expected.hs +++ b/plugins/hls-eval-plugin/test/testdata/T12.expected.hs @@ -7,4 +7,4 @@ type Dummy = 1 + 1 -- >>> type N = 1 -- >>> type M = 40 -- >>> :kind N + M + 1 --- N + M + 1 :: Nat +-- N + M + 1 :: Natural diff --git a/plugins/hls-eval-plugin/test/testdata/T12.ghc92.expected.hs b/plugins/hls-eval-plugin/test/testdata/T12.ghc92.expected.hs deleted file mode 100644 index 4f0dd67b82..0000000000 --- a/plugins/hls-eval-plugin/test/testdata/T12.ghc92.expected.hs +++ /dev/null @@ -1,10 +0,0 @@ -{-# LANGUAGE DataKinds, TypeOperators #-} -module T12 where -import GHC.TypeNats ( type (+) ) - -type Dummy = 1 + 1 - --- >>> type N = 1 --- >>> type M = 40 --- >>> :kind N + M + 1 --- N + M + 1 :: Natural diff --git a/plugins/hls-eval-plugin/test/testdata/T12.ghc92_expected.hs b/plugins/hls-eval-plugin/test/testdata/T12.ghc92_expected.hs deleted file mode 100644 index 4f0dd67b82..0000000000 --- a/plugins/hls-eval-plugin/test/testdata/T12.ghc92_expected.hs +++ /dev/null @@ -1,10 +0,0 @@ -{-# LANGUAGE DataKinds, TypeOperators #-} -module T12 where -import GHC.TypeNats ( type (+) ) - -type Dummy = 1 + 1 - --- >>> type N = 1 --- >>> type M = 40 --- >>> :kind N + M + 1 --- N + M + 1 :: Natural diff --git a/plugins/hls-eval-plugin/test/testdata/T13.expected.hs b/plugins/hls-eval-plugin/test/testdata/T13.expected.hs index 60d6787d55..60a75bdfdd 100644 --- a/plugins/hls-eval-plugin/test/testdata/T13.expected.hs +++ b/plugins/hls-eval-plugin/test/testdata/T13.expected.hs @@ -1,4 +1,4 @@ module T13 where -- >>> :kind A --- Not in scope: type constructor or class ‘A’ +-- Not in scope: type constructor or class `A' diff --git a/plugins/hls-eval-plugin/test/testdata/T13.ghc92.expected.hs b/plugins/hls-eval-plugin/test/testdata/T13.ghc92.expected.hs deleted file mode 100644 index 60a75bdfdd..0000000000 --- a/plugins/hls-eval-plugin/test/testdata/T13.ghc92.expected.hs +++ /dev/null @@ -1,4 +0,0 @@ -module T13 where - --- >>> :kind A --- Not in scope: type constructor or class `A' diff --git a/plugins/hls-eval-plugin/test/testdata/T13.ghc92_expected.hs b/plugins/hls-eval-plugin/test/testdata/T13.ghc92_expected.hs deleted file mode 100644 index f5a6d1655f..0000000000 --- a/plugins/hls-eval-plugin/test/testdata/T13.ghc92_expected.hs +++ /dev/null @@ -1,4 +0,0 @@ -module T13 where - --- >>> :kind a --- Not in scope: type variable `a' diff --git a/plugins/hls-eval-plugin/test/testdata/T15.ghc92_expected.hs b/plugins/hls-eval-plugin/test/testdata/T14.ghc98.expected.hs similarity index 72% rename from plugins/hls-eval-plugin/test/testdata/T15.ghc92_expected.hs rename to plugins/hls-eval-plugin/test/testdata/T14.ghc98.expected.hs index 54f0f38ef5..61ee830fa1 100644 --- a/plugins/hls-eval-plugin/test/testdata/T15.ghc92_expected.hs +++ b/plugins/hls-eval-plugin/test/testdata/T14.ghc98.expected.hs @@ -1,8 +1,8 @@ {-# LANGUAGE TypeApplications #-} -module T15 where +module T14 where foo :: Show a => a -> String foo = show --- >>> :type +v foo @Int +-- >>> :type foo @Int -- foo @Int :: Show Int => Int -> String diff --git a/plugins/hls-eval-plugin/test/testdata/T15.expected.hs b/plugins/hls-eval-plugin/test/testdata/T15.expected.hs deleted file mode 100644 index 54f0f38ef5..0000000000 --- a/plugins/hls-eval-plugin/test/testdata/T15.expected.hs +++ /dev/null @@ -1,8 +0,0 @@ -{-# LANGUAGE TypeApplications #-} -module T15 where - -foo :: Show a => a -> String -foo = show - --- >>> :type +v foo @Int --- foo @Int :: Show Int => Int -> String diff --git a/plugins/hls-eval-plugin/test/testdata/T15.hs b/plugins/hls-eval-plugin/test/testdata/T15.hs deleted file mode 100644 index 684333fbbd..0000000000 --- a/plugins/hls-eval-plugin/test/testdata/T15.hs +++ /dev/null @@ -1,7 +0,0 @@ -{-# LANGUAGE TypeApplications #-} -module T15 where - -foo :: Show a => a -> String -foo = show - --- >>> :type +v foo @Int diff --git a/plugins/hls-eval-plugin/test/testdata/T17.expected.hs b/plugins/hls-eval-plugin/test/testdata/T17.expected.hs index 14e2aa74a1..caf06a9fee 100644 --- a/plugins/hls-eval-plugin/test/testdata/T17.expected.hs +++ b/plugins/hls-eval-plugin/test/testdata/T17.expected.hs @@ -1,4 +1,4 @@ module T17 where -- >>> :type +no 42 --- parse error on input ‘+’ +-- parse error on input `+' diff --git a/plugins/hls-eval-plugin/test/testdata/T17.ghc92.expected.hs b/plugins/hls-eval-plugin/test/testdata/T17.ghc92.expected.hs deleted file mode 100644 index caf06a9fee..0000000000 --- a/plugins/hls-eval-plugin/test/testdata/T17.ghc92.expected.hs +++ /dev/null @@ -1,4 +0,0 @@ -module T17 where - --- >>> :type +no 42 --- parse error on input `+' diff --git a/plugins/hls-eval-plugin/test/testdata/T17.ghc92_expected.hs b/plugins/hls-eval-plugin/test/testdata/T17.ghc92_expected.hs deleted file mode 100644 index 14e2aa74a1..0000000000 --- a/plugins/hls-eval-plugin/test/testdata/T17.ghc92_expected.hs +++ /dev/null @@ -1,4 +0,0 @@ -module T17 where - --- >>> :type +no 42 --- parse error on input ‘+’ diff --git a/plugins/hls-eval-plugin/test/testdata/T20.expected.hs b/plugins/hls-eval-plugin/test/testdata/T20.expected.hs index 18d2155560..36c93b99c1 100644 --- a/plugins/hls-eval-plugin/test/testdata/T20.expected.hs +++ b/plugins/hls-eval-plugin/test/testdata/T20.expected.hs @@ -4,4 +4,4 @@ import Data.Word (Word) default (Word) -- >>> :type +d 40+ 2 --- 40+ 2 :: Word +-- 40+ 2 :: Integer diff --git a/plugins/hls-eval-plugin/test/testdata/T20.ghc92.expected.hs b/plugins/hls-eval-plugin/test/testdata/T20.ghc92.expected.hs deleted file mode 100644 index 18d2155560..0000000000 --- a/plugins/hls-eval-plugin/test/testdata/T20.ghc92.expected.hs +++ /dev/null @@ -1,7 +0,0 @@ -module T20 where -import Data.Word (Word) - -default (Word) - --- >>> :type +d 40+ 2 --- 40+ 2 :: Word diff --git a/plugins/hls-eval-plugin/test/testdata/T20.ghc92_expected.hs b/plugins/hls-eval-plugin/test/testdata/T20.ghc92_expected.hs deleted file mode 100644 index 36c93b99c1..0000000000 --- a/plugins/hls-eval-plugin/test/testdata/T20.ghc92_expected.hs +++ /dev/null @@ -1,7 +0,0 @@ -module T20 where -import Data.Word (Word) - -default (Word) - --- >>> :type +d 40+ 2 --- 40+ 2 :: Integer diff --git a/plugins/hls-eval-plugin/test/testdata/T28.expected.hs b/plugins/hls-eval-plugin/test/testdata/T28.expected.hs new file mode 100644 index 0000000000..74ecea6e75 --- /dev/null +++ b/plugins/hls-eval-plugin/test/testdata/T28.expected.hs @@ -0,0 +1,7 @@ +module T28 where + +f True = True +f False = False + +-- >>> 1+1 +-- 2 diff --git a/plugins/hls-eval-plugin/test/testdata/T28.hs b/plugins/hls-eval-plugin/test/testdata/T28.hs new file mode 100644 index 0000000000..e72910c4c2 --- /dev/null +++ b/plugins/hls-eval-plugin/test/testdata/T28.hs @@ -0,0 +1,6 @@ +module T28 where + +f True = True +f False = False + +-- >>> 1+1 diff --git a/plugins/hls-eval-plugin/test/testdata/T4139.expected.hs b/plugins/hls-eval-plugin/test/testdata/T4139.expected.hs new file mode 100644 index 0000000000..ade8332a32 --- /dev/null +++ b/plugins/hls-eval-plugin/test/testdata/T4139.expected.hs @@ -0,0 +1,7 @@ +module T4139 where + +-- >>> 'x' +-- 'x' + +main :: IO () +main = putStrLn "Hello World!" diff --git a/plugins/hls-eval-plugin/test/testdata/T4139.hs b/plugins/hls-eval-plugin/test/testdata/T4139.hs new file mode 100644 index 0000000000..855d6ef08b --- /dev/null +++ b/plugins/hls-eval-plugin/test/testdata/T4139.hs @@ -0,0 +1,6 @@ +module T4139 where + +-- >>> 'x' + +main :: IO () +main = putStrLn "Hello World!" diff --git a/plugins/hls-eval-plugin/test/testdata/TFlags.expected.hs b/plugins/hls-eval-plugin/test/testdata/TFlags.expected.hs index 8bf91c7118..2c8e0ef92a 100644 --- a/plugins/hls-eval-plugin/test/testdata/TFlags.expected.hs +++ b/plugins/hls-eval-plugin/test/testdata/TFlags.expected.hs @@ -20,8 +20,9 @@ module TFlags where Options apply only in the section where they are defined (unless they are in the setup section), so this will fail: >>> class L a b c -Too many parameters for class ‘L’ +Too many parameters for class `L' (Enable MultiParamTypeClasses to allow multi-parameter classes) +In the class declaration for `L' -} @@ -31,8 +32,9 @@ Options apply to all tests in the same section after their declaration. Not set yet: >>> class D -No parameters for class ‘D’ +No parameters for class `D' (Enable MultiParamTypeClasses to allow no-parameter classes) +In the class declaration for `D' Now it works: diff --git a/plugins/hls-eval-plugin/test/testdata/TFlags.ghc92.expected.hs b/plugins/hls-eval-plugin/test/testdata/TFlags.ghc98.expected.hs similarity index 89% rename from plugins/hls-eval-plugin/test/testdata/TFlags.ghc92.expected.hs rename to plugins/hls-eval-plugin/test/testdata/TFlags.ghc98.expected.hs index 2c8e0ef92a..2e4de4c0b7 100644 --- a/plugins/hls-eval-plugin/test/testdata/TFlags.ghc92.expected.hs +++ b/plugins/hls-eval-plugin/test/testdata/TFlags.ghc98.expected.hs @@ -21,7 +21,6 @@ Options apply only in the section where they are defined (unless they are in the >>> class L a b c Too many parameters for class `L' -(Enable MultiParamTypeClasses to allow multi-parameter classes) In the class declaration for `L' -} @@ -33,7 +32,6 @@ Not set yet: >>> class D No parameters for class `D' -(Enable MultiParamTypeClasses to allow no-parameter classes) In the class declaration for `D' Now it works: @@ -57,7 +55,7 @@ It still works {- Invalid option/flags are reported, but valid ones will be reflected >>> :set -XRank2Types -XAbsent -XDatatypeContexts -XWrong -fprint-nothing-at-all -: warning: +: warning: [GHC-53692] [-Wdeprecated-flags (in -Wdefault)] -XDatatypeContexts is deprecated: It was widely considered a misfeature, and has been removed from the Haskell language. Some flags have not been recognized: -XAbsent, -XWrong, -fprint-nothing-at-all diff --git a/plugins/hls-eval-plugin/test/testdata/TIO.expected.hs b/plugins/hls-eval-plugin/test/testdata/TIO.expected.hs index 7f984892df..016780bca7 100644 --- a/plugins/hls-eval-plugin/test/testdata/TIO.expected.hs +++ b/plugins/hls-eval-plugin/test/testdata/TIO.expected.hs @@ -1,9 +1,12 @@ -- IO expressions are supported, stdout/stderr output is ignored module TIO where +import Control.Concurrent (threadDelay) + {- Does not capture stdout, returns value. +Has a delay in order to show progress reporting. ->>> print "ABC" >> return "XYZ" +>>> threadDelay 2000000 >> print "ABC" >> return "XYZ" "XYZ" -} diff --git a/plugins/hls-eval-plugin/test/testdata/TIO.hs b/plugins/hls-eval-plugin/test/testdata/TIO.hs index 7f984892df..016780bca7 100644 --- a/plugins/hls-eval-plugin/test/testdata/TIO.hs +++ b/plugins/hls-eval-plugin/test/testdata/TIO.hs @@ -1,9 +1,12 @@ -- IO expressions are supported, stdout/stderr output is ignored module TIO where +import Control.Concurrent (threadDelay) + {- Does not capture stdout, returns value. +Has a delay in order to show progress reporting. ->>> print "ABC" >> return "XYZ" +>>> threadDelay 2000000 >> print "ABC" >> return "XYZ" "XYZ" -} diff --git a/plugins/hls-eval-plugin/test/testdata/TPropertyError.ghc910.expected.hs b/plugins/hls-eval-plugin/test/testdata/TPropertyError.ghc910.expected.hs new file mode 100644 index 0000000000..87fbda03f8 --- /dev/null +++ b/plugins/hls-eval-plugin/test/testdata/TPropertyError.ghc910.expected.hs @@ -0,0 +1,17 @@ +-- Support for property checking +module TProperty where + +-- prop> \(l::[Bool]) -> head l +-- *** Failed! (after 1 test): +-- Exception: +-- Prelude.head: empty list +-- CallStack (from HasCallStack): +-- error, called at libraries/ghc-internal/src/GHC/Internal/List.hs:2030:3 in ghc-internal:GHC.Internal.List +-- errorEmptyList, called at libraries/ghc-internal/src/GHC/Internal/List.hs:96:11 in ghc-internal:GHC.Internal.List +-- badHead, called at libraries/ghc-internal/src/GHC/Internal/List.hs:90:28 in ghc-internal:GHC.Internal.List +-- head, called at :1:27 in interactive:Ghci2 +-- HasCallStack backtrace: +-- collectBacktraces, called at libraries/ghc-internal/src/GHC/Internal/Exception.hs:92:13 in ghc-internal:GHC.Internal.Exception +-- toExceptionWithBacktrace, called at libraries/ghc-internal/src/GHC/Internal/Exception.hs:128:3 in ghc-internal:GHC.Internal.Exception +-- +-- [] diff --git a/plugins/hls-eval-plugin/test/testdata/TPropertyError.expected.hs b/plugins/hls-eval-plugin/test/testdata/TPropertyError.ghc912.expected.hs similarity index 100% rename from plugins/hls-eval-plugin/test/testdata/TPropertyError.expected.hs rename to plugins/hls-eval-plugin/test/testdata/TPropertyError.ghc912.expected.hs diff --git a/plugins/hls-eval-plugin/test/testdata/TPropertyError.windows-ghc94.expected.hs b/plugins/hls-eval-plugin/test/testdata/TPropertyError.ghc96.expected.hs similarity index 61% rename from plugins/hls-eval-plugin/test/testdata/TPropertyError.windows-ghc94.expected.hs rename to plugins/hls-eval-plugin/test/testdata/TPropertyError.ghc96.expected.hs index 6c7813d776..a90fd16600 100644 --- a/plugins/hls-eval-plugin/test/testdata/TPropertyError.windows-ghc94.expected.hs +++ b/plugins/hls-eval-plugin/test/testdata/TPropertyError.ghc96.expected.hs @@ -6,8 +6,8 @@ module TProperty where -- Exception: -- Prelude.head: empty list -- CallStack (from HasCallStack): --- error, called at libraries\base\GHC\List.hs:1646:3 in base:GHC.List --- errorEmptyList, called at libraries\base\GHC\List.hs:85:11 in base:GHC.List --- badHead, called at libraries\base\GHC\List.hs:81:28 in base:GHC.List +-- error, called at libraries/base/GHC/List.hs:1644:3 in base:GHC.List +-- errorEmptyList, called at libraries/base/GHC/List.hs:87:11 in base:GHC.List +-- badHead, called at libraries/base/GHC/List.hs:83:28 in base:GHC.List -- head, called at :1:27 in interactive:Ghci2 -- [] diff --git a/plugins/hls-eval-plugin/test/testdata/TPropertyError.ghc98.expected.hs b/plugins/hls-eval-plugin/test/testdata/TPropertyError.ghc98.expected.hs new file mode 100644 index 0000000000..55b606f0cb --- /dev/null +++ b/plugins/hls-eval-plugin/test/testdata/TPropertyError.ghc98.expected.hs @@ -0,0 +1,13 @@ +-- Support for property checking +module TProperty where + +-- prop> \(l::[Bool]) -> head l +-- *** Failed! (after 1 test): +-- Exception: +-- Prelude.head: empty list +-- CallStack (from HasCallStack): +-- error, called at libraries/base/GHC/List.hs:2004:3 in base:GHC.List +-- errorEmptyList, called at libraries/base/GHC/List.hs:90:11 in base:GHC.List +-- badHead, called at libraries/base/GHC/List.hs:84:28 in base:GHC.List +-- head, called at :1:27 in interactive:Ghci2 +-- [] diff --git a/plugins/hls-explicit-fixity-plugin/LICENSE b/plugins/hls-explicit-fixity-plugin/LICENSE deleted file mode 100644 index 261eeb9e9f..0000000000 --- a/plugins/hls-explicit-fixity-plugin/LICENSE +++ /dev/null @@ -1,201 +0,0 @@ - Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ - - TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - - 1. Definitions. - - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. - - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. - - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. - - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. - - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. - - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. - - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). - - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. - - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution." - - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. - - 2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. - - 3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. - - 4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: - - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and - - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and - - (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. - - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. - - 5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. - - 6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. - - 7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. - - 8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. - - 9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. - - END OF TERMS AND CONDITIONS - - APPENDIX: How to apply the Apache License to your work. - - To apply the Apache License to your work, attach the following - boilerplate notice, with the fields enclosed by brackets "[]" - replaced with your own identifying information. (Don't include - the brackets!) The text should be enclosed in the appropriate - comment syntax for the file format. We also recommend that a - file or class name and description of purpose be included on the - same "printed page" as the copyright notice for easier - identification within third-party archives. - - Copyright [yyyy] [name of copyright owner] - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. diff --git a/plugins/hls-explicit-fixity-plugin/hls-explicit-fixity-plugin.cabal b/plugins/hls-explicit-fixity-plugin/hls-explicit-fixity-plugin.cabal deleted file mode 100644 index 24fb5f1806..0000000000 --- a/plugins/hls-explicit-fixity-plugin/hls-explicit-fixity-plugin.cabal +++ /dev/null @@ -1,57 +0,0 @@ -cabal-version: 2.4 -name: hls-explicit-fixity-plugin -version: 2.4.0.0 -synopsis: Show fixity explicitly while hovering -description: - Please see the README on GitHub at - -license: Apache-2.0 -license-file: LICENSE -author: Lei Zhu -maintainer: julytreee@gmail.com -category: Development -build-type: Simple -extra-source-files: - LICENSE - test/testdata/*.hs - -source-repository head - type: git - location: https://github.com/haskell/haskell-language-server.git - -library - exposed-modules: Ide.Plugin.ExplicitFixity - - hs-source-dirs: src - build-depends: - base >=4.12 && <5 - , containers - , deepseq - , extra - , ghc - , ghcide == 2.4.0.0 - , hashable - , hls-plugin-api == 2.4.0.0 - , lsp >=2.3 - , text - , transformers - - ghc-options: - -Wall - -Wno-name-shadowing - -Wno-unticked-promoted-constructors - default-language: Haskell2010 - default-extensions: DataKinds - -test-suite tests - type: exitcode-stdio-1.0 - default-language: Haskell2010 - hs-source-dirs: test - main-is: Main.hs - ghc-options: -threaded -rtsopts -with-rtsopts=-N - build-depends: - , base - , filepath - , hls-explicit-fixity-plugin - , hls-test-utils == 2.4.0.0 - , text diff --git a/plugins/hls-explicit-fixity-plugin/src/Ide/Plugin/ExplicitFixity.hs b/plugins/hls-explicit-fixity-plugin/src/Ide/Plugin/ExplicitFixity.hs index e3b68164f5..92bc37f743 100644 --- a/plugins/hls-explicit-fixity-plugin/src/Ide/Plugin/ExplicitFixity.hs +++ b/plugins/hls-explicit-fixity-plugin/src/Ide/Plugin/ExplicitFixity.hs @@ -1,8 +1,7 @@ -{-# LANGUAGE DeriveGeneric #-} +{-# LANGUAGE CPP #-} +{-# LANGUAGE DataKinds #-} {-# LANGUAGE LambdaCase #-} -{-# LANGUAGE NamedFieldPuns #-} {-# LANGUAGE OverloadedStrings #-} -{-# LANGUAGE TupleSections #-} {-# LANGUAGE TypeFamilies #-} {-# OPTIONS_GHC -Wno-orphans #-} {-# OPTIONS_GHC -Wno-incomplete-uni-patterns #-} @@ -34,7 +33,7 @@ import Language.LSP.Protocol.Message import Language.LSP.Protocol.Types descriptor :: Recorder (WithPriority Log) -> PluginId -> PluginDescriptor IdeState -descriptor recorder pluginId = (defaultPluginDescriptor pluginId) +descriptor recorder pluginId = (defaultPluginDescriptor pluginId "Provides fixity information in hovers") { pluginRules = fixityRule recorder , pluginHandlers = mkPluginHandler SMethod_TextDocumentHover hover -- Make this plugin has a lower priority than ghcide's plugin to ensure @@ -62,7 +61,11 @@ hover state _ (HoverParams (TextDocumentIdentifier uri) pos _) = do in Just $ Hover (InL (mkPlainText contents')) Nothing fixityText :: (Name, Fixity) -> T.Text +#if MIN_VERSION_GLASGOW_HASKELL(9,12,0,0) + fixityText (name, Fixity precedence direction) = +#else fixityText (name, Fixity _ precedence direction) = +#endif printOutputable direction <> " " <> printOutputable precedence <> " `" <> printOutputable name <> "`" newtype FixityMap = FixityMap (M.Map Name Fixity) diff --git a/plugins/hls-explicit-fixity-plugin/test/Main.hs b/plugins/hls-explicit-fixity-plugin/test/Main.hs index 13dca58f8d..26e94091cd 100644 --- a/plugins/hls-explicit-fixity-plugin/test/Main.hs +++ b/plugins/hls-explicit-fixity-plugin/test/Main.hs @@ -1,3 +1,4 @@ +{-# LANGUAGE DataKinds #-} {-# LANGUAGE OverloadedStrings #-} module Main where @@ -40,27 +41,39 @@ tests = testGroup "Explicit fixity" , hoverTest "signature" (Position 35 2) "infixr 9 `>>>:`" , hoverTest "operator" (Position 36 2) "infixr 9 `>>>:`" , hoverTest "escape" (Position 39 2) "infixl 3 `~\\:`" - -- Ensure that there is no one extra new line in import statement - , expectFail $ hoverTest "import" (Position 2 18) "Control.Monad***" - -- Known issue, See https://github.com/haskell/haskell-language-server/pull/2973/files#r916535742 - , expectFail $ hoverTestImport "import" (Position 4 7) "infixr 9 `>>>:`" + -- TODO: Ensure that there is no one extra new line in import statement + , hoverTestExpectFail + "import" + (Position 2 18) + (BrokenIdeal "Control.Monad***") + (BrokenCurrent "Control.Monad\n\n") + , hoverTestImport "import" (Position 4 7) "infixr 9 `>>>:`" ] hoverTest :: TestName -> Position -> T.Text -> TestTree hoverTest = hoverTest' "Hover.hs" + hoverTestImport :: TestName -> Position -> T.Text -> TestTree hoverTestImport = hoverTest' "HoverImport.hs" +hoverTestExpectFail + :: TestName + -> Position + -> ExpectBroken 'Ideal T.Text + -> ExpectBroken 'Current T.Text + -> TestTree +hoverTestExpectFail title pos _ = + hoverTest title pos . unCurrent + hoverTest' :: String -> TestName -> Position -> T.Text -> TestTree hoverTest' docName title pos expected = testCase title $ runSessionWithServer def plugin testDataDir $ do doc <- openDoc docName "haskell" waitForKickDone h <- getHover doc pos - let expected' = "\n" <> sectionSeparator <> expected case h of Nothing -> liftIO $ assertFailure "No hover" Just (Hover contents _) -> case contents of - InL (MarkupContent mk txt) -> do + InL (MarkupContent _ txt) -> do liftIO $ assertBool ("Failed to find `" <> T.unpack expected <> "` in hover message: " <> T.unpack txt) $ expected `T.isInfixOf` txt @@ -68,4 +81,4 @@ hoverTest' docName title pos expected = testCase title $ runSessionWithServer de closeDoc doc testDataDir :: FilePath -testDataDir = "test" "testdata" +testDataDir = "plugins" "hls-explicit-fixity-plugin" "test" "testdata" diff --git a/plugins/hls-explicit-imports-plugin/LICENSE b/plugins/hls-explicit-imports-plugin/LICENSE deleted file mode 100644 index 261eeb9e9f..0000000000 --- a/plugins/hls-explicit-imports-plugin/LICENSE +++ /dev/null @@ -1,201 +0,0 @@ - Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ - - TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - - 1. Definitions. - - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. - - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. - - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. - - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. - - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. - - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. - - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). - - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. - - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution." - - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. - - 2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. - - 3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. - - 4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: - - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and - - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and - - (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. - - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. - - 5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. - - 6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. - - 7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. - - 8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. - - 9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. - - END OF TERMS AND CONDITIONS - - APPENDIX: How to apply the Apache License to your work. - - To apply the Apache License to your work, attach the following - boilerplate notice, with the fields enclosed by brackets "[]" - replaced with your own identifying information. (Don't include - the brackets!) The text should be enclosed in the appropriate - comment syntax for the file format. We also recommend that a - file or class name and description of purpose be included on the - same "printed page" as the copyright notice for easier - identification within third-party archives. - - Copyright [yyyy] [name of copyright owner] - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. diff --git a/plugins/hls-explicit-imports-plugin/hls-explicit-imports-plugin.cabal b/plugins/hls-explicit-imports-plugin/hls-explicit-imports-plugin.cabal deleted file mode 100644 index 77a3b796e3..0000000000 --- a/plugins/hls-explicit-imports-plugin/hls-explicit-imports-plugin.cabal +++ /dev/null @@ -1,74 +0,0 @@ -cabal-version: 2.2 -name: hls-explicit-imports-plugin -version: 2.4.0.0 -synopsis: Explicit imports plugin for Haskell Language Server -description: - Please see the README on GitHub at -license: Apache-2.0 -license-file: LICENSE -author: Pepe Iborra -maintainer: pepeiborra@gmail.com -category: Development -build-type: Simple -extra-source-files: - LICENSE - test/testdata/*.hs - test/testdata/*.yaml - -source-repository head - type: git - location: https://github.com/haskell/haskell-language-server.git - -flag pedantic - description: Enable -Werror - default: False - manual: True - -common warnings - ghc-options: -Wall - -library - import: warnings - exposed-modules: Ide.Plugin.ExplicitImports - hs-source-dirs: src - build-depends: - , aeson - , base >=4.12 && <5 - , containers - , deepseq - , ghc - , ghcide == 2.4.0.0 - , hls-graph - , hls-plugin-api == 2.4.0.0 - , lens - , lsp - , mtl - , text - , transformers - , unordered-containers - - default-language: Haskell2010 - default-extensions: - DataKinds - TypeOperators - - if flag(pedantic) - ghc-options: -Werror - -test-suite tests - import: warnings - type: exitcode-stdio-1.0 - default-language: Haskell2010 - hs-source-dirs: test - main-is: Main.hs - ghc-options: -threaded -rtsopts -with-rtsopts=-N - build-depends: - , base - , extra - , filepath - , hls-explicit-imports-plugin - , hls-test-utils - , lens - , lsp-types - , row-types - , text diff --git a/plugins/hls-explicit-imports-plugin/src/Ide/Plugin/ExplicitImports.hs b/plugins/hls-explicit-imports-plugin/src/Ide/Plugin/ExplicitImports.hs index 28cb8e1ec0..17634491fe 100644 --- a/plugins/hls-explicit-imports-plugin/src/Ide/Plugin/ExplicitImports.hs +++ b/plugins/hls-explicit-imports-plugin/src/Ide/Plugin/ExplicitImports.hs @@ -1,23 +1,22 @@ -{-# LANGUAGE CPP #-} -{-# LANGUAGE DeriveAnyClass #-} -{-# LANGUAGE DeriveGeneric #-} -{-# LANGUAGE DerivingStrategies #-} -{-# LANGUAGE ExistentialQuantification #-} -{-# LANGUAGE LambdaCase #-} -{-# LANGUAGE NamedFieldPuns #-} -{-# LANGUAGE OverloadedStrings #-} -{-# LANGUAGE RecordWildCards #-} -{-# LANGUAGE TypeFamilies #-} -{-# LANGUAGE ViewPatterns #-} +{-# LANGUAGE CPP #-} +{-# LANGUAGE DeriveAnyClass #-} +{-# LANGUAGE DerivingStrategies #-} +{-# LANGUAGE LambdaCase #-} +{-# LANGUAGE OverloadedStrings #-} +{-# LANGUAGE RecordWildCards #-} +{-# LANGUAGE TypeFamilies #-} +{-# LANGUAGE ViewPatterns #-} + module Ide.Plugin.ExplicitImports ( descriptor , descriptorForModules , abbreviateImportTitle + , abbreviateImportTitleWithoutModule , Log(..) ) where import Control.DeepSeq -import Control.Lens ((&), (?~)) +import Control.Lens (_Just, (&), (?~), (^?)) import Control.Monad.Error.Class (MonadError (throwError)) import Control.Monad.IO.Class import Control.Monad.Trans.Class (lift) @@ -25,14 +24,18 @@ import Control.Monad.Trans.Except (ExceptT) import Control.Monad.Trans.Maybe import qualified Data.Aeson as A (ToJSON (toJSON)) import Data.Aeson.Types (FromJSON) +import Data.Char (isSpace) import qualified Data.IntMap as IM (IntMap, elems, fromList, (!?)) import Data.IORef (readIORef) +import Data.List (singleton) import qualified Data.Map.Strict as Map -import Data.Maybe (isNothing, mapMaybe) +import Data.Maybe (isJust, isNothing, + mapMaybe) import qualified Data.Set as S import Data.String (fromString) import qualified Data.Text as T +import qualified Data.Text as Text import Data.Traversable (for) import qualified Data.Unique as U (hashUnique, newUnique) @@ -47,15 +50,17 @@ import GHC.Generics (Generic) import Ide.Plugin.Error (PluginError (..), getNormalizedFilePathE, handleMaybe) -import Ide.Plugin.RangeMap (filterByRange) -import qualified Ide.Plugin.RangeMap as RM (RangeMap, fromList) +import qualified Ide.Plugin.RangeMap as RM (RangeMap, + filterByRange, + fromList) import Ide.Plugin.Resolve import Ide.PluginUtils import Ide.Types +import Language.LSP.Protocol.Lens (HasInlayHint (inlayHint), + HasTextDocument (textDocument)) import qualified Language.LSP.Protocol.Lens as L import Language.LSP.Protocol.Message import Language.LSP.Protocol.Types -import Language.LSP.Server -- This plugin is named explicit-imports for historical reasons. Besides -- providing code actions and lenses to make imports explicit it also provides @@ -66,7 +71,7 @@ importCommandId = "ImportLensCommand" data Log = LogShake Shake.Log - | LogWAEResponseError ResponseError + | LogWAEResponseError (TResponseError Method_WorkspaceApplyEdit) | forall a. (Pretty a) => LogResolve a @@ -80,7 +85,7 @@ instance Pretty Log where descriptor :: Recorder (WithPriority Log) -> PluginId -> PluginDescriptor IdeState descriptor recorder = -- (almost) no one wants to see an explicit import list for Prelude - descriptorForModules recorder (/= moduleName pRELUDE) + descriptorForModules recorder (/= pRELUDE_NAME) descriptorForModules :: Recorder (WithPriority Log) @@ -91,7 +96,7 @@ descriptorForModules descriptorForModules recorder modFilter plId = let resolveRecorder = cmapWithPrio LogResolve recorder codeActionHandlers = mkCodeActionHandlerWithResolve resolveRecorder (codeActionProvider recorder) (codeActionResolveProvider recorder) - in (defaultPluginDescriptor plId) + in (defaultPluginDescriptor plId "Provides a code action to make imports explicit") { -- This plugin provides a command handler pluginCommands = [PluginCommand importCommandId "Explicit import command" (runImportCommand recorder)], @@ -101,22 +106,28 @@ descriptorForModules recorder modFilter plId = -- This plugin provides code lenses mkPluginHandler SMethod_TextDocumentCodeLens (lensProvider recorder) <> mkResolveHandler SMethod_CodeLensResolve (lensResolveProvider recorder) - -- This plugin provides code actions + -- This plugin provides inlay hints + <> mkPluginHandler SMethod_TextDocumentInlayHint (inlayHintProvider recorder) + -- This plugin provides code actions <> codeActionHandlers - } +isInlayHintsSupported :: IdeState -> Bool +isInlayHintsSupported ideState = + let clientCaps = Shake.clientCapabilities $ shakeExtras ideState + in isJust $ clientCaps ^? textDocument . _Just . inlayHint . _Just + -- | The actual command handler runImportCommand :: Recorder (WithPriority Log) -> CommandFunction IdeState IAResolveData -runImportCommand recorder ideState eird@(ResolveOne _ _) = do +runImportCommand recorder ideState _ eird@(ResolveOne _ _) = do wedit <- resolveWTextEdit ideState eird - _ <- lift $ sendRequest SMethod_WorkspaceApplyEdit (ApplyWorkspaceEditParams Nothing wedit) logErrors - return $ InR Null - where logErrors (Left re@(ResponseError{})) = do + _ <- lift $ pluginSendRequest SMethod_WorkspaceApplyEdit (ApplyWorkspaceEditParams Nothing wedit) logErrors + return $ InR Null + where logErrors (Left re) = do logWith recorder Error (LogWAEResponseError re) pure () logErrors (Right _) = pure () -runImportCommand _ _ rd = do +runImportCommand _ _ _ rd = do throwError $ PluginInvalidParams (T.pack $ "Unexpected argument for command handler:" <> show rd) @@ -133,12 +144,18 @@ runImportCommand _ _ rd = do -- the provider should produce one code lens associated to the import statement: -- > Refine imports to import Control.Monad.IO.Class (liftIO) lensProvider :: Recorder (WithPriority Log) -> PluginMethodHandler IdeState 'Method_TextDocumentCodeLens -lensProvider _ state _ CodeLensParams {_textDocument = TextDocumentIdentifier {_uri}} = do +lensProvider _ state _ CodeLensParams {_textDocument = TextDocumentIdentifier {_uri}} = do nfp <- getNormalizedFilePathE _uri (ImportActionsResult{forLens}, pm) <- runActionE "ImportActions" state $ useWithStaleE ImportActions nfp let lens = [ generateLens _uri newRange int - | (range, int) <- forLens - , Just newRange <- [toCurrentRange pm range]] + -- provide ExplicitImport only if the client does not support inlay hints + | not (isInlayHintsSupported state) + , (range, (int, ExplicitImport)) <- forLens + , Just newRange <- [toCurrentRange pm range]] <> + -- RefineImport is always provided because inlay hints cannot + [ generateLens _uri newRange int + | (range, (int, RefineImport)) <- forLens + , Just newRange <- [toCurrentRange pm range]] pure $ InL lens where -- because these are non resolved lenses we only need the range and a -- unique id to later resolve them with. These are for both refine @@ -149,12 +166,13 @@ lensProvider _ state _ CodeLensParams {_textDocument = TextDocumentIdentifier { , _range = range , _command = Nothing } + lensResolveProvider :: Recorder (WithPriority Log) -> ResolveFunction IdeState IAResolveData 'Method_CodeLensResolve lensResolveProvider _ ideState plId cl uri rd@(ResolveOne _ uid) = do nfp <- getNormalizedFilePathE uri (ImportActionsResult{forResolve}, _) <- runActionE "ImportActions" ideState $ useWithStaleE ImportActions nfp target <- handleMaybe PluginStaleResolve $ forResolve IM.!? uid - let updatedCodeLens = cl & L.command ?~ mkCommand plId target + let updatedCodeLens = cl & L.command ?~ mkCommand plId target pure updatedCodeLens where mkCommand :: PluginId -> ImportEdit -> Command mkCommand pId (ImportEdit{ieResType, ieText}) = @@ -169,6 +187,55 @@ lensResolveProvider _ ideState plId cl uri rd@(ResolveOne _ uid) = do lensResolveProvider _ _ _ _ _ rd = do throwError $ PluginInvalidParams (T.pack $ "Unexpected argument for lens resolve handler: " <> show rd) + +-- | Provide explicit imports in inlay hints. +-- Applying textEdits can make the import explicit. +-- There is currently no need to resolve inlay hints, +-- as no tooltips or commands are provided in the label. +inlayHintProvider :: Recorder (WithPriority Log) -> PluginMethodHandler IdeState 'Method_TextDocumentInlayHint +inlayHintProvider _ state _ InlayHintParams {_textDocument = TextDocumentIdentifier {_uri}, _range = visibleRange} = + if isInlayHintsSupported state + then do + nfp <- getNormalizedFilePathE _uri + (ImportActionsResult {forLens, forResolve}, pm) <- runActionE "ImportActions" state $ useWithStaleE ImportActions nfp + let inlayHints = [ inlayHint + | (range, (int, _)) <- forLens + , Just newRange <- [toCurrentRange pm range] + , isSubrangeOf newRange visibleRange + , Just ie <- [forResolve IM.!? int] + , Just inlayHint <- [generateInlayHints newRange ie pm]] + pure $ InL inlayHints + -- When the client does not support inlay hints, fallback to the code lens, + -- so there is nothing to response here. + -- `[]` is no different from `null`, we chose to use all `[]` to indicate "no information" + else pure $ InL [] + where + -- The appropriate and intended position for the hint hints to begin + -- is the end of the range for the code lens. + -- import Data.Char (isSpace) + -- |--- range ----|-- IH ---| + -- |^-_paddingLeft + -- ^-_position + generateInlayHints :: Range -> ImportEdit -> PositionMapping -> Maybe InlayHint + generateInlayHints (Range _ end) ie pm = do + label <- mkLabel ie + currentEnd <- toCurrentPosition pm end + return InlayHint { _position = currentEnd + , _label = InL label + , _kind = Nothing -- neither a type nor a parameter + , _textEdits = fmap singleton $ toTEdit pm ie + , _tooltip = Just $ InL "Make this import explicit" -- simple enough, no need to resolve + , _paddingLeft = Just True -- show an extra space before the inlay hint + , _paddingRight = Nothing + , _data_ = Nothing + } + mkLabel :: ImportEdit -> Maybe T.Text + mkLabel (ImportEdit{ieResType, ieText}) = + let title ExplicitImport = Just $ abbreviateImportTitleWithoutModule ieText + title RefineImport = Nothing -- does not provide imports statements that can be refined via inlay hints + in title ieResType + + -- |For explicit imports: If there are any implicit imports, provide both one -- code action per import to make that specific import explicit, and one code -- action to turn them all into explicit imports. For refine imports: If there @@ -179,7 +246,7 @@ codeActionProvider _ ideState _pId (CodeActionParams _ _ TextDocumentIdentifier nfp <- getNormalizedFilePathE _uri (ImportActionsResult{forCodeActions}, pm) <- runActionE "ImportActions" ideState $ useWithStaleE ImportActions nfp newRange <- toCurrentRangeE pm range - let relevantCodeActions = filterByRange newRange forCodeActions + let relevantCodeActions = RM.filterByRange newRange forCodeActions allExplicit = [InR $ mkCodeAction "Make all imports explicit" (Just $ A.toJSON $ ExplicitAll _uri) -- We should only provide this code action if there are any code @@ -215,7 +282,7 @@ codeActionResolveProvider _ ideState _ ca _ rd = do pure $ ca & L.edit ?~ wedit -------------------------------------------------------------------------------- -resolveWTextEdit :: IdeState -> IAResolveData -> ExceptT PluginError (LspT Config IO) WorkspaceEdit +resolveWTextEdit :: IdeState -> IAResolveData -> ExceptT PluginError (HandlerM Config) WorkspaceEdit -- Providing the edit for the command, or the resolve for the code action is -- completely generic, as all we need is the unique id and the text edit. resolveWTextEdit ideState (ResolveOne uri int) = do @@ -235,12 +302,14 @@ resolveWTextEdit ideState (RefineAll uri) = do pure $ mkWorkspaceEdit uri edits pm mkWorkspaceEdit :: Uri -> [ImportEdit] -> PositionMapping -> WorkspaceEdit mkWorkspaceEdit uri edits pm = - WorkspaceEdit {_changes = Just $ Map.fromList [(uri, mapMaybe toWEdit edits)] + WorkspaceEdit {_changes = Just $ Map.singleton uri (mapMaybe (toTEdit pm) edits) , _documentChanges = Nothing , _changeAnnotations = Nothing} - where toWEdit ImportEdit{ieRange, ieText} = - let newRange = toCurrentRange pm ieRange - in (\r -> TextEdit r ieText) <$> newRange + +toTEdit :: PositionMapping -> ImportEdit -> Maybe TextEdit +toTEdit pm ImportEdit{ieRange, ieText} = + let newRange = toCurrentRange pm ieRange + in (\r -> TextEdit r ieText) <$> newRange data ImportActions = ImportActions deriving (Show, Generic, Eq, Ord) @@ -258,7 +327,7 @@ data ImportActionsResult = ImportActionsResult { -- |For providing the code lenses we need to have a range, and a unique id -- that is later resolved to the new text for each import. It is stored in -- a list, because we always need to provide all the code lens in a file. - forLens :: [(Range, Int)] + forLens :: [(Range, (Int, ResultType))] -- |For the code actions we have the same data as for the code lenses, but -- we store it in a RangeMap, because that allows us to filter on a specific -- range with better performance, and code actions are almost always only @@ -335,7 +404,7 @@ minimalImportsRule recorder modFilter = defineNoDiagnostics (cmapWithPrio LogSha -- for every minimal imports | (location, origImport, minImport@(ImportDecl{ideclName = L _ mn})) <- locationImportWithMinimal -- (almost) no one wants to see an refine import list for Prelude - , mn /= moduleName pRELUDE + , mn /= pRELUDE_NAME -- we check for the inner imports , Just innerImports <- [Map.lookup mn import2Map] -- and only get those symbols used @@ -350,7 +419,7 @@ minimalImportsRule recorder modFilter = defineNoDiagnostics (cmapWithPrio LogSha pure (u, rt) let rangeAndUnique = [ ImportAction r u rt | (u, (r, (_, rt))) <- uniqueAndRangeAndText ] pure ImportActionsResult - { forLens = (\ImportAction{..} -> (iaRange, iaUniqueId)) <$> rangeAndUnique + { forLens = (\ImportAction{..} -> (iaRange, (iaUniqueId, iaResType))) <$> rangeAndUnique , forCodeActions = RM.fromList iaRange rangeAndUnique , forResolve = IM.fromList ((\(u, (r, (te, ty))) -> (u, ImportEdit r te ty)) <$> uniqueAndRangeAndText) } @@ -364,7 +433,11 @@ extractMinimalImports :: extractMinimalImports hsc TcModuleResult {..} = runMaybeT $ do -- extract the original imports and the typechecking environment let tcEnv = tmrTypechecked +#if MIN_VERSION_ghc(9,9,0) + (_, imports, _, _, _) = tmrRenamed +#else (_, imports, _, _) = tmrRenamed +#endif ParsedModule {pm_parsed_source = L loc _} = tmrParsed emss = exportedModuleStrings tmrParsed Just srcSpan <- pure $ realSpan loc @@ -399,11 +472,7 @@ extractMinimalImports hsc TcModuleResult {..} = runMaybeT $ do not $ any (\e -> ("module " ++ moduleNameString name) == e) exports isExplicitImport :: ImportDecl GhcRn -> Bool -#if MIN_VERSION_ghc(9,5,0) isExplicitImport ImportDecl {ideclImportList = Just (Exactly, _)} = True -#else -isExplicitImport ImportDecl {ideclHiding = Just (False, _)} = True -#endif isExplicitImport _ = False -- This number is somewhat arbitrarily chosen. Ideally the protocol would tell us these things, @@ -413,8 +482,6 @@ isExplicitImport _ = False maxColumns :: Int maxColumns = 120 - --- | The title of the command is ideally the minimal explicit import decl, but -- we don't want to create a really massive code lens (and the decl can be extremely large!). -- So we abbreviate it to fit a max column size, and indicate how many more items are in the list -- after the abbreviation @@ -422,7 +489,8 @@ abbreviateImportTitle :: T.Text -> T.Text abbreviateImportTitle input = let -- For starters, we only want one line in the title - oneLineText = T.unwords $ T.lines input + -- we also need to compress multiple spaces into one + oneLineText = T.unwords $ filter (not . T.null) $ T.split isSpace input -- Now, split at the max columns, leaving space for the summary text we're going to add -- (conservatively assuming we won't need to print a number larger than 100) (prefix, suffix) = T.splitAt (maxColumns - T.length (summaryText 100)) oneLineText @@ -447,15 +515,16 @@ abbreviateImportTitle input = else actualPrefix <> suffixText in title +-- Create an import abbreviate title without module for inlay hints +abbreviateImportTitleWithoutModule :: Text.Text -> Text.Text +abbreviateImportTitleWithoutModule = abbreviateImportTitle . T.dropWhile (/= '(') + +-- | The title of the command is ideally the minimal explicit import decl, but -------------------------------------------------------------------------------- filterByImport :: ImportDecl GhcRn -> Map.Map ModuleName [AvailInfo] -> Maybe (Map.Map ModuleName [AvailInfo]) -#if MIN_VERSION_ghc(9,5,0) filterByImport (ImportDecl{ideclImportList = Just (_, L _ names)}) -#else -filterByImport (ImportDecl{ideclHiding = Just (_, L _ names)}) -#endif avails = -- if there is a function defined in the current module and is used -- i.e. if a function is not reexported but defined in current @@ -464,11 +533,7 @@ filterByImport (ImportDecl{ideclHiding = Just (_, L _ names)}) then Just res else Nothing where importedNames = S.fromList $ map (ieName . unLoc) names - res = flip Map.filter avails $ \a -> - any (`S.member` importedNames) - $ concatMap - getAvailNames - a + res = Map.filter (any (any (`S.member` importedNames) . getAvailNames)) avails allFilteredAvailsNames = S.fromList $ concatMap getAvailNames $ mconcat @@ -476,22 +541,12 @@ filterByImport (ImportDecl{ideclHiding = Just (_, L _ names)}) filterByImport _ _ = Nothing constructImport :: ImportDecl GhcRn -> ImportDecl GhcRn -> (ModuleName, [AvailInfo]) -> ImportDecl GhcRn -#if MIN_VERSION_ghc(9,5,0) constructImport ImportDecl{ideclQualified = qualified, ideclImportList = origHiding} imd@ImportDecl{ideclImportList = Just (hiding, L _ names)} -#else -constructImport ImportDecl{ideclQualified = qualified, ideclHiding = origHiding} imd@ImportDecl{ideclHiding = Just (hiding, L _ names)} -#endif (newModuleName, avails) = imd { ideclName = noLocA newModuleName -#if MIN_VERSION_ghc(9,5,0) , ideclImportList = if isNothing origHiding && qualified /= NotQualified then Nothing else Just (hiding, noLocA newNames) -#else - , ideclHiding = if isNothing origHiding && qualified /= NotQualified - then Nothing - else Just (hiding, noLocA newNames) -#endif } where newNames = filter (\n -> any (n `containsAvail`) avails) names -- Check if a name is exposed by AvailInfo (the available information of a module) diff --git a/plugins/hls-explicit-imports-plugin/test/Main.hs b/plugins/hls-explicit-imports-plugin/test/Main.hs index 1ff799bbfb..01fe1d469e 100644 --- a/plugins/hls-explicit-imports-plugin/test/Main.hs +++ b/plugins/hls-explicit-imports-plugin/test/Main.hs @@ -1,18 +1,15 @@ {-# LANGUAGE DisambiguateRecordFields #-} {-# LANGUAGE LambdaCase #-} -{-# LANGUAGE NamedFieldPuns #-} -{-# LANGUAGE OverloadedLabels #-} {-# LANGUAGE OverloadedStrings #-} -{-# LANGUAGE TypeOperators #-} {-# LANGUAGE ViewPatterns #-} module Main ( main ) where import Control.Lens ((^.)) +import Control.Monad (unless) import Data.Either.Extra import Data.Foldable (find) -import Data.Row ((.+), (.==)) import Data.Text (Text) import qualified Data.Text as T import Data.Traversable (for) @@ -30,18 +27,33 @@ main = defaultTestRunner $ testGroup "import-actions" [testGroup "Refine Imports" [ codeActionGoldenTest "RefineWithOverride" 3 1 - , codeLensGoldenTest isRefineImports "RefineUsualCase" 1 - , codeLensGoldenTest isRefineImports "RefineQualified" 0 - , codeLensGoldenTest isRefineImports "RefineQualifiedExplicit" 0 + -- Although the client has inlay hints caps, refine is always provided by the code lens + , codeLensGoldenTest codeActionNoResolveCaps isRefineImports "RefineUsualCase" 1 + , codeLensGoldenTest codeActionNoResolveCaps isRefineImports "RefineQualified" 0 + , codeLensGoldenTest codeActionNoResolveCaps isRefineImports "RefineQualifiedExplicit" 0 ], testGroup "Make imports explicit" [ codeActionAllGoldenTest "ExplicitUsualCase" 3 0 , codeActionAllResolveGoldenTest "ExplicitUsualCase" 3 0 + , inlayHintsTestWithCap "ExplicitUsualCase" 2 $ (@=?) + [mkInlayHint (Position 2 16) "( a1 )" + (TextEdit (Range (Position 2 0) (Position 2 16)) "import ExplicitA ( a1 )")] + , inlayHintsTestWithoutCap "ExplicitUsualCase" 2 $ (@=?) [] , codeActionOnlyGoldenTest "ExplicitOnlyThis" 3 0 , codeActionOnlyResolveGoldenTest "ExplicitOnlyThis" 3 0 - , codeLensGoldenTest notRefineImports "ExplicitUsualCase" 0 + , inlayHintsTestWithCap "ExplicitOnlyThis" 3 $ (@=?) + [mkInlayHint (Position 3 16) "( b1 )" + (TextEdit (Range (Position 3 0) (Position 3 16)) "import ExplicitB ( b1 )")] + , inlayHintsTestWithoutCap "ExplicitOnlyThis" 3 $ (@=?) [] + -- Only when the client does not support inlay hints, explicit will be provided by code lens + , codeLensGoldenTest codeActionNoInlayHintsCaps notRefineImports "ExplicitUsualCase" 0 + , noCodeLensTest codeActionNoResolveCaps "ExplicitUsualCase" , codeActionBreakFile "ExplicitBreakFile" 4 0 + , inlayHintsTestWithCap "ExplicitBreakFile" 3 $ (@=?) + [mkInlayHint (Position 3 16) "( a1 )" + (TextEdit (Range (Position 3 0) (Position 3 16)) "import ExplicitA ( a1 )")] + , inlayHintsTestWithoutCap "ExplicitBreakFile" 3 $ (@=?) [] , codeActionStaleAction "ExplicitStaleAction" 4 0 , testCase "No CodeAction when exported" $ runSessionWithServer def explicitImportsPlugin testDataDir $ do @@ -53,6 +65,11 @@ main = defaultTestRunner $ testGroup "import-actions" doc <- openDoc "ExplicitExported.hs" "haskell" lenses <- getCodeLenses doc liftIO $ lenses @?= [] + , testCase "No InlayHints when exported" $ + runSessionWithServer def explicitImportsPlugin testDataDir $ do + doc <- openDoc "ExplicitExported.hs" "haskell" + inlayHints <- getInlayHints doc (pointRange 3 0) + liftIO $ inlayHints @?= [] , testGroup "Title abbreviation" [ testCase "not abbreviated" $ let i = "import " <> T.replicate 70 "F" <> " (Athing, Bthing, Cthing)" @@ -76,6 +93,20 @@ main = defaultTestRunner $ testGroup "import-actions" o = "import " <> T.replicate 80 "F" <> " (Athing, Bthing, ... (3 items))" in ExplicitImports.abbreviateImportTitle i @?= o ] + , testGroup "Title abbreviation without module" + [ testCase "not abbreviated" $ + let i = "import M (" <> T.replicate 70 "F" <> ", Athing, Bthing, Cthing)" + o = "(FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF, Athing, Bthing, Cthing)" + in ExplicitImports.abbreviateImportTitleWithoutModule i @?= o + , testCase "abbreviated that drop module name" $ + let i = "import " <> T.replicate 120 "F" <> " (Athing, Bthing, Cthing)" + o = "(Athing, Bthing, Cthing)" + in ExplicitImports.abbreviateImportTitleWithoutModule i @?= o + , testCase "abbreviated in import list" $ + let i = "import M (Athing, Bthing, " <> T.replicate 100 "F" <> ", Cthing, Dthing, Ething)" + o = "(Athing, Bthing, ... (4 items))" + in ExplicitImports.abbreviateImportTitleWithoutModule i @?= o + ] ]] -- code action tests @@ -88,16 +119,21 @@ codeActionAllGoldenTest fp l c = goldenWithImportActions " code action" fp codeA _ -> liftIO $ assertFailure "Unable to find CodeAction" codeActionBreakFile :: FilePath -> Int -> Int -> TestTree -codeActionBreakFile fp l c = goldenWithImportActions " code action" fp codeActionNoResolveCaps $ \doc -> do +-- If use `codeActionNoResolveCaps` instead of `codeActionNoInlayHintsCaps` here, +-- we will get a puzzling error: https://github.com/haskell/haskell-language-server/pull/4235#issuecomment-2189048997 +codeActionBreakFile fp l c = goldenWithImportActions " code action" fp codeActionNoInlayHintsCaps $ \doc -> do _ <- getCodeLenses doc changeDoc doc [edit] actions <- getCodeActions doc (pointRange l c) case find ((== Just "Make all imports explicit") . caTitle) actions of Just (InR x) -> executeCodeAction x _ -> liftIO $ assertFailure "Unable to find CodeAction" - where edit = TextDocumentContentChangeEvent $ InL $ #range .== pointRange 2 29 - .+ #rangeLength .== Nothing - .+ #text .== "x" + where edit = TextDocumentContentChangeEvent $ InL + TextDocumentContentChangePartial + { _range = pointRange 2 29 + , _rangeLength = Nothing + , _text = "x" + } codeActionStaleAction :: FilePath -> Int -> Int -> TestTree codeActionStaleAction fp l c = goldenWithImportActions " code action" fp codeActionResolveCaps $ \doc -> do @@ -111,9 +147,12 @@ codeActionStaleAction fp l c = goldenWithImportActions " code action" fp codeAct \case Just _ -> liftIO $ assertFailure "Code action still valid" Nothing -> pure () _ -> liftIO $ assertFailure "Unable to find CodeAction" - where edit = TextDocumentContentChangeEvent $ InL $ #range .== Range (Position 6 0) (Position 6 0) - .+ #rangeLength .== Nothing - .+ #text .== "\ntesting = undefined" + where edit = TextDocumentContentChangeEvent $ InL + TextDocumentContentChangePartial + { _range = Range (Position 6 0) (Position 6 0) + , _rangeLength = Nothing + , _text = "\ntesting = undefined" + } codeActionAllResolveGoldenTest :: FilePath -> Int -> Int -> TestTree codeActionAllResolveGoldenTest fp l c = goldenWithImportActions " code action resolve" fp codeActionResolveCaps $ \doc -> do @@ -148,18 +187,71 @@ caTitle _ = Nothing -- code lens tests -codeLensGoldenTest :: (CodeLens -> Bool) -> FilePath -> Int -> TestTree -codeLensGoldenTest predicate fp i = goldenWithImportActions " code lens" fp codeActionNoResolveCaps $ \doc -> do +codeLensGoldenTest :: ClientCapabilities -> (CodeLens -> Bool) -> FilePath -> Int -> TestTree +codeLensGoldenTest caps predicate fp i = goldenWithImportActions " code lens" fp caps $ \doc -> do codeLenses <- getCodeLenses doc resolvedCodeLenses <- for codeLenses resolveCodeLens (CodeLens {_command = Just c}) <- pure (filter predicate resolvedCodeLenses !! i) executeCmd c +noCodeLensTest :: ClientCapabilities -> FilePath -> TestTree +noCodeLensTest caps fp = do + testCase (fp ++ " no code lens") $ run $ \_ -> do + doc <- openDoc (fp ++ ".hs") "haskell" + codeLenses <- getCodeLenses doc + resolvedCodeLenses <- for codeLenses resolveCodeLens + unless (null resolvedCodeLenses) $ + liftIO (assertFailure "Unexpected code lens") + where + run = runSessionWithTestConfig def + { testDirLocation = Left testDataDir + , testConfigCaps = caps + , testLspConfig = def + , testPluginDescriptor = explicitImportsPlugin + } + + notRefineImports :: CodeLens -> Bool notRefineImports (CodeLens _ (Just (Command text _ _)) _) | "Refine imports to" `T.isPrefixOf` text = False notRefineImports _ = True +-- inlay hints tests + +inlayHintsTest :: ClientCapabilities -> String -> FilePath -> UInt -> ([InlayHint] -> Assertion) -> TestTree +inlayHintsTest configCaps postfix fp line assert = testCase (fp ++ postfix) $ run $ \_ -> do + doc <- openDoc (fp ++ ".hs") "haskell" + inlayHints <- getInlayHints doc (lineRange line) + liftIO $ assert inlayHints + where + -- zero-based position + lineRange line = Range (Position line 0) (Position line 1000) + run = runSessionWithTestConfig def + { testDirLocation = Left testDataDir + , testPluginDescriptor = explicitImportsPlugin + , testConfigCaps = configCaps + } + +inlayHintsTestWithCap :: FilePath -> UInt -> ([InlayHint] -> Assertion) -> TestTree +inlayHintsTestWithCap = inlayHintsTest fullLatestClientCaps " inlay hints with client caps" + +inlayHintsTestWithoutCap :: FilePath -> UInt -> ([InlayHint] -> Assertion) -> TestTree +inlayHintsTestWithoutCap = inlayHintsTest codeActionNoInlayHintsCaps " inlay hints without client caps" + + +mkInlayHint :: Position -> Text -> TextEdit -> InlayHint +mkInlayHint pos label textEdit = + InlayHint + { _position = pos + , _label = InL label + , _kind = Nothing + , _textEdits = Just [textEdit] + , _tooltip = Just $ InL "Make this import explicit" + , _paddingLeft = Just True + , _paddingRight = Nothing + , _data_ = Nothing + } + -- Execute command and wait for result executeCmd :: Command -> Session () executeCmd cmd = do @@ -174,7 +266,7 @@ goldenWithImportActions :: String -> FilePath -> ClientCapabilities -> (TextDocu goldenWithImportActions title fp caps = goldenWithHaskellAndCaps def caps explicitImportsPlugin (fp <> title <> " (golden)") testDataDir fp "expected" "hs" testDataDir :: String -testDataDir = "test" "testdata" +testDataDir = "plugins" "hls-explicit-imports-plugin" "test" "testdata" pointRange :: Int -> Int -> Range pointRange diff --git a/plugins/hls-explicit-record-fields-plugin/CHANGELOG.md b/plugins/hls-explicit-record-fields-plugin/CHANGELOG.md deleted file mode 100644 index 609eef3bed..0000000000 --- a/plugins/hls-explicit-record-fields-plugin/CHANGELOG.md +++ /dev/null @@ -1,5 +0,0 @@ -# Revision history for hls-explicit-record-fields-plugin - -## 1.0.0.0 -- YYYY-mm-dd - -* First version. Released on an unsuspecting world. diff --git a/plugins/hls-explicit-record-fields-plugin/LICENSE b/plugins/hls-explicit-record-fields-plugin/LICENSE deleted file mode 100644 index 00abc29fb4..0000000000 --- a/plugins/hls-explicit-record-fields-plugin/LICENSE +++ /dev/null @@ -1,30 +0,0 @@ -Copyright (c) 2022, Berk Ozkutuk - -All rights reserved. - -Redistribution and use in source and binary forms, with or without -modification, are permitted provided that the following conditions are met: - - * Redistributions of source code must retain the above copyright - notice, this list of conditions and the following disclaimer. - - * Redistributions in binary form must reproduce the above - copyright notice, this list of conditions and the following - disclaimer in the documentation and/or other materials provided - with the distribution. - - * Neither the name of Berk Ozkutuk nor the names of other - contributors may be used to endorse or promote products derived - from this software without specific prior written permission. - -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS -"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT -LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR -A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT -OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, -DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY -THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/plugins/hls-explicit-record-fields-plugin/hls-explicit-record-fields-plugin.cabal b/plugins/hls-explicit-record-fields-plugin/hls-explicit-record-fields-plugin.cabal deleted file mode 100644 index 96cc6b23b2..0000000000 --- a/plugins/hls-explicit-record-fields-plugin/hls-explicit-record-fields-plugin.cabal +++ /dev/null @@ -1,71 +0,0 @@ -cabal-version: 3.0 -name: hls-explicit-record-fields-plugin -version: 2.4.0.0 -synopsis: Explicit record fields plugin for Haskell Language Server -description: - Please see the README on GitHub at -license: BSD-3-Clause -license-file: LICENSE -author: Berk Ozkutuk -maintainer: berk.ozkutuk@tweag.io --- copyright: -category: Development -build-type: Simple -extra-doc-files: CHANGELOG.md -extra-source-files: - test/testdata/**/*.hs - -source-repository head - type: git - location: https://github.com/haskell/haskell-language-server - -flag pedantic - description: Enable -Werror - default: False - manual: True - -common warnings - ghc-options: -Wall - -library - import: warnings - exposed-modules: Ide.Plugin.ExplicitFields - -- other-modules: - -- other-extensions: - build-depends: - , base >=4.12 && <5 - , ghc - , ghcide == 2.4.0.0 - , hls-plugin-api == 2.4.0.0 - , lsp - , lens - , hls-graph - , text - , syb - , transformers - , ghc-boot-th - , unordered-containers - , containers - , aeson - hs-source-dirs: src - default-language: Haskell2010 - - if flag(pedantic) - ghc-options: -Werror - -Wwarn=incomplete-record-updates - -test-suite tests - import: warnings - default-language: Haskell2010 - -- other-modules: - -- other-extensions: - type: exitcode-stdio-1.0 - hs-source-dirs: test - main-is: Main.hs - build-depends: - , base - , filepath - , text - , hls-explicit-record-fields-plugin - , lsp-test - , hls-test-utils diff --git a/plugins/hls-explicit-record-fields-plugin/src/Ide/Plugin/ExplicitFields.hs b/plugins/hls-explicit-record-fields-plugin/src/Ide/Plugin/ExplicitFields.hs index eb0ee1c5e3..a761f648af 100644 --- a/plugins/hls-explicit-record-fields-plugin/src/Ide/Plugin/ExplicitFields.hs +++ b/plugins/hls-explicit-record-fields-plugin/src/Ide/Plugin/ExplicitFields.hs @@ -1,92 +1,127 @@ -{-# LANGUAGE CPP #-} -{-# LANGUAGE DataKinds #-} -{-# LANGUAGE DeriveGeneric #-} -{-# LANGUAGE ExistentialQuantification #-} -{-# LANGUAGE FlexibleContexts #-} -{-# LANGUAGE LambdaCase #-} -{-# LANGUAGE NamedFieldPuns #-} -{-# LANGUAGE OverloadedStrings #-} -{-# LANGUAGE PatternSynonyms #-} -{-# LANGUAGE TypeFamilies #-} -{-# LANGUAGE TypeOperators #-} -{-# LANGUAGE ViewPatterns #-} +{-# LANGUAGE CPP #-} +{-# LANGUAGE DataKinds #-} +{-# LANGUAGE DuplicateRecordFields #-} +{-# LANGUAGE LambdaCase #-} +{-# LANGUAGE OverloadedStrings #-} +{-# LANGUAGE PatternSynonyms #-} +{-# LANGUAGE TypeFamilies #-} +{-# LANGUAGE ViewPatterns #-} module Ide.Plugin.ExplicitFields ( descriptor , Log ) where -import Control.Lens ((&), (?~), (^.)) -import Control.Monad.IO.Class (MonadIO (liftIO)) +import Control.Arrow ((&&&)) +import Control.Lens ((&), (?~), (^.)) +import Control.Monad (replicateM) +import Control.Monad.IO.Class (MonadIO (liftIO)) +import Control.Monad.Trans.Class (lift) import Control.Monad.Trans.Maybe -import Data.Aeson (toJSON) -import Data.Generics (GenericQ, everything, - everythingBut, extQ, mkQ) -import qualified Data.IntMap.Strict as IntMap -import qualified Data.Map as Map -import Data.Maybe (fromMaybe, isJust, - maybeToList) -import Data.Text (Text) -import Data.Unique (hashUnique, newUnique) - -import Control.Monad (replicateM) -import Development.IDE (IdeState, Pretty (..), Range, - Recorder (..), Rules, - WithPriority (..), - defineNoDiagnostics, - realSrcSpanToRange, viaShow) +import Data.Aeson (ToJSON (toJSON)) +import Data.Generics (GenericQ, everything, + everythingBut, extQ, mkQ) +import qualified Data.IntMap.Strict as IntMap +import Data.List (find, intersperse) +import qualified Data.Map as Map +import Data.Maybe (fromMaybe, isJust, + mapMaybe, maybeToList) +import Data.Text (Text) +import qualified Data.Text as T +import Data.Unique (hashUnique, newUnique) +import Development.IDE (IdeState, + Location (Location), + Pretty (..), + Range (Range, _end, _start), + Recorder (..), Rules, + WithPriority (..), + defineNoDiagnostics, + getDefinition, hsep, + printName, + realSrcSpanToRange, + shakeExtras, + srcSpanToLocation, + srcSpanToRange, viaShow) import Development.IDE.Core.PluginUtils -import Development.IDE.Core.RuleTypes (TcModuleResult (..), - TypeCheck (..)) -import qualified Development.IDE.Core.Shake as Shake -import Development.IDE.GHC.Compat (HsConDetails (RecCon), - HsRecFields (..), LPat, - Outputable, getLoc, - recDotDot, unLoc) -import Development.IDE.GHC.Compat.Core (Extension (NamedFieldPuns), - GhcPass, - HsExpr (RecordCon, rcon_flds), - HsRecField, LHsExpr, - LocatedA, Name, Pass (..), - Pat (..), RealSrcSpan, - UniqFM, conPatDetails, - emptyUFM, hfbPun, hfbRHS, - hs_valds, lookupUFM, - mapConPatDetail, mapLoc, - pattern RealSrcSpan, - plusUFM_C, unitUFM) -import Development.IDE.GHC.Util (getExtensions, - printOutputable) -import Development.IDE.Graph (RuleResult) -import Development.IDE.Graph.Classes (Hashable, NFData) -import Development.IDE.Spans.Pragmas (NextPragmaInfo (..), - getFirstPragma, - insertNewPragma) -import GHC.Generics (Generic) -import Ide.Logger (Priority (..), cmapWithPrio, - logWith, (<+>)) -import Ide.Plugin.Error (PluginError (PluginInternalError, PluginStaleResolve), - getNormalizedFilePathE, - handleMaybe) -import Ide.Plugin.RangeMap (RangeMap) -import qualified Ide.Plugin.RangeMap as RangeMap -import Ide.Plugin.Resolve (mkCodeActionWithResolveAndCommand) -import Ide.Types (PluginDescriptor (..), - PluginId (..), - PluginMethodHandler, - ResolveFunction, - defaultPluginDescriptor) -import qualified Language.LSP.Protocol.Lens as L -import Language.LSP.Protocol.Message (Method (..)) -import Language.LSP.Protocol.Types (CodeAction (..), - CodeActionKind (CodeActionKind_RefactorRewrite), - CodeActionParams (..), - Command, TextEdit (..), - WorkspaceEdit (WorkspaceEdit), - type (|?) (InL, InR)) - -import Development.IDE.GHC.Compat (HsExpansion (HsExpanded), - HsExpr (XExpr)) +import Development.IDE.Core.PositionMapping (PositionMapping, + toCurrentPosition, + toCurrentRange) +import Development.IDE.Core.RuleTypes (TcModuleResult (..), + TypeCheck (..)) +import qualified Development.IDE.Core.Shake as Shake +import Development.IDE.GHC.Compat (FieldLabel (flSelector), + FieldOcc (FieldOcc), + GenLocated (L), GhcPass, + GhcTc, + HasSrcSpan (getLoc), + HsConDetails (RecCon), + HsExpr (HsApp, HsVar, XExpr), + HsFieldBind (hfbLHS), + HsRecFields (..), + HsWrap (HsWrap), + Identifier, LPat, + Located, + NamedThing (getName), + Outputable, + TcGblEnv (tcg_binds), + Var (varName), + XXExprGhcTc (..), + conLikeFieldLabels, + nameSrcSpan, + pprNameUnqualified, + recDotDot, unLoc) +import Development.IDE.GHC.Compat.Core (Extension (NamedFieldPuns), + HsExpr (RecordCon, rcon_flds), + HsRecField, LHsExpr, + LocatedA, Name, Pat (..), + RealSrcSpan, UniqFM, + conPatDetails, emptyUFM, + hfbPun, hfbRHS, + lookupUFM, + mapConPatDetail, mapLoc, + pattern RealSrcSpan, + plusUFM_C, unitUFM) +import Development.IDE.GHC.Util (getExtensions, + printOutputable, + stripOccNamePrefix) +import Development.IDE.Graph (RuleResult) +import Development.IDE.Graph.Classes (Hashable, NFData) +import Development.IDE.Spans.Pragmas (NextPragmaInfo (..), + getFirstPragma, + insertNewPragma) +import GHC.Generics (Generic) +import Ide.Logger (Priority (..), + cmapWithPrio, logWith, + (<+>)) +import Ide.Plugin.Error (PluginError (PluginInternalError, PluginStaleResolve), + getNormalizedFilePathE, + handleMaybe) +import Ide.Plugin.RangeMap (RangeMap) +import qualified Ide.Plugin.RangeMap as RangeMap +import Ide.Plugin.Resolve (mkCodeActionWithResolveAndCommand) +import Ide.Types (PluginDescriptor (..), + PluginId (..), + PluginMethodHandler, + ResolveFunction, + defaultPluginDescriptor, + mkPluginHandler) +import qualified Language.LSP.Protocol.Lens as L +import Language.LSP.Protocol.Message (Method (..), + SMethod (SMethod_TextDocumentInlayHint)) +import Language.LSP.Protocol.Types (CodeAction (..), + CodeActionKind (CodeActionKind_RefactorRewrite), + CodeActionParams (CodeActionParams), + Command, InlayHint (..), + InlayHintLabelPart (InlayHintLabelPart), + InlayHintParams (InlayHintParams, _range, _textDocument), + TextDocumentIdentifier (TextDocumentIdentifier), + TextEdit (TextEdit), + WorkspaceEdit (WorkspaceEdit), + type (|?) (InL, InR)) + +#if __GLASGOW_HASKELL__ < 910 +import Development.IDE.GHC.Compat (HsExpansion (HsExpanded)) +#endif data Log = LogShake Shake.Log @@ -106,8 +141,10 @@ descriptor :: Recorder (WithPriority Log) -> PluginId -> PluginDescriptor IdeSta descriptor recorder plId = let resolveRecorder = cmapWithPrio LogResolve recorder (carCommands, caHandlers) = mkCodeActionWithResolveAndCommand resolveRecorder plId codeActionProvider codeActionResolveProvider - in (defaultPluginDescriptor plId) - { pluginHandlers = caHandlers + ihDotdotHandler = mkPluginHandler SMethod_TextDocumentInlayHint (inlayHintDotdotProvider recorder) + ihPosRecHandler = mkPluginHandler SMethod_TextDocumentInlayHint (inlayHintPosRecProvider recorder) + in (defaultPluginDescriptor plId "Provides a code action to make record wildcards explicit") + { pluginHandlers = caHandlers <> ihDotdotHandler <> ihPosRecHandler , pluginCommands = carCommands , pluginRules = collectRecordsRule recorder *> collectNamesRule } @@ -115,18 +152,22 @@ descriptor recorder plId = codeActionProvider :: PluginMethodHandler IdeState 'Method_TextDocumentCodeAction codeActionProvider ideState _ (CodeActionParams _ _ docId range _) = do nfp <- getNormalizedFilePathE (docId ^. L.uri) - CRR {crCodeActions, enabledExtensions} <- runActionE "ExplicitFields.CollectRecords" ideState $ useE CollectRecords nfp + CRR {crCodeActions, crCodeActionResolve, enabledExtensions} <- runActionE "ExplicitFields.CollectRecords" ideState $ useE CollectRecords nfp -- All we need to build a code action is the list of extensions, and a int to -- allow us to resolve it later. - let actions = map (mkCodeAction enabledExtensions) (RangeMap.filterByRange range crCodeActions) + let recordUids = [ uid + | uid <- RangeMap.filterByRange range crCodeActions + , Just record <- [IntMap.lookup uid crCodeActionResolve] + -- Only fully saturated constructor applications can be + -- converted to the record syntax through the code action + , isConvertible record + ] + let actions = map (mkCodeAction enabledExtensions) recordUids pure $ InL actions where - mkCodeAction :: [Extension] -> Int -> Command |? CodeAction - mkCodeAction exts uid = InR CodeAction - { _title = "Expand record wildcard" - <> if NamedFieldPuns `elem` exts - then mempty - else " (needs extension: NamedFieldPuns)" + mkCodeAction :: [Extension] -> Int -> Command |? CodeAction + mkCodeAction exts uid = InR CodeAction + { _title = mkTitle exts -- TODO: `Expand positional record` without NamedFieldPuns if RecordInfoApp , _kind = Just CodeActionKind_RefactorRewrite , _diagnostics = Nothing , _isPreferred = Nothing @@ -136,6 +177,11 @@ codeActionProvider ideState _ (CodeActionParams _ _ docId range _) = do , _data_ = Just $ toJSON uid } + isConvertible :: RecordInfo -> Bool + isConvertible = \case + RecordInfoApp _ (RecordAppExpr Unsaturated _ _) -> False + _ -> True + codeActionResolveProvider :: ResolveFunction IdeState Int 'Method_CodeActionResolve codeActionResolveProvider ideState pId ca uri uid = do nfp <- getNormalizedFilePathE uri @@ -145,17 +191,117 @@ codeActionResolveProvider ideState pId ca uri uid = do -- that this resolve is stale. record <- handleMaybe PluginStaleResolve $ IntMap.lookup uid crCodeActionResolve -- We should never fail to render - rendered <- handleMaybe (PluginInternalError "Failed to render") $ renderRecordInfo nameMap record - let edits = [rendered] - <> maybeToList (pragmaEdit enabledExtensions pragma) + rendered <- handleMaybe (PluginInternalError "Failed to render") $ renderRecordInfoAsTextEdit nameMap record + let shouldInsertNamedFieldPuns (RecordInfoApp _ _) = False + shouldInsertNamedFieldPuns _ = True + whenMaybe True x = x + whenMaybe False _ = Nothing + edits = [rendered] + <> maybeToList (whenMaybe (shouldInsertNamedFieldPuns record) (pragmaEdit enabledExtensions pragma)) pure $ ca & L.edit ?~ mkWorkspaceEdit edits where mkWorkspaceEdit ::[TextEdit] -> WorkspaceEdit mkWorkspaceEdit edits = WorkspaceEdit (Just $ Map.singleton uri edits) Nothing Nothing - pragmaEdit :: [Extension] -> NextPragmaInfo -> Maybe TextEdit - pragmaEdit exts pragma = if NamedFieldPuns `elem` exts - then Nothing - else Just $ insertNewPragma pragma NamedFieldPuns + +inlayHintDotdotProvider :: Recorder (WithPriority Log) -> PluginMethodHandler IdeState 'Method_TextDocumentInlayHint +inlayHintDotdotProvider _ state pId InlayHintParams {_textDocument = TextDocumentIdentifier uri, _range = visibleRange} = do + nfp <- getNormalizedFilePathE uri + pragma <- getFirstPragma pId state nfp + runIdeActionE "ExplicitFields.CollectRecords" (shakeExtras state) $ do + (crr@CRR {crCodeActions, crCodeActionResolve}, pm) <- useWithStaleFastE CollectRecords nfp + let -- Get all records with dotdot in current nfp + records = [ record + | Just range <- [toCurrentRange pm visibleRange] + , uid <- RangeMap.elementsInRange range crCodeActions + , Just record <- [IntMap.lookup uid crCodeActionResolve] ] + -- Get the definition of each dotdot of record + locations = [ fmap (,record) (getDefinition nfp pos) + | record <- records + , pos <- maybeToList $ fmap _start $ recordInfoToDotDotRange record ] + defnLocsList <- lift $ sequence locations + pure $ InL $ mapMaybe (mkInlayHint crr pragma pm) defnLocsList + where + mkInlayHint :: CollectRecordsResult -> NextPragmaInfo -> PositionMapping -> (Maybe [(Location, Identifier)], RecordInfo) -> Maybe InlayHint + mkInlayHint CRR {enabledExtensions, nameMap} pragma pm (defnLocs, record) = + let range = recordInfoToDotDotRange record + textEdits = maybeToList (renderRecordInfoAsTextEdit nameMap record) + <> maybeToList (pragmaEdit enabledExtensions pragma) + names = renderRecordInfoAsDotdotLabelName record + in do + currentEnd <- range >>= toCurrentPosition pm . _end + names' <- names + defnLocs' <- defnLocs + let excludeDotDot (Location _ (Range _ end)) = end /= currentEnd + -- find location from dotdot definitions that name equal to label name + findLocation name locations = + let -- filter locations not within dotdot range + filteredLocations = filter (excludeDotDot . fst) locations + -- checks if 'a' is equal to 'Name' if the 'Either' is 'Right a', otherwise return 'False' + nameEq = either (const False) ((==) name) + in fmap fst $ find (nameEq . snd) filteredLocations + valueWithLoc = [ (stripOccNamePrefix $ T.pack $ printName name, findLocation name defnLocs') | name <- names' ] + -- use `, ` to separate labels with definition location + label = intersperse (mkInlayHintLabelPart (", ", Nothing)) $ fmap mkInlayHintLabelPart valueWithLoc + pure $ InlayHint { _position = currentEnd -- at the end of dotdot + , _label = InR label + , _kind = Nothing -- neither a type nor a parameter + , _textEdits = Just textEdits -- same as CodeAction + , _tooltip = Just $ InL (mkTitle enabledExtensions) -- same as CodeAction + , _paddingLeft = Just True -- padding after dotdot + , _paddingRight = Nothing + , _data_ = Nothing + } + mkInlayHintLabelPart (value, loc) = InlayHintLabelPart value Nothing loc Nothing + + +inlayHintPosRecProvider :: Recorder (WithPriority Log) -> PluginMethodHandler IdeState 'Method_TextDocumentInlayHint +inlayHintPosRecProvider _ state _pId InlayHintParams {_textDocument = TextDocumentIdentifier uri, _range = visibleRange} = do + nfp <- getNormalizedFilePathE uri + runIdeActionE "ExplicitFields.CollectRecords" (shakeExtras state) $ do + (CRR {crCodeActions, nameMap, crCodeActionResolve}, pm) <- useWithStaleFastE CollectRecords nfp + let records = [ record + | Just range <- [toCurrentRange pm visibleRange] + , uid <- RangeMap.elementsInRange range crCodeActions + , Just record <- [IntMap.lookup uid crCodeActionResolve] ] + pure $ InL (concatMap (mkInlayHints nameMap pm) records) + where + mkInlayHints :: UniqFM Name [Name] -> PositionMapping -> RecordInfo -> [InlayHint] + mkInlayHints nameMap pm record@(RecordInfoApp _ (RecordAppExpr _ _ fla)) = + let textEdits = renderRecordInfoAsTextEdit nameMap record + in mapMaybe (mkInlayHint textEdits pm) fla + mkInlayHints _ _ _ = [] + + mkInlayHint :: Maybe TextEdit -> PositionMapping -> (Located FieldLabel, HsExpr GhcTc) -> Maybe InlayHint + mkInlayHint te pm (label, _) = + let (name, loc) = ((flSelector . unLoc) &&& (srcSpanToLocation . getLoc)) label + fieldDefLoc = srcSpanToLocation (nameSrcSpan name) + in do + (Location _ recRange) <- loc + currentStart <- toCurrentPosition pm (_start recRange) + pure InlayHint { _position = currentStart + , _label = InR $ pure (mkInlayHintLabelPart name fieldDefLoc) + , _kind = Nothing -- neither a type nor a parameter + , _textEdits = Just (maybeToList te) -- same as CodeAction + , _tooltip = Just $ InL "Expand positional record" -- same as CodeAction + , _paddingLeft = Nothing + , _paddingRight = Nothing + , _data_ = Nothing + } + + mkInlayHintLabelPart name loc = InlayHintLabelPart (printFieldName (pprNameUnqualified name) <> "=") Nothing loc Nothing + +mkTitle :: [Extension] -> Text +mkTitle exts = "Expand record wildcard" + <> if NamedFieldPuns `elem` exts + then mempty + else " (needs extension: NamedFieldPuns)" + + +pragmaEdit :: [Extension] -> NextPragmaInfo -> Maybe TextEdit +pragmaEdit exts pragma = if NamedFieldPuns `elem` exts + then Nothing + else Just $ insertNewPragma pragma NamedFieldPuns + collectRecordsRule :: Recorder (WithPriority Log) -> Rules () collectRecordsRule recorder = @@ -177,12 +323,11 @@ collectRecordsRule recorder = pure CRR {crCodeActions, crCodeActionResolve, nameMap, enabledExtensions} where getEnabledExtensions :: TcModuleResult -> [Extension] - getEnabledExtensions = getExtensions . tmrParsed + getEnabledExtensions = getExtensions . tmrParsed toRangeAndUnique (uid, recordInfo) = (recordInfoToRange recordInfo, uid) getRecords :: TcModuleResult -> [RecordInfo] -getRecords (tmrRenamed -> (hs_valds -> valBinds,_,_,_)) = - collectRecords valBinds +getRecords (tcg_binds . tmrTypechecked -> valBinds) = collectRecords valBinds collectNamesRule :: Rules () collectNamesRule = defineNoDiagnostics mempty $ \CollectNames nfp -> runMaybeT $ do @@ -192,7 +337,11 @@ collectNamesRule = defineNoDiagnostics mempty $ \CollectNames nfp -> runMaybeT $ -- | Collects all 'Name's of a given source file, to be used -- in the variable usage analysis. getNames :: TcModuleResult -> UniqFM Name [Name] -getNames (tmrRenamed -> (group,_,_,_)) = collectNames group +#if __GLASGOW_HASKELL__ < 910 +getNames (tmrRenamed -> (group,_,_,_)) = collectNames group +#else +getNames (tmrRenamed -> (group,_,_,_,_)) = collectNames group +#endif data CollectRecords = CollectRecords deriving (Eq, Show, Generic) @@ -220,6 +369,7 @@ data CollectRecordsResult = CRR instance NFData CollectRecordsResult instance NFData RecordInfo +instance NFData RecordAppExpr instance Show CollectRecordsResult where show _ = "" @@ -242,22 +392,50 @@ instance Show CollectNamesResult where type instance RuleResult CollectNames = CollectNamesResult +data Saturated = Saturated | Unsaturated + deriving (Generic) + +instance NFData Saturated + +data RecordAppExpr + = RecordAppExpr + Saturated -- ^ Is the DataCon application fully saturated or partially applied? + (LHsExpr GhcTc) + [(Located FieldLabel, HsExpr GhcTc)] + deriving (Generic) + data RecordInfo - = RecordInfoPat RealSrcSpan (Pat (GhcPass 'Renamed)) - | RecordInfoCon RealSrcSpan (HsExpr (GhcPass 'Renamed)) + = RecordInfoPat RealSrcSpan (Pat GhcTc) + | RecordInfoCon RealSrcSpan (HsExpr GhcTc) + | RecordInfoApp RealSrcSpan RecordAppExpr deriving (Generic) instance Pretty RecordInfo where - pretty (RecordInfoPat ss p) = pretty (printOutputable ss) <> ":" <+> pretty (printOutputable p) - pretty (RecordInfoCon ss e) = pretty (printOutputable ss) <> ":" <+> pretty (printOutputable e) + pretty (RecordInfoPat ss p) = pretty (printFieldName ss) <> ":" <+> pretty (printOutputable p) + pretty (RecordInfoCon ss e) = pretty (printFieldName ss) <> ":" <+> pretty (printOutputable e) + pretty (RecordInfoApp ss (RecordAppExpr _ _ fla)) + = pretty (printFieldName ss) <> ":" <+> hsep (map (pretty . printOutputable) fla) recordInfoToRange :: RecordInfo -> Range recordInfoToRange (RecordInfoPat ss _) = realSrcSpanToRange ss recordInfoToRange (RecordInfoCon ss _) = realSrcSpanToRange ss +recordInfoToRange (RecordInfoApp ss _) = realSrcSpanToRange ss + +recordInfoToDotDotRange :: RecordInfo -> Maybe Range +recordInfoToDotDotRange (RecordInfoPat _ (ConPat _ _ (RecCon flds))) = srcSpanToRange . getLoc =<< rec_dotdot flds +recordInfoToDotDotRange (RecordInfoCon _ (RecordCon _ _ flds)) = srcSpanToRange . getLoc =<< rec_dotdot flds +recordInfoToDotDotRange _ = Nothing + +renderRecordInfoAsTextEdit :: UniqFM Name [Name] -> RecordInfo -> Maybe TextEdit +renderRecordInfoAsTextEdit names (RecordInfoPat ss pat) = TextEdit (realSrcSpanToRange ss) <$> showRecordPat names pat +renderRecordInfoAsTextEdit _ (RecordInfoCon ss expr) = TextEdit (realSrcSpanToRange ss) <$> showRecordCon expr +renderRecordInfoAsTextEdit _ (RecordInfoApp ss appExpr) = TextEdit (realSrcSpanToRange ss) <$> showRecordApp appExpr + +renderRecordInfoAsDotdotLabelName :: RecordInfo -> Maybe [Name] +renderRecordInfoAsDotdotLabelName (RecordInfoPat _ pat) = showRecordPatFlds pat +renderRecordInfoAsDotdotLabelName (RecordInfoCon _ expr) = showRecordConFlds expr +renderRecordInfoAsDotdotLabelName _ = Nothing -renderRecordInfo :: UniqFM Name [Name] -> RecordInfo -> Maybe TextEdit -renderRecordInfo names (RecordInfoPat ss pat) = TextEdit (realSrcSpanToRange ss) <$> showRecordPat names pat -renderRecordInfo _ (RecordInfoCon ss expr) = TextEdit (realSrcSpanToRange ss) <$> showRecordCon expr -- | Checks if a 'Name' is referenced in the given map of names. The -- 'hasNonBindingOcc' check is necessary in order to make sure that only the @@ -275,16 +453,16 @@ referencedIn name names = maybe True hasNonBindingOcc $ lookupUFM names name filterReferenced :: (a -> Maybe Name) -> UniqFM Name [Name] -> [a] -> [a] filterReferenced getName names = filter (\x -> maybe True (`referencedIn` names) (getName x)) + preprocessRecordPat - :: p ~ GhcPass 'Renamed + :: p ~ GhcTc => UniqFM Name [Name] -> HsRecFields p (LPat p) -> HsRecFields p (LPat p) -preprocessRecordPat = preprocessRecord (getFieldName . unLoc) - where - getFieldName x = case unLoc (hfbRHS x) of - VarPat _ x' -> Just $ unLoc x' - _ -> Nothing +preprocessRecordPat = preprocessRecord (fmap varName . getFieldName . unLoc) + where getFieldName x = case unLoc (hfbRHS x) of + VarPat _ x' -> Just $ unLoc x' + _ -> Nothing -- No need to check the name usage in the record construction case preprocessRecordCon :: HsRecFields (GhcPass c) arg -> HsRecFields (GhcPass c) arg @@ -327,17 +505,65 @@ preprocessRecord getName names flds = flds { rec_dotdot = Nothing , rec_flds = r punsUsed = filterReferenced getName names puns' rec_flds' = no_puns <> punsUsed -showRecordPat :: Outputable (Pat (GhcPass 'Renamed)) => UniqFM Name [Name] -> Pat (GhcPass 'Renamed) -> Maybe Text -showRecordPat names = fmap printOutputable . mapConPatDetail (\case +processRecordFlds + :: p ~ GhcPass c + => HsRecFields p arg + -> HsRecFields p arg +processRecordFlds flds = flds { rec_dotdot = Nothing , rec_flds = puns' } + where + no_pun_count = fromMaybe (length (rec_flds flds)) (recDotDot flds) + -- Field binds of the explicit form (e.g. `{ a = a' }`) should be drop + puns = drop no_pun_count (rec_flds flds) + -- `hsRecPun` is set to `True` in order to pretty-print the fields as field + -- puns (since there is similar mechanism in the `Outputable` instance as + -- explained above). + puns' = map (mapLoc (\fld -> fld { hfbPun = True })) puns + + +showRecordPat :: Outputable (Pat GhcTc) => UniqFM Name [Name] -> Pat GhcTc -> Maybe Text +showRecordPat names = fmap printFieldName . mapConPatDetail (\case RecCon flds -> Just $ RecCon (preprocessRecordPat names flds) _ -> Nothing) +showRecordPatFlds :: Pat GhcTc -> Maybe [Name] +showRecordPatFlds (ConPat _ _ args) = do + fields <- processRecCon args + names <- mapM getFieldName (rec_flds fields) + pure names + where + processRecCon (RecCon flds) = Just $ processRecordFlds flds + processRecCon _ = Nothing +#if __GLASGOW_HASKELL__ < 911 + getOccName (FieldOcc x _) = Just $ getName x +#else + getOccName (FieldOcc _ x) = Just $ getName (unLoc x) +#endif + getOccName _ = Nothing + getFieldName = getOccName . unLoc . hfbLHS . unLoc +showRecordPatFlds _ = Nothing + showRecordCon :: Outputable (HsExpr (GhcPass c)) => HsExpr (GhcPass c) -> Maybe Text showRecordCon expr@(RecordCon _ _ flds) = Just $ printOutputable $ expr { rcon_flds = preprocessRecordCon flds } showRecordCon _ = Nothing +showRecordConFlds :: p ~ GhcTc => HsExpr p -> Maybe [Name] +showRecordConFlds (RecordCon _ _ flds) = + mapM getFieldName (rec_flds $ processRecordFlds flds) + where + getVarName (HsVar _ lidp) = Just $ getName lidp + getVarName _ = Nothing + getFieldName = getVarName . unLoc . hfbRHS . unLoc +showRecordConFlds _ = Nothing + +showRecordApp :: RecordAppExpr -> Maybe Text +showRecordApp (RecordAppExpr _ recConstr fla) + = Just $ printOutputable recConstr <> " { " + <> T.intercalate ", " (showFieldWithArg <$> fla) + <> " }" + where showFieldWithArg (field, arg) = printFieldName field <> " = " <> printOutputable arg + collectRecords :: GenericQ [RecordInfo] collectRecords = everythingBut (<>) (([], False) `mkQ` getRecPatterns `extQ` getRecCons) @@ -354,7 +580,7 @@ collectRecords = everythingBut (<>) (([], False) `mkQ` getRecPatterns `extQ` get collectNames :: GenericQ (UniqFM Name [Name]) collectNames = everything (plusUFM_C (<>)) (emptyUFM `mkQ` (\x -> unitUFM x [x])) -getRecCons :: LHsExpr (GhcPass 'Renamed) -> ([RecordInfo], Bool) +getRecCons :: LHsExpr GhcTc -> ([RecordInfo], Bool) -- When we stumble upon an occurrence of HsExpanded, we only want to follow a -- single branch. We do this here, by explicitly returning occurrences from -- traversing the original branch, and returning True, which keeps syb from @@ -362,22 +588,61 @@ getRecCons :: LHsExpr (GhcPass 'Renamed) -> ([RecordInfo], Bool) -- because there is a possibility that there were be more than one result per -- branch -getRecCons (unLoc -> XExpr (HsExpanded a _)) = (collectRecords a, True) +#if __GLASGOW_HASKELL__ >= 910 +getRecCons (unLoc -> XExpr (ExpandedThingTc a _)) = (collectRecords a, False) +#else +getRecCons (unLoc -> XExpr (ExpansionExpr (HsExpanded _ a))) = (collectRecords a, True) +#endif getRecCons e@(unLoc -> RecordCon _ _ flds) | isJust (rec_dotdot flds) = (mkRecInfo e, False) where - mkRecInfo :: LHsExpr (GhcPass 'Renamed) -> [RecordInfo] + mkRecInfo :: LHsExpr GhcTc -> [RecordInfo] mkRecInfo expr = [ RecordInfoCon realSpan' (unLoc expr) | RealSrcSpan realSpan' _ <- [ getLoc expr ]] +getRecCons expr@(unLoc -> app@(HsApp _ _ _)) = + let fieldss = maybeToList $ getFields app [] + recInfo = concatMap mkRecInfo fieldss + in (recInfo, not (null recInfo)) + where + mkRecInfo :: RecordAppExpr -> [RecordInfo] + mkRecInfo appExpr = + [ RecordInfoApp realSpan' appExpr | RealSrcSpan realSpan' _ <- [ getLoc expr ] ] + + getFields :: HsExpr GhcTc -> [LHsExpr GhcTc] -> Maybe RecordAppExpr + getFields (HsApp _ constr@(unLoc -> expr) arg) args + | not (null fls) = Just $ + -- Code action is only valid if the constructor application is fully + -- saturated, but we still want to display the inlay hints for partially + -- applied constructors + RecordAppExpr + (if length fls <= length args + 1 then Saturated else Unsaturated) + constr + labelWithArgs + where fls = getExprFields expr + labelWithArgs = zipWith mkLabelWithArg fls (arg : args) + mkLabelWithArg label arg = (L (getLoc arg) label, unLoc arg) + getFields (HsApp _ constr arg) args = getFields (unLoc constr) (arg : args) + getFields _ _ = Nothing + + getExprFields :: HsExpr GhcTc -> [FieldLabel] + getExprFields (XExpr (ConLikeTc (conLikeFieldLabels -> fls) _ _)) = fls +#if __GLASGOW_HASKELL__ >= 911 + getExprFields (XExpr (WrapExpr _ expr)) = getExprFields expr +#else + getExprFields (XExpr (WrapExpr (HsWrap _ expr))) = getExprFields expr +#endif + getExprFields _ = [] getRecCons _ = ([], False) -getRecPatterns :: LPat (GhcPass 'Renamed) -> ([RecordInfo], Bool) +getRecPatterns :: LPat GhcTc -> ([RecordInfo], Bool) getRecPatterns conPat@(conPatDetails . unLoc -> Just (RecCon flds)) | isJust (rec_dotdot flds) = (mkRecInfo conPat, False) where - mkRecInfo :: LPat (GhcPass 'Renamed) -> [RecordInfo] + mkRecInfo :: LPat GhcTc -> [RecordInfo] mkRecInfo pat = [ RecordInfoPat realSpan' (unLoc pat) | RealSrcSpan realSpan' _ <- [ getLoc pat ]] getRecPatterns _ = ([], False) +printFieldName :: Outputable a => a -> Text +printFieldName = stripOccNamePrefix . printOutputable diff --git a/plugins/hls-explicit-record-fields-plugin/test/Main.hs b/plugins/hls-explicit-record-fields-plugin/test/Main.hs index 4e83ccbd80..82ef449a25 100644 --- a/plugins/hls-explicit-record-fields-plugin/test/Main.hs +++ b/plugins/hls-explicit-record-fields-plugin/test/Main.hs @@ -1,12 +1,14 @@ {-# LANGUAGE DuplicateRecordFields #-} -{-# LANGUAGE NamedFieldPuns #-} {-# LANGUAGE OverloadedStrings #-} -{-# LANGUAGE TypeOperators #-} module Main ( main ) where import Data.Either (rights) +import Data.Text (Text) import qualified Data.Text as T +import Development.IDE (filePathToUri', + toNormalizedFilePath') +import Development.IDE.Test (canonicalizeUri) import qualified Ide.Plugin.ExplicitFields as ExplicitFields import System.FilePath ((<.>), ()) import Test.Hls @@ -19,21 +21,290 @@ plugin = mkPluginTestDescriptor ExplicitFields.descriptor "explicit-fields" test :: TestTree test = testGroup "explicit-fields" - [ mkTest "WildcardOnly" "WildcardOnly" 12 10 12 20 - , mkTest "Unused" "Unused" 12 10 12 20 - , mkTest "Unused2" "Unused2" 12 10 12 20 - , mkTest "WithPun" "WithPun" 13 10 13 25 - , mkTest "WithExplicitBind" "WithExplicitBind" 12 10 12 32 - , mkTest "Mixed" "Mixed" 14 10 14 37 - , mkTest "Construction" "Construction" 16 5 16 15 - , mkTest "HsExpanded1" "HsExpanded1" 17 10 17 20 - , mkTest "HsExpanded2" "HsExpanded2" 23 10 23 22 - , mkTestNoAction "ExplicitBinds" "ExplicitBinds" 11 10 11 52 - , mkTestNoAction "Puns" "Puns" 12 10 12 31 - , mkTestNoAction "Infix" "Infix" 11 11 11 31 - , mkTestNoAction "Prefix" "Prefix" 10 11 10 28 + [ testGroup "code actions" + [ mkTest "WildcardOnly" "WildcardOnly" 12 10 12 20 + , mkTest "Unused" "Unused" 12 10 12 20 + , mkTest "Unused2" "Unused2" 12 10 12 20 + , mkTest "WithPun" "WithPun" 13 10 13 25 + , mkTest "WithExplicitBind" "WithExplicitBind" 12 10 12 32 + , mkTest "Mixed" "Mixed" 14 10 14 37 + , mkTest "Construction" "Construction" 16 5 16 15 + , mkTest "PositionalConstruction" "PositionalConstruction" 15 5 15 15 + , mkTest "HsExpanded1" "HsExpanded1" 17 10 17 20 + , mkTest "HsExpanded2" "HsExpanded2" 23 10 23 22 + , mkTestNoAction "ExplicitBinds" "ExplicitBinds" 11 10 11 52 + , mkTestNoAction "Puns" "Puns" 12 10 12 31 + , mkTestNoAction "Infix" "Infix" 11 11 11 31 + , mkTestNoAction "Prefix" "Prefix" 10 11 10 28 + , mkTestNoAction "PartiallyAppliedCon" "PartiallyAppliedCon" 7 8 7 12 + , mkTest "PolymorphicRecordConstruction" "PolymorphicRecordConstruction" 15 5 15 15 + ] + , testGroup "inlay hints" + [ mkInlayHintsTest "Construction" Nothing 16 $ \ih -> do + let mkLabelPart' = mkLabelPartOffsetLength "Construction" + foo <- mkLabelPart' 13 6 "foo" + bar <- mkLabelPart' 14 6 "bar" + baz <- mkLabelPart' 15 6 "baz" + (@?=) ih + [defInlayHint { _position = Position 16 14 + , _label = InR [ foo, commaPart + , bar, commaPart + , baz + ] + , _textEdits = Just [ mkLineTextEdit "MyRec {foo, bar, baz}" 16 5 15 + , mkPragmaTextEdit 2 + ] + , _tooltip = Just $ InL "Expand record wildcard (needs extension: NamedFieldPuns)" + , _paddingLeft = Just True + }] + , mkInlayHintsTest "ConstructionDuplicateRecordFields" Nothing 16 $ \ih -> do + let mkLabelPart' = mkLabelPartOffsetLength "ConstructionDuplicateRecordFields" + foo <- mkLabelPart' 13 6 "foo" + bar <- mkLabelPart' 14 6 "bar" + baz <- mkLabelPart' 15 6 "baz" + (@?=) ih + [defInlayHint { _position = Position 16 14 + , _label = InR [ foo, commaPart + , bar, commaPart + , baz + ] + , _textEdits = Just [ mkLineTextEdit "MyRec {foo, bar, baz}" 16 5 15 + , mkPragmaTextEdit 3 -- Not 2 of the DuplicateRecordFields pragma + ] + , _tooltip = Just $ InL "Expand record wildcard (needs extension: NamedFieldPuns)" + , _paddingLeft = Just True + }] + + , mkInlayHintsTest "PositionalConstruction" Nothing 15 $ \ih -> do + let mkLabelPart' = mkLabelPartOffsetLengthSub1 "PositionalConstruction" + foo <- mkLabelPart' 5 4 "foo=" + bar <- mkLabelPart' 6 4 "bar=" + baz <- mkLabelPart' 7 4 "baz=" + (@?=) ih + [ defInlayHint { _position = Position 15 11 + , _label = InR [ foo ] + , _textEdits = Just [ mkLineTextEdit "MyRec { foo = a, bar = b, baz = c }" 15 5 16 ] + , _tooltip = Just $ InL "Expand positional record" + , _paddingLeft = Nothing + } + , defInlayHint { _position = Position 15 13 + , _label = InR [ bar ] + , _textEdits = Just [ mkLineTextEdit "MyRec { foo = a, bar = b, baz = c }" 15 5 16 ] + , _tooltip = Just $ InL "Expand positional record" + , _paddingLeft = Nothing + } + , defInlayHint { _position = Position 15 15 + , _label = InR [ baz ] + , _textEdits = Just [ mkLineTextEdit "MyRec { foo = a, bar = b, baz = c }" 15 5 16 ] + , _tooltip = Just $ InL "Expand positional record" + , _paddingLeft = Nothing + } + ] + , mkInlayHintsTest "PositionalConstructionDuplicateRecordFields" Nothing 15 $ \ih -> do + let mkLabelPart' = mkLabelPartOffsetLengthSub1 "PositionalConstructionDuplicateRecordFields" + foo <- mkLabelPart' 5 4 "foo=" + bar <- mkLabelPart' 6 4 "bar=" + baz <- mkLabelPart' 7 4 "baz=" + (@?=) ih + [ defInlayHint { _position = Position 15 11 + , _label = InR [ foo ] + , _textEdits = Just [ mkLineTextEdit "MyRec { foo = a, bar = b, baz = c }" 15 5 16 ] + , _tooltip = Just $ InL "Expand positional record" + , _paddingLeft = Nothing + } + , defInlayHint { _position = Position 15 13 + , _label = InR [ bar ] + , _textEdits = Just [ mkLineTextEdit "MyRec { foo = a, bar = b, baz = c }" 15 5 16 ] + , _tooltip = Just $ InL "Expand positional record" + , _paddingLeft = Nothing + } + , defInlayHint { _position = Position 15 15 + , _label = InR [ baz ] + , _textEdits = Just [ mkLineTextEdit "MyRec { foo = a, bar = b, baz = c }" 15 5 16 ] + , _tooltip = Just $ InL "Expand positional record" + , _paddingLeft = Nothing + } + ] + , mkInlayHintsTest "HsExpanded1" Nothing 17 $ \ih -> do + let mkLabelPart' = mkLabelPartOffsetLength "HsExpanded1" + foo <- mkLabelPart' 11 4 "foo" + (@?=) ih + [defInlayHint { _position = Position 17 19 + , _label = InR [ foo ] + , _textEdits = Just [ mkLineTextEdit "MyRec {foo}" 17 10 20 ] + , _tooltip = Just $ InL "Expand record wildcard" + , _paddingLeft = Just True + }] + , mkInlayHintsTest "HsExpanded1" (Just " (positional)") 13 $ \ih -> do + let mkLabelPart' = mkLabelPartOffsetLengthSub1 "HsExpanded1" + foo <- mkLabelPart' 11 4 "foo=" + (@?=) ih + [defInlayHint { _position = Position 13 21 + , _label = InR [ foo ] + , _textEdits = Just [ mkLineTextEdit "MyRec { foo = 5 }" 13 15 22 ] + , _tooltip = Just $ InL "Expand positional record" + , _paddingLeft = Nothing + }] + , mkInlayHintsTest "HsExpanded1DuplicateRecordFields" (Just " (positional)") 13 $ \ih -> do + let mkLabelPart' = mkLabelPartOffsetLengthSub1 "HsExpanded1DuplicateRecordFields" + foo <- mkLabelPart' 11 4 "foo=" + (@?=) ih + [defInlayHint { _position = Position 13 21 + , _label = InR [ foo ] + , _textEdits = Just [ mkLineTextEdit "MyRec { foo = 5 }" 13 15 22 ] + , _tooltip = Just $ InL "Expand positional record" + , _paddingLeft = Nothing + }] + , mkInlayHintsTest "HsExpanded2" Nothing 23 $ \ih -> do + let mkLabelPart' = mkLabelPartOffsetLength "HsExpanded2" + bar <- mkLabelPart' 14 4 "bar" + (@?=) ih + [defInlayHint { _position = Position 23 21 + , _label = InR [ bar ] + , _textEdits = Just [ mkLineTextEdit "YourRec {bar}" 23 10 22 ] + , _tooltip = Just $ InL "Expand record wildcard" + , _paddingLeft = Just True + }] + , mkInlayHintsTest "HsExpanded2" (Just " (positional)") 16 $ \ih -> do + let mkLabelPart' = mkLabelPartOffsetLengthSub1 "HsExpanded2" + foo <- mkLabelPart' 11 4 "foo=" + (@?=) ih + [defInlayHint { _position = Position 16 21 + , _label = InR [ foo ] + , _textEdits = Just [ mkLineTextEdit "MyRec { foo = 5 }" 16 15 22 ] + , _tooltip = Just $ InL "Expand positional record" + , _paddingLeft = Nothing + }] + , mkInlayHintsTest "Mixed" Nothing 14 $ \ih -> do + let mkLabelPart' = mkLabelPartOffsetLength "Mixed" + baz <- mkLabelPart' 9 4 "baz" + quux <- mkLabelPart' 10 4 "quux" + (@?=) ih + [defInlayHint { _position = Position 14 36 + , _label = InR [ baz, commaPart + , quux + ] + , _textEdits = Just [ mkLineTextEdit "MyRec {foo, bar = bar', baz}" 14 10 37 ] + , _tooltip = Just $ InL "Expand record wildcard" + , _paddingLeft = Just True + }] + , mkInlayHintsTest "Unused" Nothing 12 $ \ih -> do + let mkLabelPart' = mkLabelPartOffsetLength "Unused" + foo <- mkLabelPart' 6 4 "foo" + bar <- mkLabelPart' 7 4 "bar" + baz <- mkLabelPart' 8 4 "baz" + (@?=) ih + [defInlayHint { _position = Position 12 19 + , _label = InR [ foo, commaPart + , bar, commaPart + , baz + ] + , _textEdits = Just [ mkLineTextEdit "MyRec {foo, bar}" 12 10 20 + , mkPragmaTextEdit 2 + ] + , _tooltip = Just $ InL "Expand record wildcard (needs extension: NamedFieldPuns)" + , _paddingLeft = Just True + }] + , mkInlayHintsTest "Unused2" Nothing 12 $ \ih -> do + let mkLabelPart' = mkLabelPartOffsetLength "Unused2" + foo <- mkLabelPart' 6 4 "foo" + bar <- mkLabelPart' 7 4 "bar" + baz <- mkLabelPart' 8 4 "baz" + (@?=) ih + [defInlayHint { _position = Position 12 19 + , _label = InR [ foo, commaPart + , bar, commaPart + , baz + ] + , _textEdits = Just [ mkLineTextEdit "MyRec {foo, bar}" 12 10 20 + , mkPragmaTextEdit 2 + ] + , _tooltip = Just $ InL "Expand record wildcard (needs extension: NamedFieldPuns)" + , _paddingLeft = Just True + }] + , mkInlayHintsTest "WildcardOnly" Nothing 12 $ \ih -> do + let mkLabelPart' = mkLabelPartOffsetLength "WildcardOnly" + foo <- mkLabelPart' 6 4 "foo" + bar <- mkLabelPart' 7 4 "bar" + baz <- mkLabelPart' 8 4 "baz" + (@?=) ih + [defInlayHint { _position = Position 12 19 + , _label = InR [ foo, commaPart + , bar, commaPart + , baz + ] + , _textEdits = Just [ mkLineTextEdit "MyRec {foo, bar, baz}" 12 10 20 + , mkPragmaTextEdit 2 + ] + , _tooltip = Just $ InL "Expand record wildcard (needs extension: NamedFieldPuns)" + , _paddingLeft = Just True + }] + , mkInlayHintsTest "WithExplicitBind" Nothing 12 $ \ih -> do + let mkLabelPart' = mkLabelPartOffsetLength "WithExplicitBind" + bar <- mkLabelPart' 7 4 "bar" + baz <- mkLabelPart' 8 4 "baz" + (@?=) ih + [defInlayHint { _position = Position 12 31 + , _label = InR [ bar, commaPart + , baz + ] + , _textEdits = Just [ mkLineTextEdit "MyRec {foo = foo', bar, baz}" 12 10 32 + , mkPragmaTextEdit 2 + ] + , _tooltip = Just $ InL "Expand record wildcard (needs extension: NamedFieldPuns)" + , _paddingLeft = Just True + }] + , mkInlayHintsTest "WithPun" Nothing 13 $ \ih -> do + let mkLabelPart' = mkLabelPartOffsetLength "WithPun" + bar <- mkLabelPart' 8 4 "bar" + baz <- mkLabelPart' 9 4 "baz" + (@?=) ih + [defInlayHint { _position = Position 13 24 + , _label = InR [ bar, commaPart + , baz + ] + , _textEdits = Just [ mkLineTextEdit "MyRec {foo, bar, baz}" 13 10 25 ] + , _tooltip = Just $ InL "Expand record wildcard" + , _paddingLeft = Just True + }] + , mkInlayHintsTest "PolymorphicRecordConstruction" Nothing 15 $ \ih -> do + let mkLabelPart' = mkLabelPartOffsetLengthSub1 "PolymorphicRecordConstruction" + foo <- mkLabelPart' 5 4 "foo=" + bar <- mkLabelPart' 6 4 "bar=" + baz <- mkLabelPart' 7 4 "baz=" + (@?=) ih + [ defInlayHint { _position = Position 15 11 + , _label = InR [ foo ] + , _textEdits = Just [ mkLineTextEdit "MyRec { foo = a, bar = b, baz = c }" 15 5 16 ] + , _tooltip = Just $ InL "Expand positional record" + , _paddingLeft = Nothing + } + , defInlayHint { _position = Position 15 13 + , _label = InR [ bar ] + , _textEdits = Just [ mkLineTextEdit "MyRec { foo = a, bar = b, baz = c }" 15 5 16 ] + , _tooltip = Just $ InL "Expand positional record" + , _paddingLeft = Nothing + } + , defInlayHint { _position = Position 15 15 + , _label = InR [ baz ] + , _textEdits = Just [ mkLineTextEdit "MyRec { foo = a, bar = b, baz = c }" 15 5 16 ] + , _tooltip = Just $ InL "Expand positional record" + , _paddingLeft = Nothing + } + ] + ] ] +mkInlayHintsTest :: FilePath -> Maybe TestName -> UInt -> ([InlayHint] -> Assertion) -> TestTree +mkInlayHintsTest fp postfix line assert = + testCase (fp ++ concat postfix) $ + runSessionWithServer def plugin testDataDir $ do + doc <- openDoc (fp ++ ".hs") "haskell" + inlayHints <- getInlayHints doc (lineRange line) + liftIO $ assert inlayHints + where + lineRange line = Range (Position line 0) (Position line 1000) + mkTestNoAction :: TestName -> FilePath -> UInt -> UInt -> UInt -> UInt -> TestTree mkTestNoAction title fp x1 y1 x2 y2 = testCase title $ @@ -68,5 +339,60 @@ isExplicitFieldsCodeAction :: CodeAction -> Bool isExplicitFieldsCodeAction CodeAction {_title} = "Expand record wildcard" `T.isPrefixOf` _title +defInlayHint :: InlayHint +defInlayHint = + InlayHint + { _position = Position 0 0 + , _label = InR [] + , _kind = Nothing + , _textEdits = Nothing + , _tooltip = Nothing + , _paddingLeft = Nothing + , _paddingRight = Nothing + , _data_ = Nothing + } + +mkLabelPart :: (Text -> UInt) -> FilePath -> UInt -> UInt -> Text -> IO InlayHintLabelPart +mkLabelPart offset fp line start value = do + uri' <- uri + pure $ InlayHintLabelPart { _location = Just (location uri' line start) + , _value = value + , _tooltip = Nothing + , _command = Nothing + } + where + toUri = fromNormalizedUri . filePathToUri' . toNormalizedFilePath' + uri = canonicalizeUri $ toUri (testDataDir (fp ++ ".hs")) + location uri line char = Location uri (Range (Position line char) (Position line (char + offset value))) + +mkLabelPartOffsetLength :: FilePath -> UInt -> UInt -> Text -> IO InlayHintLabelPart +mkLabelPartOffsetLength = mkLabelPart (fromIntegral . T.length) + +mkLabelPartOffsetLengthSub1 :: FilePath -> UInt -> UInt -> Text -> IO InlayHintLabelPart +mkLabelPartOffsetLengthSub1 = mkLabelPart (fromIntegral . subtract 1 . T.length) + +commaPart :: InlayHintLabelPart +commaPart = + InlayHintLabelPart + { _location = Nothing + , _value = ", " + , _tooltip = Nothing + , _command = Nothing + } + +mkLineTextEdit :: Text -> UInt -> UInt -> UInt -> TextEdit +mkLineTextEdit newText line x y = + TextEdit + { _range = Range (Position line x) (Position line y) + , _newText = newText + } + +mkPragmaTextEdit :: UInt -> TextEdit +mkPragmaTextEdit line = + TextEdit + { _range = Range (Position line 0) (Position line 0) + , _newText = "{-# LANGUAGE NamedFieldPuns #-}\n" + } + testDataDir :: FilePath -testDataDir = "test" "testdata" +testDataDir = "plugins" "hls-explicit-record-fields-plugin" "test" "testdata" diff --git a/plugins/hls-explicit-record-fields-plugin/test/testdata/ConstructionDuplicateRecordFields.hs b/plugins/hls-explicit-record-fields-plugin/test/testdata/ConstructionDuplicateRecordFields.hs new file mode 100644 index 0000000000..420711f0da --- /dev/null +++ b/plugins/hls-explicit-record-fields-plugin/test/testdata/ConstructionDuplicateRecordFields.hs @@ -0,0 +1,17 @@ +{-# LANGUAGE Haskell2010 #-} +{-# LANGUAGE RecordWildCards #-} +{-# LANGUAGE DuplicateRecordFields #-} +module Construction where + +data MyRec = MyRec + { foo :: Int + , bar :: Int + , baz :: Char + } + +convertMe :: () -> MyRec +convertMe _ = + let foo = 3 + bar = 5 + baz = 'a' + in MyRec {..} diff --git a/plugins/hls-explicit-record-fields-plugin/test/testdata/HsExpanded1DuplicateRecordFields.hs b/plugins/hls-explicit-record-fields-plugin/test/testdata/HsExpanded1DuplicateRecordFields.hs new file mode 100644 index 0000000000..1e37d14668 --- /dev/null +++ b/plugins/hls-explicit-record-fields-plugin/test/testdata/HsExpanded1DuplicateRecordFields.hs @@ -0,0 +1,19 @@ +{-# LANGUAGE RecordWildCards #-} +{-# LANGUAGE RebindableSyntax #-} +{-# LANGUAGE NamedFieldPuns #-} +{-# LANGUAGE DuplicateRecordFields #-} +module HsExpanded1DuplicateRecordFields where +import Prelude + +ifThenElse :: Int -> Int -> Int -> Int +ifThenElse x y z = x + y + z + +data MyRec = MyRec + { foo :: Int } + +myRecExample = MyRec 5 + +convertMe :: Int +convertMe = + if (let MyRec {..} = myRecExample + in foo) then 1 else 2 diff --git a/plugins/hls-explicit-record-fields-plugin/test/testdata/PolymorphicRecordConstruction.expected.hs b/plugins/hls-explicit-record-fields-plugin/test/testdata/PolymorphicRecordConstruction.expected.hs new file mode 100644 index 0000000000..f289508524 --- /dev/null +++ b/plugins/hls-explicit-record-fields-plugin/test/testdata/PolymorphicRecordConstruction.expected.hs @@ -0,0 +1,16 @@ +{-# LANGUAGE Haskell2010 #-} + +module PolymorphicRecordConstruction where + +data MyRec m = MyRec + { foo :: Int + , bar :: Int + , baz :: Char + } + +convertMe :: () -> MyRec () +convertMe _ = + let a = 3 + b = 5 + c = 'a' + in MyRec { foo = a, bar = b, baz = c } diff --git a/plugins/hls-explicit-record-fields-plugin/test/testdata/PolymorphicRecordConstruction.hs b/plugins/hls-explicit-record-fields-plugin/test/testdata/PolymorphicRecordConstruction.hs new file mode 100644 index 0000000000..f8b9791da5 --- /dev/null +++ b/plugins/hls-explicit-record-fields-plugin/test/testdata/PolymorphicRecordConstruction.hs @@ -0,0 +1,16 @@ +{-# LANGUAGE Haskell2010 #-} + +module PolymorphicRecordConstruction where + +data MyRec m = MyRec + { foo :: Int + , bar :: Int + , baz :: Char + } + +convertMe :: () -> MyRec () +convertMe _ = + let a = 3 + b = 5 + c = 'a' + in MyRec a b c diff --git a/plugins/hls-explicit-record-fields-plugin/test/testdata/PositionalConstruction.expected.hs b/plugins/hls-explicit-record-fields-plugin/test/testdata/PositionalConstruction.expected.hs new file mode 100644 index 0000000000..667fc25fe0 --- /dev/null +++ b/plugins/hls-explicit-record-fields-plugin/test/testdata/PositionalConstruction.expected.hs @@ -0,0 +1,16 @@ +{-# LANGUAGE Haskell2010 #-} + +module PositionalConstruction where + +data MyRec = MyRec + { foo :: Int + , bar :: Int + , baz :: Char + } + +convertMe :: () -> MyRec +convertMe _ = + let a = 3 + b = 5 + c = 'a' + in MyRec { foo = a, bar = b, baz = c } diff --git a/plugins/hls-explicit-record-fields-plugin/test/testdata/PositionalConstruction.hs b/plugins/hls-explicit-record-fields-plugin/test/testdata/PositionalConstruction.hs new file mode 100644 index 0000000000..0b2f8d9f86 --- /dev/null +++ b/plugins/hls-explicit-record-fields-plugin/test/testdata/PositionalConstruction.hs @@ -0,0 +1,16 @@ +{-# LANGUAGE Haskell2010 #-} + +module PositionalConstruction where + +data MyRec = MyRec + { foo :: Int + , bar :: Int + , baz :: Char + } + +convertMe :: () -> MyRec +convertMe _ = + let a = 3 + b = 5 + c = 'a' + in MyRec a b c diff --git a/plugins/hls-explicit-record-fields-plugin/test/testdata/PositionalConstructionDuplicateRecordFields.hs b/plugins/hls-explicit-record-fields-plugin/test/testdata/PositionalConstructionDuplicateRecordFields.hs new file mode 100644 index 0000000000..5227af9a83 --- /dev/null +++ b/plugins/hls-explicit-record-fields-plugin/test/testdata/PositionalConstructionDuplicateRecordFields.hs @@ -0,0 +1,17 @@ +{-# LANGUAGE Haskell2010 #-} +{-# LANGUAGE DuplicateRecordFields #-} +module PositionalConstruction where + +data MyRec = MyRec + { foo :: Int + , bar :: Int + , baz :: Char + } + +convertMe :: () -> MyRec +convertMe _ = + let a = 3 + b = 5 + c = 'a' + in MyRec a b c + diff --git a/plugins/hls-explicit-record-fields-plugin/test/testdata/hie.yaml b/plugins/hls-explicit-record-fields-plugin/test/testdata/hie.yaml new file mode 100644 index 0000000000..ee67c73150 --- /dev/null +++ b/plugins/hls-explicit-record-fields-plugin/test/testdata/hie.yaml @@ -0,0 +1 @@ +cradle: {direct: {arguments: []}} diff --git a/plugins/hls-explicit-record-fields-plugin/test/testdata/noop/PartiallyAppliedCon.hs b/plugins/hls-explicit-record-fields-plugin/test/testdata/noop/PartiallyAppliedCon.hs new file mode 100644 index 0000000000..2f6f52e30b --- /dev/null +++ b/plugins/hls-explicit-record-fields-plugin/test/testdata/noop/PartiallyAppliedCon.hs @@ -0,0 +1,8 @@ +{-# LANGUAGE Haskell2010 #-} + +module PartiallyAppliedCon where + +data T = MkT { fa :: Int, fb :: Char } + +foo :: Int -> Char -> T +foo x = MkT x diff --git a/plugins/hls-floskell-plugin/LICENSE b/plugins/hls-floskell-plugin/LICENSE deleted file mode 100644 index 16502c47e2..0000000000 --- a/plugins/hls-floskell-plugin/LICENSE +++ /dev/null @@ -1,201 +0,0 @@ - Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ - - TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - - 1. Definitions. - - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. - - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. - - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. - - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. - - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. - - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. - - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). - - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. - - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution." - - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. - - 2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. - - 3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. - - 4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: - - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and - - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and - - (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. - - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. - - 5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. - - 6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. - - 7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. - - 8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. - - 9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. - - END OF TERMS AND CONDITIONS - - APPENDIX: How to apply the Apache License to your work. - - To apply the Apache License to your work, attach the following - boilerplate notice, with the fields enclosed by brackets "[]" - replaced with your own identifying information. (Don't include - the brackets!) The text should be enclosed in the appropriate - comment syntax for the file format. We also recommend that a - file or class name and description of purpose be included on the - same "printed page" as the copyright notice for easier - identification within third-party archives. - - Copyright 2021 The Haskell IDE team - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. diff --git a/plugins/hls-floskell-plugin/hls-floskell-plugin.cabal b/plugins/hls-floskell-plugin/hls-floskell-plugin.cabal deleted file mode 100644 index 3c0a6b0cfb..0000000000 --- a/plugins/hls-floskell-plugin/hls-floskell-plugin.cabal +++ /dev/null @@ -1,53 +0,0 @@ -cabal-version: 2.4 -name: hls-floskell-plugin -version: 2.4.0.0 -synopsis: Integration with the Floskell code formatter -description: - Please see the README on GitHub at - -license: Apache-2.0 -license-file: LICENSE -author: The Haskell IDE Team -copyright: The Haskell IDE Team -maintainer: alan.zimm@gmail.com -category: Development -build-type: Simple -extra-source-files: - LICENSE - test/testdata/**/*.hs - -source-repository head - type: git - location: https://github.com/haskell/haskell-language-server.git - -library - -- Plugins that need exactprint have not been updated for 9.8 yet - if impl(ghc >= 9.5) - buildable: False - exposed-modules: Ide.Plugin.Floskell - hs-source-dirs: src - build-depends: - , base >=4.12 && <5 - , floskell ^>=0.10 - , ghcide == 2.4.0.0 - , hls-plugin-api == 2.4.0.0 - , lsp-types ^>=2.1 - , mtl - , text - , transformers - - default-language: Haskell2010 - -test-suite tests - if impl(ghc >= 9.5) - buildable: False - type: exitcode-stdio-1.0 - default-language: Haskell2010 - hs-source-dirs: test - main-is: Main.hs - ghc-options: -threaded -rtsopts -with-rtsopts=-N - build-depends: - , base - , filepath - , hls-floskell-plugin - , hls-test-utils == 2.4.0.0 diff --git a/plugins/hls-floskell-plugin/src/Ide/Plugin/Floskell.hs b/plugins/hls-floskell-plugin/src/Ide/Plugin/Floskell.hs index 7a7deaf629..f78761958c 100644 --- a/plugins/hls-floskell-plugin/src/Ide/Plugin/Floskell.hs +++ b/plugins/hls-floskell-plugin/src/Ide/Plugin/Floskell.hs @@ -1,3 +1,4 @@ +{-# LANGUAGE CPP #-} {-# LANGUAGE OverloadedStrings #-} module Ide.Plugin.Floskell @@ -5,12 +6,13 @@ module Ide.Plugin.Floskell , provider ) where -import Control.Monad.Except (throwError) +import Control.Monad.Except (throwError) import Control.Monad.IO.Class -import qualified Data.Text as T -import qualified Data.Text.Lazy as TL -import qualified Data.Text.Lazy.Encoding as TL -import Development.IDE hiding (pluginHandlers) +import Data.List (find) +import qualified Data.Text as T +import qualified Data.Text.Lazy as TL +import Development.IDE hiding (pluginHandlers) +import Development.IDE.Core.PluginUtils (mkFormattingHandlers) import Floskell import Ide.Plugin.Error import Ide.PluginUtils @@ -20,9 +22,11 @@ import Language.LSP.Protocol.Types -- --------------------------------------------------------------------- descriptor :: PluginId -> PluginDescriptor IdeState -descriptor plId = (defaultPluginDescriptor plId) +descriptor plId = (defaultPluginDescriptor plId desc) { pluginHandlers = mkFormattingHandlers provider } + where + desc = "Provides formatting of Haskell files via floskell. Built with floskell-" <> VERSION_floskell -- --------------------------------------------------------------------- @@ -30,16 +34,16 @@ descriptor plId = (defaultPluginDescriptor plId) -- Formats the given source in either a given Range or the whole Document. -- If the provider fails an error is returned that can be displayed to the user. provider :: FormattingHandler IdeState -provider _ideState typ contents fp _ = do +provider _ideState _token typ contents fp _ = do let file = fromNormalizedFilePath fp config <- liftIO $ findConfigOrDefault file let (range, selectedContents) = case typ of FormatText -> (fullRange contents, contents) FormatRange r -> (normalize r, extractTextInRange (extendToFullLines r) contents) - result = reformat config (Just file) . TL.encodeUtf8 $ TL.fromStrict selectedContents + result = reformat config (Just file) $ TL.fromStrict selectedContents case result of Left err -> throwError $ PluginInternalError $ T.pack $ "floskellCmd: " ++ err - Right new -> pure $ InL [TextEdit range . TL.toStrict $ TL.decodeUtf8 new] + Right new -> pure $ InL [TextEdit range $ TL.toStrict new] -- | Find Floskell Config, user and system wide or provides a default style. -- Every directory of the filepath will be searched to find a user configuration. @@ -51,7 +55,8 @@ findConfigOrDefault file = do case mbConf of Just confFile -> readAppConfig confFile Nothing -> - let gibiansky = head (filter (\s -> styleName s == "gibiansky") styles) - in pure $ defaultAppConfig { appStyle = gibiansky } + pure $ case find (\s -> styleName s == "gibiansky") styles of + Just gibiansky -> defaultAppConfig { appStyle = gibiansky } + Nothing -> defaultAppConfig -- --------------------------------------------------------------------- diff --git a/plugins/hls-floskell-plugin/test/Main.hs b/plugins/hls-floskell-plugin/test/Main.hs index baf5513287..ba4c707130 100644 --- a/plugins/hls-floskell-plugin/test/Main.hs +++ b/plugins/hls-floskell-plugin/test/Main.hs @@ -27,4 +27,4 @@ goldenWithFloskell :: TestName -> FilePath -> FilePath -> (TextDocumentIdentifie goldenWithFloskell title path desc = goldenWithHaskellDocFormatter def floskellPlugin "floskell" def title testDataDir path desc "hs" testDataDir :: FilePath -testDataDir = "test" "testdata" +testDataDir = "plugins" "hls-floskell-plugin" "test" "testdata" diff --git a/plugins/hls-fourmolu-plugin/LICENSE b/plugins/hls-fourmolu-plugin/LICENSE deleted file mode 100644 index 16502c47e2..0000000000 --- a/plugins/hls-fourmolu-plugin/LICENSE +++ /dev/null @@ -1,201 +0,0 @@ - Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ - - TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - - 1. Definitions. - - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. - - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. - - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. - - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. - - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. - - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. - - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). - - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. - - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution." - - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. - - 2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. - - 3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. - - 4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: - - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and - - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and - - (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. - - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. - - 5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. - - 6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. - - 7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. - - 8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. - - 9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. - - END OF TERMS AND CONDITIONS - - APPENDIX: How to apply the Apache License to your work. - - To apply the Apache License to your work, attach the following - boilerplate notice, with the fields enclosed by brackets "[]" - replaced with your own identifying information. (Don't include - the brackets!) The text should be enclosed in the appropriate - comment syntax for the file format. We also recommend that a - file or class name and description of purpose be included on the - same "printed page" as the copyright notice for easier - identification within third-party archives. - - Copyright 2021 The Haskell IDE team - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. diff --git a/plugins/hls-fourmolu-plugin/hls-fourmolu-plugin.cabal b/plugins/hls-fourmolu-plugin/hls-fourmolu-plugin.cabal deleted file mode 100644 index fd10d201fb..0000000000 --- a/plugins/hls-fourmolu-plugin/hls-fourmolu-plugin.cabal +++ /dev/null @@ -1,81 +0,0 @@ -cabal-version: 2.4 -name: hls-fourmolu-plugin -version: 2.4.0.0 -synopsis: Integration with the Fourmolu code formatter -description: - Please see the README on GitHub at - -license: Apache-2.0 -license-file: LICENSE -author: The Haskell IDE Team -copyright: The Haskell IDE Team -homepage: https://github.com/haskell/haskell-language-server -bug-reports: https://github.com/haskell/haskell-language-server/issues -maintainer: alan.zimm@gmail.com -category: Development -build-type: Simple -extra-source-files: - LICENSE - test/testdata/**/*.hs - -source-repository head - type: git - location: git://github.com/haskell/haskell-language-server.git - -library - -- Plugins that need exactprint have not been updated for 9.8 yet - if impl(ghc >= 9.8) - buildable: False - else - buildable: True - exposed-modules: - Ide.Plugin.Fourmolu - hs-source-dirs: src - ghc-options: -Wall - build-depends: - , base >=4.12 && <5 - , filepath - , ghc - , ghc-boot-th - , ghcide == 2.4.0.0 - , hls-plugin-api == 2.4.0.0 - , lens - , lsp - , mtl - , process-extras >= 0.7.1 - , text - , transformers - - if impl(ghc >= 9.0) && impl(ghc < 9.2) - build-depends: fourmolu ^>= 0.11 - elif impl(ghc >= 9.2) && impl(ghc < 9.8) - build-depends: fourmolu ^>= 0.14 - else - buildable: False - - -- fourmolu 0.9.0 fails to build on Windows CI for reasons unknown - if impl(ghc >= 9.2) && os(windows) && impl(ghc < 9.4) - build-depends: fourmolu > 0.9.0.0 || < 0.9.0.0 - default-language: Haskell2010 - -test-suite tests - if impl(ghc >= 9.8) - buildable: False - else - buildable: True - type: exitcode-stdio-1.0 - default-language: Haskell2010 - hs-source-dirs: test - main-is: Main.hs - ghc-options: -Wall -threaded -rtsopts -with-rtsopts=-N - build-tool-depends: - fourmolu:fourmolu - build-depends: - , base >=4.12 && <5 - , aeson - , containers - , filepath - , hls-fourmolu-plugin - , hls-plugin-api - , hls-test-utils == 2.4.0.0 - , lsp-test diff --git a/plugins/hls-fourmolu-plugin/src/Ide/Plugin/Fourmolu.hs b/plugins/hls-fourmolu-plugin/src/Ide/Plugin/Fourmolu.hs index c125c5e957..c12866d7f3 100644 --- a/plugins/hls-fourmolu-plugin/src/Ide/Plugin/Fourmolu.hs +++ b/plugins/hls-fourmolu-plugin/src/Ide/Plugin/Fourmolu.hs @@ -1,12 +1,9 @@ {-# LANGUAGE CPP #-} {-# LANGUAGE DataKinds #-} {-# LANGUAGE LambdaCase #-} -{-# LANGUAGE NamedFieldPuns #-} {-# LANGUAGE OverloadedLabels #-} {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE RecordWildCards #-} -{-# LANGUAGE TypeApplications #-} -{-# LANGUAGE TypeOperators #-} module Ide.Plugin.Fourmolu ( descriptor, @@ -15,94 +12,90 @@ module Ide.Plugin.Fourmolu ( ) where import Control.Exception -import Control.Lens ((^.)) -import Control.Monad (guard) -import Control.Monad.Error.Class (MonadError (throwError)) -import Control.Monad.IO.Class (MonadIO (liftIO)) -import Control.Monad.Trans.Class (MonadTrans (lift)) -import Control.Monad.Trans.Except (ExceptT (..), runExceptT) -import Data.Bifunctor (bimap) -import Data.List (intercalate) -import Data.Maybe (catMaybes) -import Data.Text (Text) -import qualified Data.Text as T -import Development.IDE hiding (pluginHandlers) -import Development.IDE.GHC.Compat as Compat hiding (Cpp, Warning, - hang, vcat) -import qualified Development.IDE.GHC.Compat.Util as S -import GHC.LanguageExtensions.Type (Extension (Cpp)) +import Control.Lens ((^.)) +import Control.Monad (guard) +import Control.Monad.Error.Class (MonadError (throwError)) +import Control.Monad.IO.Class (MonadIO (liftIO)) +import Control.Monad.Trans.Class (MonadTrans (lift)) +import Control.Monad.Trans.Except (ExceptT (..), runExceptT) +import Data.Bifunctor (bimap) +import Data.List (intercalate) +import Data.Maybe (catMaybes) +import Data.Text (Text) +import qualified Data.Text as T +import Data.Version (showVersion) +import Development.IDE hiding (pluginHandlers) +import Development.IDE.Core.PluginUtils (mkFormattingHandlers) +import Development.IDE.GHC.Compat as Compat hiding (Cpp, + Warning, hang, + vcat) +import qualified Development.IDE.GHC.Compat.Util as S +import GHC.LanguageExtensions.Type (Extension (Cpp)) import Ide.Plugin.Error import Ide.Plugin.Properties -import Ide.PluginUtils (makeDiffTextEdit) +import Ide.PluginUtils (makeDiffTextEdit) import Ide.Types -import Language.LSP.Protocol.Lens (HasTabSize (tabSize)) +import Language.LSP.Protocol.Lens (HasTabSize (tabSize)) import Language.LSP.Protocol.Message import Language.LSP.Protocol.Types -import Language.LSP.Server hiding (defaultConfig) +import Language.LSP.Server hiding (defaultConfig) import Ormolu import Ormolu.Config +import qualified Paths_fourmolu as Fourmolu import System.Exit import System.FilePath -import System.Process.Run (cwd, proc) -import System.Process.Text (readCreateProcessWithExitCode) -import Text.Read (readMaybe) +import System.Process.Run (cwd, proc) +import System.Process.Text (readCreateProcessWithExitCode) +import Text.Read (readMaybe) + +#if MIN_VERSION_fourmolu(0,16,0) +import qualified Data.Yaml as Yaml +#endif descriptor :: Recorder (WithPriority LogEvent) -> PluginId -> PluginDescriptor IdeState descriptor recorder plId = - (defaultPluginDescriptor plId) + (defaultPluginDescriptor plId desc) { pluginHandlers = mkFormattingHandlers $ provider recorder plId , pluginConfigDescriptor = defaultConfigDescriptor{configCustomConfig = mkCustomConfig properties} } + where + desc = T.pack $ "Provides formatting of Haskell files via fourmolu. Built with fourmolu-" <> showVersion Fourmolu.version -properties :: Properties '[ 'PropertyKey "external" 'TBoolean] +properties :: Properties '[ 'PropertyKey "external" 'TBoolean, 'PropertyKey "path" 'TString] properties = emptyProperties + & defineStringProperty + #path + "Set path to executable (for \"external\" mode)." + "fourmolu" & defineBooleanProperty #external - "Call out to an external \"fourmolu\" executable, rather than using the bundled library" + "Call out to an external \"fourmolu\" executable, rather than using the bundled library." False provider :: Recorder (WithPriority LogEvent) -> PluginId -> FormattingHandler IdeState -provider recorder plId ideState typ contents fp fo = ExceptT $ withIndefiniteProgress title Cancellable $ runExceptT $ do +provider recorder plId ideState token typ contents fp fo = ExceptT $ pluginWithIndefiniteProgress title token Cancellable $ \_updater -> runExceptT $ do fileOpts <- maybe [] (convertDynFlags . hsc_dflags . hscEnv) <$> liftIO (runAction "Fourmolu" ideState $ use GhcSession fp) useCLI <- liftIO $ runAction "Fourmolu" ideState $ usePropertyAction #external plId properties + fourmoluExePath <- fmap T.unpack $ liftIO $ runAction "Fourmolu" ideState $ usePropertyAction #path plId properties if useCLI then ExceptT . liftIO $ handle @IOException (pure . Left . PluginInternalError . T.pack . show) $ - runExceptT (cliHandler fileOpts) + runExceptT (cliHandler fourmoluExePath fileOpts) else do - logWith recorder Debug $ LogCompiledInVersion VERSION_fourmolu - FourmoluConfig{..} <- - liftIO (loadConfigFile fp') >>= \case - ConfigLoaded file opts -> do - logWith recorder Info $ ConfigPath file - pure opts - ConfigNotFound searchDirs -> do - logWith recorder Info $ NoConfigPath searchDirs - pure emptyConfig - ConfigParseError f err -> do - lift $ sendNotification SMethod_WindowShowMessage $ - ShowMessageParams - { _type_ = MessageType_Error - , _message = errorMessage - } - throwError $ PluginInternalError errorMessage - where - errorMessage = "Failed to load " <> T.pack f <> ": " <> T.pack (show err) - + logWith recorder Debug $ LogCompiledInVersion (showVersion Fourmolu.version) + FourmoluConfig{..} <- loadConfig recorder fp' let config = -#if MIN_VERSION_fourmolu(0,13,0) - refineConfig ModuleSource Nothing Nothing Nothing -#endif - defaultConfig - { cfgDynOptions = map DynOption fileOpts - , cfgFixityOverrides = cfgFileFixities - , cfgRegion = region - , cfgDebug = False - , cfgPrinterOpts = resolvePrinterOpts [lspPrinterOpts, cfgFilePrinterOpts] - } + refineConfig ModuleSource Nothing Nothing Nothing $ + defaultConfig + { cfgDynOptions = map DynOption fileOpts + , cfgFixityOverrides = cfgFileFixities + , cfgRegion = region + , cfgDebug = False + , cfgPrinterOpts = resolvePrinterOpts [lspPrinterOpts, cfgFilePrinterOpts] + } ExceptT . liftIO $ bimap (PluginInternalError . T.pack . show) (InL . makeDiffTextEdit contents) <$> try @OrmoluException (ormolu config fp' contents) @@ -115,10 +108,10 @@ provider recorder plId ideState typ contents fp fo = ExceptT $ withIndefinitePro RegionIndices Nothing Nothing FormatRange (Range (Position sl _) (Position el _)) -> RegionIndices (Just $ fromIntegral $ sl + 1) (Just $ fromIntegral $ el + 1) - cliHandler :: [String] -> ExceptT PluginError IO ([TextEdit] |? Null) - cliHandler fileOpts = do + cliHandler :: FilePath -> [String] -> ExceptT PluginError IO ([TextEdit] |? Null) + cliHandler path fileOpts = do CLIVersionInfo{noCabal} <- do -- check Fourmolu version so that we know which flags to use - (exitCode, out, _err) <- liftIO $ readCreateProcessWithExitCode ( proc "fourmolu" ["-v"] ) "" + (exitCode, out, _err) <- liftIO $ readCreateProcessWithExitCode ( proc path ["-v"] ) "" let version = do guard $ exitCode == ExitSuccess "fourmolu" : v : _ <- pure $ T.words out @@ -137,7 +130,7 @@ provider recorder plId ideState typ contents fp fo = ExceptT $ withIndefinitePro } (exitCode, out, err) <- -- run Fourmolu liftIO $ readCreateProcessWithExitCode - ( proc "fourmolu" $ + ( proc path $ map ("-o" <>) fileOpts <> mwhen noCabal ["--no-cabal"] <> catMaybes @@ -154,6 +147,49 @@ provider recorder plId ideState typ contents fp fo = ExceptT $ withIndefinitePro logWith recorder Info $ StdErr err throwError $ PluginInternalError $ "Fourmolu failed with exit code " <> T.pack (show n) +loadConfig :: + Recorder (WithPriority LogEvent) -> + FilePath -> + ExceptT PluginError (HandlerM Ide.Types.Config) FourmoluConfig +#if MIN_VERSION_fourmolu(0,16,0) +loadConfig recorder fp = do + liftIO (findConfigFile fp) >>= \case + Left (ConfigNotFound searchDirs) -> do + logWith recorder Info $ NoConfigPath searchDirs + pure emptyConfig + Right file -> do + logWith recorder Info $ ConfigPath file + liftIO (Yaml.decodeFileEither file) >>= \case + Left err -> do + let errorMessage = "Failed to load " <> T.pack file <> ": " <> T.pack (show err) + lift $ pluginSendNotification SMethod_WindowShowMessage $ + ShowMessageParams + { _type_ = MessageType_Error + , _message = errorMessage + } + throwError $ PluginInternalError errorMessage + Right cfg -> do + pure cfg +#else +loadConfig recorder fp = do + liftIO (loadConfigFile fp) >>= \case + ConfigLoaded file opts -> do + logWith recorder Info $ ConfigPath file + pure opts + ConfigNotFound searchDirs -> do + logWith recorder Info $ NoConfigPath searchDirs + pure emptyConfig + ConfigParseError f err -> do + lift $ pluginSendNotification SMethod_WindowShowMessage $ + ShowMessageParams + { _type_ = MessageType_Error + , _message = errorMessage + } + throwError $ PluginInternalError errorMessage + where + errorMessage = "Failed to load " <> T.pack f <> ": " <> T.pack (show err) +#endif + data LogEvent = NoVersion Text | ConfigPath FilePath @@ -193,8 +229,3 @@ newtype CLIVersionInfo = CLIVersionInfo mwhen :: Monoid a => Bool -> a -> a mwhen b x = if b then x else mempty - -#if !MIN_VERSION_fourmolu(0,14,0) -resolvePrinterOpts :: [PrinterOptsPartial] -> PrinterOptsTotal -resolvePrinterOpts = foldr fillMissingPrinterOpts defaultPrinterOpts -#endif diff --git a/plugins/hls-fourmolu-plugin/test/Main.hs b/plugins/hls-fourmolu-plugin/test/Main.hs index 36d462b833..483fae8ac8 100644 --- a/plugins/hls-fourmolu-plugin/test/Main.hs +++ b/plugins/hls-fourmolu-plugin/test/Main.hs @@ -39,4 +39,4 @@ goldenWithFourmolu cli title path desc = goldenWithHaskellDocFormatter def fourm conf = def{plcConfig = KM.fromList ["external" .= cli]} testDataDir :: FilePath -testDataDir = "test" "testdata" +testDataDir = "plugins" "hls-fourmolu-plugin" "test" "testdata" diff --git a/plugins/hls-gadt-plugin/LICENSE b/plugins/hls-gadt-plugin/LICENSE deleted file mode 100644 index 261eeb9e9f..0000000000 --- a/plugins/hls-gadt-plugin/LICENSE +++ /dev/null @@ -1,201 +0,0 @@ - Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ - - TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - - 1. Definitions. - - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. - - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. - - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. - - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. - - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. - - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. - - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). - - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. - - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution." - - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. - - 2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. - - 3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. - - 4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: - - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and - - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and - - (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. - - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. - - 5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. - - 6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. - - 7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. - - 8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. - - 9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. - - END OF TERMS AND CONDITIONS - - APPENDIX: How to apply the Apache License to your work. - - To apply the Apache License to your work, attach the following - boilerplate notice, with the fields enclosed by brackets "[]" - replaced with your own identifying information. (Don't include - the brackets!) The text should be enclosed in the appropriate - comment syntax for the file format. We also recommend that a - file or class name and description of purpose be included on the - same "printed page" as the copyright notice for easier - identification within third-party archives. - - Copyright [yyyy] [name of copyright owner] - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. diff --git a/plugins/hls-gadt-plugin/hls-gadt-plugin.cabal b/plugins/hls-gadt-plugin/hls-gadt-plugin.cabal deleted file mode 100644 index d1251c2fdd..0000000000 --- a/plugins/hls-gadt-plugin/hls-gadt-plugin.cabal +++ /dev/null @@ -1,75 +0,0 @@ -cabal-version: 2.4 -name: hls-gadt-plugin -version: 2.4.0.0 -synopsis: Convert to GADT syntax plugin -description: - Please see the README on GitHub at - -license: Apache-2.0 -license-file: LICENSE -author: Lei Zhu -maintainer: julytreee@gmail.com -category: Development -build-type: Simple -extra-source-files: - LICENSE - test/testdata/*.hs - -source-repository head - type: git - location: https://github.com/haskell/haskell-language-server.git - -library - -- Plugins that need exactprint have not been updated for 9.8 yet - if impl(ghc >= 9.8) - buildable: False - else - buildable: True - exposed-modules: Ide.Plugin.GADT - other-modules: Ide.Plugin.GHC - - hs-source-dirs: src - build-depends: - , aeson - , base >=4.12 && <5 - , containers - , extra - , ghc - , ghcide == 2.4.0.0 - , ghc-boot-th - , ghc-exactprint - , hls-plugin-api == 2.4.0.0 - , hls-refactor-plugin - , lens - , lsp >=2.3 - , mtl - , text - , transformers - , unordered-containers - - ghc-options: - -Wall - -Wno-name-shadowing - -Wno-unticked-promoted-constructors - default-language: Haskell2010 - default-extensions: DataKinds - -test-suite tests - if impl(ghc >= 9.8) - buildable: False - else - buildable: True - type: exitcode-stdio-1.0 - default-language: Haskell2010 - hs-source-dirs: test - main-is: Main.hs - ghc-options: -threaded -rtsopts -with-rtsopts=-N - build-depends: - , base - , filepath - , hls-gadt-plugin - , hls-test-utils == 2.4.0.0 - , lens - , lsp - , lsp-test - , text diff --git a/plugins/hls-gadt-plugin/src/Ide/Plugin/GADT.hs b/plugins/hls-gadt-plugin/src/Ide/Plugin/GADT.hs index 6146ae2ee7..7aefa2c524 100644 --- a/plugins/hls-gadt-plugin/src/Ide/Plugin/GADT.hs +++ b/plugins/hls-gadt-plugin/src/Ide/Plugin/GADT.hs @@ -1,11 +1,8 @@ {-# LANGUAGE DeriveAnyClass #-} -{-# LANGUAGE DeriveGeneric #-} {-# LANGUAGE GADTs #-} {-# LANGUAGE LambdaCase #-} {-# LANGUAGE OverloadedStrings #-} -{-# LANGUAGE RankNTypes #-} {-# LANGUAGE RecordWildCards #-} -{-# LANGUAGE TypeOperators #-} {-# LANGUAGE ViewPatterns #-} module Ide.Plugin.GADT (descriptor) where @@ -35,10 +32,9 @@ import Ide.Types import qualified Language.LSP.Protocol.Lens as L import Language.LSP.Protocol.Message import Language.LSP.Protocol.Types -import Language.LSP.Server (sendRequest) descriptor :: PluginId -> PluginDescriptor IdeState -descriptor plId = (defaultPluginDescriptor plId) +descriptor plId = (defaultPluginDescriptor plId "Provides a code action to convert datatypes to GADT syntax") { Ide.Types.pluginHandlers = mkPluginHandler SMethod_TextDocumentCodeAction codeActionHandler , pluginCommands = @@ -56,8 +52,8 @@ toGADTSyntaxCommandId = "GADT.toGADT" -- | A command replaces H98 data decl with GADT decl in place toGADTCommand :: PluginId -> CommandFunction IdeState ToGADTParams -toGADTCommand pId@(PluginId pId') state ToGADTParams{..} = withExceptT handleGhcidePluginError $ do - nfp <- withExceptT (GhcidePluginErrors) $ getNormalizedFilePathE uri +toGADTCommand pId@(PluginId pId') state _ ToGADTParams{..} = withExceptT handleGhcidePluginError $ do + nfp <- withExceptT GhcidePluginErrors $ getNormalizedFilePathE uri (decls, exts) <- getInRangeH98DeclsAndExts state range nfp (L ann decl) <- case decls of [d] -> pure d @@ -73,7 +69,7 @@ toGADTCommand pId@(PluginId pId') state ToGADTParams{..} = withExceptT handleGhc pragma <- withExceptT GhcidePluginErrors $ getFirstPragma pId state nfp let insertEdit = [insertNewPragma pragma GADTs | all (`notElem` exts) [GADTSyntax, GADTs]] - _ <- lift $ sendRequest + _ <- lift $ pluginSendRequest SMethod_WorkspaceApplyEdit (ApplyWorkspaceEditParams Nothing (workSpaceEdit nfp (TextEdit range txt : insertEdit))) (\_ -> pure ()) @@ -88,7 +84,7 @@ toGADTCommand pId@(PluginId pId') state ToGADTParams{..} = withExceptT handleGhc codeActionHandler :: PluginMethodHandler IdeState Method_TextDocumentCodeAction codeActionHandler state plId (CodeActionParams _ _ doc range _) = withExceptT handleGhcidePluginError $ do - nfp <- withExceptT (GhcidePluginErrors) $ getNormalizedFilePathE (doc ^. L.uri) + nfp <- withExceptT GhcidePluginErrors $ getNormalizedFilePathE (doc ^. L.uri) (inRangeH98Decls, _) <- getInRangeH98DeclsAndExts state range nfp let actions = map (mkAction . printOutputable . tcdLName . unLoc) inRangeH98Decls pure $ InL actions @@ -138,8 +134,8 @@ handleGhcidePluginError = \case UnexpectedNumberOfDeclarations nums -> do PluginInternalError $ "Expected one declaration but found: " <> T.pack (show nums) FailedToFindDataDeclRange -> - PluginInternalError $ "Unable to get data decl range" + PluginInternalError "Unable to get data decl range" PrettyGadtError errMsg -> - PluginInternalError $ errMsg + PluginInternalError errMsg GhcidePluginErrors errors -> errors diff --git a/plugins/hls-gadt-plugin/src/Ide/Plugin/GHC.hs b/plugins/hls-gadt-plugin/src/Ide/Plugin/GHC.hs index 6d76471a77..f5687a9db3 100644 --- a/plugins/hls-gadt-plugin/src/Ide/Plugin/GHC.hs +++ b/plugins/hls-gadt-plugin/src/Ide/Plugin/GHC.hs @@ -1,39 +1,53 @@ -{-# LANGUAGE CPP #-} -{-# LANGUAGE FlexibleContexts #-} -{-# LANGUAGE GADTs #-} -{-# LANGUAGE LambdaCase #-} -{-# LANGUAGE PatternSynonyms #-} -{-# LANGUAGE RankNTypes #-} -{-# LANGUAGE RecordWildCards #-} -{-# LANGUAGE TupleSections #-} -{-# LANGUAGE TypeApplications #-} +{-# LANGUAGE CPP #-} +{-# LANGUAGE GADTs #-} +{-# LANGUAGE LambdaCase #-} +{-# LANGUAGE PatternSynonyms #-} +{-# LANGUAGE RecordWildCards #-} {-# OPTIONS_GHC -Wno-missing-signatures #-} {-# OPTIONS_GHC -Wno-overlapping-patterns #-} module Ide.Plugin.GHC where +#if !MIN_VERSION_ghc(9,11,0) import Data.Functor ((<&>)) +#endif import Data.List.Extra (stripInfix) -import qualified Data.List.NonEmpty as NE import qualified Data.Text as T import Development.IDE import Development.IDE.GHC.Compat import Development.IDE.GHC.Compat.ExactPrint +import GHC.Parser.Annotation (DeltaPos (..), + EpAnn (..), + EpAnnComments (EpaComments)) +#if MIN_VERSION_ghc(9,11,0) +import GHC.Parser.Annotation (EpToken (..)) +#endif import Ide.PluginUtils (subRange) import Language.Haskell.GHC.ExactPrint.Parsers (parseDecl) -import GHC.Parser.Annotation (AddEpAnn (..), - Anchor (Anchor), +-- See Note [Guidelines For Using CPP In GHCIDE Import Statements] + +import qualified Data.List.NonEmpty as NE + +#if !MIN_VERSION_ghc(9,9,0) +import GHC.Parser.Annotation (Anchor (Anchor), AnchorOperation (MovedAnchor), - DeltaPos (..), - EpAnn (..), - EpAnnComments (EpaComments), - EpaLocation (EpaDelta), SrcSpanAnn' (SrcSpanAnn), + TokenLocation (..), spanAsAnchor) -#if MIN_VERSION_ghc(9,5,0) -import GHC.Parser.Annotation (TokenLocation (..)) #endif -import Language.Haskell.GHC.ExactPrint (showAst) + +#if MIN_VERSION_ghc(9,9,0) +import GHC.Parser.Annotation (EpUniToken (..), + EpaLocation' (..), + noAnn) +import Language.Haskell.GHC.ExactPrint.Utils (showAst) +#endif + +#if MIN_VERSION_ghc(9,11,0) +import GHC.Types.SrcLoc (UnhelpfulSpanReason (..)) +#else +import GHC.Parser.Annotation (AddEpAnn (..)) +#endif type GP = GhcPass Parsed @@ -87,14 +101,18 @@ h98ToGADTConDecl :: h98ToGADTConDecl dataName tyVars ctxt = \case ConDeclH98{..} -> ConDeclGADT - con_ext -#if MIN_VERSION_ghc(9,5,0) - (NE.singleton con_name) + +#if MIN_VERSION_ghc(9,11,0) + (AnnConDeclGADT [] [] NoEpUniTok) +#elif MIN_VERSION_ghc(9,9,0) + (NoEpUniTok, con_ext) #else - [con_name] + con_ext #endif -#if MIN_VERSION_ghc(9,5,0) + (NE.singleton con_name) + +#if !MIN_VERSION_ghc(9,9,0) (L NoTokenLoc HsNormalTok) #endif -- Ignore all existential type variable since GADT not needed @@ -107,12 +125,20 @@ h98ToGADTConDecl dataName tyVars ctxt = \case where -- Parameters in the data constructor renderDetails :: HsConDeclH98Details GP -> HsConDeclGADTDetails GP +#if MIN_VERSION_ghc(9,9,0) + renderDetails (PrefixCon _ args) = PrefixConGADT noExtField args +#else renderDetails (PrefixCon _ args) = PrefixConGADT args +#endif +#if MIN_VERSION_ghc(9,9,0) + renderDetails (InfixCon arg1 arg2) = PrefixConGADT noExtField [arg1, arg2] +#else renderDetails (InfixCon arg1 arg2) = PrefixConGADT [arg1, arg2] -#if MIN_VERSION_ghc(9,3,0) - renderDetails (RecCon recs) = RecConGADT recs noHsUniTok +#endif +#if MIN_VERSION_ghc(9,9,0) + renderDetails (RecCon recs) = RecConGADT NoEpUniTok recs #else - renderDetails (RecCon recs) = RecConGADT recs + renderDetails (RecCon recs) = RecConGADT recs noHsUniTok #endif @@ -182,16 +208,16 @@ prettyGADTDecl df decl = adjustTyClD = \case Right (L _ (TyClD _ tycld)) -> Right $ adjustDataDecl tycld Right x -> Left $ "Expect TyClD but got " <> showAst x -#if MIN_VERSION_ghc(9,3,0) Left err -> Left $ printWithoutUniques err -#else - Left err -> Left $ show err -#endif adjustDataDecl DataDecl{..} = DataDecl { tcdDExt = adjustWhere tcdDExt , tcdDataDefn = tcdDataDefn - { dd_cons = + { +#if MIN_VERSION_ghc(9,11,0) + dd_ext = adjustDefnWhere (dd_ext tcdDataDefn), +#endif + dd_cons = fmap adjustCon (dd_cons tcdDataDefn) } , .. @@ -200,19 +226,44 @@ prettyGADTDecl df decl = -- Make every data constructor start with a new line and 2 spaces adjustCon :: LConDecl GP -> LConDecl GP +#if MIN_VERSION_ghc(9,11,0) + adjustCon (L _ r) = + let delta = EpaDelta (UnhelpfulSpan UnhelpfulNoLocationInfo) (DifferentLine 1 2) [] + in L (EpAnn delta (AnnListItem []) (EpaComments [])) r +#elif MIN_VERSION_ghc(9,9,0) + adjustCon (L _ r) = + let delta = EpaDelta (DifferentLine 1 3) [] + in L (EpAnn delta (AnnListItem []) (EpaComments [])) r +#else adjustCon (L (SrcSpanAnn _ loc) r) = - L (SrcSpanAnn (EpAnn (go (spanAsAnchor loc)) (AnnListItem []) (EpaComments [])) loc) r - where - go (Anchor a _) = Anchor a (MovedAnchor (DifferentLine 1 2)) + let go (Anchor a _) = Anchor a (MovedAnchor (DifferentLine 1 2)) + in L (SrcSpanAnn (EpAnn (go (spanAsAnchor loc)) (AnnListItem []) (EpaComments [])) loc) r +#endif -- Adjust where annotation to the same line of the type constructor - adjustWhere tcdDExt = tcdDExt <&> map +#if MIN_VERSION_ghc(9,11,0) + -- tcdDext is just a placeholder in ghc-9.12 + adjustWhere = id +#else + adjustWhere tcdDExt = tcdDExt <&> +#if !MIN_VERSION_ghc(9,9,0) + map +#endif (\(AddEpAnn ann l) -> if ann == AnnWhere - then AddEpAnn AnnWhere (EpaDelta (SameLine 1) []) + then AddEpAnn AnnWhere d1 else AddEpAnn ann l ) +#endif +#if MIN_VERSION_ghc(9,11,0) + adjustDefnWhere annDataDefn + | andd_where annDataDefn == NoEpTok = annDataDefn + | otherwise = annDataDefn {andd_where = andd_where'} + where + (EpTok (EpaSpan aw)) = andd_where annDataDefn + andd_where' = EpTok (EpaDelta aw (SameLine 1) []) +#endif -- Remove the first extra line if exist removeExtraEmptyLine s = case stripInfix "\n\n" s of Just (x, xs) -> x <> "\n" <> xs @@ -224,9 +275,17 @@ wrapCtxt = id emptyCtxt = Nothing unWrap = unXRec @GP mapX = mapXRec @GP +#if MIN_VERSION_ghc(9,9,0) +noUsed = noAnn +#else noUsed = EpAnnNotUsed +#endif pattern UserTyVar' :: LIdP pass -> HsTyVarBndr flag pass +#if MIN_VERSION_ghc(9,11,0) +pattern UserTyVar' s <- HsTvb _ _ (HsBndrVar _ s) _ +#else pattern UserTyVar' s <- UserTyVar _ _ s +#endif -implicitTyVars = (wrapXRec @GP mkHsOuterImplicit) +implicitTyVars = wrapXRec @GP mkHsOuterImplicit diff --git a/plugins/hls-gadt-plugin/test/Main.hs b/plugins/hls-gadt-plugin/test/Main.hs index e92296eb0d..e71c19aa28 100644 --- a/plugins/hls-gadt-plugin/test/Main.hs +++ b/plugins/hls-gadt-plugin/test/Main.hs @@ -1,6 +1,5 @@ {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE RecordWildCards #-} -{-# LANGUAGE TypeOperators #-} {-# OPTIONS_GHC -Wall #-} module Main where @@ -35,14 +34,8 @@ tests = testGroup "GADT" , runTest "ConstructorContext" "ConstructorContext" 2 0 2 38 , runTest "Context" "Context" 2 0 4 41 , runTest "Pragma" "Pragma" 2 0 3 29 - , onlyWorkForGhcVersions (`elem`[GHC92, GHC94, GHC96]) "Single deriving has different output on ghc9.2+" $ - runTest "SingleDerivingGHC92" "SingleDerivingGHC92" 2 0 3 14 - , knownBrokenForGhcVersions [GHC92,GHC94,GHC96] "Single deriving has different output on ghc9.2+" $ - runTest "SingleDeriving" "SingleDeriving" 2 0 3 14 - , onlyWorkForGhcVersions (`elem`[GHC92, GHC94, GHC96]) "only ghc-9.2+ enabled GADTs pragma implicitly" $ - gadtPragmaTest "ghc-9.2 don't need to insert GADTs pragma" False - , knownBrokenForGhcVersions [GHC92,GHC94,GHC96] "ghc-9.2 has enabled GADTs pragma implicitly" $ - gadtPragmaTest "insert pragma" True + , runTest "SingleDerivingGHC92" "SingleDerivingGHC92" 2 0 3 14 + , gadtPragmaTest "ghc-9.2 don't need to insert GADTs pragma" False ] gadtPragmaTest :: TestName -> Bool -> TestTree @@ -78,4 +71,4 @@ isGADTCodeAction CodeAction{..} = case _kind of _ -> False testDataDir :: FilePath -testDataDir = "test" "testdata" +testDataDir = "plugins" "hls-gadt-plugin" "test" "testdata" diff --git a/plugins/hls-hlint-plugin/LICENSE b/plugins/hls-hlint-plugin/LICENSE deleted file mode 100644 index 261eeb9e9f..0000000000 --- a/plugins/hls-hlint-plugin/LICENSE +++ /dev/null @@ -1,201 +0,0 @@ - Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ - - TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - - 1. Definitions. - - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. - - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. - - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. - - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. - - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. - - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. - - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). - - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. - - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution." - - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. - - 2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. - - 3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. - - 4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: - - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and - - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and - - (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. - - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. - - 5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. - - 6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. - - 7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. - - 8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. - - 9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. - - END OF TERMS AND CONDITIONS - - APPENDIX: How to apply the Apache License to your work. - - To apply the Apache License to your work, attach the following - boilerplate notice, with the fields enclosed by brackets "[]" - replaced with your own identifying information. (Don't include - the brackets!) The text should be enclosed in the appropriate - comment syntax for the file format. We also recommend that a - file or class name and description of purpose be included on the - same "printed page" as the copyright notice for easier - identification within third-party archives. - - Copyright [yyyy] [name of copyright owner] - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. diff --git a/plugins/hls-hlint-plugin/hls-hlint-plugin.cabal b/plugins/hls-hlint-plugin/hls-hlint-plugin.cabal deleted file mode 100644 index 54e6f53d34..0000000000 --- a/plugins/hls-hlint-plugin/hls-hlint-plugin.cabal +++ /dev/null @@ -1,105 +0,0 @@ -cabal-version: 2.4 -name: hls-hlint-plugin -version: 2.4.0.0 -synopsis: Hlint integration plugin with Haskell Language Server -description: - Please see the README on GitHub at - -license: Apache-2.0 -license-file: LICENSE -author: The Haskell IDE Team -maintainer: atreyu.bbb@gmail.com -copyright: The Haskell IDE Team -category: Development -build-type: Simple -extra-source-files: - LICENSE - test/testdata/**/*.yaml - -- this one is not matched by the previous glob - test/testdata/ignore/.hlint.yaml - test/testdata/**/*.hs - test/testdata/**/*.h - -source-repository head - type: git - location: https://github.com/haskell/haskell-language-server.git - -flag pedantic - description: Enable -Werror - default: False - manual: True - -library - -- Plugins that need exactprint have not been updated for 9.8 yet - if impl(ghc >= 9.8) - buildable: False - else - buildable: True - exposed-modules: Ide.Plugin.Hlint - hs-source-dirs: src - build-depends: - , aeson - , base >=4.12 && <5 - , binary - , bytestring - , containers - , data-default - , deepseq - , Diff ^>=0.4.0 - , directory - , extra - , filepath - , ghc-exactprint >=0.6.3.4 - , ghcide == 2.4.0.0 - , hashable - , hlint >= 3.5 && < 3.7 - , hls-plugin-api == 2.4.0.0 - , lens - , lsp - , mtl - , refact - , regex-tdfa - , stm - , temporary - , text - , transformers - , unordered-containers - , ghc-lib-parser - , ghc-lib-parser-ex - , apply-refact - - cpp-options: -DHLINT_ON_GHC_LIB - ghc-options: - -Wall -Wredundant-constraints -Wno-name-shadowing - -Wno-unticked-promoted-constructors - - if flag(pedantic) - ghc-options: -Werror - - default-language: Haskell2010 - default-extensions: - DataKinds - TypeOperators - -test-suite tests - if impl(ghc >= 9.8) - buildable: False - else - buildable: True - type: exitcode-stdio-1.0 - default-language: Haskell2010 - hs-source-dirs: test - main-is: Main.hs - ghc-options: -threaded -rtsopts -with-rtsopts=-N - build-depends: - aeson - , base - , containers - , filepath - , hls-hlint-plugin - , hls-plugin-api - , hls-test-utils == 2.4.0.0 - , lens - , lsp-types - , row-types - , text diff --git a/plugins/hls-hlint-plugin/src/Ide/Plugin/Hlint.hs b/plugins/hls-hlint-plugin/src/Ide/Plugin/Hlint.hs index 2c02c6c6e0..5a72455eb5 100644 --- a/plugins/hls-hlint-plugin/src/Ide/Plugin/Hlint.hs +++ b/plugins/hls-hlint-plugin/src/Ide/Plugin/Hlint.hs @@ -1,32 +1,18 @@ -{-# LANGUAGE CPP #-} -{-# LANGUAGE DeriveAnyClass #-} -{-# LANGUAGE DeriveGeneric #-} -{-# LANGUAGE DuplicateRecordFields #-} -{-# LANGUAGE ExistentialQuantification #-} -{-# LANGUAGE FlexibleContexts #-} -{-# LANGUAGE FlexibleInstances #-} -{-# LANGUAGE LambdaCase #-} -{-# LANGUAGE MultiWayIf #-} -{-# LANGUAGE NamedFieldPuns #-} -{-# LANGUAGE OverloadedLabels #-} -{-# LANGUAGE OverloadedStrings #-} -{-# LANGUAGE PackageImports #-} -{-# LANGUAGE PatternSynonyms #-} -{-# LANGUAGE RecordWildCards #-} -{-# LANGUAGE ScopedTypeVariables #-} -{-# LANGUAGE StrictData #-} -{-# LANGUAGE TupleSections #-} -{-# LANGUAGE TypeApplications #-} -{-# LANGUAGE TypeFamilies #-} -{-# LANGUAGE ViewPatterns #-} +{-# LANGUAGE CPP #-} +{-# LANGUAGE DeriveAnyClass #-} +{-# LANGUAGE DuplicateRecordFields #-} +{-# LANGUAGE LambdaCase #-} +{-# LANGUAGE MultiWayIf #-} +{-# LANGUAGE OverloadedLabels #-} +{-# LANGUAGE OverloadedStrings #-} +{-# LANGUAGE PatternSynonyms #-} +{-# LANGUAGE RecordWildCards #-} +{-# LANGUAGE StrictData #-} +{-# LANGUAGE TypeFamilies #-} +{-# LANGUAGE ViewPatterns #-} {-# OPTIONS_GHC -Wno-orphans #-} --- On 9.4 we get a new redundant constraint warning, but deleting the --- constraint breaks the build on earlier versions. Rather than apply --- lots of CPP, we just disable the warning until later. -{-# OPTIONS_GHC -Wno-redundant-constraints #-} - -#ifdef HLINT_ON_GHC_LIB +#ifdef GHC_LIB #define MIN_GHC_API_VERSION(x,y,z) MIN_VERSION_ghc_lib_parser(x,y,z) #else #define MIN_GHC_API_VERSION(x,y,z) MIN_VERSION_ghc(x,y,z) @@ -42,10 +28,8 @@ import Control.Concurrent.STM import Control.DeepSeq import Control.Exception import Control.Lens ((?~), (^.)) -import Control.Monad import Control.Monad.Error.Class (MonadError (throwError)) import Control.Monad.IO.Class (MonadIO (liftIO)) -import Control.Monad.Trans.Class (MonadTrans (lift)) import Control.Monad.Trans.Except (ExceptT (..), runExceptT) import Data.Aeson.Types (FromJSON (..), @@ -58,18 +42,27 @@ import qualified Data.Map as M import Data.Maybe import qualified Data.Text as T import qualified Data.Text.Encoding as T +import Data.Text.Utf16.Rope.Mixed (Rope) +import qualified Data.Text.Utf16.Rope.Mixed as Rope import Data.Typeable import Development.IDE hiding (Error, getExtensions) import Development.IDE.Core.Compile (sourceParser) +import Development.IDE.Core.FileStore (getVersionedTextDoc) import Development.IDE.Core.Rules (defineNoFile, getParsedModuleWithComments) import Development.IDE.Core.Shake (getDiagnostics) + +#if APPLY_REFACT import qualified Refact.Apply as Refact import qualified Refact.Types as Refact +#if !MIN_VERSION_apply_refact(0,12,0) +import System.Environment (setEnv, + unsetEnv) +#endif +#endif -#ifdef HLINT_ON_GHC_LIB import Development.IDE.GHC.Compat (DynFlags, WarningFlag (Opt_WarnUnrecognisedPragmas), extensionFlags, @@ -79,18 +72,18 @@ import Development.IDE.GHC.Compat (DynFlags, import qualified Development.IDE.GHC.Compat.Util as EnumSet #if MIN_GHC_API_VERSION(9,4,0) -import qualified "ghc-lib-parser" GHC.Data.Strict as Strict +import qualified GHC.Data.Strict as Strict #endif #if MIN_GHC_API_VERSION(9,0,0) -import "ghc-lib-parser" GHC.Types.SrcLoc hiding +import GHC.Types.SrcLoc hiding (RealSrcSpan) -import qualified "ghc-lib-parser" GHC.Types.SrcLoc as GHC +import qualified GHC.Types.SrcLoc as GHC #else -import "ghc-lib-parser" SrcLoc hiding +import qualified SrcLoc as GHC +import SrcLoc hiding (RealSrcSpan) -import qualified "ghc-lib-parser" SrcLoc as GHC #endif -import "ghc-lib-parser" GHC.LanguageExtensions (Extension) +import GHC.LanguageExtensions (Extension) import Language.Haskell.GhclibParserEx.GHC.Driver.Session as GhclibParserEx (readExtension) import System.FilePath (takeFileName) import System.IO (IOMode (WriteMode), @@ -102,21 +95,7 @@ import System.IO (IOMode (Wri utf8, withFile) import System.IO.Temp -#else -import Development.IDE.GHC.Compat hiding - (setEnv, - (<+>)) -import GHC.Generics (Associativity (LeftAssociative, NotAssociative, RightAssociative)) -#if MIN_GHC_API_VERSION(9,2,0) -import Language.Haskell.GHC.ExactPrint.ExactPrint (deltaOptions) -#else -import Language.Haskell.GHC.ExactPrint.Delta (deltaOptions) -#endif -import Language.Haskell.GHC.ExactPrint.Parsers (postParseTransform) -import Language.Haskell.GHC.ExactPrint.Types (Rigidity (..)) -import Language.Haskell.GhclibParserEx.Fixity as GhclibParserEx (applyFixities) -import qualified Refact.Fixity as Refact -#endif + import Ide.Plugin.Config hiding (Config) import Ide.Plugin.Error @@ -125,15 +104,14 @@ import Ide.Plugin.Resolve import Ide.PluginUtils import Ide.Types hiding (Config) -import Language.Haskell.HLint as Hlint hiding - (Error) +import Language.Haskell.HLint as Hlint import qualified Language.LSP.Protocol.Lens as LSP import Language.LSP.Protocol.Message import Language.LSP.Protocol.Types hiding (Null) import qualified Language.LSP.Protocol.Types as LSP -import Language.LSP.Server (getVersionedTextDoc) +import Development.IDE.Core.PluginUtils as PluginUtils import qualified Development.IDE.Core.Shake as Shake import Development.IDE.Spans.Pragmas (LineSplitTextEdits (LineSplitTextEdits), NextPragmaInfo (NextPragmaInfo), @@ -143,19 +121,16 @@ import Development.IDE.Spans.Pragmas (LineSplitTe lineSplitTextEdits, nextPragmaLine) import GHC.Generics (Generic) -#if MIN_VERSION_apply_refact(0,12,0) -#else -import System.Environment (setEnv, - unsetEnv) -#endif -import Development.IDE.Core.PluginUtils as PluginUtils import Text.Regex.TDFA.Text () + -- --------------------------------------------------------------------- data Log = LogShake Shake.Log | LogApplying NormalizedFilePath (Either String WorkspaceEdit) +#if APPLY_REFACT | LogGeneratedIdeas NormalizedFilePath [[Refact.Refactoring Refact.SrcSpan]] +#endif | LogGetIdeas NormalizedFilePath | LogUsingExtensions NormalizedFilePath [String] -- Extension is only imported conditionally, so we just stringify them | forall a. (Pretty a) => LogResolve a @@ -164,12 +139,13 @@ instance Pretty Log where pretty = \case LogShake log -> pretty log LogApplying fp res -> "Applying hint(s) for" <+> viaShow fp <> ":" <+> viaShow res +#if APPLY_REFACT LogGeneratedIdeas fp ideas -> "Generated hlint ideas for for" <+> viaShow fp <> ":" <+> viaShow ideas - LogUsingExtensions fp exts -> "Using extensions for " <+> viaShow fp <> ":" <+> pretty exts +#endif + LogUsingExtensions fp exts -> "Using extensions for " <+> viaShow fp <> ":" <> line <> indent 4 (pretty exts) LogGetIdeas fp -> "Getting hlint ideas for " <+> viaShow fp LogResolve msg -> pretty msg -#ifdef HLINT_ON_GHC_LIB -- Reimplementing this, since the one in Development.IDE.GHC.Compat isn't for ghc-lib #if !MIN_GHC_API_VERSION(9,0,0) type BufSpan = () @@ -183,7 +159,6 @@ pattern RealSrcSpan x y = GHC.RealSrcSpan x y pattern RealSrcSpan x y <- ((,Nothing) -> (GHC.RealSrcSpan x, y)) #endif {-# COMPLETE RealSrcSpan, UnhelpfulSpan #-} -#endif #if MIN_GHC_API_VERSION(9,4,0) fromStrictMaybe :: Strict.Maybe a -> Maybe a @@ -195,7 +170,8 @@ descriptor :: Recorder (WithPriority Log) -> PluginId -> PluginDescriptor IdeSta descriptor recorder plId = let resolveRecorder = cmapWithPrio LogResolve recorder (pluginCommands, pluginHandlers) = mkCodeActionWithResolveAndCommand resolveRecorder plId codeActionProvider (resolveProvider recorder) - in (defaultPluginDescriptor plId) + desc = "Provides HLint diagnostics and code actions. Built with hlint-" <> VERSION_hlint + in (defaultPluginDescriptor plId desc) { pluginRules = rules recorder plId , pluginCommands = pluginCommands , pluginHandlers = pluginHandlers @@ -208,24 +184,24 @@ descriptor recorder plId = -- This rule only exists for generating file diagnostics -- so the RuleResult is empty data GetHlintDiagnostics = GetHlintDiagnostics - deriving (Eq, Show, Typeable, Generic) + deriving (Eq, Show, Generic) instance Hashable GetHlintDiagnostics instance NFData GetHlintDiagnostics type instance RuleResult GetHlintDiagnostics = () -- | Hlint rules to generate file diagnostics based on hlint hints --- | This rule is recomputed when: --- | - A file has been edited via --- | - `getIdeas` -> `getParsedModule` in any case --- | - `getIdeas` -> `getFileContents` if the hls ghc does not match the hlint default ghc --- | - The client settings have changed, to honour the `hlintOn` setting, via `getClientConfigAction` --- | - The hlint specific settings have changed, via `getHlintSettingsRule` +-- This rule is recomputed when: +-- - A file has been edited via +-- - `getIdeas` -> `getParsedModule` in any case +-- - `getIdeas` -> `getFileContents` if the hls ghc does not match the hlint default ghc +-- - The client settings have changed, to honour the `hlintOn` setting, via `getClientConfigAction` +-- - The hlint specific settings have changed, via `getHlintSettingsRule` rules :: Recorder (WithPriority Log) -> PluginId -> Rules () rules recorder plugin = do define (cmapWithPrio LogShake recorder) $ \GetHlintDiagnostics file -> do config <- getPluginConfigAction plugin - let hlintOn = pluginEnabledConfig plcDiagnosticsOn config + let hlintOn = plcGlobalOn config && plcDiagnosticsOn config ideas <- if hlintOn then getIdeas recorder file else return (Right []) return (diagnostics file ideas, Just ()) @@ -234,32 +210,47 @@ rules recorder plugin = do liftIO $ argsSettings flags action $ do - files <- getFilesOfInterestUntracked - void $ uses GetHlintDiagnostics $ Map.keys files + files <- Map.keys <$> getFilesOfInterestUntracked + Shake.runWithSignal (Proxy @"kick/start/hlint") (Proxy @"kick/done/hlint") files GetHlintDiagnostics where diagnostics :: NormalizedFilePath -> Either ParseError [Idea] -> [FileDiagnostic] diagnostics file (Right ideas) = - [(file, ShowDiag, ideaToDiagnostic i) | i <- ideas, ideaSeverity i /= Ignore] + [ideErrorFromLspDiag diag file Nothing | i <- ideas, Just diag <- [ideaToDiagnostic i]] diagnostics file (Left parseErr) = - [(file, ShowDiag, parseErrorToDiagnostic parseErr)] + [ideErrorFromLspDiag (parseErrorToDiagnostic parseErr) file Nothing] + + + ideaToDiagnostic :: Idea -> Maybe Diagnostic + ideaToDiagnostic idea = do + diagnosticSeverity <- ideaSeverityToDiagnosticSeverity (ideaSeverity idea) + pure $ + LSP.Diagnostic { + _range = srcSpanToRange $ ideaSpan idea + , _severity = Just diagnosticSeverity + -- we are encoding the fact that idea has refactorings in diagnostic code + , _code = Just (InR $ T.pack $ codePre ++ ideaHint idea) + , _source = Just "hlint" + , _message = idea2Message idea + , _relatedInformation = Nothing + , _tags = Nothing + , _codeDescription = Nothing + , _data_ = Nothing + } - ideaToDiagnostic :: Idea -> Diagnostic - ideaToDiagnostic idea = - LSP.Diagnostic { - _range = srcSpanToRange $ ideaSpan idea - , _severity = Just LSP.DiagnosticSeverity_Information - -- we are encoding the fact that idea has refactorings in diagnostic code - , _code = Just (InR $ T.pack $ codePre ++ ideaHint idea) - , _source = Just "hlint" - , _message = idea2Message idea - , _relatedInformation = Nothing - , _tags = Nothing - , _codeDescription = Nothing - , _data_ = Nothing - } - where codePre = if null $ ideaRefactoring idea then "" else "refact:" + where + codePre = if null $ ideaRefactoring idea then "" else "refact:" + + -- We only propogate error severity of hlint and downgrade other severities to Info. + -- Currently, there are just 2 error level serverities present in hlint by default: https://github.com/ndmitchell/hlint/issues/1549#issuecomment-1892701824. + -- And according to ndmitchell: The default error level severities of the two hints are justified and it's fairly uncommon to happen. + -- GH Issue about discussion on this: https://github.com/ndmitchell/hlint/issues/1549 + ideaSeverityToDiagnosticSeverity :: Hlint.Severity -> Maybe LSP.DiagnosticSeverity + ideaSeverityToDiagnosticSeverity Hlint.Ignore = Nothing + ideaSeverityToDiagnosticSeverity Hlint.Suggestion = Just LSP.DiagnosticSeverity_Information + ideaSeverityToDiagnosticSeverity Hlint.Warning = Just LSP.DiagnosticSeverity_Information + ideaSeverityToDiagnosticSeverity Hlint.Error = Just LSP.DiagnosticSeverity_Error idea2Message :: Idea -> T.Text idea2Message idea = T.unlines $ [T.pack $ ideaHint idea, "Found:", " " <> T.pack (ideaFrom idea)] @@ -310,28 +301,6 @@ getIdeas recorder nfp = do fmap applyHints' (moduleEx flags) where moduleEx :: ParseFlags -> Action (Maybe (Either ParseError ModuleEx)) -#ifndef HLINT_ON_GHC_LIB - moduleEx _flags = do - mbpm <- getParsedModuleWithComments nfp - return $ createModule <$> mbpm - where - createModule pm = Right (createModuleEx anns (applyParseFlagsFixities modu)) - where anns = pm_annotations pm - modu = pm_parsed_source pm - - applyParseFlagsFixities :: ParsedSource -> ParsedSource - applyParseFlagsFixities modul = GhclibParserEx.applyFixities (parseFlagsToFixities _flags) modul - - parseFlagsToFixities :: ParseFlags -> [(String, Fixity)] - parseFlagsToFixities = map toFixity . Hlint.fixities - - toFixity :: FixityInfo -> (String, Fixity) - toFixity (name, dir, i) = (name, Fixity NoSourceText i $ f dir) - where - f LeftAssociative = InfixL - f RightAssociative = InfixR - f NotAssociative = InfixN -#else moduleEx flags = do mbpm <- getParsedModuleWithComments nfp -- If ghc was not able to parse the module, we disable hlint diagnostics @@ -339,9 +308,9 @@ getIdeas recorder nfp = do then return Nothing else do flags' <- setExtensions flags - (_, contents) <- getFileContents nfp + contents <- getFileContents nfp let fp = fromNormalizedFilePath nfp - let contents' = T.unpack <$> contents + let contents' = T.unpack . Rope.toText <$> contents Just <$> liftIO (parseModuleEx flags' fp contents') setExtensions flags = do @@ -354,11 +323,6 @@ getIdeas recorder nfp = do -- and the ModSummary dynflags. However using the parsedFlags extensions -- can sometimes interfere with the hlint parsing of the file. -- See https://github.com/haskell/haskell-language-server/issues/1279 --- --- Note: this is used when HLINT_ON_GHC_LIB is defined. We seem to need --- these extensions to construct dynflags to parse the file again. Therefore --- using hlint default extensions doesn't seem to be a problem when --- HLINT_ON_GHC_LIB is not defined because we don't parse the file again. getExtensions :: NormalizedFilePath -> Action [Extension] getExtensions nfp = do dflags <- getFlags @@ -369,12 +333,11 @@ getExtensions nfp = do getFlags = do modsum <- use_ GetModSummary nfp return $ ms_hspp_opts $ msrModSummary modsum -#endif -- --------------------------------------------------------------------- data GetHlintSettings = GetHlintSettings - deriving (Eq, Show, Typeable, Generic) + deriving (Eq, Show, Generic) instance Hashable GetHlintSettings instance NFData GetHlintSettings instance NFData Hint where rnf = rwhnf @@ -406,14 +369,19 @@ codeActionProvider ideState _pluginId (CodeActionParams _ _ documentId _ context | let TextDocumentIdentifier uri = documentId , Just docNormalizedFilePath <- uriToNormalizedFilePath (toNormalizedUri uri) = do - verTxtDocId <- lift $ getVersionedTextDoc documentId + verTxtDocId <- + liftIO $ + runAction "Hlint.getVersionedTextDoc" ideState $ + getVersionedTextDoc documentId liftIO $ fmap (InL . map LSP.InR) $ do allDiagnostics <- atomically $ getDiagnostics ideState let numHintsInDoc = length - [diagnostic | (diagnosticNormalizedFilePath, _, diagnostic) <- allDiagnostics - , validCommand diagnostic - , diagnosticNormalizedFilePath == docNormalizedFilePath + [lspDiagnostic + | diag <- allDiagnostics + , let lspDiagnostic = fdLspDiagnostic diag + , validCommand lspDiagnostic + , fdFilePath diag == docNormalizedFilePath ] let numHintsInContext = length [diagnostic | diagnostic <- diags @@ -451,12 +419,19 @@ resolveProvider recorder ideState _plId ca uri resolveValue = do edit <- ExceptT $ liftIO $ ignoreHint recorder ideState file verTxtDocId hintTitle pure $ ca & LSP.edit ?~ edit +applyRefactAvailable :: Bool +#if APPLY_REFACT +applyRefactAvailable = True +#else +applyRefactAvailable = False +#endif + -- | Convert a hlint diagnostic into an apply and an ignore code action -- if applicable diagnosticToCodeActions :: VersionedTextDocumentIdentifier -> LSP.Diagnostic -> [LSP.CodeAction] diagnosticToCodeActions verTxtDocId diagnostic | LSP.Diagnostic{ _source = Just "hlint", _code = Just (InR code), _range = LSP.Range start _ } <- diagnostic - , let isHintApplicable = "refact:" `T.isPrefixOf` code + , let isHintApplicable = "refact:" `T.isPrefixOf` code && applyRefactAvailable , let hint = T.replace "refact:" "" code , let suppressHintTitle = "Ignore hint \"" <> hint <> "\" in this module" , let suppressHintArguments = IgnoreHint verTxtDocId hint @@ -485,7 +460,7 @@ mkCodeAction title diagnostic data_ isPreferred = , _data_ = data_ } -mkSuppressHintTextEdits :: DynFlags -> T.Text -> T.Text -> [LSP.TextEdit] +mkSuppressHintTextEdits :: DynFlags -> Rope -> T.Text -> [LSP.TextEdit] mkSuppressHintTextEdits dynFlags fileContents hint = let NextPragmaInfo{ nextPragmaLine, lineSplitTextEdits } = getNextPragmaInfo dynFlags (Just fileContents) @@ -544,6 +519,11 @@ data OneHint = } deriving (Generic, Eq, Show, ToJSON, FromJSON) applyHint :: Recorder (WithPriority Log) -> IdeState -> NormalizedFilePath -> Maybe OneHint -> VersionedTextDocumentIdentifier -> IO (Either PluginError WorkspaceEdit) +#if !APPLY_REFACT +applyHint _ _ _ _ _ = + -- https://github.com/ndmitchell/hlint/pull/1594#issuecomment-2338898673 + evaluate $ error "Cannot apply refactoring: apply-refact does not work on GHC 9.10" +#else applyHint recorder ide nfp mhint verTxtDocId = runExceptT $ do let runAction' :: Action a -> IO a @@ -556,7 +536,7 @@ applyHint recorder ide nfp mhint verTxtDocId = let commands = map ideaRefactoring ideas' logWith recorder Debug $ LogGeneratedIdeas nfp commands let fp = fromNormalizedFilePath nfp - (_, mbOldContent) <- liftIO $ runAction' $ getFileContents nfp + mbOldContent <- fmap (fmap Rope.toText) $ liftIO $ runAction' $ getFileContents nfp oldContent <- maybe (liftIO $ fmap T.decodeUtf8 (BS.readFile fp)) return mbOldContent modsum <- liftIO $ runAction' $ use_ GetModSummary nfp let dflags = ms_hspp_opts $ msrModSummary modsum @@ -567,7 +547,6 @@ applyHint recorder ide nfp mhint verTxtDocId = -- But "Idea"s returned by HLint point to starting position of the expressions -- that contain refactorings, so they are often outside the refactorings' boundaries. let position = Nothing -#ifdef HLINT_ON_GHC_LIB let writeFileUTF8NoNewLineTranslation file txt = withFile file WriteMode $ \h -> do hSetEncoding h utf8 @@ -583,22 +562,6 @@ applyHint recorder ide nfp mhint verTxtDocId = let refactExts = map show $ enabled ++ disabled (Right <$> applyRefactorings (topDir dflags) position commands temp refactExts) `catches` errorHandlers -#else - mbParsedModule <- liftIO $ runAction' $ getParsedModuleWithComments nfp - res <- - case mbParsedModule of - Nothing -> throwError "Apply hint: error parsing the module" - Just pm -> do - let anns = pm_annotations pm - let modu = pm_parsed_source pm - -- apply-refact uses RigidLayout - let rigidLayout = deltaOptions RigidLayout - (anns', modu') <- - ExceptT $ mapM (uncurry Refact.applyFixities) - $ postParseTransform (Right (anns, [], dflags, modu)) rigidLayout - liftIO $ (Right <$> Refact.applyRefactorings' position commands anns' modu') - `catches` errorHandlers -#endif case res of Right appliedFile -> do let wsEdit = diffText' True (verTxtDocId, oldContent) (T.pack appliedFile) IncludeDeletions @@ -662,7 +625,7 @@ applyRefactorings :: -- with the @LANGUAGE@ pragmas, pragmas win. [String] -> IO String -applyRefactorings = +applyRefactorings = #if MIN_VERSION_apply_refact(0,12,0) Refact.applyRefactorings #else @@ -679,3 +642,4 @@ applyRefactorings = withRuntimeLibdir libdir = bracket_ (setEnv key libdir) (unsetEnv key) where key = "GHC_EXACTPRINT_GHC_LIBDIR" #endif +#endif diff --git a/plugins/hls-hlint-plugin/test/Main.hs b/plugins/hls-hlint-plugin/test/Main.hs index 9da2aef833..4eea2a803a 100644 --- a/plugins/hls-hlint-plugin/test/Main.hs +++ b/plugins/hls-hlint-plugin/test/Main.hs @@ -1,28 +1,27 @@ -{-# LANGUAGE NamedFieldPuns #-} +{-# LANGUAGE DataKinds #-} {-# LANGUAGE OverloadedLabels #-} {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE RecordWildCards #-} -{-# LANGUAGE TypeOperators #-} module Main ( main ) where import Control.Lens ((^.)) -import Control.Monad (when) -import Data.Aeson (Value (..), object, toJSON, (.=)) +import Control.Monad (guard, when) +import Data.Aeson (Value (..), object, (.=)) import Data.Functor (void) import Data.List (find) import qualified Data.Map as Map import Data.Maybe (fromJust, isJust) -import Data.Row ((.+), (.==)) +import Data.Proxy (Proxy (Proxy)) import qualified Data.Text as T -import Ide.Plugin.Config (Config (..), PluginConfig (..)) +import Ide.Plugin.Config (Config (..)) import qualified Ide.Plugin.Config as Plugin import qualified Ide.Plugin.Hlint as HLint -import Ide.Types (PluginId) import qualified Language.LSP.Protocol.Lens as L -import System.FilePath (()) +import System.FilePath ((<.>), ()) import Test.Hls +import Test.Hls.FileSystem main :: IO () main = defaultTestRunner tests @@ -46,7 +45,7 @@ getApplyHintText :: T.Text -> T.Text getApplyHintText name = "Apply hint \"" <> name <> "\"" resolveTests :: TestTree -resolveTests = testGroup "hlint resolve tests" +resolveTests = knownBrokenForGhcVersions [GHC910] "apply-refact doesn't work on 9.10" $ testGroup "hlint resolve tests" [ ignoreHintGoldenResolveTest "Resolve version of: Ignore hint in this module inserts -Wno-unrecognised-pragmas and hlint ignore pragma if warn unrecognized pragmas is off" @@ -77,7 +76,7 @@ ignoreHintTests = testGroup "hlint ignore hint tests" ] applyHintTests :: TestTree -applyHintTests = testGroup "hlint apply hint tests" +applyHintTests = knownBrokenForGhcVersions [GHC910] "apply-refact doesn't work on 9.10" $ testGroup "hlint apply hint tests" [ applyHintGoldenTest "[#2612] Apply hint works when operator fixities go right-to-left" @@ -89,9 +88,9 @@ applyHintTests = testGroup "hlint apply hint tests" suggestionsTests :: TestTree suggestionsTests = testGroup "hlint suggestions" [ - testCase "provides 3.8 code actions including apply all" $ runHlintSession "" $ do + knownBrokenForGhcVersions [GHC910] "apply-refact doesn't work on 9.10" $ testCase "provides 3.8 code actions including apply all" $ runHlintSession "" $ do doc <- openDoc "Base.hs" "haskell" - diags@(reduceDiag:_) <- waitForDiagnosticsFromSource doc "hlint" + diags@(reduceDiag:_) <- hlintCaptureKick liftIO $ do length diags @?= 2 -- "Eta Reduce" and "Redundant Id" @@ -121,10 +120,15 @@ suggestionsTests = contents <- skipManyTill anyMessage $ getDocumentEdit doc liftIO $ contents @?= "main = undefined\nfoo x = x\n" - , testCase "falls back to pre 3.8 code actions" $ runSessionWithServerAndCaps def hlintPlugin noLiteralCaps "test/testdata" $ do + , knownBrokenForGhcVersions [GHC910] "apply-refact doesn't work on 9.10" $ testCase "falls back to pre 3.8 code actions" $ + runSessionWithTestConfig def + { testConfigCaps = noLiteralCaps + , testDirLocation = Left testDir + , testPluginDescriptor = hlintPlugin + , testShiftRoot = True} $ const $ do doc <- openDoc "Base.hs" "haskell" - _ <- waitForDiagnosticsFromSource doc "hlint" + _ <- hlintCaptureKick cars <- getAllCodeActions doc etaReduce <- liftIO $ inspectCommand cars ["Eta reduce"] @@ -136,22 +140,29 @@ suggestionsTests = , testCase ".hlint.yaml fixity rules are applied" $ runHlintSession "fixity" $ do doc <- openDoc "FixityUse.hs" "haskell" - expectNoMoreDiagnostics 3 doc "hlint" + testNoHlintDiagnostics doc , testCase "changing document contents updates hlint diagnostics" $ runHlintSession "" $ do doc <- openDoc "Base.hs" "haskell" testHlintDiagnostics doc - let change = TextDocumentContentChangeEvent $ InL $ #range .== Range (Position 1 8) (Position 1 12) - .+ #rangeLength .== Nothing - .+ #text .== "x" - changeDoc doc [change] - expectNoMoreDiagnostics 3 doc "hlint" - - let change' = TextDocumentContentChangeEvent $ InL $ #range .== Range (Position 1 8) (Position 1 12) - .+ #rangeLength .== Nothing - .+ #text .== "id x" + let change = TextDocumentContentChangeEvent $ InL + TextDocumentContentChangePartial + { _range = Range (Position 1 8) (Position 1 12) + , _rangeLength = Nothing + , _text = "x" + } + changeDoc doc [change] + -- We need to wait until hlint has been rerun and clears the diagnostic + [] <- waitForDiagnosticsFrom doc + + let change' = TextDocumentContentChangeEvent $ InL + TextDocumentContentChangePartial + { _range = Range (Position 1 8) (Position 1 12) + , _rangeLength = Nothing + , _text = "id x" + } changeDoc doc [change'] testHlintDiagnostics doc @@ -160,7 +171,7 @@ suggestionsTests = testHlintDiagnostics doc , knownBrokenForHlintOnGhcLib "hlint doesn't take in account cpp flag as ghc -D argument" $ - testCase "[#554] hlint diagnostics works with CPP via language pragma" $ runHlintSession "" $ do + testCase "[#554] hlint diagnostics works with CPP via language pragma" $ runHlintSession "cpp" $ do doc <- openDoc "CppCond.hs" "haskell" testHlintDiagnostics doc @@ -168,54 +179,49 @@ suggestionsTests = doc <- openDoc "CppHeader.hs" "haskell" testHlintDiagnostics doc - , testCase "[#590] apply-refact works with -XLambdaCase argument" $ runHlintSession "lambdacase" $ do + , knownBrokenForGhcVersions [GHC910] "apply-refact doesn't work on 9.10" $ testCase "[#590] apply-refact works with -XLambdaCase argument" $ runHlintSession "lambdacase" $ do testRefactor "LambdaCase.hs" "Redundant bracket" expectedLambdaCase - , testCase "[#1242] apply-refact works with -XTypeApplications argument" $ runHlintSession "typeapps" $ do + , knownBrokenForGhcVersions [GHC910] "apply-refact doesn't work on 9.10" $ testCase "[#1242] apply-refact works with -XTypeApplications argument" $ runHlintSession "typeapps" $ do testRefactor "TypeApplication.hs" "Redundant bracket" expectedTypeApp - , testCase "apply hints works with LambdaCase via language pragma" $ runHlintSession "" $ do + , knownBrokenForGhcVersions [GHC910] "apply-refact doesn't work on 9.10" $ testCase "apply hints works with LambdaCase via language pragma" $ runHlintSession "" $ do testRefactor "LambdaCase.hs" "Redundant bracket" ("{-# LANGUAGE LambdaCase #-}" : expectedLambdaCase) - , expectFailBecause "apply-refact doesn't work with cpp" $ + , ignoreTestBecause "apply-refact doesn't work with cpp" $ testCase "apply hints works with CPP via -XCPP argument" $ runHlintSession "cpp" $ do testRefactor "CppCond.hs" "Redundant bracket" expectedCPP - , expectFailBecause "apply-refact doesn't work with cpp" $ + , ignoreTestBecause "apply-refact doesn't work with cpp" $ testCase "apply hints works with CPP via language pragma" $ runHlintSession "" $ do testRefactor "CppCond.hs" "Redundant bracket" ("{-# LANGUAGE CPP #-}" : expectedCPP) , testCase "hlint diagnostics ignore hints honouring .hlint.yaml" $ runHlintSession "ignore" $ do doc <- openDoc "CamelCase.hs" "haskell" - expectNoMoreDiagnostics 3 doc "hlint" + testNoHlintDiagnostics doc , testCase "hlint diagnostics ignore hints honouring ANN annotations" $ runHlintSession "" $ do doc <- openDoc "IgnoreAnn.hs" "haskell" - expectNoMoreDiagnostics 3 doc "hlint" + testNoHlintDiagnostics doc , testCase "hlint diagnostics ignore hints honouring HLINT annotations" $ runHlintSession "" $ do doc <- openDoc "IgnoreAnnHlint.hs" "haskell" - expectNoMoreDiagnostics 3 doc "hlint" + testNoHlintDiagnostics doc - , knownBrokenForGhcVersions [GHC92, GHC94, GHC96] "apply-refact has different behavior on v0.10" $ - testCase "apply-refact preserve regular comments" $ runHlintSession "" $ do + , knownBrokenForGhcVersions [GHC910] "apply-refact doesn't work on 9.10" $ testCase "apply-refact preserve regular comments" $ runHlintSession "" $ do testRefactor "Comments.hs" "Redundant bracket" expectedComments - , onlyRunForGhcVersions [GHC92, GHC94, GHC96] "only run test for apply-refact-0.10" $ - testCase "apply-refact preserve regular comments" $ runHlintSession "" $ do - testRefactor "Comments.hs" "Redundant bracket" expectedComments' - - , testCase "[#2290] apply all hints works with a trailing comment" $ runHlintSession "" $ do + , knownBrokenForGhcVersions [GHC910] "apply-refact doesn't work on 9.10" $ testCase "[#2290] apply all hints works with a trailing comment" $ runHlintSession "" $ do testRefactor "TwoHintsAndComment.hs" "Apply all hints" expectedComments2 , testCase "applyAll is shown only when there is at least one diagnostic in range" $ runHlintSession "" $ do doc <- openDoc "TwoHints.hs" "haskell" - _ <- waitForDiagnosticsFromSource doc "hlint" + _ <- hlintCaptureKick firstLine <- map fromAction <$> getCodeActions doc (mkRange 0 0 0 0) secondLine <- map fromAction <$> getCodeActions doc (mkRange 1 0 1 0) @@ -230,25 +236,20 @@ suggestionsTests = liftIO $ hasApplyAll multiLine @? "Missing apply all code action" , testCase "hlint should warn about unused extensions" $ runHlintSession "unusedext" $ do - doc <- openDoc "UnusedExtension.hs" "haskell" - diags@(unusedExt:_) <- waitForDiagnosticsFromSource doc "hlint" + _ <- openDoc "UnusedExtension.hs" "haskell" + diags@(unusedExt:_) <- hlintCaptureKick liftIO $ do length diags @?= 1 unusedExt ^. L.code @?= Just (InR "refact:Unused LANGUAGE pragma") - , testCase "[#1279] hlint should not activate extensions like PatternSynonyms" $ runHlintSession "" $ do + , testCase "[#1279] hlint should not activate extensions like PatternSynonyms" $ runHlintSession "" $ do doc <- openDoc "PatternKeyword.hs" "haskell" - - waitForAllProgressDone -- hlint will report a parse error if PatternSynonyms is enabled - expectNoMoreDiagnostics 3 doc "hlint" + testNoHlintDiagnostics doc , testCase "hlint should not warn about redundant irrefutable pattern with LANGUAGE Strict" $ runHlintSession "" $ do doc <- openDoc "StrictData.hs" "haskell" - - waitForAllProgressDone - - expectNoMoreDiagnostics 3 doc "hlint" + testNoHlintDiagnostics doc ] where testRefactor file caTitle expected = do @@ -275,22 +276,24 @@ suggestionsTests = , "g = 2" , "#endif", "" ] - expectedComments = [ "-- comment before header" - , "module Comments where", "" - , "{-# standalone annotation #-}", "" - , "-- standalone comment", "" - , "-- | haddock comment" - , "f = {- inline comment -}{- inline comment inside refactored code -} 1 -- ending comment", "" - , "-- final comment" - ] - expectedComments' = [ "-- comment before header" - , "module Comments where", "" - , "{-# standalone annotation #-}", "" - , "-- standalone comment", "" - , "-- | haddock comment" - , "f = {- inline comment -} {- inline comment inside refactored code -}1 -- ending comment", "" - , "-- final comment" - ] + expectedComments = case ghcVersion of + GHC912 -> [ "-- comment before header" + , "module Comments where", "" + , "{-# standalone annotation #-}", "" + , "-- standalone comment", "" + , "-- | haddock comment" + , "f = {- inline comment -}{- inline comment inside refactored code -} 1 -- ending comment", "" + , "-- final comment" + ] + + _ -> [ "-- comment before header" + , "module Comments where", "" + , "{-# standalone annotation #-}", "" + , "-- standalone comment", "" + , "-- | haddock comment" + , "f = {- inline comment -} {- inline comment inside refactored code -}1 -- ending comment", "" + , "-- final comment" + ] expectedComments2 = [ "module TwoHintsAndComment where" , "biggest = foldr1 max -- the line above will show two hlint hints, \"eta reduce\" and \"use maximum\"" ] @@ -311,9 +314,7 @@ configTests = testGroup "hlint plugin config" [ disableHlint - diags' <- waitForDiagnosticsFrom doc - - liftIO $ noHlintDiagnostics diags' + testNoHlintDiagnostics doc , testCase "adding hlint flags to plugin configuration removes hlint diagnostics" $ runHlintSession "" $ do setIgnoringConfigurationRequests False @@ -325,9 +326,7 @@ configTests = testGroup "hlint plugin config" [ let config' = hlintConfigWithFlags ["--ignore=Redundant id", "--hint=test-hlint-config.yaml"] setHlsConfig config' - diags' <- waitForDiagnosticsFrom doc - - liftIO $ noHlintDiagnostics diags' + testNoHlintDiagnostics doc , testCase "adding hlint flags to plugin configuration adds hlint diagnostics" $ runHlintSession "" $ do setIgnoringConfigurationRequests False @@ -335,12 +334,12 @@ configTests = testGroup "hlint plugin config" [ doc <- openDoc "Generalise.hs" "haskell" - expectNoMoreDiagnostics 3 doc "hlint" + testNoHlintDiagnostics doc let config' = hlintConfigWithFlags ["--with-group=generalise"] setHlsConfig config' - diags' <- waitForDiagnosticsFromSource doc "hlint" + diags' <- hlintCaptureKick d <- liftIO $ inspectDiagnostic diags' ["Use <>"] liftIO $ do @@ -350,19 +349,51 @@ configTests = testGroup "hlint plugin config" [ ] testDir :: FilePath -testDir = "test/testdata" +testDir = "plugins/hls-hlint-plugin/test/testdata" runHlintSession :: FilePath -> Session a -> IO a -runHlintSession subdir = failIfSessionTimeout . runSessionWithServerAndCaps def hlintPlugin codeActionNoResolveCaps (testDir subdir) +runHlintSession subdir = failIfSessionTimeout . + runSessionWithTestConfig def + { testConfigCaps = codeActionNoResolveCaps + , testShiftRoot = True + , testDirLocation = Left (testDir subdir) + , testPluginDescriptor = hlintPlugin + } + . const + +hlintKickDone :: Session () +hlintKickDone = kick (Proxy @"kick/done/hlint") >>= guard . not . null + +hlintKickStart :: Session () +hlintKickStart = kick (Proxy @"kick/start/hlint") >>= guard . not . null -noHlintDiagnostics :: [Diagnostic] -> Assertion +hlintCaptureKick :: Session [Diagnostic] +hlintCaptureKick = captureKickDiagnostics hlintKickStart hlintKickDone + +noHlintDiagnostics :: HasCallStack => [Diagnostic] -> Assertion noHlintDiagnostics diags = - Just "hlint" `notElem` map (^. L.source) diags @? "There are no hlint diagnostics" + all (not . isHlintDiagnostic) diags @? "There are hlint diagnostics" + +isHlintDiagnostic :: Diagnostic -> Bool +isHlintDiagnostic diag = + Just "hlint" == diag ^. L.source -testHlintDiagnostics :: TextDocumentIdentifier -> Session () +testHlintDiagnostics :: HasCallStack => TextDocumentIdentifier -> Session () testHlintDiagnostics doc = do - diags <- waitForDiagnosticsFromSource doc "hlint" - liftIO $ length diags > 0 @? "There are hlint diagnostics" + diags <- captureKickNonEmptyDiagnostics doc + liftIO $ length diags > 0 @? "There are no hlint diagnostics" + +captureKickNonEmptyDiagnostics :: HasCallStack => TextDocumentIdentifier -> Session [Diagnostic] +captureKickNonEmptyDiagnostics doc = do + diags <- hlintCaptureKick + if null diags + then captureKickNonEmptyDiagnostics doc + else pure diags + +testNoHlintDiagnostics :: HasCallStack => TextDocumentIdentifier -> Session () +testNoHlintDiagnostics _doc = do + diags <- hlintCaptureKick + liftIO $ noHlintDiagnostics diags hlintConfigWithFlags :: [T.Text] -> Config hlintConfigWithFlags flags = @@ -388,7 +419,7 @@ disableHlint = setHlsConfig $ def { Plugin.plugins = Map.fromList [ ("hlint", de -- Although a given hlint version supports one direct ghc, we could use several versions of hlint -- each one supporting a different ghc version. It should be a temporary situation though. knownBrokenForHlintOnGhcLib :: String -> TestTree -> TestTree -knownBrokenForHlintOnGhcLib = expectFailBecause +knownBrokenForHlintOnGhcLib = ignoreTestBecause -- 1's based data Point = Point { @@ -396,10 +427,6 @@ data Point = Point { column :: !Int } -makePoint line column - | line >= 1 && column >= 1 = Point line column - | otherwise = error "Line or column is less than 1." - pointToRange :: Point -> Range pointToRange Point {..} | line <- fromIntegral $ subtract 1 line @@ -415,9 +442,9 @@ makeCodeActionNotFoundAtString :: Point -> String makeCodeActionNotFoundAtString Point {..} = "CodeAction not found at line: " <> show line <> ", column: " <> show column -makeCodeActionFoundAtString :: Point -> String -makeCodeActionFoundAtString Point {..} = - "CodeAction found at line: " <> show line <> ", column: " <> show column +-- ------------------------------------------------------------------------ +-- Test runner helpers +-- ------------------------------------------------------------------------ ignoreHintGoldenTest :: TestName -> FilePath -> Point -> T.Text -> TestTree ignoreHintGoldenTest testCaseName goldenFilename point hintName = @@ -429,8 +456,8 @@ applyHintGoldenTest testCaseName goldenFilename point hintName = do goldenTest :: TestName -> FilePath -> Point -> T.Text -> TestTree goldenTest testCaseName goldenFilename point hintText = - setupGoldenHlintTest testCaseName goldenFilename $ \document -> do - waitForDiagnosticsFromSource document "hlint" + setupGoldenHlintTest testCaseName goldenFilename codeActionNoResolveCaps $ \document -> do + _ <- hlintCaptureKick actions <- getCodeActions document $ pointToRange point case find ((== Just hintText) . getCodeActionTitle) actions of Just (InR codeAction) -> do @@ -439,9 +466,16 @@ goldenTest testCaseName goldenFilename point hintText = void $ skipManyTill anyMessage $ getDocumentEdit document _ -> liftIO $ assertFailure $ makeCodeActionNotFoundAtString point -setupGoldenHlintTest :: TestName -> FilePath -> (TextDocumentIdentifier -> Session ()) -> TestTree -setupGoldenHlintTest testName path = - goldenWithHaskellAndCaps def codeActionNoResolveCaps hlintPlugin testName testDir path "expected" "hs" + +setupGoldenHlintTest :: TestName -> FilePath -> ClientCapabilities -> (TextDocumentIdentifier -> Session ()) -> TestTree +setupGoldenHlintTest testName path config = + goldenWithTestConfig def + { testConfigCaps = config + , testShiftRoot = True + , testPluginDescriptor = hlintPlugin + , testDirLocation = Right tree + } testName tree path "expected" "hs" + where tree = mkVirtualFileTree testDir (directProject (path <.> "hs")) ignoreHintGoldenResolveTest :: TestName -> FilePath -> Point -> T.Text -> TestTree ignoreHintGoldenResolveTest testCaseName goldenFilename point hintName = @@ -453,13 +487,9 @@ applyHintGoldenResolveTest testCaseName goldenFilename point hintName = do goldenResolveTest :: TestName -> FilePath -> Point -> T.Text -> TestTree goldenResolveTest testCaseName goldenFilename point hintText = - setupGoldenHlintResolveTest testCaseName goldenFilename $ \document -> do - waitForDiagnosticsFromSource document "hlint" + setupGoldenHlintTest testCaseName goldenFilename codeActionResolveCaps $ \document -> do + _ <- hlintCaptureKick actions <- getAndResolveCodeActions document $ pointToRange point case find ((== Just hintText) . getCodeActionTitle) actions of Just (InR codeAction) -> executeCodeAction codeAction _ -> liftIO $ assertFailure $ makeCodeActionNotFoundAtString point - -setupGoldenHlintResolveTest :: TestName -> FilePath -> (TextDocumentIdentifier -> Session ()) -> TestTree -setupGoldenHlintResolveTest testName path = - goldenWithHaskellAndCaps def codeActionResolveCaps hlintPlugin testName testDir path "expected" "hs" diff --git a/plugins/hls-module-name-plugin/LICENSE b/plugins/hls-module-name-plugin/LICENSE deleted file mode 100644 index 261eeb9e9f..0000000000 --- a/plugins/hls-module-name-plugin/LICENSE +++ /dev/null @@ -1,201 +0,0 @@ - Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ - - TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - - 1. Definitions. - - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. - - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. - - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. - - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. - - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. - - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. - - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). - - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. - - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution." - - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. - - 2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. - - 3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. - - 4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: - - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and - - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and - - (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. - - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. - - 5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. - - 6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. - - 7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. - - 8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. - - 9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. - - END OF TERMS AND CONDITIONS - - APPENDIX: How to apply the Apache License to your work. - - To apply the Apache License to your work, attach the following - boilerplate notice, with the fields enclosed by brackets "[]" - replaced with your own identifying information. (Don't include - the brackets!) The text should be enclosed in the appropriate - comment syntax for the file format. We also recommend that a - file or class name and description of purpose be included on the - same "printed page" as the copyright notice for easier - identification within third-party archives. - - Copyright [yyyy] [name of copyright owner] - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. diff --git a/plugins/hls-module-name-plugin/hls-module-name-plugin.cabal b/plugins/hls-module-name-plugin/hls-module-name-plugin.cabal deleted file mode 100644 index 4648baf67b..0000000000 --- a/plugins/hls-module-name-plugin/hls-module-name-plugin.cabal +++ /dev/null @@ -1,54 +0,0 @@ -cabal-version: 2.4 -name: hls-module-name-plugin -version: 2.4.0.0 -synopsis: Module name plugin for Haskell Language Server -description: - Please see the README on GitHub at - -license: Apache-2.0 -license-file: LICENSE -author: The Haskell IDE Team -copyright: The Haskell IDE Team -maintainer: alan.zimm@gmail.com -category: Development -build-type: Simple -extra-source-files: - LICENSE - test/testdata/**/*.yaml - test/testdata/**/*.hs - test/testdata/**/*.cabal - test/testdata/**/*.project - -source-repository head - type: git - location: https://github.com/haskell/haskell-language-server.git - -library - exposed-modules: Ide.Plugin.ModuleName - hs-source-dirs: src - build-depends: - , aeson - , base >=4.12 && <5 - , containers - , directory - , filepath - , ghcide == 2.4.0.0 - , hls-plugin-api == 2.4.0.0 - , lsp - , text - , transformers - , unordered-containers - - default-language: Haskell2010 - -test-suite tests - type: exitcode-stdio-1.0 - default-language: Haskell2010 - hs-source-dirs: test - main-is: Main.hs - ghc-options: -threaded -rtsopts -with-rtsopts=-N - build-depends: - , base - , filepath - , hls-module-name-plugin - , hls-test-utils == 2.4.0.0 diff --git a/plugins/hls-module-name-plugin/src/Ide/Plugin/ModuleName.hs b/plugins/hls-module-name-plugin/src/Ide/Plugin/ModuleName.hs index 50264a68f1..5dc053f47d 100644 --- a/plugins/hls-module-name-plugin/src/Ide/Plugin/ModuleName.hs +++ b/plugins/hls-module-name-plugin/src/Ide/Plugin/ModuleName.hs @@ -2,12 +2,8 @@ {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE PatternSynonyms #-} {-# LANGUAGE RecordWildCards #-} -{-# LANGUAGE TypeOperators #-} {-# LANGUAGE ViewPatterns #-} -{-# OPTIONS_GHC -Wall -Wwarn -fno-warn-type-defaults #-} -{-# OPTIONS_GHC -Wno-name-shadowing #-} - {- | Keep the module name in sync with its file path. Provide CodeLenses to: @@ -27,24 +23,25 @@ import Control.Monad.Trans.Maybe import Data.Aeson (toJSON) import Data.Char (isLower, isUpper) import Data.List (intercalate, minimumBy, - stripPrefix, uncons) + stripPrefix) import qualified Data.List.NonEmpty as NE import qualified Data.Map as Map import Data.Maybe (mapMaybe) import Data.Ord (comparing) import Data.String (IsString) import qualified Data.Text as T +import qualified Data.Text.Utf16.Rope.Mixed as Rope import Development.IDE (GetParsedModule (GetParsedModule), GhcSession (GhcSession), IdeState, Pretty, Priority (Debug), Recorder, WithPriority, colon, evalGhcEnv, - hscEnvWithImportPaths, - logWith, + hscEnv, logWith, realSrcSpanToRange, - runAction, useWithStale, - (<+>)) + rootDir, runAction, + useWithStale, (<+>)) +import Development.IDE.Core.FileStore (getFileContents) import Development.IDE.Core.PluginUtils import Development.IDE.Core.PositionMapping (toCurrentRange) import Development.IDE.GHC.Compat (GenLocated (L), @@ -55,12 +52,10 @@ import Development.IDE.GHC.Compat (GenLocated (L), pm_parsed_source, unLoc) import Ide.Logger (Pretty (..)) import Ide.Plugin.Error +import Ide.PluginUtils (toAbsolute) import Ide.Types import Language.LSP.Protocol.Message import Language.LSP.Protocol.Types -import Language.LSP.Server -import Language.LSP.VFS (virtualFileText) -import System.Directory (makeAbsolute) import System.FilePath (dropExtension, normalise, pathSeparator, splitDirectories, @@ -69,7 +64,7 @@ import System.FilePath (dropExtension, normalise, -- |Plugin descriptor descriptor :: Recorder (WithPriority Log) -> PluginId -> PluginDescriptor IdeState descriptor recorder plId = - (defaultPluginDescriptor plId) + (defaultPluginDescriptor plId "Provides a code action to alter the module name if it is wrong") { pluginHandlers = mkPluginHandler SMethod_TextDocumentCodeLens (codeLens recorder) , pluginCommands = [PluginCommand updateModuleNameCommand "set name of module to match with file path" (command recorder)] } @@ -90,14 +85,14 @@ codeLens recorder state pluginId CodeLensParams{_textDocument=TextDocumentIdenti -- | (Quasi) Idempotent command execution: recalculate action to execute on command request command :: Recorder (WithPriority Log) -> CommandFunction IdeState Uri -command recorder state uri = do +command recorder state _ uri = do actMaybe <- action recorder state uri forM_ actMaybe $ \Replace{..} -> let -- | Convert an Action to the corresponding edit operation edit = WorkspaceEdit (Just $ Map.singleton aUri [TextEdit aRange aCode]) Nothing Nothing in - void $ lift $ sendRequest SMethod_WorkspaceApplyEdit (ApplyWorkspaceEditParams Nothing edit) (const (pure ())) + void $ lift $ pluginSendRequest SMethod_WorkspaceApplyEdit (ApplyWorkspaceEditParams Nothing edit) (const (pure ())) pure $ InR Null -- | A source code change @@ -110,13 +105,13 @@ data Action = Replace deriving (Show) -- | Required action (that can be converted to either CodeLenses or CodeActions) -action :: Recorder (WithPriority Log) -> IdeState -> Uri -> ExceptT PluginError (LspM c) [Action] +action :: Recorder (WithPriority Log) -> IdeState -> Uri -> ExceptT PluginError (HandlerM c) [Action] action recorder state uri = do nfp <- getNormalizedFilePathE uri fp <- uriToFilePathE uri - contents <- lift . getVirtualFile $ toNormalizedUri uri - let emptyModule = maybe True (T.null . T.strip . virtualFileText) contents + contents <- liftIO $ runAction "ModuleName.getFileContents" state $ getFileContents nfp + let emptyModule = maybe True (T.null . T.strip . Rope.toText) contents correctNames <- mapExceptT liftIO $ pathModuleNames recorder state nfp fp logWith recorder Debug (CorrectNames correctNames) @@ -133,17 +128,17 @@ action recorder state uri = do | emptyModule -> let code = "module " <> bestName <> " where\n" in pure [Replace uri (Range (Position 0 0) (Position 0 0)) code code] - _ -> pure $ [] + _ -> pure [] -- | Possible module names, as derived by the position of the module in the -- source directories. There may be more than one possible name, if the source -- directories are nested inside each other. pathModuleNames :: Recorder (WithPriority Log) -> IdeState -> NormalizedFilePath -> FilePath -> ExceptT PluginError IO [T.Text] pathModuleNames recorder state normFilePath filePath - | isLower . head $ takeFileName filePath = return ["Main"] + | firstLetter isLower $ takeFileName filePath = return ["Main"] | otherwise = do (session, _) <- runActionE "ModuleName.ghcSession" state $ useWithStaleE GhcSession normFilePath - srcPaths <- liftIO $ evalGhcEnv (hscEnvWithImportPaths session) $ importPaths <$> getSessionDynFlags + srcPaths <- liftIO $ evalGhcEnv (hscEnv session) $ importPaths <$> getSessionDynFlags logWith recorder Debug (SrcPaths srcPaths) -- Append a `pathSeparator` to make the path looks like a directory, @@ -152,18 +147,25 @@ pathModuleNames recorder state normFilePath filePath let paths = map (normalise . (<> pure pathSeparator)) srcPaths logWith recorder Debug (NormalisedPaths paths) - mdlPath <- liftIO $ makeAbsolute filePath + -- TODO, this can be avoid if the filePath is already absolute, + -- we can avoid the toAbsolute call in the future. + -- see Note [Root Directory] + let mdlPath = (toAbsolute $ rootDir state) filePath logWith recorder Debug (AbsoluteFilePath mdlPath) let suffixes = mapMaybe (`stripPrefix` mdlPath) paths pure (map moduleNameFrom suffixes) where + firstLetter :: (Char -> Bool) -> FilePath -> Bool + firstLetter _ [] = False + firstLetter pred (c:_) = pred c + moduleNameFrom = T.pack . intercalate "." -- Do not suggest names whose components start from a lower-case char, -- they are guaranteed to be malformed. - . filter (maybe False (isUpper . fst) . uncons) + . filter (firstLetter isUpper) . splitDirectories . dropExtension diff --git a/plugins/hls-module-name-plugin/test/Main.hs b/plugins/hls-module-name-plugin/test/Main.hs index ae5a87f0d5..ba1ed756e5 100644 --- a/plugins/hls-module-name-plugin/test/Main.hs +++ b/plugins/hls-module-name-plugin/test/Main.hs @@ -55,7 +55,6 @@ tests = let edit = TextEdit (mkRange 1 0 1 0) "f =" _ <- applyEdit doc edit newLens <- getCodeLenses doc - txt <- documentContents doc liftIO $ newLens @?= oldLens closeDoc doc ] @@ -64,4 +63,4 @@ goldenWithModuleName :: TestName -> FilePath -> (TextDocumentIdentifier -> Sessi goldenWithModuleName title path = goldenWithHaskellDoc def moduleNamePlugin title testDataDir path "expected" "hs" testDataDir :: FilePath -testDataDir = "test" "testdata" +testDataDir = "plugins" "hls-module-name-plugin" "test" "testdata" diff --git a/plugins/hls-notes-plugin/README.md b/plugins/hls-notes-plugin/README.md new file mode 100644 index 0000000000..7b05669d46 --- /dev/null +++ b/plugins/hls-notes-plugin/README.md @@ -0,0 +1,32 @@ +# Note plugin + +The [Note convention](https://gitlab.haskell.org/ghc/ghc/-/wikis/commentary/coding-style#2-using-notes) is a nice way to hoist and share big chunks of documentation out of the body of functions. This is done by referencing a long form note from within the function. This plugin extends goto-definition to jump from the reference to the note. + +# Example + +Main.hs +```haskell +module Main where + +main :: IO +main = do + doSomething -- We need this here, see Note [Do Something] in Foo + -- Using at-signs around the note works as well: + -- see @Note [Do Something]@ in Foo +``` + +Foo.hs +```haskell +module Foo where + +doSomething :: IO () +doSomething = undefined + +{- +Note [Do Something] +~~~~~~~~~~~~~~~~~~~ +Some very important explanation +-} +``` + +Using "Go-to-definition" on the Note reference in `Main.hs` will jump to the beginning of the note in `Foo.hs`. diff --git a/plugins/hls-notes-plugin/src/Ide/Plugin/Notes.hs b/plugins/hls-notes-plugin/src/Ide/Plugin/Notes.hs new file mode 100644 index 0000000000..db1696d94b --- /dev/null +++ b/plugins/hls-notes-plugin/src/Ide/Plugin/Notes.hs @@ -0,0 +1,197 @@ +module Ide.Plugin.Notes (descriptor, Log) where + +import Control.Lens ((^.)) +import Control.Monad.Except (ExceptT, MonadError, + throwError) +import Control.Monad.IO.Class (liftIO) +import qualified Data.Array as A +import Data.Foldable (foldl') +import Data.HashMap.Strict (HashMap) +import qualified Data.HashMap.Strict as HM +import qualified Data.HashSet as HS +import Data.List (uncons) +import Data.Maybe (catMaybes, listToMaybe, + mapMaybe) +import Data.Text (Text, intercalate) +import qualified Data.Text as T +import qualified Data.Text.Utf16.Rope.Mixed as Rope +import Data.Traversable (for) +import Development.IDE hiding (line) +import Development.IDE.Core.PluginUtils (runActionE, useE) +import Development.IDE.Core.Shake (toKnownFiles) +import qualified Development.IDE.Core.Shake as Shake +import Development.IDE.Graph.Classes (Hashable, NFData) +import GHC.Generics (Generic) +import Ide.Plugin.Error (PluginError (..)) +import Ide.Types +import qualified Language.LSP.Protocol.Lens as L +import Language.LSP.Protocol.Message (Method (Method_TextDocumentDefinition, Method_TextDocumentReferences), + SMethod (SMethod_TextDocumentDefinition, SMethod_TextDocumentReferences)) +import Language.LSP.Protocol.Types +import Text.Regex.TDFA (Regex, caseSensitive, + defaultCompOpt, + defaultExecOpt, + makeRegexOpts, matchAllText) + +data Log + = LogShake Shake.Log + | LogNotesFound NormalizedFilePath [(Text, [Position])] + | LogNoteReferencesFound NormalizedFilePath [(Text, [Position])] + deriving Show + +data GetNotesInFile = MkGetNotesInFile + deriving (Show, Generic, Eq, Ord) + deriving anyclass (Hashable, NFData) +-- The GetNotesInFile action scans the source file and extracts a map of note +-- definitions (note name -> position) and a map of note references +-- (note name -> [position]). +type instance RuleResult GetNotesInFile = (HM.HashMap Text Position, HM.HashMap Text [Position]) + +data GetNotes = MkGetNotes + deriving (Show, Generic, Eq, Ord) + deriving anyclass (Hashable, NFData) +-- GetNotes collects all note definition across all files in the +-- project. It returns a map from note name to pair of (filepath, position). +type instance RuleResult GetNotes = HashMap Text (NormalizedFilePath, Position) + +data GetNoteReferences = MkGetNoteReferences + deriving (Show, Generic, Eq, Ord) + deriving anyclass (Hashable, NFData) +-- GetNoteReferences collects all note references across all files in the +-- project. It returns a map from note name to list of (filepath, position). +type instance RuleResult GetNoteReferences = HashMap Text [(NormalizedFilePath, Position)] + +instance Pretty Log where + pretty = \case + LogShake l -> pretty l + LogNoteReferencesFound file refs -> "Found note references in " <> prettyNotes file refs + LogNotesFound file notes -> "Found notes in " <> prettyNotes file notes + where prettyNotes file hm = pretty (show file) <> ": [" + <> pretty (intercalate ", " (fmap (\(s, p) -> "\"" <> s <> "\" at " <> intercalate ", " (map (T.pack . show) p)) hm)) <> "]" + +{- +The first time the user requests a jump-to-definition on a note reference, the +project is indexed and searched for all note definitions. Their location and +title is then saved in the HLS database to be retrieved for all future requests. +-} +descriptor :: Recorder (WithPriority Log) -> PluginId -> PluginDescriptor IdeState +descriptor recorder plId = (defaultPluginDescriptor plId "Provides goto definition support for GHC-style notes") + { Ide.Types.pluginRules = findNotesRules recorder + , Ide.Types.pluginHandlers = + mkPluginHandler SMethod_TextDocumentDefinition jumpToNote + <> mkPluginHandler SMethod_TextDocumentReferences listReferences + } + +findNotesRules :: Recorder (WithPriority Log) -> Rules () +findNotesRules recorder = do + defineNoDiagnostics (cmapWithPrio LogShake recorder) $ \MkGetNotesInFile nfp -> do + findNotesInFile nfp recorder + + defineNoDiagnostics (cmapWithPrio LogShake recorder) $ \MkGetNotes _ -> do + targets <- toKnownFiles <$> useNoFile_ GetKnownTargets + definedNotes <- catMaybes <$> mapM (\nfp -> fmap (HM.map (nfp,) . fst) <$> use MkGetNotesInFile nfp) (HS.toList targets) + pure $ Just $ HM.unions definedNotes + + defineNoDiagnostics (cmapWithPrio LogShake recorder) $ \MkGetNoteReferences _ -> do + targets <- toKnownFiles <$> useNoFile_ GetKnownTargets + definedReferences <- catMaybes <$> for (HS.toList targets) (\nfp -> do + references <- fmap snd <$> use MkGetNotesInFile nfp + pure $ fmap (HM.map (fmap (nfp,))) references + ) + pure $ Just $ foldl' (HM.unionWith (<>)) HM.empty definedReferences + +err :: MonadError PluginError m => Text -> Maybe a -> m a +err s = maybe (throwError $ PluginInternalError s) pure + +getNote :: NormalizedFilePath -> IdeState -> Position -> ExceptT PluginError (HandlerM c) (Maybe Text) +getNote nfp state (Position l c) = do + contents <- + err "Error getting file contents" + =<< liftIO (runAction "notes.getfileContents" state (getFileContents nfp)) + line <- err "Line not found in file" (listToMaybe $ Rope.lines $ fst + (Rope.splitAtLine 1 $ snd $ Rope.splitAtLine (fromIntegral l) contents)) + pure $ listToMaybe $ mapMaybe (atPos $ fromIntegral c) $ matchAllText noteRefRegex line + where + atPos c arr = case arr A.! 0 of + -- We check if the line we are currently at contains a note + -- reference. However, we need to know if the cursor is within the + -- match or somewhere else. The second entry of the array contains + -- the title of the note as extracted by the regex. + (_, (c', len)) -> if c' <= c && c <= c' + len + then Just (fst (arr A.! 1)) else Nothing + +listReferences :: PluginMethodHandler IdeState Method_TextDocumentReferences +listReferences state _ param + | Just nfp <- uriToNormalizedFilePath uriOrig + = do + let pos@(Position l _) = param ^. L.position + noteOpt <- getNote nfp state pos + case noteOpt of + Nothing -> pure (InR Null) + Just note -> do + notes <- runActionE "notes.definedNoteReferencess" state $ useE MkGetNoteReferences nfp + poss <- err ("Note reference (a comment of the form `{- Note [" <> note <> "] -}`) not found") (HM.lookup note notes) + pure $ InL (mapMaybe (\(noteFp, pos@(Position l' _)) -> if l' == l then Nothing else Just ( + Location (fromNormalizedUri $ normalizedFilePathToUri noteFp) (Range pos pos))) poss) + where + uriOrig = toNormalizedUri $ param ^. (L.textDocument . L.uri) +listReferences _ _ _ = throwError $ PluginInternalError "conversion to normalized file path failed" + +jumpToNote :: PluginMethodHandler IdeState Method_TextDocumentDefinition +jumpToNote state _ param + | Just nfp <- uriToNormalizedFilePath uriOrig + = do + noteOpt <- getNote nfp state (param ^. L.position) + case noteOpt of + Nothing -> pure (InR (InR Null)) + Just note -> do + notes <- runActionE "notes.definedNotes" state $ useE MkGetNotes nfp + (noteFp, pos) <- err ("Note definition (a comment of the form `{- Note [" <> note <> "]\\n~~~ ... -}`) not found") (HM.lookup note notes) + pure $ InL (Definition (InL + (Location (fromNormalizedUri $ normalizedFilePathToUri noteFp) (Range pos pos)) + )) + where + uriOrig = toNormalizedUri $ param ^. (L.textDocument . L.uri) +jumpToNote _ _ _ = throwError $ PluginInternalError "conversion to normalized file path failed" + +findNotesInFile :: NormalizedFilePath -> Recorder (WithPriority Log) -> Action (Maybe (HM.HashMap Text Position, HM.HashMap Text [Position])) +findNotesInFile file recorder = do + -- GetFileContents only returns a value if the file is open in the editor of + -- the user. If not, we need to read it from disk. + contentOpt <- (snd =<<) <$> use GetFileContents file + content <- case contentOpt of + Just x -> pure $ Rope.toText x + Nothing -> liftIO $ readFileUtf8 $ fromNormalizedFilePath file + let noteMatches = (A.! 1) <$> matchAllText noteRegex content + notes = toPositions noteMatches content + logWith recorder Debug $ LogNotesFound file (HM.toList notes) + let refMatches = (A.! 1) <$> matchAllText noteRefRegex content + refs = toPositions refMatches content + logWith recorder Debug $ LogNoteReferencesFound file (HM.toList refs) + pure $ Just (HM.mapMaybe (fmap fst . uncons) notes, refs) + where + uint = fromIntegral . toInteger + -- the regex library returns the character index of the match. However + -- to return the position from HLS we need it as a (line, character) + -- tuple. To convert between the two we count the newline characters and + -- reset the current character index every time. For every regex match, + -- once we have counted up to their character index, we save the current + -- line and character values instead. + toPositions matches = snd . fst . T.foldl' (\case + (([], m), _) -> const (([], m), (0, 0, 0)) + ((x@(name, (char, _)):xs, m), (n, nc, c)) -> \char' -> + let !c' = c + 1 + (!n', !nc') = if char' == '\n' then (n + 1, c') else (n, nc) + p@(!_, !_) = if char == c then + (xs, HM.insertWith (<>) name [Position (uint n') (uint (char - nc'))] m) + else (x:xs, m) + in (p, (n', nc', c')) + ) ((matches, HM.empty), (0, 0, 0)) + +noteRefRegex, noteRegex :: Regex +(noteRefRegex, noteRegex) = + ( mkReg ("note \\[(.+)\\]" :: String) + , mkReg ("note \\[([[:print:]]+)\\][[:blank:]]*\r?\n[[:blank:]]*(--)?[[:blank:]]*~~~" :: String) + ) + where + mkReg = makeRegexOpts (defaultCompOpt { caseSensitive = False }) defaultExecOpt diff --git a/plugins/hls-notes-plugin/test/NotesTest.hs b/plugins/hls-notes-plugin/test/NotesTest.hs new file mode 100644 index 0000000000..f84bed9731 --- /dev/null +++ b/plugins/hls-notes-plugin/test/NotesTest.hs @@ -0,0 +1,77 @@ +module Main (main) where + +import Ide.Plugin.Notes (Log, descriptor) +import System.FilePath (()) +import Test.Hls + +plugin :: PluginTestDescriptor Log +plugin = mkPluginTestDescriptor descriptor "notes" + +main :: IO () +main = defaultTestRunner $ + testGroup "Notes" + [ gotoNoteTests + , noteReferenceTests + ] + +runSessionWithServer' :: FilePath -> (FilePath -> Session a) -> IO a +runSessionWithServer' fp act = + runSessionWithTestConfig def + { testLspConfig = def + , testPluginDescriptor = plugin + , testDirLocation = Left fp + } act + +noteReferenceTests :: TestTree +noteReferenceTests = testGroup "Note References" + [ + testCase "multi_file" $ runSessionWithServer' testDataDir $ \dir -> do + doc <- openDoc "NoteDef.hs" "haskell" + waitForKickDone + refs <- getReferences doc (Position 21 15) False + let fp = dir "NoteDef.hs" + liftIO $ refs @?= [ + Location (filePathToUri (dir "Other.hs")) (Range (Position 6 13) (Position 6 13)), + Location (filePathToUri fp) (Range (Position 9 9) (Position 9 9)), + Location (filePathToUri fp) (Range (Position 5 67) (Position 5 67)) + ] + ] + +gotoNoteTests :: TestTree +gotoNoteTests = testGroup "Goto Note Definition" + [ + testCase "single_file" $ runSessionWithServer' testDataDir $ \dir -> do + doc <- openDoc "NoteDef.hs" "haskell" + waitForKickDone + defs <- getDefinitions doc (Position 3 41) + let fp = dir "NoteDef.hs" + liftIO $ defs @?= InL (Definition (InR [Location (filePathToUri fp) (Range (Position 11 9) (Position 11 9))])) + , testCase "liberal_format" $ runSessionWithServer' testDataDir $ \dir -> do + doc <- openDoc "NoteDef.hs" "haskell" + waitForKickDone + defs <- getDefinitions doc (Position 5 64) + let fp = dir "NoteDef.hs" + liftIO $ defs @?= InL (Definition (InR [Location (filePathToUri fp) (Range (Position 21 11) (Position 21 11))])) + + , testCase "invalid_note" $ runSessionWithServer' testDataDir $ const $ do + doc <- openDoc "NoteDef.hs" "haskell" + waitForKickDone + defs <- getDefinitions doc (Position 6 54) + liftIO $ defs @?= InL (Definition (InR [])) + + , testCase "no_note" $ runSessionWithServer' testDataDir $ const $ do + doc <- openDoc "NoteDef.hs" "haskell" + waitForKickDone + defs <- getDefinitions doc (Position 1 0) + liftIO $ defs @?= InL (Definition (InR [])) + + , testCase "unopened_file" $ runSessionWithServer' testDataDir $ \dir -> do + doc <- openDoc "Other.hs" "haskell" + waitForKickDone + defs <- getDefinitions doc (Position 5 20) + let fp = dir "NoteDef.hs" + liftIO $ defs @?= InL (Definition (InR [Location (filePathToUri fp) (Range (Position 15 6) (Position 15 6))])) + ] + +testDataDir :: FilePath +testDataDir = "plugins" "hls-notes-plugin" "test" "testdata" diff --git a/plugins/hls-notes-plugin/test/testdata/NoteDef.hs b/plugins/hls-notes-plugin/test/testdata/NoteDef.hs new file mode 100644 index 0000000000..c4b450ced4 --- /dev/null +++ b/plugins/hls-notes-plugin/test/testdata/NoteDef.hs @@ -0,0 +1,31 @@ +module NoteDef (foo) where + +foo :: Int -> Int +foo _ = 0 -- We always return zero, see Note [Returning zero from foo] + +-- The plugin is more liberal with the note definitions, see Note [Single line comments] +-- It does not work on wrong note definitions, see Note [Not a valid Note] + +-- We can also have multiple references to the same note, see +-- Note [Single line comments] + +{- Note [Returning zero from foo] +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +This is a big long form note, with very important info + +Note [Multiple notes in comment] +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +This is also a very common thing to do for GHC + +-} + + -- Note [Single line comments] + -- ~~~~~~~~~~~~~~~~~~~~~~~~~~~ + -- GHC's notes script only allows multiline comments to define notes, but in the + -- HLS codebase this single line style can be found as well. + +{- Note [Not a valid Note] + +~~~~~~~~~~~~ +The underline needs to be directly under the Note header +-} diff --git a/plugins/hls-notes-plugin/test/testdata/Other.hs b/plugins/hls-notes-plugin/test/testdata/Other.hs new file mode 100644 index 0000000000..aa64e19a79 --- /dev/null +++ b/plugins/hls-notes-plugin/test/testdata/Other.hs @@ -0,0 +1,7 @@ +module Other where + +import NoteDef + +bar :: Int +bar = 4 -- See @Note [Multiple notes in comment]@ in NoteDef +-- See Note [Single line comments] diff --git a/plugins/hls-notes-plugin/test/testdata/hie.yaml b/plugins/hls-notes-plugin/test/testdata/hie.yaml new file mode 100644 index 0000000000..59cc740ee8 --- /dev/null +++ b/plugins/hls-notes-plugin/test/testdata/hie.yaml @@ -0,0 +1,5 @@ +cradle: + direct: + arguments: + - Other + - NoteDef diff --git a/plugins/hls-ormolu-plugin/LICENSE b/plugins/hls-ormolu-plugin/LICENSE deleted file mode 100644 index 16502c47e2..0000000000 --- a/plugins/hls-ormolu-plugin/LICENSE +++ /dev/null @@ -1,201 +0,0 @@ - Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ - - TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - - 1. Definitions. - - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. - - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. - - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. - - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. - - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. - - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. - - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). - - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. - - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution." - - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. - - 2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. - - 3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. - - 4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: - - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and - - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and - - (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. - - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. - - 5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. - - 6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. - - 7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. - - 8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. - - 9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. - - END OF TERMS AND CONDITIONS - - APPENDIX: How to apply the Apache License to your work. - - To apply the Apache License to your work, attach the following - boilerplate notice, with the fields enclosed by brackets "[]" - replaced with your own identifying information. (Don't include - the brackets!) The text should be enclosed in the appropriate - comment syntax for the file format. We also recommend that a - file or class name and description of purpose be included on the - same "printed page" as the copyright notice for easier - identification within third-party archives. - - Copyright 2021 The Haskell IDE team - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. diff --git a/plugins/hls-ormolu-plugin/hls-ormolu-plugin.cabal b/plugins/hls-ormolu-plugin/hls-ormolu-plugin.cabal deleted file mode 100644 index e1ec3cb029..0000000000 --- a/plugins/hls-ormolu-plugin/hls-ormolu-plugin.cabal +++ /dev/null @@ -1,69 +0,0 @@ -cabal-version: 2.4 -name: hls-ormolu-plugin -version: 2.4.0.0 -synopsis: Integration with the Ormolu code formatter -description: - Please see the README on GitHub at - -license: Apache-2.0 -license-file: LICENSE -author: The Haskell IDE Team -copyright: The Haskell IDE Team -maintainer: alan.zimm@gmail.com -category: Development -build-type: Simple -extra-source-files: - LICENSE - test/testdata/**/*.hs - test/testdata/.ormolu - test/testdata/test.cabal - -source-repository head - type: git - location: https://github.com/haskell/haskell-language-server.git - -library - -- Plugins that need exactprint have not been updated for 9.8 yet - if impl(ghc >= 9.7) - buildable: False - exposed-modules: Ide.Plugin.Ormolu - hs-source-dirs: src - build-depends: - , base >=4.12 && <5 - , extra - , filepath - , ghc - , ghc-boot-th - , ghcide == 2.4.0.0 - , hls-plugin-api == 2.4.0.0 - , lens - , lsp - , mtl - , process-extras >= 0.7.1 - , ormolu ^>=0.1.2 || ^>= 0.2 || ^>= 0.3 || ^>= 0.5 || ^>= 0.6 || ^>= 0.7 - , text - , transformers - - default-language: Haskell2010 - -test-suite tests - if impl(ghc >= 9.7) - buildable: False - type: exitcode-stdio-1.0 - default-language: Haskell2010 - hs-source-dirs: test - main-is: Main.hs - ghc-options: -threaded -rtsopts -with-rtsopts=-N - build-tool-depends: - ormolu:ormolu - build-depends: - , base - , aeson - , containers - , filepath - , hls-ormolu-plugin - , hls-plugin-api - , hls-test-utils == 2.4.0.0 - , lsp-types - , text - , ormolu diff --git a/plugins/hls-ormolu-plugin/src/Ide/Plugin/Ormolu.hs b/plugins/hls-ormolu-plugin/src/Ide/Plugin/Ormolu.hs index bf126e4742..90c5214d8e 100644 --- a/plugins/hls-ormolu-plugin/src/Ide/Plugin/Ormolu.hs +++ b/plugins/hls-ormolu-plugin/src/Ide/Plugin/Ormolu.hs @@ -1,11 +1,8 @@ {-# LANGUAGE CPP #-} {-# LANGUAGE DataKinds #-} {-# LANGUAGE LambdaCase #-} -{-# LANGUAGE NamedFieldPuns #-} {-# LANGUAGE OverloadedLabels #-} {-# LANGUAGE OverloadedStrings #-} -{-# LANGUAGE TypeApplications #-} -{-# LANGUAGE TypeOperators #-} module Ide.Plugin.Ormolu ( descriptor , provider @@ -13,49 +10,48 @@ module Ide.Plugin.Ormolu ) where -import Control.Exception (Handler (..), IOException, - SomeException (..), catches, - handle) -import Control.Monad.Except (ExceptT (ExceptT), runExceptT, - throwError) +import Control.Exception (Handler (..), IOException, + SomeException (..), catches, + handle) +import Control.Monad.Except (runExceptT, throwError) import Control.Monad.Extra -import Control.Monad.IO.Class (liftIO) import Control.Monad.Trans -import Control.Monad.Trans.Except (ExceptT (..), mapExceptT, - runExceptT) -import Data.Functor ((<&>)) -import Data.List (intercalate) -import Data.Maybe (catMaybes) -import Data.Text (Text) -import qualified Data.Text as T -import Development.IDE hiding (pluginHandlers) -import Development.IDE.GHC.Compat (hsc_dflags, moduleNameString) -import qualified Development.IDE.GHC.Compat as D -import qualified Development.IDE.GHC.Compat.Util as S +import Control.Monad.Trans.Except (ExceptT (..), mapExceptT) +import Data.Functor ((<&>)) +import Data.List (intercalate) +import Data.Maybe (catMaybes) +import Data.Text (Text) +import qualified Data.Text as T +import Development.IDE hiding (pluginHandlers) +import Development.IDE.Core.PluginUtils (mkFormattingHandlers) +import Development.IDE.GHC.Compat (hsc_dflags, moduleNameString) +import qualified Development.IDE.GHC.Compat as D +import qualified Development.IDE.GHC.Compat.Util as S import GHC.LanguageExtensions.Type -import Ide.Plugin.Error (PluginError (PluginInternalError)) +import Ide.Plugin.Error (PluginError (PluginInternalError)) import Ide.Plugin.Properties import Ide.PluginUtils -import Ide.Types hiding (Config) -import qualified Ide.Types as Types -import Language.LSP.Protocol.Message +import Ide.Types hiding (Config) +import qualified Ide.Types as Types import Language.LSP.Protocol.Types -import Language.LSP.Server hiding (defaultConfig) +import Language.LSP.Server hiding (defaultConfig) import Ormolu import System.Exit import System.FilePath -import System.Process.Run (cwd, proc) -import System.Process.Text (readCreateProcessWithExitCode) -import Text.Read (readMaybe) +import System.Process.Run (cwd, proc) +import System.Process.Text (readCreateProcessWithExitCode) +import Text.Read (readMaybe) -- --------------------------------------------------------------------- descriptor :: Recorder (WithPriority LogEvent) -> PluginId -> PluginDescriptor IdeState descriptor recorder plId = - (defaultPluginDescriptor plId) + (defaultPluginDescriptor plId desc) { pluginHandlers = mkFormattingHandlers $ provider recorder plId, pluginConfigDescriptor = defaultConfigDescriptor {configCustomConfig = mkCustomConfig properties} } + where + desc = "Provides formatting of Haskell files via ormolu. Built with ormolu-" <> VERSION_ormolu properties :: Properties '[ 'PropertyKey "external" 'TBoolean] properties = @@ -68,7 +64,7 @@ properties = -- --------------------------------------------------------------------- provider :: Recorder (WithPriority LogEvent) -> PluginId -> FormattingHandler IdeState -provider recorder plId ideState typ contents fp _ = ExceptT $ withIndefiniteProgress title Cancellable $ runExceptT $ do +provider recorder plId ideState token typ contents fp _ = ExceptT $ pluginWithIndefiniteProgress title token Cancellable $ \_updater -> runExceptT $ do fileOpts <- maybe [] (fromDyn . hsc_dflags . hscEnv) <$> liftIO (runAction "Ormolu" ideState $ use GhcSession fp) @@ -122,7 +118,7 @@ provider recorder plId ideState typ contents fp _ = ExceptT $ withIndefiniteProg title = T.pack $ "Formatting " <> takeFileName (fromNormalizedFilePath fp) - ret :: Either SomeException T.Text -> ExceptT PluginError (LspM Types.Config) ([TextEdit] |? Null) + ret :: Either SomeException T.Text -> ExceptT PluginError (HandlerM Types.Config) ([TextEdit] |? Null) ret (Left err) = throwError $ PluginInternalError . T.pack $ "ormoluCmd: " ++ show err ret (Right new) = pure $ InL $ makeDiffTextEdit contents new diff --git a/plugins/hls-ormolu-plugin/test/Main.hs b/plugins/hls-ormolu-plugin/test/Main.hs index 1dd8f9b3d4..05f7a2a115 100644 --- a/plugins/hls-ormolu-plugin/test/Main.hs +++ b/plugins/hls-ormolu-plugin/test/Main.hs @@ -5,8 +5,8 @@ module Main ) where import Data.Aeson +import qualified Data.Aeson.KeyMap as KM import Data.Functor -import qualified Data.Text as T import Ide.Plugin.Config import qualified Ide.Plugin.Ormolu as Ormolu import Language.LSP.Protocol.Types @@ -34,9 +34,10 @@ tests = testGroup "ormolu" $ ] goldenWithOrmolu :: Bool -> TestName -> FilePath -> FilePath -> (TextDocumentIdentifier -> Session ()) -> TestTree -goldenWithOrmolu cli title path desc = goldenWithHaskellDocFormatter def ormoluPlugin "ormolu" def title testDataDir path desc "hs" +goldenWithOrmolu cli title path desc = + goldenWithHaskellDocFormatter def ormoluPlugin "ormolu" conf title testDataDir path desc "hs" where - conf = def{plcConfig = (\(Object obj) -> obj) $ object ["external" .= cli]} + conf = def{plcConfig = KM.fromList ["external" .= cli]} testDataDir :: FilePath -testDataDir = "test" "testdata" +testDataDir = "plugins" "hls-ormolu-plugin" "test" "testdata" diff --git a/plugins/hls-overloaded-record-dot-plugin/CHANGELOG.md b/plugins/hls-overloaded-record-dot-plugin/CHANGELOG.md deleted file mode 100644 index 6179d5a570..0000000000 --- a/plugins/hls-overloaded-record-dot-plugin/CHANGELOG.md +++ /dev/null @@ -1,5 +0,0 @@ -# Revision history for hls-overloaded-record-dot-plugin - -## 1.0.0.0 -- 2023-04-16 - -* First version. diff --git a/plugins/hls-overloaded-record-dot-plugin/LICENSE b/plugins/hls-overloaded-record-dot-plugin/LICENSE deleted file mode 100644 index 16590f45c8..0000000000 --- a/plugins/hls-overloaded-record-dot-plugin/LICENSE +++ /dev/null @@ -1,30 +0,0 @@ -Copyright (c) 2023, Nathan Maxson - -All rights reserved. - -Redistribution and use in source and binary forms, with or without -modification, are permitted provided that the following conditions are met: - - * Redistributions of source code must retain the above copyright - notice, this list of conditions and the following disclaimer. - - * Redistributions in binary form must reproduce the above - copyright notice, this list of conditions and the following - disclaimer in the documentation and/or other materials provided - with the distribution. - - * Neither the name of Nathan Maxson nor the names of other - contributors may be used to endorse or promote products derived - from this software without specific prior written permission. - -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS -"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT -LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR -A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT -OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, -DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY -THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/plugins/hls-overloaded-record-dot-plugin/hls-overloaded-record-dot-plugin.cabal b/plugins/hls-overloaded-record-dot-plugin/hls-overloaded-record-dot-plugin.cabal deleted file mode 100644 index 1faf118da1..0000000000 --- a/plugins/hls-overloaded-record-dot-plugin/hls-overloaded-record-dot-plugin.cabal +++ /dev/null @@ -1,71 +0,0 @@ -cabal-version: 3.0 -name: hls-overloaded-record-dot-plugin -version: 2.4.0.0 -synopsis: Overloaded record dot plugin for Haskell Language Server -description: - Please see the README on GitHub at -license: BSD-3-Clause -license-file: LICENSE -author: Nathan Maxson -maintainer: joyfulmantis@gmail.com -category: Development -build-type: Simple -extra-doc-files: CHANGELOG.md -extra-source-files: - test/testdata/**/*.hs - -source-repository head - type: git - location: https://github.com/haskell/haskell-language-server - -common warnings - ghc-options: -Wall - -library - if impl(ghc < 9.2) - buildable: False - else - buildable: True - import: warnings - exposed-modules: Ide.Plugin.OverloadedRecordDot - build-depends: - , base >=4.16 && <5 - , aeson - , ghcide - , hls-plugin-api - , lsp - , lens - , hls-graph - , text - , syb - , transformers - , ghc-boot-th - , unordered-containers - , containers - , deepseq - hs-source-dirs: src - default-language: GHC2021 - -test-suite tests - import: warnings - if impl(ghc < 9.2) - buildable: False - else - buildable: True - default-language: GHC2021 - type: exitcode-stdio-1.0 - hs-source-dirs: test - main-is: Main.hs - build-depends: - , base - , filepath - , ghcide - , text - , hls-overloaded-record-dot-plugin - , hls-plugin-api - , lens - , lsp-test - , lsp-types - , row-types - , hls-test-utils - diff --git a/plugins/hls-overloaded-record-dot-plugin/src/Ide/Plugin/OverloadedRecordDot.hs b/plugins/hls-overloaded-record-dot-plugin/src/Ide/Plugin/OverloadedRecordDot.hs index aa48e5ae10..8ead286b67 100644 --- a/plugins/hls-overloaded-record-dot-plugin/src/Ide/Plugin/OverloadedRecordDot.hs +++ b/plugins/hls-overloaded-record-dot-plugin/src/Ide/Plugin/OverloadedRecordDot.hs @@ -36,24 +36,15 @@ import Development.IDE.Core.RuleTypes (TcModuleResult (..), import Development.IDE.Core.Shake (define, useWithStale) import qualified Development.IDE.Core.Shake as Shake -#if __GLASGOW_HASKELL__ >= 903 -import Development.IDE.GHC.Compat (HsExpr (HsRecSel)) -#else -import Development.IDE.GHC.Compat (HsExpr (HsRecFld)) -#endif - import Control.DeepSeq (rwhnf) import Development.IDE.Core.PluginUtils import Development.IDE.Core.PositionMapping (PositionMapping, toCurrentRange) import Development.IDE.GHC.Compat (Extension (OverloadedRecordDot), - GhcPass, - HsExpansion (HsExpanded), - HsExpr (HsApp, HsVar, OpApp, XExpr), - LHsExpr, Outputable, - Pass (..), appPrec, - dollarName, getLoc, - hs_valds, + GhcPass, HsExpr (..), + LHsExpr, Pass (..), + appPrec, dollarName, + getLoc, hs_valds, parenthesizeHsExpr, pattern RealSrcSpan, unLoc) @@ -88,6 +79,14 @@ import Language.LSP.Protocol.Types (CodeAction (..), WorkspaceEdit (WorkspaceEdit, _changeAnnotations, _changes, _documentChanges), type (|?) (..)) + +#if __GLASGOW_HASKELL__ < 910 +import Development.IDE.GHC.Compat (HsExpansion (HsExpanded)) +#else +import Development.IDE.GHC.Compat (XXExprGhcRn (..)) +#endif + + data Log = LogShake Shake.Log | LogCollectedRecordSelectors [RecordSelectorExpr] @@ -160,7 +159,7 @@ descriptor :: Recorder (WithPriority Log) -> PluginId descriptor recorder plId = let resolveRecorder = cmapWithPrio LogResolve recorder pluginHandler = mkCodeActionHandlerWithResolve resolveRecorder codeActionProvider resolveProvider - in (defaultPluginDescriptor plId) + in (defaultPluginDescriptor plId "Provides a code action to convert record selector usage to use overloaded record dot syntax") { pluginHandlers = pluginHandler , pluginRules = collectRecSelsRule recorder } @@ -247,8 +246,11 @@ collectRecSelsRule recorder = define (cmapWithPrio LogShake recorder) $ where getEnabledExtensions :: TcModuleResult -> [Extension] getEnabledExtensions = getExtensions . tmrParsed getRecordSelectors :: TcModuleResult -> [RecordSelectorExpr] - getRecordSelectors (tmrRenamed -> (hs_valds -> valBinds,_,_,_)) = - collectRecordSelectors valBinds +#if __GLASGOW_HASKELL__ >= 910 + getRecordSelectors (tmrRenamed -> (hs_valds -> valBinds,_,_,_,_)) = collectRecordSelectors valBinds +#else + getRecordSelectors (tmrRenamed -> (hs_valds -> valBinds,_,_,_)) = collectRecordSelectors valBinds +#endif rewriteRange :: PositionMapping -> RecordSelectorExpr -> Maybe RecordSelectorExpr rewriteRange pm recSel = @@ -264,9 +266,7 @@ convertRecordSelectors RecordSelectorExpr{..} = -- |Converts a record selector expression into record dot syntax, currently we -- are using printOutputable to do it. We are also letting GHC decide when to -- parenthesize the record expression -convertRecSel :: Outputable (LHsExpr (GhcPass 'Renamed)) - => LHsExpr (GhcPass 'Renamed) - -> LHsExpr (GhcPass 'Renamed) -> Text +convertRecSel :: LHsExpr (GhcPass 'Renamed) -> LHsExpr (GhcPass 'Renamed) -> Text convertRecSel se re = printOutputable (parenthesizeHsExpr appPrec re) <> "." <> printOutputable se @@ -284,28 +284,30 @@ getRecSels :: LHsExpr (GhcPass 'Renamed) -> ([RecordSelectorExpr], Bool) -- branch. We do this here, by explicitly returning occurrences from traversing -- the original branch, and returning True, which keeps syb from implicitly -- continuing to traverse. +#if __GLASGOW_HASKELL__ >= 910 +getRecSels (unLoc -> XExpr (ExpandedThingRn a _)) = (collectRecordSelectors a, True) +#else getRecSels (unLoc -> XExpr (HsExpanded a _)) = (collectRecordSelectors a, True) -#if __GLASGOW_HASKELL__ >= 903 +#endif -- applied record selection: "selector record" or "selector (record)" or -- "selector selector2.record2" +#if __GLASGOW_HASKELL__ >= 911 +getRecSels e@(unLoc -> HsApp _ se@(unLoc -> XExpr (HsRecSelRn _)) re) = +#else getRecSels e@(unLoc -> HsApp _ se@(unLoc -> HsRecSel _ _) re) = +#endif ( [ RecordSelectorExpr (realSrcSpanToRange realSpan') se re | RealSrcSpan realSpan' _ <- [ getLoc e ] ], False ) -- Record selection where the field is being applied with the "$" operator: -- "selector $ record" -getRecSels e@(unLoc -> OpApp _ se@(unLoc -> HsRecSel _ _) - (unLoc -> HsVar _ (unLoc -> d)) re) | d == dollarName = - ( [ RecordSelectorExpr (realSrcSpanToRange realSpan') se re - | RealSrcSpan realSpan' _ <- [ getLoc e ] ], False ) +#if __GLASGOW_HASKELL__ >= 911 +getRecSels e@(unLoc -> OpApp _ se@(unLoc -> XExpr (HsRecSelRn _)) #else -getRecSels e@(unLoc -> HsApp _ se@(unLoc -> HsRecFld _ _) re) = - ( [ RecordSelectorExpr (realSrcSpanToRange realSpan') se re - | RealSrcSpan realSpan' _ <- [ getLoc e ] ], False ) -getRecSels e@(unLoc -> OpApp _ se@(unLoc -> HsRecFld _ _) +getRecSels e@(unLoc -> OpApp _ se@(unLoc -> HsRecSel _ _) +#endif (unLoc -> HsVar _ (unLoc -> d)) re) | d == dollarName = ( [ RecordSelectorExpr (realSrcSpanToRange realSpan') se re | RealSrcSpan realSpan' _ <- [ getLoc e ] ], False ) -#endif getRecSels _ = ([], False) collectRecSelResult :: MonadIO m => IdeState -> NormalizedFilePath diff --git a/plugins/hls-overloaded-record-dot-plugin/test/Main.hs b/plugins/hls-overloaded-record-dot-plugin/test/Main.hs index e896951b67..bcbdfe184d 100644 --- a/plugins/hls-overloaded-record-dot-plugin/test/Main.hs +++ b/plugins/hls-overloaded-record-dot-plugin/test/Main.hs @@ -1,7 +1,5 @@ {-# LANGUAGE DuplicateRecordFields #-} -{-# LANGUAGE NamedFieldPuns #-} {-# LANGUAGE OverloadedStrings #-} -{-# LANGUAGE TypeOperators #-} module Main ( main ) where @@ -75,4 +73,4 @@ isExplicitFieldsCodeAction selectorName CodeAction {_title} = ("Convert `" <> selectorName <> "` to record dot syntax") `T.isPrefixOf` _title testDataDir :: FilePath -testDataDir = "test" "testdata" +testDataDir = "plugins" "hls-overloaded-record-dot-plugin" "test" "testdata" diff --git a/plugins/hls-overloaded-record-dot-plugin/test/testdata/hie.yaml b/plugins/hls-overloaded-record-dot-plugin/test/testdata/hie.yaml new file mode 100644 index 0000000000..ee67c73150 --- /dev/null +++ b/plugins/hls-overloaded-record-dot-plugin/test/testdata/hie.yaml @@ -0,0 +1 @@ +cradle: {direct: {arguments: []}} diff --git a/plugins/hls-pragmas-plugin/LICENSE b/plugins/hls-pragmas-plugin/LICENSE deleted file mode 100644 index 261eeb9e9f..0000000000 --- a/plugins/hls-pragmas-plugin/LICENSE +++ /dev/null @@ -1,201 +0,0 @@ - Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ - - TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - - 1. Definitions. - - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. - - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. - - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. - - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. - - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. - - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. - - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). - - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. - - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution." - - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. - - 2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. - - 3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. - - 4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: - - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and - - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and - - (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. - - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. - - 5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. - - 6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. - - 7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. - - 8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. - - 9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. - - END OF TERMS AND CONDITIONS - - APPENDIX: How to apply the Apache License to your work. - - To apply the Apache License to your work, attach the following - boilerplate notice, with the fields enclosed by brackets "[]" - replaced with your own identifying information. (Don't include - the brackets!) The text should be enclosed in the appropriate - comment syntax for the file format. We also recommend that a - file or class name and description of purpose be included on the - same "printed page" as the copyright notice for easier - identification within third-party archives. - - Copyright [yyyy] [name of copyright owner] - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. diff --git a/plugins/hls-pragmas-plugin/hls-pragmas-plugin.cabal b/plugins/hls-pragmas-plugin/hls-pragmas-plugin.cabal deleted file mode 100644 index a7d383c754..0000000000 --- a/plugins/hls-pragmas-plugin/hls-pragmas-plugin.cabal +++ /dev/null @@ -1,57 +0,0 @@ -cabal-version: 2.4 -name: hls-pragmas-plugin -version: 2.4.0.0 -synopsis: Pragmas plugin for Haskell Language Server -description: - Please see the README on GitHub at - -license: Apache-2.0 -license-file: LICENSE -author: The Haskell IDE Team -copyright: The Haskell IDE Team -maintainer: alan.zimm@gmail.com -category: Development -build-type: Simple -extra-source-files: - LICENSE - test/testdata/*.hs - test/testdata/*.yaml - -source-repository head - type: git - location: https://github.com/haskell/haskell-language-server.git - -library - exposed-modules: Ide.Plugin.Pragmas - hs-source-dirs: src - build-depends: - , base >=4.12 && <5 - , extra - , fuzzy - , ghc - , ghcide == 2.4.0.0 - , hls-plugin-api == 2.4.0.0 - , lens - , lsp - , text - , transformers - , unordered-containers - , containers - ghc-options: -Wall -Wno-name-shadowing - default-language: Haskell2010 - -test-suite tests - type: exitcode-stdio-1.0 - default-language: Haskell2010 - hs-source-dirs: test - main-is: Main.hs - ghc-options: -threaded -rtsopts -with-rtsopts=-N - build-depends: - , aeson - , base - , filepath - , hls-pragmas-plugin - , hls-test-utils == 2.4.0.0 - , lens - , lsp-types - , text diff --git a/plugins/hls-pragmas-plugin/src/Ide/Plugin/Pragmas.hs b/plugins/hls-pragmas-plugin/src/Ide/Plugin/Pragmas.hs index 0d8404d788..23bfd727cf 100644 --- a/plugins/hls-pragmas-plugin/src/Ide/Plugin/Pragmas.hs +++ b/plugins/hls-pragmas-plugin/src/Ide/Plugin/Pragmas.hs @@ -2,9 +2,7 @@ {-# LANGUAGE DataKinds #-} {-# LANGUAGE DuplicateRecordFields #-} {-# LANGUAGE MultiWayIf #-} -{-# LANGUAGE NamedFieldPuns #-} {-# LANGUAGE OverloadedStrings #-} -{-# LANGUAGE TypeOperators #-} {-# LANGUAGE ViewPatterns #-} -- | Provides code actions to add missing pragmas (whenever GHC suggests to) @@ -17,45 +15,47 @@ module Ide.Plugin.Pragmas , AppearWhere(..) ) where -import Control.Lens hiding (List) -import Control.Monad.IO.Class (MonadIO (liftIO)) -import Control.Monad.Trans.Class (lift) -import Data.List.Extra (nubOrdOn) -import qualified Data.Map as M -import Data.Maybe (catMaybes) -import qualified Data.Text as T -import Development.IDE -import Development.IDE.Core.Compile (sourceParser, - sourceTypecheck) +import Control.Lens hiding (List) +import Control.Monad.IO.Class (MonadIO (liftIO)) +import qualified Data.Aeson as JSON +import Data.Char (isAlphaNum) +import qualified Data.Foldable as Foldable +import Data.List.Extra (nubOrdOn) +import qualified Data.Map as M +import Data.Maybe (mapMaybe) +import qualified Data.Text as T +import Development.IDE hiding (line) +import Development.IDE.Core.Compile (sourceParser, + sourceTypecheck) import Development.IDE.Core.PluginUtils import Development.IDE.GHC.Compat -import Development.IDE.Plugin.Completions (ghcideCompletionsPluginPriority) -import qualified Development.IDE.Spans.Pragmas as Pragmas +import Development.IDE.Plugin.Completions (ghcideCompletionsPluginPriority) +import Development.IDE.Plugin.Completions.Logic (getCompletionPrefixFromRope) +import Development.IDE.Plugin.Completions.Types (PosPrefixInfo (..)) +import qualified Development.IDE.Spans.Pragmas as Pragmas import Ide.Plugin.Error import Ide.Types -import qualified Language.LSP.Protocol.Lens as L -import qualified Language.LSP.Protocol.Message as LSP -import qualified Language.LSP.Protocol.Types as LSP -import qualified Language.LSP.Server as LSP -import qualified Language.LSP.VFS as VFS -import qualified Text.Fuzzy as Fuzzy +import qualified Language.LSP.Protocol.Lens as L +import qualified Language.LSP.Protocol.Message as LSP +import qualified Language.LSP.Protocol.Types as LSP +import qualified Text.Fuzzy as Fuzzy -- --------------------------------------------------------------------- suggestPragmaDescriptor :: PluginId -> PluginDescriptor IdeState -suggestPragmaDescriptor plId = (defaultPluginDescriptor plId) +suggestPragmaDescriptor plId = (defaultPluginDescriptor plId "Provides a code action to add missing LANGUAGE pragmas") { pluginHandlers = mkPluginHandler LSP.SMethod_TextDocumentCodeAction suggestPragmaProvider , pluginPriority = defaultPluginPriority + 1000 } completionDescriptor :: PluginId -> PluginDescriptor IdeState -completionDescriptor plId = (defaultPluginDescriptor plId) +completionDescriptor plId = (defaultPluginDescriptor plId "Provides completion of LANGAUGE pragmas") { pluginHandlers = mkPluginHandler LSP.SMethod_TextDocumentCompletion completion , pluginPriority = ghcideCompletionsPluginPriority + 1 } suggestDisableWarningDescriptor :: PluginId -> PluginDescriptor IdeState -suggestDisableWarningDescriptor plId = (defaultPluginDescriptor plId) +suggestDisableWarningDescriptor plId = (defaultPluginDescriptor plId "Provides a code action to disable warnings") { pluginHandlers = mkPluginHandler LSP.SMethod_TextDocumentCodeAction suggestDisableWarningProvider -- #3636 Suggestions to disable warnings should appear last. , pluginPriority = 0 @@ -81,11 +81,11 @@ mkCodeActionProvider mkSuggest state _plId -- ghc session to get some dynflags even if module isn't parsed (hscEnv -> hsc_dflags -> sessionDynFlags, _) <- runActionE "Pragmas.GhcSession" state $ useWithStaleE GhcSession normalizedFilePath - (_, fileContents) <- liftIO $ runAction "Pragmas.GetFileContents" state $ getFileContents normalizedFilePath + fileContents <- liftIO $ runAction "Pragmas.GetFileContents" state $ getFileContents normalizedFilePath parsedModule <- liftIO $ runAction "Pragmas.GetParsedModule" state $ getParsedModule normalizedFilePath let parsedModuleDynFlags = ms_hspp_opts . pm_mod_summary <$> parsedModule nextPragmaInfo = Pragmas.getNextPragmaInfo sessionDynFlags fileContents - pedits = (nubOrdOn snd . concat $ mkSuggest parsedModuleDynFlags <$> diags) + pedits = nubOrdOn snd $ concatMap (mkSuggest parsedModuleDynFlags) diags pure $ LSP.InL $ pragmaEditToAction uri nextPragmaInfo <$> pedits @@ -122,16 +122,23 @@ suggest dflags diag = -- --------------------------------------------------------------------- suggestDisableWarning :: Diagnostic -> [PragmaEdit] -suggestDisableWarning Diagnostic {_code} - | Just (LSP.InR (T.stripPrefix "-W" -> Just w)) <- _code - , w `notElem` warningBlacklist = - pure ("Disable \"" <> w <> "\" warnings", OptGHC w) +suggestDisableWarning diagnostic + | Just (Just (JSON.Array attachedReasons)) <- diagnostic ^? attachedReason + = + [ ("Disable \"" <> w <> "\" warnings", OptGHC w) + | JSON.String attachedReason <- Foldable.toList attachedReasons + , Just w <- [T.stripPrefix "-W" attachedReason] + , w `notElem` warningBlacklist + ] | otherwise = [] --- Don't suggest disabling type errors as a solution to all type errors warningBlacklist :: [T.Text] --- warningBlacklist = [] -warningBlacklist = ["deferred-type-errors"] +warningBlacklist = + -- Don't suggest disabling type errors as a solution to all type errors. + [ "deferred-type-errors" + -- Don't suggest disabling out of scope errors as a solution to all out of scope errors. + , "deferred-out-of-scope-variables" + ] -- --------------------------------------------------------------------- @@ -146,7 +153,7 @@ suggestAddPragma mDynflags Diagnostic {_message, _source} disabled | Just dynFlags <- mDynflags = -- GHC does not export 'OnOff', so we have to view it as string - catMaybes $ T.stripPrefix "Off " . printOutputable <$> extensions dynFlags + mapMaybe (T.stripPrefix "Off " . printOutputable) (extensions dynFlags) | otherwise = -- When the module failed to parse, we don't have access to its -- dynFlags. In that case, simply don't disable any pragmas. @@ -194,30 +201,32 @@ allPragmas = -- --------------------------------------------------------------------- flags :: [T.Text] -flags = map (T.pack . stripLeading '-') $ flagsForCompletion False +flags = map T.pack $ flagsForCompletion False completion :: PluginMethodHandler IdeState 'LSP.Method_TextDocumentCompletion -completion _ide _ complParams = do +completion ide _ complParams = do let (LSP.TextDocumentIdentifier uri) = complParams ^. L.textDocument - position = complParams ^. L.position - contents <- lift $ LSP.getVirtualFile $ toNormalizedUri uri - fmap (LSP.InL) $ case (contents, uriToFilePath' uri) of + position@(Position ln col) = complParams ^. L.position + contents <- liftIO $ runAction "Pragmas.GetUriContents" ide $ getUriContents $ toNormalizedUri uri + fmap LSP.InL $ case (contents, uriToFilePath' uri) of (Just cnts, Just _path) -> - result <$> VFS.getCompletionPrefix position cnts + pure $ result $ getCompletionPrefixFromRope position cnts where - result (Just pfix) + result pfix | "{-# language" `T.isPrefixOf` line - = map buildCompletion - (Fuzzy.simpleFilter (VFS.prefixText pfix) allPragmas) + = map mkLanguagePragmaCompl $ + Fuzzy.simpleFilter word allPragmas | "{-# options_ghc" `T.isPrefixOf` line - = map buildCompletion - (Fuzzy.simpleFilter (VFS.prefixText pfix) flags) + = let optionPrefix = getGhcOptionPrefix pfix + prefixLength = fromIntegral $ T.length optionPrefix + prefixRange = LSP.Range (Position ln (col - prefixLength)) position + in map (mkGhcOptionCompl prefixRange) $ Fuzzy.simpleFilter optionPrefix flags | "{-#" `T.isPrefixOf` line = [ mkPragmaCompl (a <> suffix) b c | (a, b, c, w) <- validPragmas, w == NewLine ] - | -- Do not suggest any pragmas any of these conditions: - -- 1. Current line is a an import + | -- Do not suggest any pragmas under any of these conditions: + -- 1. Current line is an import -- 2. There is a module name right before the current word. -- Something like `Text.la` shouldn't suggest adding the -- 'LANGUAGE' pragma. @@ -227,20 +236,21 @@ completion _ide _ complParams = do | otherwise = [ mkPragmaCompl (prefix <> pragmaTemplate <> suffix) matcher detail | (pragmaTemplate, matcher, detail, appearWhere) <- validPragmas - , -- Only suggest a pragma that needs its own line if the whole line - -- fuzzily matches the pragma - (appearWhere == NewLine && Fuzzy.test line matcher ) || - -- Only suggest a pragma that appears in the middle of a line when - -- the current word is not the only thing in the line and the - -- current word fuzzily matches the pragma - (appearWhere == CanInline && line /= word && Fuzzy.test word matcher) + , case appearWhere of + -- Only suggest a pragma that needs its own line if the whole line + -- fuzzily matches the pragma + NewLine -> Fuzzy.test line matcher + -- Only suggest a pragma that appears in the middle of a line when + -- the current word is not the only thing in the line and the + -- current word fuzzily matches the pragma + CanInline -> line /= word && Fuzzy.test word matcher ] where - line = T.toLower $ VFS.fullLine pfix - module_ = VFS.prefixModule pfix - word = VFS.prefixText pfix - -- Not completely correct, may fail if more than one "{-#" exist - -- , we can ignore it since it rarely happen. + line = T.toLower $ fullLine pfix + module_ = prefixScope pfix + word = prefixText pfix + -- Not completely correct, may fail if more than one "{-#" exists. + -- We can ignore it since it rarely happens. prefix | "{-# " `T.isInfixOf` line = "" | "{-#" `T.isInfixOf` line = " " @@ -251,8 +261,7 @@ completion _ide _ complParams = do | "-}" `T.isSuffixOf` line = " #" | "}" `T.isSuffixOf` line = " #-" | otherwise = " #-}" - result Nothing = [] - _ -> return $ [] + _ -> return [] ----------------------------------------------------------------------- @@ -295,19 +304,32 @@ mkPragmaCompl insertText label detail = Nothing Nothing Nothing Nothing Nothing (Just insertText) (Just LSP.InsertTextFormat_Snippet) Nothing Nothing Nothing Nothing Nothing Nothing Nothing - -stripLeading :: Char -> String -> String -stripLeading _ [] = [] -stripLeading c (s:ss) - | s == c = ss - | otherwise = s:ss - - -buildCompletion :: T.Text -> LSP.CompletionItem -buildCompletion label = +mkLanguagePragmaCompl :: T.Text -> LSP.CompletionItem +mkLanguagePragmaCompl label = LSP.CompletionItem label Nothing (Just LSP.CompletionItemKind_Keyword) Nothing Nothing Nothing Nothing Nothing Nothing Nothing Nothing Nothing Nothing Nothing Nothing Nothing Nothing Nothing Nothing +mkGhcOptionCompl :: Range -> T.Text -> LSP.CompletionItem +mkGhcOptionCompl editRange completedFlag = + LSP.CompletionItem completedFlag Nothing (Just LSP.CompletionItemKind_Keyword) Nothing Nothing + Nothing Nothing Nothing Nothing Nothing Nothing Nothing + Nothing (Just insertCompleteFlag) Nothing Nothing Nothing Nothing Nothing + where + insertCompleteFlag = LSP.InL $ LSP.TextEdit editRange completedFlag + +-- The prefix extraction logic of getCompletionPrefix +-- doesn't consider '-' part of prefix which breaks completion +-- of flags like "-ddump-xyz". For OPTIONS_GHC completion we need the whole thing +-- to be considered completion prefix, but `prefixText posPrefixInfo` would return"xyz" in this case +getGhcOptionPrefix :: PosPrefixInfo -> T.Text +getGhcOptionPrefix PosPrefixInfo {cursorPos = Position _ col, fullLine}= + T.takeWhileEnd isGhcOptionChar beforePos + where + beforePos = T.take (fromIntegral col) fullLine - + -- Is this character contained in some GHC flag? Based on: + -- >>> nub . sort . concat $ GHC.Driver.Session.flagsForCompletion False + -- "#-.01234589=ABCDEFGHIJKLMNOPQRSTUVWX_abcdefghijklmnopqrstuvwxyz" + isGhcOptionChar :: Char -> Bool + isGhcOptionChar c = isAlphaNum c || c `elem` ("#-.=_" :: String) diff --git a/plugins/hls-pragmas-plugin/test/Main.hs b/plugins/hls-pragmas-plugin/test/Main.hs index 8eab91a91e..1e38e439ab 100644 --- a/plugins/hls-pragmas-plugin/test/Main.hs +++ b/plugins/hls-pragmas-plugin/test/Main.hs @@ -1,5 +1,5 @@ {-# LANGUAGE OverloadedStrings #-} -{-# OPTIONS_GHC -Wno-incomplete-patterns #-} + module Main ( main ) where @@ -12,7 +12,6 @@ import Ide.Plugin.Pragmas import qualified Language.LSP.Protocol.Lens as L import System.FilePath import Test.Hls -import Test.Hls.Util (onlyWorkForGhcVersions) main :: IO () main = defaultTestRunner tests @@ -74,15 +73,12 @@ codeActionTests = , codeActionTestWithPragmasSuggest "adds TypeApplications pragma" "TypeApplications" [("Add \"TypeApplications\"", "Contains TypeApplications code action")] , codeActionTestWithPragmasSuggest "after shebang" "AfterShebang" [("Add \"NamedFieldPuns\"", "Contains NamedFieldPuns code action")] , codeActionTestWithPragmasSuggest "append to existing pragmas" "AppendToExisting" [("Add \"NamedFieldPuns\"", "Contains NamedFieldPuns code action")] - , codeActionTestWithPragmasSuggest "before doc comments" "BeforeDocComment" [("Add \"NamedFieldPuns\"", "Contains NamedFieldPuns code action")] + , codeActionTestWithPragmasSuggest "before doc comments NamedFieldPuns" "BeforeDocComment" [("Add \"NamedFieldPuns\"", "Contains NamedFieldPuns code action")] , codeActionTestWithPragmasSuggest "adds TypeSynonymInstances pragma" "NeedsPragmas" [("Add \"TypeSynonymInstances\"", "Contains TypeSynonymInstances code action"), ("Add \"FlexibleInstances\"", "Contains FlexibleInstances code action")] - , codeActionTestWithDisableWarning "before doc comments" "MissingSignatures" [("Disable \"missing-signatures\" warnings", "Contains missing-signatures code action")] - , codeActionTestWithDisableWarning "before doc comments" "UnusedImports" [("Disable \"unused-imports\" warnings", "Contains unused-imports code action")] + , codeActionTestWithDisableWarning "before doc comments missing-signatures" "MissingSignatures" [("Disable \"missing-signatures\" warnings", "Contains missing-signatures code action")] + , codeActionTestWithDisableWarning "before doc comments unused-imports" "UnusedImports" [("Disable \"unused-imports\" warnings", "Contains unused-imports code action")] ] -ghc94regression :: String -ghc94regression = "to be reported" - codeActionTestWithPragmasSuggest :: String -> FilePath -> [(T.Text, String)] -> TestTree codeActionTestWithPragmasSuggest = codeActionTestWith pragmasSuggestPlugin @@ -105,8 +101,7 @@ codeActionTestWith descriptor testComment fp actions = codeActionTests' :: TestTree codeActionTests' = testGroup "additional code actions" - [ - goldenWithPragmas pragmasSuggestPlugin "no duplication" "NamedFieldPuns" $ \doc -> do + [ goldenWithPragmas pragmasSuggestPlugin "no duplication" "NamedFieldPuns" $ \doc -> do _ <- waitForDiagnosticsFrom doc cas <- map fromAction <$> getCodeActions doc (Range (Position 8 9) (Position 8 9)) ca <- liftIO $ case cas of @@ -114,28 +109,34 @@ codeActionTests' = _ -> assertFailure $ "Expected one code action, but got: " <> show cas liftIO $ (ca ^. L.title == "Add \"NamedFieldPuns\"") @? "NamedFieldPuns code action" executeCodeAction ca - , goldenWithPragmas pragmasSuggestPlugin "doesn't suggest disabling type errors" "DeferredTypeErrors" $ \doc -> do + , goldenWithPragmas pragmasDisableWarningPlugin "doesn't suggest disabling type errors" "DeferredTypeErrors" $ \doc -> do _ <- waitForDiagnosticsFrom doc cas <- map fromAction <$> getAllCodeActions doc liftIO $ "Disable \"deferred-type-errors\" warnings" `notElem` map (^. L.title) cas @? "Doesn't contain deferred-type-errors code action" liftIO $ length cas == 0 @? "Expected no code actions, but got: " <> show cas + , goldenWithPragmas pragmasDisableWarningPlugin "doesn't suggest disabling out of scope variables" "DeferredOutOfScopeVariables" $ \doc -> do + _ <- waitForDiagnosticsFrom doc + cas <- map fromAction <$> getAllCodeActions doc + liftIO $ "Disable \"deferred-out-of-scope-variables\" warnings" `notElem` map (^. L.title) cas @? "Doesn't contain deferred-out-of-scope-variables code action" + liftIO $ length cas == 0 @? "Expected no code actions, but got: " <> show cas ] completionTests :: TestTree completionTests = testGroup "completions" - [ completionTest "completes pragmas" "Completion.hs" "" "LANGUAGE" (Just InsertTextFormat_Snippet) (Just "LANGUAGE ${1:extension} #-}") (Just "{-# LANGUAGE #-}") [0, 4, 0, 34, 0, 4] - , completionTest "completes pragmas with existing closing pragma bracket" "Completion.hs" "" "LANGUAGE" (Just InsertTextFormat_Snippet) (Just "LANGUAGE ${1:extension}") (Just "{-# LANGUAGE #-}") [0, 4, 0, 31, 0, 4] - , completionTest "completes pragmas with existing closing comment bracket" "Completion.hs" "" "LANGUAGE" (Just InsertTextFormat_Snippet) (Just "LANGUAGE ${1:extension} #") (Just "{-# LANGUAGE #-}") [0, 4, 0, 32, 0, 4] - , completionTest "completes pragmas with existing closing bracket" "Completion.hs" "" "LANGUAGE" (Just InsertTextFormat_Snippet) (Just "LANGUAGE ${1:extension} #-") (Just "{-# LANGUAGE #-}") [0, 4, 0, 33, 0, 4] - , completionTest "completes options pragma" "Completion.hs" "OPTIONS" "OPTIONS_GHC" (Just InsertTextFormat_Snippet) (Just "OPTIONS_GHC -${1:option} #-}") (Just "{-# OPTIONS_GHC #-}") [0, 4, 0, 34, 0, 4] - , completionTest "completes ghc options pragma values" "Completion.hs" "{-# OPTIONS_GHC -Wno-red #-}\n" "Wno-redundant-constraints" Nothing Nothing Nothing [0, 0, 0, 0, 0, 24] - , completionTest "completes language extensions" "Completion.hs" "" "OverloadedStrings" Nothing Nothing Nothing [0, 24, 0, 31, 0, 24] - , completionTest "completes language extensions case insensitive" "Completion.hs" "lAnGuaGe Overloaded" "OverloadedStrings" Nothing Nothing Nothing [0, 4, 0, 34, 0, 24] - , completionTest "completes the Strict language extension" "Completion.hs" "Str" "Strict" Nothing Nothing Nothing [0, 13, 0, 31, 0, 16] - , completionTest "completes No- language extensions" "Completion.hs" "NoOverload" "NoOverloadedStrings" Nothing Nothing Nothing [0, 13, 0, 31, 0, 23] - , onlyWorkForGhcVersions (>=GHC92) "GHC2021 flag introduced since ghc9.2" $ - completionTest "completes GHC2021 extensions" "Completion.hs" "ghc" "GHC2021" Nothing Nothing Nothing [0, 13, 0, 31, 0, 16] + [ completionTest "completes pragmas" "Completion.hs" "" "LANGUAGE" (Just InsertTextFormat_Snippet) (Just "LANGUAGE ${1:extension} #-}") (Just "{-# LANGUAGE #-}") (0, 4, 0, 34, 0, 4) + , completionTest "completes pragmas with existing closing pragma bracket" "Completion.hs" "" "LANGUAGE" (Just InsertTextFormat_Snippet) (Just "LANGUAGE ${1:extension}") (Just "{-# LANGUAGE #-}") (0, 4, 0, 31, 0, 4) + , completionTest "completes pragmas with existing closing comment bracket" "Completion.hs" "" "LANGUAGE" (Just InsertTextFormat_Snippet) (Just "LANGUAGE ${1:extension} #") (Just "{-# LANGUAGE #-}") (0, 4, 0, 32, 0, 4) + , completionTest "completes pragmas with existing closing bracket" "Completion.hs" "" "LANGUAGE" (Just InsertTextFormat_Snippet) (Just "LANGUAGE ${1:extension} #-") (Just "{-# LANGUAGE #-}") (0, 4, 0, 33, 0, 4) + , completionTest "completes options pragma" "Completion.hs" "OPTIONS" "OPTIONS_GHC" (Just InsertTextFormat_Snippet) (Just "OPTIONS_GHC -${1:option} #-}") (Just "{-# OPTIONS_GHC #-}") (0, 4, 0, 34, 0, 4) + , completionTest "completes ghc options pragma values" "Completion.hs" "{-# OPTIONS_GHC -Wno-red #-}\n" "-Wno-redundant-constraints" Nothing Nothing Nothing (0, 0, 0, 0, 0, 24) + , completionTest "completes ghc options pragma values with multiple dashes" "Completion.hs" "{-# OPTIONS_GHC -fmax-worker-ar #-}\n" "-fmax-worker-args" Nothing Nothing Nothing (0, 0, 0, 0, 0, 31) + , completionTest "completes multiple ghc options within single pragma" "Completion.hs" "{-# OPTIONS_GHC -ddump-simpl -ddump-spl #-}\n" "-ddump-splices" Nothing Nothing Nothing (0, 0, 0, 0, 0, 39) + , completionTest "completes language extensions" "Completion.hs" "" "OverloadedStrings" Nothing Nothing Nothing (0, 24, 0, 31, 0, 24) + , completionTest "completes language extensions case insensitive" "Completion.hs" "lAnGuaGe Overloaded" "OverloadedStrings" Nothing Nothing Nothing (0, 4, 0, 34, 0, 24) + , completionTest "completes the Strict language extension" "Completion.hs" "Str" "Strict" Nothing Nothing Nothing (0, 13, 0, 31, 0, 16) + , completionTest "completes No- language extensions" "Completion.hs" "NoOverload" "NoOverloadedStrings" Nothing Nothing Nothing (0, 13, 0, 31, 0, 23) + , completionTest "completes GHC2021 extensions" "Completion.hs" "ghc" "GHC2021" Nothing Nothing Nothing (0, 13, 0, 31, 0, 16) ] completionSnippetTests :: TestTree @@ -151,7 +152,7 @@ completionSnippetTests = in completionTest (T.unpack label) "Completion.hs" input label (Just InsertTextFormat_Snippet) (Just $ "{-# " <> insertText <> " #-}") (Just detail) - [0, 0, 0, 34, 0, fromIntegral $ T.length input]) + (0, 0, 0, 34, 0, fromIntegral $ T.length input)) dontSuggestCompletionTests :: TestTree dontSuggestCompletionTests = @@ -162,7 +163,7 @@ dontSuggestCompletionTests = , provideNoCompletionsTest "when no word has been typed" "Completion.hs" Nothing (Position 3 0) , provideNoCompletionsTest "when expecting auto complete on modules" "Completion.hs" (Just $ mkEdit (8,6) (8,8) "Data.Maybe.WA") (Position 8 19) ] - individualPragmaTests = validPragmas <&> \(insertText,label,detail,appearWhere) -> + individualPragmaTests = validPragmas <&> \(_insertText,label,_detail,appearWhere) -> let completionPrompt = T.toLower $ T.init label promptLen = fromIntegral (T.length completionPrompt) in case appearWhere of @@ -176,8 +177,8 @@ mkEdit :: (UInt,UInt) -> (UInt,UInt) -> T.Text -> TextEdit mkEdit (startLine, startCol) (endLine, endCol) newText = TextEdit (Range (Position startLine startCol) (Position endLine endCol)) newText -completionTest :: String -> FilePath -> T.Text -> T.Text -> Maybe InsertTextFormat -> Maybe T.Text -> Maybe T.Text -> [UInt] -> TestTree -completionTest testComment fileName replacementText expectedLabel expectedFormat expectedInsertText detail [delFromLine, delFromCol, delToLine, delToCol, completeAtLine, completeAtCol] = +completionTest :: String -> FilePath -> T.Text -> T.Text -> Maybe InsertTextFormat -> Maybe T.Text -> Maybe T.Text -> (UInt, UInt, UInt, UInt, UInt, UInt) -> TestTree +completionTest testComment fileName replacementText expectedLabel expectedFormat expectedInsertText detail (delFromLine, delFromCol, delToLine, delToCol, completeAtLine, completeAtCol) = testCase testComment $ runSessionWithServer def pragmasCompletionPlugin testDataDir $ do doc <- openDoc fileName "haskell" _ <- waitForDiagnostics @@ -220,4 +221,4 @@ goldenWithPragmas :: PluginTestDescriptor () -> TestName -> FilePath -> (TextDoc goldenWithPragmas descriptor title path = goldenWithHaskellDoc def descriptor title testDataDir path "expected" "hs" testDataDir :: FilePath -testDataDir = "test" "testdata" +testDataDir = "plugins" "hls-pragmas-plugin" "test" "testdata" diff --git a/plugins/hls-pragmas-plugin/test/testdata/DeferredOutOfScopeVariables.expected.hs b/plugins/hls-pragmas-plugin/test/testdata/DeferredOutOfScopeVariables.expected.hs new file mode 100644 index 0000000000..38d17261dc --- /dev/null +++ b/plugins/hls-pragmas-plugin/test/testdata/DeferredOutOfScopeVariables.expected.hs @@ -0,0 +1,5 @@ +module DeferredOutOfScopeVariables where + +f :: () +f = let x = Doesn'tExist + in undefined diff --git a/plugins/hls-pragmas-plugin/test/testdata/DeferredOutOfScopeVariables.hs b/plugins/hls-pragmas-plugin/test/testdata/DeferredOutOfScopeVariables.hs new file mode 100644 index 0000000000..38d17261dc --- /dev/null +++ b/plugins/hls-pragmas-plugin/test/testdata/DeferredOutOfScopeVariables.hs @@ -0,0 +1,5 @@ +module DeferredOutOfScopeVariables where + +f :: () +f = let x = Doesn'tExist + in undefined diff --git a/plugins/hls-qualify-imported-names-plugin/LICENSE b/plugins/hls-qualify-imported-names-plugin/LICENSE deleted file mode 100644 index 261eeb9e9f..0000000000 --- a/plugins/hls-qualify-imported-names-plugin/LICENSE +++ /dev/null @@ -1,201 +0,0 @@ - Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ - - TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - - 1. Definitions. - - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. - - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. - - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. - - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. - - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. - - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. - - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). - - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. - - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution." - - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. - - 2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. - - 3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. - - 4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: - - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and - - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and - - (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. - - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. - - 5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. - - 6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. - - 7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. - - 8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. - - 9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. - - END OF TERMS AND CONDITIONS - - APPENDIX: How to apply the Apache License to your work. - - To apply the Apache License to your work, attach the following - boilerplate notice, with the fields enclosed by brackets "[]" - replaced with your own identifying information. (Don't include - the brackets!) The text should be enclosed in the appropriate - comment syntax for the file format. We also recommend that a - file or class name and description of purpose be included on the - same "printed page" as the copyright notice for easier - identification within third-party archives. - - Copyright [yyyy] [name of copyright owner] - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. diff --git a/plugins/hls-qualify-imported-names-plugin/hls-qualify-imported-names-plugin.cabal b/plugins/hls-qualify-imported-names-plugin/hls-qualify-imported-names-plugin.cabal deleted file mode 100644 index d2c7443452..0000000000 --- a/plugins/hls-qualify-imported-names-plugin/hls-qualify-imported-names-plugin.cabal +++ /dev/null @@ -1,59 +0,0 @@ -cabal-version: 2.2 -name: hls-qualify-imported-names-plugin -version: 2.4.0.0 -synopsis: A Haskell Language Server plugin that qualifies imported names -description: - Please see the README on GitHub at -license: Apache-2.0 -license-file: LICENSE -author: Jonathan Shen -maintainer: shenjonathan0@gmail.com -category: Development -build-type: Simple -extra-source-files: - LICENSE - qualify-imported-names-demo.gif - README.md - test/data/*.hs - test/data/*.yaml - -source-repository head - type: git - location: https://github.com/haskell/haskell-language-server.git - -library - exposed-modules: Ide.Plugin.QualifyImportedNames - hs-source-dirs: src - build-depends: - , aeson - , base >=4.12 && <5 - , containers - , deepseq - , ghc - , ghcide == 2.4.0.0 - , hls-graph - , hls-plugin-api == 2.4.0.0 - , lens - , lsp - , text - , unordered-containers - , dlist - , transformers - - default-language: Haskell2010 - default-extensions: - DataKinds - TypeOperators - -test-suite tests - type: exitcode-stdio-1.0 - default-language: Haskell2010 - hs-source-dirs: test - main-is: Main.hs - ghc-options: -threaded -rtsopts -with-rtsopts=-N - build-depends: - , base - , text - , filepath - , hls-qualify-imported-names-plugin - , hls-test-utils == 2.4.0.0 diff --git a/plugins/hls-qualify-imported-names-plugin/src/Ide/Plugin/QualifyImportedNames.hs b/plugins/hls-qualify-imported-names-plugin/src/Ide/Plugin/QualifyImportedNames.hs index 55692825b2..011910b880 100644 --- a/plugins/hls-qualify-imported-names-plugin/src/Ide/Plugin/QualifyImportedNames.hs +++ b/plugins/hls-qualify-imported-names-plugin/src/Ide/Plugin/QualifyImportedNames.hs @@ -1,5 +1,5 @@ +{-# LANGUAGE CPP #-} {-# LANGUAGE MultiWayIf #-} -{-# LANGUAGE NamedFieldPuns #-} {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE PatternSynonyms #-} {-# LANGUAGE RecordWildCards #-} @@ -9,19 +9,21 @@ module Ide.Plugin.QualifyImportedNames (descriptor) where import Control.Lens ((^.)) import Control.Monad (foldM) -import Control.Monad.IO.Class (MonadIO (liftIO)) import Control.Monad.Trans.State.Strict (State) import qualified Control.Monad.Trans.State.Strict as State import Data.DList (DList) import qualified Data.DList as DList -import Data.Foldable (Foldable (foldl'), find) -import qualified Data.HashMap.Strict as HashMap +import Data.Foldable (find) import Data.List (sortOn) import qualified Data.List as List import qualified Data.Map.Strict as Map import Data.Maybe (fromMaybe, isJust, mapMaybe) +import qualified Data.Set as Set import Data.Text (Text) import qualified Data.Text as Text +import qualified Data.Text.Lines as Text.Lines +import Data.Text.Utf16.Rope.Mixed (Rope) +import qualified Data.Text.Utf16.Rope.Mixed as Rope import Development.IDE (spanContainsRange) import Development.IDE.Core.PluginUtils import Development.IDE.Core.RuleTypes (GetFileContents (GetFileContents), @@ -29,8 +31,7 @@ import Development.IDE.Core.RuleTypes (GetFileContents (GetFileConte HieAstResult (HAR, refMap), TcModuleResult (TcModuleResult, tmrParsed, tmrTypechecked), TypeCheck (TypeCheck)) -import Development.IDE.Core.Service (runAction) -import Development.IDE.Core.Shake (IdeState, use) +import Development.IDE.Core.Shake (IdeState) import Development.IDE.GHC.Compat (ContextInfo (Use), GenLocated (..), GhcPs, GlobalRdrElt, GlobalRdrEnv, @@ -40,9 +41,8 @@ import Development.IDE.GHC.Compat (ContextInfo (Use), ImpDeclSpec (ImpDeclSpec, is_as, is_dloc, is_qual), ImportSpec (ImpSpec), LImportDecl, ModuleName, - Name, NameEnv, OccName, - ParsedModule, RefMap, Span, - SrcSpan, + Name, NameEnv, ParsedModule, + RefMap, Span, SrcSpan, TcGblEnv (tcg_rdr_env), emptyUFM, globalRdrEnvElts, gre_imp, gre_name, locA, @@ -56,14 +56,11 @@ import Development.IDE.GHC.Compat (ContextInfo (Use), srcSpanEndLine, srcSpanStartCol, srcSpanStartLine, unitUFM) -import Development.IDE.GHC.Error (isInsideSrcSpan) -import Development.IDE.Types.Location (NormalizedFilePath, - Position (Position), - Range (Range), Uri, - toNormalizedUri) +import Development.IDE.Types.Location (Position (Position), + Range (Range), Uri) import Ide.Plugin.Error (PluginError (PluginRuleFailed), getNormalizedFilePathE, - handleMaybe, handleMaybeM) + handleMaybe) import Ide.Types (PluginDescriptor (pluginHandlers), PluginId, PluginMethodHandler, @@ -75,11 +72,13 @@ import Language.LSP.Protocol.Message (Method (Method_TextDocumentCo import Language.LSP.Protocol.Types (CodeAction (CodeAction, _command, _data_, _diagnostics, _disabled, _edit, _isPreferred, _kind, _title), CodeActionKind (CodeActionKind_QuickFix), CodeActionParams (CodeActionParams), - TextDocumentIdentifier (TextDocumentIdentifier), TextEdit (TextEdit), WorkspaceEdit (WorkspaceEdit, _changeAnnotations, _changes, _documentChanges), - type (|?) (InL, InR), - uriToNormalizedFilePath) + type (|?) (InL, InR)) + +#if !MIN_VERSION_base(4,20,0) +import Data.Foldable (foldl') +#endif thenCmp :: Ordering -> Ordering -> Ordering {-# INLINE thenCmp #-} @@ -87,7 +86,7 @@ thenCmp EQ ordering = ordering thenCmp ordering _ = ordering descriptor :: PluginId -> PluginDescriptor IdeState -descriptor pluginId = (defaultPluginDescriptor pluginId) { +descriptor pluginId = (defaultPluginDescriptor pluginId "Provides a code action to qualify imported names") { pluginHandlers = mconcat [ mkPluginHandler SMethod_TextDocumentCodeAction codeActionProvider ] @@ -120,7 +119,7 @@ data ImportedBy = ImportedBy { } isRangeWithinImportedBy :: Range -> ImportedBy -> Bool -isRangeWithinImportedBy range (ImportedBy _ srcSpan) = fromMaybe False $ spanContainsRange srcSpan range +isRangeWithinImportedBy range ImportedBy{importedBySrcSpan} = fromMaybe False $ spanContainsRange importedBySrcSpan range globalRdrEnvToNameToImportedByMap :: GlobalRdrEnv -> NameEnv [ImportedBy] globalRdrEnvToNameToImportedByMap = @@ -174,30 +173,29 @@ refMapToUsedIdentifiers = DList.toList . Map.foldlWithKey' folder DList.empty getUsedIdentifier identifier span IdentifierDetails {..} | Just identifierSpan <- realSrcSpanToIdentifierSpan span , Right name <- identifier - , Use `elem` identInfo = Just $ UsedIdentifier name identifierSpan + , Use `Set.member` identInfo = Just $ UsedIdentifier name identifierSpan | otherwise = Nothing -occNameToText :: OccName -> Text -occNameToText = Text.pack . occNameString - updateColOffset :: Int -> Int -> Int -> Int updateColOffset row lineOffset colOffset | row == lineOffset = colOffset | otherwise = 0 -usedIdentifiersToTextEdits :: Range -> NameEnv [ImportedBy] -> Text -> [UsedIdentifier] -> [TextEdit] -usedIdentifiersToTextEdits range nameToImportedByMap sourceText usedIdentifiers +usedIdentifiersToTextEdits :: Range -> NameEnv [ImportedBy] -> Rope -> [UsedIdentifier] -> [TextEdit] +usedIdentifiersToTextEdits range nameToImportedByMap source usedIdentifiers | let sortedUsedIdentifiers = sortOn usedIdentifierSpan usedIdentifiers = - State.evalState (makeStateComputation sortedUsedIdentifiers) (Text.lines sourceText, 0, 0) + State.evalState + (makeStateComputation sortedUsedIdentifiers) + (Text.Lines.lines (Rope.toTextLines source), 0, 0) where folder :: [TextEdit] -> UsedIdentifier -> State ([Text], Int, Int) [TextEdit] - folder prevTextEdits (UsedIdentifier identifierName identifierSpan) - | Just importedBys <- lookupNameEnv nameToImportedByMap identifierName - , Just (ImportedBy alias _) <- find (isRangeWithinImportedBy range) importedBys - , let IdentifierSpan row startCol endCol = identifierSpan - , let identifierRange = identifierSpanToRange identifierSpan - , let aliasText = Text.pack $ moduleNameString alias - , let identifierText = Text.pack $ occNameString $ nameOccName identifierName + folder prevTextEdits UsedIdentifier{usedIdentifierName, usedIdentifierSpan} + | Just importedBys <- lookupNameEnv nameToImportedByMap usedIdentifierName + , Just ImportedBy{importedByAlias} <- find (isRangeWithinImportedBy range) importedBys + , let IdentifierSpan row startCol _ = usedIdentifierSpan + , let identifierRange = identifierSpanToRange usedIdentifierSpan + , let aliasText = Text.pack $ moduleNameString importedByAlias + , let identifierText = Text.pack $ occNameString $ nameOccName usedIdentifierName , let qualifiedIdentifierText = aliasText <> "." <> identifierText = do (sourceTextLines, lineOffset, updateColOffset row lineOffset -> colOffset) <- State.get let lines = List.drop (row - lineOffset) sourceTextLines @@ -228,18 +226,18 @@ usedIdentifiersToTextEdits range nameToImportedByMap sourceText usedIdentifiers -- 3. For each used name in refMap check whether the name comes from an import -- at the origin of the code action. codeActionProvider :: PluginMethodHandler IdeState Method_TextDocumentCodeAction -codeActionProvider ideState pluginId (CodeActionParams _ _ documentId range context) = do +codeActionProvider ideState _pluginId (CodeActionParams _ _ documentId range _) = do normalizedFilePath <- getNormalizedFilePathE (documentId ^. L.uri) TcModuleResult { tmrParsed, tmrTypechecked } <- runActionE "QualifyImportedNames.TypeCheck" ideState $ useE TypeCheck normalizedFilePath if isJust (findLImportDeclAt range tmrParsed) then do HAR {..} <- runActionE "QualifyImportedNames.GetHieAst" ideState (useE GetHieAst normalizedFilePath) - (_, sourceTextM) <- runActionE "QualifyImportedNames.GetFileContents" ideState (useE GetFileContents normalizedFilePath) - sourceText <- handleMaybe (PluginRuleFailed "GetFileContents") sourceTextM + (_, sourceM) <- runActionE "QualifyImportedNames.GetFileContents" ideState (useE GetFileContents normalizedFilePath) + source <- handleMaybe (PluginRuleFailed "GetFileContents") sourceM let globalRdrEnv = tcg_rdr_env tmrTypechecked nameToImportedByMap = globalRdrEnvToNameToImportedByMap globalRdrEnv usedIdentifiers = refMapToUsedIdentifiers refMap - textEdits = usedIdentifiersToTextEdits range nameToImportedByMap sourceText usedIdentifiers + textEdits = usedIdentifiersToTextEdits range nameToImportedByMap source usedIdentifiers pure $ InL (makeCodeActions (documentId ^. L.uri) textEdits) else pure $ InL [] diff --git a/plugins/hls-qualify-imported-names-plugin/test/Main.hs b/plugins/hls-qualify-imported-names-plugin/test/Main.hs index 9ea46b210c..1d932be601 100644 --- a/plugins/hls-qualify-imported-names-plugin/test/Main.hs +++ b/plugins/hls-qualify-imported-names-plugin/test/Main.hs @@ -1,9 +1,6 @@ {-# LANGUAGE BlockArguments #-} -{-# LANGUAGE MultiWayIf #-} -{-# LANGUAGE NamedFieldPuns #-} {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE RecordWildCards #-} -{-# LANGUAGE TypeOperators #-} module Main (main) where @@ -12,9 +9,7 @@ import Data.Text (Text) import qualified Ide.Plugin.QualifyImportedNames as QualifyImportedNames import System.FilePath (()) import Test.Hls (CodeAction (CodeAction, _title), - Command (Command), IdeState, - MonadIO (liftIO), - PluginDescriptor, + Command, MonadIO (liftIO), PluginTestDescriptor, Position (Position), Range (Range), Session, @@ -26,10 +21,9 @@ import Test.Hls (CodeAction (CodeAction, _title getCodeActions, goldenWithHaskellDoc, mkPluginTestDescriptor', - openDoc, rename, - runSessionWithServer, + openDoc, runSessionWithServer, testCase, testGroup, - type (|?) (InR), (@?=)) + type (|?) (InR)) import Prelude @@ -39,13 +33,11 @@ data Point = Point { column :: !Int } +makePoint :: Int -> Int -> Point makePoint line column | line >= 1 && column >= 1 = Point line column | otherwise = error "Line or column is less than 1." -isNotEmpty :: Foldable f => f a -> Bool -isNotEmpty = not . isEmpty - isEmpty :: Foldable f => f a -> Bool isEmpty = null @@ -127,7 +119,7 @@ codeActionGoldenTest testCaseName goldenFilename point = _ -> liftIO $ assertFailure $ makeCodeActionNotFoundAtString point testDataDir :: String -testDataDir = "test" "data" +testDataDir = "plugins" "hls-qualify-imported-names-plugin" "test" "data" pluginDescriptor :: PluginTestDescriptor () pluginDescriptor = mkPluginTestDescriptor' QualifyImportedNames.descriptor "qualifyImportedNames" diff --git a/plugins/hls-refactor-plugin/LICENSE b/plugins/hls-refactor-plugin/LICENSE deleted file mode 100644 index 261eeb9e9f..0000000000 --- a/plugins/hls-refactor-plugin/LICENSE +++ /dev/null @@ -1,201 +0,0 @@ - Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ - - TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - - 1. Definitions. - - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. - - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. - - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. - - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. - - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. - - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. - - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). - - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. - - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution." - - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. - - 2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. - - 3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. - - 4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: - - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and - - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and - - (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. - - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. - - 5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. - - 6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. - - 7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. - - 8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. - - 9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. - - END OF TERMS AND CONDITIONS - - APPENDIX: How to apply the Apache License to your work. - - To apply the Apache License to your work, attach the following - boilerplate notice, with the fields enclosed by brackets "[]" - replaced with your own identifying information. (Don't include - the brackets!) The text should be enclosed in the appropriate - comment syntax for the file format. We also recommend that a - file or class name and description of purpose be included on the - same "printed page" as the copyright notice for easier - identification within third-party archives. - - Copyright [yyyy] [name of copyright owner] - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. diff --git a/plugins/hls-refactor-plugin/hls-refactor-plugin.cabal b/plugins/hls-refactor-plugin/hls-refactor-plugin.cabal deleted file mode 100644 index d1fccf1eb3..0000000000 --- a/plugins/hls-refactor-plugin/hls-refactor-plugin.cabal +++ /dev/null @@ -1,140 +0,0 @@ -cabal-version: 3.0 -name: hls-refactor-plugin -version: 2.4.0.0 -synopsis: Exactprint refactorings for Haskell Language Server -description: - Please see the README on GitHub at - -license: Apache-2.0 -license-file: LICENSE -author: The Haskell IDE Team -copyright: The Haskell IDE Team -maintainer: zubin.duggal@gmail.com -category: Development -build-type: Simple -extra-source-files: - LICENSE - test/data/**/*.hs - test/data/**/*.yaml - -source-repository head - type: git - location: https://github.com/haskell/haskell-language-server.git - -library - -- Plugins that need exactprint have not been updated for 9.8 yet - if impl(ghc >= 9.8) - buildable: False - else - buildable: True - exposed-modules: Development.IDE.GHC.ExactPrint - Development.IDE.GHC.Compat.ExactPrint - Development.IDE.Plugin.CodeAction - Development.IDE.Plugin.CodeAction.Util - Development.IDE.GHC.Dump - other-modules: Development.IDE.Plugin.CodeAction.Args - Development.IDE.Plugin.CodeAction.ExactPrint - Development.IDE.Plugin.CodeAction.PositionIndexed - Development.IDE.Plugin.Plugins.AddArgument - Development.IDE.Plugin.Plugins.Diagnostic - Development.IDE.Plugin.Plugins.FillHole - Development.IDE.Plugin.Plugins.FillTypeWildcard - Development.IDE.Plugin.Plugins.ImportUtils - default-extensions: - BangPatterns - CPP - DataKinds - DeriveGeneric - DerivingStrategies - DerivingVia - DuplicateRecordFields - ExplicitNamespaces - FlexibleContexts - FlexibleInstances - FunctionalDependencies - GeneralizedNewtypeDeriving - LambdaCase - NamedFieldPuns - OverloadedStrings - PatternSynonyms - RankNTypes - RecordWildCards - ScopedTypeVariables - TupleSections - TypeApplications - TypeOperators - ViewPatterns - hs-source-dirs: src - build-depends: - , aeson - , base >=4.12 && <5 - , ghc - , bytestring - , ghc-boot - , regex-tdfa - , text-rope - , ghcide == 2.4.0.0 - , hls-plugin-api == 2.4.0.0 - , lsp - , text - , transformers - , unordered-containers - , containers - , ghc-exactprint < 1 || >= 1.4 - , extra - , retrie - , syb - , hls-graph - , dlist - , deepseq - , mtl - , lens - , data-default - , time - -- FIXME: Only needed to workaround for qualified imports in GHC 9.4 - , regex-applicative - , parser-combinators - ghc-options: -Wall -Wno-name-shadowing - default-language: Haskell2010 - -test-suite tests - if impl(ghc >= 9.8) - buildable: False - else - buildable: True - type: exitcode-stdio-1.0 - default-language: Haskell2010 - hs-source-dirs: test - main-is: Main.hs - other-modules: Test.AddArgument - ghc-options: -O0 -threaded -rtsopts -with-rtsopts=-N -Wunused-imports - build-depends: - , base - , filepath - , hls-refactor-plugin - , hls-test-utils == 2.4.0.0 - , lens - , lsp-types - , text - , aeson - , hls-plugin-api - , parser-combinators - , data-default - , extra - , text-rope - , containers - -- ghc is included to enable the MIN_VERSION_ghc macro - , ghc - , ghcide - , ghcide-test-utils - , shake - , hls-plugin-api - , lsp-test - , network-uri - , directory - , async - , regex-tdfa - , tasty-rerun - , tasty-hunit - , tasty-expected-failure - , tasty diff --git a/plugins/hls-refactor-plugin/src/Development/IDE/GHC/Compat/ExactPrint.hs b/plugins/hls-refactor-plugin/src/Development/IDE/GHC/Compat/ExactPrint.hs index 453e5477ad..7c337dcd00 100644 --- a/plugins/hls-refactor-plugin/src/Development/IDE/GHC/Compat/ExactPrint.hs +++ b/plugins/hls-refactor-plugin/src/Development/IDE/GHC/Compat/ExactPrint.hs @@ -2,16 +2,18 @@ -- multiple ghc-exactprint versions, accepting that anything more ambitious is -- pretty much impossible with the GHC 9.2 redesign of ghc-exactprint module Development.IDE.GHC.Compat.ExactPrint - ( ExactPrint - , exactPrint - , makeDeltaAst - , Retrie.Annotated, pattern Annotated, astA, annsA + ( module ExactPrint + , printA + , transformA ) where -import Development.IDE.GHC.Compat.Parser -import Language.Haskell.GHC.ExactPrint as Retrie -import qualified Retrie.ExactPrint as Retrie +import Language.Haskell.GHC.ExactPrint as ExactPrint +printA :: (ExactPrint ast) => ast -> String +printA ast = exactPrint ast -pattern Annotated :: ast -> ApiAnns -> Retrie.Annotated ast -pattern Annotated {astA, annsA} <- ((,()) . Retrie.astA -> (astA, annsA)) +transformA + :: Monad m => ast1 -> (ast1 -> TransformT m ast2) -> m ast2 +transformA ast f = do + (ast',_ ,_) <- runTransformFromT 0 (f ast) + return ast' diff --git a/plugins/hls-refactor-plugin/src/Development/IDE/GHC/Dump.hs b/plugins/hls-refactor-plugin/src/Development/IDE/GHC/Dump.hs index 1d74197445..638d14c51d 100644 --- a/plugins/hls-refactor-plugin/src/Development/IDE/GHC/Dump.hs +++ b/plugins/hls-refactor-plugin/src/Development/IDE/GHC/Dump.hs @@ -4,7 +4,7 @@ import qualified Data.ByteString as B import Data.Data hiding (Fixity) import Development.IDE.GHC.Compat hiding (LocatedA, NameAnn) -import Development.IDE.GHC.Compat.ExactPrint +import Development.IDE.GHC.Compat.ExactPrint (ExactPrint, exactPrint) import Development.IDE.GHC.Compat.Util import Generics.SYB (ext1Q, ext2Q, extQ) import GHC.Hs hiding (AnnLet) @@ -13,7 +13,7 @@ import GHC.Plugins hiding (AnnLet) import Prelude hiding ((<>)) -- | Show a GHC syntax tree in HTML. -showAstDataHtml :: (Data a, ExactPrint a, Outputable a) => a -> SDoc +showAstDataHtml :: (Data a, ExactPrint a) => a -> SDoc showAstDataHtml a0 = html $ header $$ body (tag' [("id",text (show @String "myUL"))] "ul" $ vcat @@ -32,9 +32,6 @@ showAstDataHtml a0 = html $ li = tag "li" caret x = tag' [("class", text "caret")] "span" "" <+> x nested foo cts -#if !MIN_VERSION_ghc(9,3,0) - | cts == empty = foo -#endif | otherwise = foo $$ (caret $ ul cts) body cts = tag "body" $ cts $$ tag "script" (text js) header = tag "head" $ tag "style" $ text css @@ -42,12 +39,16 @@ showAstDataHtml a0 = html $ pre = tag "pre" showAstDataHtml' :: Data a => a -> SDoc showAstDataHtml' = - (generic + generic `ext1Q` list `extQ` string `extQ` fastString `extQ` srcSpan `extQ` realSrcSpan +#if !MIN_VERSION_ghc(9,11,0) `extQ` annotation +#endif `extQ` annotationModule +#if !MIN_VERSION_ghc(9,11,0) `extQ` annotationAddEpAnn +#endif `extQ` annotationGrhsAnn `extQ` annotationEpAnnHsCase `extQ` annotationEpAnnHsLet @@ -56,12 +57,16 @@ showAstDataHtml a0 = html $ `extQ` annotationAnnParen `extQ` annotationTrailingAnn `extQ` annotationEpaLocation +#if !MIN_VERSION_ghc(9,11,0) `extQ` addEpAnn +#endif `extQ` lit `extQ` litr `extQ` litt `extQ` sourceText `extQ` deltaPos `extQ` epaAnchor +#if !MIN_VERSION_ghc(9,9,0) `extQ` anchorOp +#endif `extQ` bytestring `extQ` name `extQ` occName `extQ` moduleName `extQ` var `extQ` dataCon @@ -73,7 +78,6 @@ showAstDataHtml a0 = html $ `extQ` srcSpanAnnP `extQ` srcSpanAnnC `extQ` srcSpanAnnN - ) where generic :: Data a => a -> SDoc generic t = nested (text $ showConstr (toConstr t)) @@ -123,19 +127,32 @@ showAstDataHtml a0 = html $ sourceText :: SourceText -> SDoc sourceText NoSourceText = text "NoSourceText" + +#if MIN_VERSION_ghc(9,7,0) + sourceText (SourceText src) = text "SourceText" <+> ftext src +#else sourceText (SourceText src) = text "SourceText" <+> text src +#endif epaAnchor :: EpaLocation -> SDoc -#if MIN_VERSION_ghc(9,5,0) - epaAnchor (EpaSpan r _) = text "EpaSpan" <+> realSrcSpan r + +#if MIN_VERSION_ghc(9,9,0) + epaAnchor (EpaSpan s) = parens $ text "EpaSpan" <+> srcSpan s #else - epaAnchor (EpaSpan r) = text "EpaSpan" <+> realSrcSpan r + epaAnchor (EpaSpan r _) = text "EpaSpan" <+> realSrcSpan r #endif + +#if MIN_VERSION_ghc(9,11,0) + epaAnchor (EpaDelta s d cs) = text "EpaDelta" <+> srcSpan s <+> deltaPos d <+> showAstDataHtml' cs +#else epaAnchor (EpaDelta d cs) = text "EpaDelta" <+> deltaPos d <+> showAstDataHtml' cs +#endif +#if !MIN_VERSION_ghc(9,9,0) anchorOp :: AnchorOperation -> SDoc anchorOp UnchangedAnchor = "UnchangedAnchor" anchorOp (MovedAnchor dp) = "MovedAnchor " <> deltaPos dp +#endif deltaPos :: DeltaPos -> SDoc deltaPos (SameLine c) = text "SameLine" <+> ppr c @@ -153,18 +170,20 @@ showAstDataHtml a0 = html $ srcSpan :: SrcSpan -> SDoc srcSpan ss = char ' ' <> - (hang (ppr ss) 1 + hang (ppr ss) 1 -- TODO: show annotations here - (text "")) + (text "") realSrcSpan :: RealSrcSpan -> SDoc realSrcSpan ss = braces $ char ' ' <> - (hang (ppr ss) 1 + hang (ppr ss) 1 -- TODO: show annotations here - (text "")) + (text "") +#if !MIN_VERSION_ghc(9,11,0) addEpAnn :: AddEpAnn -> SDoc addEpAnn (AddEpAnn a s) = text "AddEpAnn" <+> ppr a <+> epaAnchor s +#endif var :: Var -> SDoc var v = braces $ text "Var:" <+> ppr v @@ -198,18 +217,22 @@ showAstDataHtml a0 = html $ located :: (Data a, Data b) => GenLocated a b -> SDoc located (L ss a) - = nested "L" $ (li (showAstDataHtml' ss) $$ li (showAstDataHtml' a)) + = nested "L" (li (showAstDataHtml' ss) $$ li (showAstDataHtml' a)) -- ------------------------- +#if !MIN_VERSION_ghc(9,11,0) annotation :: EpAnn [AddEpAnn] -> SDoc annotation = annotation' (text "EpAnn [AddEpAnn]") +#endif annotationModule :: EpAnn AnnsModule -> SDoc annotationModule = annotation' (text "EpAnn AnnsModule") +#if !MIN_VERSION_ghc(9,11,0) annotationAddEpAnn :: EpAnn AddEpAnn -> SDoc annotationAddEpAnn = annotation' (text "EpAnn AddEpAnn") +#endif annotationGrhsAnn :: EpAnn GrhsAnn -> SDoc annotationGrhsAnn = annotation' (text "EpAnn GrhsAnn") @@ -217,15 +240,14 @@ showAstDataHtml a0 = html $ annotationEpAnnHsCase :: EpAnn EpAnnHsCase -> SDoc annotationEpAnnHsCase = annotation' (text "EpAnn EpAnnHsCase") -#if MIN_VERSION_ghc(9,4,0) annotationEpAnnHsLet :: EpAnn NoEpAnns -> SDoc annotationEpAnnHsLet = annotation' (text "EpAnn NoEpAnns") -#else - annotationEpAnnHsLet :: EpAnn AnnsLet -> SDoc - annotationEpAnnHsLet = annotation' (text "EpAnn AnnsLet") -#endif +#if MIN_VERSION_ghc(9,11,0) + annotationAnnList :: EpAnn (AnnList ()) -> SDoc +#else annotationAnnList :: EpAnn AnnList -> SDoc +#endif annotationAnnList = annotation' (text "EpAnn AnnList") annotationEpAnnImportDecl :: EpAnn EpAnnImportDecl -> SDoc @@ -240,13 +262,41 @@ showAstDataHtml a0 = html $ annotationEpaLocation :: EpAnn EpaLocation -> SDoc annotationEpaLocation = annotation' (text "EpAnn EpaLocation") - annotation' :: forall a .(Data a, Typeable a) - => SDoc -> EpAnn a -> SDoc - annotation' tag anns = nested (text $ showConstr (toConstr anns)) + annotation' :: forall a. Data a => SDoc -> EpAnn a -> SDoc + annotation' _tag anns = nested (text $ showConstr (toConstr anns)) (vcat (map li $ gmapQ showAstDataHtml' anns)) -- ------------------------- +#if MIN_VERSION_ghc(9,9,0) + srcSpanAnnA :: EpAnn AnnListItem -> SDoc + srcSpanAnnA = locatedAnn'' (text "SrcSpanAnnA") + +#if MIN_VERSION_ghc(9,11,0) + srcSpanAnnL :: EpAnn (AnnList ()) -> SDoc +#else + srcSpanAnnL :: EpAnn AnnList -> SDoc +#endif + srcSpanAnnL = locatedAnn'' (text "SrcSpanAnnL") + + srcSpanAnnP :: EpAnn AnnPragma -> SDoc + srcSpanAnnP = locatedAnn'' (text "SrcSpanAnnP") + + srcSpanAnnC :: EpAnn AnnContext -> SDoc + srcSpanAnnC = locatedAnn'' (text "SrcSpanAnnC") + + srcSpanAnnN :: EpAnn NameAnn -> SDoc + srcSpanAnnN = locatedAnn'' (text "SrcSpanAnnN") + + locatedAnn'' :: forall a. Data a => SDoc -> EpAnn a -> SDoc + locatedAnn'' tag ss = parens $ + case cast ss of + Just (ann :: EpAnn a) -> + text (showConstr (toConstr ann)) + $$ vcat (gmapQ showAstDataHtml' ann) + Nothing -> text "locatedAnn:unmatched" <+> tag + <+> (parens $ text (showConstr (toConstr ss))) +#else srcSpanAnnA :: SrcSpanAnn' (EpAnn AnnListItem) -> SDoc srcSpanAnnA = locatedAnn'' (text "SrcSpanAnnA") @@ -262,16 +312,17 @@ showAstDataHtml a0 = html $ srcSpanAnnN :: SrcSpanAnn' (EpAnn NameAnn) -> SDoc srcSpanAnnN = locatedAnn'' (text "SrcSpanAnnN") - locatedAnn'' :: forall a. (Typeable a, Data a) + locatedAnn'' :: forall a. Data a => SDoc -> SrcSpanAnn' a -> SDoc locatedAnn'' tag ss = case cast ss of Just ((SrcSpanAnn ann s) :: SrcSpanAnn' a) -> - nested "SrcSpanAnn" $ ( + nested "SrcSpanAnn" ( li(showAstDataHtml' ann) $$ li(srcSpan s)) Nothing -> text "locatedAnn:unmatched" <+> tag - <+> (text (showConstr (toConstr ss))) + <+> text (showConstr (toConstr ss)) +#endif normalize_newlines :: String -> String diff --git a/plugins/hls-refactor-plugin/src/Development/IDE/GHC/ExactPrint.hs b/plugins/hls-refactor-plugin/src/Development/IDE/GHC/ExactPrint.hs index 8e570d9dc0..666de9a6f2 100644 --- a/plugins/hls-refactor-plugin/src/Development/IDE/GHC/ExactPrint.hs +++ b/plugins/hls-refactor-plugin/src/Development/IDE/GHC/ExactPrint.hs @@ -1,5 +1,6 @@ -{-# LANGUAGE TypeFamilies #-} {-# LANGUAGE GADTs #-} +{-# LANGUAGE TypeFamilies #-} +{-# OPTIONS_GHC -Wno-orphans #-} -- | This module hosts various abstractions and utility functions to work with ghc-exactprint. module Development.IDE.GHC.ExactPrint @@ -43,7 +44,7 @@ module Development.IDE.GHC.ExactPrint where import Control.Applicative (Alternative) -import Control.Arrow (right, (***)) +import Control.Arrow ((***)) import Control.DeepSeq import Control.Monad import qualified Control.Monad.Fail as Fail @@ -53,17 +54,13 @@ import Control.Monad.Trans.Except import Control.Monad.Zip import Data.Bifunctor import Data.Bool (bool) -import Data.Default (Default) import qualified Data.DList as DL import Data.Either.Extra (mapLeft) -import Data.Foldable (Foldable (fold)) import Data.Functor.Classes import Data.Functor.Contravariant import Data.Monoid (All (All), getAll) import qualified Data.Text as T -import Data.Traversable (for) import Development.IDE.Core.RuleTypes -import Development.IDE.Core.Service (runAction) import Development.IDE.Core.Shake hiding (Log) import qualified Development.IDE.Core.Shake as Shake import Development.IDE.GHC.Compat hiding (parseImport, @@ -72,46 +69,72 @@ import Development.IDE.GHC.Compat hiding (parseImport, import Development.IDE.GHC.Compat.ExactPrint import Development.IDE.Graph (RuleResult, Rules) import Development.IDE.Graph.Classes -import Development.IDE.Types.Location -import Ide.Logger (Pretty (pretty), - Recorder, - WithPriority, - cmapWithPrio) import Generics.SYB import Generics.SYB.GHC import qualified GHC.Generics as GHC +import Ide.Logger (Pretty (pretty), + Recorder, + WithPriority, + cmapWithPrio) import Ide.PluginUtils import Language.Haskell.GHC.ExactPrint.Parsers import Language.LSP.Protocol.Types -import Retrie.ExactPrint hiding (parseDecl, - parseExpr, - parsePattern, - parseType) -#if MIN_VERSION_ghc(9,9,0) -import GHC.Plugins (showSDoc) -import GHC.Utils.Outputable (Outputable (ppr)) -#else -import GHC (EpAnn (..), + +import Control.Lens (_last, (&)) +import Control.Lens.Operators ((%~)) +import Data.List (partition) +import GHC (DeltaPos (..), + SrcSpanAnnN) + +-- See Note [Guidelines For Using CPP In GHCIDE Import Statements] + +#if !MIN_VERSION_ghc(9,9,0) +import Data.Default (Default) +import GHC ( Anchor (..), + AnchorOperation, + EpAnn (..), NameAdornment (NameParens), NameAnn (..), SrcSpanAnn' (SrcSpanAnn), SrcSpanAnnA, TrailingAnn (AddCommaAnn), emptyComments, + realSrcSpan, spanAsAnchor) import GHC.Parser.Annotation (AnnContext (..), - DeltaPos (SameLine), EpaLocation (EpaDelta), deltaPos) +import GHC.Types.SrcLoc (generatedSrcSpan) +#endif +#if MIN_VERSION_ghc(9,11,0) +import GHC.Types.SrcLoc (UnhelpfulSpanReason(..)) #endif -import Data.List (partition) -import GHC (Anchor(..), realSrcSpan, AnchorOperation, DeltaPos(..), SrcSpanAnnN) -import GHC.Types.SrcLoc (generatedSrcSpan) -import Control.Lens ((&), _last) -import Control.Lens.Operators ((%~)) - -setPrecedingLines :: Default t => LocatedAn t a -> Int -> Int -> LocatedAn t a +#if MIN_VERSION_ghc(9,9,0) +import GHC ( +#if !MIN_VERSION_ghc(9,11,0) + Anchor, +#endif + AnnContext (..), + EpAnn (..), + EpaLocation, + EpaLocation' (..), +#if MIN_VERSION_ghc(9,11,0) + EpToken (..), +#endif + NameAdornment (..), + NameAnn (..), + SrcSpanAnnA, + TrailingAnn (..), + deltaPos, + emptyComments, + spanAsAnchor) +#endif +setPrecedingLines :: +#if !MIN_VERSION_ghc(9,9,0) + Default t => +#endif + LocatedAn t a -> Int -> Int -> LocatedAn t a setPrecedingLines ast n c = setEntryDP ast (deltaPos n c) ------------------------------------------------------------------------------ @@ -121,18 +144,15 @@ instance Pretty Log where pretty = \case LogShake shakeLog -> pretty shakeLog -instance Show (Annotated ParsedSource) where - show _ = "" - -instance NFData (Annotated ParsedSource) where - rnf = rwhnf - data GetAnnotatedParsedSource = GetAnnotatedParsedSource - deriving (Eq, Show, Typeable, GHC.Generic) + deriving (Eq, Show, GHC.Generic) instance Hashable GetAnnotatedParsedSource instance NFData GetAnnotatedParsedSource -type instance RuleResult GetAnnotatedParsedSource = Annotated ParsedSource +type instance RuleResult GetAnnotatedParsedSource = ParsedSource + +instance Show (HsModule GhcPs) where + show _ = "" -- | Get the latest version of the annotated parse source with comments. getAnnotatedParsedSourceRule :: Recorder (WithPriority Log) -> Rules () @@ -140,8 +160,17 @@ getAnnotatedParsedSourceRule recorder = define (cmapWithPrio LogShake recorder) pm <- use GetParsedModuleWithComments nfp return ([], fmap annotateParsedSource pm) -annotateParsedSource :: ParsedModule -> Annotated ParsedSource -annotateParsedSource (ParsedModule _ ps _ _) = unsafeMkA (makeDeltaAst ps) 0 +annotateParsedSource :: ParsedModule -> ParsedSource +annotateParsedSource (ParsedModule _ ps _) = +#if MIN_VERSION_ghc(9,9,0) + ps +#else + (makeDeltaAst ps) +#endif + +#if MIN_VERSION_ghc(9,11,0) +type Anchor = EpaLocation +#endif ------------------------------------------------------------------------------ @@ -194,7 +223,7 @@ transform :: ClientCapabilities -> VersionedTextDocumentIdentifier -> Graft (Either String) ParsedSource -> - Annotated ParsedSource -> + ParsedSource -> Either String WorkspaceEdit transform dflags ccs verTxtDocId f a = do let src = printA a @@ -211,7 +240,7 @@ transformM :: ClientCapabilities -> VersionedTextDocumentIdentifier -> Graft (ExceptStringT m) ParsedSource -> - Annotated ParsedSource -> + ParsedSource -> m (Either String WorkspaceEdit) transformM dflags ccs verTextDocId f a = runExceptT $ runExceptString $ do @@ -231,7 +260,9 @@ needsParensSpace :: -- | (Needs parens, needs space) (All, All) needsParensSpace HsLam{} = (All False, All False) +#if !MIN_VERSION_ghc(9,9,0) needsParensSpace HsLamCase{} = (All False, All True) +#endif needsParensSpace HsApp{} = mempty needsParensSpace HsAppType{} = mempty needsParensSpace OpApp{} = mempty @@ -259,7 +290,7 @@ needsParensSpace _ = mempty -} graft' :: forall ast a l. - (Data a, Typeable l, ASTElement l ast) => + (Data a, ASTElement l ast) => -- | Do we need to insert a space before this grafting? In do blocks, the -- answer is no, or we will break layout. But in function applications, -- the answer is yes, or the function call won't get its argument. Yikes! @@ -349,7 +380,7 @@ graftExprWithM dst trans = Graft $ \dflags a -> do graftWithM :: forall ast m a l. - (Fail.MonadFail m, Data a, Typeable l, ASTElement l ast) => + (Fail.MonadFail m, Data a, ASTElement l ast) => SrcSpan -> (LocatedAn l ast -> TransformT m (Maybe (LocatedAn l ast))) -> Graft m a @@ -420,8 +451,8 @@ graftDecls dst decs0 = Graft $ \dflags a -> do -- For example, if you would like to move a where-clause-defined variable to the same -- level as its parent HsDecl, you could use this function. -- --- When matching declaration is found in the sub-declarations of `a`, `Just r` is also returned with the new `a`. If --- not declaration matched, then `Nothing` is returned. +-- When matching declaration is found in the sub-declarations of `a`, `Just r` is also returned with the new `a`. +-- If no declaration matched, then `Nothing` is returned. modifySmallestDeclWithM :: forall a m r. (HasDecls a, Monad m) => @@ -435,23 +466,42 @@ modifySmallestDeclWithM validSpan f a = do TransformT (lift $ validSpan $ locA src) >>= \case True -> do (decs', r) <- f ldecl - pure $ (DL.fromList decs' <> DL.fromList rest, Just r) + pure (DL.fromList decs' <> DL.fromList rest, Just r) False -> first (DL.singleton ldecl <>) <$> modifyMatchingDecl rest modifyDeclsT' (fmap (first DL.toList) . modifyMatchingDecl) a +#if MIN_VERSION_ghc(9,11,0) +generatedAnchor :: DeltaPos -> Anchor +generatedAnchor dp = EpaDelta (UnhelpfulSpan UnhelpfulNoLocationInfo) dp [] +#elif MIN_VERSION_ghc(9,9,0) +generatedAnchor :: DeltaPos -> Anchor +generatedAnchor dp = EpaDelta dp [] +#else generatedAnchor :: AnchorOperation -> Anchor generatedAnchor anchorOp = GHC.Anchor (GHC.realSrcSpan generatedSrcSpan) anchorOp +#endif setAnchor :: Anchor -> SrcSpanAnnN -> SrcSpanAnnN +#if MIN_VERSION_ghc(9,9,0) +setAnchor anc (EpAnn _ nameAnn comments) = + EpAnn anc nameAnn comments +#else setAnchor anc (SrcSpanAnn (EpAnn _ nameAnn comments) span) = SrcSpanAnn (EpAnn anc nameAnn comments) span setAnchor _ spanAnnN = spanAnnN +#endif removeTrailingAnns :: SrcSpanAnnN -> SrcSpanAnnN +#if MIN_VERSION_ghc(9,9,0) +removeTrailingAnns (EpAnn anc nameAnn comments) = + let nameAnnSansTrailings = nameAnn {nann_trailing = []} + in EpAnn anc nameAnnSansTrailings comments +#else removeTrailingAnns (SrcSpanAnn (EpAnn anc nameAnn comments) span) = let nameAnnSansTrailings = nameAnn {nann_trailing = []} in SrcSpanAnn (EpAnn anc nameAnnSansTrailings comments) span removeTrailingAnns spanAnnN = spanAnnN +#endif -- | Modify the type signature for the given IdP. This function handles splitting a multi-sig -- SigD into multiple SigD if the type signature is changed. @@ -470,14 +520,14 @@ removeTrailingAnns spanAnnN = spanAnnN -- + foo :: Bool modifySigWithM :: forall a m. - (HasDecls a, Monad m) => + (HasDecls a, Monad m, ExactPrint a) => IdP GhcPs -> (LHsSigType GhcPs -> LHsSigType GhcPs) -> a -> TransformT m a modifySigWithM queryId f a = do let modifyMatchingSigD :: [LHsDecl GhcPs] -> TransformT m (DL.DList (LHsDecl GhcPs)) - modifyMatchingSigD [] = pure (DL.empty) + modifyMatchingSigD [] = pure DL.empty modifyMatchingSigD (ldecl@(L annSigD (SigD xsig (TypeSig xTypeSig ids (HsWC xHsWc lHsSig)))) : rest) | queryId `elem` (unLoc <$> ids) = do let newSig = f lHsSig @@ -489,22 +539,36 @@ modifySigWithM queryId f a = do let matchedId' = L (setAnchor genAnchor0 $ removeTrailingAnns annMatchedId) matchedId matchedIdSig = let sig' = SigD xsig (TypeSig xTypeSig [matchedId'] (HsWC xHsWc newSig)) - epAnn = bool (noAnnSrcSpanDP generatedSrcSpan (DifferentLine 1 0)) annSigD (null otherIds) + epAnn = bool (noAnnSrcSpanDP +#if !MIN_VERSION_ghc(9,9,0) + generatedSrcSpan +#endif + (DifferentLine 1 0)) + annSigD (null otherIds) in L epAnn sig' otherSig = case otherIds of [] -> [] - (L (SrcSpanAnn epAnn span) id1:ids) -> [ +#if MIN_VERSION_ghc(9,9,0) + (L epAnn id1:ids) -> +#else + (L (SrcSpanAnn epAnn span) id1:ids) -> +#endif + [ let epAnn' = case epAnn of EpAnn _ nameAnn commentsId1 -> EpAnn genAnchor0 nameAnn commentsId1 +#if MIN_VERSION_ghc(9,9,0) + ids' = L epAnn' id1:ids +#else EpAnnNotUsed -> EpAnn genAnchor0 mempty emptyComments ids' = L (SrcSpanAnn epAnn' span) id1:ids +#endif ids'' = ids' & _last %~ first removeTrailingAnns in L annSigD (SigD xsig (TypeSig xTypeSig ids'' (HsWC xHsWc lHsSig))) - ] + ] in pure $ DL.fromList otherSig <> DL.singleton matchedIdSig <> DL.fromList rest _ -> error "multiple ids matched" modifyMatchingSigD (ldecl : rest) = (DL.singleton ldecl <>) <$> modifyMatchingSigD rest - modifyDeclsT (fmap DL.toList . modifyMatchingSigD) a + modifyDeclsT (fmap DL.toList . modifyMatchingSigD) $ makeDeltaAst a genAnchor0 :: Anchor genAnchor0 = generatedAnchor m0 @@ -512,14 +576,29 @@ genAnchor0 = generatedAnchor m0 genAnchor1 :: Anchor genAnchor1 = generatedAnchor m1 +#if MIN_VERSION_ghc(9,9,0) +m0, m1 :: DeltaPos +m0 = SameLine 0 +m1 = SameLine 1 +#endif + + -- | Apply a transformation to the decls contained in @t@ modifyDeclsT' :: (HasDecls t, HasTransform m) => ([LHsDecl GhcPs] -> m ([LHsDecl GhcPs], r)) -> t -> m (t, r) modifyDeclsT' action t = do +#if MIN_VERSION_ghc_exactprint(1,10,0) + decls <- pure $ hsDecls t +#else decls <- liftT $ hsDecls t +#endif (decls', r) <- action decls +#if MIN_VERSION_ghc_exactprint(1,10,0) + t' <- pure $ replaceDecls t decls' +#else t' <- liftT $ replaceDecls t decls' +#endif pure (t', r) -- | Modify each LMatch in a MatchGroup @@ -538,17 +617,10 @@ modifyMgMatchesT' :: r -> (r -> r -> m r) -> TransformT m (MatchGroup GhcPs (LHsExpr GhcPs), r) -#if MIN_VERSION_ghc(9,5,0) modifyMgMatchesT' (MG xMg (L locMatches matches)) f def combineResults = do (unzip -> (matches', rs)) <- mapM f matches r' <- TransformT $ lift $ foldM combineResults def rs - pure $ (MG xMg (L locMatches matches'), r') -#else -modifyMgMatchesT' (MG xMg (L locMatches matches) originMg) f def combineResults = do - (unzip -> (matches', rs)) <- mapM f matches - r' <- lift $ foldM combineResults def rs - pure $ (MG xMg (L locMatches matches') originMg, r') -#endif + pure (MG xMg (L locMatches matches'), r') graftSmallestDeclsWithM :: forall a. @@ -595,7 +667,9 @@ class , Typeable l , Outputable l , Outputable ast +#if !MIN_VERSION_ghc(9,9,0) , Default l +#endif ) => ASTElement l ast | ast -> l where parseAST :: Parser (LocatedAn l ast) maybeParensAST :: LocatedAn l ast -> LocatedAn l ast @@ -644,31 +718,21 @@ instance ASTElement NameAnn RdrName where -- | Given an 'LHSExpr', compute its exactprint annotations. -- Note that this function will throw away any existing annotations (and format) -annotate :: (ASTElement l ast, Outputable l) +annotate :: ASTElement l ast => DynFlags -> Bool -> LocatedAn l ast -> TransformT (Either String) (LocatedAn l ast) annotate dflags needs_space ast = do uniq <- show <$> uniqueSrcSpanT let rendered = render dflags ast -#if MIN_VERSION_ghc(9,4,0) expr' <- TransformT $ lift $ mapLeft (showSDoc dflags . ppr) $ parseAST dflags uniq rendered pure $ setPrecedingLines expr' 0 (bool 0 1 needs_space) -#else - expr' <- lift $ mapLeft show $ parseAST dflags uniq rendered - pure $ setPrecedingLines expr' 0 (bool 0 1 needs_space) -#endif -- | Given an 'LHsDecl', compute its exactprint annotations. annotateDecl :: DynFlags -> LHsDecl GhcPs -> TransformT (Either String) (LHsDecl GhcPs) annotateDecl dflags ast = do uniq <- show <$> uniqueSrcSpanT let rendered = render dflags ast -#if MIN_VERSION_ghc(9,4,0) expr' <- TransformT $ lift $ mapLeft (showSDoc dflags . ppr) $ parseDecl dflags uniq rendered pure $ setPrecedingLines expr' 1 0 -#else - expr' <- lift $ mapLeft show $ parseDecl dflags uniq rendered - pure $ setPrecedingLines expr' 1 0 -#endif ------------------------------------------------------------------------------ @@ -689,47 +753,78 @@ parenthesize = parenthesizeHsExpr appPrec eqSrcSpan :: SrcSpan -> SrcSpan -> Bool eqSrcSpan l r = leftmost_smallest l r == EQ --- | Equality on SrcSpan's. --- Ignores the (Maybe BufSpan) field of SrcSpan's. -eqSrcSpanA :: SrcAnn la -> SrcAnn b -> Bool -eqSrcSpanA l r = leftmost_smallest (locA l) (locA r) == EQ - addParensToCtxt :: Maybe EpaLocation -> AnnContext -> AnnContext addParensToCtxt close_dp = addOpen . addClose where +#if MIN_VERSION_ghc(9,11,0) + addOpen it@AnnContext{ac_open = []} = it{ac_open = [EpTok (epl 0)]} +#else addOpen it@AnnContext{ac_open = []} = it{ac_open = [epl 0]} +#endif addOpen other = other addClose it +#if MIN_VERSION_ghc(9,11,0) + | Just c <- close_dp = it{ac_close = [EpTok c]} + | AnnContext{ac_close = []} <- it = it{ac_close = [EpTok (epl 0)]} +#else | Just c <- close_dp = it{ac_close = [c]} | AnnContext{ac_close = []} <- it = it{ac_close = [epl 0]} +#endif | otherwise = it epl :: Int -> EpaLocation +#if MIN_VERSION_ghc(9,11,0) +epl n = EpaDelta (UnhelpfulSpan UnhelpfulNoLocationInfo) (SameLine n) [] +#else epl n = EpaDelta (SameLine n) [] +#endif epAnn :: SrcSpan -> ann -> EpAnn ann epAnn srcSpan anns = EpAnn (spanAsAnchor srcSpan) anns emptyComments modifyAnns :: LocatedAn a ast -> (a -> a) -> LocatedAn a ast +#if MIN_VERSION_ghc(9,9,0) +modifyAnns x f = first (fmap f) x +#else modifyAnns x f = first ((fmap.fmap) f) x +#endif removeComma :: SrcSpanAnnA -> SrcSpanAnnA +#if MIN_VERSION_ghc(9,9,0) +removeComma (EpAnn anc (AnnListItem as) cs) + = EpAnn anc (AnnListItem (filter (not . isCommaAnn) as)) cs + where + isCommaAnn AddCommaAnn{} = True + isCommaAnn _ = False +#else removeComma it@(SrcSpanAnn EpAnnNotUsed _) = it removeComma (SrcSpanAnn (EpAnn anc (AnnListItem as) cs) l) - = (SrcSpanAnn (EpAnn anc (AnnListItem (filter (not . isCommaAnn) as)) cs) l) + = SrcSpanAnn (EpAnn anc (AnnListItem (filter (not . isCommaAnn) as)) cs) l where isCommaAnn AddCommaAnn{} = True isCommaAnn _ = False +#endif addParens :: Bool -> GHC.NameAnn -> GHC.NameAnn +#if MIN_VERSION_ghc(9,11,0) addParens True it@NameAnn{} = - it{nann_adornment = NameParens, nann_open = epl 0, nann_close = epl 0 } + it{nann_adornment = NameParens (EpTok (epl 0)) (EpTok (epl 0)) } addParens True it@NameAnnCommas{} = - it{nann_adornment = NameParens, nann_open = epl 0, nann_close = epl 0 } + it{nann_adornment = NameParens (EpTok (epl 0)) (EpTok (epl 0)) } addParens True it@NameAnnOnly{} = - it{nann_adornment = NameParens, nann_open = epl 0, nann_close = epl 0 } + it{nann_adornment = NameParens (EpTok (epl 0)) (EpTok (epl 0)) } +addParens True it@NameAnnTrailing{} = + NameAnn{nann_adornment = NameParens (EpTok (epl 0)) (EpTok (epl 0)), nann_name = epl 0, nann_trailing = nann_trailing it} +#else +addParens True it@NameAnn{} = + it{nann_adornment = NameParens, nann_open=epl 0, nann_close=epl 0 } +addParens True it@NameAnnCommas{} = + it{nann_adornment = NameParens, nann_open=epl 0, nann_close=epl 0 } +addParens True it@NameAnnOnly{} = + it{nann_adornment = NameParens, nann_open=epl 0, nann_close=epl 0 } addParens True NameAnnTrailing{..} = - NameAnn{nann_adornment = NameParens, nann_open = epl 0, nann_close = epl 0, nann_name = epl 0, ..} + NameAnn{nann_adornment = NameParens, nann_open=epl 0, nann_close=epl 0, nann_name = epl 0, ..} +#endif addParens _ it = it removeTrailingComma :: GenLocated SrcSpanAnnA ast -> GenLocated SrcSpanAnnA ast diff --git a/plugins/hls-refactor-plugin/src/Development/IDE/Plugin/CodeAction.hs b/plugins/hls-refactor-plugin/src/Development/IDE/Plugin/CodeAction.hs index 48130e0d73..e471d1781a 100644 --- a/plugins/hls-refactor-plugin/src/Development/IDE/Plugin/CodeAction.hs +++ b/plugins/hls-refactor-plugin/src/Development/IDE/Plugin/CodeAction.hs @@ -1,5 +1,6 @@ -- Copyright (c) 2019 The DAML Authors. All rights reserved. -- SPDX-License-Identifier: Apache-2.0 +{-# LANGUAGE CPP #-} {-# LANGUAGE GADTs #-} module Development.IDE.Plugin.CodeAction @@ -11,18 +12,18 @@ module Development.IDE.Plugin.CodeAction fillHolePluginDescriptor, extendImportPluginDescriptor, -- * For testing - matchRegExMultipleImports + matchRegExMultipleImports, + extractNotInScopeName, + NotInScope(..) ) where import Control.Applicative ((<|>)) -import Control.Applicative.Combinators.NonEmpty (sepBy1) import Control.Arrow (second, (&&&), (>>>)) import Control.Concurrent.STM.Stats (atomically) import Control.Monad.Extra import Control.Monad.IO.Class -import Control.Monad.Trans import Control.Monad.Trans.Except (ExceptT (ExceptT)) import Control.Monad.Trans.Maybe import Data.Char @@ -40,15 +41,17 @@ import Data.Ord (comparing) import qualified Data.Set as S import qualified Data.Text as T import qualified Data.Text.Encoding as T -import qualified Data.Text.Utf16.Rope as Rope +import qualified Data.Text.Utf16.Rope.Mixed as Rope +import Development.IDE.Core.FileStore (getUriContents) import Development.IDE.Core.Rules import Development.IDE.Core.RuleTypes import Development.IDE.Core.Service import Development.IDE.Core.Shake hiding (Log) import Development.IDE.GHC.Compat hiding (ImplicitPrelude) -import Development.IDE.GHC.Compat.ExactPrint +#if !MIN_VERSION_ghc(9,11,0) import Development.IDE.GHC.Compat.Util +#endif import Development.IDE.GHC.Error import Development.IDE.GHC.ExactPrint import qualified Development.IDE.GHC.ExactPrint as E @@ -65,38 +68,26 @@ import Development.IDE.Plugin.Plugins.FillHole (suggestFillH import Development.IDE.Plugin.Plugins.FillTypeWildcard (suggestFillTypeWildcard) import Development.IDE.Plugin.Plugins.ImportUtils import Development.IDE.Plugin.TypeLenses (suggestSignature) +import Development.IDE.Types.Diagnostics import Development.IDE.Types.Exports import Development.IDE.Types.Location import Development.IDE.Types.Options -import GHC.Exts (fromList) +import GHC (DeltaPos (..), + EpAnn (..), + LEpaComment) import qualified GHC.LanguageExtensions as Lang import Ide.Logger hiding (group) -import qualified Text.Regex.Applicative as RE -#if MIN_VERSION_ghc(9,4,0) -import GHC.Parser.Annotation (TokenLocation (..)) -#endif -import GHC (AddEpAnn (AddEpAnn), - Anchor (anchor_op), - AnchorOperation (..), - AnnsModule (am_main), - DeltaPos (..), - EpAnn (..), - EpaLocation (..), - LEpaComment, - hsmodAnn) -import Ide.PluginUtils (extractTextInRange, +import Ide.PluginUtils (extendToFullLines, subRange) import Ide.Types import Language.LSP.Protocol.Message (Method (..), SMethod (..)) import Language.LSP.Protocol.Types (ApplyWorkspaceEditParams (..), CodeAction (..), - CodeActionContext (CodeActionContext, _diagnostics), CodeActionKind (CodeActionKind_QuickFix), CodeActionParams (CodeActionParams), Command, - Diagnostic (..), MessageType (..), Null (Null), ShowMessageParams (..), @@ -106,27 +97,52 @@ import Language.LSP.Protocol.Types (ApplyWorkspa WorkspaceEdit (WorkspaceEdit, _changeAnnotations, _changes, _documentChanges), type (|?) (InL, InR), uriToFilePath) -import qualified Language.LSP.Server as LSP -import Language.LSP.VFS (VirtualFile, - _file_text) import qualified Text.Fuzzy.Parallel as TFP import Text.Regex.TDFA ((=~), (=~~)) +-- See Note [Guidelines For Using CPP In GHCIDE Import Statements] + +#if !MIN_VERSION_ghc(9,9,0) +import Development.IDE.GHC.Compat.ExactPrint (makeDeltaAst) +import GHC (AddEpAnn (AddEpAnn), + Anchor (anchor_op), + AnchorOperation (..), + AnnsModule (am_main), + EpaLocation (..)) +#endif + +#if MIN_VERSION_ghc(9,9,0) && !MIN_VERSION_ghc(9,11,0) +import GHC (AddEpAnn (AddEpAnn), + AnnsModule (am_main), + EpaLocation, + EpaLocation' (..), + HasLoc (..)) +#endif + +#if MIN_VERSION_ghc(9,11,0) +import GHC (AnnsModule (am_where), + EpToken (..), + EpaLocation, + EpaLocation' (..), + HasLoc (..)) +#endif + + ------------------------------------------------------------------------------------------------- -- | Generate code actions. codeAction :: PluginMethodHandler IdeState 'Method_TextDocumentCodeAction -codeAction state _ (CodeActionParams _ _ (TextDocumentIdentifier uri) _range CodeActionContext{_diagnostics= xs}) = do - contents <- lift $ LSP.getVirtualFile $ toNormalizedUri uri +codeAction state _ (CodeActionParams _ _ (TextDocumentIdentifier uri) range _) = do + contents <- liftIO $ runAction "hls-refactor-plugin.codeAction.getUriContents" state $ getUriContents $ toNormalizedUri uri liftIO $ do - let text = Rope.toText . (_file_text :: VirtualFile -> Rope.Rope) <$> contents - mbFile = toNormalizedFilePath' <$> uriToFilePath uri - diag <- atomically $ fmap (\(_, _, d) -> d) . filter (\(p, _, _) -> mbFile == Just p) <$> getDiagnostics state + let mbFile = toNormalizedFilePath' <$> uriToFilePath uri + allDiags <- atomically $ fmap fdLspDiagnostic . filter (\d -> mbFile == Just (fdFilePath d)) <$> getDiagnostics state (join -> parsedModule) <- runAction "GhcideCodeActions.getParsedModule" state $ getParsedModule `traverse` mbFile let - actions = caRemoveRedundantImports parsedModule text diag xs uri - <> caRemoveInvalidExports parsedModule text diag xs uri - pure $ InL $ actions + textContents = fmap Rope.toText contents + actions = caRemoveRedundantImports parsedModule textContents allDiags range uri + <> caRemoveInvalidExports parsedModule textContents allDiags range uri + pure $ InL actions ------------------------------------------------------------------------------------------------- @@ -145,6 +161,7 @@ iePluginDescriptor recorder plId = , wrap suggestAddRecordFieldImport ] plId + "Provides various quick fixes" in mkExactprintPluginDescriptor recorder $ old {pluginHandlers = pluginHandlers old <> mkPluginHandler SMethod_TextDocumentCodeAction codeAction } typeSigsPluginDescriptor :: Recorder (WithPriority E.Log) -> PluginId -> PluginDescriptor IdeState @@ -157,6 +174,7 @@ typeSigsPluginDescriptor recorder plId = mkExactprintPluginDescriptor recorder $ , wrap suggestConstraint ] plId + "Provides various quick fixes for type signatures" bindingsPluginDescriptor :: Recorder (WithPriority E.Log) -> PluginId -> PluginDescriptor IdeState bindingsPluginDescriptor recorder plId = mkExactprintPluginDescriptor recorder $ @@ -168,12 +186,13 @@ bindingsPluginDescriptor recorder plId = mkExactprintPluginDescriptor recorder $ , wrap suggestDeleteUnusedBinding ] plId + "Provides various quick fixes for bindings" fillHolePluginDescriptor :: Recorder (WithPriority E.Log) -> PluginId -> PluginDescriptor IdeState -fillHolePluginDescriptor recorder plId = mkExactprintPluginDescriptor recorder (mkGhcideCAPlugin (wrap suggestFillHole) plId) +fillHolePluginDescriptor recorder plId = mkExactprintPluginDescriptor recorder (mkGhcideCAPlugin (wrap suggestFillHole) plId "Provides a code action to fill a hole") extendImportPluginDescriptor :: Recorder (WithPriority E.Log) -> PluginId -> PluginDescriptor IdeState -extendImportPluginDescriptor recorder plId = mkExactprintPluginDescriptor recorder $ (defaultPluginDescriptor plId) +extendImportPluginDescriptor recorder plId = mkExactprintPluginDescriptor recorder $ (defaultPluginDescriptor plId "Provides a command to extend the import list") { pluginCommands = [extendImportCommand] } @@ -189,21 +208,21 @@ extendImportCommand = PluginCommand (CommandId extendImportCommandId) "additional edits for a completion" extendImportHandler extendImportHandler :: CommandFunction IdeState ExtendImport -extendImportHandler ideState edit@ExtendImport {..} = ExceptT $ do +extendImportHandler ideState _ edit@ExtendImport {..} = ExceptT $ do res <- liftIO $ runMaybeT $ extendImportHandler' ideState edit whenJust res $ \(nfp, wedit@WorkspaceEdit {_changes}) -> do - let (_, (head -> TextEdit {_range})) = fromJust $ _changes >>= listToMaybe . M.toList - srcSpan = rangeToSrcSpan nfp _range - LSP.sendNotification SMethod_WindowShowMessage $ - ShowMessageParams MessageType_Info $ - "Import " - <> maybe ("‘" <> newThing) (\x -> "‘" <> x <> " (" <> newThing <> ")") thingParent - <> "’ from " - <> importName - <> " (at " - <> printOutputable srcSpan - <> ")" - void $ LSP.sendRequest SMethod_WorkspaceApplyEdit (ApplyWorkspaceEditParams Nothing wedit) (\_ -> pure ()) + whenJust (listToMaybe =<< listToMaybe . M.elems =<< _changes) $ \TextEdit {_range} -> do + let srcSpan = rangeToSrcSpan nfp _range + pluginSendNotification SMethod_WindowShowMessage $ + ShowMessageParams MessageType_Info $ + "Import " + <> maybe ("‘" <> newThing) (\x -> "‘" <> x <> " (" <> newThing <> ")") thingParent + <> "’ from " + <> importName + <> " (at " + <> printOutputable srcSpan + <> ")" + void $ pluginSendRequest SMethod_WorkspaceApplyEdit (ApplyWorkspaceEditParams Nothing wedit) (\_ -> pure ()) return $ Right $ InR Null extendImportHandler' :: IdeState -> ExtendImport -> MaybeT IO (NormalizedFilePath, WorkspaceEdit) @@ -226,9 +245,13 @@ extendImportHandler' ideState ExtendImport {..} case existingImport of Just imp -> do fmap (nfp,) $ liftEither $ - rewriteToWEdit df doc - $ - extendImport (T.unpack <$> thingParent) (T.unpack newThing) (makeDeltaAst imp) + rewriteToWEdit df doc $ + extendImport (T.unpack <$> thingParent) (T.unpack newThing) +#if MIN_VERSION_ghc(9,9,0) + imp +#else + (makeDeltaAst imp) +#endif Nothing -> do let qns = (,) <$> importQual <*> Just (qualifiedImportStyle df) @@ -237,28 +260,20 @@ extendImportHandler' ideState ExtendImport {..} it = case thingParent of Nothing -> newThing Just p -> p <> "(" <> newThing <> ")" - t <- liftMaybe $ snd <$> newImportToEdit n ps (fromMaybe "" contents) - return (nfp, WorkspaceEdit {_changes=Just (GHC.Exts.fromList [(doc, [t])]), _documentChanges=Nothing, _changeAnnotations=Nothing}) + t <- liftMaybe $ snd <$> newImportToEdit n ps (Rope.toText (fromMaybe mempty contents)) + return (nfp, WorkspaceEdit {_changes=Just (M.singleton doc [t]), _documentChanges=Nothing, _changeAnnotations=Nothing}) | otherwise = mzero isWantedModule :: ModuleName -> Maybe ModuleName -> GenLocated l (ImportDecl GhcPs) -> Bool isWantedModule wantedModule Nothing (L _ it@ImportDecl{ ideclName -#if MIN_VERSION_ghc(9,5,0) , ideclImportList = Just (Exactly, _) -#else - , ideclHiding = Just (False, _) -#endif }) = not (isQualifiedImport it) && unLoc ideclName == wantedModule isWantedModule wantedModule (Just qual) (L _ ImportDecl{ ideclAs, ideclName -#if MIN_VERSION_ghc(9,5,0) , ideclImportList = Just (Exactly, _) -#else - , ideclHiding = Just (False, _) -#endif }) = - unLoc ideclName == wantedModule && (wantedModule == qual || (unLoc . reLoc <$> ideclAs) == Just qual) + unLoc ideclName == wantedModule && (wantedModule == qual || (unLoc <$> ideclAs) == Just qual) isWantedModule _ _ _ = False @@ -300,11 +315,7 @@ findSigOfBind range bind = msum [findSigOfBinds range (grhssLocalBinds grhs) -- where clause , do -#if MIN_VERSION_ghc(9,3,0) grhs <- findDeclContainingLoc (_start range) (grhssGRHSs grhs) -#else - grhs <- findDeclContainingLoc (_start range) (map reLocA $ grhssGRHSs grhs) -#endif case unLoc grhs of GRHS _ _ bd -> findSigOfExpr (unLoc bd) ] @@ -312,7 +323,7 @@ findSigOfBind range bind = findSigOfExpr :: HsExpr p -> Maybe (Sig p) findSigOfExpr = go where -#if MIN_VERSION_ghc(9,3,0) +#if !MIN_VERSION_ghc(9,9,0) go (HsLet _ _ binds _ _) = findSigOfBinds range binds #else go (HsLet _ binds _) = findSigOfBinds range binds @@ -331,7 +342,11 @@ findSigOfBinds range = go case unLoc <$> findDeclContainingLoc (_start range) lsigs of Just sig' -> Just sig' Nothing -> do +#if MIN_VERSION_ghc(9,11,0) + lHsBindLR <- findDeclContainingLoc (_start range) binds +#else lHsBindLR <- findDeclContainingLoc (_start range) (bagToList binds) +#endif findSigOfBind range (unLoc lHsBindLR) go _ = Nothing @@ -343,7 +358,11 @@ findInstanceHead df instanceHead decls = showSDoc df (ppr hsib_body) == instanceHead ] +#if MIN_VERSION_ghc(9,9,0) +findDeclContainingLoc :: (Foldable t, HasLoc l) => Position -> t (GenLocated l e) -> Maybe (GenLocated l e) +#else findDeclContainingLoc :: Foldable t => Position -> t (GenLocated (SrcSpanAnn' a) e) -> Maybe (GenLocated (SrcSpanAnn' a) e) +#endif findDeclContainingLoc loc = find (\(L l _) -> loc `isInsideSrcSpan` locA l) -- Single: @@ -355,7 +374,7 @@ findDeclContainingLoc loc = find (\(L l _) -> loc `isInsideSrcSpan` locA l) -- imported from ‘Data.ByteString’ at B.hs:6:1-22 -- imported from ‘Data.ByteString.Lazy’ at B.hs:8:1-27 -- imported from ‘Data.Text’ at B.hs:7:1-16 -suggestHideShadow :: Annotated ParsedSource -> T.Text -> Maybe TcModuleResult -> Maybe HieAstResult -> Diagnostic -> [(T.Text, [Either TextEdit Rewrite])] +suggestHideShadow :: ParsedSource -> T.Text -> Maybe TcModuleResult -> Maybe HieAstResult -> Diagnostic -> [(T.Text, [Either TextEdit Rewrite])] suggestHideShadow ps fileContents mTcM mHar Diagnostic {_message, _range} | Just [identifier, modName, s] <- matchRegexUnifySpaces @@ -373,7 +392,7 @@ suggestHideShadow ps fileContents mTcM mHar Diagnostic {_message, _range} result <> [hideAll] | otherwise = [] where - L _ HsModule {hsmodImports} = astA ps + L _ HsModule {hsmodImports} = ps suggests identifier modName s | Just tcM <- mTcM, @@ -390,7 +409,6 @@ suggestHideShadow ps fileContents mTcM mHar Diagnostic {_message, _range} findImportDeclByModuleName :: [LImportDecl GhcPs] -> String -> Maybe (LImportDecl GhcPs) findImportDeclByModuleName decls modName = flip find decls $ \case (L _ ImportDecl {..}) -> modName == moduleNameString (unLoc ideclName) - _ -> error "impossible" isTheSameLine :: SrcSpan -> SrcSpan -> Bool isTheSameLine s1 s2 @@ -409,7 +427,11 @@ isUnusedImportedId modName importSpan | occ <- mkVarOcc identifier, +#if MIN_VERSION_ghc(9,11,0) + impModsVals <- importedByUser . concat $ M.elems imp_mods, +#else impModsVals <- importedByUser . concat $ moduleEnvElts imp_mods, +#endif Just rdrEnv <- listToMaybe [ imv_all_exports @@ -429,7 +451,7 @@ suggestRemoveRedundantImport ParsedModule{pm_parsed_source = L _ HsModule{hsmod | Just [_, bindings] <- matchRegexUnifySpaces _message "The( qualified)? import of ‘([^’]*)’ from module [^ ]* is redundant" , Just (L _ impDecl) <- find (\(L (locA -> l) _) -> _start _range `isInsideSrcSpan` l && _end _range `isInsideSrcSpan` l ) hsmodImports , Just c <- contents - , ranges <- map (rangesForBindingImport impDecl . T.unpack) (T.splitOn ", " bindings) + , ranges <- map (rangesForBindingImport impDecl . T.unpack) (T.splitOn ", " bindings >>= trySplitIntoOriginalAndRecordField) , ranges' <- extendAllToIncludeCommaIfPossible False (indexedByPosition $ T.unpack c) (concat ranges) , not (null ranges') = [( "Remove " <> bindings <> " from import" , [ TextEdit r "" | r <- ranges' ] )] @@ -441,20 +463,36 @@ suggestRemoveRedundantImport ParsedModule{pm_parsed_source = L _ HsModule{hsmod | _message =~ ("The( qualified)? import of [^ ]* is redundant" :: String) = [("Remove import", [TextEdit (extendToWholeLineIfPossible contents _range) ""])] | otherwise = [] - + where + -- In case of an unused record field import, the binding from the message will not match any import directly + -- In this case, we try if we can additionally extract a record field name + -- Example: The import of ‘B(b2)’ from module ‘ModuleB’ is redundant + trySplitIntoOriginalAndRecordField :: T.Text -> [T.Text] + trySplitIntoOriginalAndRecordField binding = + case matchRegexUnifySpaces binding "([^ ]+)\\(([^)]+)\\)" of + Just [_, fields] -> [binding, fields] + _ -> [binding] + +diagInRange :: Diagnostic -> Range -> Bool +diagInRange Diagnostic {_range = dr} r = dr `subRange` extendedRange + where + -- Ensures the range captures full lines. Makes it easier to trigger the correct + -- "remove redundant" code actions from anywhere on the offending line. + extendedRange = extendToFullLines r -- Note [Removing imports is preferred] +-- ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -- It's good to prefer the remove imports code action because an unused import -- is likely to be removed and less likely the warning will be disabled. -- Therefore actions to remove a single or all redundant imports should be -- preferred, so that the client can prioritize them higher. -caRemoveRedundantImports :: Maybe ParsedModule -> Maybe T.Text -> [Diagnostic] -> [Diagnostic] -> Uri -> [Command |? CodeAction] -caRemoveRedundantImports m contents digs ctxDigs uri +caRemoveRedundantImports :: Maybe ParsedModule -> Maybe T.Text -> [Diagnostic] -> Range -> Uri -> [Command |? CodeAction] +caRemoveRedundantImports m contents allDiags contextRange uri | Just pm <- m, - r <- join $ map (\d -> repeat d `zip` suggestRemoveRedundantImport pm contents d) digs, + r <- join $ map (\d -> repeat d `zip` suggestRemoveRedundantImport pm contents d) allDiags, allEdits <- [ e | (_, (_, edits)) <- r, e <- edits], caRemoveAll <- removeAll allEdits, - ctxEdits <- [ x | x@(d, _) <- r, d `elem` ctxDigs], + ctxEdits <- [ x | x@(d, _) <- r, d `diagInRange` contextRange], not $ null ctxEdits, caRemoveCtx <- map (\(d, (title, tedit)) -> removeSingle title tedit d) ctxEdits = caRemoveCtx ++ [caRemoveAll] @@ -478,18 +516,18 @@ caRemoveRedundantImports m contents digs ctxDigs uri _data_ = Nothing _changeAnnotations = Nothing -caRemoveInvalidExports :: Maybe ParsedModule -> Maybe T.Text -> [Diagnostic] -> [Diagnostic] -> Uri -> [Command |? CodeAction] -caRemoveInvalidExports m contents digs ctxDigs uri +caRemoveInvalidExports :: Maybe ParsedModule -> Maybe T.Text -> [Diagnostic] -> Range -> Uri -> [Command |? CodeAction] +caRemoveInvalidExports m contents allDiags contextRange uri | Just pm <- m, Just txt <- contents, txt' <- indexedByPosition $ T.unpack txt, - r <- mapMaybe (groupDiag pm) digs, + r <- mapMaybe (groupDiag pm) allDiags, r' <- map (\(t,d,rs) -> (t,d,extend txt' rs)) r, caRemoveCtx <- mapMaybe removeSingle r', allRanges <- nubOrd $ [ range | (_,_,ranges) <- r, range <- ranges], allRanges' <- extend txt' allRanges, Just caRemoveAll <- removeAll allRanges', - ctxEdits <- [ x | x@(_, d, _) <- r, d `elem` ctxDigs], + ctxEdits <- [ x | x@(_, d, _) <- r, d `diagInRange` contextRange], not $ null ctxEdits = caRemoveCtx ++ [caRemoveAll] | otherwise = [] @@ -536,7 +574,7 @@ suggestRemoveRedundantExport :: ParsedModule -> Diagnostic -> Maybe (T.Text, [Ra suggestRemoveRedundantExport ParsedModule{pm_parsed_source = L _ HsModule{..}} Diagnostic{..} | msg <- unifySpaces _message , Just export <- hsmodExports - , Just exportRange <- getLocatedRange $ reLoc export + , Just exportRange <- getLocatedRange export , exports <- unLoc export , Just (removeFromExport, !ranges) <- fmap (getRanges exports . notInScope) (extractNotInScopeName msg) <|> (,[_range]) <$> matchExportItem msg @@ -607,16 +645,16 @@ suggestDeleteUnusedBinding let maybeIdx = findIndex (\(L _ id) -> isSameName id name) lnames in case maybeIdx of Nothing -> Nothing - Just _ | length lnames == 1 -> Just (getLoc $ reLoc $ head lnames, True) + Just _ | [lname] <- lnames -> Just (getLoc lname, True) Just idx -> - let targetLname = getLoc $ reLoc $ lnames !! idx + let targetLname = getLoc $ lnames !! idx startLoc = srcSpanStart targetLname endLoc = srcSpanEnd targetLname startLoc' = if idx == 0 then startLoc - else srcSpanEnd . getLoc . reLoc $ lnames !! (idx - 1) + else srcSpanEnd . getLoc $ lnames !! (idx - 1) endLoc' = if idx == 0 && idx < length lnames - 1 - then srcSpanStart . getLoc . reLoc $ lnames !! (idx + 1) + then srcSpanStart . getLoc $ lnames !! (idx + 1) else endLoc in Just (mkSrcSpan startLoc' endLoc', False) findRelatedSigSpan1 _ _ = Nothing @@ -631,14 +669,19 @@ suggestDeleteUnusedBinding indexedContent name (L _ Match{m_grhss=GRHSs{grhssLocalBinds}}) = do - let go bag lsigs = - if isEmptyBag bag - then [] - else concatMap (findRelatedSpanForHsBind indexedContent name lsigs) bag + let emptyBag bag = +#if MIN_VERSION_ghc(9,11,0) + null bag +#else + isEmptyBag bag +#endif + go bag lsigs = + if emptyBag bag + then [] + else concatMap (findRelatedSpanForHsBind indexedContent name lsigs) bag case grhssLocalBinds of (HsValBinds _ (ValBinds _ bag lsigs)) -> go bag lsigs _ -> [] - findRelatedSpanForMatch _ _ _ = [] findRelatedSpanForHsBind :: PositionIndexedString @@ -743,7 +786,7 @@ suggestExportUnusedTopBinding srcOpt ParsedModule{pm_parsed_source = L _ HsModul printExport :: ExportsAs -> T.Text -> T.Text printExport ExportName x = parenthesizeIfNeeds False x - printExport ExportPattern x = "pattern " <> x + printExport ExportPattern x = "pattern " <> parenthesizeIfNeeds False x printExport ExportFamily x = parenthesizeIfNeeds True x printExport ExportAll x = parenthesizeIfNeeds True x <> "(..)" @@ -806,7 +849,6 @@ suggestAddTypeAnnotationToSatisfyConstraints sourceOpt Diagnostic{_range=_range, | otherwise = [] where makeAnnotatedLit ty lit = "(" <> lit <> " :: " <> ty <> ")" -#if MIN_VERSION_ghc(9,4,0) pat multiple at inArg inExpr = T.concat [ ".*Defaulting the type variable " , ".*to type ‘([^ ]+)’ " , "in the following constraint" @@ -817,17 +859,6 @@ suggestAddTypeAnnotationToSatisfyConstraints sourceOpt Diagnostic{_range=_range, , if inExpr then ".+In the expression" else "" , ".+In the expression" ] -#else - pat multiple at inArg inExpr = T.concat [ ".*Defaulting the following constraint" - , if multiple then "s" else "" - , " to type ‘([^ ]+)’ " - , ".*arising from the literal ‘(.+)’" - , if inArg then ".+In the.+argument" else "" - , if at then ".+at ([^ ]*)" else "" - , if inExpr then ".+In the expression" else "" - , ".+In the expression" - ] -#endif codeEdit range ty lit replacement = let title = "Add type annotation ‘" <> ty <> "’ to ‘" <> lit <> "’" edits = [TextEdit range replacement] @@ -921,7 +952,7 @@ suggestModuleTypo Diagnostic{_range=_range,..} | "Could not find module" `T.isInfixOf` _message = case T.splitOn "Perhaps you meant" _message of [_, stuff] -> - [ ("replace with " <> modul, TextEdit _range modul) + [ ("Replace with " <> modul, TextEdit _range modul) | modul <- mapMaybe extractModule (T.lines stuff) ] _ -> [] @@ -935,7 +966,11 @@ suggestExtendImport :: ExportsMap -> ParsedSource -> Diagnostic -> [(T.Text, Cod suggestExtendImport exportsMap (L _ HsModule {hsmodImports}) Diagnostic{_range=_range,..} | Just [binding, mod, srcspan] <- matchRegexUnifySpaces _message - "Perhaps you want to add ‘([^’]*)’ to the import list in the import of ‘([^’]*)’ *\\((.*)\\)." +#if MIN_VERSION_ghc(9,7,0) + "Add ‘([^’]*)’ to the import list in the import of ‘([^’]*)’ *\\(at (.*)\\)\\." +#else + "Perhaps you want to add ‘([^’]*)’ to the import list in the import of ‘([^’]*)’ *\\((.*)\\)\\." +#endif = suggestions hsmodImports binding mod srcspan | Just (binding, mod_srcspan) <- matchRegExMultipleImports _message @@ -962,9 +997,13 @@ suggestExtendImport exportsMap (L _ HsModule {hsmodImports}) Diagnostic{_range=_ | otherwise = [] lookupExportMap binding mod | let em = getExportsMap exportsMap +#if MIN_VERSION_ghc(9,7,0) + match = mconcat $ lookupOccEnv_AllNameSpaces em (mkVarOrDataOcc binding) +#else match1 = lookupOccEnv em (mkVarOrDataOcc binding) match2 = lookupOccEnv em (mkTypeOcc binding) , Just match <- match1 <> match2 +#endif -- Only for the situation that data constructor name is same as type constructor name, -- let ident with parent be in front of the one without. , sortedMatch <- sortBy (\ident1 ident2 -> parent ident2 `compare` parent ident1) (Set.toList match) @@ -1007,7 +1046,7 @@ isPreludeImplicit = xopt Lang.ImplicitPrelude suggestImportDisambiguation :: DynFlags -> Maybe T.Text -> - Annotated ParsedSource -> + ParsedSource -> T.Text -> Diagnostic -> [(T.Text, [Either TextEdit Rewrite])] @@ -1023,7 +1062,7 @@ suggestImportDisambiguation df (Just txt) ps fileContents diag@Diagnostic {..} suggestions ambiguous modules (isJust local) | otherwise = [] where - L _ HsModule {hsmodImports} = astA ps + L _ HsModule {hsmodImports} = ps locDic = fmap (NE.fromList . DL.toList) $ @@ -1043,7 +1082,7 @@ suggestImportDisambiguation df (Just txt) ps fileContents diag@Diagnostic {..} parensed = "(" `T.isPrefixOf` T.strip (textInRange _range txt) -- > removeAllDuplicates [1, 1, 2, 3, 2] = [3] - removeAllDuplicates = map head . filter ((==1) <$> length) . group . sort + removeAllDuplicates = map NE.head . filter ((==1) . length) . NE.group . sort hasDuplicate xs = length xs /= length (S.fromList xs) suggestions symbol mods local | hasDuplicate mods = case mapM toModuleTarget (removeAllDuplicates mods) of @@ -1077,11 +1116,14 @@ suggestImportDisambiguation df (Just txt) ps fileContents diag@Diagnostic {..} _ -> False ] ++ [HideOthers restImports | not (null restImports)] - ] ++ [ ( renderUniquify mode T.empty symbol True - , disambiguateSymbol ps fileContents diag symbol mode - ) | local, not (null targetsWithRestImports) - , let mode = HideOthers (uncurry (:) (head targetsWithRestImports)) - ] + ] ++ case targetsWithRestImports of + (m,ms):_ | local -> + let mode = HideOthers (m:ms) + in [( renderUniquify mode T.empty symbol True + , disambiguateSymbol ps fileContents diag symbol mode + )] + _ -> [] + renderUniquify HideOthers {} modName symbol local = "Use " <> (if local then "local definition" else modName) <> " for " <> symbol <> ", hiding other imports" renderUniquify (ToQualified _ qual) _ symbol _ = @@ -1096,17 +1138,10 @@ occursUnqualified symbol ImportDecl{..} | isNothing ideclAs = Just False /= -- I don't find this particularly comprehensible, -- but HLint suggested me to do so... -#if MIN_VERSION_ghc(9,5,0) (ideclImportList <&> \(isHiding, L _ ents) -> let occurs = any ((symbol `symbolOccursIn`) . unLoc) ents in (isHiding == EverythingBut) && not occurs || (isHiding == Exactly) && occurs ) -#else - (ideclHiding <&> \(isHiding, L _ ents) -> - let occurs = any ((symbol `symbolOccursIn`) . unLoc) ents - in isHiding && not occurs || not isHiding && occurs - ) -#endif occursUnqualified _ _ = False symbolOccursIn :: T.Text -> IE GhcPs -> Bool @@ -1116,11 +1151,9 @@ targetModuleName :: ModuleTarget -> ModuleName targetModuleName ImplicitPrelude{} = mkModuleName "Prelude" targetModuleName (ExistingImp (L _ ImportDecl{..} :| _)) = unLoc ideclName -targetModuleName (ExistingImp _) = - error "Cannot happen!" disambiguateSymbol :: - Annotated ParsedSource -> + ParsedSource -> T.Text -> Diagnostic -> T.Text -> @@ -1162,14 +1195,25 @@ suggestFixConstructorImport Diagnostic{_range=_range,..} -- import Data.Aeson.Types( Result( Success ) ) -- or -- import Data.Aeson.Types( Result(..) ) (lsp-ui) + -- + -- On 9.8+ + -- + -- In the import of ‘ModuleA’: + -- an item called ‘Constructor’ + -- is exported, but it is a data constructor of + -- ‘A’. | Just [constructor, typ] <- matchRegexUnifySpaces _message +#if MIN_VERSION_ghc(9,7,0) + "an item called ‘([^’]*)’ is exported, but it is a data constructor of ‘([^’]*)’" +#else "‘([^’]*)’ is a data constructor of ‘([^’]*)’ To import it use" +#endif = let fixedImport = typ <> "(" <> constructor <> ")" in [("Fix import of " <> fixedImport, TextEdit _range fixedImport)] | otherwise = [] -suggestAddRecordFieldImport :: ExportsMap -> DynFlags -> Annotated ParsedSource -> T.Text -> Diagnostic -> [(T.Text, CodeActionKind, TextEdit)] +suggestAddRecordFieldImport :: ExportsMap -> DynFlags -> ParsedSource -> T.Text -> Diagnostic -> [(T.Text, CodeActionKind, TextEdit)] suggestAddRecordFieldImport exportsMap df ps fileContents Diagnostic {..} | Just fieldName <- findMissingField _message , Just (range, indent) <- newImportInsertRange ps fileContents @@ -1190,11 +1234,17 @@ suggestAddRecordFieldImport exportsMap df ps fileContents Diagnostic {..} -- | Suggests a constraint for a declaration for which a constraint is missing. suggestConstraint :: DynFlags -> ParsedSource -> Diagnostic -> [(T.Text, Rewrite)] -suggestConstraint df (makeDeltaAst -> parsedModule) diag@Diagnostic {..} +suggestConstraint df ps diag@Diagnostic {..} | Just missingConstraint <- findMissingConstraint _message - = let codeAction = if _message =~ ("the type signature for:" :: String) - then suggestFunctionConstraint df parsedModule - else suggestInstanceConstraint df parsedModule + = let +#if MIN_VERSION_ghc(9,9,0) + parsedSource = ps +#else + parsedSource = makeDeltaAst ps +#endif + codeAction = if _message =~ ("the type signature for:" :: String) + then suggestFunctionConstraint df parsedSource + else suggestInstanceConstraint df parsedSource in codeAction diag missingConstraint | otherwise = [] where @@ -1272,7 +1322,7 @@ suggestImplicitParameter (L _ HsModule {hsmodDecls}) Diagnostic {_message, _rang | otherwise = [] findTypeSignatureName :: T.Text -> Maybe T.Text -findTypeSignatureName t = matchRegexUnifySpaces t "([^ ]+) :: " <&> head +findTypeSignatureName t = matchRegexUnifySpaces t "([^ ]+) :: " >>= listToMaybe -- | Suggests a constraint for a type signature with any number of existing constraints. suggestFunctionConstraint :: DynFlags -> ParsedSource -> Diagnostic -> T.Text -> [(T.Text, Rewrite)] @@ -1313,7 +1363,11 @@ suggestFunctionConstraint df (L _ HsModule {hsmodDecls}) Diagnostic {..} missing -- | Suggests the removal of a redundant constraint for a type signature. removeRedundantConstraints :: DynFlags -> ParsedSource -> Diagnostic -> [(T.Text, Rewrite)] +#if MIN_VERSION_ghc(9,9,0) +removeRedundantConstraints df (L _ HsModule {hsmodDecls}) Diagnostic{..} +#else removeRedundantConstraints df (makeDeltaAst -> L _ HsModule {hsmodDecls}) Diagnostic{..} +#endif -- • Redundant constraint: Eq a -- • In the type signature for: -- foo :: forall a. Eq a => a -> a @@ -1360,7 +1414,8 @@ removeRedundantConstraints df (makeDeltaAst -> L _ HsModule {hsmodDecls}) Diagno & take 2 & mapMaybe ((`matchRegexUnifySpaces` "Redundant constraints?: (.+)") . T.strip) & listToMaybe - <&> (head >>> parseConstraints) + >>= listToMaybe + <&> parseConstraints formatConstraints :: [T.Text] -> T.Text formatConstraints [] = "" @@ -1377,7 +1432,7 @@ removeRedundantConstraints df (makeDeltaAst -> L _ HsModule {hsmodDecls}) Diagno ------------------------------------------------------------------------------------------------- -suggestNewOrExtendImportForClassMethod :: ExportsMap -> Annotated ParsedSource -> T.Text -> Diagnostic -> [(T.Text, CodeActionKind, [Either TextEdit Rewrite])] +suggestNewOrExtendImportForClassMethod :: ExportsMap -> ParsedSource -> T.Text -> Diagnostic -> [(T.Text, CodeActionKind, [Either TextEdit Rewrite])] suggestNewOrExtendImportForClassMethod packageExportsMap ps fileContents Diagnostic {_message} | Just [methodName, className] <- matchRegexUnifySpaces @@ -1391,7 +1446,7 @@ suggestNewOrExtendImportForClassMethod packageExportsMap ps fileContents Diagnos where suggest identInfo | importStyle <- NE.toList $ importStyles identInfo, - mImportDecl <- findImportDeclByModuleName (hsmodImports . unLoc . astA $ ps) (T.unpack moduleText) = + mImportDecl <- findImportDeclByModuleName (hsmodImports . unLoc $ ps) (T.unpack moduleText) = case mImportDecl of -- extend Just decl -> @@ -1414,7 +1469,7 @@ suggestNewOrExtendImportForClassMethod packageExportsMap ps fileContents Diagnos | otherwise -> [] where moduleText = moduleNameText identInfo -suggestNewImport :: DynFlags -> ExportsMap -> Annotated ParsedSource -> T.Text -> Diagnostic -> [(T.Text, CodeActionKind, TextEdit)] +suggestNewImport :: DynFlags -> ExportsMap -> ParsedSource -> T.Text -> Diagnostic -> [(T.Text, CodeActionKind, TextEdit)] suggestNewImport df packageExportsMap ps fileContents Diagnostic{..} | msg <- unifySpaces _message , Just thingMissing <- extractNotInScopeName msg @@ -1424,91 +1479,40 @@ suggestNewImport df packageExportsMap ps fileContents Diagnostic{..} >>= (findImportDeclByModuleName hsmodImports . T.unpack) >>= ideclAs . unLoc <&> T.pack . moduleNameString . unLoc - , -- tentative workaround for detecting qualification in GHC 9.4 - -- FIXME: We can delete this after dropping the support for GHC 9.4 - qualGHC94 <- - guard (ghcVersion == GHC94) - *> extractQualifiedModuleNameFromMissingName (extractTextInRange _range fileContents) , Just (range, indent) <- newImportInsertRange ps fileContents , extendImportSuggestions <- matchRegexUnifySpaces msg +#if MIN_VERSION_ghc(9,7,0) + "Add ‘[^’]*’ to the import list in the import of ‘([^’]*)’" +#else "Perhaps you want to add ‘[^’]*’ to the import list in the import of ‘([^’]*)’" +#endif = let qis = qualifiedImportStyle df - -- FIXME: we can use thingMissing once the support for GHC 9.4 is dropped. - -- In what fllows, @missing@ is assumed to be qualified name. - -- @thingMissing@ is already as desired with GHC != 9.4. - -- In GHC 9.4, however, GHC drops a module qualifier from a qualified symbol. - -- Thus we need to explicitly concatenate qualifier explicity in GHC 9.4. - missing - | GHC94 <- ghcVersion - , isNothing (qual <|> qual') - , Just q <- qualGHC94 = - qualify q thingMissing - | otherwise = thingMissing suggestions = nubSortBy simpleCompareImportSuggestion - (constructNewImportSuggestions packageExportsMap (qual <|> qual' <|> qualGHC94, missing) extendImportSuggestions qis) in + (constructNewImportSuggestions packageExportsMap (qual <|> qual', thingMissing) extendImportSuggestions qis) in map (\(ImportSuggestion _ kind (unNewImport -> imp)) -> (imp, kind, TextEdit range (imp <> "\n" <> T.replicate indent " "))) suggestions where - qualify q (NotInScopeDataConstructor d) = NotInScopeDataConstructor (q <> "." <> d) - qualify q (NotInScopeTypeConstructorOrClass d) = NotInScopeTypeConstructorOrClass (q <> "." <> d) - qualify q (NotInScopeThing d) = NotInScopeThing (q <> "." <> d) - - L _ HsModule {..} = astA ps + L _ HsModule {..} = ps suggestNewImport _ _ _ _ _ = [] -{- | -Extracts qualifier of the symbol from the missing symbol. -Input must be either a plain qualified variable or possibly-parenthesized qualified binary operator (though no strict checking is done for symbol part). -This is only needed to alleviate the issue #3473. - -FIXME: We can delete this after dropping the support for GHC 9.4 - ->>> extractQualifiedModuleNameFromMissingName "P.lookup" -Just "P" - ->>> extractQualifiedModuleNameFromMissingName "ΣP3_'.σlookup" -Just "\931P3_'" - ->>> extractQualifiedModuleNameFromMissingName "ModuleA.Gre_ekσ.goodδ" -Just "ModuleA.Gre_ek\963" - ->>> extractQualifiedModuleNameFromMissingName "(ModuleA.Gre_ekσ.+)" -Just "ModuleA.Gre_ek\963" - ->>> extractQualifiedModuleNameFromMissingName "(ModuleA.Gre_ekσ..|.)" -Just "ModuleA.Gre_ek\963" - ->>> extractQualifiedModuleNameFromMissingName "A.B.|." -Just "A.B" --} -extractQualifiedModuleNameFromMissingName :: T.Text -> Maybe T.Text -extractQualifiedModuleNameFromMissingName (T.strip -> missing) - = T.pack <$> (T.unpack missing RE.=~ qualIdentP) - where - {- - NOTE: Haskell 2010 allows /unicode/ upper & lower letters - as a module name component; otoh, regex-tdfa only allows - /ASCII/ letters to be matched with @[[:upper:]]@ and/or @[[:lower:]]@. - Hence we use regex-applicative(-text) for finer-grained predicates. - - RULES (from [Section 10 of Haskell 2010 Report](https://www.haskell.org/onlinereport/haskell2010/haskellch10.html)): - modid → {conid .} conid - conid → large {small | large | digit | ' } - small → ascSmall | uniSmall | _ - ascSmall → a | b | … | z - uniSmall → any Unicode lowercase letter - large → ascLarge | uniLarge - ascLarge → A | B | … | Z - uniLarge → any uppercase or titlecase Unicode letter - -} - - qualIdentP = parensQualOpP <|> qualVarP - parensQualOpP = RE.sym '(' *> modNameP <* RE.sym '.' <* RE.anySym <* RE.few RE.anySym <* RE.sym ')' - qualVarP = modNameP <* RE.sym '.' <* RE.some RE.anySym - conIDP = RE.withMatched $ - RE.psym isUpper - *> RE.many - (RE.psym $ \c -> c == '\'' || c == '_' || isUpper c || isLower c || isDigit c) - modNameP = fmap snd $ RE.withMatched $ conIDP `sepBy1` RE.sym '.' +-- | A Backward compatible implementation of `lookupOccEnv_AllNameSpaces` for +-- GHC <=9.6 +-- +-- It looks for a symbol name in all known namespaces, including types, +-- variables, and fieldnames. +-- +-- Note that on GHC >= 9.8, the record selectors are not in the `mkVarOrDataOcc` +-- anymore, but are in a custom namespace, see +-- https://gitlab.haskell.org/ghc/ghc/-/wikis/migration/9.8#new-namespace-for-record-fields, +-- hence we need to use this "AllNamespaces" implementation, otherwise we'll +-- miss them. +lookupOccEnvAllNamespaces :: ExportsMap -> T.Text -> [IdentInfo] +#if MIN_VERSION_ghc(9,7,0) +lookupOccEnvAllNamespaces exportsMap name = Set.toList $ mconcat (lookupOccEnv_AllNameSpaces (getExportsMap exportsMap) (mkTypeOcc name)) +#else +lookupOccEnvAllNamespaces exportsMap name = maybe [] Set.toList $ + lookupOccEnv (getExportsMap exportsMap) (mkVarOrDataOcc name) + <> lookupOccEnv (getExportsMap exportsMap) (mkTypeOcc name) -- look up the modified unknown name in the export map +#endif constructNewImportSuggestions @@ -1516,7 +1520,8 @@ constructNewImportSuggestions constructNewImportSuggestions exportsMap (qual, thingMissing) notTheseModules qis = nubOrdBy simpleCompareImportSuggestion [ suggestion | Just name <- [T.stripPrefix (maybe "" (<> ".") qual) $ notInScope thingMissing] -- strip away qualified module names from the unknown name - , identInfo <- maybe [] Set.toList $ (lookupOccEnv (getExportsMap exportsMap) (mkVarOrDataOcc name)) <> (lookupOccEnv (getExportsMap exportsMap) (mkTypeOcc name)) -- look up the modified unknown name in the export map + + , identInfo <- lookupOccEnvAllNamespaces exportsMap name -- look up the modified unknown name in the export map , canUseIdent thingMissing identInfo -- check if the identifier information retrieved can be used , moduleNameText identInfo `notElem` fromMaybe [] notTheseModules -- check if the module of the identifier is allowed , suggestion <- renderNewImport identInfo -- creates a list of import suggestions for the retrieved identifier information @@ -1563,12 +1568,12 @@ data ImportSuggestion = ImportSuggestion !Int !CodeActionKind !NewImport -- which would lead to an unlawful Ord instance. simpleCompareImportSuggestion :: ImportSuggestion -> ImportSuggestion -> Ordering simpleCompareImportSuggestion (ImportSuggestion s1 _ i1) (ImportSuggestion s2 _ i2) - = flip compare s1 s2 <> compare i1 i2 + = compare s2 s1 <> compare i1 i2 newtype NewImport = NewImport {unNewImport :: T.Text} deriving (Show, Eq, Ord) -newImportToEdit :: NewImport -> Annotated ParsedSource -> T.Text -> Maybe (T.Text, TextEdit) +newImportToEdit :: NewImport -> ParsedSource -> T.Text -> Maybe (T.Text, TextEdit) newImportToEdit (unNewImport -> imp) ps fileContents | Just (range, indent) <- newImportInsertRange ps fileContents = Just (imp, TextEdit range (imp <> "\n" <> T.replicate indent " ")) @@ -1582,86 +1587,119 @@ newImportToEdit (unNewImport -> imp) ps fileContents -- * If the file has neither existing imports nor a module declaration, -- the import will be inserted at line zero if there are no pragmas, -- * otherwise inserted one line after the last file-header pragma -newImportInsertRange :: Annotated ParsedSource -> T.Text -> Maybe (Range, Int) +newImportInsertRange :: ParsedSource -> T.Text -> Maybe (Range, Int) newImportInsertRange ps fileContents | Just ((l, c), col) <- case hsmodImports of -- When there is no existing imports, we only cares about the line number, setting column and indent to zero. [] -> (\line -> ((line, 0), 0)) <$> findPositionNoImports ps fileContents - _ -> findPositionFromImports (map reLoc hsmodImports) last + _ -> findPositionFromImports hsmodImports last , let insertPos = Position (fromIntegral l) (fromIntegral c) = Just (Range insertPos insertPos, col) | otherwise = Nothing where - L _ HsModule {..} = astA ps + L _ HsModule {..} = ps -- | Find the position for a new import when there isn't an existing one. -- * If there is a module declaration, a new import should be inserted under the module declaration (including exports list) -- * Otherwise, a new import should be inserted after any file-header pragma. -findPositionNoImports :: Annotated ParsedSource -> T.Text -> Maybe Int +findPositionNoImports :: ParsedSource -> T.Text -> Maybe Int findPositionNoImports ps fileContents = maybe (Just (findNextPragmaPosition fileContents)) (findPositionAfterModuleName ps) hsmodName where - L _ HsModule {..} = astA ps + L _ HsModule {..} = ps -- | find line number right after module ... where -findPositionAfterModuleName :: Annotated ParsedSource +findPositionAfterModuleName :: ParsedSource -> LocatedA ModuleName -> Maybe Int -findPositionAfterModuleName ps hsmodName' = do +findPositionAfterModuleName ps _hsmodName' = do -- Note that 'where' keyword and comments are not part of the AST. They belongs to -- the exact-print information. To locate it, we need to find the previous AST node, -- calculate the gap between it and 'where', then add them up to produce the absolute -- position of 'where'. lineOffset <- whereKeywordLineOffset -- Calculate the gap before 'where' keyword. +#if MIN_VERSION_ghc(9,9,0) + pure lineOffset +#else + -- The last AST node before 'where' keyword. Might be module name or export list. + let prevSrcSpan = maybe (getLoc _hsmodName') getLoc hsmodExports case prevSrcSpan of UnhelpfulSpan _ -> Nothing (RealSrcSpan prevSrcSpan' _) -> -- add them up produce the absolute location of 'where' keyword Just $ srcLocLine (realSrcSpanEnd prevSrcSpan') + lineOffset +#endif where - L _ HsModule {..} = astA ps - - -- The last AST node before 'where' keyword. Might be module name or export list. - prevSrcSpan = maybe (getLoc hsmodName') getLoc hsmodExports + L _ HsModule {..} = ps -- The relative position of 'where' keyword (in lines, relative to the previous AST node). -- The exact-print API changed a lot in ghc-9.2, so we need to handle it separately for different compiler versions. whereKeywordLineOffset :: Maybe Int -#if MIN_VERSION_ghc(9,5,0) whereKeywordLineOffset = case hsmodAnn hsmodExt of -#else - whereKeywordLineOffset = case hsmodAnn of -#endif EpAnn _ annsModule _ -> do -- Find the first 'where' - whereLocation <- fmap NE.head . NE.nonEmpty . mapMaybe filterWhere . am_main $ annsModule +#if MIN_VERSION_ghc(9,11,0) + whereLocation <- filterWhere $ am_where annsModule +#else + whereLocation <- listToMaybe . mapMaybe filterWhere $ am_main annsModule +#endif epaLocationToLine whereLocation +#if !MIN_VERSION_ghc(9,9,0) EpAnnNotUsed -> Nothing +#endif +#if MIN_VERSION_ghc(9,11,0) + filterWhere (EpTok loc) = Just loc + filterWhere _ = Nothing +#else filterWhere (AddEpAnn AnnWhere loc) = Just loc filterWhere _ = Nothing +#endif epaLocationToLine :: EpaLocation -> Maybe Int -#if MIN_VERSION_ghc(9,5,0) - epaLocationToLine (EpaSpan sp _) - = Just . srcLocLine . realSrcSpanEnd $ sp -#else +#if MIN_VERSION_ghc(9,9,0) epaLocationToLine (EpaSpan sp) + = fmap (srcLocLine . realSrcSpanEnd) $ srcSpanToRealSrcSpan sp +#else + epaLocationToLine (EpaSpan sp _) = Just . srcLocLine . realSrcSpanEnd $ sp #endif +#if MIN_VERSION_ghc(9,11,0) + epaLocationToLine (EpaDelta _ (SameLine _) priorComments) = Just $ sumCommentsOffset priorComments + -- 'priorComments' contains the comments right before the current EpaLocation + -- Summing line offset of priorComments is necessary, as 'line' is the gap between the last comment and + -- the current AST node + epaLocationToLine (EpaDelta _ (DifferentLine line _) priorComments) = Just (line + sumCommentsOffset priorComments) +#else epaLocationToLine (EpaDelta (SameLine _) priorComments) = Just $ sumCommentsOffset priorComments -- 'priorComments' contains the comments right before the current EpaLocation -- Summing line offset of priorComments is necessary, as 'line' is the gap between the last comment and -- the current AST node epaLocationToLine (EpaDelta (DifferentLine line _) priorComments) = Just (line + sumCommentsOffset priorComments) - +#endif sumCommentsOffset :: [LEpaComment] -> Int +#if MIN_VERSION_ghc(9,9,0) + sumCommentsOffset = sum . fmap (\(L anchor _) -> anchorOpLine anchor) +#else sumCommentsOffset = sum . fmap (\(L anchor _) -> anchorOpLine (anchor_op anchor)) +#endif +#if MIN_VERSION_ghc(9,11,0) + anchorOpLine :: EpaLocation' a -> Int + anchorOpLine EpaSpan{} = 0 + anchorOpLine (EpaDelta _ (SameLine _) _) = 0 + anchorOpLine (EpaDelta _ (DifferentLine line _) _) = line +#elif MIN_VERSION_ghc(9,9,0) + anchorOpLine :: EpaLocation' a -> Int + anchorOpLine EpaSpan{} = 0 + anchorOpLine (EpaDelta (SameLine _) _) = 0 + anchorOpLine (EpaDelta (DifferentLine line _) _) = line +#else anchorOpLine :: AnchorOperation -> Int anchorOpLine UnchangedAnchor = 0 anchorOpLine (MovedAnchor (SameLine _)) = 0 anchorOpLine (MovedAnchor (DifferentLine line _)) = line +#endif findPositionFromImports :: HasSrcSpan a => t -> (t -> a) -> Maybe ((Int, Int), Int) findPositionFromImports hsField f = case getLoc (f hsField) of @@ -1744,7 +1782,7 @@ data NotInScope = NotInScopeDataConstructor T.Text | NotInScopeTypeConstructorOrClass T.Text | NotInScopeThing T.Text - deriving Show + deriving (Show, Eq) notInScope :: NotInScope -> T.Text notInScope (NotInScopeDataConstructor t) = t @@ -1759,6 +1797,38 @@ extractNotInScopeName x = Just $ NotInScopeDataConstructor name | Just [name] <- matchRegexUnifySpaces x "ot in scope: type constructor or class [^‘]*‘([^’]*)’" = Just $ NotInScopeTypeConstructorOrClass name + | Just [name] <- matchRegexUnifySpaces x "The data constructors of ‘([^ ]+)’ are not all in scope" + = Just $ NotInScopeDataConstructor name + | Just [name] <- matchRegexUnifySpaces x "of newtype ‘([^’]*)’ is not in scope" + = Just $ NotInScopeThing name + -- Match for HasField "foo" Bar String in the context where, e.g. x.foo is + -- used, and x :: Bar. + -- + -- This usually mean that the field is not in scope and the correct fix is to + -- import (Bar(foo)) or (Bar(..)). + -- + -- However, it is more reliable to match for the type name instead of the field + -- name, and most of the time you'll want to import the complete type with all + -- their fields instead of the specific field. + -- + -- The regex is convoluted because it accounts for: + -- + -- - Qualified (or not) `HasField` + -- - The type bar is always qualified. If it is unqualified, it means that the + -- parent module is already imported, and in this context it uses an hint + -- already available in the GHC error message. However this regex accounts for + -- qualified or not, it does not cost much and should be more robust if the + -- hint changes in the future + -- - Next regex will account for polymorphic types, which appears as `HasField + -- "foo" (Bar Int)...`, e.g. see the parenthesis + | Just [_module, name] <- matchRegexUnifySpaces x "No instance for [‘(].*HasField \"[^\"]+\" ([^ (.]+\\.)*([^ (.]+).*[’)]" + = Just $ NotInScopeThing name + | Just [_module, name] <- matchRegexUnifySpaces x "No instance for [‘(].*HasField \"[^\"]+\" \\(([^ .]+\\.)*([^ .]+)[^)]*\\).*[’)]" + = Just $ NotInScopeThing name + -- The order of the "Not in scope" is important, for example, some of the + -- matcher may catch the "record" value instead of the value later. + | Just [name] <- matchRegexUnifySpaces x "Not in scope: record field ‘([^’]*)’" + = Just $ NotInScopeThing name | Just [name] <- matchRegexUnifySpaces x "ot in scope: \\(([^‘ ]+)\\)" = Just $ NotInScopeThing name | Just [name] <- matchRegexUnifySpaces x "ot in scope: ([^‘ ]+)" @@ -1800,14 +1870,11 @@ extractQualifiedModuleName x -- ‘Data.Functor’ nor ‘Data.Text’ exports ‘putStrLn’. extractDoesNotExportModuleName :: T.Text -> Maybe T.Text extractDoesNotExportModuleName x - | Just [m] <- -#if MIN_VERSION_ghc(9,4,0) - matchRegexUnifySpaces x "the module ‘([^’]*)’ does not export" - <|> matchRegexUnifySpaces x "nor ‘([^’]*)’ export" -#else - matchRegexUnifySpaces x "Module ‘([^’]*)’ does not export" - <|> matchRegexUnifySpaces x "nor ‘([^’]*)’ exports" -#endif + | Just [m] <- case ghcVersion of + GHC912 -> matchRegexUnifySpaces x "The module ‘([^’]*)’ does not export" + <|> matchRegexUnifySpaces x "nor ‘([^’]*)’ export" + _ -> matchRegexUnifySpaces x "the module ‘([^’]*)’ does not export" + <|> matchRegexUnifySpaces x "nor ‘([^’]*)’ export" = Just m | otherwise = Nothing @@ -1878,21 +1945,12 @@ textInRange (Range (Position (fromIntegral -> startRow) (fromIntegral -> startCo -- | Returns the ranges for a binding in an import declaration rangesForBindingImport :: ImportDecl GhcPs -> String -> [Range] -#if MIN_VERSION_ghc(9,5,0) rangesForBindingImport ImportDecl{ ideclImportList = Just (Exactly, L _ lies) } b = concatMap (mapMaybe srcSpanToRange . rangesForBinding' b') lies where b' = wrapOperatorInParens b -#else -rangesForBindingImport ImportDecl{ - ideclHiding = Just (False, L _ lies) - } b = - concatMap (mapMaybe srcSpanToRange . rangesForBinding' b') lies - where - b' = wrapOperatorInParens b -#endif rangesForBindingImport _ _ = [] wrapOperatorInParens :: String -> String @@ -1909,23 +1967,48 @@ smallerRangesForBindingExport lies b = concatMap (mapMaybe srcSpanToRange . ranges') lies where unqualify = snd . breakOnEnd "." - b' = wrapOperatorInParens . unqualify $ b - ranges' (L _ (IEThingWith _ thing _ inners)) + b' = wrapOperatorInParens $ unqualify b + ranges' + ( L + _ + ( IEThingWith + _ + thing + _ + inners +#if MIN_VERSION_ghc(9,9,0) + _ +#endif + ) + ) | T.unpack (printOutputable thing) == b' = [] | otherwise = [ locA l' | L l' x <- inners, T.unpack (printOutputable x) == b'] ranges' _ = [] rangesForBinding' :: String -> LIE GhcPs -> [SrcSpan] +#if MIN_VERSION_ghc(9,9,0) +rangesForBinding' b (L (locA -> l) (IEVar _ nm _)) +#else rangesForBinding' b (L (locA -> l) (IEVar _ nm)) +#endif | L _ (IEPattern _ (L _ b')) <- nm , T.unpack (printOutputable b') == b = [l] rangesForBinding' b (L (locA -> l) x@IEVar{}) | T.unpack (printOutputable x) == b = [l] rangesForBinding' b (L (locA -> l) x@IEThingAbs{}) | T.unpack (printOutputable x) == b = [l] -rangesForBinding' b (L (locA -> l) (IEThingAll _ x)) | T.unpack (printOutputable x) == b = [l] +#if MIN_VERSION_ghc(9,9,0) +rangesForBinding' b (L (locA -> l) (IEThingAll _ x _)) +#else +rangesForBinding' b (L (locA -> l) (IEThingAll _ x)) +#endif + | T.unpack (printOutputable x) == b = [l] +#if MIN_VERSION_ghc(9,9,0) +rangesForBinding' b (L (locA -> l) (IEThingWith _ thing _ inners _)) +#else rangesForBinding' b (L (locA -> l) (IEThingWith _ thing _ inners)) +#endif | T.unpack (printOutputable thing) == b = [l] | otherwise = [ locA l' | L l' x <- inners, T.unpack (printOutputable x) == b] @@ -1949,30 +2032,35 @@ regexSingleMatch msg regex = case matchRegexUnifySpaces msg regex of Just (h:_) -> Just h _ -> Nothing --- | Parses tuples like (‘Data.Map’, (app/ModuleB.hs:2:1-18)) and --- | return (Data.Map, app/ModuleB.hs:2:1-18) -regExPair :: (T.Text, T.Text) -> Maybe (T.Text, T.Text) -regExPair (modname, srcpair) = do - x <- regexSingleMatch modname "‘([^’]*)’" - y <- regexSingleMatch srcpair "\\((.*)\\)" - return (x, y) - -- | Process a list of (module_name, filename:src_span) values --- | Eg. [(Data.Map, app/ModuleB.hs:2:1-18), (Data.HashMap.Strict, app/ModuleB.hs:3:1-29)] +-- +-- Eg. [(Data.Map, app/ModuleB.hs:2:1-18), (Data.HashMap.Strict, app/ModuleB.hs:3:1-29)] regExImports :: T.Text -> Maybe [(T.Text, T.Text)] -regExImports msg = result - where - parts = T.words msg - isPrefix = not . T.isPrefixOf "(" - (mod, srcspan) = partition isPrefix parts - -- check we have matching pairs like (Data.Map, (app/src.hs:1:2-18)) - result = if length mod == length srcspan then - regExPair `traverse` zip mod srcspan - else Nothing +regExImports msg + | Just mods' <- allMatchRegex msg "‘([^’]*)’" + , Just srcspans' <- allMatchRegex msg + -- This regex has to be able to deal both with single-line srcpans like "(/path/to/File.hs:2:1-18)" + -- as well as multi-line srcspans like "(/path/to/File.hs:(3,1)-(5,2))" +#if MIN_VERSION_ghc(9,7,0) + "\\(at ([^:]+:[^ ]+)\\)" +#else + "\\(([^:]+:[^ ]+)\\)" +#endif + , mods <- [mod | [_,mod] <- mods'] + , srcspans <- [srcspan | [_,srcspan] <- srcspans'] + -- check we have matching pairs like (Data.Map, (app/src.hs:1:2-18)) + , let result = if length mods == length srcspans then + Just (zip mods srcspans) else Nothing + = result + | otherwise = Nothing matchRegExMultipleImports :: T.Text -> Maybe (T.Text, [(T.Text, T.Text)]) matchRegExMultipleImports message = do +#if MIN_VERSION_ghc(9,7,0) + let pat = T.pack "Add ‘([^’]*)’ to one of these import lists: *(‘.*\\))$" +#else let pat = T.pack "Perhaps you want to add ‘([^’]*)’ to one of these import lists: *(‘.*\\))$" +#endif (binding, imports) <- case matchRegexUnifySpaces message pat of Just [x, xs] -> Just (x, xs) _ -> Nothing diff --git a/plugins/hls-refactor-plugin/src/Development/IDE/Plugin/CodeAction/Args.hs b/plugins/hls-refactor-plugin/src/Development/IDE/Plugin/CodeAction/Args.hs index b84b4aa519..a4132dd787 100644 --- a/plugins/hls-refactor-plugin/src/Development/IDE/Plugin/CodeAction/Args.hs +++ b/plugins/hls-refactor-plugin/src/Development/IDE/Plugin/CodeAction/Args.hs @@ -19,15 +19,18 @@ import Control.Monad.Reader import Control.Monad.Trans.Maybe import Data.Either (fromRight, partitionEithers) +import Data.Functor ((<&>)) import Data.IORef.Extra import qualified Data.Map as Map -import Data.Maybe (fromMaybe) +import Data.Maybe (fromMaybe, + maybeToList) import qualified Data.Text as T +import qualified Data.Text.Utf16.Rope.Mixed as Rope import Development.IDE hiding (pluginHandlers) +import Development.IDE.Core.PluginUtils (activeDiagnosticsInRange) import Development.IDE.Core.Shake import Development.IDE.GHC.Compat -import Development.IDE.GHC.Compat.ExactPrint import Development.IDE.GHC.ExactPrint import Development.IDE.Plugin.CodeAction.ExactPrint (Rewrite, rewriteToEdit) @@ -40,7 +43,6 @@ import Ide.Plugin.Error (PluginError) import Ide.Types import Language.LSP.Protocol.Message import Language.LSP.Protocol.Types -import qualified Language.LSP.Server as LSP type CodeActionTitle = T.Text @@ -52,50 +54,51 @@ type GhcideCodeAction = ExceptT PluginError (ReaderT CodeActionArgs IO) GhcideCo ------------------------------------------------------------------------------------------------- -{-# ANN runGhcideCodeAction ("HLint: ignore Move guards forward" :: String) #-} -runGhcideCodeAction :: LSP.MonadLsp Config m => IdeState -> MessageParams Method_TextDocumentCodeAction -> GhcideCodeAction -> m GhcideCodeActionResult -runGhcideCodeAction state (CodeActionParams _ _ (TextDocumentIdentifier uri) _range CodeActionContext {_diagnostics = diags}) codeAction = do - let mbFile = toNormalizedFilePath' <$> uriToFilePath uri - runRule key = runAction ("GhcideCodeActions." <> show key) state $ runMaybeT $ MaybeT (pure mbFile) >>= MaybeT . use key - caaGhcSession <- onceIO $ runRule GhcSession - caaExportsMap <- - onceIO $ - caaGhcSession >>= \case - Just env -> do - pkgExports <- envPackageExports env - localExports <- readTVarIO (exportsMap $ shakeExtras state) - pure $ localExports <> pkgExports - _ -> pure mempty - caaIdeOptions <- onceIO $ runAction "GhcideCodeActions.getIdeOptions" state getIdeOptions - caaParsedModule <- onceIO $ runRule GetParsedModuleWithComments - caaContents <- - onceIO $ - runRule GetFileContents >>= \case - Just (_, txt) -> pure txt - _ -> pure Nothing - caaDf <- onceIO $ fmap (ms_hspp_opts . pm_mod_summary) <$> caaParsedModule - caaAnnSource <- onceIO $ runRule GetAnnotatedParsedSource - caaTmr <- onceIO $ runRule TypeCheck - caaHar <- onceIO $ runRule GetHieAst - caaBindings <- onceIO $ runRule GetBindings - caaGblSigs <- onceIO $ runRule GetGlobalBindingTypeSigs - results <- liftIO $ - - sequence - [ runReaderT (runExceptT codeAction) caa - | caaDiagnostic <- diags, - let caa = CodeActionArgs {..} - ] - let (errs, successes) = partitionEithers results - pure $ concat successes +runGhcideCodeAction :: IdeState -> MessageParams Method_TextDocumentCodeAction -> GhcideCodeAction -> HandlerM Config GhcideCodeActionResult +runGhcideCodeAction state (CodeActionParams _ _ (TextDocumentIdentifier uri) _range _) codeAction + | Just nfp <- toNormalizedFilePath' <$> uriToFilePath uri = do + let runRule key = runAction ("GhcideCodeActions." <> show key) state $ runMaybeT $ MaybeT (pure (Just nfp)) >>= MaybeT . use key + caaGhcSession <- onceIO $ runRule GhcSession + caaExportsMap <- + onceIO $ + caaGhcSession >>= \case + Just env -> do + pkgExports <- envPackageExports env + localExports <- readTVarIO (exportsMap $ shakeExtras state) + pure $ localExports <> pkgExports + _ -> pure mempty + caaIdeOptions <- onceIO $ runAction "GhcideCodeActions.getIdeOptions" state getIdeOptions + caaParsedModule <- onceIO $ runRule GetParsedModuleWithComments + caaContents <- + onceIO $ + runRule GetFileContents <&> \case + Just (_, mbContents) -> fmap Rope.toText mbContents + Nothing -> Nothing + caaDf <- onceIO $ fmap (ms_hspp_opts . pm_mod_summary) <$> caaParsedModule + caaAnnSource <- onceIO $ runRule GetAnnotatedParsedSource + caaTmr <- onceIO $ runRule TypeCheck + caaHar <- onceIO $ runRule GetHieAst + caaBindings <- onceIO $ runRule GetBindings + caaGblSigs <- onceIO $ runRule GetGlobalBindingTypeSigs + diags <- concat . maybeToList <$> activeDiagnosticsInRange (shakeExtras state) nfp _range + results <- liftIO $ + sequence + [ + runReaderT (runExceptT codeAction) CodeActionArgs {..} + | caaDiagnostic <- diags + ] + let (_errs, successes) = partitionEithers results + pure $ concat successes + | otherwise = pure [] + mkCA :: T.Text -> Maybe CodeActionKind -> Maybe Bool -> [Diagnostic] -> WorkspaceEdit -> (Command |? CodeAction) mkCA title kind isPreferred diags edit = - InR $ CodeAction title kind (Just $ diags) isPreferred Nothing (Just edit) Nothing Nothing + InR $ CodeAction title kind (Just diags) isPreferred Nothing (Just edit) Nothing Nothing -mkGhcideCAPlugin :: GhcideCodeAction -> PluginId -> PluginDescriptor IdeState -mkGhcideCAPlugin codeAction plId = - (defaultPluginDescriptor plId) +mkGhcideCAPlugin :: GhcideCodeAction -> PluginId -> T.Text -> PluginDescriptor IdeState +mkGhcideCAPlugin codeAction plId desc = + (defaultPluginDescriptor plId desc) { pluginHandlers = mkPluginHandler SMethod_TextDocumentCodeAction $ \state _ params@(CodeActionParams _ _ (TextDocumentIdentifier uri) _ CodeActionContext {_diagnostics = diags}) -> do results <- lift $ runGhcideCodeAction state params codeAction @@ -107,7 +110,7 @@ mkGhcideCAPlugin codeAction plId = ] } -mkGhcideCAsPlugin :: [GhcideCodeAction] -> PluginId -> PluginDescriptor IdeState +mkGhcideCAsPlugin :: [GhcideCodeAction] -> PluginId -> T.Text -> PluginDescriptor IdeState mkGhcideCAsPlugin codeActions = mkGhcideCAPlugin $ mconcat codeActions ------------------------------------------------------------------------------------------------- @@ -143,12 +146,12 @@ data CodeActionArgs = CodeActionArgs caaParsedModule :: IO (Maybe ParsedModule), caaContents :: IO (Maybe T.Text), caaDf :: IO (Maybe DynFlags), - caaAnnSource :: IO (Maybe (Annotated ParsedSource)), + caaAnnSource :: IO (Maybe ParsedSource), caaTmr :: IO (Maybe TcModuleResult), caaHar :: IO (Maybe HieAstResult), caaBindings :: IO (Maybe Bindings), caaGblSigs :: IO (Maybe GlobalBindingTypeSigsResult), - caaDiagnostic :: Diagnostic + caaDiagnostic :: FileDiagnostic } -- | There's no concurrency in each provider, @@ -217,17 +220,7 @@ toCodeAction3 get f = ExceptT . ReaderT $ \caa -> get caa >>= flip runReaderT ca -- | this instance returns a delta AST, useful for exactprint transforms instance ToCodeAction r => ToCodeAction (ParsedSource -> r) where -#if !MIN_VERSION_ghc(9,3,0) - toCodeAction f = ExceptT . ReaderT $ \caa@CodeActionArgs {caaAnnSource = x} -> - x >>= \case - Just s -> flip runReaderT caa . runExceptT . toCodeAction . f . astA $ s - _ -> pure $ Right [] -#else - toCodeAction f = ExceptT . ReaderT $ \caa@CodeActionArgs {caaParsedModule = x} -> - x >>= \case - Just s -> flip runReaderT caa . runExceptT . toCodeAction . f . pm_parsed_source $ s - _ -> pure $ Right [] -#endif + toCodeAction = toCodeAction2 caaAnnSource instance ToCodeAction r => ToCodeAction (ExportsMap -> r) where toCodeAction = toCodeAction3 caaExportsMap @@ -236,6 +229,9 @@ instance ToCodeAction r => ToCodeAction (IdeOptions -> r) where toCodeAction = toCodeAction3 caaIdeOptions instance ToCodeAction r => ToCodeAction (Diagnostic -> r) where + toCodeAction f = ExceptT . ReaderT $ \caa@CodeActionArgs {caaDiagnostic = x} -> flip runReaderT caa . runExceptT . toCodeAction $ f (fdLspDiagnostic x) + +instance ToCodeAction r => ToCodeAction (FileDiagnostic -> r) where toCodeAction f = ExceptT . ReaderT $ \caa@CodeActionArgs {caaDiagnostic = x} -> flip runReaderT caa . runExceptT . toCodeAction $ f x instance ToCodeAction r => ToCodeAction (Maybe ParsedModule -> r) where @@ -256,12 +252,9 @@ instance ToCodeAction r => ToCodeAction (Maybe DynFlags -> r) where instance ToCodeAction r => ToCodeAction (DynFlags -> r) where toCodeAction = toCodeAction2 caaDf -instance ToCodeAction r => ToCodeAction (Maybe (Annotated ParsedSource) -> r) where +instance ToCodeAction r => ToCodeAction (Maybe ParsedSource -> r) where toCodeAction = toCodeAction1 caaAnnSource -instance ToCodeAction r => ToCodeAction (Annotated ParsedSource -> r) where - toCodeAction = toCodeAction2 caaAnnSource - instance ToCodeAction r => ToCodeAction (Maybe TcModuleResult -> r) where toCodeAction = toCodeAction1 caaTmr diff --git a/plugins/hls-refactor-plugin/src/Development/IDE/Plugin/CodeAction/ExactPrint.hs b/plugins/hls-refactor-plugin/src/Development/IDE/Plugin/CodeAction/ExactPrint.hs index 4c07354295..0f48a3a649 100644 --- a/plugins/hls-refactor-plugin/src/Development/IDE/Plugin/CodeAction/ExactPrint.hs +++ b/plugins/hls-refactor-plugin/src/Development/IDE/Plugin/CodeAction/ExactPrint.hs @@ -1,5 +1,6 @@ -{-# LANGUAGE TypeFamilies #-} +{-# LANGUAGE CPP #-} {-# LANGUAGE GADTs #-} +{-# LANGUAGE TypeFamilies #-} module Development.IDE.Plugin.CodeAction.ExactPrint ( Rewrite (..), rewriteToEdit, @@ -17,36 +18,52 @@ module Development.IDE.Plugin.CodeAction.ExactPrint ( import Control.Monad import Control.Monad.Trans -import Data.Char (isAlphaNum) -import Data.Data (Data) -import Data.Generics (listify) -import qualified Data.Text as T -import Development.IDE.GHC.Compat hiding (Annotation) +import Data.Char (isAlphaNum) +import Data.Data (Data) +import Data.Generics (listify) +import qualified Data.Text as T +import Development.IDE.GHC.Compat hiding (Annotation) import Development.IDE.GHC.Error import Development.IDE.GHC.ExactPrint import Development.IDE.GHC.Util import Development.IDE.Spans.Common -import GHC.Exts (IsList (fromList)) -import GHC.Stack (HasCallStack) +import GHC.Exts (IsList (fromList)) +import GHC.Stack (HasCallStack) import Language.Haskell.GHC.ExactPrint import Language.LSP.Protocol.Types +import Control.Lens (_head, _last, over) +import Data.Bifunctor (first) +import Data.Maybe (fromMaybe, mapMaybe) import Development.IDE.Plugin.CodeAction.Util +import GHC (AnnContext (..), + AnnList (..), + DeltaPos (SameLine), + EpAnn (..), + IsUnicodeSyntax (NormalSyntax), + NameAdornment (NameParens), + TrailingAnn (AddCommaAnn), + emptyComments, reAnnL) + + +-- See Note [Guidelines For Using CPP In GHCIDE Import Statements] + +#if MIN_VERSION_ghc(9,11,0) +import GHC (EpToken (..) + , AnnListBrackets (..) + , EpUniToken (..)) +#else +import GHC (AddEpAnn (..), + AnnParen (..)) +#endif +#if !MIN_VERSION_ghc(9,9,0) +import Data.Default (Default (..)) +import GHC (addAnns, ann) +#endif --- GHC version specific imports. For any supported GHC version, make sure there is no warning in imports. -import Control.Lens (_head, _last, over) -import Data.Bifunctor (first) -import Data.Default (Default (..)) -import Data.Maybe (fromJust, fromMaybe, mapMaybe) -import GHC (AddEpAnn (..), AnnContext (..), AnnList (..), - AnnParen (..), DeltaPos (SameLine), EpAnn (..), - EpaLocation (EpaDelta), - IsUnicodeSyntax (NormalSyntax), - NameAdornment (NameParens), - TrailingAnn (AddCommaAnn), addAnns, ann, - emptyComments, noSrcSpanA, reAnnL) -import Language.Haskell.GHC.ExactPrint.ExactPrint (makeDeltaAst, showAst) - +#if MIN_VERSION_ghc(9,9,0) +import GHC (NoAnn (..)) +#endif ------------------------------------------------------------------------------ @@ -64,9 +81,13 @@ data Rewrite where ------------------------------------------------------------------------------ class ResetEntryDP ann where resetEntryDP :: GenLocated ann ast -> GenLocated ann ast +#if MIN_VERSION_ghc(9,9,0) +instance {-# OVERLAPPING #-} NoAnn an => ResetEntryDP (EpAnn an) where + resetEntryDP (L srcAnn x) = setEntryDP (L srcAnn{anns=noAnn} x) (SameLine 0) +#else instance {-# OVERLAPPING #-} Default an => ResetEntryDP (SrcAnn an) where - -- resetEntryDP = flip setEntryDP (SameLine 0) resetEntryDP (L srcAnn x) = setEntryDP (L srcAnn{ann=EpAnnNotUsed} x) (SameLine 0) +#endif instance {-# OVERLAPPABLE #-} ResetEntryDP fallback where resetEntryDP = id @@ -77,15 +98,13 @@ rewriteToEdit :: HasCallStack => Either String [TextEdit] rewriteToEdit dflags (Rewrite dst f) = do - (ast, anns , _) <- runTransformT - $ do + (ast, _ , _) <- runTransformT $ do ast <- f dflags pure $ traceAst "REWRITE_result" $ resetEntryDP ast - let editMap = - [ TextEdit (fromJust $ srcSpanToRange dst) $ - T.pack $ exactPrint ast - ] - pure editMap + let edits = case srcSpanToRange dst of + Just range -> [ TextEdit range $ T.pack $ exactPrint ast ] + Nothing -> [] + pure edits -- | Convert a 'Rewrite' into a 'WorkspaceEdit' rewriteToWEdit :: DynFlags @@ -118,8 +137,8 @@ removeConstraint :: removeConstraint toRemove = go . traceAst "REMOVE_CONSTRAINT_input" where go :: LHsType GhcPs -> Rewrite -#if !MIN_VERSION_ghc(9,4,0) - go (L l it@HsQualTy{hst_ctxt = Just (L l' ctxt), hst_body}) = Rewrite (locA l) $ \_ -> do +#if MIN_VERSION_ghc(9,9,0) + go lHsType@(makeDeltaAst -> L l it@HsQualTy{hst_ctxt = L l' ctxt, hst_body}) = Rewrite (locA lHsType) $ \_ -> do #else go (L l it@HsQualTy{hst_ctxt = L l' ctxt, hst_body}) = Rewrite (locA l) $ \_ -> do #endif @@ -130,11 +149,7 @@ removeConstraint toRemove = go . traceAst "REMOVE_CONSTRAINT_input" [] -> hst_body' _ -> do let ctxt'' = over _last (first removeComma) ctxt' -#if MIN_VERSION_ghc(9,4,0) L l $ it{ hst_ctxt = L l' ctxt'' -#else - L l $ it{ hst_ctxt = Just $ L l' ctxt'' -#endif , hst_body = hst_body' } go (L _ (HsParTy _ ty)) = go ty @@ -151,25 +166,27 @@ appendConstraint :: Rewrite appendConstraint constraintT = go . traceAst "appendConstraint" where -#if MIN_VERSION_ghc(9,4,0) go (L l it@HsQualTy{hst_ctxt = L l' ctxt}) = Rewrite (locA l) $ \df -> do -#else - go (L l it@HsQualTy{hst_ctxt = Just (L l' ctxt)}) = Rewrite (locA l) $ \df -> do -#endif constraint <- liftParseAST df constraintT constraint <- pure $ setEntryDP constraint (SameLine 1) +#if MIN_VERSION_ghc(9,9,0) + let l'' = fmap (addParensToCtxt close_dp) l' +#else let l'' = (fmap.fmap) (addParensToCtxt close_dp) l' +#endif -- For singleton constraints, the close Paren DP is attached to an HsPar wrapping the constraint -- we have to reposition it manually into the AnnContext close_dp = case ctxt of +#if MIN_VERSION_ghc(9,11,0) + [L _ (HsParTy (_, (EpTok ap_close)) _)] -> Just ap_close +#elif MIN_VERSION_ghc(9,9,0) + [L _ (HsParTy AnnParen{ap_close} _)] -> Just ap_close +#else [L _ (HsParTy EpAnn{anns=AnnParen{ap_close}} _)] -> Just ap_close +#endif _ -> Nothing ctxt' = over _last (first addComma) $ map dropHsParTy ctxt -#if MIN_VERSION_ghc(9,4,0) return $ L l $ it{hst_ctxt = L l'' $ ctxt' ++ [constraint]} -#else - return $ L l $ it{hst_ctxt = Just $ L l'' $ ctxt' ++ [constraint]} -#endif go (L _ HsForAllTy{hst_body}) = go hst_body go (L _ (HsParTy _ ty)) = go ty go ast@(L l _) = Rewrite (locA l) $ \df -> do @@ -177,14 +194,14 @@ appendConstraint constraintT = go . traceAst "appendConstraint" constraint <- liftParseAST df constraintT lContext <- uniqueSrcSpanT lTop <- uniqueSrcSpanT -#if MIN_VERSION_ghc(9,4,0) let context = reAnnL annCtxt emptyComments $ L lContext [resetEntryDP constraint] +#if MIN_VERSION_ghc(9,11,0) + annCtxt = AnnContext (Just (EpUniTok (epl 1) NormalSyntax)) [EpTok (epl 0) | needsParens] [EpTok (epl 0) | needsParens] #else - let context = Just $ reAnnL annCtxt emptyComments $ L lContext [resetEntryDP constraint] -#endif annCtxt = AnnContext (Just (NormalSyntax, epl 1)) [epl 0 | needsParens] [epl 0 | needsParens] +#endif needsParens = hsTypeNeedsParens sigPrec $ unLoc constraint - ast <- pure $ setEntryDP ast (SameLine 1) + ast <- pure $ setEntryDP (makeDeltaAst ast) (SameLine 1) return $ reLocA $ L lTop $ HsQualTy noExtField context ast @@ -204,10 +221,6 @@ lastMaybe :: [a] -> Maybe a lastMaybe [] = Nothing lastMaybe other = Just $ last other -liftMaybe :: String -> Maybe a -> TransformT (Either String) a -liftMaybe _ (Just x) = return x -liftMaybe s _ = TransformT $ lift $ Left s - ------------------------------------------------------------------------------ extendImport :: Maybe String -> String -> LImportDecl GhcPs -> Rewrite extendImport mparent identifier lDecl@(L l _) = @@ -233,12 +246,8 @@ extendImportTopLevel :: LImportDecl GhcPs -> TransformT (Either String) (LImportDecl GhcPs) extendImportTopLevel thing (L l it@ImportDecl{..}) -#if MIN_VERSION_ghc(9,5,0) | Just (hide, L l' lies) <- ideclImportList -#else - | Just (hide, L l' lies) <- ideclHiding -#endif - , hasSibling <- not $ null lies = do + = do src <- uniqueSrcSpanT top <- uniqueSrcSpanT let rdr = reLocA $ L src $ mkRdrUnqual $ mkVarOcc thing @@ -249,21 +258,24 @@ extendImportTopLevel thing (L l it@ImportDecl{..}) TransformT $ lift (Left $ thing <> " already imported") let lie = reLocA $ L src $ IEName -#if MIN_VERSION_ghc(9,5,0) noExtField -#endif rdr - x = reLocA $ L top $ IEVar noExtField lie + x = reLocA $ L top $ IEVar +#if MIN_VERSION_ghc(9,8,0) + Nothing -- no deprecated +#else + noExtField +#endif + lie +#if MIN_VERSION_ghc(9,9,0) + Nothing +#endif if x `elem` lies then TransformT $ lift (Left $ thing <> " already imported") else do let lies' = addCommaInImportList lies x -#if MIN_VERSION_ghc(9,5,0) return $ L l it{ideclImportList = Just (hide, L l' lies')} -#else - return $ L l it{ideclHiding = Just (hide, L l' lies')} -#endif extendImportTopLevel _ _ = TransformT $ lift $ Left "Unable to extend the import list" wildCardSymbol :: String @@ -293,48 +305,67 @@ extendImportViaParent :: LImportDecl GhcPs -> TransformT (Either String) (LImportDecl GhcPs) extendImportViaParent df parent child (L l it@ImportDecl{..}) -#if MIN_VERSION_ghc(9,5,0) | Just (hide, L l' lies) <- ideclImportList = go hide l' [] lies -#else - | Just (hide, L l' lies) <- ideclHiding = go hide l' [] lies -#endif where +#if MIN_VERSION_ghc(9,9,0) + go _hide _l' _pre ((L _ll' (IEThingAll _ (L _ ie) _)) : _xs) +#else go _hide _l' _pre ((L _ll' (IEThingAll _ (L _ ie))) : _xs) +#endif | parent == unIEWrappedName ie = TransformT $ lift . Left $ child <> " already included in " <> parent <> " imports" - go hide l' pre (lAbs@(L ll' (IEThingAbs _ absIE@(L _ ie))) : xs) +#if MIN_VERSION_ghc(9,9,0) + go hide l' pre ((L ll' (IEThingAbs _ absIE@(L _ ie) docs)) : xs) +#else + go hide l' pre ((L ll' (IEThingAbs _ absIE@(L _ ie))) : xs) +#endif -- ThingAbs ie => ThingWith ie child | parent == unIEWrappedName ie = do srcChild <- uniqueSrcSpanT let childRdr = reLocA $ L srcChild $ mkRdrUnqual $ mkVarOcc child childLIE = reLocA $ L srcChild $ IEName -#if MIN_VERSION_ghc(9,5,0) noExtField -#endif childRdr - x :: LIE GhcPs = L ll' $ IEThingWith (addAnns mempty [AddEpAnn AnnOpenP (EpaDelta (SameLine 1) []), AddEpAnn AnnCloseP def] emptyComments) absIE NoIEWildcard [childLIE] - -#if MIN_VERSION_ghc(9,5,0) - return $ L l it{ideclImportList = Just (hide, L l' $ reverse pre ++ [x] ++ xs)} + x :: LIE GhcPs = L ll' $ IEThingWith +#if MIN_VERSION_ghc(9,11,0) + (Nothing, (EpTok d1, NoEpTok, NoEpTok, EpTok noAnn)) +#elif MIN_VERSION_ghc(9,9,0) + (Nothing, [AddEpAnn AnnOpenP d1, AddEpAnn AnnCloseP noAnn]) +#elif MIN_VERSION_ghc(9,7,0) + (Nothing, addAnns mempty [AddEpAnn AnnOpenP d1, AddEpAnn AnnCloseP def] emptyComments) #else - return $ L l it{ideclHiding = Just (hide, L l' $ reverse pre ++ [x] ++ xs)} + (addAnns mempty [AddEpAnn AnnOpenP d1, AddEpAnn AnnCloseP def] emptyComments) +#endif + absIE NoIEWildcard [childLIE] +#if MIN_VERSION_ghc(9,9,0) + docs #endif + return $ L l it{ideclImportList = Just (hide, L l' $ reverse pre ++ [x] ++ xs)} + +#if MIN_VERSION_ghc(9,9,0) + go hide l' pre ((L l'' (IEThingWith l''' twIE@(L _ ie) _ lies' docs)) : xs) +#else go hide l' pre ((L l'' (IEThingWith l''' twIE@(L _ ie) _ lies')) : xs) +#endif -- ThingWith ie lies' => ThingWith ie (lies' ++ [child]) | parent == unIEWrappedName ie , child == wildCardSymbol = do -#if MIN_VERSION_ghc(9,5,0) let it' = it{ideclImportList = Just (hide, lies)} + thing = IEThingWith newl twIE (IEWildcard 2) [] +#if MIN_VERSION_ghc(9,9,0) + docs +#endif +#if MIN_VERSION_ghc(9,7,0) && !MIN_VERSION_ghc(9,9,0) + newl = fmap (\ann -> ann ++ [AddEpAnn AnnDotdot d0]) <$> l''' +#elif MIN_VERSION_ghc(9,11,0) + newl = (\(open, _, comma, close) -> (open, EpTok d0, comma, close)) <$> l''' #else - let it' = it{ideclHiding = Just (hide, lies)} + newl = (\ann -> ann ++ [AddEpAnn AnnDotdot d0]) <$> l''' #endif - thing = IEThingWith newl twIE (IEWildcard 2) [] - newl = (\ann -> ann ++ [(AddEpAnn AnnDotdot d0)]) <$> l''' lies = L l' $ reverse pre ++ [L l'' thing] ++ xs return $ L l it' - | parent == unIEWrappedName ie - , hasSibling <- not $ null lies' = - do + | parent == unIEWrappedName ie = do + let hasSibling = not $ null lies' srcChild <- uniqueSrcSpanT let childRdr = reLocA $ L srcChild $ mkRdrUnqual $ mkVarOcc child childRdr <- pure $ setEntryDP childRdr $ SameLine $ if hasSibling then 1 else 0 @@ -345,22 +376,19 @@ extendImportViaParent df parent child (L l it@ImportDecl{..}) TransformT $ lift (Left $ child <> " already included in " <> parent <> " imports") let childLIE = reLocA $ L srcChild $ IEName -#if MIN_VERSION_ghc(9,5,0) noExtField -#endif childRdr -#if MIN_VERSION_ghc(9,5,0) let it' = it{ideclImportList = Just (hide, lies)} -#else - let it' = it{ideclHiding = Just (hide, lies)} -#endif lies = L l' $ reverse pre ++ - [L l'' (IEThingWith l''' twIE NoIEWildcard (over _last fixLast lies' ++ [childLIE]))] ++ xs + [L l'' (IEThingWith l''' twIE NoIEWildcard (over _last fixLast lies' ++ [childLIE]) +#if MIN_VERSION_ghc(9,9,0) + docs +#endif + )] ++ xs fixLast = if hasSibling then first addComma else id return $ L l it' go hide l' pre (x : xs) = go hide l' (x : pre) xs - go hide l' pre [] - | hasSibling <- not $ null pre = do + go hide l' pre [] = do -- [] => ThingWith parent [child] l'' <- uniqueSrcSpanT srcParent <- uniqueSrcSpanT @@ -368,29 +396,40 @@ extendImportViaParent df parent child (L l it@ImportDecl{..}) parentRdr <- liftParseAST df parent let childRdr = reLocA $ L srcChild $ mkRdrUnqual $ mkVarOcc child isParentOperator = hasParen parent - let parentLIE = reLocA $ L srcParent $ (if isParentOperator then IEType (epl 0) parentRdr' +#if MIN_VERSION_ghc(9,11,0) + let parentLIE = reLocA $ L srcParent $ if isParentOperator then IEType (EpTok (epl 0)) parentRdr' +#else + let parentLIE = reLocA $ L srcParent $ if isParentOperator then IEType (epl 0) parentRdr' +#endif else IEName -#if MIN_VERSION_ghc(9,5,0) noExtField -#endif - parentRdr') + parentRdr' parentRdr' = modifyAnns parentRdr $ \case +#if MIN_VERSION_ghc(9,11,0) + it@NameAnn{nann_adornment = NameParens _ _} -> it{nann_adornment=NameParens (EpTok (epl 1)) (EpTok (epl 0))} +#else it@NameAnn{nann_adornment = NameParens} -> it{nann_open = epl 1, nann_close = epl 0} +#endif other -> other childLIE = reLocA $ L srcChild $ IEName -#if MIN_VERSION_ghc(9,5,0) noExtField -#endif childRdr +#if MIN_VERSION_ghc(9,11,0) + listAnn = (Nothing, (EpTok (epl 1), NoEpTok, NoEpTok, EpTok (epl 0))) +#elif MIN_VERSION_ghc(9,9,0) + listAnn = (Nothing, [AddEpAnn AnnOpenP (epl 1), AddEpAnn AnnCloseP (epl 0)]) +#elif MIN_VERSION_ghc(9,7,0) + listAnn = (Nothing, epAnn srcParent [AddEpAnn AnnOpenP (epl 1), AddEpAnn AnnCloseP (epl 0)]) +#else listAnn = epAnn srcParent [AddEpAnn AnnOpenP (epl 1), AddEpAnn AnnCloseP (epl 0)] +#endif x :: LIE GhcPs = reLocA $ L l'' $ IEThingWith listAnn parentLIE NoIEWildcard [childLIE] +#if MIN_VERSION_ghc(9,9,0) + Nothing -- TODO preserve docs? +#endif lies' = addCommaInImportList (reverse pre) x -#if MIN_VERSION_ghc(9,5,0) return $ L l it{ideclImportList = Just (hide, L l' lies')} -#else - return $ L l it{ideclHiding = Just (hide, L l' lies')} -#endif extendImportViaParent _ _ _ _ = TransformT $ lift $ Left "Unable to extend the import list via parent" -- Add an item in an import list, taking care of adding comma if needed. @@ -410,12 +449,17 @@ addCommaInImportList lies x = -- check if there is an existing trailing comma existingTrailingComma = fromMaybe False $ do L lastItemSrcAnn _ <- lastMaybe lies +#if MIN_VERSION_ghc(9,9,0) + lastItemAnn <- case lastItemSrcAnn of + EpAnn _ lastItemAnn _ -> pure lastItemAnn +#else lastItemAnn <- case ann lastItemSrcAnn of EpAnn _ lastItemAnn _ -> pure lastItemAnn _ -> Nothing +#endif pure $ any isTrailingAnnComma (lann_trailing lastItemAnn) - hasSibling = not . null $ lies + hasSibling = not $ null lies -- Setup the new item. It should have a preceding whitespace if it has siblings, and a trailing comma if the -- preceding item already has one. @@ -426,11 +470,7 @@ addCommaInImportList lies x = fixLast :: [LocatedAn AnnListItem a] -> [LocatedAn AnnListItem a] fixLast = over _last (first (if existingTrailingComma then id else addComma)) -#if MIN_VERSION_ghc(9,5,0) unIEWrappedName :: IEWrappedName GhcPs -> String -#else -unIEWrappedName :: IEWrappedName (IdP GhcPs) -> String -#endif unIEWrappedName (occName -> occ) = T.unpack $ printOutputable $ parenSymOcc occ (ppr occ) hasParen :: String -> Bool @@ -444,19 +484,10 @@ hasParen _ = False hideSymbol :: String -> LImportDecl GhcPs -> Rewrite hideSymbol symbol lidecl@(L loc ImportDecl{..}) = -#if MIN_VERSION_ghc(9,5,0) case ideclImportList of Nothing -> Rewrite (locA loc) $ extendHiding symbol lidecl Nothing Just (EverythingBut, hides) -> Rewrite (locA loc) $ extendHiding symbol lidecl (Just hides) - Just (Exactly, imports) -> Rewrite (locA loc) $ deleteFromImport symbol lidecl imports -#else - case ideclHiding of - Nothing -> Rewrite (locA loc) $ extendHiding symbol lidecl Nothing - Just (True, hides) -> Rewrite (locA loc) $ extendHiding symbol lidecl (Just hides) - Just (False, imports) -> Rewrite (locA loc) $ deleteFromImport symbol lidecl imports -#endif -hideSymbol _ (L _ (XImportDecl _)) = - error "cannot happen" + Just (Exactly, imports) -> Rewrite (locA loc) $ deleteFromImport symbol lidecl $ setEntryDP (makeDeltaAst imports) (SameLine 1) extendHiding :: String -> @@ -467,12 +498,28 @@ extendHiding :: extendHiding symbol (L l idecls) mlies df = do L l' lies <- case mlies of Nothing -> do +#if MIN_VERSION_ghc(9,11,0) + let ann :: EpAnn (AnnList (EpToken "hiding", [EpToken ","])) + ann = noAnnSrcSpanDP0 +#elif MIN_VERSION_ghc(9,9,0) + let ann = noAnnSrcSpanDP0 +#else src <- uniqueSrcSpanT let ann = noAnnSrcSpanDP0 src +#endif +#if MIN_VERSION_ghc(9,9,0) + ann' = flip fmap ann $ \x -> x +#else ann' = flip (fmap.fmap) ann $ \x -> x +#endif +#if MIN_VERSION_ghc(9,11,0) + {al_rest = (EpTok (epl 1), [NoEpTok]) + ,al_brackets=ListParens (EpTok (epl 1)) (EpTok (epl 0)) +#else {al_rest = [AddEpAnn AnnHiding (epl 1)] ,al_open = Just $ AddEpAnn AnnOpenP (epl 1) ,al_close = Just $ AddEpAnn AnnCloseP (epl 0) +#endif } return $ L ann' [] Just pr -> pure pr @@ -482,18 +529,21 @@ extendHiding symbol (L l idecls) mlies df = do rdr <- liftParseAST df symbol rdr <- pure $ modifyAnns rdr $ addParens (isOperator $ unLoc rdr) let lie = reLocA $ L src $ IEName -#if MIN_VERSION_ghc(9,5,0) noExtField -#endif rdr - x = reLocA $ L top $ IEVar noExtField lie + x = reLocA $ L top $ IEVar +#if MIN_VERSION_ghc(9,7,0) + Nothing +#else + noExtField +#endif + lie +#if MIN_VERSION_ghc(9,9,0) + Nothing +#endif x <- pure $ if hasSibling then first addComma x else x lies <- pure $ over _head (`setEntryDP` SameLine 1) lies -#if MIN_VERSION_ghc(9,5,0) return $ L l idecls{ideclImportList = Just (EverythingBut, L l' $ x : lies)} -#else - return $ L l idecls{ideclHiding = Just (True, L l' $ x : lies)} -#endif where isOperator = not . all isAlphaNum . occNameString . rdrNameOcc @@ -503,30 +553,37 @@ deleteFromImport :: XRec GhcPs [LIE GhcPs] -> DynFlags -> TransformT (Either String) (LImportDecl GhcPs) -deleteFromImport (T.pack -> symbol) (L l idecl) llies@(L lieLoc lies) _ = do +deleteFromImport (T.pack -> symbol) (L l idecl) (L lieLoc lies) _ = do let edited = L lieLoc deletedLies lidecl' = L l $ idecl -#if MIN_VERSION_ghc(9,5,0) - { ideclImportList = Just (Exactly, edited) -#else - { ideclHiding = Just (False, edited) -#endif - } + { ideclImportList = Just (Exactly, edited) } pure lidecl' where deletedLies = over _last removeTrailingComma $ mapMaybe killLie lies killLie :: LIE GhcPs -> Maybe (LIE GhcPs) +#if MIN_VERSION_ghc(9,9,0) + killLie v@(L _ (IEVar _ (L _ (unqualIEWrapName -> nam)) _)) +#else killLie v@(L _ (IEVar _ (L _ (unqualIEWrapName -> nam)))) +#endif | nam == symbol = Nothing | otherwise = Just v +#if MIN_VERSION_ghc(9,9,0) + killLie v@(L _ (IEThingAbs _ (L _ (unqualIEWrapName -> nam)) _)) +#else killLie v@(L _ (IEThingAbs _ (L _ (unqualIEWrapName -> nam)))) +#endif | nam == symbol = Nothing | otherwise = Just v +#if MIN_VERSION_ghc(9,9,0) + killLie (L lieL (IEThingWith xt ty@(L _ (unqualIEWrapName -> nam)) wild cons docs)) +#else killLie (L lieL (IEThingWith xt ty@(L _ (unqualIEWrapName -> nam)) wild cons)) +#endif | nam == symbol = Nothing | otherwise = Just $ @@ -536,4 +593,7 @@ deleteFromImport (T.pack -> symbol) (L l idecl) llies@(L lieLoc lies) _ = do ty wild (filter ((/= symbol) . unqualIEWrapName . unLoc) cons) +#if MIN_VERSION_ghc(9,9,0) + docs +#endif killLie v = Just v diff --git a/plugins/hls-refactor-plugin/src/Development/IDE/Plugin/CodeAction/RuleTypes.hs b/plugins/hls-refactor-plugin/src/Development/IDE/Plugin/CodeAction/RuleTypes.hs index c338903d35..69f3332dc0 100644 --- a/plugins/hls-refactor-plugin/src/Development/IDE/Plugin/CodeAction/RuleTypes.hs +++ b/plugins/hls-refactor-plugin/src/Development/IDE/Plugin/CodeAction/RuleTypes.hs @@ -5,7 +5,6 @@ module Development.IDE.Plugin.CodeAction.RuleTypes import Control.DeepSeq (NFData) import Data.Hashable (Hashable) -import Data.Typeable (Typeable) import Development.IDE.Graph (RuleResult) import Development.IDE.Types.Exports import Development.IDE.Types.HscEnvEq (HscEnvEq) @@ -15,7 +14,7 @@ import GHC.Generics (Generic) type instance RuleResult PackageExports = ExportsMap newtype PackageExports = PackageExports HscEnvEq - deriving (Eq, Show, Typeable, Generic) + deriving (Eq, Show, Generic) instance Hashable PackageExports instance NFData PackageExports diff --git a/plugins/hls-refactor-plugin/src/Development/IDE/Plugin/CodeAction/Util.hs b/plugins/hls-refactor-plugin/src/Development/IDE/Plugin/CodeAction/Util.hs index 197c936165..40f3c76127 100644 --- a/plugins/hls-refactor-plugin/src/Development/IDE/Plugin/CodeAction/Util.hs +++ b/plugins/hls-refactor-plugin/src/Development/IDE/Plugin/CodeAction/Util.hs @@ -27,7 +27,7 @@ debugAST :: Bool debugAST = unsafePerformIO (getEnvDefault "GHCIDE_DEBUG_AST" "0") == "1" -- | Prints an 'Outputable' value to stderr and to an HTML file for further inspection -traceAst :: (Data a, ExactPrint a, Outputable a, HasCallStack) => String -> a -> a +traceAst :: (Data a, ExactPrint a, HasCallStack) => String -> a -> a traceAst lbl x | debugAST = trace doTrace x | otherwise = x diff --git a/plugins/hls-refactor-plugin/src/Development/IDE/Plugin/Plugins/AddArgument.hs b/plugins/hls-refactor-plugin/src/Development/IDE/Plugin/Plugins/AddArgument.hs index fcec3b2887..aec82cb17f 100644 --- a/plugins/hls-refactor-plugin/src/Development/IDE/Plugin/Plugins/AddArgument.hs +++ b/plugins/hls-refactor-plugin/src/Development/IDE/Plugin/Plugins/AddArgument.hs @@ -1,41 +1,54 @@ {-# LANGUAGE CPP #-} module Development.IDE.Plugin.Plugins.AddArgument (plugin) where -#if MIN_VERSION_ghc(9,4,0) -import Development.IDE.GHC.ExactPrint (epl) -import GHC.Parser.Annotation (TokenLocation (..)) -#endif import Control.Monad (join) import Control.Monad.Trans.Class (lift) import Data.Bifunctor (Bifunctor (..)) import Data.Either.Extra (maybeToEither) import qualified Data.Text as T import Development.IDE.GHC.Compat -import Development.IDE.GHC.Compat.ExactPrint (exactPrint, - makeDeltaAst) import Development.IDE.GHC.Error (spanContainsRange) -import Development.IDE.GHC.ExactPrint (genAnchor1, - modifyMgMatchesT', +import Development.IDE.GHC.ExactPrint (modifyMgMatchesT', modifySigWithM, modifySmallestDeclWithM) import Development.IDE.Plugin.Plugins.Diagnostic -import GHC (EpAnn (..), - SrcSpanAnn' (SrcSpanAnn), - SrcSpanAnnA, - SrcSpanAnnN, - TrailingAnn (..), - emptyComments, - noAnn) -import GHC.Hs (IsUnicodeSyntax (..)) -import GHC.Types.SrcLoc (generatedSrcSpan) +import GHC.Parser.Annotation (SrcSpanAnnA, + SrcSpanAnnN, noAnn) import Ide.Plugin.Error (PluginError (PluginInternalError)) import Ide.PluginUtils (makeDiffTextEdit) import Language.Haskell.GHC.ExactPrint (TransformT (..), + exactPrint, noAnnSrcSpanDP1, runTransformT) -import Language.Haskell.GHC.ExactPrint.Transform (d1) import Language.LSP.Protocol.Types +-- See Note [Guidelines For Using CPP In GHCIDE Import Statements] + +#if MIN_VERSION_ghc(9,6,0) && !MIN_VERSION_ghc(9,9,0) +import Development.IDE.GHC.ExactPrint (epl) +import GHC.Parser.Annotation (TokenLocation (..)) +#endif + +#if !MIN_VERSION_ghc(9,9,0) +import Development.IDE.GHC.Compat.ExactPrint (makeDeltaAst) +import Development.IDE.GHC.ExactPrint (genAnchor1) +import GHC.Parser.Annotation (EpAnn (..), + SrcSpanAnn' (..), + emptyComments) +import GHC.Types.SrcLoc (generatedSrcSpan) +#endif + +#if MIN_VERSION_ghc(9,9,0) +import GHC (DeltaPos (..), + EpUniToken (..), + IsUnicodeSyntax (NormalSyntax)) +import Language.Haskell.GHC.ExactPrint (d1, setEntryDP) +#endif + +#if MIN_VERSION_ghc(9,11,0) +import GHC.Parser.Annotation (EpToken (..)) +#endif + -- When GHC tells us that a variable is not bound, it will tell us either: -- - there is an unbound variable with a given type -- - there is an unbound variable (GHC provides no type suggestion) @@ -60,11 +73,35 @@ plugin parsedModule Diagnostic {_message, _range} -- returning how many patterns there were in this match prior to the transformation: -- addArgToMatch "foo" `bar arg1 arg2 = ...` -- => (`bar arg1 arg2 foo = ...`, 2) -addArgToMatch :: T.Text -> GenLocated l (Match GhcPs body) -> (GenLocated l (Match GhcPs body), Int) +addArgToMatch :: T.Text -> GenLocated l (Match GhcPs (LocatedA (HsExpr GhcPs))) -> (GenLocated l (Match GhcPs (LocatedA (HsExpr GhcPs))), Int) + +-- NOTE: The code duplication within CPP clauses avoids a parse error with +-- `stylish-haskell`. +#if MIN_VERSION_ghc(9,11,0) +addArgToMatch name (L locMatch (Match xMatch ctxMatch (L l pats) rhs)) = + let unqualName = mkRdrUnqual $ mkVarOcc $ T.unpack name + newPat = L noAnnSrcSpanDP1 $ VarPat NoExtField $ L noAnn unqualName + -- The intention is to move `= ...` (right-hand side with equals) to the right so there's 1 space between + -- the newly added pattern and the rest + indentRhs :: GRHSs GhcPs (LocatedA (HsExpr GhcPs)) -> GRHSs GhcPs (LocatedA (HsExpr GhcPs)) + indentRhs rhs@GRHSs{grhssGRHSs} = rhs {grhssGRHSs = fmap (`setEntryDP` (SameLine 1)) grhssGRHSs } + in (L locMatch (Match xMatch ctxMatch (L l (pats <> [newPat])) (indentRhs rhs)), Prelude.length pats) +#elif MIN_VERSION_ghc(9,9,0) +addArgToMatch name (L locMatch (Match xMatch ctxMatch pats rhs)) = + let unqualName = mkRdrUnqual $ mkVarOcc $ T.unpack name + newPat = L noAnnSrcSpanDP1 $ VarPat NoExtField $ L noAnn unqualName + -- The intention is to move `= ...` (right-hand side with equals) to the right so there's 1 space between + -- the newly added pattern and the rest + indentRhs :: GRHSs GhcPs (LocatedA (HsExpr GhcPs)) -> GRHSs GhcPs (LocatedA (HsExpr GhcPs)) + indentRhs rhs@GRHSs{grhssGRHSs} = rhs {grhssGRHSs = fmap (`setEntryDP` (SameLine 1)) grhssGRHSs } + in (L locMatch (Match xMatch ctxMatch (pats <> [newPat]) (indentRhs rhs)), Prelude.length pats) +#else addArgToMatch name (L locMatch (Match xMatch ctxMatch pats rhs)) = let unqualName = mkRdrUnqual $ mkVarOcc $ T.unpack name newPat = L (noAnnSrcSpanDP1 generatedSrcSpan) $ VarPat NoExtField (noLocA unqualName) - in (L locMatch (Match xMatch ctxMatch (pats <> [newPat]) rhs), Prelude.length pats) + indentRhs = id + in (L locMatch (Match xMatch ctxMatch (pats <> [newPat]) (indentRhs rhs)), Prelude.length pats) +#endif -- Attempt to insert a binding pattern into each match for the given LHsDecl; succeeds only if the function is a FunBind. -- Also return: @@ -101,9 +138,14 @@ appendFinalPatToMatches name = \case -- -- TODO instead of inserting a typed hole; use GHC's suggested type from the error addArgumentAction :: ParsedModule -> Range -> T.Text -> Maybe T.Text -> Either PluginError [(T.Text, [TextEdit])] -addArgumentAction (ParsedModule _ moduleSrc _ _) range name _typ = do +addArgumentAction (ParsedModule _ moduleSrc _) range name _typ = do (newSource, _, _) <- runTransformT $ do - (moduleSrc', join -> matchedDeclNameMay) <- addNameAsLastArgOfMatchingDecl (makeDeltaAst moduleSrc) + (moduleSrc', join -> matchedDeclNameMay) <- addNameAsLastArgOfMatchingDecl +#if MIN_VERSION_ghc(9,9,0) + moduleSrc +#else + (makeDeltaAst moduleSrc) +#endif case matchedDeclNameMay of Just (matchedDeclName, numPats) -> modifySigWithM (unLoc matchedDeclName) (addTyHoleToTySigArg numPats) moduleSrc' Nothing -> pure moduleSrc' @@ -132,16 +174,30 @@ hsTypeFromFunTypeAsList (args, res) = -- 0 `foo :: ()` => foo :: _ -> () -- 2 `foo :: FunctionTySyn` => foo :: FunctionTySyn -- 1 `foo :: () -> () -> Int` => foo :: () -> _ -> () -> Int -addTyHoleToTySigArg :: Int -> LHsSigType GhcPs -> (LHsSigType GhcPs) +addTyHoleToTySigArg :: Int -> LHsSigType GhcPs -> LHsSigType GhcPs addTyHoleToTySigArg loc (L annHsSig (HsSig xHsSig tyVarBndrs lsigTy)) = let (args, res) = hsTypeToFunTypeAsList lsigTy -#if MIN_VERSION_ghc(9,4,0) +#if MIN_VERSION_ghc(9,9,0) + wildCardAnn = noAnnSrcSpanDP1 + newArg = + ( noAnn + , noExtField + , HsUnrestrictedArrow (EpUniTok d1 NormalSyntax) +#if MIN_VERSION_ghc(9,11,0) + , L wildCardAnn $ HsWildCardTy NoEpTok +#else + , L wildCardAnn $ HsWildCardTy noExtField +#endif + ) +#else wildCardAnn = SrcSpanAnn (EpAnn genAnchor1 (AnnListItem []) emptyComments) generatedSrcSpan arrowAnn = TokenLoc (epl 1) - newArg = (SrcSpanAnn mempty generatedSrcSpan, noAnn, HsUnrestrictedArrow (L arrowAnn HsNormalTok), L wildCardAnn $ HsWildCardTy noExtField) -#else - wildCardAnn = SrcSpanAnn (EpAnn genAnchor1 (AnnListItem [AddRarrowAnn d1]) emptyComments) generatedSrcSpan - newArg = (SrcSpanAnn mempty generatedSrcSpan, noAnn, HsUnrestrictedArrow NormalSyntax, L wildCardAnn $ HsWildCardTy noExtField) + newArg = + ( SrcSpanAnn mempty generatedSrcSpan + , noAnn + , HsUnrestrictedArrow (L arrowAnn HsNormalTok) + , L wildCardAnn $ HsWildCardTy noExtField + ) #endif -- NOTE if the location that the argument wants to be placed at is not one more than the number of arguments -- in the signature, then we return the original type signature. @@ -152,4 +208,3 @@ addTyHoleToTySigArg loc (L annHsSig (HsSig xHsSig tyVarBndrs lsigTy)) = insertArg n (a:as) = a : insertArg (n - 1) as lsigTy' = hsTypeFromFunTypeAsList (insertArg loc args, res) in L annHsSig (HsSig xHsSig tyVarBndrs lsigTy') - diff --git a/plugins/hls-refactor-plugin/src/Development/IDE/Plugin/Plugins/Diagnostic.hs b/plugins/hls-refactor-plugin/src/Development/IDE/Plugin/Plugins/Diagnostic.hs index e99c23de98..7facc8f54c 100644 --- a/plugins/hls-refactor-plugin/src/Development/IDE/Plugin/Plugins/Diagnostic.hs +++ b/plugins/hls-refactor-plugin/src/Development/IDE/Plugin/Plugins/Diagnostic.hs @@ -21,6 +21,9 @@ matchRegex message regex = case message =~~ regex of Nothing -> Nothing -- | 'matchRegex' combined with 'unifySpaces' +-- +-- >>> matchRegexUnifySpaces "hello I'm a cow" "he(ll)o" +-- Just ["ll"] matchRegexUnifySpaces :: T.Text -> T.Text -> Maybe [T.Text] matchRegexUnifySpaces message = matchRegex (unifySpaces message) @@ -44,7 +47,10 @@ matchVariableNotInScope message | otherwise = Nothing where matchVariableNotInScopeTyped message - | Just [name, typ] <- matchRegexUnifySpaces message "Variable not in scope: ([^ ]+) :: ([^*•]+)" = + | Just [name, typ0] <- matchRegexUnifySpaces message "Variable not in scope: ([^ ]+) :: ([^*•]+)" + , -- When some name in scope is similar to not-in-scope variable, the type is followed by + -- "Suggested fix: Perhaps use ..." + typ:_ <- T.splitOn " Suggested fix:" typ0 = Just (name, typ) | otherwise = Nothing matchVariableNotInScopeUntyped message diff --git a/plugins/hls-refactor-plugin/src/Development/IDE/Plugin/Plugins/FillHole.hs b/plugins/hls-refactor-plugin/src/Development/IDE/Plugin/Plugins/FillHole.hs index 35e04af6ba..eb6172c7fa 100644 --- a/plugins/hls-refactor-plugin/src/Development/IDE/Plugin/Plugins/FillHole.hs +++ b/plugins/hls-refactor-plugin/src/Development/IDE/Plugin/Plugins/FillHole.hs @@ -29,7 +29,7 @@ suggestFillHole Diagnostic{_range=_range,..} Just (firstChr, _) -> let isInfixOperator = firstChr == '(' name' = getOperatorNotation isInfixHole isInfixOperator name in - ( "replace " <> holeName <> " with " <> name + ( "Replace " <> holeName <> " with " <> name , TextEdit _range (if parenthise then addParens name' else name') ) getOperatorNotation True False name = addBackticks name @@ -69,7 +69,8 @@ processHoleSuggestions mm = (holeSuggestions, refSuggestions) (mrAfter . (=~ t " *Valid (hole fits|substitutions) include")) validHolesSection let holeFit = T.strip $ T.takeWhile (/= ':') holeFitLine - guard (not $ T.null holeFit) + guard $ not $ holeFit =~ t "Some hole fits suppressed" + guard $ not $ T.null holeFit return holeFit refSuggestions = do -- @[] -- get the text indented under Valid refinement hole fits diff --git a/plugins/hls-refactor-plugin/test/Main.hs b/plugins/hls-refactor-plugin/test/Main.hs index db6c18a4d0..da45083a08 100644 --- a/plugins/hls-refactor-plugin/test/Main.hs +++ b/plugins/hls-refactor-plugin/test/Main.hs @@ -3,16 +3,12 @@ {-# LANGUAGE DataKinds #-} {-# LANGUAGE DuplicateRecordFields #-} {-# LANGUAGE GADTs #-} -{-# LANGUAGE ImplicitParams #-} {-# LANGUAGE LambdaCase #-} -{-# LANGUAGE MultiWayIf #-} -{-# LANGUAGE NamedFieldPuns #-} {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE PatternSynonyms #-} -{-# LANGUAGE PolyKinds #-} {-# LANGUAGE RecordWildCards #-} {-# LANGUAGE TypeOperators #-} -{-# OPTIONS_GHC -Wno-deprecations -Wno-unticked-promoted-constructors #-} +{-# OPTIONS_GHC -Wno-redundant-constraints #-} -- don't warn about usage HasCallStack module Main ( main @@ -26,19 +22,15 @@ import Data.Foldable import Data.List.Extra import Data.Maybe import qualified Data.Text as T -import Data.Tuple.Extra import Development.IDE.GHC.Util import Development.IDE.Plugin.Completions.Types (extendImportCommandId) import Development.IDE.Test import Development.IDE.Types.Location import Development.Shake (getDirectoryFilesIO) -import Ide.Types import qualified Language.LSP.Protocol.Lens as L import Language.LSP.Protocol.Message import Language.LSP.Protocol.Types hiding - (SemanticTokenAbsolute (length, line), - SemanticTokenRelative (length), - SemanticTokensEdit (_start), + (SemanticTokensEdit (_start), mkRange) import Language.LSP.Test import System.Directory @@ -47,7 +39,6 @@ import qualified System.IO.Extra import System.IO.Extra hiding (withTempDir) import System.Time.Extra import Test.Tasty -import Test.Tasty.ExpectedFailure import Test.Tasty.HUnit import Text.Regex.TDFA ((=~)) @@ -55,25 +46,22 @@ import Text.Regex.TDFA ((=~)) import Development.IDE.Plugin.CodeAction (matchRegExMultipleImports) import Test.Hls -import Control.Applicative (liftA2) +import qualified Development.IDE.GHC.ExactPrint +import Development.IDE.Plugin.CodeAction (NotInScope (..)) import qualified Development.IDE.Plugin.CodeAction as Refactor -import qualified Development.IDE.Plugin.HLS.GhcIde as GhcIde import qualified Test.AddArgument main :: IO () main = defaultTestRunner tests -refactorPlugin :: IO (IdePlugins IdeState) +refactorPlugin :: PluginTestDescriptor Development.IDE.GHC.ExactPrint.Log refactorPlugin = do - exactprintLog <- pluginTestRecorder - ghcideLog <- pluginTestRecorder - pure $ IdePlugins $ - [ Refactor.iePluginDescriptor exactprintLog "ghcide-code-actions-imports-exports" - , Refactor.typeSigsPluginDescriptor exactprintLog "ghcide-code-actions-type-signatures" - , Refactor.bindingsPluginDescriptor exactprintLog "ghcide-code-actions-bindings" - , Refactor.fillHolePluginDescriptor exactprintLog "ghcide-code-actions-fill-holes" - , Refactor.extendImportPluginDescriptor exactprintLog "ghcide-completions-1" - ] ++ GhcIde.descriptors ghcideLog + mkPluginTestDescriptor Refactor.iePluginDescriptor "ghcide-code-actions-imports-exports" + <> mkPluginTestDescriptor Refactor.typeSigsPluginDescriptor "ghcide-code-actions-type-signatures" + <> mkPluginTestDescriptor Refactor.bindingsPluginDescriptor "ghcide-code-actions-bindings" + <> mkPluginTestDescriptor Refactor.fillHolePluginDescriptor "ghcide-code-actions-fill-holes" + <> mkPluginTestDescriptor Refactor.extendImportPluginDescriptor "ghcide-completions-1" + tests :: TestTree tests = @@ -82,13 +70,15 @@ tests = , codeActionTests , codeActionHelperFunctionTests , completionTests + , extractNotInScopeNameTests ] +initializeTests :: TestTree initializeTests = withResource acquire release tests where tests :: IO (TResponseMessage Method_Initialize) -> TestTree tests getInitializeResponse = testGroup "initialize response capabilities" - [ chk " code action" _codeActionProvider (Just (InR (CodeActionOptions {_workDoneProgress = Nothing, _codeActionKinds = Nothing, _resolveProvider = Just False}))) + [ chk " code action" _codeActionProvider (Just (InR (CodeActionOptions {_workDoneProgress = Just False, _codeActionKinds = Nothing, _resolveProvider = Just False}))) , che " execute command" _executeCommandProvider [extendImportCommandId] ] where @@ -97,25 +87,25 @@ initializeTests = withResource acquire release tests testCase title $ getInitializeResponse >>= \ir -> expected @=? (getActual . innerCaps) ir che :: TestName -> (ServerCapabilities -> Maybe ExecuteCommandOptions) -> [T.Text] -> TestTree - che title getActual expected = testCase title doTest - where - doTest = do - ir <- getInitializeResponse - let Just ExecuteCommandOptions {_commands = commands} = getActual $ innerCaps ir - -- Check if expected exists in commands. Note that commands can arrive in different order. - mapM_ (\e -> any (\o -> T.isSuffixOf e o) commands @? show (expected, show commands)) expected + che title getActual expected = testCase title $ do + ir <- getInitializeResponse + ExecuteCommandOptions {_commands = commands} <- case getActual $ innerCaps ir of + Just eco -> pure eco + Nothing -> assertFailure "Was expecting Just ExecuteCommandOptions, got Nothing" + -- Check if expected exists in commands. Note that commands can arrive in different order. + mapM_ (\e -> any (\o -> T.isSuffixOf e o) commands @? show (expected, show commands)) expected acquire :: IO (TResponseMessage Method_Initialize) acquire = run initializeResponse - release :: TResponseMessage Method_Initialize -> IO () - release = const $ pure () + release = mempty innerCaps :: TResponseMessage Method_Initialize -> ServerCapabilities innerCaps (TResponseMessage _ _ (Right (InitializeResult c _))) = c innerCaps (TResponseMessage _ _ (Left _)) = error "Initialization error" + completionTests :: TestTree completionTests = testGroup "auto import snippets" @@ -176,6 +166,25 @@ completionTests = "join" ["{-# LANGUAGE NoImplicitPrelude #-}", "module A where", "import Control.Monad as M ()", "import Control.Monad as N (join)", "f = N.joi"] + -- Regression test for https://github.com/haskell/haskell-language-server/issues/2824 + , completionNoCommandTest + "explicit qualified" + ["{-# LANGUAGE NoImplicitPrelude #-}", + "module A where", "import qualified Control.Monad as M (j)"] + (Position 2 38) + "join" + , completionNoCommandTest + "explicit qualified post" + ["{-# LANGUAGE NoImplicitPrelude, ImportQualifiedPost #-}", + "module A where", "import Control.Monad qualified as M (j)"] + (Position 2 38) + "join" + , completionNoCommandTest + "multiline import" + [ "{-# LANGUAGE NoImplicitPrelude #-}" + , "module A where", "import Control.Monad", " (fore)"] + (Position 3 9) + "forever" ] , testGroup "Data constructor" [ completionCommandTest @@ -248,51 +257,36 @@ completionTests = ] ] -completionCommandTest :: - String -> - [T.Text] -> - Position -> - T.Text -> - [T.Text] -> - TestTree +completionCommandTest :: TestName -> [T.Text] -> Position -> T.Text -> [T.Text] -> TestTree completionCommandTest name src pos wanted expected = testSession name $ do docId <- createDoc "A.hs" "haskell" (T.unlines src) _ <- waitForDiagnostics compls <- skipManyTill anyMessage (getCompletions docId pos) - let wantedC = find ( \case - CompletionItem {_insertText = Just x - ,_command = Just _} -> wanted `T.isPrefixOf` x - _ -> False - ) compls + let wantedC = mapMaybe (\case + CompletionItem {_insertText = Just x, _command = Just cmd} + | wanted `T.isPrefixOf` x -> Just cmd + _ -> Nothing + ) compls case wantedC of - Nothing -> - liftIO $ assertFailure $ "Cannot find expected completion in: " <> show [_label | CompletionItem {_label} <- compls] - Just CompletionItem {..} -> do - c <- assertJust "Expected a command" _command - executeCommand c + [] -> + liftIO $ assertFailure $ "Cannot find completion " <> show wanted <> " in: " <> show [_label | CompletionItem {_label} <- compls] + command:_ -> do + executeCommand command if src /= expected - then do - modifiedCode <- skipManyTill anyMessage (getDocumentEdit docId) - liftIO $ modifiedCode @?= T.unlines expected - else do - expectMessages SMethod_WorkspaceApplyEdit 1 $ \edit -> - liftIO $ assertFailure $ "Expected no edit but got: " <> show edit - -completionNoCommandTest :: - String -> - [T.Text] -> - Position -> - T.Text -> - TestTree + then do + modifiedCode <- skipManyTill anyMessage (getDocumentEdit docId) + liftIO $ modifiedCode @?= T.unlines expected + else do + expectMessages SMethod_WorkspaceApplyEdit 1 $ \edit -> + liftIO $ assertFailure $ "Expected no edit but got: " <> show edit + +completionNoCommandTest :: TestName -> [T.Text] -> Position -> T.Text -> TestTree completionNoCommandTest name src pos wanted = testSession name $ do docId <- createDoc "A.hs" "haskell" (T.unlines src) _ <- waitForDiagnostics compls <- getCompletions docId pos - let wantedC = find ( \case - CompletionItem {_insertText = Just x} -> wanted `T.isPrefixOf` x - _ -> False - ) compls - case wantedC of + let isPrefixOfInsertOrLabel ci = any (wanted `T.isPrefixOf`) [fromMaybe "" (ci ^. L.insertText), ci ^. L.label] + case find isPrefixOfInsertOrLabel compls of Nothing -> liftIO $ assertFailure $ "Cannot find expected completion in: " <> show [_label | CompletionItem {_label} <- compls] Just CompletionItem{..} -> liftIO . assertBool ("Expected no command but got: " <> show _command) $ null _command @@ -309,6 +303,8 @@ codeActionTests = testGroup "code actions" , suggestImportClassMethodTests , suggestImportTests , suggestAddRecordFieldImportTests + , suggestAddCoerceMissingConstructorImportTests + , suggestAddGenericMissingConstructorImportTests , suggestHideShadowTests , fixConstructorImportTests , fixModuleImportTypoTests @@ -325,6 +321,7 @@ codeActionTests = testGroup "code actions" , addImplicitParamsConstraintTests , removeExportTests , Test.AddArgument.tests + , suggestAddRecordFieldUpdateImportTests ] insertImportTests :: TestTree @@ -344,67 +341,61 @@ insertImportTests = testGroup "insert import" "WhereDeclLowerInFileWithCommentsBeforeIt.hs" "WhereDeclLowerInFileWithCommentsBeforeIt.expected.hs" "import Data.Int" - , expectFailBecause - "'findNextPragmaPosition' function doesn't account for case when shebang is not placed at top of file" - (checkImport - "Shebang not at top with spaces" - "ShebangNotAtTopWithSpaces.hs" - "ShebangNotAtTopWithSpaces.expected.hs" - "import Data.Monoid") - , expectFailBecause - "'findNextPragmaPosition' function doesn't account for case when shebang is not placed at top of file" - (checkImport - "Shebang not at top no space" - "ShebangNotAtTopNoSpace.hs" - "ShebangNotAtTopNoSpace.expected.hs" - "import Data.Monoid") - , expectFailBecause - ("'findNextPragmaPosition' function doesn't account for case " - ++ "when OPTIONS_GHC pragma is not placed at top of file") - (checkImport - "OPTIONS_GHC pragma not at top with spaces" - "OptionsNotAtTopWithSpaces.hs" - "OptionsNotAtTopWithSpaces.expected.hs" - "import Data.Monoid") - , expectFailBecause - ("'findNextPragmaPosition' function doesn't account for " - ++ "case when shebang is not placed at top of file") - (checkImport - "Shebang not at top of file" - "ShebangNotAtTop.hs" - "ShebangNotAtTop.expected.hs" - "import Data.Monoid") - , expectFailBecause - ("'findNextPragmaPosition' function doesn't account for case " - ++ "when OPTIONS_GHC is not placed at top of file") - (checkImport - "OPTIONS_GHC pragma not at top of file" - "OptionsPragmaNotAtTop.hs" - "OptionsPragmaNotAtTop.expected.hs" - "import Data.Monoid") - , expectFailBecause - ("'findNextPragmaPosition' function doesn't account for case when " - ++ "OPTIONS_GHC pragma is not placed at top of file") - (checkImport - "pragma not at top with comment at top" - "PragmaNotAtTopWithCommentsAtTop.hs" - "PragmaNotAtTopWithCommentsAtTop.expected.hs" - "import Data.Monoid") - , expectFailBecause - ("'findNextPragmaPosition' function doesn't account for case when " - ++ "OPTIONS_GHC pragma is not placed at top of file") - (checkImport - "pragma not at top multiple comments" - "PragmaNotAtTopMultipleComments.hs" - "PragmaNotAtTopMultipleComments.expected.hs" - "import Data.Monoid") - , expectFailBecause - "'findNextPragmaPosition' function doesn't account for case of multiline pragmas" - (checkImport - "after multiline language pragmas" - "MultiLinePragma.hs" - "MultiLinePragma.expected.hs" - "import Data.Monoid") + -- TODO: 'findNextPragmaPosition' function doesn't account for case when shebang is not + -- placed at top of file" + , checkImport + "Shebang not at top with spaces" + "ShebangNotAtTopWithSpaces.hs" + "ShebangNotAtTopWithSpaces.expected.hs" + "import Data.Monoid" + -- TODO: 'findNextPragmaPosition' function doesn't account for case when shebang is not + -- placed at top of file" + , checkImport + "Shebang not at top no space" + "ShebangNotAtTopNoSpace.hs" + "ShebangNotAtTopNoSpace.expected.hs" + "import Data.Monoid" + -- TODO: 'findNextPragmaPosition' function doesn't account for case when OPTIONS_GHC pragma is + -- not placed at top of file + , checkImport + "OPTIONS_GHC pragma not at top with spaces" + "OptionsNotAtTopWithSpaces.hs" + "OptionsNotAtTopWithSpaces.expected.hs" + "import Data.Monoid" + -- TODO: findNextPragmaPosition' function doesn't account for case when shebang is not placed + -- at top of file + , checkImport + "Shebang not at top of file" + "ShebangNotAtTop.hs" + "ShebangNotAtTop.expected.hs" + "import Data.Monoid" + -- TODO: findNextPragmaPosition' function doesn't account for case when OPTIONS_GHC is not + -- placed at top of file + , checkImport + "OPTIONS_GHC pragma not at top of file" + "OptionsPragmaNotAtTop.hs" + "OptionsPragmaNotAtTop.expected.hs" + "import Data.Monoid" + -- TODO: findNextPragmaPosition' function doesn't account for case when OPTIONS_GHC pragma is + -- not placed at top of file + , checkImport + "pragma not at top with comment at top" + "PragmaNotAtTopWithCommentsAtTop.hs" + "PragmaNotAtTopWithCommentsAtTop.expected.hs" + "import Data.Monoid" + -- TODO: findNextPragmaPosition' function doesn't account for case when OPTIONS_GHC pragma is + -- not placed at top of file + , checkImport + "pragma not at top multiple comments" + "PragmaNotAtTopMultipleComments.hs" + "PragmaNotAtTopMultipleComments.expected.hs" + "import Data.Monoid" + -- TODO: 'findNextPragmaPosition' function doesn't account for case of multiline pragmas + , checkImport + "after multiline language pragmas" + "MultiLinePragma.hs" + "MultiLinePragma.expected.hs" + "import Data.Monoid" , checkImport "pragmas not at top with module declaration" "PragmaNotAtTopWithModuleDecl.hs" @@ -544,122 +535,104 @@ importQualifiedTests = testGroup "import qualified prefix suggestions" ["import qualified Control.Monad as Control", "import Control.Monad (when)"] ] -checkImport :: String -> FilePath -> FilePath -> T.Text -> TestTree -checkImport testComment originalPath expectedPath action = - checkImport' testComment originalPath expectedPath action [] +checkImport :: TestName -> FilePath -> FilePath -> T.Text -> TestTree +checkImport testName originalPath expectedPath action = + checkImport' testName originalPath expectedPath action [] -checkImport' :: String -> FilePath -> FilePath -> T.Text -> [T.Text] -> TestTree -checkImport' testComment originalPath expectedPath action excludedActions = - testSessionWithExtraFiles "import-placement" testComment $ \dir -> +checkImport' :: TestName -> FilePath -> FilePath -> T.Text -> [T.Text] -> TestTree +checkImport' testName originalPath expectedPath action excludedActions = + testSessionWithExtraFiles "import-placement" testName $ \dir -> check (dir originalPath) (dir expectedPath) action where check :: FilePath -> FilePath -> T.Text -> Session () check originalPath expectedPath action = do oSrc <- liftIO $ readFileUtf8 originalPath - eSrc <- liftIO $ readFileUtf8 expectedPath + shouldBeDocContents <- liftIO $ readFileUtf8 expectedPath originalDoc <- createDoc originalPath "haskell" oSrc _ <- waitForDiagnostics - shouldBeDoc <- createDoc expectedPath "haskell" eSrc actionsOrCommands <- getAllCodeActions originalDoc - for_ excludedActions (\a -> liftIO $ assertNoActionWithTitle a actionsOrCommands) - chosenAction <- liftIO $ pickActionWithTitle action actionsOrCommands + for_ excludedActions (\a -> assertNoActionWithTitle a actionsOrCommands) + chosenAction <- pickActionWithTitle action actionsOrCommands executeCodeAction chosenAction originalDocAfterAction <- documentContents originalDoc - shouldBeDocContents <- documentContents shouldBeDoc liftIO $ T.replace "\r\n" "\n" shouldBeDocContents @=? T.replace "\r\n" "\n" originalDocAfterAction renameActionTests :: TestTree renameActionTests = testGroup "rename actions" - [ testSession "change to local variable name" $ do - let content = T.unlines - [ "module Testing where" - , "foo :: Int -> Int" - , "foo argName = argNme" - ] - doc <- createDoc "Testing.hs" "haskell" content - _ <- waitForDiagnostics - action <- findCodeAction doc (Range (Position 2 14) (Position 2 20)) "Replace with ‘argName’" - executeCodeAction action - contentAfterAction <- documentContents doc - let expectedContentAfterAction = T.unlines - [ "module Testing where" - , "foo :: Int -> Int" - , "foo argName = argName" - ] - liftIO $ expectedContentAfterAction @=? contentAfterAction - , testSession "change to name of imported function" $ do - let content = T.unlines - [ "module Testing where" - , "import Data.Maybe (maybeToList)" - , "foo :: Maybe a -> [a]" - , "foo = maybToList" - ] - doc <- createDoc "Testing.hs" "haskell" content - _ <- waitForDiagnostics - action <- findCodeAction doc (Range (Position 3 6) (Position 3 16)) "Replace with ‘maybeToList’" - executeCodeAction action - contentAfterAction <- documentContents doc - let expectedContentAfterAction = T.unlines - [ "module Testing where" - , "import Data.Maybe (maybeToList)" - , "foo :: Maybe a -> [a]" - , "foo = maybeToList" - ] - liftIO $ expectedContentAfterAction @=? contentAfterAction + [ check "change to local variable name" + [ "module Testing where" + , "foo :: Int -> Int" + , "foo argName = argNme" + ] + ("Replace with ‘argName’", R 2 14 2 20) + [ "module Testing where" + , "foo :: Int -> Int" + , "foo argName = argName" + ] + , check "change to name of imported function" + [ "module Testing where" + , "import Data.Maybe (maybeToList)" + , "foo :: Maybe a -> [a]" + , "foo = maybToList" + ] + ("Replace with ‘maybeToList’", R 3 6 3 16) + [ "module Testing where" + , "import Data.Maybe (maybeToList)" + , "foo :: Maybe a -> [a]" + , "foo = maybeToList" + ] + , check "change infix function" + [ "module Testing where" + , "monus :: Int -> Int" + , "monus x y = max 0 (x - y)" + , "foo x y = x `monnus` y" + ] + ("Replace with ‘monus’", R 3 12 3 20) + [ "module Testing where" + , "monus :: Int -> Int" + , "monus x y = max 0 (x - y)" + , "foo x y = x `monus` y" + ] + , check "change template function" + [ "{-# LANGUAGE TemplateHaskellQuotes #-}" + , "module Testing where" + , "import Language.Haskell.TH (Name)" + , "foo :: Name" + , "foo = 'bread" + ] + ("Replace with ‘break’", R 4 6 4 12) + [ "{-# LANGUAGE TemplateHaskellQuotes #-}" + , "module Testing where" + , "import Language.Haskell.TH (Name)" + , "foo :: Name" + , "foo = 'break" + ] , testSession "suggest multiple local variable names" $ do - let content = T.unlines + doc <- createDoc "Testing.hs" "haskell" $ T.unlines [ "module Testing where" , "foo :: Char -> Char -> Char -> Char" , "foo argument1 argument2 argument3 = argumentX" ] - doc <- createDoc "Testing.hs" "haskell" content - _ <- waitForDiagnostics - _ <- findCodeActions doc (Range (Position 2 36) (Position 2 45)) - ["Replace with ‘argument1’", "Replace with ‘argument2’", "Replace with ‘argument3’"] - return() - , testSession "change infix function" $ do - let content = T.unlines - [ "module Testing where" - , "monus :: Int -> Int" - , "monus x y = max 0 (x - y)" - , "foo x y = x `monnus` y" - ] - doc <- createDoc "Testing.hs" "haskell" content _ <- waitForDiagnostics - actionsOrCommands <- getCodeActions doc (Range (Position 3 12) (Position 3 20)) - [fixTypo] <- pure [action | InR action@CodeAction{ _title = actionTitle } <- actionsOrCommands, "monus" `T.isInfixOf` actionTitle , "Replace" `T.isInfixOf` actionTitle] - executeCodeAction fixTypo - contentAfterAction <- documentContents doc - let expectedContentAfterAction = T.unlines - [ "module Testing where" - , "monus :: Int -> Int" - , "monus x y = max 0 (x - y)" - , "foo x y = x `monus` y" - ] - liftIO $ expectedContentAfterAction @=? contentAfterAction - , testSession "change template function" $ do - let content = T.unlines - [ "{-# LANGUAGE TemplateHaskellQuotes #-}" - , "module Testing where" - , "import Language.Haskell.TH (Name)" - , "foo :: Name" - , "foo = 'bread" - ] - doc <- createDoc "Testing.hs" "haskell" content - diags <- waitForDiagnostics - actionsOrCommands <- getCodeActions doc (Range (Position 4 6) (Position 4 12)) - [fixTypo] <- pure [action | InR action@CodeAction{ _title = actionTitle } <- actionsOrCommands, "break" `T.isInfixOf` actionTitle ] - executeCodeAction fixTypo - contentAfterAction <- documentContents doc - let expectedContentAfterAction = T.unlines - [ "{-# LANGUAGE TemplateHaskellQuotes #-}" - , "module Testing where" - , "import Language.Haskell.TH (Name)" - , "foo :: Name" - , "foo = 'break" - ] - liftIO $ expectedContentAfterAction @=? contentAfterAction + actions <- getCodeActions doc (R 2 36 2 45) + traverse_ (assertActionWithTitle actions) + [ "Replace with ‘argument1’" + , "Replace with ‘argument2’" + , "Replace with ‘argument3’" + ] ] + where + check :: TestName -> [T.Text] -> (T.Text, Range) -> [T.Text] -> TestTree + check testName linesOrig (actionTitle, actionRange) linesExpected = + testSession testName $ do + let contentBefore = T.unlines linesOrig + doc <- createDoc "Testing.hs" "haskell" contentBefore + _ <- waitForDiagnostics + action <- pickActionWithTitle actionTitle =<< getCodeActions doc actionRange + executeCodeAction action + contentAfter <- documentContents doc + let expectedContent = T.unlines linesExpected + liftIO $ expectedContent @=? contentAfter typeWildCardActionTests :: TestTree typeWildCardActionTests = testGroup "type wildcard actions" @@ -667,7 +640,7 @@ typeWildCardActionTests = testGroup "type wildcard actions" [ "func :: _" , "func x = x" ] - [ "func :: p -> p" + [ if ghcVersion >= GHC910 then "func :: t -> t" else "func :: p -> p" , "func x = x" ] , testUseTypeSignature "local signature" @@ -687,7 +660,12 @@ typeWildCardActionTests = testGroup "type wildcard actions" [ "func :: _" , "func x y = x + y" ] - [ "func :: Integer -> Integer -> Integer" + [ if ghcVersion >= GHC910 then + "func :: t -> t -> t" + else if ghcVersion >= GHC98 then + "func :: a -> a -> a" -- since 9.8 GHC no longer does type defaulting (see https://gitlab.haskell.org/ghc/ghc/-/issues/24522) + else + "func :: Integer -> Integer -> Integer" , "func x y = x + y" ] , testUseTypeSignature "type in parentheses" @@ -715,7 +693,12 @@ typeWildCardActionTests = testGroup "type wildcard actions" [ "func::_" , "func x y = x + y" ] - [ "func::Integer -> Integer -> Integer" + [ if ghcVersion >= GHC910 then + "func::t -> t -> t" + else if ghcVersion >= GHC98 then + "func::a -> a -> a" -- since 9.8 GHC no longer does type defaulting (see https://gitlab.haskell.org/ghc/ghc/-/issues/24522) + else + "func::Integer -> Integer -> Integer" , "func x y = x + y" ] , testGroup "add parens if hole is part of bigger type" @@ -759,15 +742,14 @@ typeWildCardActionTests = testGroup "type wildcard actions" doc <- createDoc "Testing.hs" "haskell" content _ <- waitForDiagnostics actionsOrCommands <- getAllCodeActions doc - let [addSignature] = [action | InR action@CodeAction { _title = actionTitle } <- actionsOrCommands - , "Use type signature" `T.isInfixOf` actionTitle - ] + [addSignature] <- pure [action | InR action@CodeAction { _title = actionTitle } <- actionsOrCommands + , "Use type signature" `T.isPrefixOf` actionTitle + ] executeCodeAction addSignature contentAfterAction <- documentContents doc liftIO $ expectedContentAfterAction @=? contentAfterAction -{-# HLINT ignore "Use nubOrd" #-} removeImportTests :: TestTree removeImportTests = testGroup "remove import actions" [ testSession "redundant" $ do @@ -784,9 +766,7 @@ removeImportTests = testGroup "remove import actions" ] docB <- createDoc "ModuleB.hs" "haskell" contentB _ <- waitForDiagnostics - [InR action@CodeAction { _title = actionTitle }, _] - <- getCodeActions docB (Range (Position 2 0) (Position 2 5)) - liftIO $ "Remove import" @=? actionTitle + action <- pickActionWithTitle "Remove import" =<< getCodeActions docB (R 2 0 2 5) executeCodeAction action contentAfterAction <- documentContents docB let expectedContentAfterAction = T.unlines @@ -810,9 +790,7 @@ removeImportTests = testGroup "remove import actions" ] docB <- createDoc "ModuleB.hs" "haskell" contentB _ <- waitForDiagnostics - [InR action@CodeAction { _title = actionTitle }, _] - <- getCodeActions docB (Range (Position 2 0) (Position 2 5)) - liftIO $ "Remove import" @=? actionTitle + action <- pickActionWithTitle "Remove import" =<< getCodeActions docB (R 2 0 2 5) executeCodeAction action contentAfterAction <- documentContents docB let expectedContentAfterAction = T.unlines @@ -840,9 +818,8 @@ removeImportTests = testGroup "remove import actions" ] docB <- createDoc "ModuleB.hs" "haskell" contentB _ <- waitForDiagnostics - [InR action@CodeAction { _title = actionTitle }, _] - <- getCodeActions docB (Range (Position 2 0) (Position 2 5)) - liftIO $ "Remove _stuffD, stuffA, stuffC from import" @=? actionTitle + action <- pickActionWithTitle "Remove _stuffD, stuffA, stuffC from import" + =<< getCodeActions docB (R 2 0 2 5) executeCodeAction action contentAfterAction <- documentContents docB let expectedContentAfterAction = T.unlines @@ -868,9 +845,7 @@ removeImportTests = testGroup "remove import actions" ] docB <- createDoc "ModuleB.hs" "haskell" contentB _ <- waitForDiagnostics - [InR action@CodeAction { _title = actionTitle }, _] - <- getCodeActions docB (Range (Position 2 0) (Position 2 5)) - liftIO $ "Remove ε from import" @=? actionTitle + action <- pickActionWithTitle "Remove ε from import" =<< getCodeActions docB (R 2 0 2 5) executeCodeAction action contentAfterAction <- documentContents docB let expectedContentAfterAction = T.unlines @@ -897,9 +872,7 @@ removeImportTests = testGroup "remove import actions" ] docB <- createDoc "ModuleB.hs" "haskell" contentB _ <- waitForDiagnostics - [InR action@CodeAction { _title = actionTitle }, _] - <- getCodeActions docB (Range (Position 2 0) (Position 2 5)) - liftIO $ "Remove !!, from import" @=? actionTitle + action <- pickActionWithTitle "Remove !!, from import" =<< getCodeActions docB (R 2 0 2 5) executeCodeAction action contentAfterAction <- documentContents docB let expectedContentAfterAction = T.unlines @@ -925,9 +898,7 @@ removeImportTests = testGroup "remove import actions" ] docB <- createDoc "ModuleB.hs" "haskell" contentB _ <- waitForDiagnostics - [InR action@CodeAction { _title = actionTitle }, _] - <- getCodeActions docB (Range (Position 2 0) (Position 2 5)) - liftIO $ "Remove A from import" @=? actionTitle + action <- pickActionWithTitle "Remove A from import" =<< getCodeActions docB (R 2 0 2 5) executeCodeAction action contentAfterAction <- documentContents docB let expectedContentAfterAction = T.unlines @@ -952,9 +923,7 @@ removeImportTests = testGroup "remove import actions" ] docB <- createDoc "ModuleB.hs" "haskell" contentB _ <- waitForDiagnostics - [InR action@CodeAction { _title = actionTitle }, _] - <- getCodeActions docB (Range (Position 2 0) (Position 2 5)) - liftIO $ "Remove A, E, F from import" @=? actionTitle + action <- pickActionWithTitle "Remove A, E, F from import" =<< getCodeActions docB (R 2 0 2 5) executeCodeAction action contentAfterAction <- documentContents docB let expectedContentAfterAction = T.unlines @@ -976,9 +945,7 @@ removeImportTests = testGroup "remove import actions" ] docB <- createDoc "ModuleB.hs" "haskell" contentB _ <- waitForDiagnostics - [InR action@CodeAction { _title = actionTitle }, _] - <- getCodeActions docB (Range (Position 2 0) (Position 2 5)) - liftIO $ "Remove import" @=? actionTitle + action <- pickActionWithTitle "Remove import" =<< getCodeActions docB (R 2 0 2 5) executeCodeAction action contentAfterAction <- documentContents docB let expectedContentAfterAction = T.unlines @@ -1001,9 +968,7 @@ removeImportTests = testGroup "remove import actions" ] doc <- createDoc "ModuleC.hs" "haskell" content _ <- waitForDiagnostics - [_, _, _, _, InR action@CodeAction { _title = actionTitle }] - <- nub <$> getAllCodeActions doc - liftIO $ "Remove all redundant imports" @=? actionTitle + action <- pickActionWithTitle "Remove all redundant imports" =<< getAllCodeActions doc executeCodeAction action contentAfterAction <- documentContents doc let expectedContentAfterAction = T.unlines @@ -1033,9 +998,7 @@ removeImportTests = testGroup "remove import actions" ] docB <- createDoc "ModuleB.hs" "haskell" contentB _ <- waitForDiagnostics - [InR action@CodeAction { _title = actionTitle }, _] - <- getCodeActions docB (Range (Position 2 0) (Position 2 5)) - liftIO $ "Remove @. from import" @=? actionTitle + action <- pickActionWithTitle "Remove @. from import" =<< getCodeActions docB (R 2 0 2 5) executeCodeAction action contentAfterAction <- documentContents docB let expectedContentAfterAction = T.unlines @@ -1045,6 +1008,76 @@ removeImportTests = testGroup "remove import actions" , "x = a -- Must use something from module A, but not (@.)" ] liftIO $ expectedContentAfterAction @=? contentAfterAction + , testSession "remove redundant record field import" $ do + let contentA = T.unlines + [ "module ModuleA where" + , "data A = A {" + , " a1 :: String," + , " a2 :: Int" + , "}" + , "newA = A \"foo\" 42" + ] + _docA <- createDoc "ModuleA.hs" "haskell" contentA + let contentB = T.unlines + [ "{-# OPTIONS_GHC -Wunused-imports #-}" + , "module ModuleB where" + , "import ModuleA" + , " ( A (a1, a2)," + , " newA" + , " )" + , "x = a1 newA" + ] + docB <- createDoc "ModuleB.hs" "haskell" contentB + _ <- waitForDiagnostics + action <- pickActionWithTitle "Remove A(a2) from import" =<< getCodeActions docB (R 2 0 5 3) + executeCodeAction action + contentAfterAction <- documentContents docB + let expectedContentAfterAction = T.unlines + [ "{-# OPTIONS_GHC -Wunused-imports #-}" + , "module ModuleB where" + , "import ModuleA" + , " ( A (a1)," + , " newA" + , " )" + , "x = a1 newA" + ] + liftIO $ expectedContentAfterAction @=? contentAfterAction + , testSession "remove multiple redundant record field imports" $ do + let contentA = T.unlines + [ "module ModuleA where" + , "data A = A {" + , " a1 :: String," + , " a2 :: Int," + , " a3 :: Int," + , " a4 :: Int" + , "}" + , "newA = A \"foo\" 2 3 4" + ] + _docA <- createDoc "ModuleA.hs" "haskell" contentA + let contentB = T.unlines + [ "{-# OPTIONS_GHC -Wunused-imports #-}" + , "module ModuleB where" + , "import ModuleA" + , " ( A (a1, a2, a3, a4)," + , " newA" + , " )" + , "x = a2 newA" + ] + docB <- createDoc "ModuleB.hs" "haskell" contentB + _ <- waitForDiagnostics + action <- pickActionWithTitle "Remove A(a1), A(a3), A(a4) from import" =<< getCodeActions docB (R 2 0 5 3) + executeCodeAction action + contentAfterAction <- documentContents docB + let expectedContentAfterAction = T.unlines + [ "{-# OPTIONS_GHC -Wunused-imports #-}" + , "module ModuleB where" + , "import ModuleA" + , " ( A (a2)," + , " newA" + , " )" + , "x = a2 newA" + ] + liftIO $ expectedContentAfterAction @=? contentAfterAction ] extendImportTests :: TestTree @@ -1124,7 +1157,7 @@ extendImportTests = testGroup "extend import actions" , "x :: (:~:) [] []" , "x = Refl" ]) - (Range (Position 3 17) (Position 3 18)) + (Range (Position 3 4) (Position 3 8)) [ "Add (:~:)(..) to the import list of Data.Type.Equality" , "Add type (:~:)(Refl) to the import list of Data.Type.Equality"] (T.unlines @@ -1188,7 +1221,7 @@ extendImportTests = testGroup "extend import actions" , "import ModuleA as A (stuffB)" , "main = print (stuffB .* stuffB)" ]) - (Range (Position 2 17) (Position 2 18)) + (Range (Position 2 22) (Position 2 24)) ["Add (.*) to the import list of ModuleA"] (T.unlines [ "module ModuleB where" @@ -1202,7 +1235,7 @@ extendImportTests = testGroup "extend import actions" , "import Data.List.NonEmpty (fromList)" , "main = case (fromList []) of _ :| _ -> pure ()" ]) - (Range (Position 2 5) (Position 2 6)) + (Range (Position 2 31) (Position 2 33)) [ "Add NonEmpty((:|)) to the import list of Data.List.NonEmpty" , "Add NonEmpty(..) to the import list of Data.List.NonEmpty" ] @@ -1219,7 +1252,7 @@ extendImportTests = testGroup "extend import actions" , "import Data.Maybe (catMaybes)" , "x = Just 10" ]) - (Range (Position 3 5) (Position 2 6)) + (Range (Position 3 4) (Position 3 8)) [ "Add Maybe(Just) to the import list of Data.Maybe" , "Add Maybe(..) to the import list of Data.Maybe" ] @@ -1312,8 +1345,21 @@ extendImportTests = testGroup "extend import actions" , "b :: A" , "b = ConstructorFoo" ]) - , brokenForGHC94 "On GHC 9.4, the error messages with -fdefer-type-errors don't have necessary imported target srcspan info." $ - testSession "extend single line qualified import with value" $ template + , testSession "extend single line import in presence of extra parens" $ template + [] + ("Main.hs", T.unlines + [ "import Data.Monoid (First)" + , "f = (First Nothing) <> mempty" -- parens tripped up the regex extracting import suggestions + ]) + (Range (Position 1 6) (Position 1 7)) + [ "Add First(..) to the import list of Data.Monoid" + , "Add First(First) to the import list of Data.Monoid" + ] + (T.unlines + [ "import Data.Monoid (First (..))" + , "f = (First Nothing) <> mempty" + ]) + , testSession "extend single line qualified import with value" $ template [("ModuleA.hs", T.unlines [ "module ModuleA where" , "stuffA :: Double" @@ -1438,7 +1484,7 @@ extendImportTests = testGroup "extend import actions" , "import ModuleA ()" , "foo = bar" ]) - (Range (Position 3 17) (Position 3 18)) + (Range (Position 3 6) (Position 3 9)) ["Add bar to the import list of ModuleA", "Add bar to the import list of ModuleB"] (T.unlines @@ -1455,7 +1501,7 @@ extendImportTests = testGroup "extend import actions" , "x :: (:~:) [] []" , "x = Refl" ]) - (Range (Position 3 17) (Position 3 18)) + (Range (Position 3 4) (Position 3 8)) [ "Add type (:~:)(Refl) to the import list of Data.Type.Equality" , "Add (:~:)(..) to the import list of Data.Type.Equality"] (T.unlines @@ -1464,28 +1510,48 @@ extendImportTests = testGroup "extend import actions" , "x :: (:~:) [] []" , "x = Refl" ]) - , expectFailBecause "importing pattern synonyms is unsupported" - $ testSession "extend import list with pattern synonym" $ template - [("ModuleA.hs", T.unlines - [ "{-# LANGUAGE PatternSynonyms #-}" - , "module ModuleA where" - , "pattern Some x = Just x" - ]) - ] - ("ModuleB.hs", T.unlines - [ "module ModuleB where" - , "import A ()" - , "k (Some x) = x" - ]) - (Range (Position 2 3) (Position 2 7)) - ["Add pattern Some to the import list of A"] - (T.unlines - [ "module ModuleB where" - , "import A (pattern Some)" - , "k (Some x) = x" - ]) - , ignoreForGhcVersions [GHC92, GHC94] "Diagnostic message has no suggestions" $ - testSession "type constructor name same as data constructor name" $ template + -- TODO: importing pattern synonyms is unsupported + , testSessionExpectFail "extend import list with pattern synonym" + (BrokenIdeal $ + template + [("ModuleA.hs", T.unlines + [ "{-# LANGUAGE PatternSynonyms #-}" + , "module ModuleA where" + , "pattern Some x = Just x" + ]) + ] + ("ModuleB.hs", T.unlines + [ "module ModuleB where" + , "import A ()" + , "k (Some x) = x" + ] + ) + (Range (Position 2 3) (Position 2 7)) + ["Add pattern Some to the import list of A"] + (T.unlines + [ "module ModuleB where" + , "import A (pattern Some)" + , "k (Some x) = x" + ] + ) + ) + (BrokenCurrent $ + noCodeActionsTemplate + [("ModuleA.hs", T.unlines + [ "{-# LANGUAGE PatternSynonyms #-}" + , "module ModuleA where" + , "pattern Some x = Just x" + ]) + ] + ("ModuleB.hs", T.unlines + [ "module ModuleB where" + , "import A ()" + , "k (Some x) = x" + ] + ) + (Range (Position 2 3) (Position 2 7)) + ) + , testSession "type constructor name same as data constructor name" $ template [("ModuleA.hs", T.unlines [ "module ModuleA where" , "newtype Foo = Foo Int" @@ -1523,23 +1589,39 @@ extendImportTests = testGroup "extend import actions" , "f :: Foo" , "f = undefined" ]) + , testSession "data constructor with two multiline import lists that can be extended with it" $ template + [] + ("A.hs", T.unlines + [ "module A where" + , "import Prelude (" + , " )" + , "import Data.Maybe (" + , " )" + , "f = Nothing" + ]) + (Range (Position 5 5) (Position 5 6)) + [ "Add Maybe(..) to the import list of Data.Maybe" + , "Add Maybe(..) to the import list of Prelude" + , "Add Maybe(Nothing) to the import list of Data.Maybe" + , "Add Maybe(Nothing) to the import list of Prelude" + ] + (T.unlines + ["module A where" + , "import Prelude (" + , " )" + , "import Data.Maybe (Maybe (..)" + , " )" + , "f = Nothing" + ]) ] where codeActionTitle CodeAction{_title=x} = x template setUpModules moduleUnderTest range expectedTitles expectedContentB = do - configureCheckProject overrideCheckProject + docB <- evalProject setUpModules moduleUnderTest + codeActions <- codeActions docB range + let actualTitles = codeActionTitle <$> codeActions - mapM_ (\x -> createDoc (fst x) "haskell" (snd x)) setUpModules - docB <- createDoc (fst moduleUnderTest) "haskell" (snd moduleUnderTest) - _ <- waitForDiagnostics - waitForProgressDone - actionsOrCommands <- getCodeActions docB range - let codeActions = - filter - (liftA2 (&&) (T.isPrefixOf "Add") (not . T.isPrefixOf "Add argument") . codeActionTitle) - [ca | InR ca <- actionsOrCommands] - actualTitles = codeActionTitle <$> codeActions -- Note that we are not testing the order of the actions, as the -- order of the expected actions indicates which one we'll execute -- in this test, i.e., the first one. @@ -1548,33 +1630,55 @@ extendImportTests = testGroup "extend import actions" -- Execute the action with the same title as the first expected one. -- Since we tested that both lists have the same elements (possibly -- in a different order), this search cannot fail. - let firstTitle:_ = expectedTitles - action = fromJust $ - find ((firstTitle ==) . codeActionTitle) codeActions + firstTitle:_ <- pure expectedTitles + Just action <- pure $ find ((firstTitle ==) . codeActionTitle) codeActions executeCodeAction action contentAfterAction <- documentContents docB liftIO $ expectedContentB @=? contentAfterAction + noCodeActionsTemplate setUpModules moduleUnderTest range = do + docB <- evalProject setUpModules moduleUnderTest + codeActions' <- codeActions docB range + let actualTitles = codeActionTitle <$> codeActions' + liftIO $ [] @=? actualTitles + + evalProject setUpModules moduleUnderTest = do + configureCheckProject overrideCheckProject + + mapM_ (\(fileName, contents) -> createDoc fileName "haskell" contents) setUpModules + docB <- createDoc (fst moduleUnderTest) "haskell" (snd moduleUnderTest) + _ <- waitForDiagnostics + waitForProgressDone + + pure docB + + codeActions docB range = do + actionsOrCommands <- getCodeActions docB range + pure $ + [ ca | InR ca <- actionsOrCommands + , let title = codeActionTitle ca + , "Add" `T.isPrefixOf` title && not ("Add argument" `T.isPrefixOf` title) + ] + fixModuleImportTypoTests :: TestTree fixModuleImportTypoTests = testGroup "fix module import typo" [ testSession "works when single module suggested" $ do doc <- createDoc "A.hs" "haskell" "import Data.Cha" _ <- waitForDiagnostics - InR action@CodeAction { _title = actionTitle } : _ <- getCodeActions doc (R 0 0 0 10) - liftIO $ actionTitle @?= "replace with Data.Char" + action <- pickActionWithTitle "Replace with Data.Char" =<< getCodeActions doc (R 0 0 0 10) executeCodeAction action contentAfterAction <- documentContents doc liftIO $ contentAfterAction @?= "import Data.Char" , testSession "works when multiple modules suggested" $ do doc <- createDoc "A.hs" "haskell" "import Data.I" _ <- waitForDiagnostics - actions <- sortOn (\(InR CodeAction{_title=x}) -> x) <$> getCodeActions doc (R 0 0 0 10) - let actionTitles = [ title | InR CodeAction{_title=title} <- actions ] - liftIO $ actionTitles @?= [ "replace with Data.Eq" - , "replace with Data.Int" - , "replace with Data.Ix" - ] - let InR replaceWithDataEq : _ = actions + actions <- getCodeActions doc (R 0 0 0 10) + traverse_ (assertActionWithTitle actions) + [ "Replace with Data.Eq" + , "Replace with Data.Int" + , "Replace with Data.Ix" + ] + replaceWithDataEq <- pickActionWithTitle "Replace with Data.Eq" actions executeCodeAction replaceWithDataEq contentAfterAction <- documentContents doc liftIO $ contentAfterAction @?= "import Data.Eq" @@ -1638,11 +1742,8 @@ suggestImportClassMethodTests = doc <- createDoc "A.hs" "haskell" $ T.unlines (beforeContent <> decls) _ <- waitForDiagnostics waitForProgressDone - actions <- getCodeActions doc range - let actions' = [x | InR x <- actions] - titles = [_title | CodeAction {_title} <- actions'] - liftIO $ executeTitle `elem` titles @? T.unpack executeTitle <> " does not in " <> show titles - executeCodeAction $ fromJust $ find (\CodeAction {_title} -> _title == executeTitle) actions' + action <- pickActionWithTitle executeTitle =<< getCodeActions doc range + executeCodeAction action content <- documentContents doc liftIO $ T.unlines (expectedContent <> decls) @=? content template' executeTitle range = let c = ["module A where"] in template c range executeTitle $ c <> [executeTitle] @@ -1651,6 +1752,7 @@ suggestImportTests :: TestTree suggestImportTests = testGroup "suggest import actions" [ testGroup "Dont want suggestion" [ -- extend import + -- We don't want to suggest a new import, but extend existing imports test False ["Data.List.NonEmpty ()"] "f = nonEmpty" [] "import Data.List.NonEmpty (nonEmpty)" -- data constructor , test False [] "f = First" [] "import Data.Monoid (First)" @@ -1717,7 +1819,8 @@ suggestImportTests = testGroup "suggest import actions" , test True [] "f = (.|.)" [] "import Data.Bits (Bits(..))" , test True [] "f = empty" [] "import Control.Applicative (Alternative(..))" ] - , expectFailBecause "importing pattern synonyms is unsupported" $ test True [] "k (Some x) = x" [] "import B (pattern Some)" + -- TODO: Importing pattern synonyms is unsupported + , test False [] "k (Some x) = x" [] "import B (pattern Some)" ] where test = test' False @@ -1740,7 +1843,7 @@ suggestImportTests = testGroup "suggest import actions" actions <- getCodeActions doc range if wanted then do - action <- liftIO $ pickActionWithTitle newImp actions + action <- pickActionWithTitle newImp actions executeCodeAction action contentAfterAction <- documentContents doc liftIO $ after @=? contentAfterAction @@ -1750,24 +1853,168 @@ suggestImportTests = testGroup "suggest import actions" suggestAddRecordFieldImportTests :: TestTree suggestAddRecordFieldImportTests = testGroup "suggest imports of record fields when using OverloadedRecordDot" [ testGroup "The field is suggested when an instance resolution failure occurs" - [ ignoreForGhcVersions [GHC90, GHC94, GHC96] "Extension not present <9.2, and the assist is derived from the help message in >=9.4" theTest + ([ ignoreForGhcVersions [GHC96] "Extension not present <9.2, and the assist is derived from the help message in >=9.4" theTest ] + ++ [ + theTestIndirect qualifiedGhcRecords polymorphicType + | + qualifiedGhcRecords <- [False, True] + , polymorphicType <- [False, True] + ]) ] where theTest = testSessionWithExtraFiles "hover" def $ \dir -> do configureCheckProject False - let before = T.unlines $ "module A where" : ["import B (Foo)", "getFoo :: Foo -> Int", "getFoo x = x.foo"] - after = T.unlines $ "module A where" : ["import B (Foo, foo)", "getFoo :: Foo -> Int", "getFoo x = x.foo"] + let before = T.unlines ["module A where", "import B (Foo)", "getFoo :: Foo -> Int", "getFoo x = x.foo"] + after = T.unlines ["module A where", "import B (Foo, foo)", "getFoo :: Foo -> Int", "getFoo x = x.foo"] cradle = "cradle: {direct: {arguments: [-hide-all-packages, -package, base, -package, text, -package-env, -, A, B]}}" liftIO $ writeFileUTF8 (dir "hie.yaml") cradle liftIO $ writeFileUTF8 (dir "B.hs") $ unlines ["module B where", "data Foo = Foo { foo :: Int }"] doc <- createDoc "Test.hs" "haskell" before waitForProgressDone _ <- waitForDiagnostics - let defLine = fromIntegral $ 1 + 2 + let defLine = 3 + range = Range (Position defLine 0) (Position defLine maxBound) + actions <- getCodeActions doc range + action <- pickActionWithTitle "Add foo to the import list of B" actions + executeCodeAction action + contentAfterAction <- documentContents doc + liftIO $ after @=? contentAfterAction + + theTestIndirect qualifiedGhcRecords polymorphicType = testGroup + ((if qualifiedGhcRecords then "qualified-" else "unqualified-") + <> ("HasField " :: String) + <> + (if polymorphicType then "polymorphic-" else "monomorphic-") + <> "type ") + . (\x -> [x]) $ testSessionWithExtraFiles "hover" def $ \dir -> do + -- Hopefully enable project indexing? + configureCheckProject True + + let + before = T.unlines ["{-# LANGUAGE OverloadedRecordDot #-}", "module A where", if qualifiedGhcRecords then "" else "import GHC.Records", "import C (bar)", "spam = bar.foo"] + after = T.unlines ["{-# LANGUAGE OverloadedRecordDot #-}", "module A where", if qualifiedGhcRecords then "" else "import GHC.Records", "import C (bar)", "import B (Foo(..))", "spam = bar.foo"] + cradle = "cradle: {direct: {arguments: [-hide-all-packages, -package, base, -package, text, -package-env, -, A, B, C]}}" + liftIO $ writeFileUTF8 (dir "hie.yaml") cradle + liftIO $ writeFileUTF8 (dir "B.hs") $ unlines ["module B where", if polymorphicType then "data Foo x = Foo { foo :: x }" else "data Foo = Foo { foo :: Int }"] + liftIO $ writeFileUTF8 (dir "C.hs") $ unlines ["module C where", "import B", "bar = Foo 10" ] + doc <- createDoc "Test.hs" "haskell" before + waitForProgressDone + _ <- waitForDiagnostics + let defLine = 4 + range = Range (Position defLine 0) (Position defLine maxBound) + actions <- getCodeActions doc range + action <- pickActionWithTitle "import B (Foo(..))" actions + executeCodeAction action + contentAfterAction <- documentContents doc + liftIO $ after @=? contentAfterAction + +suggestAddRecordFieldUpdateImportTests :: TestTree +suggestAddRecordFieldUpdateImportTests = testGroup "suggest imports of record fields in update" + [ testGroup "implicit import of type" [theTest ] ] + where + theTest = testSessionWithExtraFiles "hover" def $ \dir -> do + configureCheckProject True + + let + before = T.unlines ["module C where", "import B", "biz = bar { foo = 100 }"] + after = T.unlines ["module C where", "import B", "import A (Foo(..))", "biz = bar { foo = 100 }"] + cradle = "cradle: {direct: {arguments: [-hide-all-packages, -package, base, -package, text, -package-env, -, A, B, C]}}" + liftIO $ writeFileUTF8 (dir "hie.yaml") cradle + liftIO $ writeFileUTF8 (dir "A.hs") $ unlines ["module A where", "data Foo = Foo { foo :: Int }"] + liftIO $ writeFileUTF8 (dir "B.hs") $ unlines ["module B where", "import A", "bar = Foo 10" ] + doc <- createDoc "Test.hs" "haskell" before + waitForProgressDone + diags <- waitForDiagnostics + liftIO $ print diags + let defLine = 2 + range = Range (Position defLine 0) (Position defLine maxBound) + actions <- getCodeActions doc range + liftIO $ print actions + action <- pickActionWithTitle "import A (Foo(..))" actions + executeCodeAction action + contentAfterAction <- documentContents doc + liftIO $ after @=? contentAfterAction + +extractNotInScopeNameTests :: TestTree +extractNotInScopeNameTests = + testGroup "extractNotInScopeName" [ + testGroup "record field" [ + testCase ">=ghc 910" $ Refactor.extractNotInScopeName "Not in scope: ‘foo’" @=? Just (NotInScopeThing "foo"), + testCase " do + configureCheckProject False + let before = T.unlines ["module A where", "import Data.Coerce (coerce)", "import Data.Semigroup (Sum)", "bar = coerce (10 :: Int) :: Sum Int"] + after = T.unlines ["module A where", "import Data.Coerce (coerce)", "import Data.Semigroup (Sum)", "import Data.Semigroup (Sum(..))", "bar = coerce (10 :: Int) :: Sum Int"] + cradle = "cradle: {direct: {arguments: [-hide-all-packages, -package, base, -package, text, -package-env, -, A]}}" + liftIO $ writeFileUTF8 (dir "hie.yaml") cradle + doc <- createDoc "Test.hs" "haskell" before + waitForProgressDone + _ <- waitForDiagnostics + let defLine = 3 + range = Range (Position defLine 0) (Position defLine maxBound) + actions <- getCodeActions doc range + action <- pickActionWithTitle "import Data.Semigroup (Sum(..))" actions + executeCodeAction action + contentAfterAction <- documentContents doc + liftIO $ after @=? contentAfterAction + +suggestAddGenericMissingConstructorImportTests :: TestTree +suggestAddGenericMissingConstructorImportTests = testGroup "suggest imports of type constructors when using generic deriving" + [ testGroup "The type constructors are suggested when not in scope" + [ theTest + ] + ] + where + theTest = testSessionWithExtraFiles "hover" def $ \dir -> do + configureCheckProject False + let + before = T.unlines ["module A where", "import GHC.Generics", "import Data.Semigroup (Sum)", "deriving instance Generic (Sum Int)"] + after = T.unlines ["module A where", "import GHC.Generics", "import Data.Semigroup (Sum)", "import Data.Semigroup (Sum(..))", "deriving instance Generic (Sum Int)"] + cradle = "cradle: {direct: {arguments: [-hide-all-packages, -package, base, -package, text, -package-env, -, A]}}" + liftIO $ writeFileUTF8 (dir "hie.yaml") cradle + doc <- createDoc "Test.hs" "haskell" before + waitForProgressDone + _ <- waitForDiagnostics + let defLine = 3 range = Range (Position defLine 0) (Position defLine maxBound) actions <- getCodeActions doc range - action <- liftIO $ pickActionWithTitle "Add foo to the import list of B" actions + action <- pickActionWithTitle "import Data.Semigroup (Sum(..))" actions executeCodeAction action contentAfterAction <- documentContents doc liftIO $ after @=? contentAfterAction @@ -1809,7 +2056,6 @@ suggestImportDisambiguationTests = testGroup "suggest import disambiguation acti compareTwo "HidePreludeIndented.hs" [(3,8)] "Use AVec for ++, hiding other imports" "HidePreludeIndented.expected.hs" - ] , testGroup "Vec (type)" [ testCase "AVec" $ @@ -1890,15 +2136,14 @@ suggestImportDisambiguationTests = testGroup "suggest import disambiguation acti withTarget original locs $ \dir doc actions -> do expected <- liftIO $ readFileUtf8 (dir expected) - action <- liftIO $ pickActionWithTitle cmd actions + action <- pickActionWithTitle cmd actions executeCodeAction action contentAfterAction <- documentContents doc liftIO $ T.replace "\r\n" "\n" expected @=? contentAfterAction compareHideFunctionTo = compareTwo "HideFunction.hs" - auxFiles = ["AVec.hs", "BVec.hs", "CVec.hs", "DVec.hs", "EVec.hs", "FVec.hs"] withTarget file locs k = runWithExtraFiles "hiding" $ \dir -> do doc <- openDoc file "haskell" - void $ expectDiagnostics [(file, [(DiagnosticSeverity_Error, loc, "Ambiguous occurrence") | loc <- locs])] + void $ expectDiagnostics [(file, [(DiagnosticSeverity_Error, loc, "Ambiguous occurrence", Nothing) | loc <- locs])] -- Since GHC 9.8: GHC-87110 actions <- getAllCodeActions doc k dir doc actions withHideFunction = withTarget ("HideFunction" <.> "hs") @@ -2053,7 +2298,7 @@ suggestHideShadowTests = where testOneCodeAction testName actionName start end origin expected = helper testName start end origin expected $ \cas -> do - action <- liftIO $ pickActionWithTitle actionName cas + action <- pickActionWithTitle actionName cas executeCodeAction action noCodeAction testName start end origin = helper testName start end origin origin $ \cas -> do @@ -2104,10 +2349,8 @@ insertNewDefinitionTests = testGroup "insert new definition actions" ] docB <- createDoc "ModuleB.hs" "haskell" (T.unlines $ txtB ++ txtB') _ <- waitForDiagnostics - InR action@CodeAction { _title = actionTitle } : _ - <- filter (\(InR CodeAction{_title=x}) -> "Define" `T.isPrefixOf` x) <$> - getCodeActions docB (R 0 0 0 50) - liftIO $ actionTitle @?= "Define select :: [Bool] -> Bool" + action <- pickActionWithTitle "Define select :: [Bool] -> Bool" + =<< getCodeActions docB (R 0 0 0 50) executeCodeAction action contentAfterAction <- documentContents docB liftIO $ contentAfterAction @?= T.unlines (txtB ++ @@ -2116,6 +2359,26 @@ insertNewDefinitionTests = testGroup "insert new definition actions" , "select = _" ] ++ txtB') + , testSession "insert new function definition - with similar suggestion in scope" $ do + doc <- createDoc "Module.hs" "haskell" $ T.unlines + [ "import Control.Monad" -- brings `mplus` into scope, leading to additional suggestion + -- "Perhaps use \8216mplus\8217 (imported from Control.Monad)" + , "f :: Int -> Int" + , "f x = plus x x" + ] + _ <- waitForDiagnostics + action <- pickActionWithTitle "Define plus :: Int -> Int -> Int" + =<< getCodeActions doc (R 2 0 2 13) + executeCodeAction action + contentAfterAction <- documentContents doc + liftIO $ contentAfterAction @?= T.unlines + [ "import Control.Monad" + , "f :: Int -> Int" + , "f x = plus x x" + , "" + , "plus :: Int -> Int -> Int" + , "plus = _" + ] , testSession "define a hole" $ do let txtB = ["foo True = _select [True]" @@ -2128,10 +2391,8 @@ insertNewDefinitionTests = testGroup "insert new definition actions" ] docB <- createDoc "ModuleB.hs" "haskell" (T.unlines $ txtB ++ txtB') _ <- waitForDiagnostics - InR action@CodeAction { _title = actionTitle } : _ - <- filter (\(InR CodeAction{_title=x}) -> "Define" `T.isPrefixOf` x) <$> - getCodeActions docB (R 0 0 0 50) - liftIO $ actionTitle @?= "Define select :: [Bool] -> Bool" + action <- pickActionWithTitle "Define select :: [Bool] -> Bool" + =<< getCodeActions docB (R 0 0 0 50) executeCodeAction action contentAfterAction <- documentContents docB liftIO $ contentAfterAction @?= T.unlines ( @@ -2144,7 +2405,7 @@ insertNewDefinitionTests = testGroup "insert new definition actions" ] ++ txtB') , testSession "insert new function definition - Haddock comments" $ do - let start = ["foo :: Int -> Bool" + let start = [ "foo :: Int -> Bool" , "foo x = select (x + 1)" , "" , "-- | This is a haddock comment" @@ -2159,13 +2420,12 @@ insertNewDefinitionTests = testGroup "insert new definition actions" , "" , "-- | This is a haddock comment" , "haddock :: Int -> Int" - , "haddock = undefined"] + , "haddock = undefined" + ] docB <- createDoc "ModuleB.hs" "haskell" (T.unlines start) _ <- waitForDiagnostics - InR action@CodeAction { _title = actionTitle } : _ - <- filter (\(InR CodeAction{_title=x}) -> "Define" `T.isPrefixOf` x) <$> - getCodeActions docB (R 1 0 0 50) - liftIO $ actionTitle @?= "Define select :: Int -> Bool" + action <- pickActionWithTitle "Define select :: Int -> Bool" + =<< getCodeActions docB (R 1 8 1 14) executeCodeAction action contentAfterAction <- documentContents docB liftIO $ contentAfterAction @?= T.unlines expected @@ -2188,10 +2448,8 @@ insertNewDefinitionTests = testGroup "insert new definition actions" , "normal = undefined"] docB <- createDoc "ModuleB.hs" "haskell" (T.unlines start) _ <- waitForDiagnostics - InR action@CodeAction { _title = actionTitle } : _ - <- filter (\(InR CodeAction{_title=x}) -> "Define" `T.isPrefixOf` x) <$> - getCodeActions docB (R 1 0 0 50) - liftIO $ actionTitle @?= "Define select :: Int -> Bool" + action <- pickActionWithTitle "Define select :: Int -> Bool" + =<< getCodeActions docB (R 1 8 1 14) executeCodeAction action contentAfterAction <- documentContents docB liftIO $ contentAfterAction @?= T.unlines expected @@ -2205,10 +2463,7 @@ insertNewDefinitionTests = testGroup "insert new definition actions" ] docB <- createDoc "ModuleB.hs" "haskell" (T.unlines $ txtB ++ txtB') _ <- waitForDiagnostics - InR action@CodeAction { _title = actionTitle } : _ - <- filter (\(InR CodeAction{_title=x}) -> "Define" `T.isPrefixOf` x) <$> - getCodeActions docB (R 0 0 0 50) - liftIO $ actionTitle @?= "Define select :: _" + action <- pickActionWithTitle "Define select :: _" =<< getCodeActions docB (R 0 0 0 50) executeCodeAction action contentAfterAction <- documentContents docB liftIO $ contentAfterAction @?= T.unlines (txtB ++ @@ -2219,308 +2474,269 @@ insertNewDefinitionTests = testGroup "insert new definition actions" ++ txtB') ] + deleteUnusedDefinitionTests :: TestTree deleteUnusedDefinitionTests = testGroup "delete unused definition action" [ testSession "delete unused top level binding" $ testFor - (T.unlines [ "{-# OPTIONS_GHC -Wunused-top-binds #-}" - , "module A (some) where" - , "" - , "f :: Int -> Int" - , "f 1 = let a = 1" - , " in a" - , "f 2 = 2" - , "" - , "some = ()" - ]) - (4, 0) - "Delete ‘f’" - (T.unlines [ - "{-# OPTIONS_GHC -Wunused-top-binds #-}" - , "module A (some) where" - , "" - , "some = ()" - ]) - + [ "{-# OPTIONS_GHC -Wunused-top-binds #-}" + , "module A (some) where" + , "" + , "f :: Int -> Int" + , "f 1 = let a = 1" + , " in a" + , "f 2 = 2" + , "" + , "some = ()" + ] + (4, 0) + "Delete ‘f’" + [ "{-# OPTIONS_GHC -Wunused-top-binds #-}" + , "module A (some) where" + , "" + , "some = ()" + ] , testSession "delete unused top level binding defined in infix form" $ testFor - (T.unlines [ "{-# OPTIONS_GHC -Wunused-top-binds #-}" - , "module A (some) where" - , "" - , "myPlus :: Int -> Int -> Int" - , "a `myPlus` b = a + b" - , "" - , "some = ()" - ]) - (4, 2) - "Delete ‘myPlus’" - (T.unlines [ - "{-# OPTIONS_GHC -Wunused-top-binds #-}" - , "module A (some) where" - , "" - , "some = ()" - ]) + [ "{-# OPTIONS_GHC -Wunused-top-binds #-}" + , "module A (some) where" + , "" + , "myPlus :: Int -> Int -> Int" + , "a `myPlus` b = a + b" + , "" + , "some = ()" + ] + (4, 2) + "Delete ‘myPlus’" + [ "{-# OPTIONS_GHC -Wunused-top-binds #-}" + , "module A (some) where" + , "" + , "some = ()" + ] , testSession "delete unused binding in where clause" $ testFor - (T.unlines [ "{-# OPTIONS_GHC -Wunused-binds #-}" - , "module A (h, g) where" - , "" - , "h :: Int" - , "h = 3" - , "" - , "g :: Int" - , "g = 6" - , " where" - , " h :: Int" - , " h = 4" - , "" - ]) - (10, 4) - "Delete ‘h’" - (T.unlines [ "{-# OPTIONS_GHC -Wunused-binds #-}" - , "module A (h, g) where" - , "" - , "h :: Int" - , "h = 3" - , "" - , "g :: Int" - , "g = 6" - , " where" - , "" - ]) + [ "{-# OPTIONS_GHC -Wunused-binds #-}" + , "module A (h, g) where" + , "" + , "h :: Int" + , "h = 3" + , "" + , "g :: Int" + , "g = 6" + , " where" + , " h :: Int" + , " h = 4" + , "" + ] + (10, 4) + "Delete ‘h’" + [ "{-# OPTIONS_GHC -Wunused-binds #-}" + , "module A (h, g) where" + , "" + , "h :: Int" + , "h = 3" + , "" + , "g :: Int" + , "g = 6" + , " where" + , "" + ] , testSession "delete unused binding with multi-oneline signatures front" $ testFor - (T.unlines [ "{-# OPTIONS_GHC -Wunused-binds #-}" - , "module A (b, c) where" - , "" - , "a, b, c :: Int" - , "a = 3" - , "b = 4" - , "c = 5" - ]) - (4, 0) - "Delete ‘a’" - (T.unlines [ "{-# OPTIONS_GHC -Wunused-binds #-}" - , "module A (b, c) where" - , "" - , "b, c :: Int" - , "b = 4" - , "c = 5" - ]) + [ "{-# OPTIONS_GHC -Wunused-binds #-}" + , "module A (b, c) where" + , "" + , "a, b, c :: Int" + , "a = 3" + , "b = 4" + , "c = 5" + ] + (4, 0) + "Delete ‘a’" + [ "{-# OPTIONS_GHC -Wunused-binds #-}" + , "module A (b, c) where" + , "" + , "b, c :: Int" + , "b = 4" + , "c = 5" + ] , testSession "delete unused binding with multi-oneline signatures mid" $ testFor - (T.unlines [ "{-# OPTIONS_GHC -Wunused-binds #-}" - , "module A (a, c) where" - , "" - , "a, b, c :: Int" - , "a = 3" - , "b = 4" - , "c = 5" - ]) - (5, 0) - "Delete ‘b’" - (T.unlines [ "{-# OPTIONS_GHC -Wunused-binds #-}" - , "module A (a, c) where" - , "" - , "a, c :: Int" - , "a = 3" - , "c = 5" - ]) + [ "{-# OPTIONS_GHC -Wunused-binds #-}" + , "module A (a, c) where" + , "" + , "a, b, c :: Int" + , "a = 3" + , "b = 4" + , "c = 5" + ] + (5, 0) + "Delete ‘b’" + [ "{-# OPTIONS_GHC -Wunused-binds #-}" + , "module A (a, c) where" + , "" + , "a, c :: Int" + , "a = 3" + , "c = 5" + ] , testSession "delete unused binding with multi-oneline signatures end" $ testFor - (T.unlines [ "{-# OPTIONS_GHC -Wunused-binds #-}" - , "module A (a, b) where" - , "" - , "a, b, c :: Int" - , "a = 3" - , "b = 4" - , "c = 5" - ]) - (6, 0) - "Delete ‘c’" - (T.unlines [ "{-# OPTIONS_GHC -Wunused-binds #-}" - , "module A (a, b) where" - , "" - , "a, b :: Int" - , "a = 3" - , "b = 4" - ]) + [ "{-# OPTIONS_GHC -Wunused-binds #-}" + , "module A (a, b) where" + , "" + , "a, b, c :: Int" + , "a = 3" + , "b = 4" + , "c = 5" + ] + (6, 0) + "Delete ‘c’" + [ "{-# OPTIONS_GHC -Wunused-binds #-}" + , "module A (a, b) where" + , "" + , "a, b :: Int" + , "a = 3" + , "b = 4" + ] ] where - testFor source pos expectedTitle expectedResult = do - docId <- createDoc "A.hs" "haskell" source - expectDiagnostics [ ("A.hs", [(DiagnosticSeverity_Warning, pos, "not used")]) ] - - (action, title) <- extractCodeAction docId "Delete" pos - - liftIO $ title @?= expectedTitle + testFor sourceLines pos@(l,c) expectedTitle expectedLines = do + docId <- createDoc "A.hs" "haskell" $ T.unlines sourceLines + expectDiagnostics [ ("A.hs", [(DiagnosticSeverity_Warning, pos, "not used", Nothing)]) ] + action <- pickActionWithTitle expectedTitle =<< getCodeActions docId (R l c l c) executeCodeAction action contentAfterAction <- documentContents docId - liftIO $ contentAfterAction @?= expectedResult - - extractCodeAction docId actionPrefix (l, c) = do - [action@CodeAction { _title = actionTitle }] <- findCodeActionsByPrefix docId (R l c l c) [actionPrefix] - return (action, actionTitle) + liftIO $ contentAfterAction @?= T.unlines expectedLines addTypeAnnotationsToLiteralsTest :: TestTree addTypeAnnotationsToLiteralsTest = testGroup "add type annotations to literals to satisfy constraints" - [ - testSession "add default type to satisfy one constraint" $ + [ testSession "add default type to satisfy one constraint" $ testFor - (T.unlines [ "{-# OPTIONS_GHC -Wtype-defaults #-}" - , "module A (f) where" - , "" - , "f = 1" - ]) -#if MIN_VERSION_ghc(9,4,0) - [ (DiagnosticSeverity_Warning, (3, 4), "Defaulting the type variable") ] -#else - [ (DiagnosticSeverity_Warning, (3, 4), "Defaulting the following constraint") ] -#endif - "Add type annotation ‘Integer’ to ‘1’" - (T.unlines [ "{-# OPTIONS_GHC -Wtype-defaults #-}" - , "module A (f) where" - , "" - , "f = (1 :: Integer)" - ]) + [ "{-# OPTIONS_GHC -Wtype-defaults #-}" + , "module A (f) where" + , "" + , "f = 1" + ] + [ (DiagnosticSeverity_Warning, (3, 4), "Defaulting the type variable", Nothing) ] + "Add type annotation ‘Integer’ to ‘1’" + [ "{-# OPTIONS_GHC -Wtype-defaults #-}" + , "module A (f) where" + , "" + , "f = (1 :: Integer)" + ] , testSession "add default type to satisfy one constraint in nested expressions" $ testFor - (T.unlines [ "{-# OPTIONS_GHC -Wtype-defaults #-}" - , "module A where" - , "" - , "f =" - , " let x = 3" - , " in x" - ]) -#if MIN_VERSION_ghc(9,4,0) - [ (DiagnosticSeverity_Warning, (4, 12), "Defaulting the type variable") ] -#else - [ (DiagnosticSeverity_Warning, (4, 12), "Defaulting the following constraint") ] -#endif - "Add type annotation ‘Integer’ to ‘3’" - (T.unlines [ "{-# OPTIONS_GHC -Wtype-defaults #-}" - , "module A where" - , "" - , "f =" - , " let x = (3 :: Integer)" - , " in x" - ]) + [ "{-# OPTIONS_GHC -Wtype-defaults #-}" + , "module A where" + , "" + , "f =" + , " let x = 3" + , " in x" + ] + [ (DiagnosticSeverity_Warning, (4, 12), "Defaulting the type variable", Nothing) ] + "Add type annotation ‘Integer’ to ‘3’" + [ "{-# OPTIONS_GHC -Wtype-defaults #-}" + , "module A where" + , "" + , "f =" + , " let x = (3 :: Integer)" + , " in x" + ] , testSession "add default type to satisfy one constraint in more nested expressions" $ testFor - (T.unlines [ "{-# OPTIONS_GHC -Wtype-defaults #-}" - , "module A where" - , "" - , "f =" - , " let x = let y = 5 in y" - , " in x" - ]) -#if MIN_VERSION_ghc(9,4,0) - [ (DiagnosticSeverity_Warning, (4, 20), "Defaulting the type variable") ] -#else - [ (DiagnosticSeverity_Warning, (4, 20), "Defaulting the following constraint") ] -#endif - "Add type annotation ‘Integer’ to ‘5’" - (T.unlines [ "{-# OPTIONS_GHC -Wtype-defaults #-}" - , "module A where" - , "" - , "f =" - , " let x = let y = (5 :: Integer) in y" - , " in x" - ]) + [ "{-# OPTIONS_GHC -Wtype-defaults #-}" + , "module A where" + , "" + , "f =" + , " let x = let y = 5 in y" + , " in x" + ] + [ (DiagnosticSeverity_Warning, (4, 20), "Defaulting the type variable", Nothing) ] + "Add type annotation ‘Integer’ to ‘5’" + [ "{-# OPTIONS_GHC -Wtype-defaults #-}" + , "module A where" + , "" + , "f =" + , " let x = let y = (5 :: Integer) in y" + , " in x" + ] , testSession "add default type to satisfy one constraint with duplicate literals" $ testFor - (T.unlines [ "{-# OPTIONS_GHC -Wtype-defaults #-}" - , "{-# LANGUAGE OverloadedStrings #-}" - , "module A (f) where" - , "" - , "import Debug.Trace" - , "" - , "f = seq \"debug\" traceShow \"debug\"" - ]) -#if MIN_VERSION_ghc(9,4,0) - [ (DiagnosticSeverity_Warning, (6, 8), "Defaulting the type variable") - , (DiagnosticSeverity_Warning, (6, 16), "Defaulting the type variable") - ] -#else - [ (DiagnosticSeverity_Warning, (6, 8), "Defaulting the following constraint") - , (DiagnosticSeverity_Warning, (6, 16), "Defaulting the following constraint") - ] -#endif - ("Add type annotation ‘" <> listOfChar <> "’ to ‘\"debug\"’") - (T.unlines [ "{-# OPTIONS_GHC -Wtype-defaults #-}" - , "{-# LANGUAGE OverloadedStrings #-}" - , "module A (f) where" - , "" - , "import Debug.Trace" - , "" - , "f = seq (\"debug\" :: " <> listOfChar <> ") traceShow \"debug\"" - ]) + [ "{-# OPTIONS_GHC -Wtype-defaults #-}" + , "{-# LANGUAGE OverloadedStrings #-}" + , "module A (f) where" + , "" + , "import Debug.Trace" + , "" + , "f = seq \"debug\" traceShow \"debug\"" + ] + [ (DiagnosticSeverity_Warning, (6, 8), "Defaulting the type variable", Nothing) + , (DiagnosticSeverity_Warning, (6, 16), "Defaulting the type variable", Nothing) + ] + ("Add type annotation ‘" <> stringLit <> "’ to ‘\"debug\"’") + [ "{-# OPTIONS_GHC -Wtype-defaults #-}" + , "{-# LANGUAGE OverloadedStrings #-}" + , "module A (f) where" + , "" + , "import Debug.Trace" + , "" + , "f = seq (\"debug\" :: "<> stringLit <> ") traceShow \"debug\"" + ] , testSession "add default type to satisfy two constraints" $ testFor - (T.unlines [ "{-# OPTIONS_GHC -Wtype-defaults #-}" - , "{-# LANGUAGE OverloadedStrings #-}" - , "module A (f) where" - , "" - , "import Debug.Trace" - , "" - , "f a = traceShow \"debug\" a" - ]) -#if MIN_VERSION_ghc(9,4,0) - [ (DiagnosticSeverity_Warning, (6, 6), "Defaulting the type variable") ] -#else - [ (DiagnosticSeverity_Warning, (6, 6), "Defaulting the following constraint") ] -#endif - ("Add type annotation ‘" <> listOfChar <> "’ to ‘\"debug\"’") - (T.unlines [ "{-# OPTIONS_GHC -Wtype-defaults #-}" - , "{-# LANGUAGE OverloadedStrings #-}" - , "module A (f) where" - , "" - , "import Debug.Trace" - , "" - , "f a = traceShow (\"debug\" :: " <> listOfChar <> ") a" - ]) + [ "{-# OPTIONS_GHC -Wtype-defaults #-}" + , "{-# LANGUAGE OverloadedStrings #-}" + , "module A (f) where" + , "" + , "import Debug.Trace" + , "" + , "f a = traceShow \"debug\" a" + ] + [ (DiagnosticSeverity_Warning, (6, 6), "Defaulting the type variable", Nothing) ] + ("Add type annotation ‘" <> stringLit <> "’ to ‘\"debug\"’") + [ "{-# OPTIONS_GHC -Wtype-defaults #-}" + , "{-# LANGUAGE OverloadedStrings #-}" + , "module A (f) where" + , "" + , "import Debug.Trace" + , "" + , "f a = traceShow (\"debug\" :: " <> stringLit <> ") a" + ] , testSession "add default type to satisfy two constraints with duplicate literals" $ testFor - (T.unlines [ "{-# OPTIONS_GHC -Wtype-defaults #-}" - , "{-# LANGUAGE OverloadedStrings #-}" - , "module A (f) where" - , "" - , "import Debug.Trace" - , "" - , "f = seq (\"debug\" :: [Char]) (seq (\"debug\" :: [Char]) (traceShow \"debug\"))" - ]) -#if MIN_VERSION_ghc(9,4,0) - [ (DiagnosticSeverity_Warning, (6, 54), "Defaulting the type variable") ] -#else - [ (DiagnosticSeverity_Warning, (6, 54), "Defaulting the following constraint") ] -#endif - ("Add type annotation ‘" <> listOfChar <> "’ to ‘\"debug\"’") - (T.unlines [ "{-# OPTIONS_GHC -Wtype-defaults #-}" - , "{-# LANGUAGE OverloadedStrings #-}" - , "module A (f) where" - , "" - , "import Debug.Trace" - , "" - , "f = seq (\"debug\" :: [Char]) (seq (\"debug\" :: [Char]) (traceShow (\"debug\" :: " <> listOfChar <> ")))" - ]) + [ "{-# OPTIONS_GHC -Wtype-defaults #-}" + , "{-# LANGUAGE OverloadedStrings #-}" + , "module A (f) where" + , "" + , "import Debug.Trace" + , "" + , "f = seq (\"debug\" :: [Char]) (seq (\"debug\" :: [Char]) (traceShow \"debug\"))" + ] + [ (DiagnosticSeverity_Warning, (6, 54), "Defaulting the type variable", Nothing) ] + ("Add type annotation ‘"<> stringLit <>"’ to ‘\"debug\"’") + [ "{-# OPTIONS_GHC -Wtype-defaults #-}" + , "{-# LANGUAGE OverloadedStrings #-}" + , "module A (f) where" + , "" + , "import Debug.Trace" + , "" + , "f = seq (\"debug\" :: [Char]) (seq (\"debug\" :: [Char]) (traceShow (\"debug\" :: "<> stringLit <> ")))" + ] ] where - testFor source diag expectedTitle expectedResult = do - docId <- createDoc "A.hs" "haskell" source + stringLit = if ghcVersion >= GHC912 then "[Char]" else "String" + testFor sourceLines diag expectedTitle expectedLines = do + docId <- createDoc "A.hs" "haskell" $ T.unlines sourceLines expectDiagnostics [ ("A.hs", diag) ] + let cursors = map (\(_, snd, _, _) -> snd) diag + (ls, cs) = minimum cursors + (le, ce) = maximum cursors - let cursors = map snd3 diag - (action, title) <- extractCodeAction docId "Add type annotation" (minimum cursors) (maximum cursors) - - liftIO $ title @?= expectedTitle + action <- pickActionWithTitle expectedTitle =<< getCodeActions docId (R ls cs le ce) executeCodeAction action contentAfterAction <- documentContents docId - liftIO $ contentAfterAction @?= expectedResult - - extractCodeAction docId actionPrefix (l,c) (l', c')= do - [action@CodeAction { _title = actionTitle }] <- findCodeActionsByPrefix docId (R l c l' c') [actionPrefix] - return (action, actionTitle) + liftIO $ contentAfterAction @?= T.unlines expectedLines fixConstructorImportTests :: TestTree @@ -2534,7 +2750,7 @@ fixConstructorImportTests = testGroup "fix import actions" [ "module ModuleB where" , "import ModuleA(Constructor)" ]) - (Range (Position 1 10) (Position 1 11)) + (Range (Position 1 15) (Position 1 26)) "Fix import of A(Constructor)" (T.unlines [ "module ModuleB where" @@ -2545,35 +2761,27 @@ fixConstructorImportTests = testGroup "fix import actions" template contentA contentB range expectedAction expectedContentB = do _docA <- createDoc "ModuleA.hs" "haskell" contentA docB <- createDoc "ModuleB.hs" "haskell" contentB - _diags <- waitForDiagnostics - InR action@CodeAction { _title = actionTitle } : _ - <- sortOn (\(InR CodeAction{_title=x}) -> x) <$> - getCodeActions docB range - liftIO $ expectedAction @=? actionTitle + _ <- waitForDiagnostics + action <- pickActionWithTitle expectedAction =<< getCodeActions docB range executeCodeAction action contentAfterAction <- documentContents docB liftIO $ expectedContentB @=? contentAfterAction importRenameActionTests :: TestTree -importRenameActionTests = testGroup "import rename actions" - [ testSession "Data.Mape -> Data.Map" $ check "Map" - , testSession "Data.Mape -> Data.Maybe" $ check "Maybe" ] where - check modname = do - let content = T.unlines - [ "module Testing where" - , "import Data.Mape" - ] - doc <- createDoc "Testing.hs" "haskell" content - _ <- waitForDiagnostics - actionsOrCommands <- getCodeActions doc (Range (Position 1 8) (Position 1 16)) - let [changeToMap] = [action | InR action@CodeAction{ _title = actionTitle } <- actionsOrCommands, ("Data." <> modname) `T.isInfixOf` actionTitle ] - executeCodeAction changeToMap - contentAfterAction <- documentContents doc - let expectedContentAfterAction = T.unlines - [ "module Testing where" - , "import Data." <> modname - ] - liftIO $ expectedContentAfterAction @=? contentAfterAction +importRenameActionTests = testGroup "import rename actions" $ + fmap check ["Map", "Maybe"] + where + check modname = checkCodeAction + ("Data.Mape -> Data." <> T.unpack modname) + ("Replace with Data." <> modname) + (T.unlines + [ "module Testing where" + , "import Data.Mape" + ]) + (T.unlines + [ "module Testing where" + , "import Data." <> modname + ]) fillTypedHoleTests :: TestTree fillTypedHoleTests = let @@ -2581,20 +2789,19 @@ fillTypedHoleTests = let sourceCode :: T.Text -> T.Text -> T.Text -> T.Text sourceCode a b c = T.unlines [ "module Testing where" - , "" - , "globalConvert :: Int -> String" - , "globalConvert = undefined" - , "" - , "globalInt :: Int" - , "globalInt = 3" - , "" - , "bar :: Int -> Int -> String" - , "bar n parameterInt = " <> a <> " (n + " <> b <> " + " <> c <> ") where" - , " localConvert = (flip replicate) 'x'" - , "" - , "foo :: () -> Int -> String" - , "foo = undefined" - + , "" + , "globalConvert :: Int -> String" + , "globalConvert = undefined" + , "" + , "globalInt :: Int" + , "globalInt = 3" + , "" + , "bar :: Int -> Int -> String" + , "bar n parameterInt = " <> a <> " (n + " <> b <> " + " <> c <> ") where" + , " localConvert = (flip replicate) 'x'" + , "" + , "foo :: () -> Int -> String" + , "foo = undefined" ] check :: T.Text -> T.Text -> T.Text -> T.Text -> T.Text -> T.Text -> T.Text -> TestTree @@ -2606,39 +2813,39 @@ fillTypedHoleTests = let doc <- createDoc "Testing.hs" "haskell" originalCode _ <- waitForDiagnostics actionsOrCommands <- getCodeActions doc (Range (Position 9 0) (Position 9 maxBound)) - chosenAction <- liftIO $ pickActionWithTitle actionTitle actionsOrCommands + chosenAction <- pickActionWithTitle actionTitle actionsOrCommands executeCodeAction chosenAction modifiedCode <- documentContents doc liftIO $ expectedCode @=? modifiedCode in testGroup "fill typed holes" - [ check "replace _ with show" + [ check "Replace _ with show" "_" "n" "n" "show" "n" "n" - , check "replace _ with globalConvert" + , check "Replace _ with globalConvert" "_" "n" "n" "globalConvert" "n" "n" - , check "replace _convertme with localConvert" + , check "Replace _convertme with localConvert" "_convertme" "n" "n" "localConvert" "n" "n" - , check "replace _b with globalInt" + , check "Replace _b with globalInt" "_a" "_b" "_c" "_a" "globalInt" "_c" - , check "replace _c with globalInt" + , check "Replace _c with globalInt" "_a" "_b" "_c" "_a" "_b" "globalInt" - , check "replace _c with parameterInt" + , check "Replace _c with parameterInt" "_a" "_b" "_c" "_a" "_b" "parameterInt" - , check "replace _ with foo _" + , check "Replace _ with foo _" "_" "n" "n" "(foo _)" "n" "n" - , testSession "replace _toException with E.toException" $ do + , testSession "Replace _toException with E.toException" $ do let mkDoc x = T.unlines [ "module Testing where" , "import qualified Control.Exception as E" @@ -2647,7 +2854,7 @@ fillTypedHoleTests = let doc <- createDoc "Test.hs" "haskell" $ mkDoc "_toException" _ <- waitForDiagnostics actions <- getCodeActions doc (Range (Position 3 0) (Position 3 maxBound)) - chosen <- liftIO $ pickActionWithTitle "replace _toException with E.toException" actions + chosen <- pickActionWithTitle "Replace _toException with E.toException" actions executeCodeAction chosen modifiedCode <- documentContents doc liftIO $ mkDoc "E.toException" @=? modifiedCode @@ -2663,36 +2870,36 @@ fillTypedHoleTests = let doc <- createDoc "Test.hs" "haskell" $ mkDoc "`_`" _ <- waitForDiagnostics actions <- getCodeActions doc (Range (Position 5 16) (Position 5 19)) - chosen <- liftIO $ pickActionWithTitle "replace _ with foo" actions + chosen <- pickActionWithTitle "Replace _ with foo" actions executeCodeAction chosen modifiedCode <- documentContents doc liftIO $ mkDoc "`foo`" @=? modifiedCode , testSession "postfix hole uses postfix notation of infix operator" $ do let mkDoc x = T.unlines [ "module Testing where" - , "test :: Int -> Int -> Int" - , "test a1 a2 = " <> x <> " a1 a2" + , "test :: Int -> Maybe Int -> Maybe Int" + , "test a ma = " <> x <> " (a +) ma" ] doc <- createDoc "Test.hs" "haskell" $ mkDoc "_" _ <- waitForDiagnostics actions <- getCodeActions doc (Range (Position 2 13) (Position 2 14)) - chosen <- liftIO $ pickActionWithTitle "replace _ with (+)" actions + chosen <- pickActionWithTitle "Replace _ with (<$>)" actions executeCodeAction chosen modifiedCode <- documentContents doc - liftIO $ mkDoc "(+)" @=? modifiedCode + liftIO $ mkDoc "(<$>)" @=? modifiedCode , testSession "filling infix type hole uses infix operator" $ do let mkDoc x = T.unlines [ "module Testing where" - , "test :: Int -> Int -> Int" - , "test a1 a2 = a1 " <> x <> " a2" + , "test :: Int -> Maybe Int -> Maybe Int" + , "test a ma = (a +) " <> x <> " ma" ] doc <- createDoc "Test.hs" "haskell" $ mkDoc "`_`" _ <- waitForDiagnostics actions <- getCodeActions doc (Range (Position 2 16) (Position 2 19)) - chosen <- liftIO $ pickActionWithTitle "replace _ with (+)" actions + chosen <- pickActionWithTitle "Replace _ with (<$>)" actions executeCodeAction chosen modifiedCode <- documentContents doc - liftIO $ mkDoc "+" @=? modifiedCode + liftIO $ mkDoc "<$>" @=? modifiedCode ] addInstanceConstraintTests :: TestTree @@ -2734,14 +2941,8 @@ addInstanceConstraintTests = let ] check :: T.Text -> T.Text -> T.Text -> TestTree - check actionTitle originalCode expectedCode = testSession (T.unpack actionTitle) $ do - doc <- createDoc "Testing.hs" "haskell" originalCode - _ <- waitForDiagnostics - actionsOrCommands <- getAllCodeActions doc - chosenAction <- liftIO $ pickActionWithTitle actionTitle actionsOrCommands - executeCodeAction chosenAction - modifiedCode <- documentContents doc - liftIO $ expectedCode @=? modifiedCode + check actionTitle originalCode expectedCode = + checkCodeAction (T.unpack actionTitle) actionTitle originalCode expectedCode in testGroup "add instance constraint" [ check @@ -2885,12 +3086,12 @@ addFunctionConstraintTests = let (missingMonadConstraint "Monad m => ") ] -checkCodeAction :: String -> T.Text -> T.Text -> T.Text -> TestTree +checkCodeAction :: TestName -> T.Text -> T.Text -> T.Text -> TestTree checkCodeAction testName actionTitle originalCode expectedCode = testSession testName $ do doc <- createDoc "Testing.hs" "haskell" originalCode _ <- waitForDiagnostics actionsOrCommands <- getAllCodeActions doc - chosenAction <- liftIO $ pickActionWithTitle actionTitle actionsOrCommands + chosenAction <- pickActionWithTitle actionTitle actionsOrCommands executeCodeAction chosenAction modifiedCode <- documentContents doc liftIO $ expectedCode @=? modifiedCode @@ -3065,14 +3266,8 @@ removeRedundantConstraintsTests = let check :: T.Text -> T.Text -> T.Text -> TestTree - check actionTitle originalCode expectedCode = testSession (T.unpack actionTitle) $ do - doc <- createDoc "Testing.hs" "haskell" originalCode - _ <- waitForDiagnostics - actionsOrCommands <- getAllCodeActions doc - chosenAction <- liftIO $ pickActionWithTitle actionTitle actionsOrCommands - executeCodeAction chosenAction - modifiedCode <- documentContents doc - liftIO $ expectedCode @=? modifiedCode + check actionTitle originalCode expectedCode = + checkCodeAction (T.unpack actionTitle) actionTitle originalCode expectedCode in testGroup "remove redundant function constraints" [ check @@ -3142,10 +3337,14 @@ addSigActionTests = let doc <- createDoc "Sigs.hs" "haskell" originalCode _ <- waitForDiagnostics actionsOrCommands <- getCodeActions doc (Range (Position 5 1) (Position 5 maxBound)) - chosenAction <- liftIO $ pickActionWithTitle ("add signature: " <> sig) actionsOrCommands + chosenAction <- pickActionWithTitle ("add signature: " <> sig) actionsOrCommands executeCodeAction chosenAction modifiedCode <- documentContents doc liftIO $ expectedCode @=? modifiedCode + issue806 = if ghcVersion >= GHC912 then + "hello = print" >:: "hello :: GHC.Types.ZonkAny 0 -> IO ()" -- GHC now returns ZonkAny 0 instead of Any. https://gitlab.haskell.org/ghc/ghc/-/issues/25895 + else + "hello = print" >:: "hello :: GHC.Types.Any -> IO ()" -- Documents current behavior outlined in #806 in testGroup "add signature" [ "abc = True" >:: "abc :: Bool" @@ -3154,6 +3353,7 @@ addSigActionTests = let , "(!!!) a b = a > b" >:: "(!!!) :: Ord a => a -> a -> Bool" , "a >>>> b = a + b" >:: "(>>>>) :: Num a => a -> a -> a" , "a `haha` b = a b" >:: "haha :: (t1 -> t2) -> t1 -> t2" + , issue806 , "pattern Some a = Just a" >:: "pattern Some :: a -> Maybe a" , "pattern Some a <- Just a" >:: "pattern Some :: a -> Maybe a" , "pattern Some a <- Just a\n where Some a = Just a" >:: "pattern Some :: a -> Maybe a" @@ -3168,577 +3368,584 @@ addSigActionTests = let exportUnusedTests :: TestTree exportUnusedTests = testGroup "export unused actions" - [ testGroup "don't want suggestion" - [ testSession "implicit exports" $ template - (T.unlines - [ "{-# OPTIONS_GHC -Wunused-top-binds #-}" - , "{-# OPTIONS_GHC -Wmissing-signatures #-}" - , "module A where" - , "foo = id"]) - (R 3 0 3 3) - "Export ‘foo’" - Nothing -- codeaction should not be available - , testSession "not top-level" $ template - (T.unlines - [ "{-# OPTIONS_GHC -Wunused-top-binds #-}" - , "{-# OPTIONS_GHC -Wunused-binds #-}" - , "module A (foo,bar) where" - , "foo = ()" - , " where bar = ()" - , "bar = ()"]) - (R 2 0 2 11) - "Export ‘bar’" - Nothing - , ignoreForGhcVersions [GHC92, GHC94] "Diagnostic message has no suggestions" $ - testSession "type is exported but not the constructor of same name" $ template - (T.unlines - [ "{-# OPTIONS_GHC -Wunused-top-binds #-}" - , "module A (Foo) where" - , "data Foo = Foo"]) + [ testGroup "don't want suggestion" -- in this test group we check that no code actions are created + [ testSession "implicit exports" $ templateNoAction + [ "{-# OPTIONS_GHC -Wunused-top-binds #-}" + , "{-# OPTIONS_GHC -Wmissing-signatures #-}" + , "module A where" + , "foo = id" + ] + (R 3 0 3 3) + "Export ‘foo’" + , testSession "not top-level" $ templateNoAction + [ "{-# OPTIONS_GHC -Wunused-top-binds #-}" + , "{-# OPTIONS_GHC -Wunused-binds #-}" + , "module A (foo,bar) where" + , "foo = ()" + , " where bar = ()" + , "bar = ()" + ] + (R 2 0 2 11) + "Export ‘bar’" + , testSession "type is exported but not the constructor of same name" $ templateNoAction + [ "{-# OPTIONS_GHC -Wunused-top-binds #-}" + , "module A (Foo) where" + , "data Foo = Foo" + ] (R 2 0 2 8) "Export ‘Foo’" - Nothing -- codeaction should not be available - , testSession "unused data field" $ template - (T.unlines - [ "{-# OPTIONS_GHC -Wunused-top-binds #-}" - , "module A (Foo(Foo)) where" - , "data Foo = Foo {foo :: ()}"]) - (R 2 0 2 20) - "Export ‘foo’" - Nothing -- codeaction should not be available + , testSession "unused data field" $ templateNoAction + [ "{-# OPTIONS_GHC -Wunused-top-binds #-}" + , "module A (Foo(Foo)) where" + , "data Foo = Foo {foo :: ()}" + ] + (R 2 0 2 20) + "Export ‘foo’" ] , testGroup "want suggestion" [ testSession "empty exports" $ template - (T.unlines - [ "{-# OPTIONS_GHC -Wunused-top-binds #-}" - , "module A (" - , ") where" - , "foo = id"]) - (R 3 0 3 3) - "Export ‘foo’" - (Just $ T.unlines - [ "{-# OPTIONS_GHC -Wunused-top-binds #-}" - , "module A (" - , "foo) where" - , "foo = id"]) + [ "{-# OPTIONS_GHC -Wunused-top-binds #-}" + , "module A (" + , ") where" + , "foo = id" + ] + (R 3 0 3 3) + "Export ‘foo’" + [ "{-# OPTIONS_GHC -Wunused-top-binds #-}" + , "module A (" + , "foo) where" + , "foo = id" + ] , testSession "single line explicit exports" $ template - (T.unlines - [ "{-# OPTIONS_GHC -Wunused-top-binds #-}" - , "module A (foo) where" - , "foo = id" - , "bar = foo"]) - (R 3 0 3 3) - "Export ‘bar’" - (Just $ T.unlines - [ "{-# OPTIONS_GHC -Wunused-top-binds #-}" - , "module A (foo, bar) where" - , "foo = id" - , "bar = foo"]) + [ "{-# OPTIONS_GHC -Wunused-top-binds #-}" + , "module A (foo) where" + , "foo = id" + , "bar = foo" + ] + (R 3 0 3 3) + "Export ‘bar’" + [ "{-# OPTIONS_GHC -Wunused-top-binds #-}" + , "module A (foo, bar) where" + , "foo = id" + , "bar = foo" + ] , testSession "multi line explicit exports" $ template - (T.unlines - [ "{-# OPTIONS_GHC -Wunused-top-binds #-}" - , "module A" - , " (" - , " foo) where" - , "foo = id" - , "bar = foo"]) - (R 5 0 5 3) - "Export ‘bar’" - (Just $ T.unlines - [ "{-# OPTIONS_GHC -Wunused-top-binds #-}" - , "module A" - , " (" - , " foo, bar) where" - , "foo = id" - , "bar = foo"]) + [ "{-# OPTIONS_GHC -Wunused-top-binds #-}" + , "module A" + , " (" + , " foo) where" + , "foo = id" + , "bar = foo" + ] + (R 5 0 5 3) + "Export ‘bar’" + [ "{-# OPTIONS_GHC -Wunused-top-binds #-}" + , "module A" + , " (" + , " foo, bar) where" + , "foo = id" + , "bar = foo" + ] , testSession "export list ends in comma" $ template - (T.unlines - [ "{-# OPTIONS_GHC -Wunused-top-binds #-}" - , "module A" - , " (foo," - , " ) where" - , "foo = id" - , "bar = foo"]) - (R 5 0 5 3) - "Export ‘bar’" - (Just $ T.unlines - [ "{-# OPTIONS_GHC -Wunused-top-binds #-}" - , "module A" - , " (foo," - , " bar) where" - , "foo = id" - , "bar = foo"]) + [ "{-# OPTIONS_GHC -Wunused-top-binds #-}" + , "module A" + , " (foo," + , " ) where" + , "foo = id" + , "bar = foo" + ] + (R 5 0 5 3) + "Export ‘bar’" + [ "{-# OPTIONS_GHC -Wunused-top-binds #-}" + , "module A" + , " (foo," + , " bar) where" + , "foo = id" + , "bar = foo" + ] , testSession "style of multiple exports is preserved 1" $ template - (T.unlines - [ "{-# OPTIONS_GHC -Wunused-top-binds #-}" - , "module A" - , " ( foo" - , " , bar" - , " ) where" - , "foo = id" - , "bar = foo" - , "baz = bar" - ]) - (R 7 0 7 3) - "Export ‘baz’" - (Just $ T.unlines - [ "{-# OPTIONS_GHC -Wunused-top-binds #-}" - , "module A" - , " ( foo" - , " , bar" - , " , baz" - , " ) where" - , "foo = id" - , "bar = foo" - , "baz = bar" - ]) + [ "{-# OPTIONS_GHC -Wunused-top-binds #-}" + , "module A" + , " ( foo" + , " , bar" + , " ) where" + , "foo = id" + , "bar = foo" + , "baz = bar" + ] + (R 7 0 7 3) + "Export ‘baz’" + [ "{-# OPTIONS_GHC -Wunused-top-binds #-}" + , "module A" + , " ( foo" + , " , bar" + , " , baz" + , " ) where" + , "foo = id" + , "bar = foo" + , "baz = bar" + ] , testSession "style of multiple exports is preserved 2" $ template - (T.unlines - [ "{-# OPTIONS_GHC -Wunused-top-binds #-}" - , "module A" - , " ( foo," - , " bar" - , " ) where" - , "foo = id" - , "bar = foo" - , "baz = bar" - ]) - (R 7 0 7 3) - "Export ‘baz’" - (Just $ T.unlines - [ "{-# OPTIONS_GHC -Wunused-top-binds #-}" - , "module A" - , " ( foo," - , " bar," - , " baz" - , " ) where" - , "foo = id" - , "bar = foo" - , "baz = bar" - ]) + [ "{-# OPTIONS_GHC -Wunused-top-binds #-}" + , "module A" + , " ( foo," + , " bar" + , " ) where" + , "foo = id" + , "bar = foo" + , "baz = bar" + ] + (R 7 0 7 3) + "Export ‘baz’" + [ "{-# OPTIONS_GHC -Wunused-top-binds #-}" + , "module A" + , " ( foo," + , " bar," + , " baz" + , " ) where" + , "foo = id" + , "bar = foo" + , "baz = bar" + ] , testSession "style of multiple exports is preserved and selects smallest export separator" $ template - (T.unlines - [ "{-# OPTIONS_GHC -Wunused-top-binds #-}" - , "module A" - , " ( foo" - , " , bar" - , " -- * For testing" - , " , baz" - , " ) where" - , "foo = id" - , "bar = foo" - , "baz = bar" - , "quux = bar" - ]) - (R 10 0 10 4) - "Export ‘quux’" - (Just $ T.unlines - [ "{-# OPTIONS_GHC -Wunused-top-binds #-}" - , "module A" - , " ( foo" - , " , bar" - , " -- * For testing" - , " , baz" - , " , quux" - , " ) where" - , "foo = id" - , "bar = foo" - , "baz = bar" - , "quux = bar" - ]) + [ "{-# OPTIONS_GHC -Wunused-top-binds #-}" + , "module A" + , " ( foo" + , " , bar" + , " -- * For testing" + , " , baz" + , " ) where" + , "foo = id" + , "bar = foo" + , "baz = bar" + , "quux = bar" + ] + (R 10 0 10 4) + "Export ‘quux’" + [ "{-# OPTIONS_GHC -Wunused-top-binds #-}" + , "module A" + , " ( foo" + , " , bar" + , " -- * For testing" + , " , baz" + , " , quux" + , " ) where" + , "foo = id" + , "bar = foo" + , "baz = bar" + , "quux = bar" + ] , testSession "unused pattern synonym" $ template - (T.unlines - [ "{-# OPTIONS_GHC -Wunused-top-binds #-}" - , "{-# LANGUAGE PatternSynonyms #-}" - , "module A () where" - , "pattern Foo a <- (a, _)"]) - (R 3 0 3 10) - "Export ‘Foo’" - (Just $ T.unlines - [ "{-# OPTIONS_GHC -Wunused-top-binds #-}" - , "{-# LANGUAGE PatternSynonyms #-}" - , "module A (pattern Foo) where" - , "pattern Foo a <- (a, _)"]) + [ "{-# OPTIONS_GHC -Wunused-top-binds #-}" + , "{-# LANGUAGE PatternSynonyms #-}" + , "module A () where" + , "pattern Foo a <- (a, _)" + ] + (R 3 0 3 10) + "Export ‘Foo’" + [ "{-# OPTIONS_GHC -Wunused-top-binds #-}" + , "{-# LANGUAGE PatternSynonyms #-}" + , "module A (pattern Foo) where" + , "pattern Foo a <- (a, _)" + ] + , testSession "unused pattern synonym operator" $ template + [ "{-# OPTIONS_GHC -Wunused-top-binds #-}" + , "{-# LANGUAGE PatternSynonyms #-}" + , "module A () where" + , "pattern x :+ y = (x, y)" + ] + (R 3 0 3 12) + "Export ‘:+’" + [ "{-# OPTIONS_GHC -Wunused-top-binds #-}" + , "{-# LANGUAGE PatternSynonyms #-}" + , "module A (pattern (:+)) where" + , "pattern x :+ y = (x, y)" + ] , testSession "unused data type" $ template - (T.unlines - [ "{-# OPTIONS_GHC -Wunused-top-binds #-}" - , "module A () where" - , "data Foo = Foo"]) - (R 2 0 2 7) - "Export ‘Foo’" - (Just $ T.unlines - [ "{-# OPTIONS_GHC -Wunused-top-binds #-}" - , "module A (Foo(..)) where" - , "data Foo = Foo"]) + [ "{-# OPTIONS_GHC -Wunused-top-binds #-}" + , "module A () where" + , "data Foo = Foo" + ] + (R 2 0 2 7) + "Export ‘Foo’" + [ "{-# OPTIONS_GHC -Wunused-top-binds #-}" + , "module A (Foo(..)) where" + , "data Foo = Foo" + ] , testSession "unused newtype" $ template - (T.unlines - [ "{-# OPTIONS_GHC -Wunused-top-binds #-}" - , "module A () where" - , "newtype Foo = Foo ()"]) - (R 2 0 2 10) - "Export ‘Foo’" - (Just $ T.unlines - [ "{-# OPTIONS_GHC -Wunused-top-binds #-}" - , "module A (Foo(..)) where" - , "newtype Foo = Foo ()"]) + [ "{-# OPTIONS_GHC -Wunused-top-binds #-}" + , "module A () where" + , "newtype Foo = Foo ()" + ] + (R 2 0 2 10) + "Export ‘Foo’" + [ "{-# OPTIONS_GHC -Wunused-top-binds #-}" + , "module A (Foo(..)) where" + , "newtype Foo = Foo ()" + ] , testSession "unused type synonym" $ template - (T.unlines - [ "{-# OPTIONS_GHC -Wunused-top-binds #-}" - , "module A () where" - , "type Foo = ()"]) - (R 2 0 2 7) - "Export ‘Foo’" - (Just $ T.unlines - [ "{-# OPTIONS_GHC -Wunused-top-binds #-}" - , "module A (Foo) where" - , "type Foo = ()"]) + [ "{-# OPTIONS_GHC -Wunused-top-binds #-}" + , "module A () where" + , "type Foo = ()" + ] + (R 2 0 2 7) + "Export ‘Foo’" + [ "{-# OPTIONS_GHC -Wunused-top-binds #-}" + , "module A (Foo) where" + , "type Foo = ()" + ] , testSession "unused type family" $ template - (T.unlines - [ "{-# OPTIONS_GHC -Wunused-top-binds #-}" - , "{-# LANGUAGE TypeFamilies #-}" - , "module A () where" - , "type family Foo p"]) - (R 3 0 3 15) - "Export ‘Foo’" - (Just $ T.unlines - [ "{-# OPTIONS_GHC -Wunused-top-binds #-}" - , "{-# LANGUAGE TypeFamilies #-}" - , "module A (Foo) where" - , "type family Foo p"]) + [ "{-# OPTIONS_GHC -Wunused-top-binds #-}" + , "{-# LANGUAGE TypeFamilies #-}" + , "module A () where" + , "type family Foo p" + ] + (R 3 0 3 15) + "Export ‘Foo’" + [ "{-# OPTIONS_GHC -Wunused-top-binds #-}" + , "{-# LANGUAGE TypeFamilies #-}" + , "module A (Foo) where" + , "type family Foo p" + ] , testSession "unused typeclass" $ template - (T.unlines - [ "{-# OPTIONS_GHC -Wunused-top-binds #-}" - , "module A () where" - , "class Foo a"]) - (R 2 0 2 8) - "Export ‘Foo’" - (Just $ T.unlines - [ "{-# OPTIONS_GHC -Wunused-top-binds #-}" - , "module A (Foo(..)) where" - , "class Foo a"]) + [ "{-# OPTIONS_GHC -Wunused-top-binds #-}" + , "module A () where" + , "class Foo a" + ] + (R 2 0 2 8) + "Export ‘Foo’" + [ "{-# OPTIONS_GHC -Wunused-top-binds #-}" + , "module A (Foo(..)) where" + , "class Foo a" + ] , testSession "infix" $ template - (T.unlines - [ "{-# OPTIONS_GHC -Wunused-top-binds #-}" - , "module A () where" - , "a `f` b = ()"]) - (R 2 0 2 11) - "Export ‘f’" - (Just $ T.unlines - [ "{-# OPTIONS_GHC -Wunused-top-binds #-}" - , "module A (f) where" - , "a `f` b = ()"]) + [ "{-# OPTIONS_GHC -Wunused-top-binds #-}" + , "module A () where" + , "a `f` b = ()" + ] + (R 2 0 2 11) + "Export ‘f’" + [ "{-# OPTIONS_GHC -Wunused-top-binds #-}" + , "module A (f) where" + , "a `f` b = ()" + ] , testSession "function operator" $ template - (T.unlines - [ "{-# OPTIONS_GHC -Wunused-top-binds #-}" - , "module A () where" - , "(<|) = ($)"]) - (R 2 0 2 9) - "Export ‘<|’" - (Just $ T.unlines - [ "{-# OPTIONS_GHC -Wunused-top-binds #-}" - , "module A ((<|)) where" - , "(<|) = ($)"]) + [ "{-# OPTIONS_GHC -Wunused-top-binds #-}" + , "module A () where" + , "(<|) = ($)" + ] + (R 2 0 2 9) + "Export ‘<|’" + [ "{-# OPTIONS_GHC -Wunused-top-binds #-}" + , "module A ((<|)) where" + , "(<|) = ($)" + ] , testSession "type synonym operator" $ template - (T.unlines - [ "{-# OPTIONS_GHC -Wunused-top-binds #-}" - , "{-# LANGUAGE TypeOperators #-}" - , "module A () where" - , "type (:<) = ()"]) - (R 3 0 3 13) - "Export ‘:<’" - (Just $ T.unlines - [ "{-# OPTIONS_GHC -Wunused-top-binds #-}" - , "{-# LANGUAGE TypeOperators #-}" - , "module A ((:<)) where" - , "type (:<) = ()"]) + [ "{-# OPTIONS_GHC -Wunused-top-binds #-}" + , "{-# LANGUAGE TypeOperators #-}" + , "module A () where" + , "type (:<) = ()" + ] + (R 3 0 3 13) + "Export ‘:<’" + [ "{-# OPTIONS_GHC -Wunused-top-binds #-}" + , "{-# LANGUAGE TypeOperators #-}" + , "module A ((:<)) where" + , "type (:<) = ()" + ] , testSession "type family operator" $ template - (T.unlines - [ "{-# OPTIONS_GHC -Wunused-top-binds #-}" - , "{-# LANGUAGE TypeFamilies #-}" - , "{-# LANGUAGE TypeOperators #-}" - , "module A () where" - , "type family (:<)"]) - (R 4 0 4 15) - "Export ‘:<’" - (Just $ T.unlines - [ "{-# OPTIONS_GHC -Wunused-top-binds #-}" - , "{-# LANGUAGE TypeFamilies #-}" - , "{-# LANGUAGE TypeOperators #-}" - , "module A (type (:<)) where" - , "type family (:<)"]) + [ "{-# OPTIONS_GHC -Wunused-top-binds #-}" + , "{-# LANGUAGE TypeFamilies #-}" + , "{-# LANGUAGE TypeOperators #-}" + , "module A () where" + , "type family (:<)" + ] + (R 4 0 4 15) + "Export ‘:<’" + [ "{-# OPTIONS_GHC -Wunused-top-binds #-}" + , "{-# LANGUAGE TypeFamilies #-}" + , "{-# LANGUAGE TypeOperators #-}" + , "module A (type (:<)) where" + , "type family (:<)" + ] , testSession "typeclass operator" $ template - (T.unlines - [ "{-# OPTIONS_GHC -Wunused-top-binds #-}" - , "{-# LANGUAGE TypeOperators #-}" - , "module A () where" - , "class (:<) a"]) - (R 3 0 3 11) - "Export ‘:<’" - (Just $ T.unlines - [ "{-# OPTIONS_GHC -Wunused-top-binds #-}" - , "{-# LANGUAGE TypeOperators #-}" - , "module A (type (:<)(..)) where" - , "class (:<) a"]) + [ "{-# OPTIONS_GHC -Wunused-top-binds #-}" + , "{-# LANGUAGE TypeOperators #-}" + , "module A () where" + , "class (:<) a" + ] + (R 3 0 3 11) + "Export ‘:<’" + [ "{-# OPTIONS_GHC -Wunused-top-binds #-}" + , "{-# LANGUAGE TypeOperators #-}" + , "module A (type (:<)(..)) where" + , "class (:<) a" + ] , testSession "newtype operator" $ template - (T.unlines - [ "{-# OPTIONS_GHC -Wunused-top-binds #-}" - , "{-# LANGUAGE TypeOperators #-}" - , "module A () where" - , "newtype (:<) = Foo ()"]) - (R 3 0 3 20) - "Export ‘:<’" - (Just $ T.unlines - [ "{-# OPTIONS_GHC -Wunused-top-binds #-}" - , "{-# LANGUAGE TypeOperators #-}" - , "module A (type (:<)(..)) where" - , "newtype (:<) = Foo ()"]) + [ "{-# OPTIONS_GHC -Wunused-top-binds #-}" + , "{-# LANGUAGE TypeOperators #-}" + , "module A () where" + , "newtype (:<) = Foo ()" + ] + (R 3 0 3 20) + "Export ‘:<’" + [ "{-# OPTIONS_GHC -Wunused-top-binds #-}" + , "{-# LANGUAGE TypeOperators #-}" + , "module A (type (:<)(..)) where" + , "newtype (:<) = Foo ()" + ] , testSession "data type operator" $ template - (T.unlines - [ "{-# OPTIONS_GHC -Wunused-top-binds #-}" - , "{-# LANGUAGE TypeOperators #-}" - , "module A () where" - , "data (:<) = Foo ()"]) - (R 3 0 3 17) - "Export ‘:<’" - (Just $ T.unlines - [ "{-# OPTIONS_GHC -Wunused-top-binds #-}" - , "{-# LANGUAGE TypeOperators #-}" - , "module A (type (:<)(..)) where" - , "data (:<) = Foo ()"]) + [ "{-# OPTIONS_GHC -Wunused-top-binds #-}" + , "{-# LANGUAGE TypeOperators #-}" + , "module A () where" + , "data (:<) = Foo ()" + ] + (R 3 0 3 17) + "Export ‘:<’" + [ "{-# OPTIONS_GHC -Wunused-top-binds #-}" + , "{-# LANGUAGE TypeOperators #-}" + , "module A (type (:<)(..)) where" + , "data (:<) = Foo ()" + ] ] ] where - template doc range = exportTemplate (Just range) doc - -exportTemplate :: Maybe Range -> T.Text -> T.Text -> Maybe T.Text -> Session () -exportTemplate mRange initialContent expectedAction expectedContents = do - doc <- createDoc "A.hs" "haskell" initialContent + template origLines range actionTitle expectedLines = + exportTemplate (Just range) origLines actionTitle (Just expectedLines) + templateNoAction origLines range actionTitle = + exportTemplate (Just range) origLines actionTitle Nothing + +exportTemplate :: Maybe Range -> [T.Text] -> T.Text -> Maybe [T.Text] -> Session () +exportTemplate mRange initialLines expectedAction expectedLines = do + doc <- createDoc "A.hs" "haskell" $ T.unlines initialLines _ <- waitForDiagnostics actions <- case mRange of Nothing -> getAllCodeActions doc Just range -> getCodeActions doc range - case expectedContents of + case expectedLines of Just content -> do - action <- liftIO $ pickActionWithTitle expectedAction actions + action <- pickActionWithTitle expectedAction actions executeCodeAction action contentAfterAction <- documentContents doc - liftIO $ content @=? contentAfterAction + liftIO $ T.unlines content @=? contentAfterAction Nothing -> liftIO $ [_title | InR CodeAction{_title} <- actions, _title == expectedAction ] @?= [] removeExportTests :: TestTree removeExportTests = testGroup "remove export actions" [ testSession "single export" $ template - (T.unlines - [ "module A ( a ) where" - , "b :: ()" - , "b = ()"]) + [ "module A ( a ) where" + , "b :: ()" + , "b = ()" + ] "Remove ‘a’ from export" - (Just $ T.unlines - [ "module A ( ) where" - , "b :: ()" - , "b = ()"]) + [ "module A ( ) where" + , "b :: ()" + , "b = ()" + ] , testSession "ending comma" $ template - (T.unlines - [ "module A ( a, ) where" - , "b :: ()" - , "b = ()"]) + [ "module A ( a, ) where" + , "b :: ()" + , "b = ()" + ] "Remove ‘a’ from export" - (Just $ T.unlines - [ "module A ( ) where" - , "b :: ()" - , "b = ()"]) + [ "module A ( ) where" + , "b :: ()" + , "b = ()" + ] , testSession "multiple exports" $ template - (T.unlines - [ "module A (a , c, b ) where" - , "a, c :: ()" - , "a = ()" - , "c = ()"]) + [ "module A (a , c, b ) where" + , "a, c :: ()" + , "a = ()" + , "c = ()" + ] "Remove ‘b’ from export" - (Just $ T.unlines - [ "module A (a , c ) where" - , "a, c :: ()" - , "a = ()" - , "c = ()"]) + [ "module A (a , c ) where" + , "a, c :: ()" + , "a = ()" + , "c = ()" + ] , testSession "not in scope constructor" $ template - (T.unlines - [ "module A (A (X,Y,Z,(:<)), ab) where" - , "data A = X Int | Y | (:<) Int" - , "ab :: ()" - , "ab = ()" - ]) + [ "module A (A (X,Y,Z,(:<)), ab) where" + , "data A = X Int | Y | (:<) Int" + , "ab :: ()" + , "ab = ()" + ] "Remove ‘Z’ from export" - (Just $ T.unlines - [ "module A (A (X,Y,(:<)), ab) where" - , "data A = X Int | Y | (:<) Int" - , "ab :: ()" - , "ab = ()"]) + [ "module A (A (X,Y,(:<)), ab) where" + , "data A = X Int | Y | (:<) Int" + , "ab :: ()" + , "ab = ()" + ] , testSession "multiline export" $ template - (T.unlines - [ "module A (a" - , " , b" - , " , (:*:)" - , " , ) where" - , "a,b :: ()" - , "a = ()" - , "b = ()"]) + [ "module A (a" + , " , b" + , " , (:*:)" + , " , ) where" + , "a,b :: ()" + , "a = ()" + , "b = ()" + ] "Remove ‘:*:’ from export" - (Just $ T.unlines - [ "module A (a" - , " , b" - , " " - , " , ) where" - , "a,b :: ()" - , "a = ()" - , "b = ()"]) + [ "module A (a" + , " , b" + , " " + , " , ) where" + , "a,b :: ()" + , "a = ()" + , "b = ()" + ] , testSession "qualified re-export" $ template - (T.unlines - [ "module A (M.x,a) where" - , "import qualified Data.List as M" - , "a :: ()" - , "a = ()"]) + [ "module A (M.x,a) where" + , "import qualified Data.List as M" + , "a :: ()" + , "a = ()" + ] "Remove ‘M.x’ from export" - (Just $ T.unlines - [ "module A (a) where" - , "import qualified Data.List as M" - , "a :: ()" - , "a = ()"]) + [ "module A (a) where" + , "import qualified Data.List as M" + , "a :: ()" + , "a = ()" + ] , testSession "qualified re-export ending in '.'" $ template - (T.unlines - [ "module A ((M.@.),a) where" - , "import qualified Data.List as M" - , "a :: ()" - , "a = ()"]) + [ "module A ((M.@.),a) where" + , "import qualified Data.List as M" + , "a :: ()" + , "a = ()" + ] "Remove ‘M.@.’ from export" - (Just $ T.unlines - [ "module A (a) where" - , "import qualified Data.List as M" - , "a :: ()" - , "a = ()"]) + [ "module A (a) where" + , "import qualified Data.List as M" + , "a :: ()" + , "a = ()" + ] , testSession "export module" $ template - (T.unlines - [ "module A (module B) where" - , "a :: ()" - , "a = ()"]) + [ "module A (module B) where" + , "a :: ()" + , "a = ()" + ] "Remove ‘module B’ from export" - (Just $ T.unlines - [ "module A () where" - , "a :: ()" - , "a = ()"]) + [ "module A () where" + , "a :: ()" + , "a = ()" + ] , testSession "dodgy export" $ template - (T.unlines - [ "{-# OPTIONS_GHC -Wall #-}" - , "module A (A (..)) where" - , "data X = X" - , "type A = X"]) + [ "{-# OPTIONS_GHC -Wall #-}" + , "module A (A (..)) where" + , "data X = X" + , "type A = X" + ] "Remove ‘A(..)’ from export" - (Just $ T.unlines - [ "{-# OPTIONS_GHC -Wall #-}" - , "module A () where" - , "data X = X" - , "type A = X"]) + [ "{-# OPTIONS_GHC -Wall #-}" + , "module A () where" + , "data X = X" + , "type A = X" + ] , testSession "dodgy export" $ template - (T.unlines - [ "{-# OPTIONS_GHC -Wall #-}" - , "module A (A (..)) where" - , "data X = X" - , "type A = X"]) + [ "{-# OPTIONS_GHC -Wall #-}" + , "module A (A (..)) where" + , "data X = X" + , "type A = X" + ] "Remove ‘A(..)’ from export" - (Just $ T.unlines - [ "{-# OPTIONS_GHC -Wall #-}" - , "module A () where" - , "data X = X" - , "type A = X"]) + [ "{-# OPTIONS_GHC -Wall #-}" + , "module A () where" + , "data X = X" + , "type A = X" + ] , testSession "duplicate module export" $ template - (T.unlines - [ "{-# OPTIONS_GHC -Wall #-}" - , "module A (module L,module L) where" - , "import Data.List as L" - , "a :: ()" - , "a = ()"]) + [ "{-# OPTIONS_GHC -Wall #-}" + , "module A (module L,module L) where" + , "import Data.List as L" + , "a :: ()" + , "a = ()" + ] "Remove ‘Module L’ from export" - (Just $ T.unlines - [ "{-# OPTIONS_GHC -Wall #-}" - , "module A (module L) where" - , "import Data.List as L" - , "a :: ()" - , "a = ()"]) + [ "{-# OPTIONS_GHC -Wall #-}" + , "module A (module L) where" + , "import Data.List as L" + , "a :: ()" + , "a = ()" + ] , testSession "remove all exports single" $ template - (T.unlines - [ "module A (x) where" - , "a :: ()" - , "a = ()"]) + [ "module A (x) where" + , "a :: ()" + , "a = ()" + ] "Remove all redundant exports" - (Just $ T.unlines - [ "module A () where" - , "a :: ()" - , "a = ()"]) + [ "module A () where" + , "a :: ()" + , "a = ()" + ] , testSession "remove all exports two" $ template - (T.unlines - [ "module A (x,y) where" - , "a :: ()" - , "a = ()"]) + [ "module A (x,y) where" + , "a :: ()" + , "a = ()" + ] "Remove all redundant exports" - (Just $ T.unlines - [ "module A () where" - , "a :: ()" - , "a = ()"]) + [ "module A () where" + , "a :: ()" + , "a = ()" + ] , testSession "remove all exports three" $ template - (T.unlines - [ "module A (a,x,y) where" - , "a :: ()" - , "a = ()"]) + [ "module A (a,x,y) where" + , "a :: ()" + , "a = ()" + ] "Remove all redundant exports" - (Just $ T.unlines - [ "module A (a) where" - , "a :: ()" - , "a = ()"]) + [ "module A (a) where" + , "a :: ()" + , "a = ()" + ] , testSession "remove all exports composite" $ template - (T.unlines - [ "module A (x,y,b, module Ls, a, A(X,getW, Y, Z,(:-),getV), (-+), B(B)) where" - , "data A = X {getV :: Int} | Y {getV :: Int}" - , "data B = B" - , "a,b :: ()" - , "a = ()" - , "b = ()"]) + [ "module A (x,y,b, module Ls, a, A(X,getW, Y, Z,(:-),getV), (-+), B(B)) where" + , "data A = X {getV :: Int} | Y {getV :: Int}" + , "data B = B" + , "a,b :: ()" + , "a = ()" + , "b = ()" + ] "Remove all redundant exports" - (Just $ T.unlines - [ "module A (b, a, A(X, Y,getV), B(B)) where" - , "data A = X {getV :: Int} | Y {getV :: Int}" - , "data B = B" - , "a,b :: ()" - , "a = ()" - , "b = ()"]) + [ "module A (b, a, A(X, Y,getV), B(B)) where" + , "data A = X {getV :: Int} | Y {getV :: Int}" + , "data B = B" + , "a,b :: ()" + , "a = ()" + , "b = ()" + ] ] where - template = exportTemplate Nothing + template origLines actionTitle expectedLines = + exportTemplate Nothing origLines actionTitle (Just expectedLines) codeActionHelperFunctionTests :: TestTree codeActionHelperFunctionTests = testGroup "code action helpers" - [ - extendImportTestsRegEx + [ extendImportTestsRegEx ] extendImportTestsRegEx :: TestTree extendImportTestsRegEx = testGroup "regex parsing" - [ - testCase "parse invalid multiple imports" $ template "foo bar foo" Nothing + [ testCase "parse invalid multiple imports" $ template "foo bar foo" Nothing , testCase "parse malformed import list" $ template "\n\8226 Perhaps you want to add \8216fromList\8217 to one of these import lists:\n \8216Data.Map\8217)" Nothing , testCase "parse multiple imports" $ template - "\n\8226 Perhaps you want to add \8216fromList\8217 to one of these import lists:\n \8216Data.Map\8217 (app/testlsp.hs:7:1-18)\n \8216Data.HashMap.Strict\8217 (app/testlsp.hs:8:1-29)" + (if ghcVersion >= GHC98 + then "\n\8226 Add \8216fromList\8217 to one of these import lists:\n \8216Data.Map\8217 (at app/testlsp.hs:7:1-18)\n \8216Data.HashMap.Strict\8217 (at app/testlsp.hs:8:1-29)" + else "\n\8226 Perhaps you want to add \8216fromList\8217 to one of these import lists:\n \8216Data.Map\8217 (app/testlsp.hs:7:1-18)\n \8216Data.HashMap.Strict\8217 (app/testlsp.hs:8:1-29)" + ) $ Just ("fromList",[("Data.Map","app/testlsp.hs:7:1-18"),("Data.HashMap.Strict","app/testlsp.hs:8:1-29")]) ] where template message expected = do - liftIO $ matchRegExMultipleImports message @=? expected + liftIO $ expected @=? matchRegExMultipleImports message -pickActionWithTitle :: T.Text -> [Command |? CodeAction] -> IO CodeAction -pickActionWithTitle title actions = do - assertBool ("Found no matching actions for " <> show title <> " in " <> show titles) (not $ null matches) - return $ head matches +pickActionWithTitle :: T.Text -> [Command |? CodeAction] -> Session CodeAction +pickActionWithTitle title actions = + case matches of + [] -> liftIO . assertFailure $ "CodeAction with title " <> show title <> " not found in " <> show titles + a:_ -> pure a where titles = [ actionTitle @@ -3750,54 +3957,39 @@ pickActionWithTitle title actions = do , title == actionTitle ] -assertNoActionWithTitle :: T.Text -> [Command |? CodeAction] -> IO () -assertNoActionWithTitle title actions = do - assertBool ("Unexpected code action " <> show title <> " in " <> show titles) (null matches) - pure () +assertNoActionWithTitle :: T.Text -> [Command |? CodeAction] -> Session () +assertNoActionWithTitle title actions = + liftIO $ assertBool + ("Unexpected code action " <> show title <> " in " <> show titles) + (title `notElem` titles) where titles = [ actionTitle | InR CodeAction { _title = actionTitle } <- actions ] - matches = - [ action - | InR action@CodeAction { _title = actionTitle } <- actions - , title == actionTitle + +assertActionWithTitle :: [Command |? CodeAction] -> T.Text -> Session () +assertActionWithTitle actions title = + liftIO $ assertBool + ("CodeAction with title " <> show title <>" not found in " <> show titles) + (title `elem` titles) + where + titles = + [ actionTitle + | InR CodeAction { _title = actionTitle } <- actions ] -findCodeActions :: TextDocumentIdentifier -> Range -> [T.Text] -> Session [CodeAction] -findCodeActions = findCodeActions' (==) "is not a superset of" - -findCodeActionsByPrefix :: TextDocumentIdentifier -> Range -> [T.Text] -> Session [CodeAction] -findCodeActionsByPrefix = findCodeActions' T.isPrefixOf "is not prefix of" - -findCodeActions' :: (T.Text -> T.Text -> Bool) -> String -> TextDocumentIdentifier -> Range -> [T.Text] -> Session [CodeAction] -findCodeActions' op errMsg doc range expectedTitles = do - actions <- getCodeActions doc range - let matches = sequence - [ listToMaybe - [ action - | InR action@CodeAction { _title = actionTitle } <- actions - , expectedTitle `op` actionTitle] - | expectedTitle <- expectedTitles] - let msg = show - [ actionTitle - | InR CodeAction { _title = actionTitle } <- actions - ] - ++ " " <> errMsg <> " " - ++ show expectedTitles - liftIO $ case matches of - Nothing -> assertFailure msg - Just _ -> pure () - return (fromJust matches) - -findCodeAction :: TextDocumentIdentifier -> Range -> T.Text -> Session CodeAction -findCodeAction doc range t = head <$> findCodeActions doc range [t] - -testSession :: String -> Session () -> TestTree +testSession :: TestName -> Session () -> TestTree testSession name = testCase name . run -testSessionWithExtraFiles :: HasCallStack => FilePath -> String -> (FilePath -> Session ()) -> TestTree +testSessionExpectFail + :: TestName + -> ExpectBroken 'Ideal (Session ()) + -> ExpectBroken 'Current (Session ()) + -> TestTree +testSessionExpectFail name _ = testSession name . unCurrent + +testSessionWithExtraFiles :: HasCallStack => FilePath -> TestName -> (FilePath -> Session ()) -> TestTree testSessionWithExtraFiles prefix name = testCase name . runWithExtraFiles prefix runWithExtraFiles :: HasCallStack => FilePath -> (FilePath -> Session a) -> IO a @@ -3808,10 +4000,10 @@ runWithExtraFiles prefix s = withTempDir $ \dir -> do copyTestDataFiles :: HasCallStack => FilePath -> FilePath -> IO () copyTestDataFiles dir prefix = do -- Copy all the test data files to the temporary workspace - testDataFiles <- getDirectoryFilesIO ("test/data" prefix) ["//*"] + testDataFiles <- getDirectoryFilesIO ("plugins/hls-refactor-plugin/test/data" prefix) ["//*"] for_ testDataFiles $ \f -> do createDirectoryIfMissing True $ dir takeDirectory f - copyFile ("test/data" prefix f) (dir f) + copyFile ("plugins/hls-refactor-plugin/test/data" prefix f) (dir f) run :: Session a -> IO a run s = run' (const s) @@ -3820,12 +4012,15 @@ run' :: (FilePath -> Session a) -> IO a run' s = withTempDir $ \dir -> runInDir dir (s dir) runInDir :: FilePath -> Session a -> IO a -runInDir dir act = do - plugin <- refactorPlugin - runSessionWithServer' plugin def def lspTestCaps dir act +runInDir dir act = + runSessionWithTestConfig def + { testDirLocation = Left dir + , testPluginDescriptor = refactorPlugin + , testConfigCaps = lspTestCaps } + $ const act lspTestCaps :: ClientCapabilities -lspTestCaps = fullCaps { _window = Just $ WindowClientCapabilities (Just True) Nothing Nothing } +lspTestCaps = fullLatestClientCaps { _window = Just $ WindowClientCapabilities (Just True) Nothing Nothing } pattern R :: UInt -> UInt -> UInt -> UInt -> Range pattern R x y x' y' = Range (Position x y) (Position x' y') @@ -3834,23 +4029,4 @@ pattern R x y x' y' = Range (Position x y) (Position x' y') -- Which we need to do on macOS since the $TMPDIR can be in @/private/var@ or -- @/var@ withTempDir :: (FilePath -> IO a) -> IO a -withTempDir f = System.IO.Extra.withTempDir $ \dir -> do - dir' <- canonicalizePath dir - f dir' - -ignoreForGHC92 :: String -> TestTree -> TestTree -ignoreForGHC92 = ignoreForGhcVersions [GHC92] - -brokenForGHC94 :: String -> TestTree -> TestTree -brokenForGHC94 = knownBrokenForGhcVersions [GHC94] - --- | Assert that a value is not 'Nothing', and extract the value. -assertJust :: MonadIO m => String -> Maybe a -> m a -assertJust s = \case - Nothing -> liftIO $ assertFailure s - Just x -> pure x - --- | Before ghc9, lists of Char is displayed as [Char], but with ghc9 and up, it's displayed as String -listOfChar :: T.Text -listOfChar | ghcVersion >= GHC90 = "String" - | otherwise = "[Char]" +withTempDir f = System.IO.Extra.withTempDir $ (canonicalizePath >=> f) diff --git a/plugins/hls-refactor-plugin/test/Test/AddArgument.hs b/plugins/hls-refactor-plugin/test/Test/AddArgument.hs index c08870266f..a0bf8b004e 100644 --- a/plugins/hls-refactor-plugin/test/Test/AddArgument.hs +++ b/plugins/hls-refactor-plugin/test/Test/AddArgument.hs @@ -4,17 +4,13 @@ {-# LANGUAGE DuplicateRecordFields #-} {-# LANGUAGE GADTs #-} {-# LANGUAGE OverloadedStrings #-} -{-# LANGUAGE PolyKinds #-} module Test.AddArgument (tests) where -import Data.List.Extra import qualified Data.Text as T import Development.IDE.Types.Location import Language.LSP.Protocol.Types hiding - (SemanticTokenAbsolute (length, line), - SemanticTokenRelative (length), - SemanticTokensEdit (_start), + (SemanticTokensEdit (_start), mkRange) import Language.LSP.Test import Test.Tasty @@ -38,7 +34,9 @@ tests = mkGoldenAddArgTest "AddArgWithSigAndDocs" (r 8 0 8 50), mkGoldenAddArgTest "AddArgFromLet" (r 2 0 2 50), mkGoldenAddArgTest "AddArgFromWhere" (r 3 0 3 50), - mkGoldenAddArgTest "AddArgFromWhereComments" (r 3 0 3 50), + -- TODO can we make this work for GHC 9.10? + knownBrokenForGhcVersions [GHC910, GHC912] "In GHC 9.10 and 9.12 end-of-line comment annotation is in different place" $ + mkGoldenAddArgTest "AddArgFromWhereComments" (r 3 0 3 50), mkGoldenAddArgTest "AddArgWithTypeSynSig" (r 2 0 2 50), mkGoldenAddArgTest "AddArgWithTypeSynSigContravariant" (r 2 0 2 50), mkGoldenAddArgTest "AddArgWithLambda" (r 1 0 1 50), @@ -57,16 +55,18 @@ mkGoldenAddArgTest' :: FilePath -> Range -> T.Text -> TestTree mkGoldenAddArgTest' testFileName range varName = do let action docB = do _ <- waitForDiagnostics + let matchAction a = case a of + InR CodeAction {_title = t} -> "Add" `T.isPrefixOf` t + _ -> False InR action@CodeAction {_title = actionTitle} : _ <- - filter (\(InR CodeAction {_title = x}) -> "Add" `isPrefixOf` T.unpack x) - <$> getCodeActions docB range + filter matchAction <$> getCodeActions docB range liftIO $ actionTitle @?= ("Add argument ‘" <> varName <> "’ to function") executeCodeAction action goldenWithHaskellDocInTmpDir def (mkPluginTestDescriptor Refactor.bindingsPluginDescriptor "ghcide-code-actions-bindings") (testFileName <> " (golden)") - (FS.mkVirtualFileTree "test/data/golden/add-arg" (FS.directProject $ testFileName <.> "hs")) + (FS.mkVirtualFileTree "plugins/hls-refactor-plugin/test/data/golden/add-arg" (FS.directProject $ testFileName <.> "hs")) testFileName "expected" "hs" diff --git a/plugins/hls-refactor-plugin/test/data/import-placement/MultiLinePragma.expected.hs b/plugins/hls-refactor-plugin/test/data/import-placement/MultiLinePragma.expected.hs index ca0b9f28dc..e9e8f4f604 100644 --- a/plugins/hls-refactor-plugin/test/data/import-placement/MultiLinePragma.expected.hs +++ b/plugins/hls-refactor-plugin/test/data/import-placement/MultiLinePragma.expected.hs @@ -3,8 +3,8 @@ {-# LANGUAGE RecordWildCards, OverloadedStrings #-} {-# OPTIONS_GHC -Wall, - -Wno-unused-imports #-} import Data.Monoid + -Wno-unused-imports #-} -- some comment diff --git a/plugins/hls-refactor-plugin/test/data/import-placement/OptionsNotAtTopWithSpaces.expected.hs b/plugins/hls-refactor-plugin/test/data/import-placement/OptionsNotAtTopWithSpaces.expected.hs index 912d6a210c..8595bca913 100644 --- a/plugins/hls-refactor-plugin/test/data/import-placement/OptionsNotAtTopWithSpaces.expected.hs +++ b/plugins/hls-refactor-plugin/test/data/import-placement/OptionsNotAtTopWithSpaces.expected.hs @@ -2,7 +2,6 @@ {-# LANGUAGE TupleSections #-} -import Data.Monoid @@ -11,6 +10,7 @@ class Semigroup a => SomeData a instance SomeData All {-# OPTIONS_GHC -Wno-unrecognised-pragmas #-} +import Data.Monoid addOne :: Int -> Int addOne x = x + 1 diff --git a/plugins/hls-refactor-plugin/test/data/import-placement/OptionsPragmaNotAtTop.expected.hs b/plugins/hls-refactor-plugin/test/data/import-placement/OptionsPragmaNotAtTop.expected.hs index 55a6c60dbb..a92bbab580 100644 --- a/plugins/hls-refactor-plugin/test/data/import-placement/OptionsPragmaNotAtTop.expected.hs +++ b/plugins/hls-refactor-plugin/test/data/import-placement/OptionsPragmaNotAtTop.expected.hs @@ -1,8 +1,8 @@ -import Data.Monoid class Semigroup a => SomeData a instance SomeData All {-# OPTIONS_GHC -Wno-unrecognised-pragmas #-} +import Data.Monoid addOne :: Int -> Int addOne x = x + 1 diff --git a/plugins/hls-refactor-plugin/test/data/import-placement/PragmaNotAtTopMultipleComments.expected.hs b/plugins/hls-refactor-plugin/test/data/import-placement/PragmaNotAtTopMultipleComments.expected.hs index eead1cb55e..cbe451714d 100644 --- a/plugins/hls-refactor-plugin/test/data/import-placement/PragmaNotAtTopMultipleComments.expected.hs +++ b/plugins/hls-refactor-plugin/test/data/import-placement/PragmaNotAtTopMultipleComments.expected.hs @@ -9,7 +9,6 @@ comment -} {-# LANGUAGE TupleSections #-} -import Data.Monoid {- some comment -} -- again @@ -18,6 +17,7 @@ instance SomeData All #! nix-shell --pure -i runghc -p "haskellPackages.ghcWithPackages (hp: with hp; [ turtle ])" {-# OPTIONS_GHC -Wno-unrecognised-pragmas #-} +import Data.Monoid addOne :: Int -> Int addOne x = x + 1 diff --git a/plugins/hls-refactor-plugin/test/data/import-placement/PragmaNotAtTopWithCommentsAtTop.expected.hs b/plugins/hls-refactor-plugin/test/data/import-placement/PragmaNotAtTopWithCommentsAtTop.expected.hs index 57fc1614be..57ab794a7e 100644 --- a/plugins/hls-refactor-plugin/test/data/import-placement/PragmaNotAtTopWithCommentsAtTop.expected.hs +++ b/plugins/hls-refactor-plugin/test/data/import-placement/PragmaNotAtTopWithCommentsAtTop.expected.hs @@ -4,7 +4,6 @@ -- another comment {-# LANGUAGE TupleSections #-} -import Data.Monoid {- some comment -} @@ -13,6 +12,7 @@ instance SomeData All #! nix-shell --pure -i runghc -p "haskellPackages.ghcWithPackages (hp: with hp; [ turtle ])" {-# OPTIONS_GHC -Wno-unrecognised-pragmas #-} +import Data.Monoid addOne :: Int -> Int addOne x = x + 1 diff --git a/plugins/hls-refactor-plugin/test/data/import-placement/ShebangNotAtTop.expected.hs b/plugins/hls-refactor-plugin/test/data/import-placement/ShebangNotAtTop.expected.hs index 09e503ddd3..230710232e 100644 --- a/plugins/hls-refactor-plugin/test/data/import-placement/ShebangNotAtTop.expected.hs +++ b/plugins/hls-refactor-plugin/test/data/import-placement/ShebangNotAtTop.expected.hs @@ -1,10 +1,10 @@ {-# LANGUAGE OverloadedStrings #-} -import Data.Monoid class Semigroup a => SomeData a instance SomeData All #! nix-shell --pure -i runghc -p "haskellPackages.ghcWithPackages (hp: with hp; [ turtle ])" +import Data.Monoid f :: Int -> Int f x = x * x diff --git a/plugins/hls-refactor-plugin/test/data/import-placement/ShebangNotAtTopNoSpace.expected.hs b/plugins/hls-refactor-plugin/test/data/import-placement/ShebangNotAtTopNoSpace.expected.hs index b367314238..c5977503a6 100644 --- a/plugins/hls-refactor-plugin/test/data/import-placement/ShebangNotAtTopNoSpace.expected.hs +++ b/plugins/hls-refactor-plugin/test/data/import-placement/ShebangNotAtTopNoSpace.expected.hs @@ -1,8 +1,8 @@ -import Data.Monoid class Semigroup a => SomeData a instance SomeData All #! nix-shell --pure -i runghc -p "haskellPackages.ghcWithPackages (hp: with hp; [ turtle ])" +import Data.Monoid f :: Int -> Int f x = x * x diff --git a/plugins/hls-refactor-plugin/test/data/import-placement/ShebangNotAtTopWithSpaces.expected.hs b/plugins/hls-refactor-plugin/test/data/import-placement/ShebangNotAtTopWithSpaces.expected.hs index 4c6cbe3917..8d358468da 100644 --- a/plugins/hls-refactor-plugin/test/data/import-placement/ShebangNotAtTopWithSpaces.expected.hs +++ b/plugins/hls-refactor-plugin/test/data/import-placement/ShebangNotAtTopWithSpaces.expected.hs @@ -6,7 +6,6 @@ {-# LANGUAGE TupleSections #-} -import Data.Monoid @@ -16,6 +15,7 @@ instance SomeData All #! nix-shell --pure -i runghc -p "haskellPackages.ghcWithPackages (hp: with hp; [ turtle ])" {-# OPTIONS_GHC -Wno-unrecognised-pragmas #-} +import Data.Monoid addOne :: Int -> Int addOne x = x + 1 diff --git a/plugins/hls-rename-plugin/LICENSE b/plugins/hls-rename-plugin/LICENSE deleted file mode 100644 index 261eeb9e9f..0000000000 --- a/plugins/hls-rename-plugin/LICENSE +++ /dev/null @@ -1,201 +0,0 @@ - Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ - - TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - - 1. Definitions. - - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. - - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. - - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. - - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. - - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. - - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. - - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). - - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. - - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution." - - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. - - 2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. - - 3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. - - 4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: - - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and - - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and - - (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. - - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. - - 5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. - - 6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. - - 7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. - - 8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. - - 9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. - - END OF TERMS AND CONDITIONS - - APPENDIX: How to apply the Apache License to your work. - - To apply the Apache License to your work, attach the following - boilerplate notice, with the fields enclosed by brackets "[]" - replaced with your own identifying information. (Don't include - the brackets!) The text should be enclosed in the appropriate - comment syntax for the file format. We also recommend that a - file or class name and description of purpose be included on the - same "printed page" as the copyright notice for easier - identification within third-party archives. - - Copyright [yyyy] [name of copyright owner] - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. diff --git a/plugins/hls-rename-plugin/hls-rename-plugin.cabal b/plugins/hls-rename-plugin/hls-rename-plugin.cabal deleted file mode 100644 index 48c414f5e1..0000000000 --- a/plugins/hls-rename-plugin/hls-rename-plugin.cabal +++ /dev/null @@ -1,72 +0,0 @@ -cabal-version: 2.4 -name: hls-rename-plugin -version: 2.4.0.0 -synopsis: Rename plugin for Haskell Language Server -description: - Please see the README on GitHub at - -license: Apache-2.0 -license-file: LICENSE -author: Oliver Madine -maintainer: madine.oliver@gmail.com -category: Development -build-type: Simple -extra-source-files: - LICENSE - test/testdata/*.hs - test/testdata/*.yaml - -source-repository head - type: git - location: https://github.com/haskell/haskell-language-server.git - -library - -- Plugins that need exactprint have not been updated for 9.8 yet - if impl(ghc >= 9.8) - buildable: False - else - buildable: True - exposed-modules: Ide.Plugin.Rename - hs-source-dirs: src - build-depends: - , base >=4.12 && <5 - , containers - , extra - , ghc - , ghc-exactprint - , ghcide == 2.4.0.0 - , hashable - , hiedb - , hie-compat - , hls-plugin-api == 2.4.0.0 - , hls-refactor-plugin - , lens - , lsp - , lsp-types - , mtl - , mod - , syb - , text - , transformers - , unordered-containers - - default-language: Haskell2010 - -test-suite tests - if impl(ghc >= 9.8) - buildable: False - else - buildable: True - type: exitcode-stdio-1.0 - default-language: Haskell2010 - hs-source-dirs: test - main-is: Main.hs - ghc-options: -threaded -rtsopts -with-rtsopts=-N - build-depends: - , aeson - , base - , containers - , filepath - , hls-plugin-api - , hls-rename-plugin - , hls-test-utils == 2.4.0.0 diff --git a/plugins/hls-rename-plugin/src/Ide/Plugin/Rename.hs b/plugins/hls-rename-plugin/src/Ide/Plugin/Rename.hs index 79b74d9016..2fdbee3ebc 100644 --- a/plugins/hls-rename-plugin/src/Ide/Plugin/Rename.hs +++ b/plugins/hls-rename-plugin/src/Ide/Plugin/Rename.hs @@ -1,56 +1,49 @@ -{-# LANGUAGE CPP #-} -{-# LANGUAGE DataKinds #-} -{-# LANGUAGE FlexibleContexts #-} -{-# LANGUAGE GADTs #-} -{-# LANGUAGE NamedFieldPuns #-} -{-# LANGUAGE OverloadedLabels #-} -{-# LANGUAGE OverloadedStrings #-} -{-# LANGUAGE RankNTypes #-} -{-# LANGUAGE RecordWildCards #-} -{-# LANGUAGE ScopedTypeVariables #-} -{-# LANGUAGE TypeApplications #-} +{-# LANGUAGE CPP #-} +{-# LANGUAGE DataKinds #-} +{-# LANGUAGE GADTs #-} +{-# LANGUAGE OverloadedLabels #-} +{-# LANGUAGE OverloadedStrings #-} +{-# LANGUAGE RecordWildCards #-} +{-# OPTIONS_GHC -Wno-orphans #-} module Ide.Plugin.Rename (descriptor, E.Log) where -import GHC.Parser.Annotation (AnnContext, AnnList, - AnnParen, AnnPragma) - import Compat.HieTypes import Control.Lens ((^.)) import Control.Monad -import Control.Monad.Except -import Control.Monad.IO.Class -import Control.Monad.Trans.Class -import Control.Monad.Trans.Except -import Data.Bifunctor (first) +import Control.Monad.Except (ExceptT, throwError) +import Control.Monad.IO.Class (MonadIO, liftIO) +import Control.Monad.Trans.Class (lift) +import Data.Either (rights) +import Data.Foldable (fold) import Data.Generics import Data.Hashable import Data.HashSet (HashSet) import qualified Data.HashSet as HS -import Data.List.Extra hiding (length) +import Data.List.NonEmpty (NonEmpty ((:|)), + groupWith) import qualified Data.Map as M import Data.Maybe import Data.Mod.Word -import qualified Data.Set as S import qualified Data.Text as T import Development.IDE (Recorder, WithPriority, usePropertyAction) +import Development.IDE.Core.FileStore (getVersionedTextDoc) import Development.IDE.Core.PluginUtils -import Development.IDE.Core.PositionMapping import Development.IDE.Core.RuleTypes import Development.IDE.Core.Service import Development.IDE.Core.Shake -import Development.IDE.GHC.Compat.Core +import Development.IDE.GHC.Compat import Development.IDE.GHC.Compat.ExactPrint -import Development.IDE.GHC.Compat.Parser -import Development.IDE.GHC.Compat.Units import Development.IDE.GHC.Error import Development.IDE.GHC.ExactPrint import qualified Development.IDE.GHC.ExactPrint as E import Development.IDE.Plugin.CodeAction import Development.IDE.Spans.AtPoint import Development.IDE.Types.Location +import HieDb ((:.) (..)) import HieDb.Query +import HieDb.Types (RefRow (refIsGenerated)) import Ide.Plugin.Error import Ide.Plugin.Properties import Ide.PluginUtils @@ -58,57 +51,78 @@ import Ide.Types import qualified Language.LSP.Protocol.Lens as L import Language.LSP.Protocol.Message import Language.LSP.Protocol.Types -import Language.LSP.Server instance Hashable (Mod a) where hash n = hash (unMod n) descriptor :: Recorder (WithPriority E.Log) -> PluginId -> PluginDescriptor IdeState -descriptor recorder pluginId = mkExactprintPluginDescriptor recorder $ (defaultPluginDescriptor pluginId) - { pluginHandlers = mkPluginHandler SMethod_TextDocumentRename renameProvider - , pluginConfigDescriptor = defaultConfigDescriptor - { configCustomConfig = mkCustomConfig properties } - } +descriptor recorder pluginId = mkExactprintPluginDescriptor recorder $ + (defaultPluginDescriptor pluginId "Provides renaming of Haskell identifiers") + { pluginHandlers = mconcat + [ mkPluginHandler SMethod_TextDocumentRename renameProvider + , mkPluginHandler SMethod_TextDocumentPrepareRename prepareRenameProvider + ] + , pluginConfigDescriptor = defaultConfigDescriptor + { configCustomConfig = mkCustomConfig properties } + } + +prepareRenameProvider :: PluginMethodHandler IdeState Method_TextDocumentPrepareRename +prepareRenameProvider state _pluginId (PrepareRenameParams (TextDocumentIdentifier uri) pos _progressToken) = do + nfp <- getNormalizedFilePathE uri + namesUnderCursor <- getNamesAtPos state nfp pos + -- When this handler says that rename is invalid, VSCode shows "The element can't be renamed" + -- and doesn't even allow you to create full rename request. + -- This handler deliberately approximates "things that definitely can't be renamed" + -- to mean "there is no Name at given position". + -- + -- In particular it allows some cases through (e.g. cross-module renames), + -- so that the full rename handler can give more informative error about them. + let renameValid = not $ null namesUnderCursor + pure $ InL $ PrepareRenameResult $ InR $ InR $ PrepareRenameDefaultBehavior renameValid renameProvider :: PluginMethodHandler IdeState Method_TextDocumentRename -renameProvider state pluginId (RenameParams _prog docId@(TextDocumentIdentifier uri) pos newNameText) = do - nfp <- getNormalizedFilePathE uri - directOldNames <- getNamesAtPos state nfp pos - directRefs <- concat <$> mapM (refsAtName state nfp) directOldNames - - {- References in HieDB are not necessarily transitive. With `NamedFieldPuns`, we can have - indirect references through punned names. To find the transitive closure, we do a pass of - the direct references to find the references for any punned names. - See the `IndirectPuns` test for an example. -} - indirectOldNames <- concat . filter ((>1) . Prelude.length) <$> - mapM (uncurry (getNamesAtPos state) . locToFilePos) directRefs - let oldNames = filter matchesDirect indirectOldNames ++ directOldNames - matchesDirect n = occNameFS (nameOccName n) `elem` directFS - where - directFS = map (occNameFS. nameOccName) directOldNames - refs <- HS.fromList . concat <$> mapM (refsAtName state nfp) oldNames - - -- Validate rename - crossModuleEnabled <- liftIO $ runAction "rename: config" state $ usePropertyAction #crossModule pluginId properties - unless crossModuleEnabled $ failWhenImportOrExport state nfp refs oldNames - when (any isBuiltInSyntax oldNames) $ throwError $ PluginInternalError "Invalid rename of built-in syntax" - - -- Perform rename - let newName = mkTcOcc $ T.unpack newNameText - filesRefs = collectWith locToUri refs - getFileEdit (uri, locations) = do - verTxtDocId <- lift $ getVersionedTextDoc (TextDocumentIdentifier uri) - getSrcEdit state verTxtDocId (replaceRefs newName locations) - fileEdits <- mapM getFileEdit filesRefs - pure $ InL $ foldl' (<>) mempty fileEdits +renameProvider state pluginId (RenameParams _prog (TextDocumentIdentifier uri) pos newNameText) = do + nfp <- getNormalizedFilePathE uri + directOldNames <- getNamesAtPos state nfp pos + directRefs <- concat <$> mapM (refsAtName state nfp) directOldNames + + {- References in HieDB are not necessarily transitive. With `NamedFieldPuns`, we can have + indirect references through punned names. To find the transitive closure, we do a pass of + the direct references to find the references for any punned names. + See the `IndirectPuns` test for an example. -} + indirectOldNames <- concat . filter ((>1) . length) <$> + mapM (uncurry (getNamesAtPos state) <=< locToFilePos) directRefs + let oldNames = filter matchesDirect indirectOldNames ++ directOldNames + where + matchesDirect n = occNameFS (nameOccName n) `elem` directFS + directFS = map (occNameFS . nameOccName) directOldNames + + case oldNames of + -- There were no Names at given position (e.g. rename triggered within a comment or on a keyword) + [] -> throwError $ PluginInvalidParams "No symbol to rename at given position" + _ -> do + refs <- HS.fromList . concat <$> mapM (refsAtName state nfp) oldNames + + -- Validate rename + crossModuleEnabled <- liftIO $ runAction "rename: config" state $ usePropertyAction #crossModule pluginId properties + unless crossModuleEnabled $ failWhenImportOrExport state nfp refs oldNames + when (any isBuiltInSyntax oldNames) $ throwError $ PluginInternalError "Invalid rename of built-in syntax" + + -- Perform rename + let newName = mkTcOcc $ T.unpack newNameText + filesRefs = collectWith locToUri refs + getFileEdit (uri, locations) = do + verTxtDocId <- liftIO $ runAction "rename: getVersionedTextDoc" state $ getVersionedTextDoc (TextDocumentIdentifier uri) + getSrcEdit state verTxtDocId (replaceRefs newName locations) + fileEdits <- mapM getFileEdit filesRefs + pure $ InL $ fold fileEdits -- | Limit renaming across modules. failWhenImportOrExport :: - (MonadLsp config m) => IdeState -> NormalizedFilePath -> HashSet Location -> [Name] -> - ExceptT PluginError m () + ExceptT PluginError (HandlerM config) () failWhenImportOrExport state nfp refLocs names = do pm <- runActionE "Rename.GetParsedModule" state (useE GetParsedModule nfp) @@ -126,18 +140,17 @@ failWhenImportOrExport state nfp refLocs names = do -- | Apply a function to a `ParsedSource` for a given `Uri` to compute a `WorkspaceEdit`. getSrcEdit :: - (MonadLsp config m) => IdeState -> VersionedTextDocumentIdentifier -> (ParsedSource -> ParsedSource) -> - ExceptT PluginError m WorkspaceEdit + ExceptT PluginError (HandlerM config) WorkspaceEdit getSrcEdit state verTxtDocId updatePs = do - ccs <- lift getClientCapabilities + ccs <- lift pluginGetClientCapabilities nfp <- getNormalizedFilePathE (verTxtDocId ^. L.uri) annAst <- runActionE "Rename.GetAnnotatedParsedSource" state (useE GetAnnotatedParsedSource nfp) - let (ps, anns) = (astA annAst, annsA annAst) - let src = T.pack $ exactPrint ps + let ps = annAst + src = T.pack $ exactPrint ps res = T.pack $ exactPrint (updatePs ps) pure $ diffText ccs (verTxtDocId, src) res IncludeDeletions @@ -151,13 +164,13 @@ replaceRefs newName refs = everywhere $ -- there has to be a better way... mkT (replaceLoc @AnnListItem) `extT` -- replaceLoc @AnnList `extT` -- not needed - -- replaceLoc @AnnParen `extT` -- not needed + -- replaceLoc @AnnParen `extT` -- not needed -- replaceLoc @AnnPragma `extT` -- not needed -- replaceLoc @AnnContext `extT` -- not needed -- replaceLoc @NoEpAnns `extT` -- not needed replaceLoc @NameAnn where - replaceLoc :: forall an. Typeable an => LocatedAn an RdrName -> LocatedAn an RdrName + replaceLoc :: forall an. LocatedAn an RdrName -> LocatedAn an RdrName replaceLoc (L srcSpan oldRdrName) | isRef (locA srcSpan) = L srcSpan $ replace oldRdrName replaceLoc lOldRdrName = lOldRdrName @@ -184,6 +197,8 @@ refsAtName state nfp name = do dbRefs <- case nameModule_maybe name of Nothing -> pure [] Just mod -> liftIO $ mapMaybe rowToLoc <$> withHieDb (\hieDb -> + -- See Note [Generated references] + filter (\(refRow HieDb.:. _) -> refIsGenerated refRow) <$> findReferences hieDb True @@ -194,49 +209,64 @@ refsAtName state nfp name = do ) pure $ nameLocs name ast ++ dbRefs -nameLocs :: Name -> (HieAstResult, PositionMapping) -> [Location] -nameLocs name (HAR _ _ rm _ _, pm) = - mapMaybe (toCurrentLocation pm . realSrcSpanToLocation . fst) - (concat $ M.lookup (Right name) rm) +nameLocs :: Name -> HieAstResult -> [Location] +nameLocs name (HAR _ _ rm _ _) = + concatMap (map (realSrcSpanToLocation . fst)) + (M.lookup (Right name) rm) --------------------------------------------------------------------------------------------------- -- Util getNamesAtPos :: MonadIO m => IdeState -> NormalizedFilePath -> Position -> ExceptT PluginError m [Name] getNamesAtPos state nfp pos = do - (HAR{hieAst}, pm) <- handleGetHieAst state nfp - pure $ getNamesAtPoint hieAst pos pm + HAR{hieAst} <- handleGetHieAst state nfp + pure $ getNamesAtPoint' hieAst pos handleGetHieAst :: MonadIO m => IdeState -> NormalizedFilePath -> - ExceptT PluginError m (HieAstResult, PositionMapping) + ExceptT PluginError m HieAstResult handleGetHieAst state nfp = - fmap (first removeGenerated) $ runActionE "Rename.GetHieAst" state $ useWithStaleE GetHieAst nfp + -- We explicitly do not want to allow a stale version here - we only want to rename if + -- the module compiles, otherwise we can't guarantee that we'll rename everything, + -- which is bad (see https://github.com/haskell/haskell-language-server/issues/3799) + fmap removeGenerated $ runActionE "Rename.GetHieAst" state $ useE GetHieAst nfp --- | We don't want to rename in code generated by GHC as this gives false positives. --- So we restrict the HIE file to remove all the generated code. +{- Note [Generated references] +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +GHC inserts `Use`s of record constructor everywhere where its record selectors are used, +which leads to record fields being renamed whenever corresponding constructor is renamed. +see https://github.com/haskell/haskell-language-server/issues/2915 +To work around this, we filter out compiler-generated references. +-} removeGenerated :: HieAstResult -> HieAstResult -removeGenerated HAR{..} = HAR{hieAst = go hieAst,..} +removeGenerated HAR{..} = + HAR{hieAst = sourceOnlyAsts, refMap = sourceOnlyRefMap, ..} where - go :: HieASTs a -> HieASTs a - go hf = - HieASTs (fmap goAst (getAsts hf)) - goAst (Node nsi sp xs) = Node (SourcedNodeInfo $ M.restrictKeys (getSourcedNodeInfo nsi) (S.singleton SourceInfo)) sp (map goAst xs) + goAsts :: HieASTs a -> HieASTs a + goAsts (HieASTs asts) = HieASTs (fmap goAst asts) --- head is safe since groups are non-empty -collectWith :: (Hashable a, Eq a, Eq b) => (a -> b) -> HashSet a -> [(b, HashSet a)] -collectWith f = map (\a -> (f $ head a, HS.fromList a)) . groupOn f . HS.toList + goAst :: HieAST a -> HieAST a + goAst (Node (SourcedNodeInfo sniMap) sp children) = + let sourceOnlyNodeInfos = SourcedNodeInfo $ M.delete GeneratedInfo sniMap + in Node sourceOnlyNodeInfos sp $ map goAst children -locToUri :: Location -> Uri -locToUri (Location uri _) = uri + sourceOnlyAsts = goAsts hieAst + -- Also need to regenerate the RefMap, because the one in HAR + -- is generated from HieASTs containing GeneratedInfo + sourceOnlyRefMap = generateReferencesMap $ getAsts sourceOnlyAsts + +collectWith :: (Hashable a, Eq b) => (a -> b) -> HashSet a -> [(b, HashSet a)] +collectWith f = map (\(a :| as) -> (f a, HS.fromList (a:as))) . groupWith f . HS.toList -nfpToUri :: NormalizedFilePath -> Uri -nfpToUri = filePathToUri . fromNormalizedFilePath +-- | A variant 'getNamesAtPoint' that does not expect a 'PositionMapping' +getNamesAtPoint' :: HieASTs a -> Position -> [Name] +getNamesAtPoint' hf pos = + concat $ pointCommand hf pos (rights . M.keys . getNodeIds) -showName :: Name -> String -showName = occNameString . getOccName +locToUri :: Location -> Uri +locToUri (Location uri _) = uri unsafeSrcSpanToLoc :: SrcSpan -> Location unsafeSrcSpanToLoc srcSpan = @@ -244,10 +274,8 @@ unsafeSrcSpanToLoc srcSpan = Nothing -> error "Invalid conversion from UnhelpfulSpan to Location" Just location -> location -locToFilePos :: Location -> (NormalizedFilePath, Position) -locToFilePos (Location uri (Range pos _)) = (nfp, pos) - where - Just nfp = (uriToNormalizedFilePath . toNormalizedUri) uri +locToFilePos :: Monad m => Location -> ExceptT PluginError m (NormalizedFilePath, Position) +locToFilePos (Location uri (Range pos _)) = (,pos) <$> getNormalizedFilePathE uri replaceModName :: Name -> Maybe ModuleName -> Module replaceModName name mbModName = diff --git a/plugins/hls-rename-plugin/test/Main.hs b/plugins/hls-rename-plugin/test/Main.hs index e9cfd83c8d..b935e6563f 100644 --- a/plugins/hls-rename-plugin/test/Main.hs +++ b/plugins/hls-rename-plugin/test/Main.hs @@ -1,12 +1,16 @@ -{-# LANGUAGE OverloadedStrings #-} +{-# LANGUAGE DataKinds #-} +{-# LANGUAGE DisambiguateRecordFields #-} +{-# LANGUAGE OverloadedStrings #-} module Main (main) where +import Control.Lens ((^.)) import Data.Aeson -import qualified Data.Map as M +import qualified Data.Map as M +import Data.Text (Text, pack) import Ide.Plugin.Config -import qualified Ide.Plugin.Rename as Rename -import Ide.Types (IdePlugins (IdePlugins)) +import qualified Ide.Plugin.Rename as Rename +import qualified Language.LSP.Protocol.Lens as L import System.FilePath import Test.Hls @@ -16,18 +20,18 @@ main = defaultTestRunner tests renamePlugin :: PluginTestDescriptor Rename.Log renamePlugin = mkPluginTestDescriptor Rename.descriptor "rename" --- See https://github.com/wz1000/HieDb/issues/45 -recordConstructorIssue :: String -recordConstructorIssue = "HIE references for record fields incorrect with GHC versions >= 9" - tests :: TestTree tests = testGroup "Rename" [ goldenWithRename "Data constructor" "DataConstructor" $ \doc -> rename doc (Position 0 15) "Op" + , goldenWithRename "Data constructor with fields" "DataConstructorWithFields" $ \doc -> + rename doc (Position 1 13) "FooRenamed" + , knownBrokenForGhcVersions [GHC96, GHC98] "renaming Constructor{..} with RecordWildcard removes .." $ + goldenWithRename "Data constructor with fields" "DataConstructorWithFieldsRecordWildcards" $ \doc -> + rename doc (Position 1 13) "FooRenamed" , goldenWithRename "Exported function" "ExportedFunction" $ \doc -> rename doc (Position 2 1) "quux" - , ignoreForGhcVersions [GHC90, GHC92] recordConstructorIssue $ - goldenWithRename "Field Puns" "FieldPuns" $ \doc -> + , goldenWithRename "Field Puns" "FieldPuns" $ \doc -> rename doc (Position 7 13) "bleh" , goldenWithRename "Function argument" "FunctionArgument" $ \doc -> rename doc (Position 3 4) "y" @@ -41,8 +45,7 @@ tests = testGroup "Rename" rename doc (Position 3 8) "baz" , goldenWithRename "Import hiding" "ImportHiding" $ \doc -> rename doc (Position 0 22) "hiddenFoo" - , ignoreForGhcVersions [GHC90, GHC92] recordConstructorIssue $ - goldenWithRename "Indirect Puns" "IndirectPuns" $ \doc -> + , goldenWithRename "Indirect Puns" "IndirectPuns" $ \doc -> rename doc (Position 4 23) "blah" , goldenWithRename "Let expression" "LetExpression" $ \doc -> rename doc (Position 5 11) "foobar" @@ -54,8 +57,7 @@ tests = testGroup "Rename" rename doc (Position 3 12) "baz" , goldenWithRename "Realigns do block indentation" "RealignDo" $ \doc -> rename doc (Position 0 2) "fooBarQuux" - , ignoreForGhcVersions [GHC90, GHC92] recordConstructorIssue $ - goldenWithRename "Record field" "RecordField" $ \doc -> + , goldenWithRename "Record field" "RecordField" $ \doc -> rename doc (Position 6 9) "number" , goldenWithRename "Shadowed name" "ShadowedName" $ \doc -> rename doc (Position 1 1) "baz" @@ -65,11 +67,86 @@ tests = testGroup "Rename" rename doc (Position 2 17) "BinaryTree" , goldenWithRename "Type variable" "TypeVariable" $ \doc -> rename doc (Position 0 13) "b" + , goldenWithRename "Rename within comment" "Comment" $ \doc -> do + let expectedError = TResponseError + (InR ErrorCodes_InvalidParams) + "rename: Invalid Params: No symbol to rename at given position" + Nothing + renameExpectError expectedError doc (Position 0 10) "ImpossibleRename" + + , testCase "fails when module does not compile" $ runRenameSession "" $ do + doc <- openDoc "FunctionArgument.hs" "haskell" + expectNoMoreDiagnostics 3 doc "typecheck" + + -- Update the document so it doesn't compile + let change = TextDocumentContentChangeEvent $ InL TextDocumentContentChangePartial + { _range = Range (Position 2 13) (Position 2 17) + , _rangeLength = Nothing + , _text = "A" + } + changeDoc doc [change] + diags@(tcDiag : _) <- waitForDiagnosticsFrom doc + + -- Make sure there's a typecheck error + liftIO $ do + length diags @?= 1 + tcDiag ^. L.range @?= Range (Position 2 13) (Position 2 14) + tcDiag ^. L.severity @?= Just DiagnosticSeverity_Error + tcDiag ^. L.source @?= Just "typecheck" + + -- Make sure renaming fails + renameErr <- expectRenameError doc (Position 3 0) "foo'" + liftIO $ do + renameErr ^. L.code @?= InL LSPErrorCodes_RequestFailed + renameErr ^. L.message @?= "rename: Rule Failed: GetHieAst" + + -- Update the document so it compiles + let change' = TextDocumentContentChangeEvent $ InL TextDocumentContentChangePartial + { _range = Range (Position 2 13) (Position 2 14) + , _rangeLength = Nothing + , _text = "Int" + } + changeDoc doc [change'] + expectNoMoreDiagnostics 3 doc "typecheck" + + -- Make sure renaming succeeds + rename doc (Position 3 0) "foo'" ] goldenWithRename :: TestName-> FilePath -> (TextDocumentIdentifier -> Session ()) -> TestTree goldenWithRename title path act = - goldenWithHaskellDoc (def { plugins = M.fromList [("rename", def { plcConfig = "crossModule" .= True })] }) renamePlugin title testDataDir path "expected" "hs" act + goldenWithHaskellDoc (def { plugins = M.fromList [("rename", def { plcConfig = "crossModule" .= True })] }) + renamePlugin title testDataDir path "expected" "hs" act + +renameExpectError :: TResponseError Method_TextDocumentRename -> TextDocumentIdentifier -> Position -> Text -> Session () +renameExpectError expectedError doc pos newName = do + let params = RenameParams Nothing doc pos newName + rsp <- request SMethod_TextDocumentRename params + case rsp ^. L.result of + Right _ -> liftIO $ assertFailure $ "Was expecting " <> show expectedError <> ", got success" + Left actualError -> liftIO $ assertEqual "ResponseError" expectedError actualError testDataDir :: FilePath -testDataDir = "test" "testdata" +testDataDir = "plugins" "hls-rename-plugin" "test" "testdata" + +-- | Attempts to renames the term at the specified position, expecting a failure +expectRenameError :: + TextDocumentIdentifier -> + Position -> + String -> + Session (TResponseError Method_TextDocumentRename) +expectRenameError doc pos newName = do + let params = RenameParams Nothing doc pos (pack newName) + rsp <- request SMethod_TextDocumentRename params + case rsp ^. L.result of + Left err -> pure err + Right _ -> liftIO $ assertFailure $ + "Got unexpected successful rename response for " <> show (doc ^. L.uri) + +runRenameSession :: FilePath -> Session a -> IO a +runRenameSession subdir = failIfSessionTimeout + . runSessionWithTestConfig def + { testDirLocation = Left $ testDataDir subdir + , testPluginDescriptor = renamePlugin + , testConfigCaps = codeActionNoResolveCaps } + . const diff --git a/plugins/hls-rename-plugin/test/testdata/Comment.expected.hs b/plugins/hls-rename-plugin/test/testdata/Comment.expected.hs new file mode 100644 index 0000000000..d58fd349a8 --- /dev/null +++ b/plugins/hls-rename-plugin/test/testdata/Comment.expected.hs @@ -0,0 +1 @@ +{- IShouldNotBeRenaemable -} diff --git a/plugins/hls-rename-plugin/test/testdata/Comment.hs b/plugins/hls-rename-plugin/test/testdata/Comment.hs new file mode 100644 index 0000000000..d58fd349a8 --- /dev/null +++ b/plugins/hls-rename-plugin/test/testdata/Comment.hs @@ -0,0 +1 @@ +{- IShouldNotBeRenaemable -} diff --git a/plugins/hls-rename-plugin/test/testdata/DataConstructorWithFields.expected.hs b/plugins/hls-rename-plugin/test/testdata/DataConstructorWithFields.expected.hs new file mode 100644 index 0000000000..5fc38c7f01 --- /dev/null +++ b/plugins/hls-rename-plugin/test/testdata/DataConstructorWithFields.expected.hs @@ -0,0 +1,14 @@ +{-# LANGUAGE NamedFieldPuns #-} +data Foo = FooRenamed { a :: Int, b :: Bool } + +foo1 :: Foo +foo1 = FooRenamed { a = 1, b = True } + +foo2 :: Foo +foo2 = FooRenamed 1 True + +fun1 :: Foo -> Int +fun1 FooRenamed {a} = a + +fun2 :: Foo -> Int +fun2 FooRenamed {a = i} = i diff --git a/plugins/hls-rename-plugin/test/testdata/DataConstructorWithFields.hs b/plugins/hls-rename-plugin/test/testdata/DataConstructorWithFields.hs new file mode 100644 index 0000000000..abd8031096 --- /dev/null +++ b/plugins/hls-rename-plugin/test/testdata/DataConstructorWithFields.hs @@ -0,0 +1,14 @@ +{-# LANGUAGE NamedFieldPuns #-} +data Foo = Foo { a :: Int, b :: Bool } + +foo1 :: Foo +foo1 = Foo { a = 1, b = True } + +foo2 :: Foo +foo2 = Foo 1 True + +fun1 :: Foo -> Int +fun1 Foo {a} = a + +fun2 :: Foo -> Int +fun2 Foo {a = i} = i diff --git a/plugins/hls-rename-plugin/test/testdata/DataConstructorWithFieldsRecordWildcards.expected.hs b/plugins/hls-rename-plugin/test/testdata/DataConstructorWithFieldsRecordWildcards.expected.hs new file mode 100644 index 0000000000..b5dd83cecb --- /dev/null +++ b/plugins/hls-rename-plugin/test/testdata/DataConstructorWithFieldsRecordWildcards.expected.hs @@ -0,0 +1,5 @@ +{-# LANGUAGE RecordWildCards #-} +data Foo = FooRenamed { a :: Int, b :: Bool } + +fun :: Foo -> Int +fun FooRenamed {..} = a diff --git a/plugins/hls-rename-plugin/test/testdata/DataConstructorWithFieldsRecordWildcards.hs b/plugins/hls-rename-plugin/test/testdata/DataConstructorWithFieldsRecordWildcards.hs new file mode 100644 index 0000000000..8e624b0816 --- /dev/null +++ b/plugins/hls-rename-plugin/test/testdata/DataConstructorWithFieldsRecordWildcards.hs @@ -0,0 +1,5 @@ +{-# LANGUAGE RecordWildCards #-} +data Foo = Foo { a :: Int, b :: Bool } + +fun :: Foo -> Int +fun Foo {..} = a diff --git a/plugins/hls-retrie-plugin/LICENSE b/plugins/hls-retrie-plugin/LICENSE deleted file mode 100644 index 261eeb9e9f..0000000000 --- a/plugins/hls-retrie-plugin/LICENSE +++ /dev/null @@ -1,201 +0,0 @@ - Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ - - TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - - 1. Definitions. - - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. - - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. - - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. - - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. - - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. - - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. - - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). - - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. - - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution." - - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. - - 2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. - - 3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. - - 4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: - - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and - - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and - - (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. - - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. - - 5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. - - 6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. - - 7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. - - 8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. - - 9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. - - END OF TERMS AND CONDITIONS - - APPENDIX: How to apply the Apache License to your work. - - To apply the Apache License to your work, attach the following - boilerplate notice, with the fields enclosed by brackets "[]" - replaced with your own identifying information. (Don't include - the brackets!) The text should be enclosed in the appropriate - comment syntax for the file format. We also recommend that a - file or class name and description of purpose be included on the - same "printed page" as the copyright notice for easier - identification within third-party archives. - - Copyright [yyyy] [name of copyright owner] - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. diff --git a/plugins/hls-retrie-plugin/changelog.md b/plugins/hls-retrie-plugin/changelog.md deleted file mode 100644 index 6aa75fc28b..0000000000 --- a/plugins/hls-retrie-plugin/changelog.md +++ /dev/null @@ -1,2 +0,0 @@ -### 0.1.1.0 (2021-02-..) -* Fix bug in Retrieve "fold/unfold in local file" commands (#1202) diff --git a/plugins/hls-retrie-plugin/hls-retrie-plugin.cabal b/plugins/hls-retrie-plugin/hls-retrie-plugin.cabal deleted file mode 100644 index 25d4b58edb..0000000000 --- a/plugins/hls-retrie-plugin/hls-retrie-plugin.cabal +++ /dev/null @@ -1,81 +0,0 @@ -cabal-version: 2.2 -name: hls-retrie-plugin -version: 2.4.0.0 -synopsis: Retrie integration plugin for Haskell Language Server -description: - Please see the README on GitHub at -license: Apache-2.0 -license-file: LICENSE -author: Pepe Iborra -maintainer: pepeiborra@gmail.com -category: Development -build-type: Simple -extra-source-files: - LICENSE - test/Main.hs - test/testdata/*.hs - test/testdata/*.yaml - -source-repository head - type: git - location: https://github.com/haskell/haskell-language-server.git - -library - -- Plugins that need exactprint have not been updated for 9.8 yet - if impl(ghc >= 9.8) - buildable: False - else - buildable: True - exposed-modules: Ide.Plugin.Retrie - hs-source-dirs: src - build-depends: - , aeson - , base >=4.12 && <5 - , bytestring - , containers - , deepseq - , directory - , extra - , ghc - , ghcide == 2.4.0.0 - , hashable - , hls-plugin-api == 2.4.0.0 - , hls-refactor-plugin - , lens - , lsp - , lsp-types - , mtl - , retrie >=0.1.1.0 - , safe-exceptions - , stm - , text - , transformers - , unordered-containers - - default-language: Haskell2010 - default-extensions: - DataKinds - TypeOperators - - ghc-options: -Wno-unticked-promoted-constructors - -test-suite tests - if impl(ghc >= 9.8) - buildable: False - else - buildable: True - type: exitcode-stdio-1.0 - default-language: Haskell2010 - hs-source-dirs: test - main-is: Main.hs - ghc-options: -threaded -rtsopts -with-rtsopts=-N - build-depends: - , aeson - , base - , containers - , filepath - , hls-plugin-api - , hls-refactor-plugin - , hls-retrie-plugin - , hls-test-utils == 2.4.0.0 - , text diff --git a/plugins/hls-retrie-plugin/src/Ide/Plugin/Retrie.hs b/plugins/hls-retrie-plugin/src/Ide/Plugin/Retrie.hs index f20b39bc66..2e39ffcd98 100644 --- a/plugins/hls-retrie-plugin/src/Ide/Plugin/Retrie.hs +++ b/plugins/hls-retrie-plugin/src/Ide/Plugin/Retrie.hs @@ -1,24 +1,17 @@ {-# LANGUAGE CPP #-} {-# LANGUAGE DeriveAnyClass #-} -{-# LANGUAGE DeriveGeneric #-} -{-# LANGUAGE FlexibleContexts #-} +{-# LANGUAGE DerivingStrategies #-} {-# LANGUAGE LambdaCase #-} -{-# LANGUAGE NamedFieldPuns #-} {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE PartialTypeSignatures #-} {-# LANGUAGE PatternSynonyms #-} -{-# LANGUAGE RankNTypes #-} {-# LANGUAGE RecordWildCards #-} -{-# LANGUAGE ScopedTypeVariables #-} -{-# LANGUAGE StandaloneDeriving #-} -{-# LANGUAGE TypeApplications #-} {-# LANGUAGE TypeFamilies #-} {-# LANGUAGE ViewPatterns #-} {-# OPTIONS -Wno-orphans #-} -{-# LANGUAGE TupleSections #-} -module Ide.Plugin.Retrie (descriptor) where +module Ide.Plugin.Retrie (descriptor, Log) where import Control.Concurrent.STM (readTVarIO) import Control.Exception.Safe (Exception (..), @@ -31,61 +24,52 @@ import Control.Monad.IO.Class (MonadIO (liftIO)) import Control.Monad.Trans.Class (MonadTrans (lift)) import Control.Monad.Trans.Except (ExceptT (..), runExceptT) -import Control.Monad.Trans.Maybe -import Control.Monad.Trans.Writer.Strict +import Control.Monad.Trans.Maybe (MaybeT) import Data.Aeson (FromJSON (..), - ToJSON (..), Value) + ToJSON (..)) import Data.Bifunctor (second) import qualified Data.ByteString as BS -import Data.Coerce import Data.Data import Data.Either (partitionEithers) -import Data.Hashable (Hashable (hash), - unhashed) -import qualified Data.HashMap.Strict as HM +import Data.Hashable (unhashed) import qualified Data.HashSet as Set import Data.IORef.Extra (atomicModifyIORef'_, newIORef, readIORef) import Data.List.Extra (find, nubOrdOn) import qualified Data.Map as Map -import Data.Maybe (catMaybes, fromJust, - listToMaybe) +import Data.Maybe (catMaybes) +import Data.Monoid (First (First)) import Data.String (IsString) import qualified Data.Text as T import qualified Data.Text.Encoding as T -import Data.Typeable (Typeable) -import Debug.Trace +import qualified Data.Text.Utf16.Rope.Mixed as Rope import Development.IDE hiding (pluginHandlers) +import Development.IDE.Core.Actions (lookupMod) +import Development.IDE.Core.PluginUtils import Development.IDE.Core.PositionMapping import Development.IDE.Core.Shake (ShakeExtras (ShakeExtras, knownTargetsVar), - clientCapabilities, getShakeExtras, hiedbWriter, toKnownFiles, withHieDb) import Development.IDE.GHC.Compat (GRHSs (GRHSs), GenLocated (L), GhcPs, - GhcRn, GhcTc, + GhcRn, HsBindLR (FunBind), HsExpr (HsApp, OpApp), HsGroup (..), HsValBindsLR (..), - HscEnv, IdP, - ImportDecl (..), LHsExpr, - LRuleDecls, Match, - ModIface, + HscEnv, ImportDecl (..), + LHsExpr, LRuleDecls, + Match, ModIface, ModSummary (ModSummary, ms_hspp_buf, ms_mod), - Name, Outputable, - ParsedModule (..), - RealSrcLoc, + Outputable, ParsedModule, RuleDecl (HsRule), RuleDecls (HsRules), SourceText (..), TyClDecl (SynDecl), TyClGroup (..), fun_id, - hm_iface, isQual, - isQual_maybe, isVarOcc, + isQual, isQual_maybe, locA, mi_fixities, - moduleName, moduleNameString, ms_hspp_opts, nameModule_maybe, @@ -96,47 +80,37 @@ import Development.IDE.GHC.Compat (GRHSs (GRHSs), pattern NotBoot, pattern RealSrcSpan, pm_parsed_source, - printWithoutUniques, rdrNameOcc, rds_rules, srcSpanFile, topDir, unLoc, unLocA) import qualified Development.IDE.GHC.Compat as GHC import Development.IDE.GHC.Compat.Util hiding (catch, try) -import Development.IDE.GHC.Dump (showAstDataHtml) -import Development.IDE.GHC.ExactPrint (ExceptStringT (ExceptStringT), - GetAnnotatedParsedSource (GetAnnotatedParsedSource), - TransformT, - graftExprWithM, - graftSmallestDeclsWithM, - hoistGraft, transformM) -import qualified GHC (Module, ParsedSource, - moduleName, parseModule) +import Development.IDE.GHC.ExactPrint (GetAnnotatedParsedSource (GetAnnotatedParsedSource), + TransformT) +import Development.IDE.Spans.AtPoint (LookupModule, + nameToLocation) +import Development.IDE.Types.Shake (WithHieDb) import qualified GHC as GHCGHC import GHC.Generics (Generic) -import GHC.Hs.Dump -import Ide.Plugin.Error +import Ide.Plugin.Error (PluginError (PluginInternalError), + getNormalizedFilePathE) import Ide.PluginUtils import Ide.Types import qualified Language.LSP.Protocol.Lens as L import Language.LSP.Protocol.Message as LSP import Language.LSP.Protocol.Types as LSP -import Language.LSP.Server (LspM, - ProgressCancellable (Cancellable), - sendNotification, - sendRequest, - withIndefiniteProgress) +import Language.LSP.Server (ProgressCancellable (Cancellable)) import Retrie (Annotated (astA), AnnotatedModule, Fixity (Fixity), FixityDirection (InfixL), Options, Options_ (..), - RewriteSpec, Verbosity (Loud), addImports, apply, applyWithUpdate) import Retrie.Context import Retrie.CPP (CPP (NoCPP), parseCPP) -import Retrie.ExactPrint (Annotated, fix, +import Retrie.ExactPrint (fix, makeDeltaAst, transformA, unsafeMkA) import Retrie.Expr (mkLocatedHsVar) import Retrie.Fixity (FixityEnv, lookupOp, @@ -153,45 +127,37 @@ import Retrie.SYB (everything, extQ, listify, mkQ) import Retrie.Types import Retrie.Universe (Universe) -import System.Directory (makeAbsolute) -#if MIN_VERSION_ghc(9,3,0) import GHC.Types.PkgQual -#endif -import Control.Arrow ((&&&)) -import Control.Exception (evaluate) -import Data.Monoid (First (First)) -import Development.IDE.Core.Actions (lookupMod) -import Development.IDE.Core.PluginUtils -import Development.IDE.Spans.AtPoint (LookupModule, - getNamesAtPoint, - nameToLocation) -import Development.IDE.Types.Shake (WithHieDb) -import Retrie.ExactPrint (makeDeltaAst) -import Retrie.GHC (ann) +data Log + = LogParsingModule FilePath -descriptor :: PluginId -> PluginDescriptor IdeState -descriptor plId = - (defaultPluginDescriptor plId) +instance Pretty Log where + pretty = \case + LogParsingModule fp -> "Parsing module:" <+> pretty fp + +descriptor :: Recorder (WithPriority Log) -> PluginId -> PluginDescriptor IdeState +descriptor recorder plId = + (defaultPluginDescriptor plId "Provides code actions to inline Haskell definitions") { pluginHandlers = mkPluginHandler SMethod_TextDocumentCodeAction provider, - pluginCommands = [retrieCommand, retrieInlineThisCommand] + pluginCommands = [retrieCommand recorder, retrieInlineThisCommand recorder] } -retrieCommandName :: T.Text -retrieCommandName = "retrieCommand" +retrieCommandId :: CommandId +retrieCommandId = "retrieCommand" -retrieInlineThisCommandName :: T.Text -retrieInlineThisCommandName = "retrieInlineThisCommand" +retrieInlineThisCommandId :: CommandId +retrieInlineThisCommandId = "retrieInlineThisCommand" -retrieCommand :: PluginCommand IdeState -retrieCommand = - PluginCommand (coerce retrieCommandName) "run the refactoring" runRetrieCmd +retrieCommand :: Recorder (WithPriority Log) -> PluginCommand IdeState +retrieCommand recorder = + PluginCommand retrieCommandId "run the refactoring" (runRetrieCmd recorder) -retrieInlineThisCommand :: PluginCommand IdeState -retrieInlineThisCommand = - PluginCommand (coerce retrieInlineThisCommandName) "inline function call" - runRetrieInlineThisCmd +retrieInlineThisCommand :: Recorder (WithPriority Log) -> PluginCommand IdeState +retrieInlineThisCommand recorder = + PluginCommand retrieInlineThisCommandId "inline function call" + (runRetrieInlineThisCmd recorder) -- | Parameters for the runRetrie PluginCommand. data RunRetrieParams = RunRetrieParams @@ -201,10 +167,11 @@ data RunRetrieParams = RunRetrieParams restrictToOriginatingFile :: Bool } deriving (Eq, Show, Generic, FromJSON, ToJSON) -runRetrieCmd :: CommandFunction IdeState RunRetrieParams -runRetrieCmd state RunRetrieParams{originatingFile = uri, ..} = ExceptT $ - withIndefiniteProgress description Cancellable $ do - runExceptT $ do + +runRetrieCmd :: Recorder (WithPriority Log) -> CommandFunction IdeState RunRetrieParams +runRetrieCmd recorder state token RunRetrieParams{originatingFile = uri, ..} = ExceptT $ + pluginWithIndefiniteProgress description token Cancellable $ \_updater -> do + _ <- runExceptT $ do nfp <- getNormalizedFilePathE uri (session, _) <- runActionE "Retrie.GhcSessionDeps" state $ @@ -214,18 +181,19 @@ runRetrieCmd state RunRetrieParams{originatingFile = uri, ..} = ExceptT $ let importRewrites = concatMap (extractImports ms binds) rewrites (errors, edits) <- liftIO $ callRetrie + recorder state (hscEnv session) (map Right rewrites <> map Left importRewrites) nfp restrictToOriginatingFile unless (null errors) $ - lift $ sendNotification SMethod_WindowShowMessage $ + lift $ pluginSendNotification SMethod_WindowShowMessage $ ShowMessageParams MessageType_Warning $ T.unlines $ "## Found errors during rewrite:" : ["-" <> T.pack (show e) | e <- errors] - lift $ sendRequest SMethod_WorkspaceApplyEdit (ApplyWorkspaceEditParams Nothing edits) (\_ -> pure ()) + _ <- lift $ pluginSendRequest SMethod_WorkspaceApplyEdit (ApplyWorkspaceEditParams Nothing edits) (\_ -> pure ()) return () return $ Right $ InR Null @@ -236,8 +204,8 @@ data RunRetrieInlineThisParams = RunRetrieInlineThisParams } deriving (Eq, Show, Generic, FromJSON, ToJSON) -runRetrieInlineThisCmd :: CommandFunction IdeState RunRetrieInlineThisParams -runRetrieInlineThisCmd state RunRetrieInlineThisParams{..} = do +runRetrieInlineThisCmd :: Recorder (WithPriority Log) -> CommandFunction IdeState RunRetrieInlineThisParams +runRetrieInlineThisCmd recorder state _token RunRetrieInlineThisParams{..} = do nfp <- getNormalizedFilePathE $ getLocationUri inlineIntoThisLocation nfpSource <- getNormalizedFilePathE $ getLocationUri inlineFromThisLocation -- What we do here: @@ -246,23 +214,15 @@ runRetrieInlineThisCmd state RunRetrieInlineThisParams{..} = do -- Run retrie to get a list of changes -- Select the change that inlines the identifier in the given position -- Apply the edit - ast <- runActionE "retrie" state $ - useE GetAnnotatedParsedSource nfp astSrc <- runActionE "retrie" state $ useE GetAnnotatedParsedSource nfpSource - msr <- runActionE "retrie" state $ - useE GetModSummaryWithoutTimestamps nfp - hiFileRes <- runActionE "retrie" state $ - useE GetModIface nfpSource - let fixityEnv = fixityEnvFromModIface (hirModIface hiFileRes) - fromRange = rangeToRealSrcSpan nfpSource $ getLocationRange inlineFromThisLocation + let fromRange = rangeToRealSrcSpan nfpSource $ getLocationRange inlineFromThisLocation intoRange = rangeToRealSrcSpan nfp $ getLocationRange inlineIntoThisLocation - inlineRewrite <- liftIO $ constructInlineFromIdentifer astSrc fromRange + inlineRewrite <- liftIO $ constructInlineFromIdentifer (unsafeMkA astSrc 0) fromRange when (null inlineRewrite) $ throwError $ PluginInternalError "Empty rewrite" - let ShakeExtras{..} = shakeExtras state (session, _) <- runActionE "retrie" state $ useWithStaleE GhcSessionDeps nfp - (fixityEnv, cpp) <- liftIO $ getCPPmodule state (hscEnv session) $ fromNormalizedFilePath nfp + (fixityEnv, cpp) <- liftIO $ getCPPmodule recorder state (hscEnv session) $ fromNormalizedFilePath nfp result <- liftIO $ try @_ @SomeException $ runRetrie fixityEnv (applyWithUpdate myContextUpdater inlineRewrite) cpp case result of @@ -274,7 +234,7 @@ runRetrieInlineThisCmd state RunRetrieInlineThisParams{..} = do ourReplacement = [ r | r@Replacement{..} <- replacements , RealSrcSpan intoRange Nothing `GHC.isSubspanOf` replLocation] - lift $ sendRequest SMethod_WorkspaceApplyEdit + _ <- lift $ pluginSendRequest SMethod_WorkspaceApplyEdit (ApplyWorkspaceEditParams Nothing wedit) (\_ -> pure ()) return $ InR Null @@ -352,7 +312,7 @@ provider state plId (CodeActionParams _ _ (TextDocumentIdentifier uri) range ca) retrieCommands <- lift $ forM rewrites $ \(title, kind, params) -> liftIO $ do - let c = mkLspCommand plId (coerce retrieCommandName) title (Just [toJSON params]) + let c = mkLspCommand plId retrieCommandId title (Just [toJSON params]) return $ CodeAction title (Just kind) Nothing Nothing Nothing Nothing (Just c) Nothing inlineSuggestions <- liftIO $ runIdeAction "" extras $ @@ -367,33 +327,36 @@ provider state plId (CodeActionParams _ _ (TextDocumentIdentifier uri) range ca) getLocationUri :: Location -> Uri getLocationUri Location{_uri} = _uri +getLocationRange :: Location -> Range getLocationRange Location{_range} = _range -getBinds :: NormalizedFilePath -> ExceptT PluginError Action (ModSummary, [HsBindLR GhcRn GhcRn], PositionMapping, [LRuleDecls GhcRn], [TyClGroup GhcRn]) +getBinds :: NormalizedFilePath -> ExceptT PluginError Action + ( ModSummary + , [HsBindLR GhcRn GhcRn] + , PositionMapping + , [LRuleDecls GhcRn] + , [TyClGroup GhcRn] + ) getBinds nfp = do (tm, posMapping) <- useWithStaleE TypeCheck nfp -- we use the typechecked source instead of the parsed source -- to be able to extract module names from the Ids, -- so that we can include adding the required imports in the retrie command let rn = tmrRenamed tm - ( HsGroup - { hs_valds = - XValBindsLR - (GHC.NValBinds binds _sigs :: GHC.NHsValBindsLR GhcRn), - hs_ruleds, - hs_tyclds - }, - _, - _, - _ - ) = rn - - topLevelBinds = - [ decl - | (_, bagBinds) <- binds, - L _ decl <- bagToList bagBinds - ] - return (tmrModSummary tm, topLevelBinds, posMapping, hs_ruleds, hs_tyclds) + case rn of +#if MIN_VERSION_ghc(9,9,0) + (HsGroup{hs_valds, hs_ruleds, hs_tyclds}, _, _, _, _) -> do +#else + (HsGroup{hs_valds, hs_ruleds, hs_tyclds}, _, _, _) -> do +#endif + topLevelBinds <- case hs_valds of + ValBinds{} -> throwError $ PluginInternalError "getBinds: ValBinds not supported" + XValBindsLR (GHC.NValBinds binds _sigs :: GHC.NHsValBindsLR GhcRn) -> + pure [ decl + | (_, bagBinds) <- binds + , L _ decl <- bagToList bagBinds + ] + return (tmrModSummary tm, topLevelBinds, posMapping, hs_ruleds, hs_tyclds) suggestBindRewrites :: Uri -> @@ -417,8 +380,15 @@ suggestBindRewrites originatingFile pos ms_mod FunBind {fun_id = L (locA -> l') suggestBindRewrites _ _ _ _ = [] -- find all the identifiers in the AST for which have source definitions -suggestBindInlines :: PluginId -> Uri -> [HsBindLR GhcRn GhcRn] -> Range -> WithHieDb -> _ -> IdeAction [Command] -suggestBindInlines plId uri binds range hie lookupMod = do +suggestBindInlines :: + PluginId + -> Uri + -> [HsBindLR GhcRn GhcRn] + -> Range + -> WithHieDb + -> (FilePath -> GHCGHC.ModuleName -> GHCGHC.Unit -> Bool -> MaybeT IdeAction Uri) + -> IdeAction [Command] +suggestBindInlines plId _uri binds range hie lookupMod = do identifiers <- definedIdentifiers return $ map (\(name, siteLoc, srcLoc) -> let @@ -429,7 +399,7 @@ suggestBindInlines plId uri binds range hie lookupMod = do , inlineFromThisLocation = srcLoc , inlineThisDefinition= printedName } - in mkLspCommand plId (coerce retrieInlineThisCommandName) title (Just [toJSON params]) + in mkLspCommand plId retrieInlineThisCommandId title (Just [toJSON params]) ) (Set.toList identifiers) where @@ -437,7 +407,11 @@ suggestBindInlines plId uri binds range hie lookupMod = do -- we search for candidates to inline in RHSs only, skipping LHSs everything (<>) (pure mempty `mkQ` getGRHSIdentifierDetails hie lookupMod) binds - getGRHSIdentifierDetails :: WithHieDb -> _ -> GRHSs GhcRn (LHsExpr GhcRn) -> IdeAction (Set.HashSet (GHC.OccName, Location, Location)) + getGRHSIdentifierDetails :: + WithHieDb + -> (FilePath -> GHCGHC.ModuleName -> GHCGHC.Unit -> Bool -> MaybeT IdeAction Uri) + -> GRHSs GhcRn (LHsExpr GhcRn) + -> IdeAction (Set.HashSet (GHC.OccName, Location, Location)) getGRHSIdentifierDetails a b it@GRHSs{} = -- we only select candidates for which we have source code everything (<>) (pure mempty `mkQ` getDefinedIdentifierDetailsViaHieDb a b) it @@ -458,7 +432,6 @@ describeRestriction restrictToOriginatingFile = if restrictToOriginatingFile then " in current file" else "" suggestTypeRewrites :: - (Outputable (IdP GhcRn)) => Uri -> GHC.Module -> TyClDecl GhcRn -> @@ -492,11 +465,7 @@ suggestRuleRewrites originatingFile pos ms_mod (L _ HsRules {rds_rules}) = ] | L (locA -> l) r <- rds_rules, pos `isInsideSrcSpan` l, -#if MIN_VERSION_ghc(9,5,0) let HsRule {rd_name = L _ rn} = r, -#else - let HsRule {rd_name = L _ (_, rn)} = r, -#endif let ruleName = unpackFS rn ] where @@ -529,7 +498,7 @@ data CallRetrieError | NoParse NormalizedFilePath | GHCParseError NormalizedFilePath String | NoTypeCheck NormalizedFilePath - deriving (Eq, Typeable) + deriving (Eq) instance Show CallRetrieError where show (CallRetrieInternalError msg f) = msg <> " - " <> fromNormalizedFilePath f @@ -540,13 +509,14 @@ instance Show CallRetrieError where instance Exception CallRetrieError callRetrie :: + Recorder (WithPriority Log) -> IdeState -> HscEnv -> [Either ImportSpec RewriteSpec] -> NormalizedFilePath -> Bool -> IO ([CallRetrieError], WorkspaceEdit) -callRetrie state session rewrites origin restrictToOriginatingFile = do +callRetrie recorder state session rewrites origin restrictToOriginatingFile = do knownFiles <- toKnownFiles . unhashed <$> readTVarIO (knownTargetsVar $ shakeExtras state) let -- TODO cover all workspaceFolders @@ -574,9 +544,9 @@ callRetrie state session rewrites origin restrictToOriginatingFile = do targets <- getTargetFiles retrieOptions (getGroundTerms retrie) results <- forM targets $ \t -> runExceptT $ do - (fixityEnv, cpp) <- ExceptT $ try $ getCPPmodule state session t + (fixityEnv, cpp) <- ExceptT $ try $ getCPPmodule recorder state session t -- TODO add the imports to the resulting edits - (_user, ast, change@(Change _replacements _imports)) <- + (_user, _ast, change@(Change _replacements _imports)) <- lift $ runRetrie fixityEnv retrie cpp return $ asTextEdits change @@ -637,8 +607,12 @@ parseSpecs state origin originParsedModule originFixities specs = do originFixities specs +constructfromFunMatches :: + Annotated [GHCGHC.LocatedA (ImportDecl GhcPs)] + -> GHCGHC.LocatedN GHCGHC.RdrName + -> GHCGHC.MatchGroup GhcPs (GHCGHC.LocatedA (HsExpr GhcPs)) + -> TransformT IO [Rewrite Universe] constructfromFunMatches imps fun_id fun_matches = do - let fName = occNameFS (GHC.occName (unLoc fun_id)) fe <- mkLocatedHsVar fun_id rewrites <- concat <$> forM (unLoc $ GHC.mg_alts fun_matches) (matchToRewrites fe imps LeftToRight) @@ -647,24 +621,31 @@ constructfromFunMatches imps fun_id fun_matches = do assert (not $ null urewrites) $ return urewrites -showQuery = ppRewrite +-- showQuery :: Rewrite Universe -> String +-- showQuery = ppRewrite +-- -- showQuery :: Rewrite (LHsExpr GhcPs) -> String -- showQuery q = unlines -- [ "template: " <> show (hash (printOutputable . showAstData NoBlankSrcSpan . astA . tTemplate . fst . qResult $ q)) -- , "quantifiers: " <> show (hash (T.pack (show(Ext.toList $ qQuantifiers q)))) -- , "matcher: " <> show (hash (printOutputable . showAstData NoBlankSrcSpan . astA . qPattern $ q)) -- ] +-- +-- s :: Data a => a -> String +-- s = T.unpack . printOutputable . showAstData NoBlankSrcSpan +-- NoBlankEpAnnotations -s :: Data a => a -> String -s = T.unpack . printOutputable . showAstData NoBlankSrcSpan - NoBlankEpAnnotations +constructInlineFromIdentifer :: Data a => Annotated (GenLocated l a) -> GHCGHC.RealSrcSpan -> IO [Rewrite Universe] constructInlineFromIdentifer originParsedModule originSpan = do -- traceM $ s $ astA originParsedModule fmap astA $ transformA originParsedModule $ \(L _ m) -> do let ast = everything (<>) (First Nothing `mkQ` matcher) m - matcher :: HsBindLR GhcPs GhcPs -> First _ + matcher :: HsBindLR GhcPs GhcPs + -> First ( GHCGHC.LocatedN GHCGHC.RdrName + , GHCGHC.MatchGroup GhcPs (GHCGHC.LocatedA (HsExpr GhcPs)) + ) matcher FunBind{fun_id, fun_matches} - -- | trace (show (GHC.getLocA fun_id) <> ": " <> s fun_id) False = undefined + -- trace (show (GHC.getLocA fun_id) <> ": " <> s fun_id) False = undefined | RealSrcSpan sp _ <- GHC.getLocA fun_id , sp == originSpan = First $ Just (fun_id, fun_matches) @@ -674,7 +655,7 @@ constructInlineFromIdentifer originParsedModule originSpan = do -> let imports = mempty in constructfromFunMatches imports fun_id fun_matches - _ -> return $ error "cound not find source code to inline" + _ -> return $ error "could not find source code to inline" asEditMap :: [(Uri, TextEdit)] -> Map.Map Uri [TextEdit] asEditMap = Map.fromListWith (++) . map (second pure) @@ -724,7 +705,9 @@ deriving instance ToJSON RewriteSpec newtype IE name = IEVar name - deriving (Eq, Show, Generic, FromJSON, ToJSON) + deriving stock (Eq, Show, Generic) + deriving anyclass (FromJSON, ToJSON) + data ImportSpec = AddImport { ideclNameString :: String, @@ -741,33 +724,36 @@ toImportDecl AddImport {..} = GHC.ImportDecl {ideclSource = ideclSource', ..} ideclSource' = if ideclSource then IsBoot else NotBoot toMod = noLocA . GHC.mkModuleName ideclName = toMod ideclNameString -#if MIN_VERSION_ghc(9,3,0) - ideclPkgQual = NoRawPkgQual -#else - ideclPkgQual = Nothing -#endif ideclSafe = False ideclImplicit = False - ideclHiding = Nothing ideclSourceSrc = NoSourceText -#if MIN_VERSION_ghc(9,5,0) + ideclAs = toMod <$> ideclAsString + ideclQualified = if ideclQualifiedBool then GHC.QualifiedPre else GHC.NotQualified + + ideclPkgQual = NoRawPkgQual + + ideclImportList = Nothing ideclExt = GHCGHC.XImportDeclPass - { ideclAnn = GHCGHC.EpAnnNotUsed + { ideclAnn = +#if MIN_VERSION_ghc(9,9,0) + GHCGHC.noAnn +#else + GHCGHC.EpAnnNotUsed +#endif , ideclSourceText = ideclSourceSrc , ideclImplicit = ideclImplicit } -#else - ideclExt = GHCGHC.EpAnnNotUsed -#endif - ideclAs = toMod <$> ideclAsString - ideclQualified = if ideclQualifiedBool then GHC.QualifiedPre else GHC.NotQualified +reuseParsedModule :: IdeState -> NormalizedFilePath -> IO (FixityEnv, Annotated GHCGHC.ParsedSource) reuseParsedModule state f = do pm <- useOrFail state "Retrie.GetParsedModule" NoParse GetParsedModule f (fixities, pm') <- fixFixities state f (fixAnns pm) return (fixities, pm') -getCPPmodule state session t = do - nt <- toNormalizedFilePath' <$> makeAbsolute t + +getCPPmodule :: Recorder (WithPriority Log) -> IdeState -> HscEnv -> FilePath -> IO (FixityEnv, CPP AnnotatedModule) +getCPPmodule recorder state session t = do + -- TODO: is it safe to drop this makeAbsolute? + let nt = toNormalizedFilePath' $ (toAbsolute $ rootDir state) t let getParsedModule f contents = do modSummary <- msrModSummary <$> useOrFail state "Retrie.GetModSummary" (CallRetrieInternalError "file not found") GetModSummary nt @@ -776,17 +762,17 @@ getCPPmodule state session t = do { ms_hspp_buf = Just (stringToStringBuffer contents) } - logPriority (ideLogger state) Info $ T.pack $ "Parsing module: " <> t + logWith recorder Info $ LogParsingModule t parsed <- evalGhcEnv session (GHCGHC.parseModule ms') `catch` \e -> throwIO (GHCParseError nt (show @SomeException e)) (fixities, parsed) <- fixFixities state f (fixAnns parsed) return (fixities, parsed) contents <- do - (_, mbContentsVFS) <- + mbContentsVFS <- runAction "Retrie.GetFileContents" state $ getFileContents nt case mbContentsVFS of - Just contents -> return contents + Just contents -> return $ Rope.toText contents Nothing -> T.decodeUtf8 <$> BS.readFile (fromNormalizedFilePath nt) if any (T.isPrefixOf "#if" . T.toLower) (T.lines contents) then do diff --git a/plugins/hls-retrie-plugin/test/Main.hs b/plugins/hls-retrie-plugin/test/Main.hs index 5f8d12658a..96a25b0c4c 100644 --- a/plugins/hls-retrie-plugin/test/Main.hs +++ b/plugins/hls-retrie-plugin/test/Main.hs @@ -1,33 +1,38 @@ {-# LANGUAGE DisambiguateRecordFields #-} -{-# LANGUAGE NamedFieldPuns #-} +{-# LANGUAGE LambdaCase #-} {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE PartialTypeSignatures #-} -{-# LANGUAGE TypeOperators #-} module Main (main) where -import Control.Concurrent (threadDelay) import Control.Monad (void) -import Data.Aeson import qualified Data.Map as M import Data.Text (Text) -import qualified Development.IDE.GHC.ExactPrint +import qualified Development.IDE.GHC.ExactPrint as ExactPrint import qualified Development.IDE.Plugin.CodeAction as Refactor +import Ide.Logger import Ide.Plugin.Config import qualified Ide.Plugin.Retrie as Retrie -import Ide.Types (IdePlugins (IdePlugins)) import System.FilePath import Test.Hls -import Test.Hls (PluginTestDescriptor) + +data LogWrap + = RetrieLog Retrie.Log + | ExactPrintLog ExactPrint.Log + +instance Pretty LogWrap where + pretty = \case + RetrieLog msg -> pretty msg + ExactPrintLog msg -> pretty msg main :: IO () main = defaultTestRunner tests -retriePlugin :: PluginTestDescriptor a -retriePlugin = mkPluginTestDescriptor' Retrie.descriptor "retrie" +retriePlugin :: PluginTestDescriptor LogWrap +retriePlugin = mkPluginTestDescriptor (Retrie.descriptor . cmapWithPrio RetrieLog) "retrie" -refactorPlugin :: PluginTestDescriptor Development.IDE.GHC.ExactPrint.Log -refactorPlugin = mkPluginTestDescriptor Refactor.iePluginDescriptor "refactor" +refactorPlugin :: PluginTestDescriptor LogWrap +refactorPlugin = mkPluginTestDescriptor (Refactor.iePluginDescriptor . cmapWithPrio ExactPrintLog) "refactor" tests :: TestTree tests = testGroup "Retrie" @@ -55,24 +60,24 @@ inlineThisTests = testGroup "Inline this" ] ] - +testProvider :: TestName -> FilePath -> UInt -> UInt -> [Text] -> TestTree testProvider title file line row expected = testCase title $ runWithRetrie $ do adoc <- openDoc (file <.> "hs") "haskell" - waitForTypecheck adoc + _ <- waitForTypecheck adoc let position = Position line row codeActions <- getCodeActions adoc $ Range position position liftIO $ map codeActionTitle codeActions @?= map Just expected testCommand :: TestName -> FilePath -> UInt -> UInt -> TestTree testCommand title file row col = goldenWithRetrie title file $ \adoc -> do - waitForTypecheck adoc + _ <- waitForTypecheck adoc let p = Position row col codeActions <- getCodeActions adoc $ Range p p case codeActions of [InR ca] -> do executeCodeAction ca void $ skipManyTill anyMessage $ getDocumentEdit adoc - [] -> error "No code actions found" + cas -> liftIO . assertFailure $ "One code action expected, got " <> show (length cas) codeActionTitle :: (Command |? CodeAction) -> Maybe Text codeActionTitle (InR CodeAction {_title}) = Just _title @@ -80,15 +85,15 @@ codeActionTitle _ = Nothing goldenWithRetrie :: TestName -> FilePath -> (TextDocumentIdentifier -> Session ()) -> TestTree goldenWithRetrie title path act = - goldenWithHaskellDoc (def { plugins = M.fromList [("retrie", def)] }) testPlugins title testDataDir path "expected" "hs" act + goldenWithHaskellDoc (def { plugins = M.singleton "retrie" def }) testPlugins title testDataDir path "expected" "hs" act runWithRetrie :: Session a -> IO a runWithRetrie = runSessionWithServer def testPlugins testDataDir -testPlugins :: PluginTestDescriptor Development.IDE.GHC.ExactPrint.Log +testPlugins :: PluginTestDescriptor LogWrap testPlugins = retriePlugin <> refactorPlugin -- needed for the GetAnnotatedParsedSource rule testDataDir :: FilePath -testDataDir = "test" "testdata" +testDataDir = "plugins" "hls-retrie-plugin" "test" "testdata" diff --git a/plugins/hls-semantic-tokens-plugin/README.md b/plugins/hls-semantic-tokens-plugin/README.md new file mode 100644 index 0000000000..5d6be35ef5 --- /dev/null +++ b/plugins/hls-semantic-tokens-plugin/README.md @@ -0,0 +1,66 @@ +# Semantic tokens (LSP) plugin for Haskell language server + +## Purpose + +The purpose of this plugin is to provide semantic tokens for the Haskell language server, +according to the [LSP specification](https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_semanticTokens) +It can be used to provide semantic highlighting for Haskell code in editors by given semantic type and modifiers for some tokens. +A lot of editors support semantic highlighting through LSP, for example vscode, vim, emacs, etc. + +## Features + +### Semantic types and modifiers + +The handles request for semantic tokens for the whole file. +It supports semantic types and but not yet modifiers from the LSP specification. + +Default semantic types defined in lsp diverge greatly from the ones used in ghc. +But default semantic types allows user with less configuration to get semantic highlighting. +That is why we use default semantic types for now. By mapping ghc semantic types to lsp semantic types. +The mapping is defined in `Mapping.hs` file. + +### delta semantic tokens, range semantic tokens and refresh + +It is not yet support capabilities for delta semantic tokens, which might be +crucial for performance. +It should be implemented in the future. + +## checkList + +* Supported PluginMethodHandler + * [x] [textDocument/semanticTokens/full](https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#semanticTokens_fullRequest). + * [ ] [textDocument/semanticTokens/full/delta](https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#semanticTokens_deltaRequest) + * [ ] [workspace/semanticTokens/refresh](https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#semanticTokens_refreshRequest) + +* Supported semantic tokens type: + * [x] class and class method + * [x] type family name (data family) + * [x] data constructor name (not distinguishing record and normal data, and GADT) + * [x] type constructor name (GADT) + * [x] record field name + * [x] type synonym + * [x] pattern synonym + * [x] ~~pattern bindings~~ In favor of differing functions and none-functions from its type + * [x] ~~value bindings~~ In favor of differing functions and none-functions from its type + * [x] functions + * [x] none-function variables + * [x] imported name + +* Supported modifiers(planning): + * [future] declaration (as in class declearations, type definition and type family) + * [future] definition (as in class instance declaration, left hand side value binding, and type family instance) + * [future] modification (as in rec field update) + +## Implementation details + +* [x] Compute visible names from renamedsource +* [x] Compute `NameSemanticMap` for imported and top level name tokens using `HscEnv`(with deps) and type checked result +* [x] Compute current module `NameSemanticMap` using `RefMap a` from the result of `GetHieAst` +* [x] Compute all visible `(Name, Span)` in current module, in turn compute their semantic token using the combination map of the above two `NameSemanticMap` +* [x] use default legends, Candidates map of token type with default token type: [Maps to default token types](https://github.com/soulomoon/haskell-language-server/blob/master/plugins/hls-semantic-tokens-plugin/src/Ide/Plugin/SemanticTokens/Mappings.hs) +* [x] add args support to turn the plugin on and off +* [x] enhence test +* [x] enhence error reporting. +* [x] computation of semantic tokens is pushed into rule `getSemanticTokensRule` +* [future] make use of modifiers +* [future] hadling customize legends using server capabilities (how?) diff --git a/plugins/hls-semantic-tokens-plugin/src/Ide/Plugin/SemanticTokens.hs b/plugins/hls-semantic-tokens-plugin/src/Ide/Plugin/SemanticTokens.hs new file mode 100644 index 0000000000..28e05f5e8c --- /dev/null +++ b/plugins/hls-semantic-tokens-plugin/src/Ide/Plugin/SemanticTokens.hs @@ -0,0 +1,24 @@ +{-# LANGUAGE OverloadedStrings #-} + + +module Ide.Plugin.SemanticTokens (descriptor) where + +import Development.IDE +import qualified Ide.Plugin.SemanticTokens.Internal as Internal +import Ide.Plugin.SemanticTokens.Types +import Ide.Types +import Language.LSP.Protocol.Message + +descriptor :: Recorder (WithPriority SemanticLog) -> PluginId -> PluginDescriptor IdeState +descriptor recorder plId = + (defaultPluginDescriptor plId "Provides semantic tokens") + { Ide.Types.pluginHandlers = + mkPluginHandler SMethod_TextDocumentSemanticTokensFull (Internal.semanticTokensFull recorder) + <> mkPluginHandler SMethod_TextDocumentSemanticTokensFullDelta (Internal.semanticTokensFullDelta recorder), + Ide.Types.pluginRules = Internal.getSemanticTokensRule recorder, + pluginConfigDescriptor = + defaultConfigDescriptor + { configInitialGenericConfig = (configInitialGenericConfig defaultConfigDescriptor) {plcGlobalOn = False} + , configCustomConfig = mkCustomConfig Internal.semanticConfigProperties + } + } diff --git a/plugins/hls-semantic-tokens-plugin/src/Ide/Plugin/SemanticTokens/Internal.hs b/plugins/hls-semantic-tokens-plugin/src/Ide/Plugin/SemanticTokens/Internal.hs new file mode 100644 index 0000000000..b8b07e667f --- /dev/null +++ b/plugins/hls-semantic-tokens-plugin/src/Ide/Plugin/SemanticTokens/Internal.hs @@ -0,0 +1,163 @@ +{-# LANGUAGE DataKinds #-} +{-# LANGUAGE DerivingStrategies #-} +{-# LANGUAGE OverloadedLabels #-} +{-# LANGUAGE OverloadedRecordDot #-} +{-# LANGUAGE OverloadedStrings #-} +{-# LANGUAGE RecordWildCards #-} +{-# LANGUAGE TemplateHaskell #-} +{-# LANGUAGE TypeFamilies #-} +{-# LANGUAGE UnicodeSyntax #-} + +-- | +-- This module provides the core functionality of the plugin. +module Ide.Plugin.SemanticTokens.Internal (semanticTokensFull, getSemanticTokensRule, semanticConfigProperties, semanticTokensFullDelta) where + +import Control.Concurrent.STM (stateTVar) +import Control.Concurrent.STM.Stats (atomically) +import Control.Lens ((^.)) +import Control.Monad.Except (ExceptT, liftEither, + withExceptT) +import Control.Monad.IO.Class (MonadIO (..)) +import Control.Monad.Trans (lift) +import Control.Monad.Trans.Except (runExceptT) +import qualified Data.Map.Strict as M +import Data.Text (Text) +import qualified Data.Text as T +import Development.IDE (Action, + GetDocMap (GetDocMap), + GetHieAst (GetHieAst), + HieAstResult (HAR, hieAst, hieModule, refMap), + IdeResult, IdeState, + Priority (..), + Recorder, Rules, + WithPriority, + cmapWithPrio, define, + fromNormalizedFilePath, + hieKind) +import Development.IDE.Core.PluginUtils (runActionE, useE, + useWithStaleE) +import Development.IDE.Core.Rules (toIdeResult) +import Development.IDE.Core.RuleTypes (DocAndTyThingMap (..)) +import Development.IDE.Core.Shake (ShakeExtras (..), + getShakeExtras, + getVirtualFile) +import Development.IDE.GHC.Compat hiding (Warning) +import Development.IDE.GHC.Compat.Util (mkFastString) +import Ide.Logger (logWith) +import Ide.Plugin.Error (PluginError (PluginInternalError), + getNormalizedFilePathE, + handleMaybe, + handleMaybeM) +import Ide.Plugin.SemanticTokens.Mappings +import Ide.Plugin.SemanticTokens.Query +import Ide.Plugin.SemanticTokens.SemanticConfig (mkSemanticConfigFunctions) +import Ide.Plugin.SemanticTokens.Tokenize (computeRangeHsSemanticTokenTypeList) +import Ide.Plugin.SemanticTokens.Types +import Ide.Types +import qualified Language.LSP.Protocol.Lens as L +import Language.LSP.Protocol.Message (MessageResult, + Method (Method_TextDocumentSemanticTokensFull, Method_TextDocumentSemanticTokensFullDelta)) +import Language.LSP.Protocol.Types (NormalizedFilePath, + SemanticTokens, + type (|?) (InL, InR)) +import Prelude hiding (span) +import qualified StmContainers.Map as STM + + +$mkSemanticConfigFunctions + +----------------------- +---- the api +----------------------- + +computeSemanticTokens :: Recorder (WithPriority SemanticLog) -> PluginId -> IdeState -> NormalizedFilePath -> ExceptT PluginError Action SemanticTokens +computeSemanticTokens recorder pid _ nfp = do + config <- lift $ useSemanticConfigAction pid + logWith recorder Debug (LogConfig config) + semanticId <- lift getAndIncreaseSemanticTokensId + (RangeHsSemanticTokenTypes {rangeSemanticList}, mapping) <- useWithStaleE GetSemanticTokens nfp + withExceptT PluginInternalError $ liftEither $ rangeSemanticsSemanticTokens semanticId config mapping rangeSemanticList + +semanticTokensFull :: Recorder (WithPriority SemanticLog) -> PluginMethodHandler IdeState 'Method_TextDocumentSemanticTokensFull +semanticTokensFull recorder state pid param = runActionE "SemanticTokens.semanticTokensFull" state computeSemanticTokensFull + where + computeSemanticTokensFull :: ExceptT PluginError Action (MessageResult Method_TextDocumentSemanticTokensFull) + computeSemanticTokensFull = do + nfp <- getNormalizedFilePathE (param ^. L.textDocument . L.uri) + items <- computeSemanticTokens recorder pid state nfp + lift $ setSemanticTokens nfp items + return $ InL items + + +semanticTokensFullDelta :: Recorder (WithPriority SemanticLog) -> PluginMethodHandler IdeState 'Method_TextDocumentSemanticTokensFullDelta +semanticTokensFullDelta recorder state pid param = do + nfp <- getNormalizedFilePathE (param ^. L.textDocument . L.uri) + let previousVersionFromParam = param ^. L.previousResultId + runActionE "SemanticTokens.semanticTokensFullDelta" state $ computeSemanticTokensFullDelta recorder previousVersionFromParam pid state nfp + where + computeSemanticTokensFullDelta :: Recorder (WithPriority SemanticLog) -> Text -> PluginId -> IdeState -> NormalizedFilePath -> ExceptT PluginError Action (MessageResult Method_TextDocumentSemanticTokensFullDelta) + computeSemanticTokensFullDelta recorder previousVersionFromParam pid state nfp = do + semanticTokens <- computeSemanticTokens recorder pid state nfp + previousSemanticTokensMaybe <- lift $ getPreviousSemanticTokens nfp + lift $ setSemanticTokens nfp semanticTokens + case previousSemanticTokensMaybe of + Nothing -> return $ InL semanticTokens + Just previousSemanticTokens -> + if Just previousVersionFromParam == previousSemanticTokens^.L.resultId + then return $ InR $ InL $ makeSemanticTokensDeltaWithId (semanticTokens^.L.resultId) previousSemanticTokens semanticTokens + else do + logWith recorder Warning (LogSemanticTokensDeltaMisMatch previousVersionFromParam (previousSemanticTokens^.L.resultId)) + return $ InL semanticTokens + +-- | Defines the 'getSemanticTokensRule' function, compute semantic tokens for a Haskell source file. +-- +-- This Rule collects information from various sources, including: +-- +-- Imported name token type from Rule 'GetDocMap' +-- Local names token type from 'hieAst' +-- Name locations from 'hieAst' +-- Visible names from 'tmrRenamed' + +-- +-- It then combines this information to compute the semantic tokens for the file. +getSemanticTokensRule :: Recorder (WithPriority SemanticLog) -> Rules () +getSemanticTokensRule recorder = + define (cmapWithPrio LogShake recorder) $ \GetSemanticTokens nfp -> handleError recorder $ do + (HAR {..}) <- withExceptT LogDependencyError $ useE GetHieAst nfp + (DKMap {getTyThingMap}, _) <- withExceptT LogDependencyError $ useWithStaleE GetDocMap nfp + ast <- handleMaybe (LogNoAST $ show nfp) $ getAsts hieAst M.!? (HiePath . mkFastString . fromNormalizedFilePath) nfp + virtualFile <- handleMaybeM LogNoVF $ getVirtualFile nfp + let hsFinder = idSemantic getTyThingMap (hieKindFunMasksKind hieKind) refMap + return $ computeRangeHsSemanticTokenTypeList hsFinder virtualFile ast + + +-- taken from /haskell-language-server/plugins/hls-code-range-plugin/src/Ide/Plugin/CodeRange/Rules.hs + +-- | Handle error in 'Action'. Returns an 'IdeResult' with no value and no diagnostics on error. (but writes log) +handleError :: Recorder (WithPriority msg) -> ExceptT msg Action a -> Action (IdeResult a) +handleError recorder action' = do + valueEither <- runExceptT action' + case valueEither of + Left msg -> do + logWith recorder Warning msg + pure $ toIdeResult (Left []) + Right value -> pure $ toIdeResult (Right value) + +----------------------- +-- helper functions +----------------------- + +-- keep track of the semantic tokens response id +-- so that we can compute the delta between two versions +getAndIncreaseSemanticTokensId :: Action SemanticTokenId +getAndIncreaseSemanticTokensId = do + ShakeExtras{semanticTokensId} <- getShakeExtras + liftIO $ atomically $ do + i <- stateTVar semanticTokensId (\val -> (val, val+1)) + return $ T.pack $ show i + +getPreviousSemanticTokens :: NormalizedFilePath -> Action (Maybe SemanticTokens) +getPreviousSemanticTokens uri = getShakeExtras >>= liftIO . atomically . STM.lookup uri . semanticTokensCache + +setSemanticTokens :: NormalizedFilePath -> SemanticTokens -> Action () +setSemanticTokens uri tokens = getShakeExtras >>= liftIO . atomically . STM.insert tokens uri . semanticTokensCache diff --git a/plugins/hls-semantic-tokens-plugin/src/Ide/Plugin/SemanticTokens/Mappings.hs b/plugins/hls-semantic-tokens-plugin/src/Ide/Plugin/SemanticTokens/Mappings.hs new file mode 100644 index 0000000000..d9bfc4449d --- /dev/null +++ b/plugins/hls-semantic-tokens-plugin/src/Ide/Plugin/SemanticTokens/Mappings.hs @@ -0,0 +1,248 @@ +{-# LANGUAGE GADTs #-} +{-# LANGUAGE OverloadedStrings #-} +{-# LANGUAGE TypeFamilies #-} + + +-- | +-- This module provides mappings to convert token type information in the Haskell IDE plugin. It includes functions for: +-- +-- 1. Mapping semantic token type to and from the LSP default token type. +-- 2. Mapping from GHC type and tyThing to semantic token type. +-- 3. Mapping from hieAst identifier details to haskell semantic token type. +-- 4. Mapping from LSP tokens to SemanticTokenOriginal. +module Ide.Plugin.SemanticTokens.Mappings where + +import qualified Data.Array as A +import Data.List.Extra (chunksOf, (!?)) +import qualified Data.Map.Strict as Map +import Data.Maybe (mapMaybe) +import qualified Data.Set as Set +import Data.Text (Text, unpack) +import Development.IDE (HieKind (HieFresh, HieFromDisk)) +import Development.IDE.GHC.Compat +import Ide.Plugin.SemanticTokens.Types +import Ide.Plugin.SemanticTokens.Utils (mkRange) +import Language.LSP.Protocol.Types (LspEnum (knownValues), + SemanticTokenAbsolute (SemanticTokenAbsolute), + SemanticTokenRelative (SemanticTokenRelative), + SemanticTokenTypes (..), + SemanticTokens (SemanticTokens), + UInt, absolutizeTokens) +import Language.LSP.VFS hiding (line) + +-- * 0. Mapping name to Hs semantic token type. + +nameInfixOperator :: Name -> Maybe HsSemanticTokenType +nameInfixOperator name | isSymOcc (nameOccName name) = Just TOperator +nameInfixOperator _ = Nothing + +-- * 1. Mapping semantic token type to and from the LSP default token type. + +-- | map from haskell semantic token type to LSP default token type +toLspTokenType :: SemanticTokensConfig -> HsSemanticTokenType -> SemanticTokenTypes +toLspTokenType conf tk = case tk of + TFunction -> stFunction conf + TVariable -> stVariable conf + TClassMethod -> stClassMethod conf + TTypeVariable -> stTypeVariable conf + TDataConstructor -> stDataConstructor conf + TClass -> stClass conf + TTypeConstructor -> stTypeConstructor conf + TTypeSynonym -> stTypeSynonym conf + TTypeFamily -> stTypeFamily conf + TRecordField -> stRecordField conf + TPatternSynonym -> stPatternSynonym conf + TModule -> stModule conf + TOperator -> stOperator conf + +lspTokenReverseMap :: SemanticTokensConfig -> Map.Map SemanticTokenTypes HsSemanticTokenType +lspTokenReverseMap config + | length xs /= Map.size mr = error "lspTokenReverseMap: token type mapping is not bijection" + | otherwise = mr + where xs = enumFrom minBound + mr = Map.fromList $ map (\x -> (toLspTokenType config x, x)) xs + +lspTokenTypeHsTokenType :: SemanticTokensConfig -> SemanticTokenTypes -> Maybe HsSemanticTokenType +lspTokenTypeHsTokenType cf tk = Map.lookup tk (lspTokenReverseMap cf) + +-- * 2. Mapping from GHC type and tyThing to semantic token type. + +-- | tyThingSemantic +tyThingSemantic :: TyThing -> Maybe HsSemanticTokenType +tyThingSemantic ty | (Just hst) <- tyThingSemantic' ty = Just hst <> nameInfixOperator (getName ty) +tyThingSemantic _ = Nothing +tyThingSemantic' :: TyThing -> Maybe HsSemanticTokenType +tyThingSemantic' ty = case ty of + AnId vid + | isTyVar vid -> Just TTypeVariable + | isRecordSelector vid -> Just TRecordField + | isClassOpId vid -> Just TClassMethod + | isFunVar vid -> Just TFunction + | otherwise -> Just TVariable + AConLike con -> case con of + RealDataCon _ -> Just TDataConstructor + PatSynCon _ -> Just TPatternSynonym + ATyCon tyCon + | isTypeSynonymTyCon tyCon -> Just TTypeSynonym + | isTypeFamilyTyCon tyCon -> Just TTypeFamily + | isClassTyCon tyCon -> Just TClass + -- fall back to TTypeConstructor the result + | otherwise -> Just TTypeConstructor + ACoAxiom _ -> Nothing + where + isFunVar :: Var -> Bool + isFunVar var = isFunType $ varType var + +-- expand the type synonym https://hackage.haskell.org/package/ghc-9.8.1/docs/src/GHC.Core.Type.html +expandTypeSyn :: Type -> Type +expandTypeSyn ty + | Just ty' <- coreView ty = expandTypeSyn ty' + | otherwise = ty + +isFunType :: Type -> Bool +isFunType a = case expandTypeSyn a of + ForAllTy _ t -> isFunType t + -- Development.IDE.GHC.Compat.Core.FunTy(pattern synonym), FunTyFlag which is used to distinguish + -- (->, =>, etc..) + FunTy flg _ rhs -> isVisibleFunArg flg || isFunType rhs + _x -> isFunTy a + + +hieKindFunMasksKind :: HieKind a -> HieFunMaskKind a +hieKindFunMasksKind hieKind = case hieKind of + HieFresh -> HieFreshFun + HieFromDisk full_file -> HieFromDiskFun $ recoverFunMaskArray (hie_types full_file) + +-- wz1000 offered +-- the idea from https://gitlab.haskell.org/ghc/haddock/-/blob/b0b0e0366457c9aefebcc94df74e5de4d00e17b7/haddock-api/src/Haddock/Backends/Hyperlinker/Utils.hs#L107 +-- optimize version of looking for which types are functions without unfolding the whole type +recoverFunMaskArray :: + -- | flat types + A.Array TypeIndex HieTypeFlat -> + -- | array of bool indicating whether the type is a function + A.Array TypeIndex Bool +recoverFunMaskArray flattened = unflattened + where + -- The recursion in 'unflattened' is crucial - it's what gives us sharing + -- function indicator check. + unflattened :: A.Array TypeIndex Bool + unflattened = fmap (go . fmap (unflattened A.!)) flattened + + -- Unfold an 'HieType' whose sub-terms have already been unfolded + go :: HieType Bool -> Bool + go (HTyVarTy _name) = False + go (HAppTy _f _x) = False + go (HLitTy _lit) = False + go (HForAllTy ((_n, _k), _af) b) = b + go (HFunTy {}) = True + go (HQualTy _constraint b) = b + go (HCastTy b) = b + go HCoercionTy = False + -- we have no enough information to expand the type synonym + go (HTyConApp _ _) = False + +typeSemantic :: HieFunMaskKind hType -> hType -> Maybe HsSemanticTokenType +typeSemantic kind t = case kind of + HieFreshFun -> if isFunType t then Just TFunction else Nothing + HieFromDiskFun arr -> if arr A.! t then Just TFunction else Nothing + +-- * 3. Mapping from hieAst ContextInfo to haskell semantic token type. + +infoTokenType :: ContextInfo -> Maybe HsSemanticTokenType +infoTokenType x = case x of + Use -> Nothing + MatchBind -> Nothing + IEThing _ -> Nothing + TyDecl -> Nothing -- type signature + ValBind RegularBind _ _ -> Just TVariable + ValBind InstanceBind _ _ -> Just TClassMethod + PatternBind {} -> Just TVariable + ClassTyDecl _ -> Just TClassMethod + TyVarBind _ _ -> Just TTypeVariable + RecField _ _ -> Just TRecordField + -- data constructor, type constructor, type synonym, type family + Decl ClassDec _ -> Just TClass + Decl DataDec _ -> Just TTypeConstructor + Decl ConDec _ -> Just TDataConstructor + Decl SynDec _ -> Just TTypeSynonym + Decl FamDec _ -> Just TTypeFamily + -- instance dec is class method + Decl InstDec _ -> Just TClassMethod + Decl PatSynDec _ -> Just TPatternSynonym + EvidenceVarUse -> Nothing + EvidenceVarBind {} -> Nothing + +-- * 4. Mapping from LSP tokens to SemanticTokenOriginal. + +-- | recoverSemanticTokens +-- for debug and test. +-- this function is used to recover the original tokens(with token in haskell token type zoon) +-- from the lsp semantic tokens(with token in lsp token type zoon) +-- the `SemanticTokensConfig` used should be a map with bijection property +recoverSemanticTokens :: SemanticTokensConfig -> VirtualFile -> SemanticTokens -> Either Text [SemanticTokenOriginal HsSemanticTokenType] +recoverSemanticTokens config v s = do + tks <- recoverLspSemanticTokens v s + return $ map (lspTokenHsToken config) tks + +-- | lspTokenHsToken +-- for debug and test. +-- use the `SemanticTokensConfig` to convert lsp token type to haskell token type +-- the `SemanticTokensConfig` used should be a map with bijection property +lspTokenHsToken :: SemanticTokensConfig -> SemanticTokenOriginal SemanticTokenTypes -> SemanticTokenOriginal HsSemanticTokenType +lspTokenHsToken config (SemanticTokenOriginal tokenType location name) = + case lspTokenTypeHsTokenType config tokenType of + Just t -> SemanticTokenOriginal t location name + Nothing -> error "recoverSemanticTokens: unknown lsp token type" + +-- | recoverLspSemanticTokens +-- for debug and test. +-- this function is used to recover the original tokens(with token in standard lsp token type zoon) +-- from the lsp semantic tokens(with token in lsp token type zoon) +recoverLspSemanticTokens :: VirtualFile -> SemanticTokens -> Either Text [SemanticTokenOriginal SemanticTokenTypes] +recoverLspSemanticTokens vsf (SemanticTokens _ xs) = do + tokens <- dataActualToken xs + return $ mapMaybe (tokenOrigin sourceCode) tokens + where + sourceCode = unpack $ virtualFileText vsf + tokenOrigin :: [Char] -> SemanticTokenAbsolute -> Maybe (SemanticTokenOriginal SemanticTokenTypes) + tokenOrigin sourceCode' (SemanticTokenAbsolute line startChar len tokenType _tokenModifiers) = do + -- convert back to count from 1 + let range = mkRange line startChar len + CodePointRange (CodePointPosition x y) (CodePointPosition _ y1) <- rangeToCodePointRange vsf range + let line' = x + let startChar' = y + let len' = y1 - y + let tLine = lines sourceCode' !? fromIntegral line' + let name = maybe "no source" (take (fromIntegral len') . drop (fromIntegral startChar')) tLine + return $ SemanticTokenOriginal tokenType (Loc (line' + 1) (startChar' + 1) len') name + + dataActualToken :: [UInt] -> Either Text [SemanticTokenAbsolute] + dataActualToken dt = + maybe decodeError (Right . absolutizeTokens) $ + mapM fromTuple (chunksOf 5 $ map fromIntegral dt) + where + decodeError = Left "recoverSemanticTokenRelative: wrong token data" + fromTuple [a, b, c, d, _] = SemanticTokenRelative a b c <$> fromInt (fromIntegral d) <*> return [] + fromTuple _ = Nothing + + + -- legends :: SemanticTokensLegend + fromInt :: Int -> Maybe SemanticTokenTypes + fromInt i = Set.toAscList knownValues !? i + +-- Note [Semantic information from Multiple Sources] +-- ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +-- We group Name into 2 categories since the information source is different: +-- 1. Locally defined Name +-- Information source is current module's HieAst, +-- Either from ContextInfo(all except differing function and none-function) +-- or from Hie Type(Differing Function and Non-function Variable) +-- 2. Imported Name +-- Information source is `TyThing` for the `Name`, looked up in `HscEnv`(with all imported things loaded). +-- `TyThing` is information rich, since it is used to represent the things that a name can refer to in ghc. +-- The reason why we need special handling for imported name is that +-- Up to 9.8 +-- 1. For Hie Type, IfaceTyCon in hie type does not contain enough information to distinguish class, type syn, type family etc.. +-- 2. Most imported name is only annotated as [Use] in the ContextInfo from hie. +-- 3. `namespace` in `Name` is limited, we can only classify `VarName, FldName, DataName, TvNamem, TcClsName`. +-- 4. WiredIn `Name` have `TyThing` attached, but not many are WiredIn names. diff --git a/plugins/hls-semantic-tokens-plugin/src/Ide/Plugin/SemanticTokens/Query.hs b/plugins/hls-semantic-tokens-plugin/src/Ide/Plugin/SemanticTokens/Query.hs new file mode 100644 index 0000000000..fb7fdd9e71 --- /dev/null +++ b/plugins/hls-semantic-tokens-plugin/src/Ide/Plugin/SemanticTokens/Query.hs @@ -0,0 +1,92 @@ +-- | +-- The query module is used to query the semantic tokens from the AST +module Ide.Plugin.SemanticTokens.Query where + +import Control.Applicative ((<|>)) +import Data.Foldable (fold) +import qualified Data.Map.Strict as M +import Data.Maybe (listToMaybe, mapMaybe) +import qualified Data.Set as Set +import Data.Text (Text) +import Development.IDE.Core.PositionMapping (PositionMapping, + toCurrentRange) +import Development.IDE.GHC.Compat +import Ide.Plugin.SemanticTokens.Mappings +import Ide.Plugin.SemanticTokens.Types (HieFunMaskKind, + HsSemanticTokenType (TModule), + RangeSemanticTokenTypeList, + SemanticTokenId, + SemanticTokensConfig) +import Language.LSP.Protocol.Types (Position (Position), + Range (Range), + SemanticTokenAbsolute (SemanticTokenAbsolute), + SemanticTokens (SemanticTokens), + SemanticTokensDelta (SemanticTokensDelta), + defaultSemanticTokensLegend, + makeSemanticTokens, + makeSemanticTokensDelta) +import Prelude hiding (length, span) + +--------------------------------------------------------- + +-- * extract semantic + +--------------------------------------------------------- + +idSemantic :: forall a. NameEnv TyThing -> HieFunMaskKind a -> RefMap a -> Identifier -> Maybe HsSemanticTokenType +idSemantic _ _ _ (Left _) = Just TModule +idSemantic tyThingMap hieKind rm (Right n) = + nameSemanticFromHie hieKind rm n -- local name + <|> (lookupNameEnv tyThingMap n >>= tyThingSemantic) -- global name + + +--------------------------------------------------------- + +-- * extract semantic from HieAst for local variables + +--------------------------------------------------------- + +nameSemanticFromHie :: forall a. HieFunMaskKind a -> RefMap a -> Name -> Maybe HsSemanticTokenType +nameSemanticFromHie hieKind rm n = idSemanticFromRefMap rm (Right n) + where + idSemanticFromRefMap :: RefMap a -> Identifier -> Maybe HsSemanticTokenType + idSemanticFromRefMap rm' name' = do + spanInfos <- M.lookup name' rm' + let typeTokenType = foldMap (typeSemantic hieKind) $ listToMaybe $ mapMaybe (identType . snd) spanInfos + contextInfoTokenType <- foldMap (contextInfosMaybeTokenType . identInfo . snd) spanInfos + fold [typeTokenType, Just contextInfoTokenType, nameInfixOperator n] + + contextInfosMaybeTokenType :: Set.Set ContextInfo -> Maybe HsSemanticTokenType + contextInfosMaybeTokenType details = foldMap infoTokenType (Set.toList details) + + +------------------------------------------------- + +-- * extract lsp semantic tokens from RangeSemanticTokenTypeList + +------------------------------------------------- + +rangeSemanticsSemanticTokens :: SemanticTokenId -> SemanticTokensConfig -> PositionMapping -> RangeSemanticTokenTypeList -> Either Text SemanticTokens +rangeSemanticsSemanticTokens sid stc mapping = + makeSemanticTokensWithId (Just sid) . mapMaybe (\(ran, tk) -> toAbsSemanticToken <$> toCurrentRange mapping ran <*> return tk) + where + toAbsSemanticToken :: Range -> HsSemanticTokenType -> SemanticTokenAbsolute + toAbsSemanticToken (Range (Position startLine startColumn) (Position _endLine endColumn)) tokenType = + let len = endColumn - startColumn + in SemanticTokenAbsolute + (fromIntegral startLine) + (fromIntegral startColumn) + (fromIntegral len) + (toLspTokenType stc tokenType) + [] + +makeSemanticTokensWithId :: Maybe SemanticTokenId -> [SemanticTokenAbsolute] -> Either Text SemanticTokens +makeSemanticTokensWithId sid tokens = do + (SemanticTokens _ tokens) <- makeSemanticTokens defaultSemanticTokensLegend tokens + return $ SemanticTokens sid tokens + +makeSemanticTokensDeltaWithId :: Maybe SemanticTokenId -> SemanticTokens -> SemanticTokens -> SemanticTokensDelta +makeSemanticTokensDeltaWithId sid previousTokens currentTokens = + let (SemanticTokensDelta _ stEdits) = makeSemanticTokensDelta previousTokens currentTokens + in SemanticTokensDelta sid stEdits + diff --git a/plugins/hls-semantic-tokens-plugin/src/Ide/Plugin/SemanticTokens/SemanticConfig.hs b/plugins/hls-semantic-tokens-plugin/src/Ide/Plugin/SemanticTokens/SemanticConfig.hs new file mode 100644 index 0000000000..e9e8034ce3 --- /dev/null +++ b/plugins/hls-semantic-tokens-plugin/src/Ide/Plugin/SemanticTokens/SemanticConfig.hs @@ -0,0 +1,134 @@ +{-# LANGUAGE DataKinds #-} +{-# LANGUAGE GADTs #-} +{-# LANGUAGE OverloadedLabels #-} +{-# LANGUAGE OverloadedStrings #-} +{-# LANGUAGE TemplateHaskellQuotes #-} +{-# LANGUAGE TypeFamilies #-} + +module Ide.Plugin.SemanticTokens.SemanticConfig where + +import Data.Char (toLower) +import Data.Default (def) +import qualified Data.Set as S +import Data.Text (Text) +import qualified Data.Text as T +import Development.IDE (Action, usePropertyAction) +import GHC.TypeLits (KnownSymbol) +import Ide.Plugin.Properties (KeyNameProxy, NotElem, + Properties, + PropertyKey (type PropertyKey), + PropertyType (type TEnum), + defineEnumProperty, + emptyProperties) +import Ide.Plugin.SemanticTokens.Types +import Ide.Types (PluginId) +import Language.Haskell.TH +import Language.LSP.Protocol.Types (LspEnum (..), + SemanticTokenTypes) + +docName :: HsSemanticTokenType -> T.Text +docName tt = case tt of + TVariable -> "variables" + TFunction -> "functions" + TDataConstructor -> "data constructors" + TTypeVariable -> "type variables" + TClassMethod -> "typeclass methods" + TPatternSynonym -> "pattern synonyms" + TTypeConstructor -> "type constructors" + TClass -> "typeclasses" + TTypeSynonym -> "type synonyms" + TTypeFamily -> "type families" + TRecordField -> "record fields" + TModule -> "modules" + TOperator -> "operators" + +toConfigName :: String -> String +toConfigName = ("st" <>) + +type LspTokenTypeDescriptions = [(SemanticTokenTypes, T.Text)] + +lspTokenTypeDescriptions :: LspTokenTypeDescriptions +lspTokenTypeDescriptions = + map + ( \x -> + (x, "LSP Semantic Token Type: " <> toEnumBaseType x) + ) + $ S.toList knownValues + +allHsTokenTypes :: [HsSemanticTokenType] +allHsTokenTypes = enumFrom minBound + +lowerFirst :: String -> String +lowerFirst [] = [] +lowerFirst (x : xs) = toLower x : xs + +allHsTokenNameStrings :: [String] +allHsTokenNameStrings = map (drop 1 . show) allHsTokenTypes + +defineSemanticProperty :: + (NotElem s r, KnownSymbol s) => + (KeyNameProxy s, Text, SemanticTokenTypes) -> + Properties r -> + Properties ('PropertyKey s (TEnum SemanticTokenTypes) : r) +defineSemanticProperty (lb, tokenType, st) = + defineEnumProperty + lb + tokenType + lspTokenTypeDescriptions + st + +semanticDef :: SemanticTokensConfig +semanticDef = def + +-- | it produces the following functions: +-- semanticConfigProperties :: Properties '[ +-- 'PropertyKey "Variable" ('TEnum SemanticTokenTypes), +-- ... +-- ] +-- useSemanticConfigAction :: PluginId -> Action SemanticTokensConfig +mkSemanticConfigFunctions :: Q [Dec] +mkSemanticConfigFunctions = do + let pid = mkName "pid" + let semanticConfigPropertiesName = mkName "semanticConfigProperties" + let useSemanticConfigActionName = mkName "useSemanticConfigAction" + let allLabelStrs = map ((<> "Token") . lowerFirst) allHsTokenNameStrings + allLabels = map (LabelE . (<> "Token") . lowerFirst) allHsTokenNameStrings + allFieldsNames = map (mkName . toConfigName) allHsTokenNameStrings + allVariableNames = map (mkName . ("_variable_" <>) . toConfigName) allHsTokenNameStrings + -- <- useSemanticConfigAction label pid config + mkGetProperty (variable, label) = + BindS + (VarP variable) + (AppE (VarE 'usePropertyAction) label `AppE` VarE pid `AppE` VarE semanticConfigPropertiesName) + getProperties = zipWith (curry mkGetProperty) allVariableNames allLabels + recordUpdate = + RecUpdE (VarE 'semanticDef) $ + zipWith (\fieldName variableName -> (fieldName, VarE variableName)) allFieldsNames allVariableNames + -- get and then update record + bb = DoE Nothing $ getProperties ++ [NoBindS $ AppE (VarE 'return) recordUpdate] + let useSemanticConfigAction = FunD useSemanticConfigActionName [Clause [VarP pid] (NormalB bb) []] + let useSemanticConfigActionSig = SigD useSemanticConfigActionName (ArrowT `AppT` ConT ''PluginId `AppT` (ConT ''Action `AppT` ConT ''SemanticTokensConfig)) + + -- SemanticConfigProperties + nameAndDescList <- + mapM + ( \(lb, x) -> do + desc <- [|"LSP semantic token type to use for " <> docName x|] + lspToken <- [|toLspTokenType def x|] + return $ TupE [Just lb, Just desc, Just lspToken] + ) + $ zip allLabels allHsTokenTypes + let body = foldr (AppE . AppE (VarE 'defineSemanticProperty)) (VarE 'emptyProperties) nameAndDescList + let propertiesType = + foldr + ( \la -> + AppT + ( PromotedConsT + `AppT` (AppT (ConT 'PropertyKey) (LitT (StrTyLit la)) `AppT` AppT (ConT 'TEnum) (ConT ''SemanticTokenTypes)) + ) + ) + PromotedNilT + allLabelStrs + let semanticConfigProperties = FunD semanticConfigPropertiesName [Clause [] (NormalB body) []] + let semanticConfigPropertiesSig = SigD semanticConfigPropertiesName (AppT (ConT ''Properties) propertiesType) + return [semanticConfigPropertiesSig, semanticConfigProperties, useSemanticConfigActionSig, useSemanticConfigAction] diff --git a/plugins/hls-semantic-tokens-plugin/src/Ide/Plugin/SemanticTokens/Tokenize.hs b/plugins/hls-semantic-tokens-plugin/src/Ide/Plugin/SemanticTokens/Tokenize.hs new file mode 100644 index 0000000000..2ed11be333 --- /dev/null +++ b/plugins/hls-semantic-tokens-plugin/src/Ide/Plugin/SemanticTokens/Tokenize.hs @@ -0,0 +1,200 @@ +{-# LANGUAGE OverloadedRecordDot #-} +{-# LANGUAGE OverloadedStrings #-} + +module Ide.Plugin.SemanticTokens.Tokenize (computeRangeHsSemanticTokenTypeList) where + +import Control.Lens (Identity (runIdentity)) +import Control.Monad (foldM, guard) +import Control.Monad.State.Strict (MonadState (get), + MonadTrans (lift), + evalStateT, modify, put) +import Control.Monad.Trans.State.Strict (StateT, runStateT) +import Data.Char (isAlphaNum) +import Data.DList (DList) +import qualified Data.DList as DL +import qualified Data.Map.Strict as M +import qualified Data.Map.Strict as Map +import Data.Text (Text) +import qualified Data.Text as T +import qualified Data.Text.Rope as Char +import qualified Data.Text.Utf16.Rope as Utf16 +import Data.Text.Utf16.Rope.Mixed (Rope) +import qualified Data.Text.Utf16.Rope.Mixed as Rope +import Development.IDE.GHC.Compat +import Development.IDE.GHC.Error (realSrcSpanToCodePointRange) +import Ide.Plugin.SemanticTokens.Types (HsSemanticTokenType (TModule), + RangeHsSemanticTokenTypes (..)) +import Language.LSP.Protocol.Types (Position (Position), + Range (Range), UInt, mkRange) +import Language.LSP.VFS hiding (line) +import Prelude hiding (length, span) + +type Tokenizer m a = StateT PTokenState m a +type HsSemanticLookup = Identifier -> Maybe HsSemanticTokenType + + +data PTokenState = PTokenState + { + rope :: !Rope -- the remains of rope we are working on + , cursor :: !Char.Position -- the cursor position of the current rope to the start of the original file in code point position + , columnsInUtf16 :: !UInt -- the column of the start of the current rope in utf16 + } + +data SplitResult + = NoSplit (Text, Range) -- does not need to split, token text, token range + | Split (Text, Range, Range) -- token text, prefix range(module range), token range + deriving (Show) + +getSplitTokenText :: SplitResult -> Text +getSplitTokenText (NoSplit (t, _)) = t +getSplitTokenText (Split (t, _, _)) = t + + +mkPTokenState :: VirtualFile -> PTokenState +mkPTokenState vf = + PTokenState + { + rope = vf._file_text, + cursor = Char.Position 0 0, + columnsInUtf16 = 0 + } + +-- lift a Tokenizer Maybe a to Tokenizer m a, +-- if the Maybe is Nothing, do nothing, recover the state, and return the mempty value +-- if the Maybe is Just x, do the action, and keep the state, and return x +liftMaybeM :: (Monad m, Monoid a) => Tokenizer Maybe a -> Tokenizer m a +liftMaybeM p = do + st <- get + maybe (return mempty) (\(ans, st') -> put st' >> return ans) $ runStateT p st + +foldMapM :: (Monad m, Monoid b, Foldable t) => (a -> m b) -> t a -> m b +foldMapM f ta = foldM (\b a -> mappend b <$> f a) mempty ta + +computeRangeHsSemanticTokenTypeList :: HsSemanticLookup -> VirtualFile -> HieAST a -> RangeHsSemanticTokenTypes +computeRangeHsSemanticTokenTypeList lookupHsTokenType vf ast = + RangeHsSemanticTokenTypes $ DL.toList $ runIdentity $ evalStateT (foldAst lookupHsTokenType ast) (mkPTokenState vf) +-- | foldAst +-- visit every leaf node in the ast in depth first order +foldAst :: (Monad m) => HsSemanticLookup -> HieAST t -> Tokenizer m (DList (Range, HsSemanticTokenType)) +foldAst lookupHsTokenType ast = if null (nodeChildren ast) + then liftMaybeM (visitLeafIds lookupHsTokenType ast) + else foldMapM (foldAst lookupHsTokenType) $ nodeChildren ast + +visitLeafIds :: HsSemanticLookup -> HieAST t -> Tokenizer Maybe (DList (Range, HsSemanticTokenType)) +visitLeafIds lookupHsTokenType leaf = liftMaybeM $ do + let span = nodeSpan leaf + (ran, token) <- focusTokenAt leaf + -- if `focusTokenAt` succeed, we can safely assume we have shift the cursor correctly + -- we do not need to recover the cursor state, even if the following computation failed + liftMaybeM $ do + -- only handle the leaf node with single column token + guard $ srcSpanStartLine span == srcSpanEndLine span + splitResult <- lift $ splitRangeByText token ran + foldMapM (combineNodeIds lookupHsTokenType ran splitResult) $ Map.filterWithKey (\k _ -> k == SourceInfo) $ getSourcedNodeInfo $ sourcedNodeInfo leaf + where + combineNodeIds :: (Monad m) => HsSemanticLookup -> Range -> SplitResult -> NodeInfo a -> Tokenizer m (DList (Range, HsSemanticTokenType)) + combineNodeIds lookupHsTokenType ran ranSplit (NodeInfo _ _ bd) = + case (maybeTokenType, ranSplit) of + (Nothing, _) -> return mempty + (Just TModule, _) -> return $ DL.singleton (ran, TModule) + (Just tokenType, NoSplit (_, tokenRan)) -> return $ DL.singleton (tokenRan, tokenType) + (Just tokenType, Split (_, ranPrefix, tokenRan)) -> return $ DL.fromList [(ranPrefix, TModule),(tokenRan, tokenType)] + where maybeTokenType = foldMap (getIdentifier lookupHsTokenType ranSplit) (M.keys bd) + + getIdentifier :: HsSemanticLookup -> SplitResult -> Identifier -> Maybe HsSemanticTokenType + getIdentifier lookupHsTokenType ranSplit idt = do + case idt of + Left _moduleName -> Just TModule + Right name -> do + occStr <- T.pack <$> case (occNameString . nameOccName) name of + -- the generated selector name with {-# LANGUAGE DuplicateRecordFields #-} + '$' : 's' : 'e' : 'l' : ':' : xs -> Just $ takeWhile (/= ':') xs + -- other generated names that should not be visible + '$' : c : _ | isAlphaNum c -> Nothing + c : ':' : _ | isAlphaNum c -> Nothing + ns -> Just ns + guard $ getSplitTokenText ranSplit == occStr + lookupHsTokenType idt + + +focusTokenAt :: + -- | leaf node we want to focus on + HieAST a -> + -- | (token, remains) + Tokenizer Maybe (Range, Text) +focusTokenAt leaf = do + PTokenState{cursor, rope, columnsInUtf16} <- get + let span = nodeSpan leaf + let (tokenStartPos, tokenEndPos) = srcSpanCharPositions span + -- tokenStartOff: the offset position of the token start position to the cursor position + tokenStartOff <- lift $ tokenStartPos `sub` cursor + -- tokenOff: the offset position of the token end position to the token start position + tokenOff <- lift $ tokenEndPos `sub` tokenStartPos + (gap, tokenStartRope) <- lift $ charSplitAtPositionMaybe tokenStartOff rope + (token, remains) <- lift $ charSplitAtPositionMaybe tokenOff tokenStartRope + -- ncs: token start column in utf16 + let ncs = newColumn columnsInUtf16 gap + -- nce: token end column in utf16 + let nce = newColumn ncs token + -- compute the new range for utf16, tuning the columns is enough + let ran = codePointRangeToRangeWith ncs nce $ realSrcSpanToCodePointRange span + modify $ \s -> s {columnsInUtf16 = nce, rope = remains, cursor = tokenEndPos} + return (ran, token) + where + srcSpanCharPositions :: RealSrcSpan -> (Char.Position, Char.Position) + srcSpanCharPositions real = + ( realSrcLocRopePosition $ realSrcSpanStart real, + realSrcLocRopePosition $ realSrcSpanEnd real + ) + charSplitAtPositionMaybe :: Char.Position -> Rope -> Maybe (Text, Rope) + charSplitAtPositionMaybe tokenOff rpe = do + let (prefix, suffix) = Rope.charSplitAtPosition tokenOff rpe + guard $ Rope.charLengthAsPosition prefix == tokenOff + return (Rope.toText prefix, suffix) + sub :: Char.Position -> Char.Position -> Maybe Char.Position + sub (Char.Position l1 c1) (Char.Position l2 c2) + | l1 == l2 && c1 >= c2 = Just $ Char.Position 0 (c1 - c2) + | l1 > l2 = Just $ Char.Position (l1 - l2) c1 + | otherwise = Nothing + realSrcLocRopePosition :: RealSrcLoc -> Char.Position + realSrcLocRopePosition real = Char.Position (fromIntegral $ srcLocLine real - 1) (fromIntegral $ srcLocCol real - 1) + -- | newColumn + -- rope do not treat single \n in our favor + -- for example, the row length of "123\n" and "123" are both 1 + -- we are forced to use text to compute new column + newColumn :: UInt -> Text -> UInt + newColumn n rp = case T.breakOnEnd "\n" rp of + ("", nEnd) -> n + utf16Length nEnd + (_, nEnd) -> utf16Length nEnd + codePointRangeToRangeWith :: UInt -> UInt -> CodePointRange -> Range + codePointRangeToRangeWith newStartCol newEndCol (CodePointRange (CodePointPosition startLine _) (CodePointPosition endLine _)) = + Range (Position startLine newStartCol) (Position endLine newEndCol) + +-- | splitRangeByText +-- split a qualified identifier into module name and identifier and/or strip the (), `` +-- for `ModuleA.b`, break it into `ModuleA.` and `b` +-- for `(b)`, strip `()`, and get `b` +-- for `(ModuleA.b)`, strip `()` and break it into `ModuleA.` and `b` +splitRangeByText :: Text -> Range -> Maybe SplitResult +splitRangeByText tk ran = do + let (ran', tk') = case T.uncons tk of + Just ('(', xs) -> (subOneRange ran, T.takeWhile (/= ')') xs) + Just ('`', xs) -> (subOneRange ran, T.takeWhile (/= '`') xs) + _ -> (ran, tk) + let (prefix, tk'') = T.breakOnEnd "." tk' + splitRange tk'' (utf16PositionPosition $ Rope.utf16LengthAsPosition $ Rope.fromText prefix) ran' + where + splitRange :: Text -> Position -> Range -> Maybe SplitResult + splitRange tx (Position l c) r@(Range (Position l1 c1) (Position l2 c2)) + | l1 + l > l2 || (l1 + l == l2 && c > c2) = Nothing -- out of range + | l==0 && c==0 = Just $ NoSplit (tx, r) + | otherwise = let c' = if l <= 0 then c1+c else c + in Just $ Split (tx, mkRange l1 c1 (l1 + l) c', mkRange (l1 + l) c' l2 c2) + subOneRange :: Range -> Range + subOneRange (Range (Position l1 c1) (Position l2 c2)) = Range (Position l1 (c1 + 1)) (Position l2 (c2 - 1)) + utf16PositionPosition :: Utf16.Position -> Position + utf16PositionPosition (Utf16.Position l c) = Position (fromIntegral l) (fromIntegral c) + + +utf16Length :: Integral i => Text -> i +utf16Length = fromIntegral . Utf16.length . Utf16.fromText diff --git a/plugins/hls-semantic-tokens-plugin/src/Ide/Plugin/SemanticTokens/Types.hs b/plugins/hls-semantic-tokens-plugin/src/Ide/Plugin/SemanticTokens/Types.hs new file mode 100644 index 0000000000..7f445bf7ac --- /dev/null +++ b/plugins/hls-semantic-tokens-plugin/src/Ide/Plugin/SemanticTokens/Types.hs @@ -0,0 +1,160 @@ +{-# LANGUAGE DataKinds #-} +{-# LANGUAGE DerivingVia #-} +{-# LANGUAGE GADTs #-} +{-# LANGUAGE OverloadedStrings #-} +{-# LANGUAGE StrictData #-} +{-# LANGUAGE TypeFamilies #-} + +module Ide.Plugin.SemanticTokens.Types where + +import Control.DeepSeq (NFData (rnf), rwhnf) +import qualified Data.Array as A +import Data.Default (Default (def)) +import Development.IDE (Pretty (pretty), RuleResult) +import qualified Development.IDE.Core.Shake as Shake +import Development.IDE.GHC.Compat hiding (loc) +import Development.IDE.Graph.Classes (Hashable) +import GHC.Generics (Generic) +import Language.LSP.Protocol.Types +-- import template haskell +import Data.Text (Text) +import Ide.Plugin.Error (PluginError) +import Language.Haskell.TH.Syntax (Lift) + + +-- !!!! order of declarations matters deriving enum and ord +-- since token may come from different source and we want to keep the most specific one +-- and we might want to merge them. +data HsSemanticTokenType + = TVariable -- none function variable + | TFunction -- function + | TDataConstructor -- Data constructor + | TTypeVariable -- Type variable + | TClassMethod -- Class method + | TPatternSynonym -- Pattern synonym + | TTypeConstructor -- Type (Type constructor) + | TClass -- Type class + | TTypeSynonym -- Type synonym + | TTypeFamily -- type family + | TRecordField -- from match bind + | TOperator-- operator + | TModule -- module name + deriving (Eq, Ord, Show, Enum, Bounded, Generic, Lift) + +-- type SemanticTokensConfig = SemanticTokensConfig_ Identity +instance Default SemanticTokensConfig where + def = STC + { stFunction = SemanticTokenTypes_Function + , stVariable = SemanticTokenTypes_Variable + , stDataConstructor = SemanticTokenTypes_EnumMember + , stTypeVariable = SemanticTokenTypes_TypeParameter + , stClassMethod = SemanticTokenTypes_Method + -- pattern syn is like a limited version of macro of constructing a term + , stPatternSynonym = SemanticTokenTypes_Macro + -- normal data type is a tagged union type look like enum type + -- and a record is a product type like struct + -- but we don't distinguish them yet + , stTypeConstructor = SemanticTokenTypes_Enum + , stClass = SemanticTokenTypes_Class + , stTypeSynonym = SemanticTokenTypes_Type + , stTypeFamily = SemanticTokenTypes_Interface + , stRecordField = SemanticTokenTypes_Property + , stModule = SemanticTokenTypes_Namespace + , stOperator = SemanticTokenTypes_Operator + } +-- | SemanticTokensConfig_ is a configuration for the semantic tokens plugin. +-- it contains map between the hs semantic token type and default token type. +data SemanticTokensConfig = STC + { stFunction :: !SemanticTokenTypes + , stVariable :: !SemanticTokenTypes + , stDataConstructor :: !SemanticTokenTypes + , stTypeVariable :: !SemanticTokenTypes + , stClassMethod :: !SemanticTokenTypes + , stPatternSynonym :: !SemanticTokenTypes + , stTypeConstructor :: !SemanticTokenTypes + , stClass :: !SemanticTokenTypes + , stTypeSynonym :: !SemanticTokenTypes + , stTypeFamily :: !SemanticTokenTypes + , stRecordField :: !SemanticTokenTypes + , stModule :: !SemanticTokenTypes + , stOperator :: !SemanticTokenTypes + } deriving (Generic, Show) + + +instance Semigroup HsSemanticTokenType where + -- one in higher enum is more specific + a <> b = max a b + +data SemanticTokenOriginal tokenType = SemanticTokenOriginal + { _tokenType :: tokenType, + _loc :: Loc, + _name :: String + } + deriving (Eq, Ord) + +-- +instance (Show tokenType) => Show (SemanticTokenOriginal tokenType) where + show (SemanticTokenOriginal tk loc name) = show loc <> " " <> show tk <> " " <> show name + +data Loc = Loc + { _line :: UInt, + _startChar :: UInt, + _len :: UInt + } + deriving (Eq, Ord) + +instance Show Loc where + show (Loc line startChar len) = show line <> ":" <> show startChar <> "-" <> show (startChar + len) + +data GetSemanticTokens = GetSemanticTokens + deriving (Eq, Show, Generic) + +instance Hashable GetSemanticTokens + +instance NFData GetSemanticTokens + +type RangeSemanticTokenTypeList = [(Range, HsSemanticTokenType)] + +newtype RangeHsSemanticTokenTypes = RangeHsSemanticTokenTypes {rangeSemanticList :: RangeSemanticTokenTypeList} + +instance NFData RangeHsSemanticTokenTypes where + rnf :: RangeHsSemanticTokenTypes -> () + rnf (RangeHsSemanticTokenTypes a) = rwhnf a + +instance Show RangeHsSemanticTokenTypes where + show (RangeHsSemanticTokenTypes xs) = unlines $ map showRangeToken xs + +showRangeToken :: (Range, HsSemanticTokenType) -> String +showRangeToken (ran, tk) = showRange ran <> " " <> show tk +showRange :: Range -> String +showRange (Range (Position l1 c1) (Position l2 c2)) = show l1 <> ":" <> show c1 <> "-" <> show l2 <> ":" <> show c2 + +type instance RuleResult GetSemanticTokens = RangeHsSemanticTokenTypes + +data HieFunMaskKind kind where + HieFreshFun :: HieFunMaskKind Type + HieFromDiskFun :: A.Array TypeIndex Bool -> HieFunMaskKind TypeIndex + +data SemanticLog + = LogShake Shake.Log + | LogDependencyError PluginError + | LogNoAST FilePath + | LogConfig SemanticTokensConfig + | LogMsg String + | LogNoVF + | LogSemanticTokensDeltaMisMatch Text (Maybe Text) + +instance Pretty SemanticLog where + pretty theLog = case theLog of + LogShake shakeLog -> pretty shakeLog + LogNoAST path -> "no HieAst exist for file" <> pretty path + LogNoVF -> "no VirtualSourceFile exist for file" + LogConfig config -> "SemanticTokensConfig_: " <> pretty (show config) + LogMsg msg -> "SemanticLog Debug Message: " <> pretty msg + LogSemanticTokensDeltaMisMatch previousIdFromRequest previousIdFromCache + -> "SemanticTokensDeltaMisMatch: previousIdFromRequest: " <> pretty previousIdFromRequest + <> " previousIdFromCache: " <> pretty previousIdFromCache + LogDependencyError err -> "SemanticTokens' dependency error: " <> pretty err + + +type SemanticTokenId = Text diff --git a/plugins/hls-semantic-tokens-plugin/src/Ide/Plugin/SemanticTokens/Utils.hs b/plugins/hls-semantic-tokens-plugin/src/Ide/Plugin/SemanticTokens/Utils.hs new file mode 100644 index 0000000000..52cd56a21f --- /dev/null +++ b/plugins/hls-semantic-tokens-plugin/src/Ide/Plugin/SemanticTokens/Utils.hs @@ -0,0 +1,98 @@ +{-# LANGUAGE OverloadedStrings #-} +{-# LANGUAGE TypeFamilies #-} +{-# OPTIONS_GHC -Wno-orphans #-} + + +module Ide.Plugin.SemanticTokens.Utils where + +import Data.ByteString (ByteString) +import Data.ByteString.Char8 (unpack) +import qualified Data.Map.Strict as Map +import Development.IDE (Position (..), Range (..)) +import Development.IDE.GHC.Compat +import Prelude hiding (length, span) + +deriving instance Show DeclType +deriving instance Show BindType +deriving instance Show RecFieldContext + +instance Show ContextInfo where + show x = case x of + Use -> "Use" + MatchBind -> "MatchBind" + IEThing _ -> "IEThing IEType" -- imported + TyDecl -> "TyDecl" + ValBind bt _ sp -> "ValBind of " <> show bt <> show sp + PatternBind {} -> "PatternBind" + ClassTyDecl _ -> "ClassTyDecl" + Decl d _ -> "Decl of " <> show d + TyVarBind _ _ -> "TyVarBind" + RecField c _ -> "RecField of " <> show c + EvidenceVarBind {} -> "EvidenceVarBind" + EvidenceVarUse -> "EvidenceVarUse" + +showCompactRealSrc :: RealSrcSpan -> String +showCompactRealSrc x = show (srcSpanStartLine x) <> ":" <> show (srcSpanStartCol x) <> "-" <> show (srcSpanEndCol x) + +-- type RefMap a = M.Map Identifier [(Span, IdentifierDetails a)] +showRefMap :: RefMap a -> String +showRefMap m = unlines + [ + showIdentifier idn ++ ":" + ++ "\n" ++ unlines [showSDocUnsafe (ppr span) ++ "\n" ++ showIdentifierDetails v | (span, v) <- spans] + | (idn, spans) <- Map.toList m] + +showIdentifierDetails :: IdentifierDetails a -> String +showIdentifierDetails x = show $ identInfo x + +showIdentifier :: Identifier -> String +showIdentifier (Left x) = showSDocUnsafe (ppr x) +showIdentifier (Right x) = nameStableString x + +showLocatedNames :: [LIdP GhcRn] -> String +showLocatedNames xs = unlines + [ showSDocUnsafe (ppr locName) ++ " " ++ show (getLoc locName) + | locName <- xs] + +showClearName :: Name -> String +showClearName name = occNameString (occName name) <> ":" <> showSDocUnsafe (ppr name) <> ":" <> showNameType name + +showName :: Name -> String +showName name = showSDocUnsafe (ppr name) <> ":" <> showNameType name + +showNameType :: Name -> String +showNameType name + | isWiredInName name = "WiredInName" + | isSystemName name = "SystemName" + | isInternalName name = "InternalName" + | isExternalName name = "ExternalName" + | otherwise = "UnknownName" + +bytestringString :: ByteString -> String +bytestringString = map (toEnum . fromEnum) . unpack + +spanNamesString :: [(Span, Name)] -> String +spanNamesString xs = unlines + [ showSDocUnsafe (ppr span) ++ " " ++ showSDocUnsafe (ppr name) + | (span, name) <- xs] + +nameTypesString :: [(Name, Type)] -> String +nameTypesString xs = unlines + [ showSDocUnsafe (ppr span) ++ " " ++ showSDocUnsafe (ppr name) + | (span, name) <- xs] + + +showSpan :: RealSrcSpan -> String +showSpan x = show (srcSpanStartLine x) <> ":" <> show (srcSpanStartCol x) <> "-" <> show (srcSpanEndCol x) + + +-- rangeToCodePointRange +mkRange :: (Integral a1, Integral a2) => a1 -> a2 -> a2 -> Range +mkRange startLine startCol len = + Range (Position (fromIntegral startLine) (fromIntegral startCol)) (Position (fromIntegral startLine) (fromIntegral $ startCol + len)) + + +rangeShortStr :: Range -> String +rangeShortStr (Range (Position startLine startColumn) (Position endLine endColumn)) = + show startLine <> ":" <> show startColumn <> "-" <> show endLine <> ":" <> show endColumn + diff --git a/plugins/hls-semantic-tokens-plugin/test/SemanticTokensTest.hs b/plugins/hls-semantic-tokens-plugin/test/SemanticTokensTest.hs new file mode 100644 index 0000000000..a0d1648fb3 --- /dev/null +++ b/plugins/hls-semantic-tokens-plugin/test/SemanticTokensTest.hs @@ -0,0 +1,305 @@ +{-# LANGUAGE DataKinds #-} +{-# LANGUAGE DisambiguateRecordFields #-} +{-# LANGUAGE OverloadedStrings #-} + +import Control.Lens ((^.), (^?)) +import Data.Aeson (KeyValue (..), Object) +import qualified Data.Aeson.KeyMap as KV +import Data.Default +import Data.Functor (void) +import qualified Data.List as T +import Data.Map.Strict as Map hiding (map) +import Data.String (fromString) +import Data.Text (Text) +import qualified Data.Text as Text +import qualified Data.Text.Utf16.Rope.Mixed as Rope +import Data.Version (Version (..)) +import Development.IDE (Pretty) +import Development.IDE.Plugin.Test (WaitForIdeRuleResult (..)) +import Ide.Plugin.SemanticTokens +import Ide.Plugin.SemanticTokens.Mappings +import Ide.Plugin.SemanticTokens.Types +import Ide.Types +import qualified Language.LSP.Protocol.Lens as L +import Language.LSP.Protocol.Types +import qualified Language.LSP.Test as Test +import Language.LSP.VFS (VirtualFile (..)) +import System.FilePath +import System.Info (compilerVersion) +import Test.Hls +import qualified Test.Hls.FileSystem as FS +import Test.Hls.FileSystem (file, text) + +testDataDir :: FilePath +testDataDir = "plugins" "hls-semantic-tokens-plugin" "test" "testdata" testVersionDir + +mkFs :: [FS.FileTree] -> FS.VirtualFileTree +mkFs = FS.mkVirtualFileTree testDataDir + +semanticTokensPlugin :: Test.Hls.PluginTestDescriptor SemanticLog +semanticTokensPlugin = Test.Hls.mkPluginTestDescriptor enabledSemanticDescriptor "SemanticTokens" + where + enabledSemanticDescriptor recorder plId = + let semanticDescriptor = Ide.Plugin.SemanticTokens.descriptor recorder plId + in semanticDescriptor + { pluginConfigDescriptor = + (pluginConfigDescriptor semanticDescriptor) + { configInitialGenericConfig = + (configInitialGenericConfig (pluginConfigDescriptor semanticDescriptor)) + { plcGlobalOn = True + } + } + } + +-- if 9_10 and after we change the directory to the testdata/before_9_10 directory +-- if 9_10 and after we change the directory to the testdata/after_9_10 directory + +testVersionDir :: FilePath +testVersionDir + | compilerVersion >= Version [9, 10] [] = "after_9_10" + | otherwise = "before_9_10" + +goldenWithHaskellAndCapsOutPut :: (Pretty b) => Config -> PluginTestDescriptor b -> TestName -> FS.VirtualFileTree -> FilePath -> String -> (TextDocumentIdentifier -> Session String) -> TestTree +goldenWithHaskellAndCapsOutPut config plugin title tree path desc act = + goldenGitDiff title (FS.vftOriginalRoot tree path <.> desc) $ + fromString + <$> ( runSessionWithServerInTmpDir config plugin tree $ + do + doc <- openDoc (path <.> "hs") "haskell" + void waitForBuildQueue + act doc + ) + +goldenWithSemanticTokensWithDefaultConfig :: TestName -> FilePath -> TestTree +goldenWithSemanticTokensWithDefaultConfig title path = + goldenWithHaskellAndCapsOutPut + def + semanticTokensPlugin + title + (mkFs $ FS.directProject (path <.> "hs")) + path + "expected" + (docSemanticTokensString def) + +docSemanticTokensString :: SemanticTokensConfig -> TextDocumentIdentifier -> Session String +docSemanticTokensString cf doc = do + xs <- map (lspTokenHsToken cf) <$> docLspSemanticTokensString doc + return $ unlines . map show $ xs + +docLspSemanticTokensString :: (HasCallStack) => TextDocumentIdentifier -> Session [SemanticTokenOriginal Language.LSP.Protocol.Types.SemanticTokenTypes] +docLspSemanticTokensString doc = do + res <- Test.getSemanticTokens doc + textContent <- documentContents doc + let vfs = VirtualFile 0 0 (Rope.fromText textContent) + case res ^? Language.LSP.Protocol.Types._L of + Just tokens -> do + either (error . show) pure $ recoverLspSemanticTokens vfs tokens + _noTokens -> error "No tokens found" + +-- | Pass a param and return the response from `semanticTokensFull` +-- getSemanticTokensFullDelta :: TextDocumentIdentifier -> Session _ +getSemanticTokensFullDelta :: TextDocumentIdentifier -> Text -> Session (SemanticTokens |? (SemanticTokensDelta |? Null)) +getSemanticTokensFullDelta doc lastResultId = do + let params = SemanticTokensDeltaParams Nothing Nothing doc lastResultId + rsp <- request SMethod_TextDocumentSemanticTokensFullDelta params + case rsp ^. L.result of + Right x -> return x + _ -> error "No tokens found" + +semanticTokensClassTests :: TestTree +semanticTokensClassTests = + testGroup + "type class" + [ goldenWithSemanticTokensWithDefaultConfig "golden type class" "TClass", + goldenWithSemanticTokensWithDefaultConfig "imported class method InstanceClassMethodBind" "TInstanceClassMethodBind", + goldenWithSemanticTokensWithDefaultConfig "imported class method TInstanceClassMethodUse" "TInstanceClassMethodUse", + goldenWithSemanticTokensWithDefaultConfig "imported deriving" "TClassImportedDeriving" + ] + +semanticTokensValuePatternTests :: TestTree +semanticTokensValuePatternTests = + testGroup + "value and patterns " + [ goldenWithSemanticTokensWithDefaultConfig "value bind" "TValBind", + goldenWithSemanticTokensWithDefaultConfig "pattern match" "TPatternMatch", + goldenWithSemanticTokensWithDefaultConfig "pattern bind" "TPatternbind" + ] + +mkSemanticConfig :: Object -> Config +mkSemanticConfig setting = def {plugins = Map.insert "SemanticTokens" conf (plugins def)} + where + conf = def {plcConfig = setting} + +directFile :: FilePath -> Text -> [FS.FileTree] +directFile fp content = + [ FS.directCradle [Text.pack fp], + file fp (text content) + ] + +semanticTokensConfigTest :: TestTree +semanticTokensConfigTest = + testGroup + "semantic token config test" + [ testCase "function to variable" $ do + let content = Text.unlines ["module Hello where", "go _ = 1"] + let fs = mkFs $ directFile "Hello.hs" content + let funcVar = KV.fromList ["functionToken" .= var] + var :: String + var = "variable" + Test.Hls.runSessionWithTestConfig def + { testPluginDescriptor = semanticTokensPlugin + , testConfigSession = def + { ignoreConfigurationRequests = False + } + , testConfigCaps = fullLatestClientCaps + , testDirLocation = Right fs + , testLspConfig = mkSemanticConfig funcVar + } + $ const $ do + -- modifySemantic funcVar + void waitForBuildQueue + doc <- openDoc "Hello.hs" "haskell" + void waitForBuildQueue + result1 <- docLspSemanticTokensString doc + liftIO $ unlines (map show result1) @?= + T.unlines (["1:8-13 SemanticTokenTypes_Namespace \"Hello\"" | compilerVersion >= Version [9, 10] []] + ++ ["2:1-3 SemanticTokenTypes_Variable \"go\""]) + ] + + +semanticTokensFullDeltaTests :: TestTree +semanticTokensFullDeltaTests = + testGroup "semanticTokensFullDeltaTests" + [ testCase "null delta since unchanged" $ do + let file1 = "TModuleA.hs" + let expectDelta = InR (InL (SemanticTokensDelta (Just "1") [])) + Test.Hls.runSessionWithServerInTmpDir def semanticTokensPlugin (mkFs $ FS.directProjectMulti [file1]) $ do + doc1 <- openDoc file1 "haskell" + _ <- waitForAction "TypeCheck" doc1 + _ <- Test.getSemanticTokens doc1 + delta <- getSemanticTokensFullDelta doc1 "0" + liftIO $ delta @?= expectDelta, + testCase "add tokens" $ do + let file1 = "TModuleA.hs" + let expectDelta + | compilerVersion >= Version [9, 10] [] = InR (InL (SemanticTokensDelta (Just "1") [SemanticTokensEdit 25 0 (Just [2, 0, 3, 8, 0])])) + | otherwise = InR (InL (SemanticTokensDelta (Just "1") [SemanticTokensEdit 20 0 (Just [2, 0, 3, 8, 0])])) + -- r c l t m + -- where r = row, c = column, l = length, t = token, m = modifier + Test.Hls.runSessionWithServerInTmpDir def semanticTokensPlugin (mkFs $ FS.directProjectMulti [file1]) $ do + doc1 <- openDoc file1 "haskell" + _ <- waitForAction "TypeCheck" doc1 + _ <- Test.getSemanticTokens doc1 + -- open the file and append a line to it + let change = TextDocumentContentChangeEvent $ InL TextDocumentContentChangePartial + { _range = Range (Position 4 0) (Position 4 6) + , _rangeLength = Nothing + , _text = "foo = 1" + } + changeDoc doc1 [change] + _ <- waitForAction "TypeCheck" doc1 + delta <- getSemanticTokensFullDelta doc1 "0" + liftIO $ delta @?= expectDelta, + testCase "remove tokens" $ do + let file1 = "TModuleA.hs" + let expectDelta + | compilerVersion >= Version [9, 10] [] = InR (InL (SemanticTokensDelta (Just "1") [SemanticTokensEdit 5 20 (Just [])])) + | otherwise = InR (InL (SemanticTokensDelta (Just "1") [SemanticTokensEdit 0 20 (Just [])])) + -- delete all tokens + Test.Hls.runSessionWithServerInTmpDir def semanticTokensPlugin (mkFs $ FS.directProjectMulti [file1]) $ do + doc1 <- openDoc file1 "haskell" + _ <- waitForAction "TypeCheck" doc1 + _ <- Test.getSemanticTokens doc1 + -- open the file and append a line to it + let change = TextDocumentContentChangeEvent $ InL TextDocumentContentChangePartial + { _range = Range (Position 2 0) (Position 2 28) + , _rangeLength = Nothing + , _text = Text.replicate 28 " " + } + changeDoc doc1 [change] + _ <- waitForAction "TypeCheck" doc1 + delta <- getSemanticTokensFullDelta doc1 "0" + liftIO $ delta @?= expectDelta + ] + +semanticTokensTests :: TestTree +semanticTokensTests = + testGroup "other semantic Token test" + [ testCase "module import test" $ do + let file1 = "TModuleA.hs" + let file2 = "TModuleB.hs" + Test.Hls.runSessionWithServerInTmpDir def semanticTokensPlugin (mkFs $ FS.directProjectMulti [file1, file2]) $ do + doc1 <- openDoc file1 "haskell" + doc2 <- openDoc file2 "haskell" + check1 <- waitForAction "TypeCheck" doc1 + check2 <- waitForAction "TypeCheck" doc2 + case check1 of + Right (WaitForIdeRuleResult _) -> return () + Left _ -> error "TypeCheck1 failed" + case check2 of + Right (WaitForIdeRuleResult _) -> return () + Left _ -> error "TypeCheck2 failed" + + result <- docSemanticTokensString def doc2 + let expect = + unlines + ( + -- > 9.10 have module name in the token + (["1:8-16 TModule \"TModuleB\"" | compilerVersion >= Version [9, 10] []]) + ++ + [ + "3:8-16 TModule \"TModuleA\"", + "4:18-26 TModule \"TModuleA\"", + "6:1-3 TVariable \"go\"", + "6:6-10 TDataConstructor \"Game\"", + "8:1-5 TVariable \"a\\66560bb\"", + "8:8-17 TModule \"TModuleA.\"", + "8:17-20 TRecordField \"a\\66560b\"", + "8:21-23 TVariable \"go\"" + ]) + liftIO $ result @?= expect, + goldenWithSemanticTokensWithDefaultConfig "mixed constancy test result generated from one ghc version" "T1", + goldenWithSemanticTokensWithDefaultConfig "pattern bind" "TPatternSynonym", + goldenWithSemanticTokensWithDefaultConfig "type family" "TTypefamily", + goldenWithSemanticTokensWithDefaultConfig "TUnicodeSyntax" "TUnicodeSyntax", + goldenWithSemanticTokensWithDefaultConfig "TQualifiedName" "TQualifiedName", + goldenWithSemanticTokensWithDefaultConfig "TDoc" "TDoc" + ] + +semanticTokensDataTypeTests :: TestTree +semanticTokensDataTypeTests = + testGroup + "get semantic Tokens" + [ goldenWithSemanticTokensWithDefaultConfig "simple datatype" "TDataType", + goldenWithSemanticTokensWithDefaultConfig "record" "TRecord", + goldenWithSemanticTokensWithDefaultConfig "record With DuplicateRecordFields" "TRecordDuplicateRecordFields", + goldenWithSemanticTokensWithDefaultConfig "datatype import" "TDatatypeImported", + goldenWithSemanticTokensWithDefaultConfig "datatype family" "TDataFamily", + goldenWithSemanticTokensWithDefaultConfig "GADT" "TGADT" + ] + +semanticTokensFunctionTests :: TestTree +semanticTokensFunctionTests = + testGroup + "get semantic of functions" + [ goldenWithSemanticTokensWithDefaultConfig "functions" "TFunction", + goldenWithSemanticTokensWithDefaultConfig "local functions" "TFunctionLocal", + goldenWithSemanticTokensWithDefaultConfig "functions under type synonym" "TFunctionUnderTypeSynonym", + goldenWithSemanticTokensWithDefaultConfig "function in let binding" "TFunctionLet", + goldenWithSemanticTokensWithDefaultConfig "negative case non-function with constraint" "TNoneFunctionWithConstraint", + goldenWithSemanticTokensWithDefaultConfig "TOperator" "TOperator" + ] + +main :: IO () +main = + defaultTestRunner $ + testGroup + "Semantic tokens" + [ semanticTokensTests, + semanticTokensClassTests, + semanticTokensDataTypeTests, + semanticTokensValuePatternTests, + semanticTokensFunctionTests, + semanticTokensConfigTest, + semanticTokensFullDeltaTests + ] diff --git a/plugins/hls-semantic-tokens-plugin/test/testdata/after_9_10/T1.expected b/plugins/hls-semantic-tokens-plugin/test/testdata/after_9_10/T1.expected new file mode 100644 index 0000000000..eff5c79768 --- /dev/null +++ b/plugins/hls-semantic-tokens-plugin/test/testdata/after_9_10/T1.expected @@ -0,0 +1,82 @@ +4:8-12 TModule "Main" +9:6-9 TTypeConstructor "Foo" +9:12-15 TDataConstructor "Foo" +9:18-21 TRecordField "foo" +9:25-28 TTypeConstructor "Int" +11:7-10 TClass "Boo" +11:11-12 TTypeVariable "a" +12:3-6 TClassMethod "boo" +12:10-11 TTypeVariable "a" +12:15-16 TTypeVariable "a" +14:10-13 TClass "Boo" +14:14-17 TTypeConstructor "Int" +15:5-8 TClassMethod "boo" +15:9-10 TVariable "x" +15:13-14 TVariable "x" +15:15-16 TOperator "+" +17:6-8 TTypeConstructor "Dd" +17:11-13 TDataConstructor "Dd" +17:14-17 TTypeConstructor "Int" +19:9-12 TPatternSynonym "One" +19:15-18 TDataConstructor "Foo" +21:1-4 TVariable "ggg" +21:7-10 TPatternSynonym "One" +23:6-9 TTypeConstructor "Doo" +23:12-15 TDataConstructor "Doo" +23:16-24 TModule "Prelude." +23:24-27 TTypeConstructor "Int" +24:6-10 TTypeSynonym "Bar1" +24:13-16 TTypeConstructor "Int" +25:6-10 TTypeSynonym "Bar2" +25:13-16 TTypeConstructor "Doo" +27:1-3 TFunction "bb" +27:8-11 TClass "Boo" +27:12-13 TTypeVariable "a" +27:18-19 TTypeVariable "a" +27:23-24 TTypeVariable "a" +28:1-3 TFunction "bb" +28:4-5 TVariable "x" +28:9-12 TClassMethod "boo" +28:13-14 TVariable "x" +29:1-3 TFunction "aa" +29:7-11 TTypeVariable "cool" +29:15-18 TTypeConstructor "Int" +29:22-26 TTypeVariable "cool" +30:1-3 TFunction "aa" +30:4-5 TVariable "x" +30:9-10 TVariable "c" +30:14-16 TFunction "aa" +30:17-18 TVariable "x" +30:19-20 TVariable "c" +31:12-14 TVariable "xx" +31:16-18 TVariable "yy" +32:11-13 TVariable "dd" +34:2-4 TVariable "zz" +34:6-8 TVariable "kk" +35:1-3 TFunction "cc" +35:7-10 TTypeConstructor "Foo" +35:15-18 TTypeConstructor "Int" +35:20-23 TTypeConstructor "Int" +35:28-31 TTypeConstructor "Int" +36:1-3 TFunction "cc" +36:4-5 TVariable "f" +36:7-9 TVariable "gg" +36:11-13 TVariable "vv" +37:10-12 TVariable "gg" +38:14-17 TRecordField "foo" +38:18-19 TOperator "$" +38:20-21 TVariable "f" +38:24-27 TRecordField "foo" +39:14-17 TRecordField "foo" +39:18-19 TOperator "$" +39:20-21 TVariable "f" +39:24-27 TRecordField "foo" +41:1-3 TFunction "go" +41:6-9 TRecordField "foo" +42:1-4 TFunction "add" +42:8-16 TModule "Prelude." +42:16-17 TOperator "+" +47:1-5 TVariable "main" +47:9-11 TTypeConstructor "IO" +48:1-5 TVariable "main" +48:8-16 TFunction "putStrLn" diff --git a/plugins/hls-semantic-tokens-plugin/test/testdata/after_9_10/T1.hs b/plugins/hls-semantic-tokens-plugin/test/testdata/after_9_10/T1.hs new file mode 100644 index 0000000000..07b0476c1e --- /dev/null +++ b/plugins/hls-semantic-tokens-plugin/test/testdata/after_9_10/T1.hs @@ -0,0 +1,48 @@ +-- patter syn +{-# LANGUAGE PatternSynonyms #-} + +module Main where + +-- import Data.Set (Set, insert) + + +data Foo = Foo { foo :: Int } + +class Boo a where + boo :: a -> a + +instance Boo Int where + boo x = x + 1 + +data Dd = Dd Int + +pattern One = Foo 1 + +ggg = One + +data Doo = Doo Prelude.Int +type Bar1 = Int +type Bar2 = Doo + +bb :: (Boo a) => a -> a +bb x = boo x +aa :: cool -> Int -> cool +aa x = \c -> aa x c + where (xx, yy) = (1, 2) + dd = 1 + +(zz, kk) = (1, 2) +cc :: Foo -> (Int, Int) -> Int +cc f (gg, vv)= + case gg of + 1 -> foo $ f { foo = 1 } + 2 -> foo $ f { foo = 1 } + +go = foo +add = (Prelude.+) + +-- sub :: Int -> Int -> Int +-- sub x y = add x y + +main :: IO () +main = putStrLn "Hello, Haskell!" diff --git a/plugins/hls-semantic-tokens-plugin/test/testdata/after_9_10/TClass.expected b/plugins/hls-semantic-tokens-plugin/test/testdata/after_9_10/TClass.expected new file mode 100644 index 0000000000..f7bb4cd513 --- /dev/null +++ b/plugins/hls-semantic-tokens-plugin/test/testdata/after_9_10/TClass.expected @@ -0,0 +1,6 @@ +1:8-14 TModule "TClass" +4:7-10 TClass "Foo" +4:11-12 TTypeVariable "a" +5:3-6 TClassMethod "foo" +5:10-11 TTypeVariable "a" +5:15-18 TTypeConstructor "Int" diff --git a/plugins/hls-semantic-tokens-plugin/test/testdata/after_9_10/TClass.hs b/plugins/hls-semantic-tokens-plugin/test/testdata/after_9_10/TClass.hs new file mode 100644 index 0000000000..692754ec71 --- /dev/null +++ b/plugins/hls-semantic-tokens-plugin/test/testdata/after_9_10/TClass.hs @@ -0,0 +1,6 @@ +module TClass where + + +class Foo a where + foo :: a -> Int + diff --git a/plugins/hls-semantic-tokens-plugin/test/testdata/after_9_10/TClassImportedDeriving.expected b/plugins/hls-semantic-tokens-plugin/test/testdata/after_9_10/TClassImportedDeriving.expected new file mode 100644 index 0000000000..9ca97d9082 --- /dev/null +++ b/plugins/hls-semantic-tokens-plugin/test/testdata/after_9_10/TClassImportedDeriving.expected @@ -0,0 +1,4 @@ +2:8-30 TModule "TClassImportedDeriving" +4:6-9 TTypeConstructor "Foo" +4:12-15 TDataConstructor "Foo" +4:26-30 TClass "Show" diff --git a/plugins/hls-semantic-tokens-plugin/test/testdata/after_9_10/TClassImportedDeriving.hs b/plugins/hls-semantic-tokens-plugin/test/testdata/after_9_10/TClassImportedDeriving.hs new file mode 100644 index 0000000000..8afd8afbd9 --- /dev/null +++ b/plugins/hls-semantic-tokens-plugin/test/testdata/after_9_10/TClassImportedDeriving.hs @@ -0,0 +1,10 @@ +{-# LANGUAGE StandaloneDeriving #-} +module TClassImportedDeriving where +-- deriving method source span of Show occurrence +data Foo = Foo deriving (Show) + +-- standalone deriving method not in the same position +-- deriving instance Eq Foo + +-- a :: Foo -> Foo -> Bool +-- a = (==) diff --git a/plugins/hls-semantic-tokens-plugin/test/testdata/after_9_10/TDataFamily.expected b/plugins/hls-semantic-tokens-plugin/test/testdata/after_9_10/TDataFamily.expected new file mode 100644 index 0000000000..b3b477e541 --- /dev/null +++ b/plugins/hls-semantic-tokens-plugin/test/testdata/after_9_10/TDataFamily.expected @@ -0,0 +1,13 @@ +2:8-19 TModule "TDatafamily" +5:13-18 TTypeFamily "XList" +5:19-20 TTypeVariable "a" +8:15-20 TTypeFamily "XList" +8:21-25 TTypeConstructor "Char" +8:28-33 TDataConstructor "XCons" +8:35-39 TTypeConstructor "Char" +8:42-47 TTypeFamily "XList" +8:48-52 TTypeConstructor "Char" +8:56-60 TDataConstructor "XNil" +11:15-20 TTypeFamily "XList" +11:26-35 TDataConstructor "XListUnit" +11:37-40 TTypeConstructor "Int" diff --git a/plugins/hls-semantic-tokens-plugin/test/testdata/after_9_10/TDataFamily.hs b/plugins/hls-semantic-tokens-plugin/test/testdata/after_9_10/TDataFamily.hs new file mode 100644 index 0000000000..b9047a72d2 --- /dev/null +++ b/plugins/hls-semantic-tokens-plugin/test/testdata/after_9_10/TDataFamily.hs @@ -0,0 +1,11 @@ +{-# LANGUAGE TypeFamilies #-} +module TDatafamily where + +-- Declare a list-like data family +data family XList a + +-- Declare a list-like instance for Char +data instance XList Char = XCons !Char !(XList Char) | XNil + +-- Declare a number-like instance for () +data instance XList () = XListUnit !Int diff --git a/plugins/hls-semantic-tokens-plugin/test/testdata/after_9_10/TDataType.expected b/plugins/hls-semantic-tokens-plugin/test/testdata/after_9_10/TDataType.expected new file mode 100644 index 0000000000..7f03f4ed54 --- /dev/null +++ b/plugins/hls-semantic-tokens-plugin/test/testdata/after_9_10/TDataType.expected @@ -0,0 +1,5 @@ +1:8-17 TModule "TDataType" +3:6-9 TTypeConstructor "Foo" +3:12-15 TDataConstructor "Foo" +3:16-19 TTypeConstructor "Int" +3:30-32 TClass "Eq" diff --git a/plugins/hls-semantic-tokens-plugin/test/testdata/after_9_10/TDataType.hs b/plugins/hls-semantic-tokens-plugin/test/testdata/after_9_10/TDataType.hs new file mode 100644 index 0000000000..894065e391 --- /dev/null +++ b/plugins/hls-semantic-tokens-plugin/test/testdata/after_9_10/TDataType.hs @@ -0,0 +1,3 @@ +module TDataType where + +data Foo = Foo Int deriving (Eq) diff --git a/plugins/hls-semantic-tokens-plugin/test/testdata/after_9_10/TDatatypeImported.expected b/plugins/hls-semantic-tokens-plugin/test/testdata/after_9_10/TDatatypeImported.expected new file mode 100644 index 0000000000..78ebf2bc22 --- /dev/null +++ b/plugins/hls-semantic-tokens-plugin/test/testdata/after_9_10/TDatatypeImported.expected @@ -0,0 +1,6 @@ +1:8-25 TModule "TDatatypeImported" +3:8-17 TModule "System.IO" +5:1-3 TVariable "go" +5:7-9 TTypeConstructor "IO" +6:1-3 TVariable "go" +6:6-11 TFunction "print" diff --git a/plugins/hls-semantic-tokens-plugin/test/testdata/after_9_10/TDatatypeImported.hs b/plugins/hls-semantic-tokens-plugin/test/testdata/after_9_10/TDatatypeImported.hs new file mode 100644 index 0000000000..f6ac8996d9 --- /dev/null +++ b/plugins/hls-semantic-tokens-plugin/test/testdata/after_9_10/TDatatypeImported.hs @@ -0,0 +1,6 @@ +module TDatatypeImported where + +import System.IO + +go :: IO () +go = print 1 diff --git a/plugins/hls-semantic-tokens-plugin/test/testdata/after_9_10/TDoc.expected b/plugins/hls-semantic-tokens-plugin/test/testdata/after_9_10/TDoc.expected new file mode 100644 index 0000000000..30b1cdb345 --- /dev/null +++ b/plugins/hls-semantic-tokens-plugin/test/testdata/after_9_10/TDoc.expected @@ -0,0 +1,6 @@ +1:8-12 TModule "TDoc" +4:5-10 TVariable "hello" +5:1-6 TVariable "hello" +5:10-13 TTypeConstructor "Int" +6:1-6 TVariable "hello" +6:9-15 TClassMethod "length" diff --git a/plugins/hls-semantic-tokens-plugin/test/testdata/after_9_10/TDoc.hs b/plugins/hls-semantic-tokens-plugin/test/testdata/after_9_10/TDoc.hs new file mode 100644 index 0000000000..dc5801b0e6 --- /dev/null +++ b/plugins/hls-semantic-tokens-plugin/test/testdata/after_9_10/TDoc.hs @@ -0,0 +1,9 @@ +module TDoc where + +-- | +-- `hello` +hello :: Int +hello = length "Hello, Haskell!" + + + diff --git a/plugins/hls-semantic-tokens-plugin/test/testdata/after_9_10/TFunction.expected b/plugins/hls-semantic-tokens-plugin/test/testdata/after_9_10/TFunction.expected new file mode 100644 index 0000000000..2b715e0a40 --- /dev/null +++ b/plugins/hls-semantic-tokens-plugin/test/testdata/after_9_10/TFunction.expected @@ -0,0 +1,12 @@ +1:8-17 TModule "TFunction" +3:1-2 TFunction "f" +3:13-14 TTypeVariable "a" +3:16-17 TTypeVariable "a" +3:21-22 TTypeVariable "a" +4:1-2 TFunction "f" +4:3-4 TVariable "x" +4:7-8 TVariable "x" +6:1-2 TVariable "x" +6:6-7 TTypeVariable "a" +7:1-2 TVariable "x" +7:5-14 TVariable "undefined" diff --git a/plugins/hls-semantic-tokens-plugin/test/testdata/after_9_10/TFunction.hs b/plugins/hls-semantic-tokens-plugin/test/testdata/after_9_10/TFunction.hs new file mode 100644 index 0000000000..4efe5cecc4 --- /dev/null +++ b/plugins/hls-semantic-tokens-plugin/test/testdata/after_9_10/TFunction.hs @@ -0,0 +1,7 @@ +module TFunction where + +f :: forall a. a -> a +f x = x + +x :: a +x = undefined diff --git a/plugins/hls-semantic-tokens-plugin/test/testdata/after_9_10/TFunctionLet.expected b/plugins/hls-semantic-tokens-plugin/test/testdata/after_9_10/TFunctionLet.expected new file mode 100644 index 0000000000..f51938a712 --- /dev/null +++ b/plugins/hls-semantic-tokens-plugin/test/testdata/after_9_10/TFunctionLet.expected @@ -0,0 +1,6 @@ +1:8-20 TModule "TFunctionLet" +3:1-2 TVariable "y" +3:6-9 TTypeConstructor "Int" +4:1-2 TVariable "y" +4:9-10 TFunction "f" +4:11-12 TVariable "x" diff --git a/plugins/hls-semantic-tokens-plugin/test/testdata/after_9_10/TFunctionLet.hs b/plugins/hls-semantic-tokens-plugin/test/testdata/after_9_10/TFunctionLet.hs new file mode 100644 index 0000000000..96854c34ad --- /dev/null +++ b/plugins/hls-semantic-tokens-plugin/test/testdata/after_9_10/TFunctionLet.hs @@ -0,0 +1,4 @@ +module TFunctionLet where + +y :: Int +y = let f x = 1 in 1 diff --git a/plugins/hls-semantic-tokens-plugin/test/testdata/after_9_10/TFunctionLocal.expected b/plugins/hls-semantic-tokens-plugin/test/testdata/after_9_10/TFunctionLocal.expected new file mode 100644 index 0000000000..34e040d641 --- /dev/null +++ b/plugins/hls-semantic-tokens-plugin/test/testdata/after_9_10/TFunctionLocal.expected @@ -0,0 +1,8 @@ +1:8-22 TModule "TFunctionLocal" +3:1-2 TFunction "f" +3:6-9 TTypeConstructor "Int" +3:13-16 TTypeConstructor "Int" +4:1-2 TFunction "f" +4:7-8 TFunction "g" +6:5-6 TFunction "g" +6:7-8 TVariable "x" diff --git a/plugins/hls-semantic-tokens-plugin/test/testdata/after_9_10/TFunctionLocal.hs b/plugins/hls-semantic-tokens-plugin/test/testdata/after_9_10/TFunctionLocal.hs new file mode 100644 index 0000000000..fed144b00c --- /dev/null +++ b/plugins/hls-semantic-tokens-plugin/test/testdata/after_9_10/TFunctionLocal.hs @@ -0,0 +1,8 @@ +module TFunctionLocal where + +f :: Int -> Int +f 1 = g 1 + where + g x = 1 + + diff --git a/plugins/hls-semantic-tokens-plugin/test/testdata/after_9_10/TFunctionUnderTypeSynonym.expected b/plugins/hls-semantic-tokens-plugin/test/testdata/after_9_10/TFunctionUnderTypeSynonym.expected new file mode 100644 index 0000000000..0779402a83 --- /dev/null +++ b/plugins/hls-semantic-tokens-plugin/test/testdata/after_9_10/TFunctionUnderTypeSynonym.expected @@ -0,0 +1,18 @@ +1:8-33 TModule "TFunctionUnderTypeSynonym" +3:6-8 TTypeSynonym "T1" +3:11-14 TTypeConstructor "Int" +3:18-21 TTypeConstructor "Int" +4:6-8 TTypeSynonym "T2" +4:18-19 TTypeVariable "a" +4:21-22 TTypeVariable "a" +4:26-27 TTypeVariable "a" +5:1-3 TFunction "f1" +5:7-9 TTypeSynonym "T1" +6:1-3 TFunction "f1" +6:4-5 TVariable "x" +6:8-9 TVariable "x" +7:1-3 TFunction "f2" +7:7-9 TTypeSynonym "T2" +8:1-3 TFunction "f2" +8:4-5 TVariable "x" +8:8-9 TVariable "x" diff --git a/plugins/hls-semantic-tokens-plugin/test/testdata/after_9_10/TFunctionUnderTypeSynonym.hs b/plugins/hls-semantic-tokens-plugin/test/testdata/after_9_10/TFunctionUnderTypeSynonym.hs new file mode 100644 index 0000000000..6485232394 --- /dev/null +++ b/plugins/hls-semantic-tokens-plugin/test/testdata/after_9_10/TFunctionUnderTypeSynonym.hs @@ -0,0 +1,9 @@ +module TFunctionUnderTypeSynonym where + +type T1 = Int -> Int +type T2 = forall a. a -> a +f1 :: T1 +f1 x = x +f2 :: T2 +f2 x = x + diff --git a/plugins/hls-semantic-tokens-plugin/test/testdata/after_9_10/TGADT.expected b/plugins/hls-semantic-tokens-plugin/test/testdata/after_9_10/TGADT.expected new file mode 100644 index 0000000000..3f07298543 --- /dev/null +++ b/plugins/hls-semantic-tokens-plugin/test/testdata/after_9_10/TGADT.expected @@ -0,0 +1,14 @@ +3:8-13 TModule "TGADT" +5:6-9 TTypeConstructor "Lam" +6:3-7 TDataConstructor "Lift" +6:11-12 TTypeVariable "a" +6:36-39 TTypeConstructor "Lam" +6:40-41 TTypeVariable "a" +7:3-6 TDataConstructor "Lam" +7:12-15 TTypeConstructor "Lam" +7:16-17 TTypeVariable "a" +7:21-24 TTypeConstructor "Lam" +7:25-26 TTypeVariable "b" +7:36-39 TTypeConstructor "Lam" +7:41-42 TTypeVariable "a" +7:46-47 TTypeVariable "b" diff --git a/plugins/hls-semantic-tokens-plugin/test/testdata/after_9_10/TGADT.hs b/plugins/hls-semantic-tokens-plugin/test/testdata/after_9_10/TGADT.hs new file mode 100644 index 0000000000..e0cccf8bed --- /dev/null +++ b/plugins/hls-semantic-tokens-plugin/test/testdata/after_9_10/TGADT.hs @@ -0,0 +1,7 @@ +{-# LANGUAGE TypeFamilies #-} +{-# LANGUAGE GADTs #-} +module TGADT where + +data Lam :: * -> * where + Lift :: a -> Lam a -- ^ lifted value + Lam :: (Lam a -> Lam b) -> Lam (a -> b) -- ^ lambda abstraction diff --git a/plugins/hls-semantic-tokens-plugin/test/testdata/after_9_10/TInstanceClassMethodBind.expected b/plugins/hls-semantic-tokens-plugin/test/testdata/after_9_10/TInstanceClassMethodBind.expected new file mode 100644 index 0000000000..b93e340ac3 --- /dev/null +++ b/plugins/hls-semantic-tokens-plugin/test/testdata/after_9_10/TInstanceClassMethodBind.expected @@ -0,0 +1,8 @@ +1:8-32 TModule "TInstanceClassMethodBind" +4:6-9 TTypeConstructor "Foo" +4:12-15 TDataConstructor "Foo" +4:16-19 TTypeConstructor "Int" +5:10-14 TClass "Show" +5:15-18 TTypeConstructor "Foo" +6:5-9 TClassMethod "show" +6:12-21 TVariable "undefined" diff --git a/plugins/hls-semantic-tokens-plugin/test/testdata/after_9_10/TInstanceClassMethodBind.hs b/plugins/hls-semantic-tokens-plugin/test/testdata/after_9_10/TInstanceClassMethodBind.hs new file mode 100644 index 0000000000..33976a48c1 --- /dev/null +++ b/plugins/hls-semantic-tokens-plugin/test/testdata/after_9_10/TInstanceClassMethodBind.hs @@ -0,0 +1,6 @@ +module TInstanceClassMethodBind where + + +data Foo = Foo Int +instance Show Foo where + show = undefined diff --git a/plugins/hls-semantic-tokens-plugin/test/testdata/after_9_10/TInstanceClassMethodUse.expected b/plugins/hls-semantic-tokens-plugin/test/testdata/after_9_10/TInstanceClassMethodUse.expected new file mode 100644 index 0000000000..3fc60caab3 --- /dev/null +++ b/plugins/hls-semantic-tokens-plugin/test/testdata/after_9_10/TInstanceClassMethodUse.expected @@ -0,0 +1,3 @@ +1:8-31 TModule "TInstanceClassMethodUse" +4:1-3 TFunction "go" +4:8-12 TClassMethod "show" diff --git a/plugins/hls-semantic-tokens-plugin/test/testdata/after_9_10/TInstanceClassMethodUse.hs b/plugins/hls-semantic-tokens-plugin/test/testdata/after_9_10/TInstanceClassMethodUse.hs new file mode 100644 index 0000000000..689d1643d4 --- /dev/null +++ b/plugins/hls-semantic-tokens-plugin/test/testdata/after_9_10/TInstanceClassMethodUse.hs @@ -0,0 +1,5 @@ +module TInstanceClassMethodUse where + + +go = show + diff --git a/plugins/hls-semantic-tokens-plugin/test/testdata/after_9_10/TModuleA.hs b/plugins/hls-semantic-tokens-plugin/test/testdata/after_9_10/TModuleA.hs new file mode 100644 index 0000000000..d76f64fc1f --- /dev/null +++ b/plugins/hls-semantic-tokens-plugin/test/testdata/after_9_10/TModuleA.hs @@ -0,0 +1,5 @@ +module TModuleA where + +data Game = Game {a𐐀b :: Int} + + diff --git a/plugins/hls-semantic-tokens-plugin/test/testdata/after_9_10/TModuleB.hs b/plugins/hls-semantic-tokens-plugin/test/testdata/after_9_10/TModuleB.hs new file mode 100644 index 0000000000..d2bfe4b7fa --- /dev/null +++ b/plugins/hls-semantic-tokens-plugin/test/testdata/after_9_10/TModuleB.hs @@ -0,0 +1,8 @@ +module TModuleB where + +import TModuleA +import qualified TModuleA + +go = Game 1 + +a𐐀bb = TModuleA.a𐐀b go diff --git a/plugins/hls-semantic-tokens-plugin/test/testdata/after_9_10/TNoneFunctionWithConstraint.expected b/plugins/hls-semantic-tokens-plugin/test/testdata/after_9_10/TNoneFunctionWithConstraint.expected new file mode 100644 index 0000000000..a004142952 --- /dev/null +++ b/plugins/hls-semantic-tokens-plugin/test/testdata/after_9_10/TNoneFunctionWithConstraint.expected @@ -0,0 +1,7 @@ +1:8-35 TModule "TNoneFunctionWithConstraint" +3:1-2 TVariable "x" +3:7-9 TClass "Eq" +3:10-11 TTypeVariable "a" +3:16-17 TTypeVariable "a" +4:1-2 TVariable "x" +4:5-14 TVariable "undefined" diff --git a/plugins/hls-semantic-tokens-plugin/test/testdata/after_9_10/TNoneFunctionWithConstraint.hs b/plugins/hls-semantic-tokens-plugin/test/testdata/after_9_10/TNoneFunctionWithConstraint.hs new file mode 100644 index 0000000000..9a7119dbdb --- /dev/null +++ b/plugins/hls-semantic-tokens-plugin/test/testdata/after_9_10/TNoneFunctionWithConstraint.hs @@ -0,0 +1,5 @@ +module TNoneFunctionWithConstraint where + +x :: (Eq a) => a +x = undefined + diff --git a/plugins/hls-semantic-tokens-plugin/test/testdata/after_9_10/TOperator.expected b/plugins/hls-semantic-tokens-plugin/test/testdata/after_9_10/TOperator.expected new file mode 100644 index 0000000000..c8b2ecb29d --- /dev/null +++ b/plugins/hls-semantic-tokens-plugin/test/testdata/after_9_10/TOperator.expected @@ -0,0 +1,34 @@ +1:8-17 TModule "TOperator" +4:1-3 TFunction "go" +4:4-5 TFunction "f" +4:6-7 TVariable "x" +4:10-11 TFunction "f" +4:11-12 TOperator "$" +4:12-13 TVariable "x" +6:2-6 TOperator "$$$$" +7:1-2 TVariable "x" +7:7-11 TOperator "$$$$" +8:6-7 TTypeVariable "a" +8:8-11 TOperator ":+:" +8:12-13 TTypeVariable "b" +8:16-19 TDataConstructor "Add" +8:20-21 TTypeVariable "a" +8:22-23 TTypeVariable "b" +9:7-10 TOperator ":-:" +9:12-13 TTypeVariable "a" +9:14-15 TTypeVariable "b" +9:19-20 TTypeVariable "a" +9:22-23 TTypeVariable "b" +11:1-4 TFunction "add" +11:8-11 TTypeConstructor "Int" +11:12-15 TOperator ":+:" +11:16-19 TTypeConstructor "Int" +11:23-26 TTypeConstructor "Int" +11:27-30 TOperator ":-:" +11:31-34 TTypeConstructor "Int" +13:1-4 TFunction "add" +13:6-9 TDataConstructor "Add" +13:10-11 TVariable "x" +13:12-13 TVariable "y" +13:18-19 TVariable "x" +13:21-22 TVariable "y" diff --git a/plugins/hls-semantic-tokens-plugin/test/testdata/after_9_10/TOperator.hs b/plugins/hls-semantic-tokens-plugin/test/testdata/after_9_10/TOperator.hs new file mode 100644 index 0000000000..e2f06c92fa --- /dev/null +++ b/plugins/hls-semantic-tokens-plugin/test/testdata/after_9_10/TOperator.hs @@ -0,0 +1,13 @@ +module TOperator where + +-- imported operator +go f x = f$x +-- operator defined in local module +($$$$) = b +x = 1 $$$$ 2 +data a :+: b = Add a b +type (:-:) a b = (a, b) +-- type take precedence over operator +add :: Int :+: Int -> Int :-: Int +-- class method take precedence over operator +add (Add x y) = (x, y) diff --git a/plugins/hls-semantic-tokens-plugin/test/testdata/after_9_10/TPatternMatch.expected b/plugins/hls-semantic-tokens-plugin/test/testdata/after_9_10/TPatternMatch.expected new file mode 100644 index 0000000000..b17e52e27f --- /dev/null +++ b/plugins/hls-semantic-tokens-plugin/test/testdata/after_9_10/TPatternMatch.expected @@ -0,0 +1,3 @@ +1:8-21 TModule "TPatternMatch" +4:1-2 TFunction "g" +4:4-11 TDataConstructor "Nothing" diff --git a/plugins/hls-semantic-tokens-plugin/test/testdata/after_9_10/TPatternMatch.hs b/plugins/hls-semantic-tokens-plugin/test/testdata/after_9_10/TPatternMatch.hs new file mode 100644 index 0000000000..95e97c1abb --- /dev/null +++ b/plugins/hls-semantic-tokens-plugin/test/testdata/after_9_10/TPatternMatch.hs @@ -0,0 +1,6 @@ +module TPatternMatch where + + +g (Nothing, _) = 1 + + diff --git a/plugins/hls-semantic-tokens-plugin/test/testdata/after_9_10/TPatternSynonym.expected b/plugins/hls-semantic-tokens-plugin/test/testdata/after_9_10/TPatternSynonym.expected new file mode 100644 index 0000000000..b9cff7321a --- /dev/null +++ b/plugins/hls-semantic-tokens-plugin/test/testdata/after_9_10/TPatternSynonym.expected @@ -0,0 +1,2 @@ +2:8-23 TModule "TPatternSynonym" +5:9-12 TPatternSynonym "Foo" diff --git a/plugins/hls-semantic-tokens-plugin/test/testdata/after_9_10/TPatternSynonym.hs b/plugins/hls-semantic-tokens-plugin/test/testdata/after_9_10/TPatternSynonym.hs new file mode 100644 index 0000000000..adff673ce8 --- /dev/null +++ b/plugins/hls-semantic-tokens-plugin/test/testdata/after_9_10/TPatternSynonym.hs @@ -0,0 +1,7 @@ +{-# LANGUAGE PatternSynonyms #-} +module TPatternSynonym where + + +pattern Foo = 1 + + diff --git a/plugins/hls-semantic-tokens-plugin/test/testdata/after_9_10/TPatternbind.expected b/plugins/hls-semantic-tokens-plugin/test/testdata/after_9_10/TPatternbind.expected new file mode 100644 index 0000000000..ab12539d12 --- /dev/null +++ b/plugins/hls-semantic-tokens-plugin/test/testdata/after_9_10/TPatternbind.expected @@ -0,0 +1,8 @@ +1:8-17 TModule "TVariable" +3:2-3 TVariable "a" +3:5-6 TVariable "b" +5:1-2 TFunction "f" +5:3-4 TFunction "g" +5:5-6 TVariable "y" +5:9-10 TFunction "g" +5:11-12 TVariable "y" diff --git a/plugins/hls-semantic-tokens-plugin/test/testdata/after_9_10/TPatternbind.hs b/plugins/hls-semantic-tokens-plugin/test/testdata/after_9_10/TPatternbind.hs new file mode 100644 index 0000000000..49e642a35d --- /dev/null +++ b/plugins/hls-semantic-tokens-plugin/test/testdata/after_9_10/TPatternbind.hs @@ -0,0 +1,9 @@ +module TVariable where + +(a, b) = (1, 2) + +f g y = g y + + + + diff --git a/plugins/hls-semantic-tokens-plugin/test/testdata/after_9_10/TQualifiedName.expected b/plugins/hls-semantic-tokens-plugin/test/testdata/after_9_10/TQualifiedName.expected new file mode 100644 index 0000000000..df305195ed --- /dev/null +++ b/plugins/hls-semantic-tokens-plugin/test/testdata/after_9_10/TQualifiedName.expected @@ -0,0 +1,13 @@ +1:8-22 TModule "TQualifiedName" +3:18-27 TModule "Data.List" +6:1-2 TVariable "a" +6:5-13 TModule "Prelude." +6:13-22 TVariable "undefined" +7:1-2 TVariable "b" +7:8-18 TModule "Data.List." +7:18-22 TClassMethod "elem" +8:1-2 TVariable "c" +8:6-14 TModule "Prelude." +8:14-15 TOperator "+" +9:1-2 TVariable "d" +9:6-7 TOperator "+" diff --git a/plugins/hls-semantic-tokens-plugin/test/testdata/after_9_10/TQualifiedName.hs b/plugins/hls-semantic-tokens-plugin/test/testdata/after_9_10/TQualifiedName.hs new file mode 100644 index 0000000000..5dbdcc1d52 --- /dev/null +++ b/plugins/hls-semantic-tokens-plugin/test/testdata/after_9_10/TQualifiedName.hs @@ -0,0 +1,9 @@ +module TQualifiedName where + +import qualified Data.List + + +a = Prelude.undefined +b = 1 `Data.List.elem` [1, 2] +c = (Prelude.+) 1 1 +d = (+) 1 1 diff --git a/plugins/hls-semantic-tokens-plugin/test/testdata/after_9_10/TRecord.expected b/plugins/hls-semantic-tokens-plugin/test/testdata/after_9_10/TRecord.expected new file mode 100644 index 0000000000..5be40a4a39 --- /dev/null +++ b/plugins/hls-semantic-tokens-plugin/test/testdata/after_9_10/TRecord.expected @@ -0,0 +1,5 @@ +1:8-15 TModule "TRecord" +4:6-9 TTypeConstructor "Foo" +4:12-15 TDataConstructor "Foo" +4:18-21 TRecordField "foo" +4:25-28 TTypeConstructor "Int" diff --git a/plugins/hls-semantic-tokens-plugin/test/testdata/after_9_10/TRecord.hs b/plugins/hls-semantic-tokens-plugin/test/testdata/after_9_10/TRecord.hs new file mode 100644 index 0000000000..b3176a154f --- /dev/null +++ b/plugins/hls-semantic-tokens-plugin/test/testdata/after_9_10/TRecord.hs @@ -0,0 +1,7 @@ +module TRecord where + + +data Foo = Foo { foo :: Int } + + + diff --git a/plugins/hls-semantic-tokens-plugin/test/testdata/after_9_10/TRecordDuplicateRecordFields.expected b/plugins/hls-semantic-tokens-plugin/test/testdata/after_9_10/TRecordDuplicateRecordFields.expected new file mode 100644 index 0000000000..04ef050ab0 --- /dev/null +++ b/plugins/hls-semantic-tokens-plugin/test/testdata/after_9_10/TRecordDuplicateRecordFields.expected @@ -0,0 +1,5 @@ +3:8-36 TModule "TRecordDuplicateRecordFields" +5:6-9 TTypeConstructor "Foo" +5:12-15 TDataConstructor "Foo" +5:18-21 TRecordField "boo" +5:26-32 TTypeSynonym "String" diff --git a/plugins/hls-semantic-tokens-plugin/test/testdata/after_9_10/TRecordDuplicateRecordFields.hs b/plugins/hls-semantic-tokens-plugin/test/testdata/after_9_10/TRecordDuplicateRecordFields.hs new file mode 100644 index 0000000000..395a1d3731 --- /dev/null +++ b/plugins/hls-semantic-tokens-plugin/test/testdata/after_9_10/TRecordDuplicateRecordFields.hs @@ -0,0 +1,5 @@ +{-# LANGUAGE DuplicateRecordFields #-} + +module TRecordDuplicateRecordFields where + +data Foo = Foo { boo :: !String } diff --git a/plugins/hls-semantic-tokens-plugin/test/testdata/after_9_10/TTypefamily.expected b/plugins/hls-semantic-tokens-plugin/test/testdata/after_9_10/TTypefamily.expected new file mode 100644 index 0000000000..1aa6bf4687 --- /dev/null +++ b/plugins/hls-semantic-tokens-plugin/test/testdata/after_9_10/TTypefamily.expected @@ -0,0 +1,9 @@ +2:8-19 TModule "TTypefamily" +4:13-16 TTypeFamily "Foo" +4:17-18 TTypeVariable "a" +5:3-6 TTypeFamily "Foo" +5:7-10 TTypeConstructor "Int" +5:13-16 TTypeConstructor "Int" +6:3-6 TTypeFamily "Foo" +6:7-8 TTypeVariable "a" +6:11-17 TTypeSynonym "String" diff --git a/plugins/hls-semantic-tokens-plugin/test/testdata/after_9_10/TTypefamily.hs b/plugins/hls-semantic-tokens-plugin/test/testdata/after_9_10/TTypefamily.hs new file mode 100644 index 0000000000..d8c925e370 --- /dev/null +++ b/plugins/hls-semantic-tokens-plugin/test/testdata/after_9_10/TTypefamily.hs @@ -0,0 +1,6 @@ +{-# LANGUAGE TypeFamilies #-} +module TTypefamily where + +type family Foo a where + Foo Int = Int + Foo a = String diff --git a/plugins/hls-semantic-tokens-plugin/test/testdata/after_9_10/TUnicodeSyntax.expected b/plugins/hls-semantic-tokens-plugin/test/testdata/after_9_10/TUnicodeSyntax.expected new file mode 100644 index 0000000000..ad9f6ea762 --- /dev/null +++ b/plugins/hls-semantic-tokens-plugin/test/testdata/after_9_10/TUnicodeSyntax.expected @@ -0,0 +1,2 @@ +1:8-22 TModule "TUnicodeSyntax" +3:1-4 TVariable "a\66560b" diff --git a/plugins/hls-semantic-tokens-plugin/test/testdata/after_9_10/TUnicodeSyntax.hs b/plugins/hls-semantic-tokens-plugin/test/testdata/after_9_10/TUnicodeSyntax.hs new file mode 100644 index 0000000000..1b8c7c1baa --- /dev/null +++ b/plugins/hls-semantic-tokens-plugin/test/testdata/after_9_10/TUnicodeSyntax.hs @@ -0,0 +1,5 @@ +module TUnicodeSyntax where + +a𐐀b = "a𐐀b" + + diff --git a/plugins/hls-semantic-tokens-plugin/test/testdata/after_9_10/TValBind.expected b/plugins/hls-semantic-tokens-plugin/test/testdata/after_9_10/TValBind.expected new file mode 100644 index 0000000000..700509c968 --- /dev/null +++ b/plugins/hls-semantic-tokens-plugin/test/testdata/after_9_10/TValBind.expected @@ -0,0 +1,5 @@ +1:8-16 TModule "TValBind" +4:1-6 TVariable "hello" +4:10-13 TTypeConstructor "Int" +5:1-6 TVariable "hello" +5:9-15 TClassMethod "length" diff --git a/plugins/hls-semantic-tokens-plugin/test/testdata/after_9_10/TValBind.hs b/plugins/hls-semantic-tokens-plugin/test/testdata/after_9_10/TValBind.hs new file mode 100644 index 0000000000..506af37a42 --- /dev/null +++ b/plugins/hls-semantic-tokens-plugin/test/testdata/after_9_10/TValBind.hs @@ -0,0 +1,8 @@ +module TValBind where + + +hello :: Int +hello = length "Hello, Haskell!" + + + diff --git a/plugins/hls-semantic-tokens-plugin/test/testdata/before_9_10/T1.expected b/plugins/hls-semantic-tokens-plugin/test/testdata/before_9_10/T1.expected new file mode 100644 index 0000000000..cbf7699f19 --- /dev/null +++ b/plugins/hls-semantic-tokens-plugin/test/testdata/before_9_10/T1.expected @@ -0,0 +1,81 @@ +9:6-9 TTypeConstructor "Foo" +9:12-15 TDataConstructor "Foo" +9:18-21 TRecordField "foo" +9:25-28 TTypeConstructor "Int" +11:7-10 TClass "Boo" +11:11-12 TTypeVariable "a" +12:3-6 TClassMethod "boo" +12:10-11 TTypeVariable "a" +12:15-16 TTypeVariable "a" +14:10-13 TClass "Boo" +14:14-17 TTypeConstructor "Int" +15:5-8 TClassMethod "boo" +15:9-10 TVariable "x" +15:13-14 TVariable "x" +15:15-16 TOperator "+" +17:6-8 TTypeConstructor "Dd" +17:11-13 TDataConstructor "Dd" +17:14-17 TTypeConstructor "Int" +19:9-12 TPatternSynonym "One" +19:15-18 TDataConstructor "Foo" +21:1-4 TVariable "ggg" +21:7-10 TPatternSynonym "One" +23:6-9 TTypeConstructor "Doo" +23:12-15 TDataConstructor "Doo" +23:16-24 TModule "Prelude." +23:24-27 TTypeConstructor "Int" +24:6-10 TTypeSynonym "Bar1" +24:13-16 TTypeConstructor "Int" +25:6-10 TTypeSynonym "Bar2" +25:13-16 TTypeConstructor "Doo" +27:1-3 TFunction "bb" +27:8-11 TClass "Boo" +27:12-13 TTypeVariable "a" +27:18-19 TTypeVariable "a" +27:23-24 TTypeVariable "a" +28:1-3 TFunction "bb" +28:4-5 TVariable "x" +28:9-12 TClassMethod "boo" +28:13-14 TVariable "x" +29:1-3 TFunction "aa" +29:7-11 TTypeVariable "cool" +29:15-18 TTypeConstructor "Int" +29:22-26 TTypeVariable "cool" +30:1-3 TFunction "aa" +30:4-5 TVariable "x" +30:9-10 TVariable "c" +30:14-16 TFunction "aa" +30:17-18 TVariable "x" +30:19-20 TVariable "c" +31:12-14 TVariable "xx" +31:16-18 TVariable "yy" +32:11-13 TVariable "dd" +34:2-4 TVariable "zz" +34:6-8 TVariable "kk" +35:1-3 TFunction "cc" +35:7-10 TTypeConstructor "Foo" +35:15-18 TTypeConstructor "Int" +35:20-23 TTypeConstructor "Int" +35:28-31 TTypeConstructor "Int" +36:1-3 TFunction "cc" +36:4-5 TVariable "f" +36:7-9 TVariable "gg" +36:11-13 TVariable "vv" +37:10-12 TVariable "gg" +38:14-17 TRecordField "foo" +38:18-19 TOperator "$" +38:20-21 TVariable "f" +38:24-27 TRecordField "foo" +39:14-17 TRecordField "foo" +39:18-19 TOperator "$" +39:20-21 TVariable "f" +39:24-27 TRecordField "foo" +41:1-3 TFunction "go" +41:6-9 TRecordField "foo" +42:1-4 TFunction "add" +42:8-16 TModule "Prelude." +42:16-17 TOperator "+" +47:1-5 TVariable "main" +47:9-11 TTypeConstructor "IO" +48:1-5 TVariable "main" +48:8-16 TFunction "putStrLn" diff --git a/plugins/hls-semantic-tokens-plugin/test/testdata/before_9_10/T1.hs b/plugins/hls-semantic-tokens-plugin/test/testdata/before_9_10/T1.hs new file mode 100644 index 0000000000..07b0476c1e --- /dev/null +++ b/plugins/hls-semantic-tokens-plugin/test/testdata/before_9_10/T1.hs @@ -0,0 +1,48 @@ +-- patter syn +{-# LANGUAGE PatternSynonyms #-} + +module Main where + +-- import Data.Set (Set, insert) + + +data Foo = Foo { foo :: Int } + +class Boo a where + boo :: a -> a + +instance Boo Int where + boo x = x + 1 + +data Dd = Dd Int + +pattern One = Foo 1 + +ggg = One + +data Doo = Doo Prelude.Int +type Bar1 = Int +type Bar2 = Doo + +bb :: (Boo a) => a -> a +bb x = boo x +aa :: cool -> Int -> cool +aa x = \c -> aa x c + where (xx, yy) = (1, 2) + dd = 1 + +(zz, kk) = (1, 2) +cc :: Foo -> (Int, Int) -> Int +cc f (gg, vv)= + case gg of + 1 -> foo $ f { foo = 1 } + 2 -> foo $ f { foo = 1 } + +go = foo +add = (Prelude.+) + +-- sub :: Int -> Int -> Int +-- sub x y = add x y + +main :: IO () +main = putStrLn "Hello, Haskell!" diff --git a/plugins/hls-semantic-tokens-plugin/test/testdata/before_9_10/TClass.expected b/plugins/hls-semantic-tokens-plugin/test/testdata/before_9_10/TClass.expected new file mode 100644 index 0000000000..e369963b0e --- /dev/null +++ b/plugins/hls-semantic-tokens-plugin/test/testdata/before_9_10/TClass.expected @@ -0,0 +1,5 @@ +4:7-10 TClass "Foo" +4:11-12 TTypeVariable "a" +5:3-6 TClassMethod "foo" +5:10-11 TTypeVariable "a" +5:15-18 TTypeConstructor "Int" diff --git a/plugins/hls-semantic-tokens-plugin/test/testdata/before_9_10/TClass.hs b/plugins/hls-semantic-tokens-plugin/test/testdata/before_9_10/TClass.hs new file mode 100644 index 0000000000..692754ec71 --- /dev/null +++ b/plugins/hls-semantic-tokens-plugin/test/testdata/before_9_10/TClass.hs @@ -0,0 +1,6 @@ +module TClass where + + +class Foo a where + foo :: a -> Int + diff --git a/plugins/hls-semantic-tokens-plugin/test/testdata/before_9_10/TClassImportedDeriving.expected b/plugins/hls-semantic-tokens-plugin/test/testdata/before_9_10/TClassImportedDeriving.expected new file mode 100644 index 0000000000..3bbeb3e66c --- /dev/null +++ b/plugins/hls-semantic-tokens-plugin/test/testdata/before_9_10/TClassImportedDeriving.expected @@ -0,0 +1,3 @@ +4:6-9 TTypeConstructor "Foo" +4:12-15 TDataConstructor "Foo" +4:26-30 TClass "Show" diff --git a/plugins/hls-semantic-tokens-plugin/test/testdata/before_9_10/TClassImportedDeriving.hs b/plugins/hls-semantic-tokens-plugin/test/testdata/before_9_10/TClassImportedDeriving.hs new file mode 100644 index 0000000000..8afd8afbd9 --- /dev/null +++ b/plugins/hls-semantic-tokens-plugin/test/testdata/before_9_10/TClassImportedDeriving.hs @@ -0,0 +1,10 @@ +{-# LANGUAGE StandaloneDeriving #-} +module TClassImportedDeriving where +-- deriving method source span of Show occurrence +data Foo = Foo deriving (Show) + +-- standalone deriving method not in the same position +-- deriving instance Eq Foo + +-- a :: Foo -> Foo -> Bool +-- a = (==) diff --git a/plugins/hls-semantic-tokens-plugin/test/testdata/before_9_10/TDataFamily.expected b/plugins/hls-semantic-tokens-plugin/test/testdata/before_9_10/TDataFamily.expected new file mode 100644 index 0000000000..c95c0689f0 --- /dev/null +++ b/plugins/hls-semantic-tokens-plugin/test/testdata/before_9_10/TDataFamily.expected @@ -0,0 +1,12 @@ +5:13-18 TTypeFamily "XList" +5:19-20 TTypeVariable "a" +8:15-20 TTypeFamily "XList" +8:21-25 TTypeConstructor "Char" +8:28-33 TDataConstructor "XCons" +8:35-39 TTypeConstructor "Char" +8:42-47 TTypeFamily "XList" +8:48-52 TTypeConstructor "Char" +8:56-60 TDataConstructor "XNil" +11:15-20 TTypeFamily "XList" +11:26-35 TDataConstructor "XListUnit" +11:37-40 TTypeConstructor "Int" diff --git a/plugins/hls-semantic-tokens-plugin/test/testdata/before_9_10/TDataFamily.hs b/plugins/hls-semantic-tokens-plugin/test/testdata/before_9_10/TDataFamily.hs new file mode 100644 index 0000000000..b9047a72d2 --- /dev/null +++ b/plugins/hls-semantic-tokens-plugin/test/testdata/before_9_10/TDataFamily.hs @@ -0,0 +1,11 @@ +{-# LANGUAGE TypeFamilies #-} +module TDatafamily where + +-- Declare a list-like data family +data family XList a + +-- Declare a list-like instance for Char +data instance XList Char = XCons !Char !(XList Char) | XNil + +-- Declare a number-like instance for () +data instance XList () = XListUnit !Int diff --git a/plugins/hls-semantic-tokens-plugin/test/testdata/before_9_10/TDataType.expected b/plugins/hls-semantic-tokens-plugin/test/testdata/before_9_10/TDataType.expected new file mode 100644 index 0000000000..bdf280c45e --- /dev/null +++ b/plugins/hls-semantic-tokens-plugin/test/testdata/before_9_10/TDataType.expected @@ -0,0 +1,4 @@ +3:6-9 TTypeConstructor "Foo" +3:12-15 TDataConstructor "Foo" +3:16-19 TTypeConstructor "Int" +3:30-32 TClass "Eq" diff --git a/plugins/hls-semantic-tokens-plugin/test/testdata/before_9_10/TDataType.hs b/plugins/hls-semantic-tokens-plugin/test/testdata/before_9_10/TDataType.hs new file mode 100644 index 0000000000..894065e391 --- /dev/null +++ b/plugins/hls-semantic-tokens-plugin/test/testdata/before_9_10/TDataType.hs @@ -0,0 +1,3 @@ +module TDataType where + +data Foo = Foo Int deriving (Eq) diff --git a/plugins/hls-semantic-tokens-plugin/test/testdata/before_9_10/TDatatypeImported.expected b/plugins/hls-semantic-tokens-plugin/test/testdata/before_9_10/TDatatypeImported.expected new file mode 100644 index 0000000000..2c2cd492a0 --- /dev/null +++ b/plugins/hls-semantic-tokens-plugin/test/testdata/before_9_10/TDatatypeImported.expected @@ -0,0 +1,5 @@ +3:8-17 TModule "System.IO" +5:1-3 TVariable "go" +5:7-9 TTypeConstructor "IO" +6:1-3 TVariable "go" +6:6-11 TFunction "print" diff --git a/plugins/hls-semantic-tokens-plugin/test/testdata/before_9_10/TDatatypeImported.hs b/plugins/hls-semantic-tokens-plugin/test/testdata/before_9_10/TDatatypeImported.hs new file mode 100644 index 0000000000..f6ac8996d9 --- /dev/null +++ b/plugins/hls-semantic-tokens-plugin/test/testdata/before_9_10/TDatatypeImported.hs @@ -0,0 +1,6 @@ +module TDatatypeImported where + +import System.IO + +go :: IO () +go = print 1 diff --git a/plugins/hls-semantic-tokens-plugin/test/testdata/before_9_10/TDoc.expected b/plugins/hls-semantic-tokens-plugin/test/testdata/before_9_10/TDoc.expected new file mode 100644 index 0000000000..405308c3c8 --- /dev/null +++ b/plugins/hls-semantic-tokens-plugin/test/testdata/before_9_10/TDoc.expected @@ -0,0 +1,5 @@ +4:5-10 TVariable "hello" +5:1-6 TVariable "hello" +5:10-13 TTypeConstructor "Int" +6:1-6 TVariable "hello" +6:9-15 TClassMethod "length" diff --git a/plugins/hls-semantic-tokens-plugin/test/testdata/before_9_10/TDoc.hs b/plugins/hls-semantic-tokens-plugin/test/testdata/before_9_10/TDoc.hs new file mode 100644 index 0000000000..dc5801b0e6 --- /dev/null +++ b/plugins/hls-semantic-tokens-plugin/test/testdata/before_9_10/TDoc.hs @@ -0,0 +1,9 @@ +module TDoc where + +-- | +-- `hello` +hello :: Int +hello = length "Hello, Haskell!" + + + diff --git a/plugins/hls-semantic-tokens-plugin/test/testdata/before_9_10/TFunction.expected b/plugins/hls-semantic-tokens-plugin/test/testdata/before_9_10/TFunction.expected new file mode 100644 index 0000000000..f34510728b --- /dev/null +++ b/plugins/hls-semantic-tokens-plugin/test/testdata/before_9_10/TFunction.expected @@ -0,0 +1,11 @@ +3:1-2 TFunction "f" +3:13-14 TTypeVariable "a" +3:16-17 TTypeVariable "a" +3:21-22 TTypeVariable "a" +4:1-2 TFunction "f" +4:3-4 TVariable "x" +4:7-8 TVariable "x" +6:1-2 TVariable "x" +6:6-7 TTypeVariable "a" +7:1-2 TVariable "x" +7:5-14 TVariable "undefined" diff --git a/plugins/hls-semantic-tokens-plugin/test/testdata/before_9_10/TFunction.hs b/plugins/hls-semantic-tokens-plugin/test/testdata/before_9_10/TFunction.hs new file mode 100644 index 0000000000..4efe5cecc4 --- /dev/null +++ b/plugins/hls-semantic-tokens-plugin/test/testdata/before_9_10/TFunction.hs @@ -0,0 +1,7 @@ +module TFunction where + +f :: forall a. a -> a +f x = x + +x :: a +x = undefined diff --git a/plugins/hls-semantic-tokens-plugin/test/testdata/before_9_10/TFunctionLet.expected b/plugins/hls-semantic-tokens-plugin/test/testdata/before_9_10/TFunctionLet.expected new file mode 100644 index 0000000000..3f27b723db --- /dev/null +++ b/plugins/hls-semantic-tokens-plugin/test/testdata/before_9_10/TFunctionLet.expected @@ -0,0 +1,5 @@ +3:1-2 TVariable "y" +3:6-9 TTypeConstructor "Int" +4:1-2 TVariable "y" +4:9-10 TFunction "f" +4:11-12 TVariable "x" diff --git a/plugins/hls-semantic-tokens-plugin/test/testdata/before_9_10/TFunctionLet.hs b/plugins/hls-semantic-tokens-plugin/test/testdata/before_9_10/TFunctionLet.hs new file mode 100644 index 0000000000..96854c34ad --- /dev/null +++ b/plugins/hls-semantic-tokens-plugin/test/testdata/before_9_10/TFunctionLet.hs @@ -0,0 +1,4 @@ +module TFunctionLet where + +y :: Int +y = let f x = 1 in 1 diff --git a/plugins/hls-semantic-tokens-plugin/test/testdata/before_9_10/TFunctionLocal.expected b/plugins/hls-semantic-tokens-plugin/test/testdata/before_9_10/TFunctionLocal.expected new file mode 100644 index 0000000000..176606e396 --- /dev/null +++ b/plugins/hls-semantic-tokens-plugin/test/testdata/before_9_10/TFunctionLocal.expected @@ -0,0 +1,7 @@ +3:1-2 TFunction "f" +3:6-9 TTypeConstructor "Int" +3:13-16 TTypeConstructor "Int" +4:1-2 TFunction "f" +4:7-8 TFunction "g" +6:5-6 TFunction "g" +6:7-8 TVariable "x" diff --git a/plugins/hls-semantic-tokens-plugin/test/testdata/before_9_10/TFunctionLocal.hs b/plugins/hls-semantic-tokens-plugin/test/testdata/before_9_10/TFunctionLocal.hs new file mode 100644 index 0000000000..fed144b00c --- /dev/null +++ b/plugins/hls-semantic-tokens-plugin/test/testdata/before_9_10/TFunctionLocal.hs @@ -0,0 +1,8 @@ +module TFunctionLocal where + +f :: Int -> Int +f 1 = g 1 + where + g x = 1 + + diff --git a/plugins/hls-semantic-tokens-plugin/test/testdata/before_9_10/TFunctionUnderTypeSynonym.expected b/plugins/hls-semantic-tokens-plugin/test/testdata/before_9_10/TFunctionUnderTypeSynonym.expected new file mode 100644 index 0000000000..010cf0c613 --- /dev/null +++ b/plugins/hls-semantic-tokens-plugin/test/testdata/before_9_10/TFunctionUnderTypeSynonym.expected @@ -0,0 +1,17 @@ +3:6-8 TTypeSynonym "T1" +3:11-14 TTypeConstructor "Int" +3:18-21 TTypeConstructor "Int" +4:6-8 TTypeSynonym "T2" +4:18-19 TTypeVariable "a" +4:21-22 TTypeVariable "a" +4:26-27 TTypeVariable "a" +5:1-3 TFunction "f1" +5:7-9 TTypeSynonym "T1" +6:1-3 TFunction "f1" +6:4-5 TVariable "x" +6:8-9 TVariable "x" +7:1-3 TFunction "f2" +7:7-9 TTypeSynonym "T2" +8:1-3 TFunction "f2" +8:4-5 TVariable "x" +8:8-9 TVariable "x" diff --git a/plugins/hls-semantic-tokens-plugin/test/testdata/before_9_10/TFunctionUnderTypeSynonym.hs b/plugins/hls-semantic-tokens-plugin/test/testdata/before_9_10/TFunctionUnderTypeSynonym.hs new file mode 100644 index 0000000000..6485232394 --- /dev/null +++ b/plugins/hls-semantic-tokens-plugin/test/testdata/before_9_10/TFunctionUnderTypeSynonym.hs @@ -0,0 +1,9 @@ +module TFunctionUnderTypeSynonym where + +type T1 = Int -> Int +type T2 = forall a. a -> a +f1 :: T1 +f1 x = x +f2 :: T2 +f2 x = x + diff --git a/plugins/hls-semantic-tokens-plugin/test/testdata/before_9_10/TGADT.expected b/plugins/hls-semantic-tokens-plugin/test/testdata/before_9_10/TGADT.expected new file mode 100644 index 0000000000..ad3ac0f086 --- /dev/null +++ b/plugins/hls-semantic-tokens-plugin/test/testdata/before_9_10/TGADT.expected @@ -0,0 +1,13 @@ +5:6-9 TTypeConstructor "Lam" +6:3-7 TDataConstructor "Lift" +6:11-12 TTypeVariable "a" +6:36-39 TTypeConstructor "Lam" +6:40-41 TTypeVariable "a" +7:3-6 TDataConstructor "Lam" +7:12-15 TTypeConstructor "Lam" +7:16-17 TTypeVariable "a" +7:21-24 TTypeConstructor "Lam" +7:25-26 TTypeVariable "b" +7:36-39 TTypeConstructor "Lam" +7:41-42 TTypeVariable "a" +7:46-47 TTypeVariable "b" diff --git a/plugins/hls-semantic-tokens-plugin/test/testdata/before_9_10/TGADT.hs b/plugins/hls-semantic-tokens-plugin/test/testdata/before_9_10/TGADT.hs new file mode 100644 index 0000000000..e0cccf8bed --- /dev/null +++ b/plugins/hls-semantic-tokens-plugin/test/testdata/before_9_10/TGADT.hs @@ -0,0 +1,7 @@ +{-# LANGUAGE TypeFamilies #-} +{-# LANGUAGE GADTs #-} +module TGADT where + +data Lam :: * -> * where + Lift :: a -> Lam a -- ^ lifted value + Lam :: (Lam a -> Lam b) -> Lam (a -> b) -- ^ lambda abstraction diff --git a/plugins/hls-semantic-tokens-plugin/test/testdata/before_9_10/TInstanceClassMethodBind.expected b/plugins/hls-semantic-tokens-plugin/test/testdata/before_9_10/TInstanceClassMethodBind.expected new file mode 100644 index 0000000000..a4a6ef98e0 --- /dev/null +++ b/plugins/hls-semantic-tokens-plugin/test/testdata/before_9_10/TInstanceClassMethodBind.expected @@ -0,0 +1,7 @@ +4:6-9 TTypeConstructor "Foo" +4:12-15 TDataConstructor "Foo" +4:16-19 TTypeConstructor "Int" +5:10-14 TClass "Show" +5:15-18 TTypeConstructor "Foo" +6:5-9 TClassMethod "show" +6:12-21 TVariable "undefined" diff --git a/plugins/hls-semantic-tokens-plugin/test/testdata/before_9_10/TInstanceClassMethodBind.hs b/plugins/hls-semantic-tokens-plugin/test/testdata/before_9_10/TInstanceClassMethodBind.hs new file mode 100644 index 0000000000..33976a48c1 --- /dev/null +++ b/plugins/hls-semantic-tokens-plugin/test/testdata/before_9_10/TInstanceClassMethodBind.hs @@ -0,0 +1,6 @@ +module TInstanceClassMethodBind where + + +data Foo = Foo Int +instance Show Foo where + show = undefined diff --git a/plugins/hls-semantic-tokens-plugin/test/testdata/before_9_10/TInstanceClassMethodUse.expected b/plugins/hls-semantic-tokens-plugin/test/testdata/before_9_10/TInstanceClassMethodUse.expected new file mode 100644 index 0000000000..2bf39be435 --- /dev/null +++ b/plugins/hls-semantic-tokens-plugin/test/testdata/before_9_10/TInstanceClassMethodUse.expected @@ -0,0 +1,2 @@ +4:1-3 TFunction "go" +4:8-12 TClassMethod "show" diff --git a/plugins/hls-semantic-tokens-plugin/test/testdata/before_9_10/TInstanceClassMethodUse.hs b/plugins/hls-semantic-tokens-plugin/test/testdata/before_9_10/TInstanceClassMethodUse.hs new file mode 100644 index 0000000000..689d1643d4 --- /dev/null +++ b/plugins/hls-semantic-tokens-plugin/test/testdata/before_9_10/TInstanceClassMethodUse.hs @@ -0,0 +1,5 @@ +module TInstanceClassMethodUse where + + +go = show + diff --git a/plugins/hls-semantic-tokens-plugin/test/testdata/before_9_10/TModuleA.hs b/plugins/hls-semantic-tokens-plugin/test/testdata/before_9_10/TModuleA.hs new file mode 100644 index 0000000000..d76f64fc1f --- /dev/null +++ b/plugins/hls-semantic-tokens-plugin/test/testdata/before_9_10/TModuleA.hs @@ -0,0 +1,5 @@ +module TModuleA where + +data Game = Game {a𐐀b :: Int} + + diff --git a/plugins/hls-semantic-tokens-plugin/test/testdata/before_9_10/TModuleB.hs b/plugins/hls-semantic-tokens-plugin/test/testdata/before_9_10/TModuleB.hs new file mode 100644 index 0000000000..d2bfe4b7fa --- /dev/null +++ b/plugins/hls-semantic-tokens-plugin/test/testdata/before_9_10/TModuleB.hs @@ -0,0 +1,8 @@ +module TModuleB where + +import TModuleA +import qualified TModuleA + +go = Game 1 + +a𐐀bb = TModuleA.a𐐀b go diff --git a/plugins/hls-semantic-tokens-plugin/test/testdata/before_9_10/TNoneFunctionWithConstraint.expected b/plugins/hls-semantic-tokens-plugin/test/testdata/before_9_10/TNoneFunctionWithConstraint.expected new file mode 100644 index 0000000000..2dd89fd1da --- /dev/null +++ b/plugins/hls-semantic-tokens-plugin/test/testdata/before_9_10/TNoneFunctionWithConstraint.expected @@ -0,0 +1,6 @@ +3:1-2 TVariable "x" +3:7-9 TClass "Eq" +3:10-11 TTypeVariable "a" +3:16-17 TTypeVariable "a" +4:1-2 TVariable "x" +4:5-14 TVariable "undefined" diff --git a/plugins/hls-semantic-tokens-plugin/test/testdata/before_9_10/TNoneFunctionWithConstraint.hs b/plugins/hls-semantic-tokens-plugin/test/testdata/before_9_10/TNoneFunctionWithConstraint.hs new file mode 100644 index 0000000000..9a7119dbdb --- /dev/null +++ b/plugins/hls-semantic-tokens-plugin/test/testdata/before_9_10/TNoneFunctionWithConstraint.hs @@ -0,0 +1,5 @@ +module TNoneFunctionWithConstraint where + +x :: (Eq a) => a +x = undefined + diff --git a/plugins/hls-semantic-tokens-plugin/test/testdata/before_9_10/TOperator.expected b/plugins/hls-semantic-tokens-plugin/test/testdata/before_9_10/TOperator.expected new file mode 100644 index 0000000000..c19e7cb904 --- /dev/null +++ b/plugins/hls-semantic-tokens-plugin/test/testdata/before_9_10/TOperator.expected @@ -0,0 +1,33 @@ +4:1-3 TFunction "go" +4:4-5 TFunction "f" +4:6-7 TVariable "x" +4:10-11 TFunction "f" +4:11-12 TOperator "$" +4:12-13 TVariable "x" +6:2-6 TOperator "$$$$" +7:1-2 TVariable "x" +7:7-11 TOperator "$$$$" +8:6-7 TTypeVariable "a" +8:8-11 TOperator ":+:" +8:12-13 TTypeVariable "b" +8:16-19 TDataConstructor "Add" +8:20-21 TTypeVariable "a" +8:22-23 TTypeVariable "b" +9:7-10 TOperator ":-:" +9:12-13 TTypeVariable "a" +9:14-15 TTypeVariable "b" +9:19-20 TTypeVariable "a" +9:22-23 TTypeVariable "b" +11:1-4 TFunction "add" +11:8-11 TTypeConstructor "Int" +11:12-15 TOperator ":+:" +11:16-19 TTypeConstructor "Int" +11:23-26 TTypeConstructor "Int" +11:27-30 TOperator ":-:" +11:31-34 TTypeConstructor "Int" +13:1-4 TFunction "add" +13:6-9 TDataConstructor "Add" +13:10-11 TVariable "x" +13:12-13 TVariable "y" +13:18-19 TVariable "x" +13:21-22 TVariable "y" diff --git a/plugins/hls-semantic-tokens-plugin/test/testdata/before_9_10/TOperator.hs b/plugins/hls-semantic-tokens-plugin/test/testdata/before_9_10/TOperator.hs new file mode 100644 index 0000000000..e2f06c92fa --- /dev/null +++ b/plugins/hls-semantic-tokens-plugin/test/testdata/before_9_10/TOperator.hs @@ -0,0 +1,13 @@ +module TOperator where + +-- imported operator +go f x = f$x +-- operator defined in local module +($$$$) = b +x = 1 $$$$ 2 +data a :+: b = Add a b +type (:-:) a b = (a, b) +-- type take precedence over operator +add :: Int :+: Int -> Int :-: Int +-- class method take precedence over operator +add (Add x y) = (x, y) diff --git a/plugins/hls-semantic-tokens-plugin/test/testdata/before_9_10/TPatternMatch.expected b/plugins/hls-semantic-tokens-plugin/test/testdata/before_9_10/TPatternMatch.expected new file mode 100644 index 0000000000..0535662e63 --- /dev/null +++ b/plugins/hls-semantic-tokens-plugin/test/testdata/before_9_10/TPatternMatch.expected @@ -0,0 +1,2 @@ +4:1-2 TFunction "g" +4:4-11 TDataConstructor "Nothing" diff --git a/plugins/hls-semantic-tokens-plugin/test/testdata/before_9_10/TPatternMatch.hs b/plugins/hls-semantic-tokens-plugin/test/testdata/before_9_10/TPatternMatch.hs new file mode 100644 index 0000000000..95e97c1abb --- /dev/null +++ b/plugins/hls-semantic-tokens-plugin/test/testdata/before_9_10/TPatternMatch.hs @@ -0,0 +1,6 @@ +module TPatternMatch where + + +g (Nothing, _) = 1 + + diff --git a/plugins/hls-semantic-tokens-plugin/test/testdata/before_9_10/TPatternSynonym.expected b/plugins/hls-semantic-tokens-plugin/test/testdata/before_9_10/TPatternSynonym.expected new file mode 100644 index 0000000000..7cdf5260cb --- /dev/null +++ b/plugins/hls-semantic-tokens-plugin/test/testdata/before_9_10/TPatternSynonym.expected @@ -0,0 +1 @@ +5:9-12 TPatternSynonym "Foo" diff --git a/plugins/hls-semantic-tokens-plugin/test/testdata/before_9_10/TPatternSynonym.hs b/plugins/hls-semantic-tokens-plugin/test/testdata/before_9_10/TPatternSynonym.hs new file mode 100644 index 0000000000..adff673ce8 --- /dev/null +++ b/plugins/hls-semantic-tokens-plugin/test/testdata/before_9_10/TPatternSynonym.hs @@ -0,0 +1,7 @@ +{-# LANGUAGE PatternSynonyms #-} +module TPatternSynonym where + + +pattern Foo = 1 + + diff --git a/plugins/hls-semantic-tokens-plugin/test/testdata/before_9_10/TPatternbind.expected b/plugins/hls-semantic-tokens-plugin/test/testdata/before_9_10/TPatternbind.expected new file mode 100644 index 0000000000..6c62634487 --- /dev/null +++ b/plugins/hls-semantic-tokens-plugin/test/testdata/before_9_10/TPatternbind.expected @@ -0,0 +1,7 @@ +3:2-3 TVariable "a" +3:5-6 TVariable "b" +5:1-2 TFunction "f" +5:3-4 TFunction "g" +5:5-6 TVariable "y" +5:9-10 TFunction "g" +5:11-12 TVariable "y" diff --git a/plugins/hls-semantic-tokens-plugin/test/testdata/before_9_10/TPatternbind.hs b/plugins/hls-semantic-tokens-plugin/test/testdata/before_9_10/TPatternbind.hs new file mode 100644 index 0000000000..49e642a35d --- /dev/null +++ b/plugins/hls-semantic-tokens-plugin/test/testdata/before_9_10/TPatternbind.hs @@ -0,0 +1,9 @@ +module TVariable where + +(a, b) = (1, 2) + +f g y = g y + + + + diff --git a/plugins/hls-semantic-tokens-plugin/test/testdata/before_9_10/TQualifiedName.expected b/plugins/hls-semantic-tokens-plugin/test/testdata/before_9_10/TQualifiedName.expected new file mode 100644 index 0000000000..0ca7cd7d5b --- /dev/null +++ b/plugins/hls-semantic-tokens-plugin/test/testdata/before_9_10/TQualifiedName.expected @@ -0,0 +1,12 @@ +3:18-27 TModule "Data.List" +6:1-2 TVariable "a" +6:5-13 TModule "Prelude." +6:13-22 TVariable "undefined" +7:1-2 TVariable "b" +7:8-18 TModule "Data.List." +7:18-22 TClassMethod "elem" +8:1-2 TVariable "c" +8:6-14 TModule "Prelude." +8:14-15 TOperator "+" +9:1-2 TVariable "d" +9:6-7 TOperator "+" diff --git a/plugins/hls-semantic-tokens-plugin/test/testdata/before_9_10/TQualifiedName.hs b/plugins/hls-semantic-tokens-plugin/test/testdata/before_9_10/TQualifiedName.hs new file mode 100644 index 0000000000..5dbdcc1d52 --- /dev/null +++ b/plugins/hls-semantic-tokens-plugin/test/testdata/before_9_10/TQualifiedName.hs @@ -0,0 +1,9 @@ +module TQualifiedName where + +import qualified Data.List + + +a = Prelude.undefined +b = 1 `Data.List.elem` [1, 2] +c = (Prelude.+) 1 1 +d = (+) 1 1 diff --git a/plugins/hls-semantic-tokens-plugin/test/testdata/before_9_10/TRecord.expected b/plugins/hls-semantic-tokens-plugin/test/testdata/before_9_10/TRecord.expected new file mode 100644 index 0000000000..43b8e4d3b0 --- /dev/null +++ b/plugins/hls-semantic-tokens-plugin/test/testdata/before_9_10/TRecord.expected @@ -0,0 +1,4 @@ +4:6-9 TTypeConstructor "Foo" +4:12-15 TDataConstructor "Foo" +4:18-21 TRecordField "foo" +4:25-28 TTypeConstructor "Int" diff --git a/plugins/hls-semantic-tokens-plugin/test/testdata/before_9_10/TRecord.hs b/plugins/hls-semantic-tokens-plugin/test/testdata/before_9_10/TRecord.hs new file mode 100644 index 0000000000..b3176a154f --- /dev/null +++ b/plugins/hls-semantic-tokens-plugin/test/testdata/before_9_10/TRecord.hs @@ -0,0 +1,7 @@ +module TRecord where + + +data Foo = Foo { foo :: Int } + + + diff --git a/plugins/hls-semantic-tokens-plugin/test/testdata/before_9_10/TRecordDuplicateRecordFields.expected b/plugins/hls-semantic-tokens-plugin/test/testdata/before_9_10/TRecordDuplicateRecordFields.expected new file mode 100644 index 0000000000..70fdc63e18 --- /dev/null +++ b/plugins/hls-semantic-tokens-plugin/test/testdata/before_9_10/TRecordDuplicateRecordFields.expected @@ -0,0 +1,4 @@ +5:6-9 TTypeConstructor "Foo" +5:12-15 TDataConstructor "Foo" +5:18-21 TRecordField "boo" +5:26-32 TTypeSynonym "String" diff --git a/plugins/hls-semantic-tokens-plugin/test/testdata/before_9_10/TRecordDuplicateRecordFields.hs b/plugins/hls-semantic-tokens-plugin/test/testdata/before_9_10/TRecordDuplicateRecordFields.hs new file mode 100644 index 0000000000..395a1d3731 --- /dev/null +++ b/plugins/hls-semantic-tokens-plugin/test/testdata/before_9_10/TRecordDuplicateRecordFields.hs @@ -0,0 +1,5 @@ +{-# LANGUAGE DuplicateRecordFields #-} + +module TRecordDuplicateRecordFields where + +data Foo = Foo { boo :: !String } diff --git a/plugins/hls-semantic-tokens-plugin/test/testdata/before_9_10/TTypefamily.expected b/plugins/hls-semantic-tokens-plugin/test/testdata/before_9_10/TTypefamily.expected new file mode 100644 index 0000000000..08019bc3f3 --- /dev/null +++ b/plugins/hls-semantic-tokens-plugin/test/testdata/before_9_10/TTypefamily.expected @@ -0,0 +1,8 @@ +4:13-16 TTypeFamily "Foo" +4:17-18 TTypeVariable "a" +5:3-6 TTypeFamily "Foo" +5:7-10 TTypeConstructor "Int" +5:13-16 TTypeConstructor "Int" +6:3-6 TTypeFamily "Foo" +6:7-8 TTypeVariable "a" +6:11-17 TTypeSynonym "String" diff --git a/plugins/hls-semantic-tokens-plugin/test/testdata/before_9_10/TTypefamily.hs b/plugins/hls-semantic-tokens-plugin/test/testdata/before_9_10/TTypefamily.hs new file mode 100644 index 0000000000..d8c925e370 --- /dev/null +++ b/plugins/hls-semantic-tokens-plugin/test/testdata/before_9_10/TTypefamily.hs @@ -0,0 +1,6 @@ +{-# LANGUAGE TypeFamilies #-} +module TTypefamily where + +type family Foo a where + Foo Int = Int + Foo a = String diff --git a/plugins/hls-semantic-tokens-plugin/test/testdata/before_9_10/TUnicodeSyntax.expected b/plugins/hls-semantic-tokens-plugin/test/testdata/before_9_10/TUnicodeSyntax.expected new file mode 100644 index 0000000000..0b94b7c045 --- /dev/null +++ b/plugins/hls-semantic-tokens-plugin/test/testdata/before_9_10/TUnicodeSyntax.expected @@ -0,0 +1 @@ +3:1-4 TVariable "a\66560b" diff --git a/plugins/hls-semantic-tokens-plugin/test/testdata/before_9_10/TUnicodeSyntax.hs b/plugins/hls-semantic-tokens-plugin/test/testdata/before_9_10/TUnicodeSyntax.hs new file mode 100644 index 0000000000..1b8c7c1baa --- /dev/null +++ b/plugins/hls-semantic-tokens-plugin/test/testdata/before_9_10/TUnicodeSyntax.hs @@ -0,0 +1,5 @@ +module TUnicodeSyntax where + +a𐐀b = "a𐐀b" + + diff --git a/plugins/hls-semantic-tokens-plugin/test/testdata/before_9_10/TValBind.expected b/plugins/hls-semantic-tokens-plugin/test/testdata/before_9_10/TValBind.expected new file mode 100644 index 0000000000..ec20b01e56 --- /dev/null +++ b/plugins/hls-semantic-tokens-plugin/test/testdata/before_9_10/TValBind.expected @@ -0,0 +1,4 @@ +4:1-6 TVariable "hello" +4:10-13 TTypeConstructor "Int" +5:1-6 TVariable "hello" +5:9-15 TClassMethod "length" diff --git a/plugins/hls-semantic-tokens-plugin/test/testdata/before_9_10/TValBind.hs b/plugins/hls-semantic-tokens-plugin/test/testdata/before_9_10/TValBind.hs new file mode 100644 index 0000000000..506af37a42 --- /dev/null +++ b/plugins/hls-semantic-tokens-plugin/test/testdata/before_9_10/TValBind.hs @@ -0,0 +1,8 @@ +module TValBind where + + +hello :: Int +hello = length "Hello, Haskell!" + + + diff --git a/plugins/hls-splice-plugin/LICENSE b/plugins/hls-splice-plugin/LICENSE deleted file mode 100644 index 261eeb9e9f..0000000000 --- a/plugins/hls-splice-plugin/LICENSE +++ /dev/null @@ -1,201 +0,0 @@ - Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ - - TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - - 1. Definitions. - - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. - - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. - - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. - - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. - - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. - - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. - - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). - - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. - - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution." - - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. - - 2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. - - 3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. - - 4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: - - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and - - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and - - (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. - - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. - - 5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. - - 6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. - - 7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. - - 8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. - - 9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. - - END OF TERMS AND CONDITIONS - - APPENDIX: How to apply the Apache License to your work. - - To apply the Apache License to your work, attach the following - boilerplate notice, with the fields enclosed by brackets "[]" - replaced with your own identifying information. (Don't include - the brackets!) The text should be enclosed in the appropriate - comment syntax for the file format. We also recommend that a - file or class name and description of purpose be included on the - same "printed page" as the copyright notice for easier - identification within third-party archives. - - Copyright [yyyy] [name of copyright owner] - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. diff --git a/plugins/hls-splice-plugin/hls-splice-plugin.cabal b/plugins/hls-splice-plugin/hls-splice-plugin.cabal deleted file mode 100644 index 89a8be1d6b..0000000000 --- a/plugins/hls-splice-plugin/hls-splice-plugin.cabal +++ /dev/null @@ -1,84 +0,0 @@ -cabal-version: 2.4 -name: hls-splice-plugin -version: 2.4.0.0 -synopsis: - HLS Plugin to expand TemplateHaskell Splices and QuasiQuotes - -description: - Please see the README on GitHub at - -license: Apache-2.0 -license-file: LICENSE -author: - https://github.com/haskell/haskell-language-server/contributors - -maintainer: - https://github.com/haskell/haskell-language-server/contributors - -category: Development -build-type: Simple -extra-source-files: - LICENSE - test/testdata/*.hs - test/testdata/*.yaml - -source-repository head - type: git - location: https://github.com/haskell/haskell-language-server.git - -library - -- Plugins that need exactprint have not been updated for 9.8 yet - if impl(ghc >= 9.8) - buildable: False - else - buildable: True - exposed-modules: - Ide.Plugin.Splice - Ide.Plugin.Splice.Types - - ghc-options: -Wall -Wno-unticked-promoted-constructors - hs-source-dirs: src - build-depends: - , aeson - , base >=4.12 && <5 - , containers - , dlist - , extra - , foldl - , ghc - , ghc-exactprint - , ghcide == 2.4.0.0 - , hls-plugin-api == 2.4.0.0 - , hls-refactor-plugin - , lens - , lsp - , mtl - , retrie - , syb - , text - , transformers - , unliftio-core - , unordered-containers - - default-language: Haskell2010 - default-extensions: - DataKinds - TypeOperators - -test-suite tests - if impl(ghc >= 9.8) - buildable: False - else - buildable: True - type: exitcode-stdio-1.0 - default-language: Haskell2010 - hs-source-dirs: test - main-is: Main.hs - ghc-options: -threaded -rtsopts -with-rtsopts=-N - build-depends: - , base - , filepath - , hls-splice-plugin - , hls-test-utils == 2.4.0.0 - , text - , row-types diff --git a/plugins/hls-splice-plugin/src/Ide/Plugin/Splice.hs b/plugins/hls-splice-plugin/src/Ide/Plugin/Splice.hs index 424465a636..de468e2a87 100644 --- a/plugins/hls-splice-plugin/src/Ide/Plugin/Splice.hs +++ b/plugins/hls-splice-plugin/src/Ide/Plugin/Splice.hs @@ -1,86 +1,75 @@ {-# LANGUAGE CPP #-} -{-# LANGUAGE DeriveDataTypeable #-} {-# LANGUAGE DerivingStrategies #-} {-# LANGUAGE DuplicateRecordFields #-} -{-# LANGUAGE FlexibleContexts #-} {-# LANGUAGE GADTs #-} {-# LANGUAGE LambdaCase #-} {-# LANGUAGE MagicHash #-} -{-# LANGUAGE NamedFieldPuns #-} {-# LANGUAGE OverloadedStrings #-} -{-# LANGUAGE RankNTypes #-} +{-# LANGUAGE PatternSynonyms #-} {-# LANGUAGE RecordWildCards #-} -{-# LANGUAGE ScopedTypeVariables #-} -{-# LANGUAGE TupleSections #-} -{-# LANGUAGE TypeApplications #-} {-# LANGUAGE TypeFamilies #-} {-# LANGUAGE ViewPatterns #-} -{-# LANGUAGE PatternSynonyms #-} -{-# LANGUAGE MultiParamTypeClasses #-} -{-# LANGUAGE FlexibleInstances #-} - -module Ide.Plugin.Splice - ( descriptor, - ) -where - -import Control.Applicative (Alternative ((<|>))) -import Control.Arrow ( Arrow(first) ) -import Control.Exception ( SomeException ) -import qualified Control.Foldl as L -import Control.Lens (Identity (..), ix, view, (%~), - (<&>), (^.)) -import Control.Monad ( guard, unless, forM ) -import Control.Monad.Error.Class ( MonadError(throwError) ) -import Control.Monad.Extra (eitherM) -import qualified Control.Monad.Fail as Fail -import Control.Monad.IO.Unlift ( MonadIO(..), askRunInIO ) -import Control.Monad.Trans.Class ( MonadTrans(lift) ) -import Control.Monad.Trans.Except ( ExceptT(..), runExceptT ) + +module Ide.Plugin.Splice (descriptor) where + +import Control.Applicative (Alternative ((<|>))) +import Control.Arrow (Arrow (first)) +import Control.Exception (SomeException) +import qualified Control.Foldl as L +import Control.Lens (Identity (..), ix, view, + (%~), (<&>), (^.)) +import Control.Monad (forM, guard, unless) +import Control.Monad.Error.Class (MonadError (throwError)) +import Control.Monad.Extra (eitherM) +import qualified Control.Monad.Fail as Fail +import Control.Monad.IO.Unlift (MonadIO (..), + askRunInIO) +import Control.Monad.Trans.Class (MonadTrans (lift)) +import Control.Monad.Trans.Except (ExceptT (..), + runExceptT) import Control.Monad.Trans.Maybe -import Data.Aeson hiding (Null) -import qualified Data.Bifunctor as B (first) -import Data.Foldable (Foldable (foldl')) +import Data.Aeson hiding (Null) +import qualified Data.Bifunctor as B (first) import Data.Function import Data.Generics -import qualified Data.Kind as Kinds -import Data.List (sortOn) -import Data.Maybe (fromMaybe, listToMaybe, - mapMaybe) -import qualified Data.Text as T +import qualified Data.Kind as Kinds +import Data.List (sortOn) +import Data.Maybe (fromMaybe, listToMaybe, + mapMaybe) +import qualified Data.Text as T import Development.IDE +import Development.IDE.Core.FileStore (getVersionedTextDoc) import Development.IDE.Core.PluginUtils -import Development.IDE.GHC.Compat as Compat hiding (getLoc) +import Development.IDE.GHC.Compat as Compat import Development.IDE.GHC.Compat.ExactPrint -import qualified Development.IDE.GHC.Compat.Util as Util +import qualified Development.IDE.GHC.Compat.Util as Util import Development.IDE.GHC.ExactPrint -import Language.Haskell.GHC.ExactPrint.Transform (TransformT(TransformT)) - -#if MIN_VERSION_ghc(9,4,1) - -import GHC.Data.Bag (Bag) - -#endif - import GHC.Exts +import qualified GHC.Runtime.Loader as Loader +import qualified GHC.Types.Error as Error +import Ide.Plugin.Error (PluginError (PluginInternalError)) +import Ide.Plugin.Splice.Types +import Ide.Types +import qualified Language.LSP.Protocol.Lens as J +import Language.LSP.Protocol.Message +import Language.LSP.Protocol.Types +#if !MIN_VERSION_base(4,20,0) +import Data.Foldable (Foldable (foldl')) +#endif -import GHC.Parser.Annotation (SrcSpanAnn'(..)) -import qualified GHC.Types.Error as Error +import GHC.Data.Bag (Bag) +#if MIN_VERSION_ghc(9,9,0) +import GHC.Parser.Annotation (EpAnn (..)) +#else +import GHC.Parser.Annotation (SrcSpanAnn' (..)) +#endif -import Ide.Plugin.Splice.Types -import Ide.Types -import Language.Haskell.GHC.ExactPrint (uniqueSrcSpanT) -import Language.LSP.Server -import Language.LSP.Protocol.Types -import Language.LSP.Protocol.Message -import qualified Language.LSP.Protocol.Lens as J -import Ide.Plugin.Error (PluginError(PluginInternalError)) descriptor :: PluginId -> PluginDescriptor IdeState descriptor plId = - (defaultPluginDescriptor plId) + (defaultPluginDescriptor plId "Provides a code action to evaluate a TemplateHaskell splice") { pluginCommands = commands , pluginHandlers = mkPluginHandler SMethod_TextDocumentCodeAction codeAction } @@ -103,11 +92,11 @@ expandTHSplice :: -- | Inplace? ExpandStyle -> CommandFunction IdeState ExpandSpliceParams -expandTHSplice _eStyle ideState params@ExpandSpliceParams {..} = ExceptT $ do - clientCapabilities <- getClientCapabilities +expandTHSplice _eStyle ideState _ params@ExpandSpliceParams {..} = ExceptT $ do + clientCapabilities <- pluginGetClientCapabilities rio <- askRunInIO let reportEditor :: ReportEditor - reportEditor msgTy msgs = liftIO $ rio $ sendNotification SMethod_WindowShowMessage (ShowMessageParams msgTy (T.unlines msgs)) + reportEditor msgTy msgs = liftIO $ rio $ pluginSendNotification SMethod_WindowShowMessage (ShowMessageParams msgTy (T.unlines msgs)) expandManually :: NormalizedFilePath -> ExceptT PluginError IO WorkspaceEdit expandManually fp = do mresl <- @@ -202,9 +191,9 @@ expandTHSplice _eStyle ideState params@ExpandSpliceParams {..} = ExceptT $ do pure (Right edits) case res of Nothing -> pure $ Right $ InR Null - Just (Left err) -> pure $ Left $ err + Just (Left err) -> pure $ Left err Just (Right edit) -> do - _ <- sendRequest SMethod_WorkspaceApplyEdit (ApplyWorkspaceEditParams Nothing edit) (\_ -> pure ()) + _ <- pluginSendRequest SMethod_WorkspaceApplyEdit (ApplyWorkspaceEditParams Nothing edit) (\_ -> pure ()) pure $ Right $ InR Null where @@ -216,12 +205,12 @@ setupHscEnv :: IdeState -> NormalizedFilePath -> ParsedModule - -> ExceptT PluginError IO (Annotated ParsedSource, HscEnv, DynFlags) + -> ExceptT PluginError IO (ParsedSource, HscEnv, DynFlags) setupHscEnv ideState fp pm = do hscEnvEq <- runActionE "expandTHSplice.fallback.ghcSessionDeps" ideState $ useE GhcSessionDeps fp let ps = annotateParsedSource pm - hscEnv0 = hscEnvWithImportPaths hscEnvEq + hscEnv0 = hscEnv hscEnvEq modSum = pm_mod_summary pm hscEnv <- liftIO $ setupDynFlagsForGHCiLike hscEnv0 $ ms_hspp_opts modSum pure (ps, hscEnv, hsc_dflags hscEnv) @@ -232,10 +221,10 @@ setupDynFlagsForGHCiLike env dflags = do platform = targetPlatform dflags3 dflags3a = setWays hostFullWays dflags3 dflags3b = - foldl gopt_set dflags3a $ + foldl' gopt_set dflags3a $ concatMap (wayGeneralFlags platform) hostFullWays dflags3c = - foldl gopt_unset dflags3b $ + foldl' gopt_unset dflags3b $ concatMap (wayUnsetGeneralFlags platform) hostFullWays dflags4 = dflags3c @@ -243,7 +232,7 @@ setupDynFlagsForGHCiLike env dflags = do `gopt_set` Opt_IgnoreOptimChanges `gopt_set` Opt_IgnoreHpcChanges `gopt_unset` Opt_DiagnosticsShowCaret - initializePlugins (hscSetFlags dflags4 env) + Loader.initializePlugins (hscSetFlags dflags4 env) adjustToRange :: Uri -> Range -> WorkspaceEdit -> WorkspaceEdit adjustToRange uri ran (WorkspaceEdit mhult mlt x) = @@ -254,7 +243,7 @@ adjustToRange uri ran (WorkspaceEdit mhult mlt x) = let minStart = case L.fold (L.premap (view J.range) L.minimum) eds of Nothing -> error "impossible" - Just v -> v + Just v -> v in adjustLine minStart <$> eds adjustATextEdits :: Traversable f => f (TextEdit |? AnnotatedTextEdit) -> f (TextEdit |? AnnotatedTextEdit) @@ -282,8 +271,13 @@ adjustToRange uri ran (WorkspaceEdit mhult mlt x) = -- `GenLocated`. In GHC >= 9.2 this will be a SrcSpanAnn', with annotations; -- earlier it will just be a plain `SrcSpan`. {-# COMPLETE AsSrcSpan #-} +#if MIN_VERSION_ghc(9,9,0) +pattern AsSrcSpan :: SrcSpan -> EpAnn ann +pattern AsSrcSpan locA <- (getLoc -> locA) +#else pattern AsSrcSpan :: SrcSpan -> SrcSpanAnn' a pattern AsSrcSpan locA <- SrcSpanAnn {locA} +#endif findSubSpansDesc :: SrcSpan -> [(LHsExpr GhcTc, a)] -> [(SrcSpan, a)] findSubSpansDesc srcSpan = @@ -298,11 +292,9 @@ data SpliceClass where OneToOneAST :: HasSplice AnnListItem ast => Proxy# ast -> SpliceClass IsHsDecl :: SpliceClass -#if MIN_VERSION_ghc(9,5,0) data HsSpliceCompat pass = UntypedSplice (HsUntypedSplice pass) | TypedSplice (LHsExpr pass) -#endif class (Outputable (ast GhcRn), ASTElement l (ast GhcPs)) => HasSplice l ast where @@ -311,43 +303,24 @@ class (Outputable (ast GhcRn), ASTElement l (ast GhcPs)) => HasSplice l ast wher expandSplice :: Proxy# ast -> SpliceOf ast GhcPs -> RnM (Either (ast GhcPs) (ast GhcRn), FreeVars) instance HasSplice AnnListItem HsExpr where -#if MIN_VERSION_ghc(9,5,0) type SpliceOf HsExpr = HsSpliceCompat matchSplice _ (HsUntypedSplice _ spl) = Just (UntypedSplice spl) - matchSplice _ (HsTypedSplice _ spl) = Just (TypedSplice spl) -#else - type SpliceOf HsExpr = HsSplice - matchSplice _ (HsSpliceE _ spl) = Just spl -#endif - matchSplice _ _ = Nothing -#if MIN_VERSION_ghc(9,5,0) + matchSplice _ (HsTypedSplice _ spl) = Just (TypedSplice spl) + matchSplice _ _ = Nothing expandSplice _ (UntypedSplice e) = fmap (first Right) $ rnUntypedSpliceExpr e expandSplice _ (TypedSplice e) = fmap (first Right) $ rnTypedSplice e -#else - expandSplice _ = fmap (first Right) . rnSpliceExpr -#endif instance HasSplice AnnListItem Pat where -#if MIN_VERSION_ghc(9,5,0) type SpliceOf Pat = HsUntypedSplice -#else - type SpliceOf Pat = HsSplice -#endif matchSplice _ (SplicePat _ spl) = Just spl matchSplice _ _ = Nothing expandSplice _ = -#if MIN_VERSION_ghc(9,5,0) fmap (first (Left . unLoc . utsplice_result . snd )) . -#endif rnSplicePat instance HasSplice AnnListItem HsType where -#if MIN_VERSION_ghc(9,5,0) type SpliceOf HsType = HsUntypedSplice -#else - type SpliceOf HsType = HsSplice -#endif matchSplice _ (HsSpliceTy _ spl) = Just spl matchSplice _ _ = Nothing expandSplice _ = fmap (first Right) . rnSpliceType @@ -365,7 +338,7 @@ manualCalcEdit :: ClientCapabilities -> ReportEditor -> Range -> - Annotated ParsedSource -> + ParsedSource -> HscEnv -> TcGblEnv -> RealSrcSpan -> @@ -403,7 +376,7 @@ manualCalcEdit clientCapabilities reportEditor ran ps hscEnv typechkd srcSpan _e (fst <$> expandSplice astP spl) ) Just <$> case eExpr of - Left x -> pure $ L _spn x + Left x -> pure $ L _spn x Right y -> unRenamedE dflags y _ -> pure Nothing let (warns, errs) = @@ -422,14 +395,8 @@ manualCalcEdit clientCapabilities reportEditor ran ps hscEnv typechkd srcSpan _e pure resl where dflags = hsc_dflags hscEnv - -#if MIN_VERSION_ghc(9,4,1) showErrors = showBag -#else - showErrors = show -#endif -#if MIN_VERSION_ghc(9,4,1) showBag :: Error.Diagnostic a => Bag (Error.MsgEnvelope a) -> String showBag = show . fmap (fmap toDiagnosticMessage) @@ -437,15 +404,12 @@ toDiagnosticMessage :: forall a. Error.Diagnostic a => a -> Error.DiagnosticMess toDiagnosticMessage message = Error.DiagnosticMessage { diagMessage = Error.diagnosticMessage -#if MIN_VERSION_ghc(9,5,0) (Error.defaultDiagnosticOpts @a) -#endif message , diagReason = Error.diagnosticReason message , diagHints = Error.diagnosticHints message } -#endif -- | FIXME: Is thereAny "clever" way to do this exploiting TTG? unRenamedE :: @@ -462,15 +426,11 @@ unRenamedE dflags expr = do showSDoc dflags $ ppr expr pure expr' where -#if MIN_VERSION_ghc(9,4,1) showErrors = showBag . Error.getMessages -#else - showErrors = show -#endif data SearchResult r = Continue | Stop | Here r - deriving (Read, Show, Eq, Ord, Data, Typeable) + deriving (Read, Show, Eq, Ord, Data) fromSearchResult :: SearchResult a -> Maybe a fromSearchResult (Here r) = Just r @@ -480,7 +440,7 @@ fromSearchResult _ = Nothing -- TODO: Declaration Splices won't appear in HieAst; perhaps we must just use Parsed/Renamed ASTs? codeAction :: PluginMethodHandler IdeState Method_TextDocumentCodeAction codeAction state plId (CodeActionParams _ _ docId ran _) = do - verTxtDocId <- lift $ getVersionedTextDoc docId + verTxtDocId <- liftIO $ runAction "splice.codeAction.getVersionedTextDoc" state $ getVersionedTextDoc docId liftIO $ fmap (fromMaybe ( InL [])) $ runMaybeT $ do fp <- MaybeT $ pure $ uriToNormalizedFilePath $ toNormalizedUri theUri @@ -514,13 +474,9 @@ codeAction state plId (CodeActionParams _ _ docId ran _) = do (L (AsSrcSpan l@(RealSrcSpan spLoc _)) expr :: LHsExpr GhcPs) | spanIsRelevant l -> case expr of -#if MIN_VERSION_ghc(9,5,0) - HsTypedSplice{} -> Here (spLoc, Expr) + HsTypedSplice{} -> Here (spLoc, Expr) HsUntypedSplice{} -> Here (spLoc, Expr) -#else - HsSpliceE {} -> Here (spLoc, Expr) -#endif - _ -> Continue + _ -> Continue _ -> Stop ) `extQ` \case diff --git a/plugins/hls-splice-plugin/src/Ide/Plugin/Splice/Types.hs b/plugins/hls-splice-plugin/src/Ide/Plugin/Splice/Types.hs index cc17bf9c86..8652762276 100644 --- a/plugins/hls-splice-plugin/src/Ide/Plugin/Splice/Types.hs +++ b/plugins/hls-splice-plugin/src/Ide/Plugin/Splice/Types.hs @@ -1,5 +1,4 @@ {-# LANGUAGE DeriveAnyClass #-} -{-# LANGUAGE DeriveGeneric #-} {-# LANGUAGE DerivingStrategies #-} {-# LANGUAGE OverloadedStrings #-} diff --git a/plugins/hls-splice-plugin/test/Main.hs b/plugins/hls-splice-plugin/test/Main.hs index 4f57273d8e..38cbd4d5da 100644 --- a/plugins/hls-splice-plugin/test/Main.hs +++ b/plugins/hls-splice-plugin/test/Main.hs @@ -1,8 +1,5 @@ {-# LANGUAGE DuplicateRecordFields #-} -{-# LANGUAGE NamedFieldPuns #-} -{-# LANGUAGE OverloadedLabels #-} {-# LANGUAGE OverloadedStrings #-} -{-# LANGUAGE TypeOperators #-} {-# LANGUAGE ViewPatterns #-} module Main ( main @@ -10,7 +7,6 @@ module Main import Control.Monad (void) import Data.List (find) -import Data.Row import Data.Text (Text) import qualified Data.Text as T import qualified Data.Text.IO as T @@ -91,13 +87,15 @@ goldenTestWithEdit fp expect tc line col = { _start = Position 0 0 , _end = Position (fromIntegral $ length lns + 1) 1 } - waitForAllProgressDone -- cradle - waitForAllProgressDone - alt <- liftIO $ T.readFile (fp <.> "error.hs") + + void waitForDiagnostics + void waitForBuildQueue + alt <- liftIO $ T.readFile (testDataDir fp <.> "error.hs") void $ applyEdit doc $ TextEdit theRange alt - changeDoc doc [TextDocumentContentChangeEvent $ InL $ #range .== theRange - .+ #rangeLength .== Nothing - .+ #text .== alt] + changeDoc doc [TextDocumentContentChangeEvent $ InL + TextDocumentContentChangePartial {_range = theRange, _rangeLength = Nothing, _text = alt} + ] + void waitForDiagnostics -- wait for the entire build to finish void waitForBuildQueue @@ -109,7 +107,7 @@ goldenTestWithEdit fp expect tc line col = _ -> liftIO $ assertFailure "No CodeAction detected" testDataDir :: FilePath -testDataDir = "test" "testdata" +testDataDir = "plugins" "hls-splice-plugin" "test" "testdata" pointRange :: Int -> Int -> Range pointRange (subtract 1 -> fromIntegral -> line) (subtract 1 -> fromIntegral -> col) = diff --git a/plugins/hls-stan-plugin/LICENSE b/plugins/hls-stan-plugin/LICENSE deleted file mode 100644 index 261eeb9e9f..0000000000 --- a/plugins/hls-stan-plugin/LICENSE +++ /dev/null @@ -1,201 +0,0 @@ - Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ - - TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - - 1. Definitions. - - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. - - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. - - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. - - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. - - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. - - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. - - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). - - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. - - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution." - - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. - - 2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. - - 3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. - - 4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: - - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and - - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and - - (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. - - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. - - 5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. - - 6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. - - 7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. - - 8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. - - 9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. - - END OF TERMS AND CONDITIONS - - APPENDIX: How to apply the Apache License to your work. - - To apply the Apache License to your work, attach the following - boilerplate notice, with the fields enclosed by brackets "[]" - replaced with your own identifying information. (Don't include - the brackets!) The text should be enclosed in the appropriate - comment syntax for the file format. We also recommend that a - file or class name and description of purpose be included on the - same "printed page" as the copyright notice for easier - identification within third-party archives. - - Copyright [yyyy] [name of copyright owner] - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. diff --git a/plugins/hls-stan-plugin/hls-stan-plugin.cabal b/plugins/hls-stan-plugin/hls-stan-plugin.cabal deleted file mode 100644 index 51574b257e..0000000000 --- a/plugins/hls-stan-plugin/hls-stan-plugin.cabal +++ /dev/null @@ -1,84 +0,0 @@ -cabal-version: 2.4 -name: hls-stan-plugin -version: 2.4.0.0 -synopsis: Stan integration plugin with Haskell Language Server -description: - Please see the README on GitHub at - -license: Apache-2.0 -license-file: LICENSE -author: The Haskell IDE Team -maintainer: uhbif19@gmail.com -copyright: The Haskell IDE Team -category: Development -build-type: Simple -extra-source-files: - LICENSE - test/testdata/*.hs - -source-repository head - type: git - location: https://github.com/haskell/haskell-language-server.git - -flag pedantic - description: Enable -Werror - default: False - manual: True - -library - if (impl(ghc > 8.8.1) && impl(ghc <= 9.2.3) || impl(ghc >= 9.4.0) && impl(ghc < 9.10.0)) - buildable: True - else - buildable: False - exposed-modules: Ide.Plugin.Stan - hs-source-dirs: src - build-depends: - base - , containers - , data-default - , deepseq - , hashable - , hie-compat - , hls-plugin-api - , ghc - , ghcide - , lsp-types - , text - , transformers - , unordered-containers - , stan - - default-language: Haskell2010 - default-extensions: - LambdaCase - NamedFieldPuns - DeriveGeneric - TypeFamilies - StandaloneDeriving - DuplicateRecordFields - OverloadedStrings - -test-suite test - if (impl(ghc > 8.8.1) && impl(ghc <= 9.2.3) || impl(ghc >= 9.4.0) && impl(ghc < 9.10.0)) - buildable: True - else - buildable: False - type: exitcode-stdio-1.0 - default-language: Haskell2010 - hs-source-dirs: test - main-is: Main.hs - ghc-options: -threaded -rtsopts -with-rtsopts=-N - build-depends: - aeson - , base - , containers - , filepath - , hls-stan-plugin - , hls-plugin-api - , hls-test-utils == 2.4.0.0 - , lens - , lsp-types - , text - default-extensions: - NamedFieldPuns - OverloadedStrings diff --git a/plugins/hls-stan-plugin/src/Ide/Plugin/Stan.hs b/plugins/hls-stan-plugin/src/Ide/Plugin/Stan.hs index c44805df7a..a1efb7f150 100644 --- a/plugins/hls-stan-plugin/src/Ide/Plugin/Stan.hs +++ b/plugins/hls-stan-plugin/src/Ide/Plugin/Stan.hs @@ -1,60 +1,98 @@ +{-# LANGUAGE CPP #-} +{-# LANGUAGE PatternSynonyms #-} module Ide.Plugin.Stan (descriptor, Log) where -import Compat.HieTypes (HieASTs, HieFile) -import Control.DeepSeq (NFData) -import Control.Monad (void) -import Control.Monad.IO.Class (liftIO) -import Control.Monad.Trans.Class (lift) -import Control.Monad.Trans.Maybe (MaybeT (MaybeT), runMaybeT) -import Data.Default -import Data.Foldable (toList) -import Data.Hashable (Hashable) -import qualified Data.HashMap.Strict as HM -import qualified Data.Map as Map -import Data.Maybe (fromJust, mapMaybe) -import qualified Data.Text as T +import Compat.HieTypes (HieFile (..)) +import Control.DeepSeq (NFData) +import Control.Monad (void) +import Control.Monad.IO.Class (liftIO) +import Data.Foldable (toList) +import Data.Hashable (Hashable) +import qualified Data.HashMap.Strict as HM +import Data.Maybe (mapMaybe) +import qualified Data.Text as T import Development.IDE -import Development.IDE (Diagnostic (_codeDescription)) -import Development.IDE.Core.Rules (getHieFile, - getSourceFileSource) -import Development.IDE.Core.RuleTypes (HieAstResult (..)) -import qualified Development.IDE.Core.Shake as Shake -import Development.IDE.GHC.Compat (HieASTs (HieASTs), - RealSrcSpan (..), mkHieFile', - mkRealSrcLoc, mkRealSrcSpan, - runHsc, srcSpanEndCol, - srcSpanEndLine, - srcSpanStartCol, - srcSpanStartLine, tcg_exports) -import Development.IDE.GHC.Error (realSrcSpanToRange) -import GHC.Generics (Generic) -import Ide.Plugin.Config -import Ide.Types (PluginDescriptor (..), - PluginId, configHasDiagnostics, - defaultConfigDescriptor, - defaultPluginDescriptor, - pluginEnabledConfig) -import qualified Language.LSP.Protocol.Types as LSP -import Stan.Analysis (Analysis (..), runAnalysis) -import Stan.Category (Category (..)) -import Stan.Core.Id (Id (..)) -import Stan.Inspection (Inspection (..)) -import Stan.Inspection.All (inspectionsIds, inspectionsMap) -import Stan.Observation (Observation (..)) +import Development.IDE.Core.Rules (getHieFile) +import qualified Development.IDE.Core.Shake as Shake +import GHC.Generics (Generic) +import Ide.Plugin.Config (PluginConfig (..)) +import Ide.Types (PluginDescriptor (..), PluginId, + configHasDiagnostics, + configInitialGenericConfig, + defaultConfigDescriptor, + defaultPluginDescriptor) +import qualified Language.LSP.Protocol.Types as LSP +import Stan (createCabalExtensionsMap, + getStanConfig) +import Stan.Analysis (Analysis (..), runAnalysis) +import Stan.Category (Category (..)) +import Stan.Cli (StanArgs (..)) +import Stan.Config (Config, ConfigP (..), applyConfig) +import Stan.Config.Pretty (prettyConfigCli) +import Stan.Core.Id (Id (..)) +import Stan.EnvVars (EnvVars (..), envVarsToText) +import Stan.Inspection (Inspection (..)) +import Stan.Inspection.All (inspectionsIds, inspectionsMap) +import Stan.Observation (Observation (..)) +import Stan.Report.Settings (OutputSettings (..), + ToggleSolution (..), + Verbosity (..)) +import Stan.Toml (usedTomlFiles) +import System.Directory (makeRelativeToCurrentDirectory) +import Trial (Fatality, Trial (..), fiasco, + pattern FiascoL, pattern ResultL, + prettyTrial, prettyTrialWith) descriptor :: Recorder (WithPriority Log) -> PluginId -> PluginDescriptor IdeState -descriptor recorder plId = (defaultPluginDescriptor plId) +descriptor recorder plId = (defaultPluginDescriptor plId desc) { pluginRules = rules recorder plId - , pluginConfigDescriptor = defaultConfigDescriptor + , pluginConfigDescriptor = defConfigDescriptor { configHasDiagnostics = True + -- We disable this plugin by default because users have been complaining about + -- the diagnostics, see https://github.com/haskell/haskell-language-server/issues/3916 + , configInitialGenericConfig = (configInitialGenericConfig defConfigDescriptor) + { plcGlobalOn = False + } } } + where + defConfigDescriptor = defaultConfigDescriptor + desc = "Provides stan diagnostics. Built with stan-" <> VERSION_stan + +data Log = LogShake !Shake.Log + | LogWarnConf ![(Fatality, T.Text)] + | LogDebugStanConfigResult ![FilePath] !(Trial T.Text Config) + | LogDebugStanEnvVars !EnvVars -newtype Log = LogShake Shake.Log deriving (Show) +-- We use this function to remove the terminal escape sequences emmited by Trial pretty printing functions. +-- See https://github.com/kowainik/trial/pull/73#issuecomment-1868233235 +stripModifiers :: T.Text -> T.Text +stripModifiers = go "" + where + go acc txt = + case T.findIndex (== '\x1B') txt of + Nothing -> acc <> txt + Just index -> let (beforeEsc, afterEsc) = T.splitAt index txt + in go (acc <> beforeEsc) (consumeEscapeSequence afterEsc) + consumeEscapeSequence :: T.Text -> T.Text + consumeEscapeSequence txt = + case T.findIndex (== 'm') txt of + Nothing -> txt + Just index -> T.drop (index + 1) txt instance Pretty Log where pretty = \case LogShake log -> pretty log + LogWarnConf errs -> "Fiasco encountered when trying to load stan configuration. Using default inspections:" + <> line <> (pretty $ show errs) + LogDebugStanConfigResult fps t -> "Config result using: " + <> pretty fps <> line <> pretty (stripModifiers $ prettyTrialWith (T.unpack . prettyConfigCli) t) + LogDebugStanEnvVars envVars -> "EnvVars " <> + case envVars of + EnvVars trial@(FiascoL _) -> pretty (stripModifiers $ prettyTrial trial) + + -- if the envVars are not set, 'envVarsToText returns an empty string' + _ -> "found: " <> (pretty $ envVarsToText envVars) data GetStanDiagnostics = GetStanDiagnostics deriving (Eq, Show, Generic) @@ -70,14 +108,56 @@ rules recorder plId = do define (cmapWithPrio LogShake recorder) $ \GetStanDiagnostics file -> do config <- getPluginConfigAction plId - if pluginEnabledConfig plcDiagnosticsOn config then do + if plcGlobalOn config && plcDiagnosticsOn config then do maybeHie <- getHieFile file case maybeHie of Nothing -> return ([], Nothing) Just hie -> do - let enabledInspections = HM.fromList [(LSP.fromNormalizedFilePath file, inspectionsIds)] - -- This should use Cabal config for extensions and Stan config for inspection preferences is the future - let analysis = runAnalysis Map.empty enabledInspections [] [hie] + let isLoud = False -- in Stan: notJson = not isLoud + let stanArgs = + StanArgs + { stanArgsHiedir = "" -- :: !FilePath -- ^ Directory with HIE files + , stanArgsCabalFilePath = [] -- :: ![FilePath] -- ^ Path to @.cabal@ files. + , stanArgsOutputSettings = OutputSettings NonVerbose ShowSolution -- :: !OutputSettings -- ^ Settings for output terminal report + -- doesnt matter, because it is silenced by isLoud + , stanArgsReport = Nothing -- :: !(Maybe ReportArgs) -- ^ @HTML@ report settings + , stanArgsUseDefaultConfigFile = fiasco "" -- :: !(TaggedTrial Text Bool) -- ^ Use default @.stan.toml@ file + , stanArgsConfigFile = Nothing -- :: !(Maybe FilePath) -- ^ Path to a custom configurations file. + , stanArgsConfig = ConfigP + { configChecks = fiasco "'hls-stan-plugin' doesn't receive CLI options for: checks" + , configRemoved = fiasco "'hls-stan-plugin' doesn't receive CLI options for: remove" + , configIgnored = fiasco "'hls-stan-plugin' doesn't receive CLI options for: ignore" + } + -- if they are not fiascos, .stan.toml's aren't taken into account + ,stanArgsJsonOut = not isLoud -- :: !Bool -- ^ Output the machine-readable output in JSON format instead. + } + + (configTrial, useDefConfig, env) <- liftIO $ getStanConfig stanArgs isLoud + tomlsUsedByStan <- liftIO $ usedTomlFiles useDefConfig (stanArgsConfigFile stanArgs) + logWith recorder Debug (LogDebugStanConfigResult tomlsUsedByStan configTrial) + + -- If envVar is set to 'False', stan will ignore all local and global .stan.toml files + logWith recorder Debug (LogDebugStanEnvVars env) + + -- Note that Stan works in terms of relative paths, but the HIE come in as absolute. Without + -- making its path relative, the file name(s) won't line up with the associated Map keys. + relativeHsFilePath <- liftIO $ makeRelativeToCurrentDirectory $ fromNormalizedFilePath file + let hieRelative = hie{hie_hs_file=relativeHsFilePath} + + (checksMap, ignoredObservations) <- case configTrial of + FiascoL es -> do + logWith recorder Development.IDE.Warning (LogWarnConf es) + -- If we can't read the config file, default to using all inspections: + let allInspections = HM.singleton relativeHsFilePath inspectionsIds + pure (allInspections, []) + ResultL _warnings stanConfig -> do + -- HashMap of *relative* file paths to info about enabled checks for those file paths. + let checksMap = applyConfig [relativeHsFilePath] stanConfig + pure (checksMap, configIgnored stanConfig) + + -- A Map from *relative* file paths (just one, in this case) to language extension info: + cabalExtensionsMap <- liftIO $ createCabalExtensionsMap isLoud (stanArgsCabalFilePath stanArgs) [hieRelative] + let analysis = runAnalysis cabalExtensionsMap checksMap ignoredObservations [hieRelative] return (analysisToDiagnostics file analysis, Just ()) else return ([], Nothing) @@ -107,17 +187,18 @@ rules recorder plId = do "Possible solutions:" ] ++ map (" - " <>) (inspectionSolution inspection) - return ( file, - ShowDiag, - LSP.Diagnostic - { _range = realSrcSpanToRange observationSrcSpan, - _severity = Just LSP.DiagnosticSeverity_Hint, - _code = Just (LSP.InR $ unId (inspectionId inspection)), - _source = Just "stan", - _message = message, - _relatedInformation = Nothing, - _tags = Nothing, - _codeDescription = Nothing, - _data_ = Nothing - } - ) + return $ + ideErrorFromLspDiag + LSP.Diagnostic + { _range = realSrcSpanToRange observationSrcSpan, + _severity = Just LSP.DiagnosticSeverity_Hint, + _code = Just (LSP.InR $ unId (inspectionId inspection)), + _source = Just "stan", + _message = message, + _relatedInformation = Nothing, + _tags = Nothing, + _codeDescription = Nothing, + _data_ = Nothing + } + file + Nothing diff --git a/plugins/hls-stan-plugin/test/Main.hs b/plugins/hls-stan-plugin/test/Main.hs index 6c27e399d3..231707d142 100644 --- a/plugins/hls-stan-plugin/test/Main.hs +++ b/plugins/hls-stan-plugin/test/Main.hs @@ -4,12 +4,9 @@ module Main where import Control.Lens ((^.)) -import Control.Monad (void) -import Data.List (find) -import Data.Text (Text) import qualified Data.Text as T -import qualified Data.Text.IO as T import qualified Ide.Plugin.Stan as Stan +import Ide.Types import qualified Language.LSP.Protocol.Lens as L import System.FilePath import Test.Hls @@ -33,14 +30,56 @@ tests = assertBool "" $ T.isPrefixOf expectedPrefix (reduceDiag ^. L.message) reduceDiag ^. L.source @?= Just "stan" return () + , testCase "ignores diagnostics from .stan.toml" $ + runStanSession "" $ do + doc <- openDoc ("dir" "configTest.hs") "haskell" + diags <- waitForDiagnosticsFromSource doc "stan" + liftIO $ length diags @?= 0 + return () + , testCase "respects LANGUAGE pragmas in the source file" $ + runStanSession "" $ do + doc <- openDoc ("extensions-language-pragma" "LanguagePragmaTest.hs") "haskell" + diags <- waitForDiagnosticsFromSource doc "stan" + -- We must include at least one valid diagnostic in our test file to avoid + -- the false-positive case where Stan finds no analyses to perform due to a + -- bad mapping, which would also lead to zero diagnostics being returned. + liftIO $ length diags @?= 1 + return () + , testCase "respects language extensions defined in the .cabal file" $ + runStanSession "" $ do + doc <- openDoc ("extensions-cabal-file" "CabalFileTest.hs") "haskell" + diags <- waitForDiagnosticsFromSource doc "stan" + -- We need at least one valid diagnostic here too, for the same reason as above. + liftIO $ length diags @?= 1 + return () ] testDir :: FilePath -testDir = "test/testdata" +testDir = "plugins" "hls-stan-plugin" "test" "testdata" stanPlugin :: PluginTestDescriptor Stan.Log -stanPlugin = mkPluginTestDescriptor Stan.descriptor "stan" +stanPlugin = mkPluginTestDescriptor enabledStanDescriptor "stan" + where + -- We have to explicitly enable the plugin as it is disabled by default as + -- per request: https://github.com/haskell/haskell-language-server/issues/3916 + -- + enabledStanDescriptor recorder plId = + let stanPluginDescriptor = Stan.descriptor recorder plId + in stanPluginDescriptor + { pluginConfigDescriptor = (pluginConfigDescriptor stanPluginDescriptor) + { configInitialGenericConfig = (configInitialGenericConfig (pluginConfigDescriptor stanPluginDescriptor)) + { plcGlobalOn = True + } + } + } runStanSession :: FilePath -> Session a -> IO a runStanSession subdir = - failIfSessionTimeout . runSessionWithServer def stanPlugin (testDir subdir) + failIfSessionTimeout + . runSessionWithTestConfig def{ + testConfigCaps=codeActionNoResolveCaps + , testShiftRoot=True + , testPluginDescriptor=stanPlugin + , testDirLocation=Left (testDir subdir) + } + . const diff --git a/plugins/hls-stan-plugin/test/testdata/.hie/Main.hie b/plugins/hls-stan-plugin/test/testdata/.hie/Main.hie deleted file mode 100644 index 0c7367ab46..0000000000 Binary files a/plugins/hls-stan-plugin/test/testdata/.hie/Main.hie and /dev/null differ diff --git a/plugins/hls-stan-plugin/test/testdata/.stan.toml b/plugins/hls-stan-plugin/test/testdata/.stan.toml new file mode 100644 index 0000000000..ce73b7f29c --- /dev/null +++ b/plugins/hls-stan-plugin/test/testdata/.stan.toml @@ -0,0 +1,22 @@ +# See https://github.com/kowainik/stan/issues/531 +# Unix +[[check]] +type = "Exclude" +id = "STAN-0103" +file = "dir/configTest.hs" + +[[check]] +type = "Exclude" +id = "STAN-0212" +directory = "dir/" + +# Windows +[[check]] +type = "Exclude" +id = "STAN-0103" +file = "dir\\configTest.hs" + +[[check]] +type = "Exclude" +id = "STAN-0212" +directory = "dir\\" diff --git a/plugins/hls-stan-plugin/test/testdata/dir/configTest.hs b/plugins/hls-stan-plugin/test/testdata/dir/configTest.hs new file mode 100644 index 0000000000..add256058b --- /dev/null +++ b/plugins/hls-stan-plugin/test/testdata/dir/configTest.hs @@ -0,0 +1,3 @@ +a = length [1..] + +b = undefined diff --git a/plugins/hls-stan-plugin/test/testdata/extensions-cabal-file/CabalFileTest.hs b/plugins/hls-stan-plugin/test/testdata/extensions-cabal-file/CabalFileTest.hs new file mode 100644 index 0000000000..77b6dc3845 --- /dev/null +++ b/plugins/hls-stan-plugin/test/testdata/extensions-cabal-file/CabalFileTest.hs @@ -0,0 +1,7 @@ +module CabalFileTest () where + +-- With `StrictData` enabled in the `.cabal` file, Stan shouldn't complain here: +data A = A Int Int + +-- ...but it should still complain here! +kewlFunc = undefined diff --git a/plugins/hls-stan-plugin/test/testdata/extensions-cabal-file/cabal-file-test.cabal b/plugins/hls-stan-plugin/test/testdata/extensions-cabal-file/cabal-file-test.cabal new file mode 100644 index 0000000000..094f06d1dd --- /dev/null +++ b/plugins/hls-stan-plugin/test/testdata/extensions-cabal-file/cabal-file-test.cabal @@ -0,0 +1,9 @@ +cabal-version: 3.0 +name: cabal-file-test +version: 0.0.0.0 + +library + exposed-modules: CabalFileTest + hs-source-dirs: extensions-cabal-file + -- Specifically, we're testing that Stan respects the following extension definition: + default-extensions: StrictData diff --git a/plugins/hls-stan-plugin/test/testdata/extensions-language-pragma/LanguagePragmaTest.hs b/plugins/hls-stan-plugin/test/testdata/extensions-language-pragma/LanguagePragmaTest.hs new file mode 100644 index 0000000000..6f5631ac8c --- /dev/null +++ b/plugins/hls-stan-plugin/test/testdata/extensions-language-pragma/LanguagePragmaTest.hs @@ -0,0 +1,9 @@ +{-# LANGUAGE StrictData #-} + +module LanguagePragmaTest () where + +-- With the above `StrictData` language pragma, Stan shouldn't complain here: +data A = A Int Int + +-- ...but it should still complain here! +kewlFunc = undefined diff --git a/plugins/hls-stan-plugin/test/testdata/extensions-language-pragma/language-pragma-test.cabal b/plugins/hls-stan-plugin/test/testdata/extensions-language-pragma/language-pragma-test.cabal new file mode 100644 index 0000000000..336388d4fa --- /dev/null +++ b/plugins/hls-stan-plugin/test/testdata/extensions-language-pragma/language-pragma-test.cabal @@ -0,0 +1,11 @@ +cabal-version: 3.0 +name: language-pragma-test +version: 0.0.0.0 + +-- Without at least a minimal valid `.cabal` file, Stan won't bother building its +-- map of language extensions. This means it also won't detect LANGUAGE pragmas +-- without this file. + +library + exposed-modules: LanguagePragmaTest + hs-source-dirs: extensions-language-pragma diff --git a/plugins/hls-stylish-haskell-plugin/LICENSE b/plugins/hls-stylish-haskell-plugin/LICENSE deleted file mode 100644 index 16502c47e2..0000000000 --- a/plugins/hls-stylish-haskell-plugin/LICENSE +++ /dev/null @@ -1,201 +0,0 @@ - Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ - - TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - - 1. Definitions. - - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. - - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. - - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. - - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. - - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. - - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. - - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). - - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. - - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution." - - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. - - 2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. - - 3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. - - 4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: - - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and - - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and - - (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. - - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. - - 5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. - - 6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. - - 7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. - - 8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. - - 9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. - - END OF TERMS AND CONDITIONS - - APPENDIX: How to apply the Apache License to your work. - - To apply the Apache License to your work, attach the following - boilerplate notice, with the fields enclosed by brackets "[]" - replaced with your own identifying information. (Don't include - the brackets!) The text should be enclosed in the appropriate - comment syntax for the file format. We also recommend that a - file or class name and description of purpose be included on the - same "printed page" as the copyright notice for easier - identification within third-party archives. - - Copyright 2021 The Haskell IDE team - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. diff --git a/plugins/hls-stylish-haskell-plugin/hls-stylish-haskell-plugin.cabal b/plugins/hls-stylish-haskell-plugin/hls-stylish-haskell-plugin.cabal deleted file mode 100644 index 3087806a98..0000000000 --- a/plugins/hls-stylish-haskell-plugin/hls-stylish-haskell-plugin.cabal +++ /dev/null @@ -1,59 +0,0 @@ -cabal-version: 2.4 -name: hls-stylish-haskell-plugin -version: 2.4.0.0 -synopsis: Integration with the Stylish Haskell code formatter -description: - Please see the README on GitHub at -license: Apache-2.0 -license-file: LICENSE -author: The Haskell IDE Team -copyright: The Haskell IDE Team -maintainer: alan.zimm@gmail.com -category: Development -build-type: Simple -extra-source-files: - LICENSE - test/testdata/*.hs - -source-repository head - type: git - location: https://github.com/haskell/haskell-language-server.git - -library - -- Plugins that need exactprint have not been updated for 9.8 yet - if impl(ghc >= 9.8) - buildable: False - else - buildable: True - exposed-modules: Ide.Plugin.StylishHaskell - hs-source-dirs: src - build-depends: - , base >=4.12 && <5 - , directory - , filepath - , ghc - , ghc-boot-th - , ghcide == 2.4.0.0 - , hls-plugin-api == 2.4.0.0 - , lsp-types - , mtl - , stylish-haskell ^>=0.12 || ^>=0.13 || ^>=0.14.2 - , text - - default-language: Haskell2010 - -test-suite tests - if impl(ghc >= 9.8) - buildable: False - else - buildable: True - type: exitcode-stdio-1.0 - default-language: Haskell2010 - hs-source-dirs: test - main-is: Main.hs - ghc-options: -threaded -rtsopts -with-rtsopts=-N - build-depends: - , base - , filepath - , hls-stylish-haskell-plugin - , hls-test-utils == 2.4.0.0 diff --git a/plugins/hls-stylish-haskell-plugin/src/Ide/Plugin/StylishHaskell.hs b/plugins/hls-stylish-haskell-plugin/src/Ide/Plugin/StylishHaskell.hs index 9a2aff6908..767cc061df 100644 --- a/plugins/hls-stylish-haskell-plugin/src/Ide/Plugin/StylishHaskell.hs +++ b/plugins/hls-stylish-haskell-plugin/src/Ide/Plugin/StylishHaskell.hs @@ -1,8 +1,11 @@ +{-# LANGUAGE CPP #-} +{-# LANGUAGE LambdaCase #-} {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE ViewPatterns #-} module Ide.Plugin.StylishHaskell ( descriptor , provider + , Log ) where @@ -10,7 +13,8 @@ import Control.Monad.Except (throwError) import Control.Monad.IO.Class import Data.Text (Text) import qualified Data.Text as T -import Development.IDE hiding (pluginHandlers) +import Development.IDE hiding (getExtensions, + pluginHandlers) import Development.IDE.Core.PluginUtils import Development.IDE.GHC.Compat (ModSummary (ms_hspp_opts), extensionFlags) @@ -24,16 +28,26 @@ import Language.LSP.Protocol.Types as LSP import System.Directory import System.FilePath -descriptor :: PluginId -> PluginDescriptor IdeState -descriptor plId = (defaultPluginDescriptor plId) - { pluginHandlers = mkFormattingHandlers provider +data Log + = LogLanguageExtensionFromDynFlags + +instance Pretty Log where + pretty = \case + LogLanguageExtensionFromDynFlags -> "stylish-haskell uses the language extensions from DynFlags" + + +descriptor :: Recorder (WithPriority Log) -> PluginId -> PluginDescriptor IdeState +descriptor recorder plId = (defaultPluginDescriptor plId desc) + { pluginHandlers = mkFormattingHandlers (provider recorder) } + where + desc = "Provides formatting of Haskell files via stylish-haskell. Built with stylish-haskell-" <> VERSION_stylish_haskell -- | Formatter provider of stylish-haskell. -- Formats the given source in either a given Range or the whole Document. -- If the provider fails an error is returned that can be displayed to the user. -provider :: FormattingHandler IdeState -provider ide typ contents fp _opts = do +provider :: Recorder (WithPriority Log) -> FormattingHandler IdeState +provider recorder ide _token typ contents fp _opts = do (msrModSummary -> ms_hspp_opts -> dyn) <- runActionE "stylish-haskell" ide $ useE GetModSummary fp let file = fromNormalizedFilePath fp config <- liftIO $ loadConfigFrom file @@ -49,7 +63,7 @@ provider ide typ contents fp _opts = do getMergedConfig dyn config | null (configLanguageExtensions config) = do - logInfo (ideLogger ide) "stylish-haskell uses the language extensions from DynFlags" + logWith recorder Info LogLanguageExtensionFromDynFlags pure $ config { configLanguageExtensions = getExtensions dyn } @@ -65,10 +79,15 @@ provider ide typ contents fp _opts = do -- If no such file has been found, return default config. loadConfigFrom :: FilePath -> IO Config loadConfigFrom file = do +#if MIN_VERSION_stylish_haskell(0,15,0) + let configSearchStrategy = SearchFromDirectory (takeDirectory file) + config <- loadConfig (makeVerbose False) configSearchStrategy +#else currDir <- getCurrentDirectory setCurrentDirectory (takeDirectory file) config <- loadConfig (makeVerbose False) Nothing setCurrentDirectory currDir +#endif pure config -- | Run stylish-haskell on the given text with the given configuration. diff --git a/plugins/hls-stylish-haskell-plugin/test/Main.hs b/plugins/hls-stylish-haskell-plugin/test/Main.hs index 9dadebf598..22e9499947 100644 --- a/plugins/hls-stylish-haskell-plugin/test/Main.hs +++ b/plugins/hls-stylish-haskell-plugin/test/Main.hs @@ -10,8 +10,8 @@ import Test.Hls main :: IO () main = defaultTestRunner tests -stylishHaskellPlugin :: PluginTestDescriptor () -stylishHaskellPlugin = mkPluginTestDescriptor' StylishHaskell.descriptor "stylishHaskell" +stylishHaskellPlugin :: PluginTestDescriptor StylishHaskell.Log +stylishHaskellPlugin = mkPluginTestDescriptor StylishHaskell.descriptor "stylishHaskell" tests :: TestTree tests = testGroup "stylish-haskell" @@ -25,4 +25,4 @@ goldenWithStylishHaskell :: TestName -> FilePath -> FilePath -> (TextDocumentIde goldenWithStylishHaskell title fp desc = goldenWithHaskellDocFormatter def stylishHaskellPlugin "stylishHaskell" def title testDataDir fp desc "hs" testDataDir :: FilePath -testDataDir = "test" "testdata" +testDataDir = "plugins" "hls-stylish-haskell-plugin" "test" "testdata" diff --git a/release/update_versions.sh b/release/update_versions.sh new file mode 100755 index 0000000000..ac9e9c752c --- /dev/null +++ b/release/update_versions.sh @@ -0,0 +1,19 @@ +#!/usr/bin/env bash + +set -ex + +function replaceHlsVersion() { + # Update all `version:` fields + sed -ri "s/^version:( +)${1}/version:\1${2}/" ./*.cabal ./**/*.cabal + # Update all constraints expected to be in the form `== `. + # We usually don't force an exact version, so this is relatively unambiguous. + # We could introduce some more ad-hoc parsing, if there is still ambiguity. + sed -ri "s/== ${1}/== ${2}/" ./*.cabal ./**/*.cabal +} + +if [ $# -ne 2 ]; +then + echo "USAGE: ./relase/update_versions.sh " +fi + +replaceHlsVersion "${1}" "${2}" diff --git a/release/upload.sh b/release/upload.sh index 29f6849757..22dc6d438d 100755 --- a/release/upload.sh +++ b/release/upload.sh @@ -35,7 +35,7 @@ fi echo HLS version $ver -host="gitlab-storage.haskell.org" +host="gitlab.haskell.org:2222" usage() { echo "Usage: [rel_name=] SIGNING_KEY= $0 " diff --git a/scripts/release/create-yaml-snippet.sh b/scripts/release/create-yaml-snippet.sh index 2fb7413f82..6ee25b01b5 100644 --- a/scripts/release/create-yaml-snippet.sh +++ b/scripts/release/create-yaml-snippet.sh @@ -28,56 +28,61 @@ cat < /dev/stdout dlUri: https://downloads.haskell.org/~hls/haskell-language-server-$RELEASE/haskell-language-server-$RELEASE-x86_64-linux-deb10.tar.xz dlSubdir: haskell-language-server-$RELEASE dlHash: $(sha256sum "haskell-language-server-$RELEASE-x86_64-linux-deb10.tar.xz" | awk '{ print $1 }') + '(>= 11 && < 12)': &hls-${RELEASE//./}-64-deb11 + dlUri: https://downloads.haskell.org/~hls/haskell-language-server-$RELEASE/haskell-language-server-$RELEASE-x86_64-linux-deb11.tar.xz + dlSubdir: haskell-language-server-$RELEASE + dlHash: $(sha256sum "haskell-language-server-$RELEASE-x86_64-linux-deb11.tar.xz" | awk '{ print $1 }') + '>= 12': &hls-${RELEASE//./}-64-deb12 + dlUri: https://downloads.haskell.org/~hls/haskell-language-server-$RELEASE/haskell-language-server-$RELEASE-x86_64-linux-deb12.tar.xz + dlSubdir: haskell-language-server-$RELEASE + dlHash: $(sha256sum "haskell-language-server-$RELEASE-x86_64-linux-deb12.tar.xz" | awk '{ print $1 }') unknown_versioning: &hls-${RELEASE//./}-64-deb11 dlUri: https://downloads.haskell.org/~hls/haskell-language-server-$RELEASE/haskell-language-server-$RELEASE-x86_64-linux-deb11.tar.xz dlSubdir: haskell-language-server-$RELEASE dlHash: $(sha256sum "haskell-language-server-$RELEASE-x86_64-linux-deb11.tar.xz" | awk '{ print $1 }') Linux_Ubuntu: '( >= 16 && < 19 )': &hls-${RELEASE//./}-64-ubuntu18 - dlUri: https://downloads.haskell.org/~hls/haskell-language-server-$RELEASE/haskell-language-server-$RELEASE-x86_64-linux-ubuntu18.04.tar.xz + dlUri: https://downloads.haskell.org/~hls/haskell-language-server-$RELEASE/haskell-language-server-$RELEASE-x86_64-linux-ubuntu1804.tar.xz dlSubdir: haskell-language-server-$RELEASE - dlHash: $(sha256sum "haskell-language-server-$RELEASE-x86_64-linux-ubuntu18.04.tar.xz" | awk '{ print $1 }') + dlHash: $(sha256sum "haskell-language-server-$RELEASE-x86_64-linux-ubuntu1804.tar.xz" | awk '{ print $1 }') '( >= 20 && < 22 )': &hls-${RELEASE//./}-64-ubuntu20 - dlUri: https://downloads.haskell.org/~hls/haskell-language-server-$RELEASE/haskell-language-server-$RELEASE-x86_64-linux-ubuntu20.04.tar.xz + dlUri: https://downloads.haskell.org/~hls/haskell-language-server-$RELEASE/haskell-language-server-$RELEASE-x86_64-linux-ubuntu2004.tar.xz dlSubdir: haskell-language-server-$RELEASE - dlHash: $(sha256sum "haskell-language-server-$RELEASE-x86_64-linux-ubuntu20.04.tar.xz" | awk '{ print $1 }') + dlHash: $(sha256sum "haskell-language-server-$RELEASE-x86_64-linux-ubuntu2004.tar.xz" | awk '{ print $1 }') unknown_versioning: &hls-${RELEASE//./}-64-ubuntu22 - dlUri: https://downloads.haskell.org/~hls/haskell-language-server-$RELEASE/haskell-language-server-$RELEASE-x86_64-linux-ubuntu22.04.tar.xz + dlUri: https://downloads.haskell.org/~hls/haskell-language-server-$RELEASE/haskell-language-server-$RELEASE-x86_64-linux-ubuntu2204.tar.xz dlSubdir: haskell-language-server-$RELEASE - dlHash: $(sha256sum "haskell-language-server-$RELEASE-x86_64-linux-ubuntu22.04.tar.xz" | awk '{ print $1 }') + dlHash: $(sha256sum "haskell-language-server-$RELEASE-x86_64-linux-ubuntu2204.tar.xz" | awk '{ print $1 }') Linux_Mint: '< 20': - dlUri: https://downloads.haskell.org/~hls/haskell-language-server-$RELEASE/haskell-language-server-$RELEASE-x86_64-linux-mint19.3.tar.xz + dlUri: https://downloads.haskell.org/~hls/haskell-language-server-$RELEASE/haskell-language-server-$RELEASE-x86_64-linux-mint193.tar.xz dlSubdir: haskell-language-server-$RELEASE - dlHash: $(sha256sum "haskell-language-server-$RELEASE-x86_64-linux-mint19.3.tar.xz" | awk '{ print $1 }') + dlHash: $(sha256sum "haskell-language-server-$RELEASE-x86_64-linux-mint193.tar.xz" | awk '{ print $1 }') '(>= 20 && < 21)': - dlUri: https://downloads.haskell.org/~hls/haskell-language-server-$RELEASE/haskell-language-server-$RELEASE-x86_64-linux-mint20.2.tar.xz + dlUri: https://downloads.haskell.org/~hls/haskell-language-server-$RELEASE/haskell-language-server-$RELEASE-x86_64-linux-mint202.tar.xz dlSubdir: haskell-language-server-$RELEASE - dlHash: $(sha256sum "haskell-language-server-$RELEASE-x86_64-linux-mint20.2.tar.xz" | awk '{ print $1 }') - '>= 21': *hls-${RELEASE//./}-64-ubuntu22 - Linux_Fedora: - '< 33': &hls-${RELEASE//./}-64-fedora27 - dlUri: https://downloads.haskell.org/~hls/haskell-language-server-$RELEASE/haskell-language-server-$RELEASE-x86_64-linux-fedora27.tar.xz + dlHash: $(sha256sum "haskell-language-server-$RELEASE-x86_64-linux-mint202.tar.xz" | awk '{ print $1 }') + '>= 21': + dlUri: https://downloads.haskell.org/~hls/haskell-language-server-$RELEASE/haskell-language-server-$RELEASE-x86_64-linux-mint213.tar.xz dlSubdir: haskell-language-server-$RELEASE - dlHash: $(sha256sum "haskell-language-server-$RELEASE-x86_64-linux-fedora27.tar.xz" | awk '{ print $1 }') - '>= 33': &hls-${RELEASE//./}-64-fedora33 + dlHash: $(sha256sum "haskell-language-server-$RELEASE-x86_64-linux-mint213.tar.xz" | awk '{ print $1 }') + Linux_Fedora: + '(>= 33 && < 40)': &hls-${RELEASE//./}-64-fedora33 dlUri: https://downloads.haskell.org/~hls/haskell-language-server-$RELEASE/haskell-language-server-$RELEASE-x86_64-linux-fedora33.tar.xz dlSubdir: haskell-language-server-$RELEASE dlHash: $(sha256sum "haskell-language-server-$RELEASE-x86_64-linux-fedora33.tar.xz" | awk '{ print $1 }') - unknown_versioning: *hls-${RELEASE//./}-64-fedora27 - Linux_CentOS: - '( >= 7 && < 8 )': &hls-${RELEASE//./}-64-centos - dlUri: https://downloads.haskell.org/~hls/haskell-language-server-$RELEASE/haskell-language-server-$RELEASE-x86_64-linux-centos7.tar.xz + '>= 40': &hls-${RELEASE//./}-64-fedora40 + dlUri: https://downloads.haskell.org/~hls/haskell-language-server-$RELEASE/haskell-language-server-$RELEASE-x86_64-linux-fedora40.tar.xz dlSubdir: haskell-language-server-$RELEASE - dlHash: $(sha256sum "haskell-language-server-$RELEASE-x86_64-linux-centos7.tar.xz" | awk '{ print $1 }') - unknown_versioning: *hls-${RELEASE//./}-64-centos - Linux_RedHat: - unknown_versioning: *hls-${RELEASE//./}-64-centos + dlHash: $(sha256sum "haskell-language-server-$RELEASE-x86_64-linux-fedora40.tar.xz" | awk '{ print $1 }') + unknown_versioning: *hls-${RELEASE//./}-64-unknown Linux_UnknownLinux: - unknown_versioning: + unknown_versioning: &hls-${RELEASE//./}-64-unknown dlUri: https://downloads.haskell.org/~hls/haskell-language-server-$RELEASE/haskell-language-server-$RELEASE-x86_64-linux-unknown.tar.xz dlSubdir: haskell-language-server-$RELEASE dlHash: $(sha256sum "haskell-language-server-$RELEASE-x86_64-linux-unknown.tar.xz" | awk '{ print $1 }') + Linux_RedHat: + unknown_versioning: *hls-${RELEASE//./}-64-unknown Darwin: unknown_versioning: dlUri: https://downloads.haskell.org/~hls/haskell-language-server-$RELEASE/haskell-language-server-$RELEASE-x86_64-apple-darwin.tar.xz @@ -87,17 +92,12 @@ cat < /dev/stdout unknown_versioning: dlUri: https://downloads.haskell.org/~hls/haskell-language-server-$RELEASE/haskell-language-server-$RELEASE-x86_64-mingw64.zip dlHash: $(sha256sum "haskell-language-server-$RELEASE-x86_64-mingw64.zip" | awk '{ print $1 }') - FreeBSD: - unknown_versioning: - dlUri: https://downloads.haskell.org/~hls/haskell-language-server-$RELEASE/haskell-language-server-$RELEASE-x86_64-freebsd.tar.xz - dlSubdir: haskell-language-server-$RELEASE - dlHash: $(sha256sum "haskell-language-server-$RELEASE-x86_64-freebsd.tar.xz" | awk '{ print $1 }') A_ARM64: Linux_UnknownLinux: unknown_versioning: - dlUri: https://downloads.haskell.org/~hls/haskell-language-server-$RELEASE/haskell-language-server-$RELEASE-aarch64-linux-ubuntu20.tar.xz + dlUri: https://downloads.haskell.org/~hls/haskell-language-server-$RELEASE/haskell-language-server-$RELEASE-aarch64-linux-ubuntu2004.tar.xz dlSubdir: haskell-language-server-$RELEASE - dlHash: $(sha256sum "haskell-language-server-$RELEASE-aarch64-linux-ubuntu20.tar.xz" | awk '{ print $1 }') + dlHash: $(sha256sum "haskell-language-server-$RELEASE-aarch64-linux-ubuntu2004.tar.xz" | awk '{ print $1 }') Darwin: unknown_versioning: dlUri: https://downloads.haskell.org/~hls/haskell-language-server-$RELEASE/haskell-language-server-$RELEASE-aarch64-apple-darwin.tar.xz diff --git a/scripts/release/download-gh-artifacts.sh b/scripts/release/download-gh-artifacts.sh index fc6638f181..217422eedb 100644 --- a/scripts/release/download-gh-artifacts.sh +++ b/scripts/release/download-gh-artifacts.sh @@ -22,12 +22,21 @@ cd "gh-release-artifacts/haskell-language-server-${RELEASE}" # github gh release download "$RELEASE" +## We can't do cirrus releases any more, as we build HLS releases with ghcup vanilla binaries. +## Vanilla means "upstream", aka GHC HQ, and GHC HQ does not provide bindists for FreeBSD. +## Until we start using ghcup's mainstream distribution channel, we can't even begin to build +## binaries for FreeBSD. We keep this here for the next generation or when the situation changes. +## +## We don't use ghcup's mainstream distribution channel, as we only provide vanilla binaries +## as requested by the ghcup distribution channel team. # cirrus -curl --fail -L -o "haskell-language-server-${RELEASE}-x86_64-freebsd.tar.xz" \ - "https://api.cirrus-ci.com/v1/artifact/github/haskell/haskell-language-server/bindist/bindist/out/haskell-language-server-${RELEASE}-x86_64-freebsd.tar.xz?branch=${RELEASE}" +# curl --fail -L -o "haskell-language-server-${RELEASE}-x86_64-freebsd.tar.xz" \ +# "https://api.cirrus-ci.com/v1/artifact/github/haskell/haskell-language-server/bindist/bindist/out/haskell-language-server-${RELEASE}-x86_64-freebsd.tar.xz?branch=${RELEASE}" sha256sum haskell-language-server-* > SHA256SUMS gpg --detach-sign -u "${SIGNER}" SHA256SUMS -gh release upload "$RELEASE" "haskell-language-server-${RELEASE}-x86_64-freebsd.tar.xz" SHA256SUMS SHA256SUMS.sig +## see comment above +# gh release upload "$RELEASE" "haskell-language-server-${RELEASE}-x86_64-freebsd.tar.xz" SHA256SUMS SHA256SUMS.sig +gh release upload "$RELEASE" SHA256SUMS SHA256SUMS.sig diff --git a/shake-bench/shake-bench.cabal b/shake-bench/shake-bench.cabal index c55485963a..c381089aba 100644 --- a/shake-bench/shake-bench.cabal +++ b/shake-bench/shake-bench.cabal @@ -16,8 +16,7 @@ source-repository head location: https://github.com/haskell/haskell-language-server.git library - -- Depends on Chart which is unbuildable after this point - if impl(ghc >= 9.5) + if impl(ghc > 9.11) buildable: False exposed-modules: Development.Benchmark.Rules hs-source-dirs: src @@ -39,18 +38,8 @@ library mtl, shake, text - default-language: Haskell2010 + default-language: GHC2021 default-extensions: - BangPatterns - DeriveFunctor - DeriveGeneric - FlexibleContexts - GeneralizedNewtypeDeriving LambdaCase - NamedFieldPuns RecordWildCards - ScopedTypeVariables - StandaloneDeriving - TupleSections - TypeApplications ViewPatterns diff --git a/shake-bench/src/Development/Benchmark/Rules.hs b/shake-bench/src/Development/Benchmark/Rules.hs index f131e45d60..8ba2b3f0df 100644 --- a/shake-bench/src/Development/Benchmark/Rules.hs +++ b/shake-bench/src/Development/Benchmark/Rules.hs @@ -1,14 +1,10 @@ -{-# LANGUAGE AllowAmbiguousTypes #-} -{-# LANGUAGE ApplicativeDo #-} -{-# LANGUAGE ConstraintKinds #-} -{-# LANGUAGE DeriveAnyClass #-} -{-# LANGUAGE DerivingStrategies #-} -{-# LANGUAGE DuplicateRecordFields #-} -{-# LANGUAGE ExistentialQuantification #-} -{-# LANGUAGE OverloadedStrings #-} -{-# LANGUAGE RankNTypes #-} -{-# LANGUAGE TypeOperators #-} -{-# LANGUAGE TypeFamilies #-} +{-# LANGUAGE AllowAmbiguousTypes #-} +{-# LANGUAGE ApplicativeDo #-} +{-# LANGUAGE DeriveAnyClass #-} +{-# LANGUAGE DerivingStrategies #-} +{-# LANGUAGE DuplicateRecordFields #-} +{-# LANGUAGE OverloadedStrings #-} +{-# LANGUAGE TypeFamilies #-} {- | This module provides a bunch of Shake rules to build multiple revisions of a @@ -52,6 +48,7 @@ module Development.Benchmark.Rules ( buildRules, MkBuildRules(..), OutputFolder, ProjectRoot, benchRules, MkBenchRules(..), BenchProject(..), ProfilingMode(..), + addGetParentOracle, csvRules, svgRules, heapProfileRules, @@ -81,11 +78,13 @@ import Data.Aeson (FromJSON (..), import Data.Aeson.Lens (AsJSON (_JSON), _Object, _String) import Data.ByteString.Lazy (ByteString) -import Data.Char (isDigit) -import Data.List (find, isInfixOf, +import Data.Char (isAlpha, isDigit) +import Data.List (find, intercalate, + isInfixOf, + isSuffixOf, stripPrefix, transpose) -import Data.List.Extra (lower) +import Data.List.Extra (lower, splitOn) import Data.Maybe (fromMaybe) import Data.String (fromString) import Data.Text (Text) @@ -132,7 +131,7 @@ type RuleResultForExample e = , IsExample e) data Configuration = Configuration {confName :: String, confValue :: ByteString} - deriving (Binary, Eq, Generic, Hashable, NFData, Show, Typeable) + deriving (Binary, Eq, Generic, Hashable, NFData, Show) type instance RuleResult GetConfigurations = [Configuration] -- | Knowledge needed to run an example @@ -148,7 +147,9 @@ allTargetsForExample prof baseFolder ex = do configurations <- askOracle $ GetConfigurations () let buildFolder = baseFolder profilingPath prof return $ - [buildFolder getExampleName ex "results.csv"] + [ + buildFolder getExampleName ex "results.csv" + , buildFolder getExampleName ex "resultDiff.csv"] ++ [ buildFolder getExampleName ex escaped (escapeExperiment e) <.> "svg" | e <- experiments ] @@ -191,6 +192,8 @@ phonyRules prefix executableName prof buildFolder examples = do allTargetsForExample prof buildFolder ex need $ (buildFolder profilingPath prof "results.csv") : concat exampleTargets + need $ (buildFolder profilingPath prof "resultDiff.csv") + : concat exampleTargets phony (prefix <> "all-binaries") $ need =<< allBinaries buildFolder executableName -------------------------------------------------------------------------------- type OutputFolder = FilePath @@ -337,7 +340,7 @@ benchRules build MkBenchRules{..} = do ++ concat [[ "-h" , "-i" <> show i - , "-po" <> outHp + , "-po" <> dropExtension outHp , "-qg"] | CheapHeapProfiling i <- [prof]] ++ ["-RTS"] @@ -388,69 +391,92 @@ parseMaxResidencyAndAllocations input = -------------------------------------------------------------------------------- - +-- | oracles to get previous version of a given version +-- used for diff the results +addGetParentOracle :: Rules () +addGetParentOracle = void $ addOracle $ \(GetParent name) -> findPrev name <$> askOracle (GetVersions ()) -- | Rules to aggregate the CSV output of individual experiments csvRules :: forall example . RuleResultForExample example => FilePattern -> Rules () csvRules build = do + let genConfig resultName prefixName prefixOracles out = do + configurations <- prefixOracles + let allResultFiles = [takeDirectory out c resultName | c <- configurations ] + allResults <- traverse readFileLines allResultFiles + let header = head $ head allResults + results = map tail allResults + header' = prefixName <> ", " <> header + results' = zipWith (\v -> map (\l -> v <> ", " <> l)) configurations results + writeFileChanged out $ unlines $ header' : interleave results' -- build results for every experiment*example - build -/- "*/*/*/*/results.csv" %> \out -> do + priority 1 $ build -/- "*/*/*/*/results.csv" %> \out -> do experiments <- askOracle $ GetExperiments () - let allResultFiles = [takeDirectory out escaped (escapeExperiment e) <.> "csv" | e <- experiments] allResults <- traverse readFileLines allResultFiles - let header = head $ head allResults results = map tail allResults writeFileChanged out $ unlines $ header : concat results - + priority 2 $ build -/- "*/*/*/*/resultDiff.csv" %> \out -> do + let out2@[b, flav, example, ver, conf, exp_] = splitDirectories out + prev <- fmap T.unpack $ askOracle $ GetParent $ T.pack ver + allResultsCur <- readFileLines $ joinPath [b ,flav, example, ver, conf] "results.csv" + allResultsPrev <- readFileLines $ joinPath [b ,flav, example, prev, conf] "results.csv" + let resultsPrev = tail allResultsPrev + let resultsCur = tail allResultsCur + let resultDiff = zipWith convertToDiffResults resultsCur resultsPrev + writeFileChanged out $ unlines $ head allResultsCur : resultDiff -- aggregate all configurations for an experiment - build -/- "*/*/*/results.csv" %> \out -> do - configurations <- map confName <$> askOracle (GetConfigurations ()) - let allResultFiles = [takeDirectory out c "results.csv" | c <- configurations ] - - allResults <- traverse readFileLines allResultFiles - - let header = head $ head allResults - results = map tail allResults - header' = "configuration, " <> header - results' = zipWith (\v -> map (\l -> v <> ", " <> l)) configurations results - - writeFileChanged out $ unlines $ header' : interleave results' - + priority 3 $ build -/- "*/*/*/results.csv" %> genConfig "results.csv" + "Configuration" (map confName <$> askOracle (GetConfigurations ())) + priority 3 $ build -/- "*/*/*/resultDiff.csv" %> genConfig "resultDiff.csv" + "Configuration" (map confName <$> askOracle (GetConfigurations ())) -- aggregate all experiments for an example - build -/- "*/*/results.csv" %> \out -> do - versions <- map (T.unpack . humanName) <$> askOracle (GetVersions ()) - let allResultFiles = [takeDirectory out v "results.csv" | v <- versions] - - allResults <- traverse readFileLines allResultFiles - - let header = head $ head allResults - results = map tail allResults - header' = "version, " <> header - results' = zipWith (\v -> map (\l -> v <> ", " <> l)) versions results - - writeFileChanged out $ unlines $ header' : interleave results' - + priority 4 $ build -/- "*/*/results.csv" %> genConfig "results.csv" + "Version" (map (T.unpack . humanName) <$> askOracle (GetVersions ())) + priority 4 $ build -/- "*/*/resultDiff.csv" %> genConfig "resultDiff.csv" + "Version" (map (T.unpack . humanName) <$> askOracle (GetVersions ())) -- aggregate all examples - build -/- "*/results.csv" %> \out -> do - examples <- map (getExampleName @example) <$> askOracle (GetExamples ()) - let allResultFiles = [takeDirectory out e "results.csv" | e <- examples] - - allResults <- traverse readFileLines allResultFiles - - let header = head $ head allResults - results = map tail allResults - header' = "example, " <> header - results' = zipWith (\e -> map (\l -> e <> ", " <> l)) examples results + priority 5 $ build -/- "*/results.csv" %> genConfig "results.csv" + "Example" (map getExampleName <$> askOracle (GetExamples ())) + priority 5 $ build -/- "*/resultDiff.csv" %> genConfig "resultDiff.csv" + "Example" (map getExampleName <$> askOracle (GetExamples ())) + +convertToDiffResults :: String -> String -> String +convertToDiffResults line baseLine = intercalate "," diffResults + where items = parseLine line + baseItems = parseLine baseLine + diffItems = zipWith diffItem items baseItems + diffResults = map showItemDiffResult diffItems + +showItemDiffResult :: (Item, Maybe Double) -> String +showItemDiffResult (ItemString x, _) = x +showItemDiffResult (_, Nothing) = "NA" +showItemDiffResult (Mem x, Just y) = printf "%.2f" (y * 100 - 100) <> "%" +showItemDiffResult (Time x, Just y) = printf "%.2f" (y * 100 - 100) <> "%" + +diffItem :: Item -> Item -> (Item, Maybe Double) +diffItem (Mem x) (Mem y) = (Mem x, Just $ fromIntegral x / fromIntegral y) +diffItem (Time x) (Time y) = (Time x, if y == 0 then Nothing else Just $ x / y) +diffItem (ItemString x) (ItemString y) = (ItemString x, Nothing) +diffItem _ _ = (ItemString "no match", Nothing) + +data Item = Mem Int | Time Double | ItemString String + deriving (Show) - writeFileChanged out $ unlines $ header' : concat results' +parseLine :: String -> [Item] +parseLine = map f . splitOn "," + where + f x + | "MB" `isSuffixOf` x = Mem $ read $ reverse $ drop 2 $ reverse x + | otherwise = + case readMaybe @Double x of + Just time -> Time time + Nothing -> ItemString x -------------------------------------------------------------------------------- -- | Rules to produce charts for the GC stats svgRules :: FilePattern -> Rules () svgRules build = do - void $ addOracle $ \(GetParent name) -> findPrev name <$> askOracle (GetVersions ()) -- chart GC stats for an experiment on a given revision priority 1 $ build -/- "*/*/*/*/*.svg" %> \out -> do @@ -509,7 +535,7 @@ heapProfileRules build = do build -/- "*/*/*/*/*.heap.svg" %> \out -> do let hpFile = dropExtension2 out <.> "hp" need [hpFile] - cmd_ ("hp2pretty" :: String) [hpFile] + cmd_ ("eventlog2html" :: String) ["--heap-profile", hpFile] liftIO $ renameFile (dropExtension hpFile <.> "svg") out dropExtension2 :: FilePath -> FilePath diff --git a/src/HlsPlugins.hs b/src/HlsPlugins.hs index 4d37185998..87a1af7392 100644 --- a/src/HlsPlugins.hs +++ b/src/HlsPlugins.hs @@ -1,6 +1,5 @@ -{-# LANGUAGE CPP #-} -{-# LANGUAGE ExistentialQuantification #-} -{-# LANGUAGE OverloadedStrings #-} +{-# LANGUAGE CPP #-} +{-# LANGUAGE OverloadedStrings #-} module HlsPlugins where import Ide.Logger (Pretty (pretty), Recorder, @@ -94,6 +93,10 @@ import qualified Ide.Plugin.ExplicitFields as ExplicitFields import qualified Ide.Plugin.OverloadedRecordDot as OverloadedRecordDot #endif +#if hls_notes +import qualified Ide.Plugin.Notes as Notes +#endif + -- formatters #if hls_floskell @@ -108,6 +111,10 @@ import qualified Ide.Plugin.Fourmolu as Fourmolu import qualified Ide.Plugin.CabalFmt as CabalFmt #endif +#if hls_cabalgild +import qualified Ide.Plugin.CabalGild as CabalGild +#endif + #if hls_ormolu import qualified Ide.Plugin.Ormolu as Ormolu #endif @@ -120,6 +127,11 @@ import qualified Ide.Plugin.StylishHaskell as StylishHaskell import qualified Development.IDE.Plugin.CodeAction as Refactor #endif +#if hls_semanticTokens +import qualified Ide.Plugin.SemanticTokens as SemanticTokens +#endif + + data Log = forall a. (Pretty a) => Log PluginId a instance Pretty Log where @@ -140,6 +152,7 @@ idePlugins recorder = pluginDescToIdePlugins allPlugins allPlugins = #if hls_cabal let pId = "cabal" in Cabal.descriptor (pluginRecorder pId) pId : + let caId = "cabalHaskellIntegration" in Cabal.haskellInteractionDescriptor (pluginRecorder caId) caId : #endif #if hls_pragmas Pragmas.suggestPragmaDescriptor "pragmas-suggest" : @@ -153,25 +166,33 @@ idePlugins recorder = pluginDescToIdePlugins allPlugins let pId = "fourmolu" in Fourmolu.descriptor (pluginRecorder pId) pId: #endif #if hls_cabalfmt + let pId = "cabal-fmt" in CabalFmt.descriptor (pluginRecorder pId) pId: +#endif +#if hls_cabalgild -- this pId needs to be kept in sync with the hardcoded -- cabalFormattingProvider in the Default Config - let pId = "cabal-fmt" in CabalFmt.descriptor (pluginRecorder pId) pId: + let pId = "cabal-gild" in CabalGild.descriptor (pluginRecorder pId) pId: #endif #if hls_ormolu + -- this pId needs to be kept in sync with the hardcoded + -- haskellFormattingProvider in the Default Config let pId = "ormolu" in Ormolu.descriptor (pluginRecorder pId) pId : #endif #if hls_stylishHaskell - StylishHaskell.descriptor "stylish-haskell" : + let pId = "stylish-haskell" in StylishHaskell.descriptor (pluginRecorder pId) pId : #endif #if hls_rename let pId = "rename" in Rename.descriptor (pluginRecorder pId) pId: #endif #if hls_retrie - Retrie.descriptor "retrie" : + let pId = "retrie" in Retrie.descriptor (pluginRecorder pId) pId : #endif #if hls_callHierarchy CallHierarchy.descriptor "callHierarchy" : #endif +#if hls_semanticTokens + let pId = "semanticTokens" in SemanticTokens.descriptor (pluginRecorder pId) pId: +#endif #if hls_class let pId = "class" in Class.descriptor (pluginRecorder pId) pId: #endif @@ -223,6 +244,9 @@ idePlugins recorder = pluginDescToIdePlugins allPlugins #endif #if hls_overloaded_record_dot let pId = "overloaded-record-dot" in OverloadedRecordDot.descriptor (pluginRecorder pId) pId : +#endif +#if hls_notes + let pId = "notes" in Notes.descriptor (pluginRecorder pId) pId : #endif GhcIde.descriptors (pluginRecorder "ghcide") diff --git a/src/Ide/Arguments.hs b/src/Ide/Arguments.hs index 6af7551adf..be7f35e455 100644 --- a/src/Ide/Arguments.hs +++ b/src/Ide/Arguments.hs @@ -1,11 +1,8 @@ -- Copyright (c) 2019 The DAML Authors. All rights reserved. -- SPDX-License-Identifier: Apache-2.0 -{-# LANGUAGE CPP #-} -{-# LANGUAGE RecordWildCards #-} -{-# LANGUAGE TemplateHaskell #-} -{-# LANGUAGE TupleSections #-} -{-# OPTIONS_GHC -Wno-dodgy-imports #-} -- GHC no longer exports def in GHC 8.6 and above -{-# LANGUAGE TypeApplications #-} +{-# LANGUAGE CPP #-} +{-# LANGUAGE RecordWildCards #-} +{-# LANGUAGE TemplateHaskell #-} module Ide.Arguments ( Arguments(..) @@ -36,6 +33,7 @@ data Arguments | BiosMode BiosAction | Ghcide GhcideArguments | VSCodeExtensionSchemaMode + | PluginsCustomConfigMarkdownReferenceMode | DefaultConfigurationMode | PrintLibDir @@ -72,6 +70,7 @@ getArguments exeName plugins = execParser opts <|> hsubparser ( command "vscode-extension-schema" extensionSchemaCommand <> command "generate-default-config" generateDefaultConfigCommand + <> command "plugins-custom-config-markdown-reference" pluginsCustomConfigMarkdownReferenceCommand ) <|> listPluginsParser <|> BiosMode <$> biosParser @@ -89,6 +88,9 @@ getArguments exeName plugins = execParser opts generateDefaultConfigCommand = info (pure DefaultConfigurationMode) (fullDesc <> progDesc "Print config supported by the server with default values") + pluginsCustomConfigMarkdownReferenceCommand = + info (pure PluginsCustomConfigMarkdownReferenceMode) + (fullDesc <> progDesc "Print markdown reference for plugins custom config") printVersionParser :: String -> Parser PrintVersion printVersionParser exeName = diff --git a/src/Ide/Main.hs b/src/Ide/Main.hs index 726eebc524..f122b53fa6 100644 --- a/src/Ide/Main.hs +++ b/src/Ide/Main.hs @@ -1,12 +1,9 @@ -- Copyright (c) 2019 The DAML Authors. All rights reserved. -- SPDX-License-Identifier: Apache-2.0 -{-# LANGUAGE CPP #-} -{-# OPTIONS_GHC -Wno-dodgy-imports #-} -- GHC no longer exports def in GHC 8.6 and above -{-# LANGUAGE OverloadedStrings #-} -{-# LANGUAGE RecordWildCards #-} -{-# LANGUAGE ScopedTypeVariables #-} -{-# LANGUAGE TypeApplications #-} -{-# LANGUAGE TypeFamilies #-} +{-# LANGUAGE CPP #-} +{-# LANGUAGE OverloadedStrings #-} +{-# LANGUAGE RecordWildCards #-} +{-# LANGUAGE TypeFamilies #-} module Ide.Main(defaultMain, runLspMode, Log(..)) where @@ -14,28 +11,31 @@ import Control.Monad.Extra import qualified Data.Aeson.Encode.Pretty as A import Data.Coerce (coerce) import Data.Default -import Data.List (sort) +import Data.Function ((&)) +import Data.List (sortOn) import Data.Text (Text) import qualified Data.Text as T +import qualified Data.Text.IO as T (putStrLn) import Data.Text.Lazy.Encoding (decodeUtf8) import qualified Data.Text.Lazy.IO as LT -import Development.IDE.Core.Rules hiding (Log, logToPriority) -import Development.IDE.Core.Tracing (withTelemetryLogger) +import Development.IDE.Core.Rules hiding (Log) +import Development.IDE.Core.Tracing (withTelemetryRecorder) import Development.IDE.Main (isLSP) import qualified Development.IDE.Main as IDEMain import qualified Development.IDE.Session as Session import qualified Development.IDE.Types.Options as Ghcide -import GHC.Stack (emptyCallStack) import qualified HIE.Bios.Environment as HieBios import HIE.Bios.Types hiding (Log) import qualified HIE.Bios.Types as HieBios import Ide.Arguments import Ide.Logger as G -import Ide.Plugin.ConfigUtils (pluginsToDefaultConfig, +import Ide.Plugin.ConfigUtils (pluginsCustomConfigToMarkdownTables, + pluginsToDefaultConfig, pluginsToVSCodeExtensionSchema) import Ide.Types (IdePlugins, PluginId (PluginId), - ipMap, pluginId) + describePlugin, ipMap, pluginId) import Ide.Version +import Prettyprinter as PP import System.Directory import qualified System.Directory.Extra as IO import System.FilePath @@ -46,6 +46,7 @@ data Log | LogLspStart !GhcideArguments ![PluginId] | LogIDEMain IDEMain.Log | LogHieBios HieBios.Log + | LogSession Session.Log | LogOther T.Text deriving Show @@ -61,6 +62,7 @@ instance Pretty Log where , "PluginIds:" <+> pretty (coerce @_ @[Text] pluginIds) ] LogIDEMain iDEMainLog -> pretty iDEMainLog LogHieBios hieBiosLog -> pretty hieBiosLog + LogSession sessionLog -> pretty sessionLog LogOther t -> pretty t defaultMain :: Recorder (WithPriority Log) -> Arguments -> IdePlugins IdeState -> IO () @@ -83,15 +85,17 @@ defaultMain recorder args idePlugins = do putStrLn haskellLanguageServerNumericVersion ListPluginsMode -> do - let pluginNames = sort - $ map ((\(PluginId t) -> T.unpack t) . pluginId) + let pluginSummary = + PP.vsep + $ map describePlugin + $ sortOn pluginId $ ipMap idePlugins - mapM_ putStrLn pluginNames + print pluginSummary BiosMode PrintCradleType -> do dir <- IO.getCurrentDirectory hieYaml <- Session.findCradle def (dir "a") - cradle <- Session.loadCradle def hieYaml dir + cradle <- Session.loadCradle def (cmapWithPrio LogSession recorder) hieYaml dir print cradle Ghcide ghcideArgs -> do @@ -101,14 +105,16 @@ defaultMain recorder args idePlugins = do VSCodeExtensionSchemaMode -> do LT.putStrLn $ decodeUtf8 $ encodePrettySorted $ pluginsToVSCodeExtensionSchema idePlugins + PluginsCustomConfigMarkdownReferenceMode -> do + T.putStrLn $ pluginsCustomConfigToMarkdownTables idePlugins DefaultConfigurationMode -> do LT.putStrLn $ decodeUtf8 $ encodePrettySorted $ pluginsToDefaultConfig idePlugins PrintLibDir -> do d <- getCurrentDirectory let initialFp = d "a" hieYaml <- Session.findCradle def initialFp - cradle <- Session.loadCradle def hieYaml d - (CradleSuccess libdir) <- HieBios.getRuntimeGhcLibDir (toCologActionWithPrio (cmapWithPrio LogHieBios recorder)) cradle + cradle <- Session.loadCradle def (cmapWithPrio LogSession recorder) hieYaml d + (CradleSuccess libdir) <- HieBios.getRuntimeGhcLibDir cradle putStr libdir where encodePrettySorted = A.encodePretty' A.defConfig @@ -118,7 +124,7 @@ defaultMain recorder args idePlugins = do -- --------------------------------------------------------------------- runLspMode :: Recorder (WithPriority Log) -> GhcideArguments -> IdePlugins IdeState -> IO () -runLspMode recorder ghcideArgs@GhcideArguments{..} idePlugins = withTelemetryLogger $ \telemetryLogger -> do +runLspMode recorder ghcideArgs@GhcideArguments{..} idePlugins = withTelemetryRecorder $ \telemetryRecorder' -> do let log = logWith recorder whenJust argsCwd IO.setCurrentDirectory dir <- IO.getCurrentDirectory @@ -127,14 +133,13 @@ runLspMode recorder ghcideArgs@GhcideArguments{..} idePlugins = withTelemetryLog when (isLSP argsCommand) $ do log Info $ LogLspStart ghcideArgs (map pluginId $ ipMap idePlugins) - -- exists so old-style logging works. intended to be phased out - let logger = Logger $ \p m -> logger_ recorder (WithPriority p emptyCallStack $ LogOther m) - args = (if argsTesting then IDEMain.testing else IDEMain.defaultArguments) - (cmapWithPrio LogIDEMain recorder) logger idePlugins + let args = (if argsTesting then IDEMain.testing else IDEMain.defaultArguments) + (cmapWithPrio LogIDEMain recorder) dir idePlugins - IDEMain.defaultMain (cmapWithPrio LogIDEMain recorder) args + let telemetryRecorder = telemetryRecorder' & cmapWithPrio pretty + + IDEMain.defaultMain (cmapWithPrio LogIDEMain $ recorder <> telemetryRecorder) args { IDEMain.argCommand = argsCommand - , IDEMain.argsLogger = pure logger <> pure telemetryLogger , IDEMain.argsThreads = if argsThreads == 0 then Nothing else Just $ fromIntegral argsThreads , IDEMain.argsIdeOptions = \config sessionLoader -> let defOptions = IDEMain.argsIdeOptions args config sessionLoader diff --git a/stack-lts21.yaml b/stack-lts21.yaml deleted file mode 100644 index 74f5b8c4dc..0000000000 --- a/stack-lts21.yaml +++ /dev/null @@ -1,90 +0,0 @@ -resolver: lts-21.2 # ghc-9.4 - -packages: - - . - - ./hie-compat - - ./hls-graph - - ./ghcide/ - - ./ghcide/test - - ./hls-plugin-api - - ./hls-test-utils - # - ./shake-bench - - ./plugins/hls-alternate-number-format-plugin - - ./plugins/hls-cabal-fmt-plugin - - ./plugins/hls-cabal-plugin - - ./plugins/hls-call-hierarchy-plugin - - ./plugins/hls-change-type-signature-plugin - - ./plugins/hls-class-plugin - - ./plugins/hls-code-range-plugin - - ./plugins/hls-eval-plugin - - ./plugins/hls-explicit-fixity-plugin - - ./plugins/hls-explicit-imports-plugin - - ./plugins/hls-explicit-record-fields-plugin - - ./plugins/hls-floskell-plugin - - ./plugins/hls-fourmolu-plugin - - ./plugins/hls-gadt-plugin - - ./plugins/hls-hlint-plugin - - ./plugins/hls-module-name-plugin - - ./plugins/hls-ormolu-plugin - - ./plugins/hls-overloaded-record-dot-plugin - - ./plugins/hls-pragmas-plugin - - ./plugins/hls-qualify-imported-names-plugin - - ./plugins/hls-refactor-plugin - - ./plugins/hls-rename-plugin - - ./plugins/hls-retrie-plugin - - ./plugins/hls-splice-plugin - - ./plugins/hls-stan-plugin - - ./plugins/hls-stylish-haskell-plugin - -ghc-options: - "$everything": -haddock - -# stylish-haskell>strict -allow-newer: true - -extra-deps: -- floskell-0.10.7 -- hiedb-0.4.4.0 -- hie-bios-0.12.1 -- implicit-hie-0.1.2.7 -- implicit-hie-cradle-0.5.0.1 -- monad-dijkstra-0.1.1.3 -- algebraic-graphs-0.6.1 -- retrie-1.2.2 -- stylish-haskell-0.14.4.0 -- lsp-2.3.0.0 -- lsp-test-0.16.0.1 -- lsp-types-2.1.0.0 - -# stan dependencies not found in the stackage snapshot -- stan-0.1.0.2 -- clay-0.14.0 -- colourista-0.1.0.2 -- dir-traverse-0.2.3.0 -- extensions-0.1.0.0 -- relude-1.2.1.0 -- slist-0.2.1.0 -- tomland-1.3.3.2 -- trial-0.0.0.0 -- trial-optparse-applicative-0.0.0.0 -- trial-tomland-0.0.0.0 -- validation-selective-0.2.0.0 - -configure-options: - ghcide: - - --disable-library-for-ghci - haskell-language-server: - - --disable-library-for-ghci - -flags: - haskell-language-server: - pedantic: true - stylish-haskell: - ghc-lib: true - retrie: - BuildExecutable: false - -nix: - packages: [ icu libcxx zlib ] - -concurrent-tests: false diff --git a/stack-lts22.yaml b/stack-lts22.yaml new file mode 100644 index 0000000000..63efc35f30 --- /dev/null +++ b/stack-lts22.yaml @@ -0,0 +1,69 @@ +resolver: lts-22.43 # ghc-9.6.6 + +packages: + - . + - ./hie-compat + - ./hls-graph + - ./ghcide/ + - ./hls-plugin-api + - ./hls-test-utils + # - ./shake-bench + +ghc-options: + "$everything": -haddock + +allow-newer: true +allow-newer-deps: + - extensions + # stan dependencies + - directory-ospath-streaming + +extra-deps: + - Diff-0.5 + - floskell-0.11.1 + - hiedb-0.7.0.0 + - hie-bios-0.15.0 + - implicit-hie-0.1.4.0 + - lsp-2.7.0.0 + - lsp-test-0.17.1.0 + - lsp-types-2.3.0.0 + - monad-dijkstra-0.1.1.4 # 5 + - retrie-1.2.3 + + # stan and friends + - stan-0.2.1.0 + - dir-traverse-0.2.3.0 + - extensions-0.1.0.1 + - tomland-1.3.3.2 + - trial-0.0.0.0 + - trial-optparse-applicative-0.0.0.0 + - trial-tomland-0.0.0.0 + - validation-selective-0.2.0.0 + - cabal-add-0.1 + - cabal-install-parsers-0.6.1.1 + - directory-ospath-streaming-0.2.2 + + +configure-options: + ghcide: + - --disable-library-for-ghci + haskell-language-server: + - --disable-library-for-ghci + +flags: + haskell-language-server: + pedantic: true + stylish-haskell: + ghc-lib: true + retrie: + BuildExecutable: false + cabal-add: + cabal-syntax: true + # stan dependencies + directory-ospath-streaming: + os-string: false + +nix: + packages: [icu libcxx zlib] + +concurrent-tests: false diff --git a/stack.yaml b/stack.yaml index 5c5703a168..f6dd73d66a 100644 --- a/stack.yaml +++ b/stack.yaml @@ -1,89 +1,63 @@ -resolver: nightly-2023-07-10 # ghc-9.6.2 +resolver: lts-23.18 # ghc-9.8.4 packages: - . - ./hie-compat - ./hls-graph - ./ghcide/ - - ./ghcide/test - ./hls-plugin-api - ./hls-test-utils # - ./shake-bench - - ./plugins/hls-alternate-number-format-plugin - - ./plugins/hls-cabal-fmt-plugin - - ./plugins/hls-cabal-plugin - - ./plugins/hls-call-hierarchy-plugin - - ./plugins/hls-change-type-signature-plugin - - ./plugins/hls-class-plugin - - ./plugins/hls-code-range-plugin - - ./plugins/hls-eval-plugin - - ./plugins/hls-explicit-fixity-plugin - - ./plugins/hls-explicit-imports-plugin - - ./plugins/hls-explicit-record-fields-plugin - # - ./plugins/hls-floskell-plugin - - ./plugins/hls-fourmolu-plugin - - ./plugins/hls-gadt-plugin - - ./plugins/hls-hlint-plugin - - ./plugins/hls-module-name-plugin - - ./plugins/hls-ormolu-plugin - - ./plugins/hls-overloaded-record-dot-plugin - - ./plugins/hls-pragmas-plugin - - ./plugins/hls-qualify-imported-names-plugin - - ./plugins/hls-refactor-plugin - - ./plugins/hls-rename-plugin - - ./plugins/hls-retrie-plugin - - ./plugins/hls-splice-plugin - - ./plugins/hls-stan-plugin - - ./plugins/hls-stylish-haskell-plugin ghc-options: "$everything": -haddock allow-newer: true +allow-newer-deps: + - extensions + - hw-fingertree + - retrie + # stan dependencies + - directory-ospath-streaming extra-deps: -- Cabal-syntax-3.10.1.0@sha256:bb835ebab577fd0f9c11dab96210dbb8d68ffc62652576f4b092563c345930e7,7434 -# - floskell-0.10.7 -- hiedb-0.4.4.0 -- hie-bios-0.12.1 -- implicit-hie-0.1.2.7 -- implicit-hie-cradle-0.5.0.1 -- algebraic-graphs-0.6.1 -- retrie-1.2.2 -- hw-fingertree-0.1.2.1 -- hw-prim-0.6.3.2 -- ansi-terminal-0.11.5 -- lsp-2.3.0.0 -- lsp-test-0.16.0.1 -- lsp-types-2.1.0.0 - -# stan dependencies not found in the stackage snapshot -- stan-0.1.0.2 -- clay-0.14.0 -- colourista-0.1.0.2 -- dir-traverse-0.2.3.0 -- extensions-0.1.0.1 -- relude-1.2.1.0 -- slist-0.2.1.0 -- tomland-1.3.3.2 -- trial-0.0.0.0 -- trial-optparse-applicative-0.0.0.0 -- trial-tomland-0.0.0.0 -- validation-selective-0.2.0.0 + - floskell-0.11.1 + - hiedb-0.7.0.0 + - implicit-hie-0.1.4.0 + - hie-bios-0.15.0 + - hw-fingertree-0.1.2.1 + - monad-dijkstra-0.1.1.5 + - retrie-1.2.3 + + # stan dependencies not found in the stackage snapshot + - stan-0.2.1.0 + - dir-traverse-0.2.3.0 + - extensions-0.1.0.1 + - trial-0.0.0.0 + - trial-optparse-applicative-0.0.0.0 + - trial-tomland-0.0.0.0 + - directory-ospath-streaming-0.2.2 configure-options: ghcide: - - --disable-library-for-ghci + - --disable-library-for-ghci haskell-language-server: - - --disable-library-for-ghci + - --disable-library-for-ghci flags: haskell-language-server: pedantic: true + stylish-haskell: + ghc-lib: true retrie: BuildExecutable: false + cabal-add: + cabal-syntax: true + # stan dependencies + directory-ospath-streaming: + os-string: false nix: - packages: [ icu libcxx zlib ] + packages: [icu libcxx zlib] concurrent-tests: false diff --git a/test/functional/Config.hs b/test/functional/Config.hs index a474051808..874792784f 100644 --- a/test/functional/Config.hs +++ b/test/functional/Config.hs @@ -1,7 +1,5 @@ {-# LANGUAGE DataKinds #-} -{-# LANGUAGE DeriveGeneric #-} {-# LANGUAGE OverloadedStrings #-} -{-# LANGUAGE TypeApplications #-} {-# LANGUAGE TypeFamilies #-} module Config (tests) where @@ -10,12 +8,11 @@ import Control.Monad import Data.Hashable import qualified Data.HashMap.Strict as HM import qualified Data.Map as Map -import Data.Typeable (Typeable) import Development.IDE (RuleResult, action, define, getFilesOfInterestUntracked, getPluginConfigAction, ideErrorText, uses_) -import Development.IDE.Test (expectDiagnostics) +import Development.IDE.Test (ExpectedDiagnostic, expectDiagnostics) import GHC.Generics import Ide.Plugin.Config import Ide.Types @@ -45,13 +42,15 @@ genericConfigTests = testGroup "generic plugin config" setHlsConfig $ changeConfig "someplugin" def{plcHoverOn = False} -- getting only the expected diagnostics means the plugin wasn't enabled expectDiagnostics standardDiagnostics - , expectFailBecause "partial config is not supported" $ - testCase "custom defaults and non overlapping user config" $ runConfigSession "diagnostics" $ do + -- TODO: Partial config is not supported + , testCase "custom defaults and non overlapping user config" $ runConfigSession "diagnostics" $ do _doc <- createDoc "Foo.hs" "haskell" "module Foo where\nfoo = False" -- test that the user config doesn't accidentally override the initial config setHlsConfig $ changeConfig testPluginId def{plcHoverOn = False} -- getting only the expected diagnostics means the plugin wasn't enabled - expectDiagnostics standardDiagnostics + expectDiagnosticsFail + (BrokenIdeal standardDiagnostics) + (BrokenCurrent testPluginDiagnostics) , testCase "custom defaults and overlapping user plugin config" $ runConfigSession "diagnostics" $ do _doc <- createDoc "Foo.hs" "haskell" "module Foo where\nfoo = False" -- test that the user config overrides the default initial config @@ -66,17 +65,20 @@ genericConfigTests = testGroup "generic plugin config" expectDiagnostics standardDiagnostics ] where - standardDiagnostics = [("Foo.hs", [(DiagnosticSeverity_Warning, (1,0), "Top-level binding")])] - testPluginDiagnostics = [("Foo.hs", [(DiagnosticSeverity_Error, (0,0), "testplugin")])] + standardDiagnostics = [("Foo.hs", [(DiagnosticSeverity_Warning, (1,0), "Top-level binding", Nothing)])] + testPluginDiagnostics = [("Foo.hs", [(DiagnosticSeverity_Error, (0,0), "testplugin", Nothing)])] runConfigSession subdir session = do - recorder <- pluginTestRecorder - failIfSessionTimeout $ runSessionWithServer' @() (plugin recorder) def (def {ignoreConfigurationRequests=False}) fullCaps ("test/testdata" subdir) session + failIfSessionTimeout $ + runSessionWithTestConfig def + { testConfigSession=def {ignoreConfigurationRequests=False}, testShiftRoot=True + , testPluginDescriptor=plugin, testDirLocation=Left ("test/testdata" subdir) } + (const session) testPluginId = "testplugin" -- A disabled-by-default plugin that creates diagnostics plugin = mkPluginTestDescriptor' @() pd testPluginId - pd plId = (defaultPluginDescriptor plId) + pd plId = (defaultPluginDescriptor plId "") { pluginConfigDescriptor = configDisabled , pluginRules = do @@ -99,7 +101,14 @@ genericConfigTests = testGroup "generic plugin config" data GetTestDiagnostics = GetTestDiagnostics - deriving (Eq, Show, Typeable, Generic) + deriving (Eq, Show, Generic) instance Hashable GetTestDiagnostics instance NFData GetTestDiagnostics type instance RuleResult GetTestDiagnostics = () + +expectDiagnosticsFail + :: HasCallStack + => ExpectBroken 'Ideal [(FilePath, [ExpectedDiagnostic])] + -> ExpectBroken 'Current [(FilePath, [ExpectedDiagnostic])] + -> Session () +expectDiagnosticsFail _ = expectDiagnostics . unCurrent diff --git a/test/functional/ConfigSchema.hs b/test/functional/ConfigSchema.hs new file mode 100644 index 0000000000..2ece6972e9 --- /dev/null +++ b/test/functional/ConfigSchema.hs @@ -0,0 +1,58 @@ +module ConfigSchema where + + +import qualified Data.ByteString.Lazy.Char8 as BS +import Data.Char (toLower) +import System.FilePath (()) +import System.Process.Extra +import Test.Hls +import Test.Hls.Command + +-- | Integration test to capture changes to the generated default config and the vscode schema. +-- +-- Changes to the vscode schema need to be communicated to vscode-haskell plugin maintainers, +-- otherwise users can't make use of the new configurations. +-- +-- In general, changes to the schema need to be done consciously when new plugin or features are added. +-- To fix a failing of these tests, review the change. If it is expected, accept the change via: +-- +-- @ +-- TASTY_PATTERN="generate schema" cabal test func-test --test-options=--accept +-- @ +-- +-- As changes need to be applied for all GHC version specific configs, you either need to run this command for each +-- GHC version that is affected by the config change, or manually add the change to all other golden config files. +-- Likely, the easiest way is to run CI and apply the generated diffs manually. +tests :: TestTree +tests = testGroup "generate schema" + [ goldenGitDiff "vscode-extension-schema" (vscodeSchemaFp ghcVersion) $ do + stdout <- readProcess hlsExeCommand ["vscode-extension-schema"] "" + pure $ BS.pack stdout + , goldenGitDiff "generate-default-config" (defaultConfigFp ghcVersion) $ do + stdout <- readProcess hlsExeCommand ["generate-default-config"] "" + pure $ BS.pack stdout + , goldenGitDiff "plugins-custom-config-markdown-reference" (markdownReferenceFp ghcVersion) $ do + stdout <- readProcess hlsExeCommand ["plugins-custom-config-markdown-reference"] "" + pure $ BS.pack stdout + ] + +vscodeSchemaFp :: GhcVersion -> FilePath +vscodeSchemaFp ghcVer = "test" "testdata" "schema" prettyGhcVersion ghcVer vscodeSchemaJson + +defaultConfigFp :: GhcVersion -> FilePath +defaultConfigFp ghcVer = "test" "testdata" "schema" prettyGhcVersion ghcVer generateDefaultConfigJson + +markdownReferenceFp :: GhcVersion -> FilePath +markdownReferenceFp ghcVer = "test" "testdata" "schema" prettyGhcVersion ghcVer markdownReferenceMd + +vscodeSchemaJson :: FilePath +vscodeSchemaJson = "vscode-extension-schema.golden.json" + +generateDefaultConfigJson :: FilePath +generateDefaultConfigJson = "default-config.golden.json" + +markdownReferenceMd :: FilePath +markdownReferenceMd = "markdown-reference.md" + +prettyGhcVersion :: GhcVersion -> String +prettyGhcVersion ghcVer = map toLower (show ghcVer) diff --git a/test/functional/Format.hs b/test/functional/Format.hs index 6b174a68d1..a8fe534e9d 100644 --- a/test/functional/Format.hs +++ b/test/functional/Format.hs @@ -23,18 +23,18 @@ tests = testGroup "format document" providerTests :: TestTree providerTests = testGroup "lsp formatting provider" - [ testCase "respects none" $ runSessionWithConfig (formatConfig "none") hlsCommand fullCaps "test/testdata/format" $ do + [ testCase "respects none" $ runSessionWithConfig (formatConfig "none") hlsLspCommand fullLatestClientCaps "test/testdata/format" $ do void configurationRequest doc <- openDoc "Format.hs" "haskell" resp <- request SMethod_TextDocumentFormatting $ DocumentFormattingParams Nothing doc (FormattingOptions 2 True Nothing Nothing Nothing) liftIO $ case resp ^. L.result of - result@(Left (ResponseError reason message Nothing)) -> case reason of + result@(Left (TResponseError reason message Nothing)) -> case reason of (InR ErrorCodes_MethodNotFound) -> pure () -- No formatter - (InR ErrorCodes_InvalidRequest) | "No plugin enabled for SMethod_TextDocumentFormatting" `T.isPrefixOf` message -> pure () + (InR ErrorCodes_InvalidRequest) | "No plugin" `T.isPrefixOf` message -> pure () _ -> assertFailure $ "strange response from formatting provider:" ++ show result result -> assertFailure $ "strange response from formatting provider:" ++ show result - , requiresOrmoluPlugin . requiresFloskellPlugin $ testCase "can change on the fly" $ runSessionWithConfig (formatConfig "none") hlsCommand fullCaps "test/testdata/format" $ do + , requiresOrmoluPlugin . requiresFloskellPlugin $ testCase "can change on the fly" $ runSessionWithConfig (formatConfig "none") hlsLspCommand fullLatestClientCaps "test/testdata/format" $ do void configurationRequest formattedOrmolu <- liftIO $ T.readFile "test/testdata/format/Format.ormolu.formatted.hs" formattedFloskell <- liftIO $ T.readFile "test/testdata/format/Format.floskell.formatted.hs" diff --git a/test/functional/FunctionalBadProject.hs b/test/functional/FunctionalBadProject.hs index e6242ba9c1..150f9cdb04 100644 --- a/test/functional/FunctionalBadProject.hs +++ b/test/functional/FunctionalBadProject.hs @@ -12,17 +12,17 @@ import Test.Hls.Command tests :: TestTree tests = testGroup "behaviour on malformed projects" [ testCase "Missing module diagnostic" $ do - runSession hlsCommand fullCaps "test/testdata/missingModuleTest/missingModule/" $ do + runSession hlsLspCommand fullLatestClientCaps "test/testdata/missingModuleTest/missingModule/" $ do doc <- openDoc "src/MyLib.hs" "haskell" [diag] <- waitForDiagnosticsFrom doc liftIO $ assertBool "missing module name" $ "MyLib" `T.isInfixOf` (diag ^. L.message) liftIO $ assertBool "module missing context" $ "may not be listed" `T.isInfixOf` (diag ^. L.message) , testCase "Missing module diagnostic - no matching prefix" $ do - runSession hlsCommand fullCaps "test/testdata/missingModuleTest/noPrefixMatch/" $ do + runSession hlsLspCommand fullLatestClientCaps "test/testdata/missingModuleTest/noPrefixMatch/" $ do doc <- openDoc "app/Other.hs" "haskell" [diag] <- waitForDiagnosticsFrom doc liftIO $ assertBool "missing module name" $ "Other" `T.isInfixOf` (diag ^. L.message) liftIO $ assertBool "hie-bios message" $ - "Cabal {component = Just \"exe:testExe\"}" `T.isInfixOf` (diag ^. L.message) + "Cabal" `T.isInfixOf` (diag ^. L.message) ] diff --git a/test/functional/HieBios.hs b/test/functional/HieBios.hs index 0e6fe562f2..5a06026b53 100644 --- a/test/functional/HieBios.hs +++ b/test/functional/HieBios.hs @@ -11,7 +11,7 @@ import Test.Hls.Command tests :: TestTree tests = testGroup "hie-bios" [ testCase "loads main-is module" $ do - runSession hlsCommand fullCaps "test/testdata/hieBiosMainIs" $ do + runSession hlsLspCommand fullLatestClientCaps "test/testdata/hieBiosMainIs" $ do _ <- openDoc "Main.hs" "haskell" (diag:_) <- waitForDiagnostics liftIO $ "Top-level binding with no type signature:" `T.isInfixOf` (diag ^. L.message) diff --git a/test/functional/Main.hs b/test/functional/Main.hs index a214f3cd65..daa342f694 100644 --- a/test/functional/Main.hs +++ b/test/functional/Main.hs @@ -1,6 +1,7 @@ module Main where import Config +import ConfigSchema import Format import FunctionalBadProject import HieBios @@ -10,8 +11,9 @@ import Test.Hls main :: IO () main = defaultTestRunner $ testGroup "haskell-language-server" [ Config.tests - , ignoreInEnv [HostOS Windows, GhcVer GHC90, GhcVer GHC92] "Tests gets stuck in ci" $ Format.tests + , ConfigSchema.tests + , ignoreInEnv [HostOS Windows] "Tests gets stuck in ci" Format.tests , FunctionalBadProject.tests , HieBios.tests - , ignoreInEnv [HostOS Windows, GhcVer GHC90] "Tests gets stuck in ci" $ Progress.tests + , ignoreInEnv [HostOS Windows] "Tests gets stuck in ci" Progress.tests ] diff --git a/test/functional/Progress.hs b/test/functional/Progress.hs index d7a0a4090c..ed82a02350 100644 --- a/test/functional/Progress.hs +++ b/test/functional/Progress.hs @@ -1,10 +1,8 @@ {-# LANGUAGE DataKinds #-} {-# LANGUAGE DuplicateRecordFields #-} -{-# LANGUAGE FlexibleContexts #-} {-# LANGUAGE GADTs #-} {-# LANGUAGE LambdaCase #-} {-# LANGUAGE OverloadedStrings #-} -{-# LANGUAGE PolyKinds #-} {-# LANGUAGE ViewPatterns #-} module Progress (tests) where @@ -28,18 +26,19 @@ tests = testGroup "window/workDoneProgress" [ testCase "sends indefinite progress notifications" $ - runSession hlsCommand progressCaps "test/testdata/diagnostics" $ do + runSession hlsLspCommand progressCaps "test/testdata/diagnostics" $ do let path = "Foo.hs" _ <- openDoc path "haskell" - expectProgressMessages [pack ("Setting up diagnostics (for " ++ path ++ ")"), "Processing", "Indexing"] [] + expectProgressMessages [pack ("Setting up diagnostics (for " ++ path ++ ")"), "Processing", "Indexing"] [] [] , requiresEvalPlugin $ testCase "eval plugin sends progress reports" $ - runSession hlsCommand progressCaps "plugins/hls-eval-plugin/test/testdata" $ do - doc <- openDoc "T1.hs" "haskell" + runSession hlsLspCommand progressCaps "plugins/hls-eval-plugin/test/testdata" $ do + doc <- openDoc "TIO.hs" "haskell" lspId <- sendRequest SMethod_TextDocumentCodeLens (CodeLensParams Nothing Nothing doc) - (codeLensResponse, activeProgressTokens) <- expectProgressMessagesTill + (codeLensResponse, createdProgressTokens, activeProgressTokens) <- expectProgressMessagesTill (responseForId SMethod_TextDocumentCodeLens lspId) - ["Setting up testdata (for T1.hs)", "Processing", "Indexing"] + ["Setting up testdata (for TIO.hs)", "Processing"] + [] [] -- this is a test so exceptions result in fails @@ -54,31 +53,31 @@ tests = (command ^. L.command) (decode $ encode $ fromJust $ command ^. L.arguments) - expectProgressMessages ["Evaluating"] activeProgressTokens + expectProgressMessages ["Evaluating"] createdProgressTokens activeProgressTokens _ -> error $ "Unexpected response result: " ++ show response , requiresOrmoluPlugin $ testCase "ormolu plugin sends progress notifications" $ do - runSessionWithConfig (def { ignoreConfigurationRequests = False }) hlsCommand progressCaps "test/testdata/format" $ do + runSessionWithConfig (def { ignoreConfigurationRequests = False }) hlsLspCommand progressCaps "test/testdata/format" $ do void configurationRequest setHlsConfig (formatLspConfig "ormolu") doc <- openDoc "Format.hs" "haskell" - expectProgressMessages ["Setting up testdata (for Format.hs)", "Processing", "Indexing"] [] + expectProgressMessages ["Setting up format (for Format.hs)", "Processing", "Indexing"] [] [] _ <- sendRequest SMethod_TextDocumentFormatting $ DocumentFormattingParams Nothing doc (FormattingOptions 2 True Nothing Nothing Nothing) - expectProgressMessages ["Formatting Format.hs"] [] + expectProgressMessages ["Formatting Format.hs"] [] [] , requiresFourmoluPlugin $ testCase "fourmolu plugin sends progress notifications" $ do - runSessionWithConfig (def { ignoreConfigurationRequests = False }) hlsCommand progressCaps "test/testdata/format" $ do + runSessionWithConfig (def { ignoreConfigurationRequests = False }) hlsLspCommand progressCaps "test/testdata/format" $ do void configurationRequest setHlsConfig (formatLspConfig "fourmolu") doc <- openDoc "Format.hs" "haskell" - expectProgressMessages ["Setting up testdata (for Format.hs)", "Processing", "Indexing"] [] + expectProgressMessages ["Setting up format (for Format.hs)", "Processing", "Indexing"] [] [] _ <- sendRequest SMethod_TextDocumentFormatting $ DocumentFormattingParams Nothing doc (FormattingOptions 2 True Nothing Nothing Nothing) - expectProgressMessages ["Formatting Format.hs"] [] + expectProgressMessages ["Formatting Format.hs"] [] [] ] formatLspConfig :: Text -> Config formatLspConfig provider = def { formattingProvider = provider } progressCaps :: ClientCapabilities -progressCaps = fullCaps{_window = Just (WindowClientCapabilities (Just True) Nothing Nothing)} +progressCaps = fullLatestClientCaps{_window = Just (WindowClientCapabilities (Just True) Nothing Nothing)} data ProgressMessage = ProgressCreate WorkDoneProgressCreateParams @@ -115,57 +114,59 @@ interestingMessage :: Session a -> Session (InterestingMessage a) interestingMessage theMessage = fmap InterestingMessage theMessage <|> fmap ProgressMessage progressMessage -expectProgressMessagesTill :: Session a -> [Text] -> [ProgressToken] -> Session (a, [ProgressToken]) -expectProgressMessagesTill stopMessage expectedTitles activeProgressTokens = do +expectProgressMessagesTill :: Session a -> [Text] -> [ProgressToken] -> [ProgressToken] -> Session (a, [ProgressToken], [ProgressToken]) +expectProgressMessagesTill stopMessage expectedTitles createdProgressTokens activeProgressTokens = do message <- skipManyTill anyMessage (interestingMessage stopMessage) case message of InterestingMessage a -> do liftIO $ null expectedTitles @? "Expected titles not empty " <> show expectedTitles - pure (a, activeProgressTokens) + pure (a, createdProgressTokens, activeProgressTokens) ProgressMessage progressMessage -> updateExpectProgressStateAndRecurseWith (expectProgressMessagesTill stopMessage) progressMessage expectedTitles + createdProgressTokens activeProgressTokens {- | Test that the server is correctly producing a sequence of progress related - messages. Each create must be pair with a corresponding begin and end, + messages. Creates can be dangling, but should be paired with a corresponding begin and end, optionally with some progress in between. Tokens must match. The begin messages have titles describing the work that is in-progress, we check that the titles we see are those we expect. -} -expectProgressMessages :: [Text] -> [ProgressToken] -> Session () -expectProgressMessages [] [] = pure () -expectProgressMessages expectedTitles activeProgressTokens = do +expectProgressMessages :: [Text] -> [ProgressToken] -> [ProgressToken] -> Session () +expectProgressMessages [] _ [] = pure () +expectProgressMessages expectedTitles createdProgressTokens activeProgressTokens = do message <- skipManyTill anyMessage progressMessage - updateExpectProgressStateAndRecurseWith expectProgressMessages message expectedTitles activeProgressTokens + updateExpectProgressStateAndRecurseWith expectProgressMessages message expectedTitles createdProgressTokens activeProgressTokens -updateExpectProgressStateAndRecurseWith :: ([Text] -> [ProgressToken] -> Session a) +updateExpectProgressStateAndRecurseWith :: ([Text] -> [ProgressToken] -> [ProgressToken] -> Session a) -> ProgressMessage -> [Text] -> [ProgressToken] + -> [ProgressToken] -> Session a -updateExpectProgressStateAndRecurseWith f progressMessage expectedTitles activeProgressTokens = do +updateExpectProgressStateAndRecurseWith f progressMessage expectedTitles createdProgressTokens activeProgressTokens = do case progressMessage of ProgressCreate params -> do - f expectedTitles ((params ^. L.token): activeProgressTokens) + f expectedTitles ((params ^. L.token): createdProgressTokens) activeProgressTokens ProgressBegin token params -> do - liftIO $ token `expectedIn` activeProgressTokens - f (delete (params ^. L.title) expectedTitles) activeProgressTokens + liftIO $ token `expectedIn` createdProgressTokens + f (delete (params ^. L.title) expectedTitles) (delete token createdProgressTokens) (token:activeProgressTokens) ProgressReport token _ -> do liftIO $ token `expectedIn` activeProgressTokens - f expectedTitles activeProgressTokens + f expectedTitles createdProgressTokens activeProgressTokens ProgressEnd token _ -> do liftIO $ token `expectedIn` activeProgressTokens - f expectedTitles (delete token activeProgressTokens) + f expectedTitles createdProgressTokens (delete token activeProgressTokens) expectedIn :: (Foldable t, Eq a, Show a) => a -> t a -> Assertion expectedIn a as = a `elem` as @? "Unexpected " ++ show a -getMessageResult :: TResponseMessage m -> MessageResult m +getMessageResult :: Show (ErrorData m) => TResponseMessage m -> MessageResult m getMessageResult rsp = case rsp ^. L.result of - Right x -> x - Left err -> throw $ UnexpectedResponseError (SomeLspId $ fromJust $ rsp ^. L.id) err + Right x -> x + Left err -> throw $ UnexpectedResponseError (fromJust $ rsp ^. L.id) err diff --git a/test/testdata/schema/ghc910/default-config.golden.json b/test/testdata/schema/ghc910/default-config.golden.json new file mode 100644 index 0000000000..3b4e687ef9 --- /dev/null +++ b/test/testdata/schema/ghc910/default-config.golden.json @@ -0,0 +1,158 @@ +{ + "cabalFormattingProvider": "cabal-gild", + "checkParents": "CheckOnSave", + "checkProject": true, + "formattingProvider": "ormolu", + "maxCompletions": 40, + "plugin": { + "alternateNumberFormat": { + "globalOn": true + }, + "cabal": { + "codeActionsOn": true, + "completionOn": true, + "diagnosticsOn": true, + "hoverOn": true, + "symbolsOn": true + }, + "cabal-fmt": { + "config": { + "path": "cabal-fmt" + } + }, + "cabal-gild": { + "config": { + "path": "cabal-gild" + } + }, + "cabalHaskellIntegration": { + "globalOn": true + }, + "callHierarchy": { + "globalOn": true + }, + "changeTypeSignature": { + "globalOn": true + }, + "class": { + "codeActionsOn": true, + "codeLensOn": true + }, + "eval": { + "codeActionsOn": true, + "codeLensOn": true, + "config": { + "diff": true, + "exception": false + } + }, + "explicit-fields": { + "codeActionsOn": true, + "inlayHintsOn": true + }, + "explicit-fixity": { + "globalOn": true + }, + "fourmolu": { + "config": { + "external": false, + "path": "fourmolu" + } + }, + "gadt": { + "globalOn": true + }, + "ghcide-code-actions-bindings": { + "globalOn": true + }, + "ghcide-code-actions-fill-holes": { + "globalOn": true + }, + "ghcide-code-actions-imports-exports": { + "globalOn": true + }, + "ghcide-code-actions-type-signatures": { + "globalOn": true + }, + "ghcide-completions": { + "config": { + "autoExtendOn": true, + "snippetsOn": true + }, + "globalOn": true + }, + "ghcide-hover-and-symbols": { + "hoverOn": true, + "symbolsOn": true + }, + "ghcide-type-lenses": { + "config": { + "mode": "always" + }, + "globalOn": true + }, + "hlint": { + "codeActionsOn": true, + "config": { + "flags": [] + }, + "diagnosticsOn": true + }, + "importLens": { + "codeActionsOn": true, + "codeLensOn": true, + "inlayHintsOn": true + }, + "moduleName": { + "globalOn": true + }, + "ormolu": { + "config": { + "external": false + } + }, + "overloaded-record-dot": { + "globalOn": true + }, + "pragmas-completion": { + "globalOn": true + }, + "pragmas-disable": { + "globalOn": true + }, + "pragmas-suggest": { + "globalOn": true + }, + "qualifyImportedNames": { + "globalOn": true + }, + "rename": { + "config": { + "crossModule": false + }, + "globalOn": true + }, + "semanticTokens": { + "config": { + "classMethodToken": "method", + "classToken": "class", + "dataConstructorToken": "enumMember", + "functionToken": "function", + "moduleToken": "namespace", + "operatorToken": "operator", + "patternSynonymToken": "macro", + "recordFieldToken": "property", + "typeConstructorToken": "enum", + "typeFamilyToken": "interface", + "typeSynonymToken": "type", + "typeVariableToken": "typeParameter", + "variableToken": "variable" + }, + "globalOn": false + }, + "stan": { + "globalOn": false + } + }, + "sessionLoading": "singleComponent" +} diff --git a/test/testdata/schema/ghc910/markdown-reference.md b/test/testdata/schema/ghc910/markdown-reference.md new file mode 100644 index 0000000000..668323ce66 --- /dev/null +++ b/test/testdata/schema/ghc910/markdown-reference.md @@ -0,0 +1,66 @@ +## hlint +| Property | Description | Default | Allowed values | +| --- | --- | --- | --- | +| `flags` | Flags used by hlint | `TODO: Array values` |   | + +## cabal-fmt +| Property | Description | Default | Allowed values | +| --- | --- | --- | --- | +| `path` | Set path to 'cabal-fmt' executable | `"cabal-fmt"` |   | + +## ghcide-completions +| Property | Description | Default | Allowed values | +| --- | --- | --- | --- | +| `autoExtendOn` | Extends the import list automatically when completing a out-of-scope identifier | `True` |   | +| `snippetsOn` | Inserts snippets when using code completions | `True` |   | + +## eval +| Property | Description | Default | Allowed values | +| --- | --- | --- | --- | +| `exception` | Enable marking exceptions with `*** Exception:` similarly to doctest and GHCi. | `False` |   | +| `diff` | Enable the diff output (WAS/NOW) of eval lenses | `True` |   | + +## ghcide-type-lenses +| Property | Description | Default | Allowed values | +| --- | --- | --- | --- | +| `mode` | Control how type lenses are shown | `Always` |
  • Always
  • Exported
  • Diagnostics
| + +## ormolu +| Property | Description | Default | Allowed values | +| --- | --- | --- | --- | +| `external` | Call out to an external "ormolu" executable, rather than using the bundled library | `False` |   | + +## rename +| Property | Description | Default | Allowed values | +| --- | --- | --- | --- | +| `crossModule` | Enable experimental cross-module renaming | `False` |   | + +## semanticTokens +| Property | Description | Default | Allowed values | +| --- | --- | --- | --- | +| `variableToken` | LSP semantic token type to use for variables | `SemanticTokenTypes_Variable` |
  • SemanticTokenTypes_Namespace
  • SemanticTokenTypes_Type
  • SemanticTokenTypes_Class
  • SemanticTokenTypes_Enum
  • SemanticTokenTypes_Interface
  • SemanticTokenTypes_Struct
  • SemanticTokenTypes_TypeParameter
  • SemanticTokenTypes_Parameter
  • SemanticTokenTypes_Variable
  • SemanticTokenTypes_Property
  • SemanticTokenTypes_EnumMember
  • SemanticTokenTypes_Event
  • SemanticTokenTypes_Function
  • SemanticTokenTypes_Method
  • SemanticTokenTypes_Macro
  • SemanticTokenTypes_Keyword
  • SemanticTokenTypes_Modifier
  • SemanticTokenTypes_Comment
  • SemanticTokenTypes_String
  • SemanticTokenTypes_Number
  • SemanticTokenTypes_Regexp
  • SemanticTokenTypes_Operator
  • SemanticTokenTypes_Decorator
| +| `functionToken` | LSP semantic token type to use for functions | `SemanticTokenTypes_Function` |
  • SemanticTokenTypes_Namespace
  • SemanticTokenTypes_Type
  • SemanticTokenTypes_Class
  • SemanticTokenTypes_Enum
  • SemanticTokenTypes_Interface
  • SemanticTokenTypes_Struct
  • SemanticTokenTypes_TypeParameter
  • SemanticTokenTypes_Parameter
  • SemanticTokenTypes_Variable
  • SemanticTokenTypes_Property
  • SemanticTokenTypes_EnumMember
  • SemanticTokenTypes_Event
  • SemanticTokenTypes_Function
  • SemanticTokenTypes_Method
  • SemanticTokenTypes_Macro
  • SemanticTokenTypes_Keyword
  • SemanticTokenTypes_Modifier
  • SemanticTokenTypes_Comment
  • SemanticTokenTypes_String
  • SemanticTokenTypes_Number
  • SemanticTokenTypes_Regexp
  • SemanticTokenTypes_Operator
  • SemanticTokenTypes_Decorator
| +| `dataConstructorToken` | LSP semantic token type to use for data constructors | `SemanticTokenTypes_EnumMember` |
  • SemanticTokenTypes_Namespace
  • SemanticTokenTypes_Type
  • SemanticTokenTypes_Class
  • SemanticTokenTypes_Enum
  • SemanticTokenTypes_Interface
  • SemanticTokenTypes_Struct
  • SemanticTokenTypes_TypeParameter
  • SemanticTokenTypes_Parameter
  • SemanticTokenTypes_Variable
  • SemanticTokenTypes_Property
  • SemanticTokenTypes_EnumMember
  • SemanticTokenTypes_Event
  • SemanticTokenTypes_Function
  • SemanticTokenTypes_Method
  • SemanticTokenTypes_Macro
  • SemanticTokenTypes_Keyword
  • SemanticTokenTypes_Modifier
  • SemanticTokenTypes_Comment
  • SemanticTokenTypes_String
  • SemanticTokenTypes_Number
  • SemanticTokenTypes_Regexp
  • SemanticTokenTypes_Operator
  • SemanticTokenTypes_Decorator
| +| `typeVariableToken` | LSP semantic token type to use for type variables | `SemanticTokenTypes_TypeParameter` |
  • SemanticTokenTypes_Namespace
  • SemanticTokenTypes_Type
  • SemanticTokenTypes_Class
  • SemanticTokenTypes_Enum
  • SemanticTokenTypes_Interface
  • SemanticTokenTypes_Struct
  • SemanticTokenTypes_TypeParameter
  • SemanticTokenTypes_Parameter
  • SemanticTokenTypes_Variable
  • SemanticTokenTypes_Property
  • SemanticTokenTypes_EnumMember
  • SemanticTokenTypes_Event
  • SemanticTokenTypes_Function
  • SemanticTokenTypes_Method
  • SemanticTokenTypes_Macro
  • SemanticTokenTypes_Keyword
  • SemanticTokenTypes_Modifier
  • SemanticTokenTypes_Comment
  • SemanticTokenTypes_String
  • SemanticTokenTypes_Number
  • SemanticTokenTypes_Regexp
  • SemanticTokenTypes_Operator
  • SemanticTokenTypes_Decorator
| +| `classMethodToken` | LSP semantic token type to use for typeclass methods | `SemanticTokenTypes_Method` |
  • SemanticTokenTypes_Namespace
  • SemanticTokenTypes_Type
  • SemanticTokenTypes_Class
  • SemanticTokenTypes_Enum
  • SemanticTokenTypes_Interface
  • SemanticTokenTypes_Struct
  • SemanticTokenTypes_TypeParameter
  • SemanticTokenTypes_Parameter
  • SemanticTokenTypes_Variable
  • SemanticTokenTypes_Property
  • SemanticTokenTypes_EnumMember
  • SemanticTokenTypes_Event
  • SemanticTokenTypes_Function
  • SemanticTokenTypes_Method
  • SemanticTokenTypes_Macro
  • SemanticTokenTypes_Keyword
  • SemanticTokenTypes_Modifier
  • SemanticTokenTypes_Comment
  • SemanticTokenTypes_String
  • SemanticTokenTypes_Number
  • SemanticTokenTypes_Regexp
  • SemanticTokenTypes_Operator
  • SemanticTokenTypes_Decorator
| +| `patternSynonymToken` | LSP semantic token type to use for pattern synonyms | `SemanticTokenTypes_Macro` |
  • SemanticTokenTypes_Namespace
  • SemanticTokenTypes_Type
  • SemanticTokenTypes_Class
  • SemanticTokenTypes_Enum
  • SemanticTokenTypes_Interface
  • SemanticTokenTypes_Struct
  • SemanticTokenTypes_TypeParameter
  • SemanticTokenTypes_Parameter
  • SemanticTokenTypes_Variable
  • SemanticTokenTypes_Property
  • SemanticTokenTypes_EnumMember
  • SemanticTokenTypes_Event
  • SemanticTokenTypes_Function
  • SemanticTokenTypes_Method
  • SemanticTokenTypes_Macro
  • SemanticTokenTypes_Keyword
  • SemanticTokenTypes_Modifier
  • SemanticTokenTypes_Comment
  • SemanticTokenTypes_String
  • SemanticTokenTypes_Number
  • SemanticTokenTypes_Regexp
  • SemanticTokenTypes_Operator
  • SemanticTokenTypes_Decorator
| +| `typeConstructorToken` | LSP semantic token type to use for type constructors | `SemanticTokenTypes_Enum` |
  • SemanticTokenTypes_Namespace
  • SemanticTokenTypes_Type
  • SemanticTokenTypes_Class
  • SemanticTokenTypes_Enum
  • SemanticTokenTypes_Interface
  • SemanticTokenTypes_Struct
  • SemanticTokenTypes_TypeParameter
  • SemanticTokenTypes_Parameter
  • SemanticTokenTypes_Variable
  • SemanticTokenTypes_Property
  • SemanticTokenTypes_EnumMember
  • SemanticTokenTypes_Event
  • SemanticTokenTypes_Function
  • SemanticTokenTypes_Method
  • SemanticTokenTypes_Macro
  • SemanticTokenTypes_Keyword
  • SemanticTokenTypes_Modifier
  • SemanticTokenTypes_Comment
  • SemanticTokenTypes_String
  • SemanticTokenTypes_Number
  • SemanticTokenTypes_Regexp
  • SemanticTokenTypes_Operator
  • SemanticTokenTypes_Decorator
| +| `classToken` | LSP semantic token type to use for typeclasses | `SemanticTokenTypes_Class` |
  • SemanticTokenTypes_Namespace
  • SemanticTokenTypes_Type
  • SemanticTokenTypes_Class
  • SemanticTokenTypes_Enum
  • SemanticTokenTypes_Interface
  • SemanticTokenTypes_Struct
  • SemanticTokenTypes_TypeParameter
  • SemanticTokenTypes_Parameter
  • SemanticTokenTypes_Variable
  • SemanticTokenTypes_Property
  • SemanticTokenTypes_EnumMember
  • SemanticTokenTypes_Event
  • SemanticTokenTypes_Function
  • SemanticTokenTypes_Method
  • SemanticTokenTypes_Macro
  • SemanticTokenTypes_Keyword
  • SemanticTokenTypes_Modifier
  • SemanticTokenTypes_Comment
  • SemanticTokenTypes_String
  • SemanticTokenTypes_Number
  • SemanticTokenTypes_Regexp
  • SemanticTokenTypes_Operator
  • SemanticTokenTypes_Decorator
| +| `typeSynonymToken` | LSP semantic token type to use for type synonyms | `SemanticTokenTypes_Type` |
  • SemanticTokenTypes_Namespace
  • SemanticTokenTypes_Type
  • SemanticTokenTypes_Class
  • SemanticTokenTypes_Enum
  • SemanticTokenTypes_Interface
  • SemanticTokenTypes_Struct
  • SemanticTokenTypes_TypeParameter
  • SemanticTokenTypes_Parameter
  • SemanticTokenTypes_Variable
  • SemanticTokenTypes_Property
  • SemanticTokenTypes_EnumMember
  • SemanticTokenTypes_Event
  • SemanticTokenTypes_Function
  • SemanticTokenTypes_Method
  • SemanticTokenTypes_Macro
  • SemanticTokenTypes_Keyword
  • SemanticTokenTypes_Modifier
  • SemanticTokenTypes_Comment
  • SemanticTokenTypes_String
  • SemanticTokenTypes_Number
  • SemanticTokenTypes_Regexp
  • SemanticTokenTypes_Operator
  • SemanticTokenTypes_Decorator
| +| `typeFamilyToken` | LSP semantic token type to use for type families | `SemanticTokenTypes_Interface` |
  • SemanticTokenTypes_Namespace
  • SemanticTokenTypes_Type
  • SemanticTokenTypes_Class
  • SemanticTokenTypes_Enum
  • SemanticTokenTypes_Interface
  • SemanticTokenTypes_Struct
  • SemanticTokenTypes_TypeParameter
  • SemanticTokenTypes_Parameter
  • SemanticTokenTypes_Variable
  • SemanticTokenTypes_Property
  • SemanticTokenTypes_EnumMember
  • SemanticTokenTypes_Event
  • SemanticTokenTypes_Function
  • SemanticTokenTypes_Method
  • SemanticTokenTypes_Macro
  • SemanticTokenTypes_Keyword
  • SemanticTokenTypes_Modifier
  • SemanticTokenTypes_Comment
  • SemanticTokenTypes_String
  • SemanticTokenTypes_Number
  • SemanticTokenTypes_Regexp
  • SemanticTokenTypes_Operator
  • SemanticTokenTypes_Decorator
| +| `recordFieldToken` | LSP semantic token type to use for record fields | `SemanticTokenTypes_Property` |
  • SemanticTokenTypes_Namespace
  • SemanticTokenTypes_Type
  • SemanticTokenTypes_Class
  • SemanticTokenTypes_Enum
  • SemanticTokenTypes_Interface
  • SemanticTokenTypes_Struct
  • SemanticTokenTypes_TypeParameter
  • SemanticTokenTypes_Parameter
  • SemanticTokenTypes_Variable
  • SemanticTokenTypes_Property
  • SemanticTokenTypes_EnumMember
  • SemanticTokenTypes_Event
  • SemanticTokenTypes_Function
  • SemanticTokenTypes_Method
  • SemanticTokenTypes_Macro
  • SemanticTokenTypes_Keyword
  • SemanticTokenTypes_Modifier
  • SemanticTokenTypes_Comment
  • SemanticTokenTypes_String
  • SemanticTokenTypes_Number
  • SemanticTokenTypes_Regexp
  • SemanticTokenTypes_Operator
  • SemanticTokenTypes_Decorator
| +| `operatorToken` | LSP semantic token type to use for operators | `SemanticTokenTypes_Operator` |
  • SemanticTokenTypes_Namespace
  • SemanticTokenTypes_Type
  • SemanticTokenTypes_Class
  • SemanticTokenTypes_Enum
  • SemanticTokenTypes_Interface
  • SemanticTokenTypes_Struct
  • SemanticTokenTypes_TypeParameter
  • SemanticTokenTypes_Parameter
  • SemanticTokenTypes_Variable
  • SemanticTokenTypes_Property
  • SemanticTokenTypes_EnumMember
  • SemanticTokenTypes_Event
  • SemanticTokenTypes_Function
  • SemanticTokenTypes_Method
  • SemanticTokenTypes_Macro
  • SemanticTokenTypes_Keyword
  • SemanticTokenTypes_Modifier
  • SemanticTokenTypes_Comment
  • SemanticTokenTypes_String
  • SemanticTokenTypes_Number
  • SemanticTokenTypes_Regexp
  • SemanticTokenTypes_Operator
  • SemanticTokenTypes_Decorator
| +| `moduleToken` | LSP semantic token type to use for modules | `SemanticTokenTypes_Namespace` |
  • SemanticTokenTypes_Namespace
  • SemanticTokenTypes_Type
  • SemanticTokenTypes_Class
  • SemanticTokenTypes_Enum
  • SemanticTokenTypes_Interface
  • SemanticTokenTypes_Struct
  • SemanticTokenTypes_TypeParameter
  • SemanticTokenTypes_Parameter
  • SemanticTokenTypes_Variable
  • SemanticTokenTypes_Property
  • SemanticTokenTypes_EnumMember
  • SemanticTokenTypes_Event
  • SemanticTokenTypes_Function
  • SemanticTokenTypes_Method
  • SemanticTokenTypes_Macro
  • SemanticTokenTypes_Keyword
  • SemanticTokenTypes_Modifier
  • SemanticTokenTypes_Comment
  • SemanticTokenTypes_String
  • SemanticTokenTypes_Number
  • SemanticTokenTypes_Regexp
  • SemanticTokenTypes_Operator
  • SemanticTokenTypes_Decorator
| + +## fourmolu +| Property | Description | Default | Allowed values | +| --- | --- | --- | --- | +| `external` | Call out to an external "fourmolu" executable, rather than using the bundled library. | `False` |   | +| `path` | Set path to executable (for "external" mode). | `"fourmolu"` |   | + +## cabal-gild +| Property | Description | Default | Allowed values | +| --- | --- | --- | --- | +| `path` | Set path to 'cabal-gild' executable | `"cabal-gild"` |   | + + diff --git a/test/testdata/schema/ghc910/vscode-extension-schema.golden.json b/test/testdata/schema/ghc910/vscode-extension-schema.golden.json new file mode 100644 index 0000000000..4ca08f296c --- /dev/null +++ b/test/testdata/schema/ghc910/vscode-extension-schema.golden.json @@ -0,0 +1,1046 @@ +{ + "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.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.stan.globalOn": { + "default": false, + "description": "Enables stan plugin", + "scope": "resource", + "type": "boolean" + } +} diff --git a/test/testdata/schema/ghc912/default-config.golden.json b/test/testdata/schema/ghc912/default-config.golden.json new file mode 100644 index 0000000000..0dfbd39df2 --- /dev/null +++ b/test/testdata/schema/ghc912/default-config.golden.json @@ -0,0 +1,155 @@ +{ + "cabalFormattingProvider": "cabal-gild", + "checkParents": "CheckOnSave", + "checkProject": true, + "formattingProvider": "ormolu", + "maxCompletions": 40, + "plugin": { + "alternateNumberFormat": { + "globalOn": true + }, + "cabal": { + "codeActionsOn": true, + "completionOn": true, + "diagnosticsOn": true, + "hoverOn": true, + "symbolsOn": true + }, + "cabal-fmt": { + "config": { + "path": "cabal-fmt" + } + }, + "cabal-gild": { + "config": { + "path": "cabal-gild" + } + }, + "cabalHaskellIntegration": { + "globalOn": true + }, + "callHierarchy": { + "globalOn": true + }, + "changeTypeSignature": { + "globalOn": true + }, + "class": { + "codeActionsOn": true, + "codeLensOn": true + }, + "eval": { + "codeActionsOn": true, + "codeLensOn": true, + "config": { + "diff": true, + "exception": false + } + }, + "explicit-fields": { + "codeActionsOn": true, + "inlayHintsOn": true + }, + "explicit-fixity": { + "globalOn": true + }, + "fourmolu": { + "config": { + "external": false, + "path": "fourmolu" + } + }, + "gadt": { + "globalOn": true + }, + "ghcide-code-actions-bindings": { + "globalOn": true + }, + "ghcide-code-actions-fill-holes": { + "globalOn": true + }, + "ghcide-code-actions-imports-exports": { + "globalOn": true + }, + "ghcide-code-actions-type-signatures": { + "globalOn": true + }, + "ghcide-completions": { + "config": { + "autoExtendOn": true, + "snippetsOn": true + }, + "globalOn": true + }, + "ghcide-hover-and-symbols": { + "hoverOn": true, + "symbolsOn": true + }, + "ghcide-type-lenses": { + "config": { + "mode": "always" + }, + "globalOn": true + }, + "hlint": { + "codeActionsOn": true, + "config": { + "flags": [] + }, + "diagnosticsOn": true + }, + "importLens": { + "codeActionsOn": true, + "codeLensOn": true, + "inlayHintsOn": true + }, + "moduleName": { + "globalOn": true + }, + "ormolu": { + "config": { + "external": false + } + }, + "overloaded-record-dot": { + "globalOn": true + }, + "pragmas-completion": { + "globalOn": true + }, + "pragmas-disable": { + "globalOn": true + }, + "pragmas-suggest": { + "globalOn": true + }, + "qualifyImportedNames": { + "globalOn": true + }, + "rename": { + "config": { + "crossModule": false + }, + "globalOn": true + }, + "semanticTokens": { + "config": { + "classMethodToken": "method", + "classToken": "class", + "dataConstructorToken": "enumMember", + "functionToken": "function", + "moduleToken": "namespace", + "operatorToken": "operator", + "patternSynonymToken": "macro", + "recordFieldToken": "property", + "typeConstructorToken": "enum", + "typeFamilyToken": "interface", + "typeSynonymToken": "type", + "typeVariableToken": "typeParameter", + "variableToken": "variable" + }, + "globalOn": false + } + }, + "sessionLoading": "singleComponent" +} diff --git a/test/testdata/schema/ghc912/markdown-reference.md b/test/testdata/schema/ghc912/markdown-reference.md new file mode 100644 index 0000000000..668323ce66 --- /dev/null +++ b/test/testdata/schema/ghc912/markdown-reference.md @@ -0,0 +1,66 @@ +## hlint +| Property | Description | Default | Allowed values | +| --- | --- | --- | --- | +| `flags` | Flags used by hlint | `TODO: Array values` |   | + +## cabal-fmt +| Property | Description | Default | Allowed values | +| --- | --- | --- | --- | +| `path` | Set path to 'cabal-fmt' executable | `"cabal-fmt"` |   | + +## ghcide-completions +| Property | Description | Default | Allowed values | +| --- | --- | --- | --- | +| `autoExtendOn` | Extends the import list automatically when completing a out-of-scope identifier | `True` |   | +| `snippetsOn` | Inserts snippets when using code completions | `True` |   | + +## eval +| Property | Description | Default | Allowed values | +| --- | --- | --- | --- | +| `exception` | Enable marking exceptions with `*** Exception:` similarly to doctest and GHCi. | `False` |   | +| `diff` | Enable the diff output (WAS/NOW) of eval lenses | `True` |   | + +## ghcide-type-lenses +| Property | Description | Default | Allowed values | +| --- | --- | --- | --- | +| `mode` | Control how type lenses are shown | `Always` |
  • Always
  • Exported
  • Diagnostics
| + +## ormolu +| Property | Description | Default | Allowed values | +| --- | --- | --- | --- | +| `external` | Call out to an external "ormolu" executable, rather than using the bundled library | `False` |   | + +## rename +| Property | Description | Default | Allowed values | +| --- | --- | --- | --- | +| `crossModule` | Enable experimental cross-module renaming | `False` |   | + +## semanticTokens +| Property | Description | Default | Allowed values | +| --- | --- | --- | --- | +| `variableToken` | LSP semantic token type to use for variables | `SemanticTokenTypes_Variable` |
  • SemanticTokenTypes_Namespace
  • SemanticTokenTypes_Type
  • SemanticTokenTypes_Class
  • SemanticTokenTypes_Enum
  • SemanticTokenTypes_Interface
  • SemanticTokenTypes_Struct
  • SemanticTokenTypes_TypeParameter
  • SemanticTokenTypes_Parameter
  • SemanticTokenTypes_Variable
  • SemanticTokenTypes_Property
  • SemanticTokenTypes_EnumMember
  • SemanticTokenTypes_Event
  • SemanticTokenTypes_Function
  • SemanticTokenTypes_Method
  • SemanticTokenTypes_Macro
  • SemanticTokenTypes_Keyword
  • SemanticTokenTypes_Modifier
  • SemanticTokenTypes_Comment
  • SemanticTokenTypes_String
  • SemanticTokenTypes_Number
  • SemanticTokenTypes_Regexp
  • SemanticTokenTypes_Operator
  • SemanticTokenTypes_Decorator
| +| `functionToken` | LSP semantic token type to use for functions | `SemanticTokenTypes_Function` |
  • SemanticTokenTypes_Namespace
  • SemanticTokenTypes_Type
  • SemanticTokenTypes_Class
  • SemanticTokenTypes_Enum
  • SemanticTokenTypes_Interface
  • SemanticTokenTypes_Struct
  • SemanticTokenTypes_TypeParameter
  • SemanticTokenTypes_Parameter
  • SemanticTokenTypes_Variable
  • SemanticTokenTypes_Property
  • SemanticTokenTypes_EnumMember
  • SemanticTokenTypes_Event
  • SemanticTokenTypes_Function
  • SemanticTokenTypes_Method
  • SemanticTokenTypes_Macro
  • SemanticTokenTypes_Keyword
  • SemanticTokenTypes_Modifier
  • SemanticTokenTypes_Comment
  • SemanticTokenTypes_String
  • SemanticTokenTypes_Number
  • SemanticTokenTypes_Regexp
  • SemanticTokenTypes_Operator
  • SemanticTokenTypes_Decorator
| +| `dataConstructorToken` | LSP semantic token type to use for data constructors | `SemanticTokenTypes_EnumMember` |
  • SemanticTokenTypes_Namespace
  • SemanticTokenTypes_Type
  • SemanticTokenTypes_Class
  • SemanticTokenTypes_Enum
  • SemanticTokenTypes_Interface
  • SemanticTokenTypes_Struct
  • SemanticTokenTypes_TypeParameter
  • SemanticTokenTypes_Parameter
  • SemanticTokenTypes_Variable
  • SemanticTokenTypes_Property
  • SemanticTokenTypes_EnumMember
  • SemanticTokenTypes_Event
  • SemanticTokenTypes_Function
  • SemanticTokenTypes_Method
  • SemanticTokenTypes_Macro
  • SemanticTokenTypes_Keyword
  • SemanticTokenTypes_Modifier
  • SemanticTokenTypes_Comment
  • SemanticTokenTypes_String
  • SemanticTokenTypes_Number
  • SemanticTokenTypes_Regexp
  • SemanticTokenTypes_Operator
  • SemanticTokenTypes_Decorator
| +| `typeVariableToken` | LSP semantic token type to use for type variables | `SemanticTokenTypes_TypeParameter` |
  • SemanticTokenTypes_Namespace
  • SemanticTokenTypes_Type
  • SemanticTokenTypes_Class
  • SemanticTokenTypes_Enum
  • SemanticTokenTypes_Interface
  • SemanticTokenTypes_Struct
  • SemanticTokenTypes_TypeParameter
  • SemanticTokenTypes_Parameter
  • SemanticTokenTypes_Variable
  • SemanticTokenTypes_Property
  • SemanticTokenTypes_EnumMember
  • SemanticTokenTypes_Event
  • SemanticTokenTypes_Function
  • SemanticTokenTypes_Method
  • SemanticTokenTypes_Macro
  • SemanticTokenTypes_Keyword
  • SemanticTokenTypes_Modifier
  • SemanticTokenTypes_Comment
  • SemanticTokenTypes_String
  • SemanticTokenTypes_Number
  • SemanticTokenTypes_Regexp
  • SemanticTokenTypes_Operator
  • SemanticTokenTypes_Decorator
| +| `classMethodToken` | LSP semantic token type to use for typeclass methods | `SemanticTokenTypes_Method` |
  • SemanticTokenTypes_Namespace
  • SemanticTokenTypes_Type
  • SemanticTokenTypes_Class
  • SemanticTokenTypes_Enum
  • SemanticTokenTypes_Interface
  • SemanticTokenTypes_Struct
  • SemanticTokenTypes_TypeParameter
  • SemanticTokenTypes_Parameter
  • SemanticTokenTypes_Variable
  • SemanticTokenTypes_Property
  • SemanticTokenTypes_EnumMember
  • SemanticTokenTypes_Event
  • SemanticTokenTypes_Function
  • SemanticTokenTypes_Method
  • SemanticTokenTypes_Macro
  • SemanticTokenTypes_Keyword
  • SemanticTokenTypes_Modifier
  • SemanticTokenTypes_Comment
  • SemanticTokenTypes_String
  • SemanticTokenTypes_Number
  • SemanticTokenTypes_Regexp
  • SemanticTokenTypes_Operator
  • SemanticTokenTypes_Decorator
| +| `patternSynonymToken` | LSP semantic token type to use for pattern synonyms | `SemanticTokenTypes_Macro` |
  • SemanticTokenTypes_Namespace
  • SemanticTokenTypes_Type
  • SemanticTokenTypes_Class
  • SemanticTokenTypes_Enum
  • SemanticTokenTypes_Interface
  • SemanticTokenTypes_Struct
  • SemanticTokenTypes_TypeParameter
  • SemanticTokenTypes_Parameter
  • SemanticTokenTypes_Variable
  • SemanticTokenTypes_Property
  • SemanticTokenTypes_EnumMember
  • SemanticTokenTypes_Event
  • SemanticTokenTypes_Function
  • SemanticTokenTypes_Method
  • SemanticTokenTypes_Macro
  • SemanticTokenTypes_Keyword
  • SemanticTokenTypes_Modifier
  • SemanticTokenTypes_Comment
  • SemanticTokenTypes_String
  • SemanticTokenTypes_Number
  • SemanticTokenTypes_Regexp
  • SemanticTokenTypes_Operator
  • SemanticTokenTypes_Decorator
| +| `typeConstructorToken` | LSP semantic token type to use for type constructors | `SemanticTokenTypes_Enum` |
  • SemanticTokenTypes_Namespace
  • SemanticTokenTypes_Type
  • SemanticTokenTypes_Class
  • SemanticTokenTypes_Enum
  • SemanticTokenTypes_Interface
  • SemanticTokenTypes_Struct
  • SemanticTokenTypes_TypeParameter
  • SemanticTokenTypes_Parameter
  • SemanticTokenTypes_Variable
  • SemanticTokenTypes_Property
  • SemanticTokenTypes_EnumMember
  • SemanticTokenTypes_Event
  • SemanticTokenTypes_Function
  • SemanticTokenTypes_Method
  • SemanticTokenTypes_Macro
  • SemanticTokenTypes_Keyword
  • SemanticTokenTypes_Modifier
  • SemanticTokenTypes_Comment
  • SemanticTokenTypes_String
  • SemanticTokenTypes_Number
  • SemanticTokenTypes_Regexp
  • SemanticTokenTypes_Operator
  • SemanticTokenTypes_Decorator
| +| `classToken` | LSP semantic token type to use for typeclasses | `SemanticTokenTypes_Class` |
  • SemanticTokenTypes_Namespace
  • SemanticTokenTypes_Type
  • SemanticTokenTypes_Class
  • SemanticTokenTypes_Enum
  • SemanticTokenTypes_Interface
  • SemanticTokenTypes_Struct
  • SemanticTokenTypes_TypeParameter
  • SemanticTokenTypes_Parameter
  • SemanticTokenTypes_Variable
  • SemanticTokenTypes_Property
  • SemanticTokenTypes_EnumMember
  • SemanticTokenTypes_Event
  • SemanticTokenTypes_Function
  • SemanticTokenTypes_Method
  • SemanticTokenTypes_Macro
  • SemanticTokenTypes_Keyword
  • SemanticTokenTypes_Modifier
  • SemanticTokenTypes_Comment
  • SemanticTokenTypes_String
  • SemanticTokenTypes_Number
  • SemanticTokenTypes_Regexp
  • SemanticTokenTypes_Operator
  • SemanticTokenTypes_Decorator
| +| `typeSynonymToken` | LSP semantic token type to use for type synonyms | `SemanticTokenTypes_Type` |
  • SemanticTokenTypes_Namespace
  • SemanticTokenTypes_Type
  • SemanticTokenTypes_Class
  • SemanticTokenTypes_Enum
  • SemanticTokenTypes_Interface
  • SemanticTokenTypes_Struct
  • SemanticTokenTypes_TypeParameter
  • SemanticTokenTypes_Parameter
  • SemanticTokenTypes_Variable
  • SemanticTokenTypes_Property
  • SemanticTokenTypes_EnumMember
  • SemanticTokenTypes_Event
  • SemanticTokenTypes_Function
  • SemanticTokenTypes_Method
  • SemanticTokenTypes_Macro
  • SemanticTokenTypes_Keyword
  • SemanticTokenTypes_Modifier
  • SemanticTokenTypes_Comment
  • SemanticTokenTypes_String
  • SemanticTokenTypes_Number
  • SemanticTokenTypes_Regexp
  • SemanticTokenTypes_Operator
  • SemanticTokenTypes_Decorator
| +| `typeFamilyToken` | LSP semantic token type to use for type families | `SemanticTokenTypes_Interface` |
  • SemanticTokenTypes_Namespace
  • SemanticTokenTypes_Type
  • SemanticTokenTypes_Class
  • SemanticTokenTypes_Enum
  • SemanticTokenTypes_Interface
  • SemanticTokenTypes_Struct
  • SemanticTokenTypes_TypeParameter
  • SemanticTokenTypes_Parameter
  • SemanticTokenTypes_Variable
  • SemanticTokenTypes_Property
  • SemanticTokenTypes_EnumMember
  • SemanticTokenTypes_Event
  • SemanticTokenTypes_Function
  • SemanticTokenTypes_Method
  • SemanticTokenTypes_Macro
  • SemanticTokenTypes_Keyword
  • SemanticTokenTypes_Modifier
  • SemanticTokenTypes_Comment
  • SemanticTokenTypes_String
  • SemanticTokenTypes_Number
  • SemanticTokenTypes_Regexp
  • SemanticTokenTypes_Operator
  • SemanticTokenTypes_Decorator
| +| `recordFieldToken` | LSP semantic token type to use for record fields | `SemanticTokenTypes_Property` |
  • SemanticTokenTypes_Namespace
  • SemanticTokenTypes_Type
  • SemanticTokenTypes_Class
  • SemanticTokenTypes_Enum
  • SemanticTokenTypes_Interface
  • SemanticTokenTypes_Struct
  • SemanticTokenTypes_TypeParameter
  • SemanticTokenTypes_Parameter
  • SemanticTokenTypes_Variable
  • SemanticTokenTypes_Property
  • SemanticTokenTypes_EnumMember
  • SemanticTokenTypes_Event
  • SemanticTokenTypes_Function
  • SemanticTokenTypes_Method
  • SemanticTokenTypes_Macro
  • SemanticTokenTypes_Keyword
  • SemanticTokenTypes_Modifier
  • SemanticTokenTypes_Comment
  • SemanticTokenTypes_String
  • SemanticTokenTypes_Number
  • SemanticTokenTypes_Regexp
  • SemanticTokenTypes_Operator
  • SemanticTokenTypes_Decorator
| +| `operatorToken` | LSP semantic token type to use for operators | `SemanticTokenTypes_Operator` |
  • SemanticTokenTypes_Namespace
  • SemanticTokenTypes_Type
  • SemanticTokenTypes_Class
  • SemanticTokenTypes_Enum
  • SemanticTokenTypes_Interface
  • SemanticTokenTypes_Struct
  • SemanticTokenTypes_TypeParameter
  • SemanticTokenTypes_Parameter
  • SemanticTokenTypes_Variable
  • SemanticTokenTypes_Property
  • SemanticTokenTypes_EnumMember
  • SemanticTokenTypes_Event
  • SemanticTokenTypes_Function
  • SemanticTokenTypes_Method
  • SemanticTokenTypes_Macro
  • SemanticTokenTypes_Keyword
  • SemanticTokenTypes_Modifier
  • SemanticTokenTypes_Comment
  • SemanticTokenTypes_String
  • SemanticTokenTypes_Number
  • SemanticTokenTypes_Regexp
  • SemanticTokenTypes_Operator
  • SemanticTokenTypes_Decorator
| +| `moduleToken` | LSP semantic token type to use for modules | `SemanticTokenTypes_Namespace` |
  • SemanticTokenTypes_Namespace
  • SemanticTokenTypes_Type
  • SemanticTokenTypes_Class
  • SemanticTokenTypes_Enum
  • SemanticTokenTypes_Interface
  • SemanticTokenTypes_Struct
  • SemanticTokenTypes_TypeParameter
  • SemanticTokenTypes_Parameter
  • SemanticTokenTypes_Variable
  • SemanticTokenTypes_Property
  • SemanticTokenTypes_EnumMember
  • SemanticTokenTypes_Event
  • SemanticTokenTypes_Function
  • SemanticTokenTypes_Method
  • SemanticTokenTypes_Macro
  • SemanticTokenTypes_Keyword
  • SemanticTokenTypes_Modifier
  • SemanticTokenTypes_Comment
  • SemanticTokenTypes_String
  • SemanticTokenTypes_Number
  • SemanticTokenTypes_Regexp
  • SemanticTokenTypes_Operator
  • SemanticTokenTypes_Decorator
| + +## fourmolu +| Property | Description | Default | Allowed values | +| --- | --- | --- | --- | +| `external` | Call out to an external "fourmolu" executable, rather than using the bundled library. | `False` |   | +| `path` | Set path to executable (for "external" mode). | `"fourmolu"` |   | + +## cabal-gild +| Property | Description | Default | Allowed values | +| --- | --- | --- | --- | +| `path` | Set path to 'cabal-gild' executable | `"cabal-gild"` |   | + + diff --git a/test/testdata/schema/ghc912/vscode-extension-schema.golden.json b/test/testdata/schema/ghc912/vscode-extension-schema.golden.json new file mode 100644 index 0000000000..77d398438e --- /dev/null +++ b/test/testdata/schema/ghc912/vscode-extension-schema.golden.json @@ -0,0 +1,1040 @@ +{ + "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.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" + } +} diff --git a/test/testdata/schema/ghc96/default-config.golden.json b/test/testdata/schema/ghc96/default-config.golden.json new file mode 100644 index 0000000000..8467b451f1 --- /dev/null +++ b/test/testdata/schema/ghc96/default-config.golden.json @@ -0,0 +1,164 @@ +{ + "cabalFormattingProvider": "cabal-gild", + "checkParents": "CheckOnSave", + "checkProject": true, + "formattingProvider": "ormolu", + "maxCompletions": 40, + "plugin": { + "alternateNumberFormat": { + "globalOn": true + }, + "cabal": { + "codeActionsOn": true, + "completionOn": true, + "diagnosticsOn": true, + "hoverOn": true, + "symbolsOn": true + }, + "cabal-fmt": { + "config": { + "path": "cabal-fmt" + } + }, + "cabal-gild": { + "config": { + "path": "cabal-gild" + } + }, + "cabalHaskellIntegration": { + "globalOn": true + }, + "callHierarchy": { + "globalOn": true + }, + "changeTypeSignature": { + "globalOn": true + }, + "class": { + "codeActionsOn": true, + "codeLensOn": true + }, + "eval": { + "codeActionsOn": true, + "codeLensOn": true, + "config": { + "diff": true, + "exception": false + } + }, + "explicit-fields": { + "codeActionsOn": true, + "inlayHintsOn": true + }, + "explicit-fixity": { + "globalOn": true + }, + "fourmolu": { + "config": { + "external": false, + "path": "fourmolu" + } + }, + "gadt": { + "globalOn": true + }, + "ghcide-code-actions-bindings": { + "globalOn": true + }, + "ghcide-code-actions-fill-holes": { + "globalOn": true + }, + "ghcide-code-actions-imports-exports": { + "globalOn": true + }, + "ghcide-code-actions-type-signatures": { + "globalOn": true + }, + "ghcide-completions": { + "config": { + "autoExtendOn": true, + "snippetsOn": true + }, + "globalOn": true + }, + "ghcide-hover-and-symbols": { + "hoverOn": true, + "symbolsOn": true + }, + "ghcide-type-lenses": { + "config": { + "mode": "always" + }, + "globalOn": true + }, + "hlint": { + "codeActionsOn": true, + "config": { + "flags": [] + }, + "diagnosticsOn": true + }, + "importLens": { + "codeActionsOn": true, + "codeLensOn": true, + "inlayHintsOn": true + }, + "moduleName": { + "globalOn": true + }, + "ormolu": { + "config": { + "external": false + } + }, + "overloaded-record-dot": { + "globalOn": true + }, + "pragmas-completion": { + "globalOn": true + }, + "pragmas-disable": { + "globalOn": true + }, + "pragmas-suggest": { + "globalOn": true + }, + "qualifyImportedNames": { + "globalOn": true + }, + "rename": { + "config": { + "crossModule": false + }, + "globalOn": true + }, + "retrie": { + "globalOn": true + }, + "semanticTokens": { + "config": { + "classMethodToken": "method", + "classToken": "class", + "dataConstructorToken": "enumMember", + "functionToken": "function", + "moduleToken": "namespace", + "operatorToken": "operator", + "patternSynonymToken": "macro", + "recordFieldToken": "property", + "typeConstructorToken": "enum", + "typeFamilyToken": "interface", + "typeSynonymToken": "type", + "typeVariableToken": "typeParameter", + "variableToken": "variable" + }, + "globalOn": false + }, + "splice": { + "globalOn": true + }, + "stan": { + "globalOn": false + } + }, + "sessionLoading": "singleComponent" +} diff --git a/test/testdata/schema/ghc96/markdown-reference.md b/test/testdata/schema/ghc96/markdown-reference.md new file mode 100644 index 0000000000..668323ce66 --- /dev/null +++ b/test/testdata/schema/ghc96/markdown-reference.md @@ -0,0 +1,66 @@ +## hlint +| Property | Description | Default | Allowed values | +| --- | --- | --- | --- | +| `flags` | Flags used by hlint | `TODO: Array values` |   | + +## cabal-fmt +| Property | Description | Default | Allowed values | +| --- | --- | --- | --- | +| `path` | Set path to 'cabal-fmt' executable | `"cabal-fmt"` |   | + +## ghcide-completions +| Property | Description | Default | Allowed values | +| --- | --- | --- | --- | +| `autoExtendOn` | Extends the import list automatically when completing a out-of-scope identifier | `True` |   | +| `snippetsOn` | Inserts snippets when using code completions | `True` |   | + +## eval +| Property | Description | Default | Allowed values | +| --- | --- | --- | --- | +| `exception` | Enable marking exceptions with `*** Exception:` similarly to doctest and GHCi. | `False` |   | +| `diff` | Enable the diff output (WAS/NOW) of eval lenses | `True` |   | + +## ghcide-type-lenses +| Property | Description | Default | Allowed values | +| --- | --- | --- | --- | +| `mode` | Control how type lenses are shown | `Always` |
  • Always
  • Exported
  • Diagnostics
| + +## ormolu +| Property | Description | Default | Allowed values | +| --- | --- | --- | --- | +| `external` | Call out to an external "ormolu" executable, rather than using the bundled library | `False` |   | + +## rename +| Property | Description | Default | Allowed values | +| --- | --- | --- | --- | +| `crossModule` | Enable experimental cross-module renaming | `False` |   | + +## semanticTokens +| Property | Description | Default | Allowed values | +| --- | --- | --- | --- | +| `variableToken` | LSP semantic token type to use for variables | `SemanticTokenTypes_Variable` |
  • SemanticTokenTypes_Namespace
  • SemanticTokenTypes_Type
  • SemanticTokenTypes_Class
  • SemanticTokenTypes_Enum
  • SemanticTokenTypes_Interface
  • SemanticTokenTypes_Struct
  • SemanticTokenTypes_TypeParameter
  • SemanticTokenTypes_Parameter
  • SemanticTokenTypes_Variable
  • SemanticTokenTypes_Property
  • SemanticTokenTypes_EnumMember
  • SemanticTokenTypes_Event
  • SemanticTokenTypes_Function
  • SemanticTokenTypes_Method
  • SemanticTokenTypes_Macro
  • SemanticTokenTypes_Keyword
  • SemanticTokenTypes_Modifier
  • SemanticTokenTypes_Comment
  • SemanticTokenTypes_String
  • SemanticTokenTypes_Number
  • SemanticTokenTypes_Regexp
  • SemanticTokenTypes_Operator
  • SemanticTokenTypes_Decorator
| +| `functionToken` | LSP semantic token type to use for functions | `SemanticTokenTypes_Function` |
  • SemanticTokenTypes_Namespace
  • SemanticTokenTypes_Type
  • SemanticTokenTypes_Class
  • SemanticTokenTypes_Enum
  • SemanticTokenTypes_Interface
  • SemanticTokenTypes_Struct
  • SemanticTokenTypes_TypeParameter
  • SemanticTokenTypes_Parameter
  • SemanticTokenTypes_Variable
  • SemanticTokenTypes_Property
  • SemanticTokenTypes_EnumMember
  • SemanticTokenTypes_Event
  • SemanticTokenTypes_Function
  • SemanticTokenTypes_Method
  • SemanticTokenTypes_Macro
  • SemanticTokenTypes_Keyword
  • SemanticTokenTypes_Modifier
  • SemanticTokenTypes_Comment
  • SemanticTokenTypes_String
  • SemanticTokenTypes_Number
  • SemanticTokenTypes_Regexp
  • SemanticTokenTypes_Operator
  • SemanticTokenTypes_Decorator
| +| `dataConstructorToken` | LSP semantic token type to use for data constructors | `SemanticTokenTypes_EnumMember` |
  • SemanticTokenTypes_Namespace
  • SemanticTokenTypes_Type
  • SemanticTokenTypes_Class
  • SemanticTokenTypes_Enum
  • SemanticTokenTypes_Interface
  • SemanticTokenTypes_Struct
  • SemanticTokenTypes_TypeParameter
  • SemanticTokenTypes_Parameter
  • SemanticTokenTypes_Variable
  • SemanticTokenTypes_Property
  • SemanticTokenTypes_EnumMember
  • SemanticTokenTypes_Event
  • SemanticTokenTypes_Function
  • SemanticTokenTypes_Method
  • SemanticTokenTypes_Macro
  • SemanticTokenTypes_Keyword
  • SemanticTokenTypes_Modifier
  • SemanticTokenTypes_Comment
  • SemanticTokenTypes_String
  • SemanticTokenTypes_Number
  • SemanticTokenTypes_Regexp
  • SemanticTokenTypes_Operator
  • SemanticTokenTypes_Decorator
| +| `typeVariableToken` | LSP semantic token type to use for type variables | `SemanticTokenTypes_TypeParameter` |
  • SemanticTokenTypes_Namespace
  • SemanticTokenTypes_Type
  • SemanticTokenTypes_Class
  • SemanticTokenTypes_Enum
  • SemanticTokenTypes_Interface
  • SemanticTokenTypes_Struct
  • SemanticTokenTypes_TypeParameter
  • SemanticTokenTypes_Parameter
  • SemanticTokenTypes_Variable
  • SemanticTokenTypes_Property
  • SemanticTokenTypes_EnumMember
  • SemanticTokenTypes_Event
  • SemanticTokenTypes_Function
  • SemanticTokenTypes_Method
  • SemanticTokenTypes_Macro
  • SemanticTokenTypes_Keyword
  • SemanticTokenTypes_Modifier
  • SemanticTokenTypes_Comment
  • SemanticTokenTypes_String
  • SemanticTokenTypes_Number
  • SemanticTokenTypes_Regexp
  • SemanticTokenTypes_Operator
  • SemanticTokenTypes_Decorator
| +| `classMethodToken` | LSP semantic token type to use for typeclass methods | `SemanticTokenTypes_Method` |
  • SemanticTokenTypes_Namespace
  • SemanticTokenTypes_Type
  • SemanticTokenTypes_Class
  • SemanticTokenTypes_Enum
  • SemanticTokenTypes_Interface
  • SemanticTokenTypes_Struct
  • SemanticTokenTypes_TypeParameter
  • SemanticTokenTypes_Parameter
  • SemanticTokenTypes_Variable
  • SemanticTokenTypes_Property
  • SemanticTokenTypes_EnumMember
  • SemanticTokenTypes_Event
  • SemanticTokenTypes_Function
  • SemanticTokenTypes_Method
  • SemanticTokenTypes_Macro
  • SemanticTokenTypes_Keyword
  • SemanticTokenTypes_Modifier
  • SemanticTokenTypes_Comment
  • SemanticTokenTypes_String
  • SemanticTokenTypes_Number
  • SemanticTokenTypes_Regexp
  • SemanticTokenTypes_Operator
  • SemanticTokenTypes_Decorator
| +| `patternSynonymToken` | LSP semantic token type to use for pattern synonyms | `SemanticTokenTypes_Macro` |
  • SemanticTokenTypes_Namespace
  • SemanticTokenTypes_Type
  • SemanticTokenTypes_Class
  • SemanticTokenTypes_Enum
  • SemanticTokenTypes_Interface
  • SemanticTokenTypes_Struct
  • SemanticTokenTypes_TypeParameter
  • SemanticTokenTypes_Parameter
  • SemanticTokenTypes_Variable
  • SemanticTokenTypes_Property
  • SemanticTokenTypes_EnumMember
  • SemanticTokenTypes_Event
  • SemanticTokenTypes_Function
  • SemanticTokenTypes_Method
  • SemanticTokenTypes_Macro
  • SemanticTokenTypes_Keyword
  • SemanticTokenTypes_Modifier
  • SemanticTokenTypes_Comment
  • SemanticTokenTypes_String
  • SemanticTokenTypes_Number
  • SemanticTokenTypes_Regexp
  • SemanticTokenTypes_Operator
  • SemanticTokenTypes_Decorator
| +| `typeConstructorToken` | LSP semantic token type to use for type constructors | `SemanticTokenTypes_Enum` |
  • SemanticTokenTypes_Namespace
  • SemanticTokenTypes_Type
  • SemanticTokenTypes_Class
  • SemanticTokenTypes_Enum
  • SemanticTokenTypes_Interface
  • SemanticTokenTypes_Struct
  • SemanticTokenTypes_TypeParameter
  • SemanticTokenTypes_Parameter
  • SemanticTokenTypes_Variable
  • SemanticTokenTypes_Property
  • SemanticTokenTypes_EnumMember
  • SemanticTokenTypes_Event
  • SemanticTokenTypes_Function
  • SemanticTokenTypes_Method
  • SemanticTokenTypes_Macro
  • SemanticTokenTypes_Keyword
  • SemanticTokenTypes_Modifier
  • SemanticTokenTypes_Comment
  • SemanticTokenTypes_String
  • SemanticTokenTypes_Number
  • SemanticTokenTypes_Regexp
  • SemanticTokenTypes_Operator
  • SemanticTokenTypes_Decorator
| +| `classToken` | LSP semantic token type to use for typeclasses | `SemanticTokenTypes_Class` |
  • SemanticTokenTypes_Namespace
  • SemanticTokenTypes_Type
  • SemanticTokenTypes_Class
  • SemanticTokenTypes_Enum
  • SemanticTokenTypes_Interface
  • SemanticTokenTypes_Struct
  • SemanticTokenTypes_TypeParameter
  • SemanticTokenTypes_Parameter
  • SemanticTokenTypes_Variable
  • SemanticTokenTypes_Property
  • SemanticTokenTypes_EnumMember
  • SemanticTokenTypes_Event
  • SemanticTokenTypes_Function
  • SemanticTokenTypes_Method
  • SemanticTokenTypes_Macro
  • SemanticTokenTypes_Keyword
  • SemanticTokenTypes_Modifier
  • SemanticTokenTypes_Comment
  • SemanticTokenTypes_String
  • SemanticTokenTypes_Number
  • SemanticTokenTypes_Regexp
  • SemanticTokenTypes_Operator
  • SemanticTokenTypes_Decorator
| +| `typeSynonymToken` | LSP semantic token type to use for type synonyms | `SemanticTokenTypes_Type` |
  • SemanticTokenTypes_Namespace
  • SemanticTokenTypes_Type
  • SemanticTokenTypes_Class
  • SemanticTokenTypes_Enum
  • SemanticTokenTypes_Interface
  • SemanticTokenTypes_Struct
  • SemanticTokenTypes_TypeParameter
  • SemanticTokenTypes_Parameter
  • SemanticTokenTypes_Variable
  • SemanticTokenTypes_Property
  • SemanticTokenTypes_EnumMember
  • SemanticTokenTypes_Event
  • SemanticTokenTypes_Function
  • SemanticTokenTypes_Method
  • SemanticTokenTypes_Macro
  • SemanticTokenTypes_Keyword
  • SemanticTokenTypes_Modifier
  • SemanticTokenTypes_Comment
  • SemanticTokenTypes_String
  • SemanticTokenTypes_Number
  • SemanticTokenTypes_Regexp
  • SemanticTokenTypes_Operator
  • SemanticTokenTypes_Decorator
| +| `typeFamilyToken` | LSP semantic token type to use for type families | `SemanticTokenTypes_Interface` |
  • SemanticTokenTypes_Namespace
  • SemanticTokenTypes_Type
  • SemanticTokenTypes_Class
  • SemanticTokenTypes_Enum
  • SemanticTokenTypes_Interface
  • SemanticTokenTypes_Struct
  • SemanticTokenTypes_TypeParameter
  • SemanticTokenTypes_Parameter
  • SemanticTokenTypes_Variable
  • SemanticTokenTypes_Property
  • SemanticTokenTypes_EnumMember
  • SemanticTokenTypes_Event
  • SemanticTokenTypes_Function
  • SemanticTokenTypes_Method
  • SemanticTokenTypes_Macro
  • SemanticTokenTypes_Keyword
  • SemanticTokenTypes_Modifier
  • SemanticTokenTypes_Comment
  • SemanticTokenTypes_String
  • SemanticTokenTypes_Number
  • SemanticTokenTypes_Regexp
  • SemanticTokenTypes_Operator
  • SemanticTokenTypes_Decorator
| +| `recordFieldToken` | LSP semantic token type to use for record fields | `SemanticTokenTypes_Property` |
  • SemanticTokenTypes_Namespace
  • SemanticTokenTypes_Type
  • SemanticTokenTypes_Class
  • SemanticTokenTypes_Enum
  • SemanticTokenTypes_Interface
  • SemanticTokenTypes_Struct
  • SemanticTokenTypes_TypeParameter
  • SemanticTokenTypes_Parameter
  • SemanticTokenTypes_Variable
  • SemanticTokenTypes_Property
  • SemanticTokenTypes_EnumMember
  • SemanticTokenTypes_Event
  • SemanticTokenTypes_Function
  • SemanticTokenTypes_Method
  • SemanticTokenTypes_Macro
  • SemanticTokenTypes_Keyword
  • SemanticTokenTypes_Modifier
  • SemanticTokenTypes_Comment
  • SemanticTokenTypes_String
  • SemanticTokenTypes_Number
  • SemanticTokenTypes_Regexp
  • SemanticTokenTypes_Operator
  • SemanticTokenTypes_Decorator
| +| `operatorToken` | LSP semantic token type to use for operators | `SemanticTokenTypes_Operator` |
  • SemanticTokenTypes_Namespace
  • SemanticTokenTypes_Type
  • SemanticTokenTypes_Class
  • SemanticTokenTypes_Enum
  • SemanticTokenTypes_Interface
  • SemanticTokenTypes_Struct
  • SemanticTokenTypes_TypeParameter
  • SemanticTokenTypes_Parameter
  • SemanticTokenTypes_Variable
  • SemanticTokenTypes_Property
  • SemanticTokenTypes_EnumMember
  • SemanticTokenTypes_Event
  • SemanticTokenTypes_Function
  • SemanticTokenTypes_Method
  • SemanticTokenTypes_Macro
  • SemanticTokenTypes_Keyword
  • SemanticTokenTypes_Modifier
  • SemanticTokenTypes_Comment
  • SemanticTokenTypes_String
  • SemanticTokenTypes_Number
  • SemanticTokenTypes_Regexp
  • SemanticTokenTypes_Operator
  • SemanticTokenTypes_Decorator
| +| `moduleToken` | LSP semantic token type to use for modules | `SemanticTokenTypes_Namespace` |
  • SemanticTokenTypes_Namespace
  • SemanticTokenTypes_Type
  • SemanticTokenTypes_Class
  • SemanticTokenTypes_Enum
  • SemanticTokenTypes_Interface
  • SemanticTokenTypes_Struct
  • SemanticTokenTypes_TypeParameter
  • SemanticTokenTypes_Parameter
  • SemanticTokenTypes_Variable
  • SemanticTokenTypes_Property
  • SemanticTokenTypes_EnumMember
  • SemanticTokenTypes_Event
  • SemanticTokenTypes_Function
  • SemanticTokenTypes_Method
  • SemanticTokenTypes_Macro
  • SemanticTokenTypes_Keyword
  • SemanticTokenTypes_Modifier
  • SemanticTokenTypes_Comment
  • SemanticTokenTypes_String
  • SemanticTokenTypes_Number
  • SemanticTokenTypes_Regexp
  • SemanticTokenTypes_Operator
  • SemanticTokenTypes_Decorator
| + +## fourmolu +| Property | Description | Default | Allowed values | +| --- | --- | --- | --- | +| `external` | Call out to an external "fourmolu" executable, rather than using the bundled library. | `False` |   | +| `path` | Set path to executable (for "external" mode). | `"fourmolu"` |   | + +## cabal-gild +| Property | Description | Default | Allowed values | +| --- | --- | --- | --- | +| `path` | Set path to 'cabal-gild' executable | `"cabal-gild"` |   | + + diff --git a/test/testdata/schema/ghc96/vscode-extension-schema.golden.json b/test/testdata/schema/ghc96/vscode-extension-schema.golden.json new file mode 100644 index 0000000000..1c0b19eb27 --- /dev/null +++ b/test/testdata/schema/ghc96/vscode-extension-schema.golden.json @@ -0,0 +1,1058 @@ +{ + "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.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" + } +} diff --git a/test/testdata/schema/ghc98/default-config.golden.json b/test/testdata/schema/ghc98/default-config.golden.json new file mode 100644 index 0000000000..8467b451f1 --- /dev/null +++ b/test/testdata/schema/ghc98/default-config.golden.json @@ -0,0 +1,164 @@ +{ + "cabalFormattingProvider": "cabal-gild", + "checkParents": "CheckOnSave", + "checkProject": true, + "formattingProvider": "ormolu", + "maxCompletions": 40, + "plugin": { + "alternateNumberFormat": { + "globalOn": true + }, + "cabal": { + "codeActionsOn": true, + "completionOn": true, + "diagnosticsOn": true, + "hoverOn": true, + "symbolsOn": true + }, + "cabal-fmt": { + "config": { + "path": "cabal-fmt" + } + }, + "cabal-gild": { + "config": { + "path": "cabal-gild" + } + }, + "cabalHaskellIntegration": { + "globalOn": true + }, + "callHierarchy": { + "globalOn": true + }, + "changeTypeSignature": { + "globalOn": true + }, + "class": { + "codeActionsOn": true, + "codeLensOn": true + }, + "eval": { + "codeActionsOn": true, + "codeLensOn": true, + "config": { + "diff": true, + "exception": false + } + }, + "explicit-fields": { + "codeActionsOn": true, + "inlayHintsOn": true + }, + "explicit-fixity": { + "globalOn": true + }, + "fourmolu": { + "config": { + "external": false, + "path": "fourmolu" + } + }, + "gadt": { + "globalOn": true + }, + "ghcide-code-actions-bindings": { + "globalOn": true + }, + "ghcide-code-actions-fill-holes": { + "globalOn": true + }, + "ghcide-code-actions-imports-exports": { + "globalOn": true + }, + "ghcide-code-actions-type-signatures": { + "globalOn": true + }, + "ghcide-completions": { + "config": { + "autoExtendOn": true, + "snippetsOn": true + }, + "globalOn": true + }, + "ghcide-hover-and-symbols": { + "hoverOn": true, + "symbolsOn": true + }, + "ghcide-type-lenses": { + "config": { + "mode": "always" + }, + "globalOn": true + }, + "hlint": { + "codeActionsOn": true, + "config": { + "flags": [] + }, + "diagnosticsOn": true + }, + "importLens": { + "codeActionsOn": true, + "codeLensOn": true, + "inlayHintsOn": true + }, + "moduleName": { + "globalOn": true + }, + "ormolu": { + "config": { + "external": false + } + }, + "overloaded-record-dot": { + "globalOn": true + }, + "pragmas-completion": { + "globalOn": true + }, + "pragmas-disable": { + "globalOn": true + }, + "pragmas-suggest": { + "globalOn": true + }, + "qualifyImportedNames": { + "globalOn": true + }, + "rename": { + "config": { + "crossModule": false + }, + "globalOn": true + }, + "retrie": { + "globalOn": true + }, + "semanticTokens": { + "config": { + "classMethodToken": "method", + "classToken": "class", + "dataConstructorToken": "enumMember", + "functionToken": "function", + "moduleToken": "namespace", + "operatorToken": "operator", + "patternSynonymToken": "macro", + "recordFieldToken": "property", + "typeConstructorToken": "enum", + "typeFamilyToken": "interface", + "typeSynonymToken": "type", + "typeVariableToken": "typeParameter", + "variableToken": "variable" + }, + "globalOn": false + }, + "splice": { + "globalOn": true + }, + "stan": { + "globalOn": false + } + }, + "sessionLoading": "singleComponent" +} diff --git a/test/testdata/schema/ghc98/markdown-reference.md b/test/testdata/schema/ghc98/markdown-reference.md new file mode 100644 index 0000000000..668323ce66 --- /dev/null +++ b/test/testdata/schema/ghc98/markdown-reference.md @@ -0,0 +1,66 @@ +## hlint +| Property | Description | Default | Allowed values | +| --- | --- | --- | --- | +| `flags` | Flags used by hlint | `TODO: Array values` |   | + +## cabal-fmt +| Property | Description | Default | Allowed values | +| --- | --- | --- | --- | +| `path` | Set path to 'cabal-fmt' executable | `"cabal-fmt"` |   | + +## ghcide-completions +| Property | Description | Default | Allowed values | +| --- | --- | --- | --- | +| `autoExtendOn` | Extends the import list automatically when completing a out-of-scope identifier | `True` |   | +| `snippetsOn` | Inserts snippets when using code completions | `True` |   | + +## eval +| Property | Description | Default | Allowed values | +| --- | --- | --- | --- | +| `exception` | Enable marking exceptions with `*** Exception:` similarly to doctest and GHCi. | `False` |   | +| `diff` | Enable the diff output (WAS/NOW) of eval lenses | `True` |   | + +## ghcide-type-lenses +| Property | Description | Default | Allowed values | +| --- | --- | --- | --- | +| `mode` | Control how type lenses are shown | `Always` |
  • Always
  • Exported
  • Diagnostics
| + +## ormolu +| Property | Description | Default | Allowed values | +| --- | --- | --- | --- | +| `external` | Call out to an external "ormolu" executable, rather than using the bundled library | `False` |   | + +## rename +| Property | Description | Default | Allowed values | +| --- | --- | --- | --- | +| `crossModule` | Enable experimental cross-module renaming | `False` |   | + +## semanticTokens +| Property | Description | Default | Allowed values | +| --- | --- | --- | --- | +| `variableToken` | LSP semantic token type to use for variables | `SemanticTokenTypes_Variable` |
  • SemanticTokenTypes_Namespace
  • SemanticTokenTypes_Type
  • SemanticTokenTypes_Class
  • SemanticTokenTypes_Enum
  • SemanticTokenTypes_Interface
  • SemanticTokenTypes_Struct
  • SemanticTokenTypes_TypeParameter
  • SemanticTokenTypes_Parameter
  • SemanticTokenTypes_Variable
  • SemanticTokenTypes_Property
  • SemanticTokenTypes_EnumMember
  • SemanticTokenTypes_Event
  • SemanticTokenTypes_Function
  • SemanticTokenTypes_Method
  • SemanticTokenTypes_Macro
  • SemanticTokenTypes_Keyword
  • SemanticTokenTypes_Modifier
  • SemanticTokenTypes_Comment
  • SemanticTokenTypes_String
  • SemanticTokenTypes_Number
  • SemanticTokenTypes_Regexp
  • SemanticTokenTypes_Operator
  • SemanticTokenTypes_Decorator
| +| `functionToken` | LSP semantic token type to use for functions | `SemanticTokenTypes_Function` |
  • SemanticTokenTypes_Namespace
  • SemanticTokenTypes_Type
  • SemanticTokenTypes_Class
  • SemanticTokenTypes_Enum
  • SemanticTokenTypes_Interface
  • SemanticTokenTypes_Struct
  • SemanticTokenTypes_TypeParameter
  • SemanticTokenTypes_Parameter
  • SemanticTokenTypes_Variable
  • SemanticTokenTypes_Property
  • SemanticTokenTypes_EnumMember
  • SemanticTokenTypes_Event
  • SemanticTokenTypes_Function
  • SemanticTokenTypes_Method
  • SemanticTokenTypes_Macro
  • SemanticTokenTypes_Keyword
  • SemanticTokenTypes_Modifier
  • SemanticTokenTypes_Comment
  • SemanticTokenTypes_String
  • SemanticTokenTypes_Number
  • SemanticTokenTypes_Regexp
  • SemanticTokenTypes_Operator
  • SemanticTokenTypes_Decorator
| +| `dataConstructorToken` | LSP semantic token type to use for data constructors | `SemanticTokenTypes_EnumMember` |
  • SemanticTokenTypes_Namespace
  • SemanticTokenTypes_Type
  • SemanticTokenTypes_Class
  • SemanticTokenTypes_Enum
  • SemanticTokenTypes_Interface
  • SemanticTokenTypes_Struct
  • SemanticTokenTypes_TypeParameter
  • SemanticTokenTypes_Parameter
  • SemanticTokenTypes_Variable
  • SemanticTokenTypes_Property
  • SemanticTokenTypes_EnumMember
  • SemanticTokenTypes_Event
  • SemanticTokenTypes_Function
  • SemanticTokenTypes_Method
  • SemanticTokenTypes_Macro
  • SemanticTokenTypes_Keyword
  • SemanticTokenTypes_Modifier
  • SemanticTokenTypes_Comment
  • SemanticTokenTypes_String
  • SemanticTokenTypes_Number
  • SemanticTokenTypes_Regexp
  • SemanticTokenTypes_Operator
  • SemanticTokenTypes_Decorator
| +| `typeVariableToken` | LSP semantic token type to use for type variables | `SemanticTokenTypes_TypeParameter` |
  • SemanticTokenTypes_Namespace
  • SemanticTokenTypes_Type
  • SemanticTokenTypes_Class
  • SemanticTokenTypes_Enum
  • SemanticTokenTypes_Interface
  • SemanticTokenTypes_Struct
  • SemanticTokenTypes_TypeParameter
  • SemanticTokenTypes_Parameter
  • SemanticTokenTypes_Variable
  • SemanticTokenTypes_Property
  • SemanticTokenTypes_EnumMember
  • SemanticTokenTypes_Event
  • SemanticTokenTypes_Function
  • SemanticTokenTypes_Method
  • SemanticTokenTypes_Macro
  • SemanticTokenTypes_Keyword
  • SemanticTokenTypes_Modifier
  • SemanticTokenTypes_Comment
  • SemanticTokenTypes_String
  • SemanticTokenTypes_Number
  • SemanticTokenTypes_Regexp
  • SemanticTokenTypes_Operator
  • SemanticTokenTypes_Decorator
| +| `classMethodToken` | LSP semantic token type to use for typeclass methods | `SemanticTokenTypes_Method` |
  • SemanticTokenTypes_Namespace
  • SemanticTokenTypes_Type
  • SemanticTokenTypes_Class
  • SemanticTokenTypes_Enum
  • SemanticTokenTypes_Interface
  • SemanticTokenTypes_Struct
  • SemanticTokenTypes_TypeParameter
  • SemanticTokenTypes_Parameter
  • SemanticTokenTypes_Variable
  • SemanticTokenTypes_Property
  • SemanticTokenTypes_EnumMember
  • SemanticTokenTypes_Event
  • SemanticTokenTypes_Function
  • SemanticTokenTypes_Method
  • SemanticTokenTypes_Macro
  • SemanticTokenTypes_Keyword
  • SemanticTokenTypes_Modifier
  • SemanticTokenTypes_Comment
  • SemanticTokenTypes_String
  • SemanticTokenTypes_Number
  • SemanticTokenTypes_Regexp
  • SemanticTokenTypes_Operator
  • SemanticTokenTypes_Decorator
| +| `patternSynonymToken` | LSP semantic token type to use for pattern synonyms | `SemanticTokenTypes_Macro` |
  • SemanticTokenTypes_Namespace
  • SemanticTokenTypes_Type
  • SemanticTokenTypes_Class
  • SemanticTokenTypes_Enum
  • SemanticTokenTypes_Interface
  • SemanticTokenTypes_Struct
  • SemanticTokenTypes_TypeParameter
  • SemanticTokenTypes_Parameter
  • SemanticTokenTypes_Variable
  • SemanticTokenTypes_Property
  • SemanticTokenTypes_EnumMember
  • SemanticTokenTypes_Event
  • SemanticTokenTypes_Function
  • SemanticTokenTypes_Method
  • SemanticTokenTypes_Macro
  • SemanticTokenTypes_Keyword
  • SemanticTokenTypes_Modifier
  • SemanticTokenTypes_Comment
  • SemanticTokenTypes_String
  • SemanticTokenTypes_Number
  • SemanticTokenTypes_Regexp
  • SemanticTokenTypes_Operator
  • SemanticTokenTypes_Decorator
| +| `typeConstructorToken` | LSP semantic token type to use for type constructors | `SemanticTokenTypes_Enum` |
  • SemanticTokenTypes_Namespace
  • SemanticTokenTypes_Type
  • SemanticTokenTypes_Class
  • SemanticTokenTypes_Enum
  • SemanticTokenTypes_Interface
  • SemanticTokenTypes_Struct
  • SemanticTokenTypes_TypeParameter
  • SemanticTokenTypes_Parameter
  • SemanticTokenTypes_Variable
  • SemanticTokenTypes_Property
  • SemanticTokenTypes_EnumMember
  • SemanticTokenTypes_Event
  • SemanticTokenTypes_Function
  • SemanticTokenTypes_Method
  • SemanticTokenTypes_Macro
  • SemanticTokenTypes_Keyword
  • SemanticTokenTypes_Modifier
  • SemanticTokenTypes_Comment
  • SemanticTokenTypes_String
  • SemanticTokenTypes_Number
  • SemanticTokenTypes_Regexp
  • SemanticTokenTypes_Operator
  • SemanticTokenTypes_Decorator
| +| `classToken` | LSP semantic token type to use for typeclasses | `SemanticTokenTypes_Class` |
  • SemanticTokenTypes_Namespace
  • SemanticTokenTypes_Type
  • SemanticTokenTypes_Class
  • SemanticTokenTypes_Enum
  • SemanticTokenTypes_Interface
  • SemanticTokenTypes_Struct
  • SemanticTokenTypes_TypeParameter
  • SemanticTokenTypes_Parameter
  • SemanticTokenTypes_Variable
  • SemanticTokenTypes_Property
  • SemanticTokenTypes_EnumMember
  • SemanticTokenTypes_Event
  • SemanticTokenTypes_Function
  • SemanticTokenTypes_Method
  • SemanticTokenTypes_Macro
  • SemanticTokenTypes_Keyword
  • SemanticTokenTypes_Modifier
  • SemanticTokenTypes_Comment
  • SemanticTokenTypes_String
  • SemanticTokenTypes_Number
  • SemanticTokenTypes_Regexp
  • SemanticTokenTypes_Operator
  • SemanticTokenTypes_Decorator
| +| `typeSynonymToken` | LSP semantic token type to use for type synonyms | `SemanticTokenTypes_Type` |
  • SemanticTokenTypes_Namespace
  • SemanticTokenTypes_Type
  • SemanticTokenTypes_Class
  • SemanticTokenTypes_Enum
  • SemanticTokenTypes_Interface
  • SemanticTokenTypes_Struct
  • SemanticTokenTypes_TypeParameter
  • SemanticTokenTypes_Parameter
  • SemanticTokenTypes_Variable
  • SemanticTokenTypes_Property
  • SemanticTokenTypes_EnumMember
  • SemanticTokenTypes_Event
  • SemanticTokenTypes_Function
  • SemanticTokenTypes_Method
  • SemanticTokenTypes_Macro
  • SemanticTokenTypes_Keyword
  • SemanticTokenTypes_Modifier
  • SemanticTokenTypes_Comment
  • SemanticTokenTypes_String
  • SemanticTokenTypes_Number
  • SemanticTokenTypes_Regexp
  • SemanticTokenTypes_Operator
  • SemanticTokenTypes_Decorator
| +| `typeFamilyToken` | LSP semantic token type to use for type families | `SemanticTokenTypes_Interface` |
  • SemanticTokenTypes_Namespace
  • SemanticTokenTypes_Type
  • SemanticTokenTypes_Class
  • SemanticTokenTypes_Enum
  • SemanticTokenTypes_Interface
  • SemanticTokenTypes_Struct
  • SemanticTokenTypes_TypeParameter
  • SemanticTokenTypes_Parameter
  • SemanticTokenTypes_Variable
  • SemanticTokenTypes_Property
  • SemanticTokenTypes_EnumMember
  • SemanticTokenTypes_Event
  • SemanticTokenTypes_Function
  • SemanticTokenTypes_Method
  • SemanticTokenTypes_Macro
  • SemanticTokenTypes_Keyword
  • SemanticTokenTypes_Modifier
  • SemanticTokenTypes_Comment
  • SemanticTokenTypes_String
  • SemanticTokenTypes_Number
  • SemanticTokenTypes_Regexp
  • SemanticTokenTypes_Operator
  • SemanticTokenTypes_Decorator
| +| `recordFieldToken` | LSP semantic token type to use for record fields | `SemanticTokenTypes_Property` |
  • SemanticTokenTypes_Namespace
  • SemanticTokenTypes_Type
  • SemanticTokenTypes_Class
  • SemanticTokenTypes_Enum
  • SemanticTokenTypes_Interface
  • SemanticTokenTypes_Struct
  • SemanticTokenTypes_TypeParameter
  • SemanticTokenTypes_Parameter
  • SemanticTokenTypes_Variable
  • SemanticTokenTypes_Property
  • SemanticTokenTypes_EnumMember
  • SemanticTokenTypes_Event
  • SemanticTokenTypes_Function
  • SemanticTokenTypes_Method
  • SemanticTokenTypes_Macro
  • SemanticTokenTypes_Keyword
  • SemanticTokenTypes_Modifier
  • SemanticTokenTypes_Comment
  • SemanticTokenTypes_String
  • SemanticTokenTypes_Number
  • SemanticTokenTypes_Regexp
  • SemanticTokenTypes_Operator
  • SemanticTokenTypes_Decorator
| +| `operatorToken` | LSP semantic token type to use for operators | `SemanticTokenTypes_Operator` |
  • SemanticTokenTypes_Namespace
  • SemanticTokenTypes_Type
  • SemanticTokenTypes_Class
  • SemanticTokenTypes_Enum
  • SemanticTokenTypes_Interface
  • SemanticTokenTypes_Struct
  • SemanticTokenTypes_TypeParameter
  • SemanticTokenTypes_Parameter
  • SemanticTokenTypes_Variable
  • SemanticTokenTypes_Property
  • SemanticTokenTypes_EnumMember
  • SemanticTokenTypes_Event
  • SemanticTokenTypes_Function
  • SemanticTokenTypes_Method
  • SemanticTokenTypes_Macro
  • SemanticTokenTypes_Keyword
  • SemanticTokenTypes_Modifier
  • SemanticTokenTypes_Comment
  • SemanticTokenTypes_String
  • SemanticTokenTypes_Number
  • SemanticTokenTypes_Regexp
  • SemanticTokenTypes_Operator
  • SemanticTokenTypes_Decorator
| +| `moduleToken` | LSP semantic token type to use for modules | `SemanticTokenTypes_Namespace` |
  • SemanticTokenTypes_Namespace
  • SemanticTokenTypes_Type
  • SemanticTokenTypes_Class
  • SemanticTokenTypes_Enum
  • SemanticTokenTypes_Interface
  • SemanticTokenTypes_Struct
  • SemanticTokenTypes_TypeParameter
  • SemanticTokenTypes_Parameter
  • SemanticTokenTypes_Variable
  • SemanticTokenTypes_Property
  • SemanticTokenTypes_EnumMember
  • SemanticTokenTypes_Event
  • SemanticTokenTypes_Function
  • SemanticTokenTypes_Method
  • SemanticTokenTypes_Macro
  • SemanticTokenTypes_Keyword
  • SemanticTokenTypes_Modifier
  • SemanticTokenTypes_Comment
  • SemanticTokenTypes_String
  • SemanticTokenTypes_Number
  • SemanticTokenTypes_Regexp
  • SemanticTokenTypes_Operator
  • SemanticTokenTypes_Decorator
| + +## fourmolu +| Property | Description | Default | Allowed values | +| --- | --- | --- | --- | +| `external` | Call out to an external "fourmolu" executable, rather than using the bundled library. | `False` |   | +| `path` | Set path to executable (for "external" mode). | `"fourmolu"` |   | + +## cabal-gild +| Property | Description | Default | Allowed values | +| --- | --- | --- | --- | +| `path` | Set path to 'cabal-gild' executable | `"cabal-gild"` |   | + + diff --git a/test/testdata/schema/ghc98/vscode-extension-schema.golden.json b/test/testdata/schema/ghc98/vscode-extension-schema.golden.json new file mode 100644 index 0000000000..1c0b19eb27 --- /dev/null +++ b/test/testdata/schema/ghc98/vscode-extension-schema.golden.json @@ -0,0 +1,1058 @@ +{ + "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.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" + } +} diff --git a/test/utils/Test/Hls/Command.hs b/test/utils/Test/Hls/Command.hs index c8e7d4de45..b0e0febc3c 100644 --- a/test/utils/Test/Hls/Command.hs +++ b/test/utils/Test/Hls/Command.hs @@ -1,5 +1,8 @@ module Test.Hls.Command - ( hlsCommand + ( hlsExeCommand + , hlsLspCommand + , hlsWrapperLspCommand + , hlsWrapperExeCommand ) where @@ -12,8 +15,20 @@ import System.IO.Unsafe (unsafePerformIO) -- Both @stack test@ and @cabal new-test@ setup the environment so @hls@ is -- on PATH. Cabal seems to respond to @build-tool-depends@ specifically while -- stack just puts all project executables on PATH. -hlsCommand :: String -{-# NOINLINE hlsCommand #-} -hlsCommand = unsafePerformIO $ do +hlsExeCommand :: String +{-# NOINLINE hlsExeCommand #-} +hlsExeCommand = unsafePerformIO $ do testExe <- fromMaybe "haskell-language-server" <$> lookupEnv "HLS_TEST_EXE" - pure $ testExe ++ " --lsp -d -j4" + pure testExe + +hlsLspCommand :: String +hlsLspCommand = hlsExeCommand ++ " --lsp --test -d -j4" + +hlsWrapperLspCommand :: String +hlsWrapperLspCommand = hlsWrapperExeCommand ++ " --lsp --test -d -j4" + +hlsWrapperExeCommand :: String +{-# NOINLINE hlsWrapperExeCommand #-} +hlsWrapperExeCommand = unsafePerformIO $ do + testExe <- fromMaybe "haskell-language-server-wrapper" <$> lookupEnv "HLS_WRAPPER_TEST_EXE" + pure testExe diff --git a/test/utils/Test/Hls/Flags.hs b/test/utils/Test/Hls/Flags.hs index 7ff17af076..8e60ebb93e 100644 --- a/test/utils/Test/Hls/Flags.hs +++ b/test/utils/Test/Hls/Flags.hs @@ -10,7 +10,7 @@ import Test.Hls (TestTree, ignoreTestBecause) -- | Disable test unless the eval flag is set requiresEvalPlugin :: TestTree -> TestTree -#if eval +#if hls_eval requiresEvalPlugin = id #else requiresEvalPlugin = ignoreTestBecause "Eval plugin disabled" @@ -19,7 +19,7 @@ requiresEvalPlugin = ignoreTestBecause "Eval plugin disabled" -- * Formatters -- | Disable test unless the floskell flag is set requiresFloskellPlugin :: TestTree -> TestTree -#if floskell +#if hls_floskell requiresFloskellPlugin = id #else requiresFloskellPlugin = ignoreTestBecause "Floskell plugin disabled" @@ -27,7 +27,7 @@ requiresFloskellPlugin = ignoreTestBecause "Floskell plugin disabled" -- | Disable test unless the fourmolu flag is set requiresFourmoluPlugin :: TestTree -> TestTree -#if fourmolu +#if hls_fourmolu requiresFourmoluPlugin = id #else requiresFourmoluPlugin = ignoreTestBecause "Fourmolu plugin disabled" @@ -35,7 +35,7 @@ requiresFourmoluPlugin = ignoreTestBecause "Fourmolu plugin disabled" -- | Disable test unless the ormolu flag is set requiresOrmoluPlugin :: TestTree -> TestTree -#if ormolu +#if hls_ormolu requiresOrmoluPlugin = id #else requiresOrmoluPlugin = ignoreTestBecause "Ormolu plugin disabled" diff --git a/test/wrapper/Main.hs b/test/wrapper/Main.hs index 4879d23603..0fbfa76b7a 100644 --- a/test/wrapper/Main.hs +++ b/test/wrapper/Main.hs @@ -9,10 +9,16 @@ main = defaultTestRunner $ testGroup "haskell-language-server-wrapper" [projectG projectGhcVersionTests :: TestTree projectGhcVersionTests = testGroup "--project-ghc-version" - [ stackTest "9.2.8" + [ testCase "stack with global ghc" $ do + ghcVer <- ghcNumericVersion + let writeStackYaml = writeFile "stack.yaml" $ + -- Use system-ghc and install-ghc to avoid stack downloading ghc in CI + -- (and use ghcup-managed ghc instead) + "{resolver: ghc-" ++ ghcVer ++ ", system-ghc: true, install-ghc: false}" + testDir writeStackYaml "test/wrapper/testdata/stack-specific-ghc" ghcVer , testCase "cabal with global ghc" $ do - ghcVer <- trimEnd <$> readProcess "ghc" ["--numeric-version"] "" - testDir "test/wrapper/testdata/cabal-cur-ver" ghcVer + ghcVer <- ghcNumericVersion + testDir (pure ()) "test/wrapper/testdata/cabal-cur-ver" ghcVer , testCase "stack with existing cabal build artifact" $ do -- Should report cabal as existing build artifacts are more important than -- the existence of 'stack.yaml' @@ -20,12 +26,12 @@ projectGhcVersionTests = testGroup "--project-ghc-version" ("cradleOptsProg = CradleAction: Cabal" `isInfixOf`) ] where - stackTest ghcVer= testCase ("stack with ghc " ++ ghcVer) $ - testDir ("test/wrapper/testdata/stack-" ++ ghcVer) ghcVer + ghcNumericVersion = trimEnd <$> readProcess "ghc" ["--numeric-version"] "" -testDir :: FilePath -> String -> Assertion -testDir dir expectedVer = +testDir :: IO () -> FilePath -> String -> Assertion +testDir extraSetup dir expectedVer = withCurrentDirectoryInTmp dir $ do + extraSetup testExe <- fromMaybe "haskell-language-server-wrapper" <$> lookupEnv "HLS_WRAPPER_TEST_EXE" actualVer <- trimEnd <$> readProcess testExe ["--project-ghc-version"] "" diff --git a/test/wrapper/testdata/stack-9.2.8/stack.yaml b/test/wrapper/testdata/stack-9.2.8/stack.yaml deleted file mode 100644 index 4324da7693..0000000000 --- a/test/wrapper/testdata/stack-9.2.8/stack.yaml +++ /dev/null @@ -1 +0,0 @@ -resolver: ghc-9.2.8 diff --git a/test/wrapper/testdata/stack-9.2.8/Lib.hs b/test/wrapper/testdata/stack-specific-ghc/Lib.hs similarity index 100% rename from test/wrapper/testdata/stack-9.2.8/Lib.hs rename to test/wrapper/testdata/stack-specific-ghc/Lib.hs diff --git a/test/wrapper/testdata/stack-9.2.8/foo.cabal b/test/wrapper/testdata/stack-specific-ghc/foo.cabal similarity index 100% rename from test/wrapper/testdata/stack-9.2.8/foo.cabal rename to test/wrapper/testdata/stack-specific-ghc/foo.cabal diff --git a/test/wrapper/testdata/stack-with-dist-newstyle/stack.yaml b/test/wrapper/testdata/stack-with-dist-newstyle/stack.yaml index e467bdb282..d95c1a7a03 100644 --- a/test/wrapper/testdata/stack-with-dist-newstyle/stack.yaml +++ b/test/wrapper/testdata/stack-with-dist-newstyle/stack.yaml @@ -1,2 +1,2 @@ # specific version does not matter -resolver: ghc-9.2.5 +resolver: ghc-9.6.5