hls-overloaded-record-dot-plugin 2.0.0.1 → 2.1.0.0
raw patch · 3 files changed
+150/−87 lines, 3 filesdep +aesondep +lsp-typesdep +row-typesPVP ok
version bump matches the API change (PVP)
Dependencies added: aeson, lsp-types, row-types
API changes (from Hackage documentation)
+ Ide.Plugin.OverloadedRecordDot: instance Data.Aeson.Types.FromJSON.FromJSON Ide.Plugin.OverloadedRecordDot.ORDResolveData
+ Ide.Plugin.OverloadedRecordDot: instance Data.Aeson.Types.ToJSON.ToJSON Ide.Plugin.OverloadedRecordDot.ORDResolveData
+ Ide.Plugin.OverloadedRecordDot: instance GHC.Generics.Generic Ide.Plugin.OverloadedRecordDot.ORDResolveData
+ Ide.Plugin.OverloadedRecordDot: instance GHC.Show.Show Ide.Plugin.OverloadedRecordDot.ORDResolveData
Files
- hls-overloaded-record-dot-plugin.cabal +7/−1
- src/Ide/Plugin/OverloadedRecordDot.hs +109/−73
- test/Main.hs +34/−13
hls-overloaded-record-dot-plugin.cabal view
@@ -1,6 +1,6 @@ cabal-version: 3.0 name: hls-overloaded-record-dot-plugin-version: 2.0.0.1+version: 2.1.0.0 synopsis: Overloaded record dot plugin for Haskell Language Server description: Please see the README on GitHub at <https://github.com/haskell/haskell-language-server#readme>@@ -30,6 +30,7 @@ exposed-modules: Ide.Plugin.OverloadedRecordDot build-depends: , base >=4.16 && <5+ , aeson , ghcide , hls-plugin-api , lsp@@ -58,8 +59,13 @@ build-depends: , base , filepath+ , ghcide , text , hls-overloaded-record-dot-plugin+ , hls-plugin-api+ , lens , lsp-test+ , lsp-types+ , row-types , hls-test-utils
src/Ide/Plugin/OverloadedRecordDot.hs view
@@ -3,9 +3,9 @@ {-# LANGUAGE LambdaCase #-} {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE PatternSynonyms #-}+{-# LANGUAGE RecordWildCards #-} {-# LANGUAGE TypeFamilies #-} {-# LANGUAGE ViewPatterns #-}- module Ide.Plugin.OverloadedRecordDot ( descriptor , Log@@ -14,24 +14,26 @@ -- based off of Berk Okzuturk's hls-explicit-records-fields-plugin import Control.Lens ((^.))+import Control.Monad (replicateM) import Control.Monad.IO.Class (MonadIO, liftIO) import Control.Monad.Trans.Except (ExceptT)-import Data.Generics (GenericQ, everything,- everythingBut, mkQ)-import qualified Data.HashMap.Strict as HashMap+import Data.Aeson (FromJSON, ToJSON, toJSON)+import Data.Generics (GenericQ, everythingBut,+ mkQ)+import qualified Data.IntMap.Strict as IntMap+import qualified Data.Map as Map import Data.Maybe (mapMaybe, maybeToList) import Data.Text (Text)+import Data.Unique (hashUnique, newUnique) import Development.IDE (IdeState, NormalizedFilePath, Pretty (..), Range, Recorder (..), Rules, WithPriority (..), realSrcSpanToRange)-import Development.IDE.Core.Rules (runAction) import Development.IDE.Core.RuleTypes (TcModuleResult (..), TypeCheck (..))-import Development.IDE.Core.Shake (define, use,- useWithStale)+import Development.IDE.Core.Shake (define, useWithStale) import qualified Development.IDE.Core.Shake as Shake #if __GLASGOW_HASKELL__ >= 903@@ -41,17 +43,17 @@ #endif import Control.DeepSeq (rwhnf)-import Development.IDE.Core.PositionMapping (PositionMapping (PositionMapping),+import Development.IDE.Core.PluginUtils+import Development.IDE.Core.PositionMapping (PositionMapping, toCurrentRange) import Development.IDE.GHC.Compat (Extension (OverloadedRecordDot), GhcPass, HsExpansion (HsExpanded),- HsExpr (HsApp, HsPar, HsVar, OpApp, XExpr),+ HsExpr (HsApp, HsVar, OpApp, XExpr), LHsExpr, Outputable,- Pass (..), RealSrcSpan,- appPrec, dollarName,- getLoc, hs_valds,- parenthesizeHsContext,+ Pass (..), appPrec,+ dollarName, getLoc,+ hs_valds, parenthesizeHsExpr, pattern RealSrcSpan, unLoc)@@ -62,42 +64,41 @@ import Development.IDE.Spans.Pragmas (NextPragmaInfo (..), getFirstPragma, insertNewPragma)-import Development.IDE.Types.Logger (Priority (..),+import GHC.Generics (Generic)+import Ide.Logger (Priority (..), cmapWithPrio, logWith, (<+>))-import GHC.Generics (Generic)+import Ide.Plugin.Error (PluginError (..),+ getNormalizedFilePathE,+ handleMaybe) import Ide.Plugin.RangeMap (RangeMap) import qualified Ide.Plugin.RangeMap as RangeMap-import Ide.PluginUtils (getNormalizedFilePath,- handleMaybeM,- pluginResponse)+import Ide.Plugin.Resolve (mkCodeActionHandlerWithResolve) import Ide.Types (PluginDescriptor (..), PluginId (..), PluginMethodHandler,- defaultPluginDescriptor,- mkPluginHandler)-import Language.LSP.Types (CodeAction (..),- CodeActionKind (CodeActionRefactorRewrite),+ 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, List (..),- Method (..),- SMethod (..),- TextEdit (..),- WorkspaceEdit (WorkspaceEdit),- fromNormalizedUri,- normalizedFilePathToUri,- type (|?) (InR))-import qualified Language.LSP.Types.Lens as L+ TextEdit (..), Uri (..),+ WorkspaceEdit (WorkspaceEdit, _changeAnnotations, _changes, _documentChanges),+ type (|?) (..))+ data Log = LogShake Shake.Log | LogCollectedRecordSelectors [RecordSelectorExpr]- | LogTextEdits [TextEdit]+ | forall a. (Pretty a) => LogResolve a instance Pretty Log where pretty = \case LogShake shakeLog -> pretty shakeLog LogCollectedRecordSelectors recs -> "Collected record selectors:" <+> pretty recs+ LogResolve msg -> pretty msg data CollectRecordSelectors = CollectRecordSelectors deriving (Eq, Show, Generic)@@ -106,7 +107,14 @@ instance NFData CollectRecordSelectors data CollectRecordSelectorsResult = CRSR- { recordInfos :: RangeMap RecordSelectorExpr+ { -- |We store everything in here that we need to create the unresolved+ -- codeAction: the range, an uniquely identifiable int, and the selector+ --selector expression (HSExpr) that we use to generate the name+ records :: RangeMap (Int, HsExpr (GhcPass 'Renamed))+ -- |This is for when we need to fully generate a textEdit. It contains the+ -- whole expression we are interested in indexed to the unique id we got+ -- from the previous field+ , recordInfos :: IntMap.IntMap RecordSelectorExpr , enabledExtensions :: [Extension] } deriving (Generic)@@ -130,63 +138,86 @@ recordExpr :: LHsExpr (GhcPass 'Renamed) } instance Pretty RecordSelectorExpr where- pretty (RecordSelectorExpr l rs se) = pretty (printOutputable rs) <> ":"+ pretty (RecordSelectorExpr _ rs se) = pretty (printOutputable rs) <> ":" <+> pretty (printOutputable se) instance NFData RecordSelectorExpr where rnf = rwhnf +-- |The data that is serialized and placed in the data field of resolvable+-- code actions+data ORDResolveData = ORDRD {+ -- |We need the uri to get shake results+ uri :: Uri+ -- |The unique id that allows us to find the specific codeAction we want+, uniqueID :: Int+} deriving (Generic, Show)+instance ToJSON ORDResolveData+instance FromJSON ORDResolveData+ descriptor :: Recorder (WithPriority Log) -> PluginId -> PluginDescriptor IdeState-descriptor recorder plId = (defaultPluginDescriptor plId)- { pluginHandlers =- mkPluginHandler STextDocumentCodeAction codeActionProvider+descriptor recorder plId =+ let resolveRecorder = cmapWithPrio LogResolve recorder+ pluginHandler = mkCodeActionHandlerWithResolve resolveRecorder codeActionProvider resolveProvider+ in (defaultPluginDescriptor plId)+ { pluginHandlers = pluginHandler , pluginRules = collectRecSelsRule recorder } -codeActionProvider :: PluginMethodHandler IdeState 'TextDocumentCodeAction-codeActionProvider ideState pId (CodeActionParams _ _ caDocId caRange _) =- pluginResponse $ do- nfp <- getNormalizedFilePath (caDocId ^. L.uri)- pragma <- getFirstPragma pId ideState nfp- CRSR crsMap exts <- collectRecSelResult ideState nfp- let pragmaEdit =- if OverloadedRecordDot `elem` exts- then Nothing- else Just $ insertNewPragma pragma OverloadedRecordDot- edits crs = convertRecordSelectors crs : maybeToList pragmaEdit- changes crs =- Just $ HashMap.singleton (fromNormalizedUri- (normalizedFilePathToUri nfp))- (List (edits crs))- mkCodeAction crs = InR CodeAction+resolveProvider :: ResolveFunction IdeState ORDResolveData 'Method_CodeActionResolve+resolveProvider ideState plId ca uri (ORDRD _ int) =+ do+ nfp <- getNormalizedFilePathE uri+ CRSR _ crsDetails exts <- collectRecSelResult ideState nfp+ pragma <- getFirstPragma plId ideState nfp+ rse <- handleMaybe PluginStaleResolve $ IntMap.lookup int crsDetails+ pure $ ca {_edit = mkWorkspaceEdit uri rse exts pragma}++codeActionProvider :: PluginMethodHandler IdeState 'Method_TextDocumentCodeAction+codeActionProvider ideState _ (CodeActionParams _ _ caDocId caRange _) =+ do+ nfp <- getNormalizedFilePathE (caDocId ^. L.uri)+ CRSR crsMap _ exts <- collectRecSelResult ideState nfp+ let mkCodeAction (crsM, nse) = InR CodeAction { -- We pass the record selector to the title function, so that -- we can have the name of the record selector in the title of -- the codeAction. This allows the user can easily distinguish -- between the different codeActions when using nested record -- selectors, the disadvantage is we need to print out the -- name of the record selector which will decrease performance- _title = mkCodeActionTitle exts crs- , _kind = Just CodeActionRefactorRewrite+ _title = mkCodeActionTitle exts nse+ , _kind = Just CodeActionKind_RefactorRewrite , _diagnostics = Nothing , _isPreferred = Nothing , _disabled = Nothing- , _edit = Just $ WorkspaceEdit (changes crs) Nothing Nothing+ , _edit = Nothing , _command = Nothing- , _xdata = Nothing+ , _data_ = Just $ toJSON $ ORDRD (caDocId ^. L.uri) crsM } actions = map mkCodeAction (RangeMap.filterByRange caRange crsMap)- pure $ List actions+ pure $ InL actions where- mkCodeActionTitle :: [Extension] -> RecordSelectorExpr-> Text- mkCodeActionTitle exts (RecordSelectorExpr _ se _) =+ mkCodeActionTitle :: [Extension] -> HsExpr (GhcPass 'Renamed) -> Text+ mkCodeActionTitle exts se = if OverloadedRecordDot `elem` exts then title else title <> " (needs extension: OverloadedRecordDot)" where- title = "Convert `" <> name <> "` to record dot syntax"- name = printOutputable se+ title = "Convert `" <> printOutputable se <> "` to record dot syntax" +mkWorkspaceEdit:: Uri -> RecordSelectorExpr -> [Extension] -> NextPragmaInfo-> Maybe WorkspaceEdit+mkWorkspaceEdit uri recSel exts pragma =+ Just $ WorkspaceEdit+ { _changes =+ Just (Map.singleton uri (convertRecordSelectors recSel : maybeToList pragmaEdit))+ , _documentChanges = Nothing+ , _changeAnnotations = Nothing}+ where pragmaEdit =+ if OverloadedRecordDot `elem` exts+ then Nothing+ else Just $ insertNewPragma pragma OverloadedRecordDot+ collectRecSelsRule :: Recorder (WithPriority Log) -> Rules () collectRecSelsRule recorder = define (cmapWithPrio LogShake recorder) $ \CollectRecordSelectors nfp ->@@ -202,11 +233,17 @@ -- the OverloadedRecordDot pragma exts = getEnabledExtensions tmr recSels = mapMaybe (rewriteRange pm) (getRecordSelectors tmr)+ -- We are creating a list as long as our rec selectors of unique int s+ -- created by calling hashUnique on a Unique. The reason why we are+ -- extracting the ints is because they don't need any work to serialize.+ uniques <- liftIO $ replicateM (length recSels) (hashUnique <$> newUnique) logWith recorder Debug (LogCollectedRecordSelectors recSels)- let -- We need the rangeMap to be able to filter by range later- crsMap :: RangeMap RecordSelectorExpr- crsMap = RangeMap.fromList location recSels- pure ([], CRSR <$> Just crsMap <*> Just exts)+ let crsUniquesAndDetails = zip uniques recSels+ -- We need the rangeMap to be able to filter by range later+ rangeAndUnique = toRangeAndUnique <$> crsUniquesAndDetails+ crsMap :: RangeMap (Int, HsExpr (GhcPass 'Renamed))+ crsMap = RangeMap.fromList' rangeAndUnique+ pure ([], CRSR <$> Just crsMap <*> Just (IntMap.fromList crsUniquesAndDetails) <*> Just exts) where getEnabledExtensions :: TcModuleResult -> [Extension] getEnabledExtensions = getExtensions . tmrParsed getRecordSelectors :: TcModuleResult -> [RecordSelectorExpr]@@ -218,10 +255,11 @@ case toCurrentRange pm (location recSel) of Just newLoc -> Just $ recSel{location = newLoc} Nothing -> Nothing+ toRangeAndUnique (uid, RecordSelectorExpr l (unLoc -> se) _) = (l, (uid, se)) convertRecordSelectors :: RecordSelectorExpr -> TextEdit-convertRecordSelectors (RecordSelectorExpr l se re) =- TextEdit l $ convertRecSel se re+convertRecordSelectors RecordSelectorExpr{..} =+ TextEdit location $ convertRecSel selectorExpr recordExpr -- |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@@ -271,10 +309,8 @@ getRecSels _ = ([], False) collectRecSelResult :: MonadIO m => IdeState -> NormalizedFilePath- -> ExceptT String m CollectRecordSelectorsResult+ -> ExceptT PluginError m CollectRecordSelectorsResult collectRecSelResult ideState =- handleMaybeM "Unable to TypeCheck"- . liftIO- . runAction "overloadedRecordDot.collectRecordSelectors" ideState- . use CollectRecordSelectors+ runActionE "overloadedRecordDot.collectRecordSelectors" ideState+ . useE CollectRecordSelectors
test/Main.hs view
@@ -11,31 +11,42 @@ import System.FilePath ((</>)) import Test.Hls - main :: IO ()-main = defaultTestRunner test+main =+ defaultTestRunner test plugin :: PluginTestDescriptor OverloadedRecordDot.Log plugin = mkPluginTestDescriptor OverloadedRecordDot.descriptor "overloaded-record-dot" test :: TestTree test = testGroup "overloaded-record-dot"- [ mkTest "Simple" "Simple" "name" 10 7 10 15,- mkTest "NoPragmaNeeded" "NoPragmaNeeded" "name" 11 7 11 15,- mkTest "NestedParens" "NestedParens" "name" 15 7 15 24,- mkTest "NestedDot" "NestedDot" "name" 17 7 17 22,- mkTest "NestedDollar" "NestedDollar" "name" 15 7 15 24,- mkTest "MultilineCase" "MultilineCase" "name" 10 7 12 15,- mkTest "Multiline" "Multiline" "name" 10 7 11 15,- mkTest "MultilineExpanded" "MultilineExpanded" "owner" 28 8 28 19- ]+ (mkTest "Simple" "Simple" "name" 10 7 10 15+ <> mkTest "NoPragmaNeeded" "NoPragmaNeeded" "name" 11 7 11 15+ <> mkTest "NestedParens" "NestedParens" "name" 15 7 15 24+ <> mkTest "NestedDot" "NestedDot" "name" 17 7 17 22+ <> mkTest "NestedDollar" "NestedDollar" "name" 15 7 15 24+ <> mkTest "MultilineCase" "MultilineCase" "name" 10 7 12 15+ <> mkTest "Multiline" "Multiline" "name" 10 7 11 15+ <> mkTest "MultilineExpanded" "MultilineExpanded" "owner" 28 8 28 19) -mkTest :: TestName -> FilePath -> T.Text -> UInt -> UInt -> UInt -> UInt -> TestTree+mkTest :: TestName -> FilePath -> T.Text -> UInt -> UInt -> UInt -> UInt -> [TestTree] mkTest title fp selectorName x1 y1 x2 y2 =- goldenWithHaskellDoc plugin title testDataDir fp "expected" "hs" $ \doc -> do+ [mkNoResolveTest (title <> " without resolve") fp selectorName x1 y1 x2 y2,+ mkResolveTest (title <> " with resolve") fp selectorName x1 y1 x2 y2]++mkNoResolveTest :: TestName -> FilePath -> T.Text -> UInt -> UInt -> UInt -> UInt -> TestTree+mkNoResolveTest title fp selectorName x1 y1 x2 y2 =+ goldenWithHaskellAndCaps codeActionNoResolveCaps plugin title testDataDir fp "expected" "hs" $ \doc -> do (act:_) <- getExplicitFieldsActions doc selectorName x1 y1 x2 y2 executeCodeAction act +mkResolveTest :: TestName -> FilePath -> T.Text -> UInt -> UInt -> UInt -> UInt -> TestTree+mkResolveTest title fp selectorName x1 y1 x2 y2 =+ goldenWithHaskellAndCaps codeActionResolveCaps plugin title testDataDir fp "expected" "hs" $ \doc -> do+ (act:_) <- getAndResolveExplicitFieldsActions doc selectorName x1 y1 x2 y2+ executeCodeAction act++ getExplicitFieldsActions :: TextDocumentIdentifier -> T.Text@@ -43,6 +54,16 @@ -> Session [CodeAction] getExplicitFieldsActions doc selectorName x1 y1 x2 y2 = findExplicitFieldsAction selectorName <$> getCodeActions doc range+ where+ range = Range (Position x1 y1) (Position x2 y2)++getAndResolveExplicitFieldsActions+ :: TextDocumentIdentifier+ -> T.Text+ -> UInt -> UInt -> UInt -> UInt+ -> Session [CodeAction]+getAndResolveExplicitFieldsActions doc selectorName x1 y1 x2 y2 = do+ findExplicitFieldsAction selectorName <$> getAndResolveCodeActions doc range where range = Range (Position x1 y1) (Position x2 y2)