Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -267,6 +267,10 @@ jobs:
name: Test hls-export-plugin test suite
run: cabal test ${CABAL_ARGS} hls-export-plugin-tests || cabal test ${CABAL_ARGS} hls-export-plugin-tests

- if: matrix.test && matrix.ghc == '9.14'
name: Test hls-case-split-plugin test suite
run: cabal test ${CABAL_ARGS} hls-case-split-plugin-tests || cabal test ${CABAL_ARGS} hls-case-split-plugin-tests

test_post_job:
if: always()
runs-on: ubuntu-latest
Expand Down
2 changes: 1 addition & 1 deletion cabal.project
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ packages:
./hls-plugin-api
./hls-test-utils

index-state: 2026-07-30T14:41:11Z
index-state: 2026-08-07T04:49:30Z

tests: True
test-show-details: direct
Expand Down
9 changes: 9 additions & 0 deletions docs/features.md
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,15 @@ Provided by: `hls-pragmas-plugin`

Completions for language pragmas.

### `case`/`\case` pattern completion

Provided by: `hls-case-split-plugin`

Completion of the patterns of a `case`/`\case` expression.

Note: The number of patterns that are inserted is limited to the value of
`-fmax-uncovered-patterns` plus 1.

## Formatting

Format your code with various Haskell code formatters.
Expand Down
6 changes: 6 additions & 0 deletions ghcide/src/Development/IDE/GHC/Compat/Error.hs
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ module Development.IDE.GHC.Compat.Error (
_TcRnMessageWithCtx,
_GhcPsMessage,
_GhcDsMessage,
_DsMessage,
_GhcDriverMessage,
_ReportHoleError,
_TcRnIllegalWildcardInType,
Expand Down Expand Up @@ -80,6 +81,11 @@ _GhcDsMessage = prism' GhcDsMessage (\case
GhcDsMessage dsMsg -> Just dsMsg
_ -> Nothing)

_DsMessage :: Fold GhcMessage DsMessage
_DsMessage = prism' GhcDsMessage $ \case
GhcDsMessage dsmsg -> Just dsmsg
_ -> Nothing

_GhcDriverMessage :: Prism' GhcMessage DriverMessage
_GhcDriverMessage = prism' GhcDriverMessage (\case
GhcDriverMessage driverMsg -> Just driverMsg
Expand Down
54 changes: 54 additions & 0 deletions haskell-language-server.cabal
Original file line number Diff line number Diff line change
Expand Up @@ -575,6 +575,59 @@ test-suite hls-explicit-imports-plugin-tests
, lsp-types
, text

-----------------------------
-- case split plugin
-----------------------------

flag caseSplit
description: Enable caseSplit plugin
default: True
manual: True

common caseSplit
if flag(casesplit) && !impl(ghc < 9.14)
build-depends: haskell-language-server:hls-case-split-plugin
cpp-options: -Dhls_caseSplit

library hls-case-split-plugin
import: defaults, pedantic, warnings
if !flag(casesplit) || impl(ghc < 9.14)
buildable: False
exposed-modules: Ide.Plugin.CaseSplit
hs-source-dirs: plugins/hls-case-split-plugin/src
build-depends:
, extra
, ghc
, haskell-language-server:hls-refactor-plugin
, ghcide == 2.14.0.0
, hls-plugin-api == 2.14.0.0
, lens
, lsp
, mtl
, syb
, text
, transformers
, transformers-either
, ghc-exactprint >= 1.14.1.0

default-extensions:
DataKinds

test-suite hls-case-split-plugin-tests
import: defaults, pedantic, test-defaults, warnings
if !flag(casesplit) || impl(ghc < 9.14)
buildable: False
type: exitcode-stdio-1.0
hs-source-dirs: plugins/hls-case-split-plugin/test
main-is: Main.hs
build-depends:
, filepath
, haskell-language-server:hls-case-split-plugin
, hls-test-utils == 2.14.0.0
, lens
, lsp-types
, text

