Skip to content

Commit 93ca53e

Browse files
author
Christian Berg
committed
implement a very easy case split using -Wincomplete-uni-patterns (haskell#3525)
1 parent e37ec7d commit 93ca53e

File tree

5 files changed

+327
-0
lines changed

5 files changed

+327
-0
lines changed

haskell-language-server.cabal

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1526,6 +1526,67 @@ test-suite hls-refactor-plugin-tests
15261526
, tasty-expected-failure
15271527
, tasty
15281528

1529+
1530+
-----------------------------
1531+
-- complete case plugin
1532+
-----------------------------
1533+
1534+
flag completeCase
1535+
description: Enable Case Completion plugin
1536+
default: True
1537+
manual: True
1538+
1539+
common completeCase
1540+
if flag(completeCase)
1541+
build-depends: haskell-language-server:hls-complete-case-plugin
1542+
cpp-options: -Dhls_completeCase
1543+
1544+
library hls-complete-case-plugin
1545+
import: defaults, warnings
1546+
exposed-modules:
1547+
Ide.Plugin.CompleteCase
1548+
other-modules:
1549+
hs-source-dirs: plugins/hls-complete-case-plugin/src
1550+
build-depends:
1551+
, base >=4.12 && <5
1552+
, containers
1553+
, extra
1554+
, ghcide == 2.6.0.0
1555+
, hls-plugin-api == 2.6.0.0
1556+
, ghcide
1557+
, deepseq
1558+
, hls-graph
1559+
, bytestring
1560+
, lens
1561+
, text
1562+
, lsp
1563+
, mtl
1564+
, semigroupoids
1565+
, hashable
1566+
, transformers
1567+
, vector
1568+
1569+
-- test-suite hls-code-range-plugin-tests
1570+
-- import: defaults, test-defaults, warnings
1571+
-- type: exitcode-stdio-1.0
1572+
-- hs-source-dirs: plugins/hls-code-range-plugin/test
1573+
-- main-is: Main.hs
1574+
-- other-modules:
1575+
-- Ide.Plugin.CodeRangeTest
1576+
-- Ide.Plugin.CodeRange.RulesTest
1577+
-- build-depends:
1578+
-- , base
1579+
-- , bytestring
1580+
-- , filepath
1581+
-- , haskell-language-server:hls-code-range-plugin
1582+
-- , hls-test-utils == 2.6.0.0
1583+
-- , lens
1584+
-- , lsp
1585+
-- , lsp-test
1586+
-- , transformers
1587+
-- , vector
1588+
1589+
15291590
-----------------------------
15301591
-- semantic tokens plugin
15311592
-----------------------------
@@ -1629,6 +1690,7 @@ library
16291690
, alternateNumberFormat
16301691
, qualifyImportedNames
16311692
, codeRange
1693+
, completeCase
16321694
, gadt
16331695
, explicitFixity
16341696
, explicitFields
@@ -1854,3 +1916,4 @@ benchmark benchmark
18541916
, shake-bench == 0.2.*
18551917
, text
18561918
, yaml
1919+
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
# Complete Case Plugin
2+
3+
Background: [#3525](https://github.com/haskell/haskell-language-server/issues/3525)
4+
Lines changed: 242 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,242 @@
1+
{-# LANGUAGE RecordWildCards #-}
2+
{-# LANGUAGE OverloadedRecordDot #-}
3+
{-# LANGUAGE DataKinds #-}
4+
{-# LANGUAGE OverloadedStrings #-}
5+
{-# LANGUAGE DerivingStrategies #-}
6+
{-# OPTIONS_GHC -Wno-incomplete-patterns #-}
7+
module Ide.Plugin.CompleteCase
8+
( descriptor
9+
)
10+
where
11+
12+
import Control.Monad.IO.Class (MonadIO (liftIO))
13+
import Control.Monad.Trans.Except (ExceptT(..), mapExceptT, throwE,runExceptT)
14+
15+
import Development.IDE (Action,
16+
IdeState (shakeExtras),
17+
Range (Range), Recorder,
18+
WithPriority (WithPriority),
19+
cmapWithPrio)
20+
21+
import Development.IDE.Core.PluginUtils
22+
import Development.IDE.Core.PositionMapping (PositionMapping,
23+
toCurrentRange)
24+
import Ide.Logger (Pretty (..),
25+
Priority (Debug, Error, Info, Warning),
26+
Recorder, WithPriority,
27+
cmapWithPrio, logWith,
28+
nest,
29+
toCologActionWithPrio,
30+
vcat, viaShow, (<+>))
31+
import Ide.Plugin.Error
32+
import Ide.PluginUtils (positionInRange)
33+
import Ide.Types (PluginDescriptor (..),
34+
PluginId,
35+
PluginMethodHandler,
36+
PluginCommand(..),
37+
CommandFunction(..),
38+
ResolveFunction,
39+
mkResolveHandler,
40+
defaultPluginDescriptor,
41+
mkPluginHandler, defaultPluginPriority)
42+
import Language.LSP.Protocol.Message (Method (..),
43+
SMethod (..))
44+
import Language.LSP.Protocol.Types (NormalizedFilePath, Null (Null),
45+
Position (..),
46+
CodeAction(..),
47+
CompletionParams (..),
48+
CodeActionContext (..),
49+
Diagnostic(..),
50+
Range(..),
51+
CodeActionKind(..),
52+
TextEdit(..),
53+
Position(..),
54+
CodeActionParams (..),
55+
WorkspaceEdit(..),
56+
TextDocumentIdentifier (TextDocumentIdentifier),
57+
Uri, type (|?) (InL, InR), CompletionList, CompletionItem (CompletionItem))
58+
import Prelude hiding (log, span)
59+
60+
import Data.Typeable (Typeable)
61+
import Data.Hashable (Hashable(..))
62+
import Control.DeepSeq (NFData (..))
63+
import GHC.Generics (Generic(..))
64+
65+
import Data.ByteString qualified as BS
66+
67+
import qualified Development.IDE.Core.Shake as Shake
68+
import Development.IDE.Graph.Internal.Types
69+
import Development.IDE.Graph.Internal.Rules
70+
import Development.IDE.Core.Tracing
71+
import Development.IDE.Types.Shake (encodeShakeValue, ShakeValue (ShakeNoCutoff))
72+
import Development.IDE.Types.Shake (A(..))
73+
import Development.IDE.Types.Shake (Value(..))
74+
75+
import Language.LSP.Server (ProgressCancellable (Cancellable),
76+
sendNotification,
77+
sendRequest,
78+
withIndefiniteProgress)
79+
80+
import Data.Text qualified as T
81+
import Data.Map.Strict qualified as Map
82+
83+
84+
-- data Log
85+
-- = LogShake Shake.Log
86+
-- | LogNoAST
87+
-- | LogRequest Range
88+
-- deriving stock Show
89+
90+
-- instance Pretty Log where
91+
-- pretty log = case log of
92+
-- LogShake shakeLog -> pretty shakeLog
93+
-- LogNoAST -> "no HieAst exist for file"
94+
-- LogRequest range -> pretty $ show range
95+
type CompleteCaseLog = String
96+
97+
98+
descriptor :: Recorder (WithPriority CompleteCaseLog) -> PluginId -> PluginDescriptor IdeState
99+
descriptor recorder plId = (defaultPluginDescriptor plId "Provides **case** completions")
100+
{ pluginHandlers =
101+
mkPluginHandler SMethod_TextDocumentCompletion (requestCompletionHandler recorder)
102+
<> mkPluginHandler SMethod_TextDocumentCodeAction (requestResolveMissingCases recorder)
103+
<> mkResolveHandler SMethod_CompletionItemResolve (requestCompletionsResolve recorder)
104+
, pluginPriority = defaultPluginPriority
105+
}
106+
107+
-- textCompletionCommand :: PluginId -> PluginCommand IdeState
108+
-- textCompletionCommand plId = PluginCommand "completeCase" "addCompletion" (runCompletionCommand plId)
109+
110+
-- runCompletionCommand :: PluginId -> CommandFunction IdeState CompletionParams
111+
-- runCompletionCommand plId st mtoken CompletionParams {..} =
112+
-- let cmd = do
113+
-- throwE (PluginInternalError "woops")
114+
115+
-- -- pure $ InR (InR Null)
116+
117+
118+
-- in ExceptT $
119+
-- withIndefiniteProgress "Evaluating" mtoken Cancellable $ \_updater ->
120+
-- runExceptT $ cmd
121+
122+
123+
requestCompletionsResolve :: Recorder (WithPriority CompleteCaseLog) -> ResolveFunction IdeState CompletionItem 'Method_CompletionItemResolve
124+
requestCompletionsResolve recorder ide _ q@CompletionItem {..} file _ =
125+
do
126+
logWith recorder Info $ "RESOLVE!!!!!:"
127+
logWith recorder Info $ (show q)
128+
129+
pure q
130+
131+
132+
requestResolveMissingCases :: Recorder (WithPriority CompleteCaseLog) -> PluginMethodHandler IdeState 'Method_TextDocumentCodeAction
133+
requestResolveMissingCases recorder ide _ q@(CodeActionParams{..}) =
134+
do
135+
let has_typecheck_hole = not $ null $ filter (\diag -> T.isInfixOf "Found hole: " $ diag._message ) type_check_diags
136+
has_missing_patterns = filter (\diag -> T.isInfixOf "Pattern match" diag._message) q._context._diagnostics
137+
logWith recorder Info $ "requestResolveMissingCases:"
138+
-- logWith recorder Info $ (show q)
139+
logWith recorder Info $ "has_hole: " <> show has_typecheck_hole
140+
logWith recorder Info $ "has_missing_pats: " <> (show $ not $ null $ has_missing_patterns)
141+
-- TODO:
142+
let indentation = 2
143+
144+
case has_missing_patterns of
145+
[missing_diag] -> do
146+
let is_lambda_case = T.isInfixOf "\\case" missing_diag._message && not (T.isInfixOf "\\cases" missing_diag._message)
147+
is_lambda_cases = T.isInfixOf "\\case" missing_diag._message && not (T.isInfixOf "\\cases" missing_diag._message)
148+
result_start_line = missing_diag._range._end._line
149+
result_start_col = missing_diag._range._end._character + if is_lambda_case then 5 else if is_lambda_cases then 6 else 1
150+
result_whitespace = missing_diag._range._start._character + indentation
151+
-- "Pattern match(es) are non-exhaustive\nIn a \\case alternative:\n Patterns of type \8216Maybe Config\8217 not matched:\n Nothing\n Just _"
152+
msg_lines = T.strip <$> (drop 3 $ T.lines missing_diag._message)
153+
154+
pure
155+
$ InL [ InR
156+
CodeAction {
157+
_title = "add missing matches "
158+
, _kind = Just $ CodeActionKind_RefactorRewrite
159+
, _diagnostics = Just [missing_diag]
160+
, _disabled = Nothing
161+
, _isPreferred = Just True
162+
, _edit = Just
163+
WorkspaceEdit {
164+
_changes = Just $ Map.fromList [(uri, [
165+
TextEdit {
166+
_range =
167+
Range {
168+
_start = Position { _line = result_start_line, _character = result_start_col }
169+
, _end = Position { _line = result_start_line, _character = result_start_col}
170+
}
171+
, _newText = "\n" <> (T.unlines $ (\x -> T.replicate (fromIntegral result_whitespace) " " <> x <> " -> _ ") <$> msg_lines) }
172+
] ) ]
173+
, _documentChanges = Nothing
174+
, _changeAnnotations = Nothing
175+
}
176+
, _command = Nothing
177+
, _data_ = Nothing }
178+
]
179+
_ -> pure $ InR Null
180+
181+
182+
where
183+
uri :: Uri
184+
TextDocumentIdentifier uri = _textDocument
185+
186+
type_check_diags =
187+
filter (\diag -> diag._source == Just "typecheck") (q._context._diagnostics)
188+
189+
190+
191+
requestCompletionHandler :: Recorder (WithPriority CompleteCaseLog) -> PluginMethodHandler IdeState 'Method_TextDocumentCompletion
192+
requestCompletionHandler recorder ide _ q@CompletionParams {..} = do
193+
do
194+
logWith recorder Info $ "requestCompletionHandler:"
195+
logWith recorder Info $ (show q)
196+
197+
let fp :: NormalizedFilePath = undefined -- <- getNormalizedFilePathE uri
198+
199+
mapExceptT liftIO $ runCompletions ide fp pos
200+
201+
where
202+
uri :: Uri
203+
TextDocumentIdentifier uri = _textDocument
204+
205+
pos = _position
206+
207+
runCompletions :: IdeState -> NormalizedFilePath -> Position -> ExceptT PluginError IO ([CompletionItem] |? (CompletionList |? Null))
208+
runCompletions ide file positions =
209+
pure $ InR (InR Null)
210+
211+
212+
213+
-- data CaseCompletions = CaseCompletions
214+
-- deriving (Eq, Show, Typeable, Generic)
215+
-- instance Hashable CaseCompletions
216+
-- instance NFData CaseCompletions
217+
218+
219+
-- -- addRule
220+
-- -- :: forall key value. (RuleResult key ~ value, Typeable key, Hashable key, Eq key,Typeable value)
221+
-- -- => (key -> Maybe BS.ByteString -> RunMode -> Action (RunResult value))
222+
-- -- -> Rules ()
223+
224+
-- produceCompletions :: Recorder (WithPriority CompleteCaseLog) -> Rules ()
225+
-- produceCompletions recorder = do
226+
-- define recorder (\k file -> pure Nothing)
227+
-- -- (\CaseCompletions file ->
228+
-- -- do
229+
-- -- logWith recorder Info $ "Trying to find completions on " <> show file
230+
-- -- pure (RunResult ChangedStore (encodeShakeValue ShakeNoCutoff) $ A (Failed False) ) :: Action (Shake.IdeResult CachedCompletions))
231+
-- where
232+
-- define :: Shake.IdeRule k v => Recorder (WithPriority CompleteCaseLog) -> (k -> NormalizedFilePath -> Action (Shake.IdeResult v)) -> Rules ()
233+
-- define recorder op = defineEarlyCutOff recorder $ Shake.Rule $ \k v -> (Nothing, ) <$> op k v
234+
235+
-- defineEarlyCutOff :: Shake.IdeRule k v => Recorder (WithPriority CompleteCaseLog) -> Shake.RuleBody k v -> Rules ()
236+
-- defineEarlyCutOff recorder (Shake.Rule op) =
237+
-- addRule $ \(Shake.Q (key, file)) (old :: Maybe BS.ByteString) mode -> do
238+
-- extras <- Shake.getShakeExtras
239+
-- defineEarlyCutoff' key file mbOld mode action
240+
241+
-- defineEarlyCutoff' ::Shake.IdeRule k v => NormalizedFilePath -> Maybe BS.ByteString -> RunMode -> (Development.IDE.Types.Shake.Value v -> Action (Maybe BS.ByteString, Shake.IdeResult v)) -> Action (RunResult (A (RuleResult k)))
242+
-- defineEarlyCutoff' = undefined
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
2+
# High Priority Todos
3+
4+
- get to compile and run
5+
- see calling output with requested position in Extensions output in VSCode
6+
7+
# Other Todos
8+
9+
- understand APIs
10+
11+

src/HlsPlugins.hs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,10 @@ import qualified Development.IDE.Plugin.CodeAction as Refactor
123123
import qualified Ide.Plugin.SemanticTokens as SemanticTokens
124124
#endif
125125

126+
#if hls_completeCase
127+
import qualified Ide.Plugin.CompleteCase as CompleteCase
128+
#endif
129+
126130

127131
data Log = forall a. (Pretty a) => Log PluginId a
128132

@@ -222,6 +226,9 @@ idePlugins recorder = pluginDescToIdePlugins allPlugins
222226
let pId = "ghcide-code-actions-fill-holes" in Refactor.fillHolePluginDescriptor (pluginRecorder pId) pId :
223227
let pId = "ghcide-extend-import-action" in Refactor.extendImportPluginDescriptor (pluginRecorder pId) pId :
224228
#endif
229+
#if hls_completeCase
230+
let pId = "complete-case" in CompleteCase.descriptor (pluginRecorder pId) pId :
231+
#endif
225232
#if explicitFixity
226233
let pId = "explicit-fixity" in ExplicitFixity.descriptor (pluginRecorder pId) pId :
227234
#endif

0 commit comments

Comments
 (0)