-----------------------------
-- rename plugin
-----------------------------
Expand Down Expand Up @@ -1853,6 +1906,7 @@ library
, class
, eval
, importLens
, caseSplit
, rename
, hlint
, stan
Expand Down
804 changes: 804 additions & 0 deletions plugins/hls-case-split-plugin/src/Ide/Plugin/CaseSplit.hs

Large diffs are not rendered by default.

195 changes: 195 additions & 0 deletions plugins/hls-case-split-plugin/test/Main.hs
Original file line number Diff line number Diff line change
@@ -0,0 +1,195 @@
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE OverloadedLists #-}
{-# LANGUAGE OverloadedStrings #-}

module Main
( main
) where

import Control.Lens (Prism', prism', (^.), (^..), (^?))
import Data.Text (Text)
import qualified Ide.Plugin.CaseSplit as CS
import qualified Language.LSP.Protocol.Lens as L
import System.FilePath
import Test.Hls hiding (waitForDiagnosticsFrom)
import qualified Test.Hls.FileSystem as FS

main :: IO ()
main = defaultTestRunner tests

caseSplitPlugin :: PluginTestDescriptor CS.Log
caseSplitPlugin = mkPluginTestDescriptor CS.descriptor "case split"

tests :: TestTree
tests = testGroup
"case split"
[ codeActionTests
]

codeActionTests :: TestTree
codeActionTests = testGroup
"code actions" $ let title = CS.caseSplitPluginCodeActionTitle in
[ goldenWithClass "No patterns, no braces" "TNoPatternsNoBraces" $
Prelude.flip inspectCodeAction [title]
, goldenWithClass "Some patterns, no braces" "TSomePatternsNoBraces" $
Prelude.flip inspectCodeAction [title]
, goldenWithClass "Some patterns, with braces" "TSomePatternsWithBraces" $
Prelude.flip inspectCodeAction [title]
, goldenWithClass "No patterns, with braces" "TNoPatternsWithBraces" $
Prelude.flip inspectCodeAction [title]

-- Patterns with irregular indentation
, goldenWithClass "Jagged patterns, no braces" "TJaggedNoBraces" $
Prelude.flip inspectCodeAction [title]
, goldenWithClass "Jagged patterns, with braces" "TJaggedWithBraces" $
Prelude.flip inspectCodeAction [title]

-- Patterns on one line
, goldenWithClass "Some patterns on one line, no braces" "TSomePatternsOnOneLineNoBraces" $
Prelude.flip inspectCodeAction [title]
, goldenWithClass "Some patterns on one line, with braces" "TSomePatternsOnOneLineWithBraces" $
Prelude.flip inspectCodeAction [title]

-- Records
, goldenWithClass "Records' field names are ignored" "TRecordsFieldNamesIgnored" $
Prelude.flip inspectCodeAction [title]
, goldenWithClass "Too many fields are collapsed" "TManyFields" $
Prelude.flip inspectCodeAction [title]

-- GADTs
, goldenWithClass "GADT - simple" "TGADTsimple" $
Prelude.flip inspectCodeAction [title]
, goldenWithClass "GADT - advanced" "TGADTadvanced" $
Prelude.flip inspectCodeAction [title]

-- LambdaCase
, goldenWithClass "LambdaCase, no patterns, no braces" "TLambdaCaseNoPatternsNoBraces" $
Prelude.flip inspectCodeAction [title]
, goldenWithClass "LambdaCase, no patterns, with braces" "TLambdaCaseNoPatternsWithBraces" $
Prelude.flip inspectCodeAction [title]
, goldenWithClass "LambdaCase, some patterns, no braces" "TLambdaCaseSomePatternsNoBraces" $
Prelude.flip inspectCodeAction [title]
, goldenWithClass "LambdaCase, some patterns, with braces" "TLambdaCaseSomePatternsWithBraces" $
Prelude.flip inspectCodeAction [title]
, goldenWithClass "LambdaCase in `do`, no patterns, no braces" "TLambdaCaseInDoNoPatternsNoBraces" $
Prelude.flip inspectCodeAction [title]
, goldenWithClass "LambdaCase in `do`, no patterns, with braces" "TLambdaCaseInDoNoPatternsWithBraces" $
Prelude.flip inspectCodeAction [title]
, goldenWithClass "LambdaCase in `do`, some patterns, no braces" "TLambdaCaseInDoSomePatternsNoBraces" $
Prelude.flip inspectCodeAction [title]
, goldenWithClass "LambdaCase in `do`, some patterns, with braces" "TLambdaCaseInDoSomePatternsWithBraces" $
Prelude.flip inspectCodeAction [title]

-- Inside where
, expectNoCodeActionAvailable "Inside `where`, without signature" "TInsideWhereWithoutSignature"
, goldenWithClass "Inside `where`" "TInsideWhere" $
Prelude.flip inspectCodeAction [title]
, goldenWithClass "Inside nested `where`" "TInsideNestedWhere" $
Prelude.flip inspectCodeAction [title]

-- Overlapping diagnostics
, goldenWithClass "Expression is `_`" "TExpressionIsUnderscore" $
Prelude.flip inspectCodeAction [title]
, goldenWithRange "Overlapping pattern matches" "TOverlappingExistingPatterns" $
Range (Position 15 4) (Position 15 5)

-- Inside let
, goldenWithClass "Inside `let`'s declarations" "TInsideLetDeclarations" $
Prelude.flip inspectCodeAction [title]
, goldenWithClass "Inside `let`'s expression" "TInsideLetExpression" $
Prelude.flip inspectCodeAction [title]

-- Inside do
, goldenWithClass "Inside `let`'s declarations inside `do`" "TInsideLetDeclarationsInsideDo" $
Prelude.flip inspectCodeAction [title]
, goldenWithClass "Inside `let`'s expression inside `do`" "TInsideLetExpressionInsideDo" $
Prelude.flip inspectCodeAction [title]
, goldenWithClass "Inside `do`" "TInsideDo" $
Prelude.flip inspectCodeAction [title]

-- Nested case expressions
, goldenWithClass "Complete `case` nested in incomplete `case`" "TCompleteCaseInsideIncompleteCase" $
Prelude.flip inspectCodeAction [title]
, goldenWithRange "Incomplete `case` nested in complete `case`" "TIncompleteCaseInsideCompleteCase" $
Range (Position 15 16) (Position 15 17)
, goldenWithRange "Incomplete `case` nested in incomplete `case`" "TIncompleteCaseInsideIncompleteCase" $
Range (Position 15 30) (Position 15 31)

-- Extreme cursor positions
, expectCodeActionsAvailable "Cursor before `c` of `case`" "TNoPatternsNoBraces"
(Range (Position 12 7) (Position 12 8))
[]

, expectCodeActionsAvailable "Cursor on `c` of `case`" "TNoPatternsNoBraces"
(Range (Position 12 8) (Position 12 9))
[ CS.caseSplitPluginCodeActionTitle
]

, expectCodeActionsAvailable "Cursor on `f` of `of`" "TNoPatternsNoBraces"
(Range (Position 12 16) (Position 13 0))
[ CS.caseSplitPluginCodeActionTitle
]

-- Support UnicodeSyntax
, goldenWithClass "Use → instead of -> when UnicodeSyntax is On" "TUnicodeArrow" $
Prelude.flip inspectCodeAction [title]

-- Some more corner cases
, expectNoCodeActionAvailable "No action on `Int`" "TInt"
, expectNoCodeActionAvailable "Cannot see through condition of a single catch-all pattern" "TWithCond"
, goldenWithClass "Ignore catch-all pattern in presence of non-catch-all pattern" "TWithCondAndPat" $
Prelude.flip inspectCodeAction [title]
]

waitForDiagnosticsFrom :: TextDocumentIdentifier -> Session [Diagnostic]
waitForDiagnosticsFrom doc = do
diagsNot <- skipManyTill anyMessage (message SMethod_TextDocumentPublishDiagnostics)
let diags = diagsNot ^. L.params . L.diagnostics
if doc ^. L.uri /= diagsNot ^. L.params . L.uri
|| ((not .) . any) ((\case Just (InR "GHC-62161") -> True
_ -> False) . (^. L.code)) diags
then waitForDiagnosticsFrom doc
Comment thread
Aster89 marked this conversation as resolved.
else return diags

_CACodeAction :: Prism' (Command |? CodeAction) CodeAction
_CACodeAction = prism' InR $ \case
InR action -> Just action
_ -> Nothing

expectCodeActionsAvailable :: TestName -> FilePath -> Range -> [Text] -> TestTree
expectCodeActionsAvailable title path range actionTitles =
testCase title $ do
runSessionWithServerInTmpDir def caseSplitPlugin (mkFs $ FS.directProject (path <.> "hs")) $ do
doc <- openDoc (path <.> "hs") "haskell"
_ <- waitForDiagnosticsFrom doc
caResults <- getCodeActions doc range
liftIO $ map (^? _CACodeAction . L.title) caResults
@?= expectedActions
where
expectedActions = Just <$> actionTitles

expectNoCodeActionAvailable :: TestName -> FilePath -> TestTree
expectNoCodeActionAvailable title path = expectCodeActionsAvailable title path anywhere []
where
anywhere = Range (Position 0 0) (Position 999 999)

goldenWithRange :: TestName -> FilePath -> Range -> TestTree
goldenWithRange title path range =
goldenWithHaskellDocInTmpDir def caseSplitPlugin title (mkFs $ FS.directProject (path <.> "hs")) path "expected" "hs" $ \doc -> do
_ <- waitForDiagnosticsFrom doc
[action] <- concatMap (^.. _CACodeAction) <$> getCodeActions doc range
executeCodeAction action

goldenWithClass :: TestName -> FilePath -> ([Command |? CodeAction] -> IO CodeAction) -> TestTree
goldenWithClass title path findAction =
goldenWithHaskellDocInTmpDir def caseSplitPlugin title (mkFs $ FS.directProject (path <.> "hs")) path "expected" "hs" $ \doc -> do
_ <- waitForDiagnosticsFrom doc
actions <- getAllCodeActions doc
action <- liftIO $ findAction actions
executeCodeAction action

testDataDir :: FilePath
testDataDir = "plugins" </> "hls-case-split-plugin" </> "test" </> "testdata"

mkFs :: [FS.FileTree] -> FS.VirtualFileTree
mkFs = FS.mkVirtualFileTree testDataDir
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
{-# LANGUAGE EmptyCase #-}
{-# OPTIONS_GHC -Wall -fmax-uncovered-patterns=99 #-}
{-# LANGUAGE OrPatterns #-}
module T where

data X = A
| B
| C Int
| D Int Int
| E
| F

foo :: X -> Int
foo x = case x of
A -> 3
a@(B; C _) -> case a of
B -> 3
C _ -> 4
D _ _ -> _
E -> _
F -> _
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
{-# LANGUAGE EmptyCase #-}
{-# OPTIONS_GHC -Wall -fmax-uncovered-patterns=99 #-}
{-# LANGUAGE OrPatterns #-}
module T where

data X = A
| B
| C Int
| D Int Int
| E
| F

foo :: X -> Int
foo x = case x of
A -> 3
a@(B; C _) -> case a of
B -> 3
C _ -> 4
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
{-# LANGUAGE EmptyCase #-}
{-# OPTIONS_GHC -Wall -fmax-uncovered-patterns=99 #-}
module T where

data X = A
| B
| C Int
| D Int Int
| E
| F

foo :: Int
foo = case _ :: X of
A -> _
B -> _
C _ -> _
D _ _ -> _
E -> _
F -> _
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
{-# LANGUAGE EmptyCase #-}
{-# OPTIONS_GHC -Wall -fmax-uncovered-patterns=99 #-}
module T where

data X = A
| B
| C Int
| D Int Int
| E
| F

foo :: Int
foo = case _ :: X of
Loading
Loading