diff --git a/cabal.project b/cabal.project index f1872a8547..542232bfb0 100644 --- a/cabal.project +++ b/cabal.project @@ -4,8 +4,9 @@ packages: ./ghcide ./hls-plugin-api ./plugins/tactics - ./plugins/hls-hlint-plugin + ./plugins/hls-class-plugin ./plugins/hls-explicit-imports-plugin + ./plugins/hls-hlint-plugin ./plugins/hls-retrie-plugin tests: true diff --git a/exe/Plugins.hs b/exe/Plugins.hs index 9f424f65aa..83c3899daa 100644 --- a/exe/Plugins.hs +++ b/exe/Plugins.hs @@ -12,6 +12,10 @@ import Ide.Plugin.GhcIde as GhcIde -- haskell-language-server optional plugins +#if class +import Ide.Plugin.Class as Class +#endif + #if eval import Ide.Plugin.Eval as Eval #endif @@ -101,6 +105,9 @@ idePlugins includeExamples = pluginDescToIdePlugins allPlugins #if AGPL && brittany , Brittany.descriptor "brittany" #endif +#if class + , Class.descriptor "class" +#endif #if eval , Eval.descriptor "eval" #endif diff --git a/haskell-language-server.cabal b/haskell-language-server.cabal index ea9517f08e..063146793d 100644 --- a/haskell-language-server.cabal +++ b/haskell-language-server.cabal @@ -87,6 +87,11 @@ flag all-formatters default: True manual: True +flag class + description: Enable class plugin + default: False + manual: True + flag eval description: Enable eval plugin default: False @@ -154,6 +159,11 @@ common example-plugins other-modules: Ide.Plugin.Example, Ide.Plugin.Example2 +common class + if flag(class) || flag(all-plugins) + build-depends: hls-class-plugin + cpp-options: -Dclass + common eval if flag(eval) || flag(all-plugins) hs-source-dirs: plugins/default/src @@ -244,6 +254,7 @@ executable haskell-language-server , common-deps -- plugins , example-plugins + , class , eval , importLens , retrie @@ -394,6 +405,7 @@ test-suite func-test main-is: Main.hs other-modules: + Class Command Completion Config diff --git a/hie-cabal.yaml b/hie-cabal.yaml index c62c4307e7..324cf6fdb6 100644 --- a/hie-cabal.yaml +++ b/hie-cabal.yaml @@ -45,6 +45,9 @@ cradle: # Plugins: + - path: "./plugins/hls-class-plugin/src" + component: "hls-class-plugin" + - path: "./plugins/tactics/src" component: "hls-tactics-plugin:lib:hls-tactics-plugin" diff --git a/hie-stack.yaml b/hie-stack.yaml index abd01fad4a..1673b48e54 100644 --- a/hie-stack.yaml +++ b/hie-stack.yaml @@ -41,6 +41,9 @@ cradle: # Plugins: + - path: "./plugins/hls-class-plugin/src" + component: "hls-class-plugin:lib" + - path: "./plugins/hls-explicit-imports-plugin/src" component: "hls-explicit-imports-plugin:lib" diff --git a/nix/default.nix b/nix/default.nix index 02ed1cbea1..9eef54b152 100644 --- a/nix/default.nix +++ b/nix/default.nix @@ -20,10 +20,11 @@ let shake-bench = gitignoreSource ../ghcide/shake-bench; hie-compat = gitignoreSource ../ghcide/hie-compat; hls-plugin-api = gitignoreSource ../hls-plugin-api; - hls-tactics-plugin = gitignoreSource ../plugins/tactics; - hls-hlint-plugin = gitignoreSource ../plugins/hls-hlint-plugin; + hls-class-plugin = gitignoreSource ../plugins/hls-class-plugin; hls-explicit-imports-plugin = gitignoreSource ../plugins/hls-explicit-imports-plugin; + hls-hlint-plugin = gitignoreSource ../plugins/hls-hlint-plugin; hls-retrie-plugin = gitignoreSource ../plugins/hls-retrie-plugin; + hls-tactics-plugin = gitignoreSource ../plugins/tactics; }); in { diff --git a/plugins/hls-class-plugin/LICENSE b/plugins/hls-class-plugin/LICENSE new file mode 100644 index 0000000000..261eeb9e9f --- /dev/null +++ b/plugins/hls-class-plugin/LICENSE @@ -0,0 +1,201 @@ + 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 new file mode 100644 index 0000000000..b626d65db4 --- /dev/null +++ b/plugins/hls-class-plugin/hls-class-plugin.cabal @@ -0,0 +1,29 @@ +cabal-version: 2.2 +name: hls-class-plugin +version: 0.1.0.0 +synopsis: Explicit imports plugin for Haskell Language Server +license: Apache-2.0 +license-file: LICENSE +author: Junyoung Clare Jang +maintainer: jjc9310@gmail.com +category: Development +build-type: Simple + +library + exposed-modules: Ide.Plugin.Class + hs-source-dirs: src + build-depends: aeson + , base + , containers + , haskell-lsp + , hls-plugin-api + , ghc + , ghc-exactprint + , ghcide + , lens + , shake + , text + , transformers + , unordered-containers + + default-language: Haskell2010 diff --git a/plugins/hls-class-plugin/src/Ide/Plugin/Class.hs b/plugins/hls-class-plugin/src/Ide/Plugin/Class.hs new file mode 100644 index 0000000000..27dc547b0e --- /dev/null +++ b/plugins/hls-class-plugin/src/Ide/Plugin/Class.hs @@ -0,0 +1,207 @@ +{-# LANGUAGE DeriveAnyClass #-} +{-# LANGUAGE DeriveGeneric #-} +{-# LANGUAGE OverloadedStrings #-} +{-# LANGUAGE RecordWildCards #-} +{-# LANGUAGE TypeFamilies #-} +{-# LANGUAGE ViewPatterns #-} +module Ide.Plugin.Class + ( descriptor + ) where + +import BooleanFormula +import Class +import ConLike +import Control.Applicative +import Control.Lens hiding (List, use) +import Control.Monad +import Control.Monad.Trans.Class +import Control.Monad.Trans.Maybe +import Data.Aeson +import Data.Char +import qualified Data.HashMap.Strict as H +import Data.List +import qualified Data.Map.Strict as Map +import Data.Maybe +import qualified Data.Text as T +import Development.IDE +import Development.IDE.Core.PositionMapping (fromCurrentRange, toCurrentRange) +import Development.IDE.GHC.Compat hiding (getLoc) +import Development.IDE.Spans.AtPoint +import qualified GHC.Generics as Generics +import GhcPlugins hiding (Var, getLoc, (<>)) +import Ide.Plugin +import Ide.PluginUtils +import Ide.Types +import Language.Haskell.GHC.ExactPrint +import Language.Haskell.GHC.ExactPrint.Parsers (parseDecl) +import Language.Haskell.GHC.ExactPrint.Types hiding (GhcPs, Parens) +import Language.Haskell.LSP.Core +import Language.Haskell.LSP.Types +import qualified Language.Haskell.LSP.Types.Lens as J +import SrcLoc +import TcEnv +import TcRnMonad + +descriptor :: PluginId -> PluginDescriptor +descriptor plId = (defaultPluginDescriptor plId) + { pluginCommands = commands + , pluginCodeActionProvider = Just codeAction + } + +commands :: [PluginCommand] +commands + = [ PluginCommand "addMinimalMethodPlaceholders" "add placeholders for minimal methods" addMethodPlaceholders + ] + +-- | Parameter for the addMethods PluginCommand. +data AddMinimalMethodsParams = AddMinimalMethodsParams + { uri :: Uri + , range :: Range + , methodGroup :: List T.Text + } + deriving (Show, Eq, Generics.Generic, ToJSON, FromJSON) + +addMethodPlaceholders :: CommandFunction AddMinimalMethodsParams +addMethodPlaceholders lf state AddMinimalMethodsParams{..} = fmap (fromMaybe errorResult) . runMaybeT $ do + docPath <- MaybeT . pure . uriToNormalizedFilePath $ toNormalizedUri uri + pm <- MaybeT . runAction "classplugin" state $ use GetParsedModule docPath + let + ps = pm_parsed_source pm + anns = relativiseApiAnns ps (pm_annotations pm) + old = T.pack $ exactPrint ps anns + + (hsc_dflags . hscEnv -> df) <- MaybeT . runAction "classplugin" state $ use GhcSessionDeps docPath + List (unzip -> (mAnns, mDecls)) <- MaybeT . pure $ traverse (makeMethodDecl df) methodGroup + let + (ps', (anns', _), _) = runTransform (mergeAnns (mergeAnnList mAnns) anns) (addMethodDecls ps mDecls) + new = T.pack $ exactPrint ps' anns' + + pure (Right Null, Just (WorkspaceApplyEdit, ApplyWorkspaceEditParams (workspaceEdit caps old new))) + where + errorResult = (Right Null, Nothing) + + caps = clientCapabilities lf + indent = 2 + + makeMethodDecl df mName = + case parseDecl df (T.unpack mName) . T.unpack $ toMethodName mName <> " = _" of + Right (ann, d) -> Just (setPrecedingLines d 1 indent ann, d) + Left _ -> Nothing + + addMethodDecls :: ParsedSource -> [LHsDecl GhcPs] -> Transform (Located (HsModule GhcPs)) + addMethodDecls ps mDecls = do + d <- findInstDecl ps + newSpan <- uniqueSrcSpanT + let + annKey = mkAnnKey d + newAnnKey = AnnKey newSpan (CN "HsValBinds") + addWhere mkds@(Map.lookup annKey -> Just ann) + = Map.insert newAnnKey ann2 mkds2 + where + ann1 = ann + { annsDP = annsDP ann ++ [(G AnnWhere, DP (0, 1))] + , annCapturedSpan = Just newAnnKey + , annSortKey = Just (fmap getLoc mDecls) + } + mkds2 = Map.insert annKey ann1 mkds + ann2 = annNone + { annEntryDelta = DP (1, indent) + } + addWhere _ = panic "Ide.Plugin.Class.addMethodPlaceholder" + modifyAnnsT addWhere + modifyAnnsT (captureOrderAnnKey newAnnKey mDecls) + foldM (insertAfter d) ps (reverse mDecls) + + findInstDecl :: ParsedSource -> Transform (LHsDecl GhcPs) + findInstDecl ps = head . filter (containRange range . getLoc) <$> hsDecls ps + + workspaceEdit caps old new + = diffText caps (uri, old) new IncludeDeletions + + toMethodName n + | Just (h, _) <- T.uncons n + , not (isAlpha h) + = "(" <> n <> ")" + | otherwise + = n + +-- | +-- This implementation is ad-hoc in a sense that the diagnostic detection mechanism is +-- sensitive to the format of diagnostic messages from GHC. +codeAction :: CodeActionProvider +codeAction _ state plId docId _ context = fmap (fromMaybe errorResult) . runMaybeT $ do + docPath <- MaybeT . pure . uriToNormalizedFilePath $ toNormalizedUri uri + actions <- join <$> mapM (mkActions docPath) methodDiags + pure . Right . List $ actions + where + errorResult = Right (List []) + uri = docId ^. J.uri + List diags = context ^. J.diagnostics + + ghcDiags = filter (\d -> d ^. J.source == Just "typecheck") diags + methodDiags = filter (\d -> isClassMethodWarning (d ^. J.message)) ghcDiags + + mkActions docPath diag = do + ident <- findClassIdentifier docPath range + cls <- findClassFromIdentifier docPath ident + lift . traverse mkAction . minDefToMethodGroups . classMinimalDef $ cls + where + range = diag ^. J.range + + mkAction methodGroup + = mkCodeAction title + <$> mkLspCommand plId "addMinimalMethodPlaceholders" title (Just cmdParams) + where + title = mkTitle methodGroup + cmdParams = mkCmdParams methodGroup + + mkTitle methodGroup + = "Add placeholders for " + <> mconcat (intersperse ", " (fmap (\m -> "'" <> m <> "'") methodGroup)) + + mkCmdParams methodGroup = [toJSON (AddMinimalMethodsParams uri range (List methodGroup))] + + mkCodeAction title + = CACodeAction + . CodeAction title (Just CodeActionQuickFix) (Just (List [])) Nothing + . Just + + findClassIdentifier docPath range = do + (hieAst -> hf, pmap) <- MaybeT . runAction "classplugin" state $ useWithStale GetHieAst docPath + pure + $ head . head + $ pointCommand hf (fromJust (fromCurrentRange pmap range) ^. J.start & J.character -~ 1) + ( (Map.keys . Map.filter isClassNodeIdentifier . nodeIdentifiers . nodeInfo) + <=< nodeChildren + ) + + findClassFromIdentifier docPath (Right name) = do + (hscEnv -> hscenv, _) <- MaybeT . runAction "classplugin" state $ useWithStale GhcSessionDeps docPath + (tmrTypechecked -> thisMod, _) <- MaybeT . runAction "classplugin" state $ useWithStale TypeCheck docPath + MaybeT . fmap snd . initTcWithGbl hscenv thisMod ghostSpan $ do + tcthing <- tcLookup name + case tcthing of + AGlobal (AConLike (RealDataCon con)) + | Just cls <- tyConClass_maybe (dataConOrigTyCon con) -> pure cls + _ -> panic "Ide.Plugin.Class.findClassFromIdentifier" + findClassFromIdentifier _ (Left _) = panic "Ide.Plugin.Class.findClassIdentifier" + +ghostSpan :: RealSrcSpan +ghostSpan = realSrcLocSpan $ mkRealSrcLoc (fsLit "") 1 1 + +containRange :: Range -> SrcSpan -> Bool +containRange range x = isInsideSrcSpan (range ^. J.start) x || isInsideSrcSpan (range ^. J.end) x + +isClassNodeIdentifier :: IdentifierDetails a -> Bool +isClassNodeIdentifier = isNothing . identType + +isClassMethodWarning :: T.Text -> Bool +isClassMethodWarning = T.isPrefixOf "• No explicit implementation for" + +minDefToMethodGroups :: BooleanFormula Name -> [[T.Text]] +minDefToMethodGroups = go + where + go (Var mn) = [[T.pack . occNameString . occName $ mn]] + go (Or ms) = concatMap (go . unLoc) ms + go (And ms) = foldr (liftA2 (<>)) [[]] (fmap (go . unLoc) ms) + go (Parens m) = go (unLoc m) diff --git a/shell.nix b/shell.nix index 0b0df7427e..68851af9b0 100644 --- a/shell.nix +++ b/shell.nix @@ -29,10 +29,11 @@ let defaultCompiler = "ghc" + lib.replaceStrings ["."] [""] haskellPackages.ghc. p.shake-bench p.hie-compat p.hls-plugin-api - p.hls-tactics-plugin - p.hls-hlint-plugin + p.hls-class-plugin p.hls-explicit-imports-plugin + p.hls-hlint-plugin p.hls-retrie-plugin + p.hls-tactics-plugin ]; isSupported = compiler == "default" || compiler == defaultCompiler; diff --git a/stack-8.10.1.yaml b/stack-8.10.1.yaml index 684ee46436..22391e5461 100644 --- a/stack-8.10.1.yaml +++ b/stack-8.10.1.yaml @@ -5,10 +5,11 @@ packages: - ./ghcide/hie-compat - ./ghcide/ - ./hls-plugin-api -- ./plugins/tactics -- ./plugins/hls-hlint-plugin +- ./plugins/hls-class-plugin - ./plugins/hls-explicit-imports-plugin +- ./plugins/hls-hlint-plugin - ./plugins/hls-retrie-plugin +- ./plugins/tactics ghc-options: "$everything": -haddock diff --git a/stack-8.10.2.yaml b/stack-8.10.2.yaml index 065d95c53d..9b7a16c630 100644 --- a/stack-8.10.2.yaml +++ b/stack-8.10.2.yaml @@ -5,10 +5,11 @@ packages: - ./ghcide/hie-compat - ./ghcide/ - ./hls-plugin-api -- ./plugins/tactics -- ./plugins/hls-hlint-plugin +- ./plugins/hls-class-plugin - ./plugins/hls-explicit-imports-plugin +- ./plugins/hls-hlint-plugin - ./plugins/hls-retrie-plugin +- ./plugins/tactics ghc-options: "$everything": -haddock diff --git a/stack-8.6.4.yaml b/stack-8.6.4.yaml index 105957a4a4..0ef3a38e8f 100644 --- a/stack-8.6.4.yaml +++ b/stack-8.6.4.yaml @@ -6,10 +6,11 @@ packages: - ./ghcide/hie-compat - ./ghcide/ - ./hls-plugin-api - - ./plugins/tactics - - ./plugins/hls-hlint-plugin + - ./plugins/hls-class-plugin - ./plugins/hls-explicit-imports-plugin + - ./plugins/hls-hlint-plugin - ./plugins/hls-retrie-plugin + - ./plugins/tactics ghc-options: "$everything": -haddock diff --git a/stack-8.6.5.yaml b/stack-8.6.5.yaml index 09006220e6..785a71aab3 100644 --- a/stack-8.6.5.yaml +++ b/stack-8.6.5.yaml @@ -5,10 +5,11 @@ packages: - ./ghcide/hie-compat - ./ghcide/ - ./hls-plugin-api - - ./plugins/tactics - - ./plugins/hls-hlint-plugin + - ./plugins/hls-class-plugin - ./plugins/hls-explicit-imports-plugin + - ./plugins/hls-hlint-plugin - ./plugins/hls-retrie-plugin + - ./plugins/tactics ghc-options: "$everything": -haddock diff --git a/stack-8.8.2.yaml b/stack-8.8.2.yaml index e61d580023..42e1b1bc51 100644 --- a/stack-8.8.2.yaml +++ b/stack-8.8.2.yaml @@ -5,10 +5,11 @@ packages: - ./ghcide/hie-compat - ./ghcide/ - ./hls-plugin-api - - ./plugins/tactics - - ./plugins/hls-hlint-plugin + - ./plugins/hls-class-plugin - ./plugins/hls-explicit-imports-plugin + - ./plugins/hls-hlint-plugin - ./plugins/hls-retrie-plugin + - ./plugins/tactics ghc-options: "$everything": -haddock diff --git a/stack-8.8.3.yaml b/stack-8.8.3.yaml index 0ef996cfbb..eaf22cdd57 100644 --- a/stack-8.8.3.yaml +++ b/stack-8.8.3.yaml @@ -5,10 +5,11 @@ packages: - ./ghcide/hie-compat - ./ghcide/ - ./hls-plugin-api -- ./plugins/tactics -- ./plugins/hls-hlint-plugin +- ./plugins/hls-class-plugin - ./plugins/hls-explicit-imports-plugin +- ./plugins/hls-hlint-plugin - ./plugins/hls-retrie-plugin +- ./plugins/tactics ghc-options: "$everything": -haddock diff --git a/stack-8.8.4.yaml b/stack-8.8.4.yaml index 9c994cc8ed..811f443b70 100644 --- a/stack-8.8.4.yaml +++ b/stack-8.8.4.yaml @@ -5,10 +5,11 @@ packages: - ./ghcide/hie-compat - ./ghcide/ - ./hls-plugin-api -- ./plugins/tactics -- ./plugins/hls-hlint-plugin +- ./plugins/hls-class-plugin - ./plugins/hls-explicit-imports-plugin +- ./plugins/hls-hlint-plugin - ./plugins/hls-retrie-plugin +- ./plugins/tactics ghc-options: "$everything": -haddock diff --git a/stack.yaml b/stack.yaml index 4251c90a99..fcaf10a1ab 100644 --- a/stack.yaml +++ b/stack.yaml @@ -5,10 +5,11 @@ packages: - ./ghcide/hie-compat - ./ghcide/ - ./hls-plugin-api -- ./plugins/tactics -- ./plugins/hls-hlint-plugin +- ./plugins/hls-class-plugin - ./plugins/hls-explicit-imports-plugin +- ./plugins/hls-hlint-plugin - ./plugins/hls-retrie-plugin +- ./plugins/tactics ghc-options: "$everything": -haddock diff --git a/test/functional/Class.hs b/test/functional/Class.hs new file mode 100644 index 0000000000..777071c485 --- /dev/null +++ b/test/functional/Class.hs @@ -0,0 +1,73 @@ +{-# LANGUAGE LambdaCase #-} +-- {-# LANGUAGE ViewPatterns #-} +{-# LANGUAGE OverloadedStrings #-} +{-# LANGUAGE ScopedTypeVariables #-} +module Class + ( tests + ) +where + +import Control.Lens hiding ((<.>)) +import Control.Monad.IO.Class (MonadIO(liftIO)) +import qualified Data.ByteString.Lazy as BS +import qualified Data.Text.Encoding as T +import Language.Haskell.LSP.Test +import Language.Haskell.LSP.Types hiding (_title, _command) +import qualified Language.Haskell.LSP.Types.Lens as J +import System.FilePath +import Test.Hls.Util +import Test.Tasty +import Test.Tasty.Golden +import Test.Tasty.HUnit + +tests :: TestTree +tests = testGroup + "class" + [ testCase "Produces addMinimalMethodPlaceholders code actions for one instance" $ do + runSession hlsCommand fullCaps classPath $ do + doc <- openDoc "T1.hs" "haskell" + _ <- waitForDiagnosticsFromSource doc "typecheck" + caResults <- getAllCodeActions doc + liftIO $ map (^? _CACodeAction . J.title) caResults + @?= + [ Just "Add placeholders for '=='" + , Just "Add placeholders for '/='" + ] + , glodenTest "Creates a placeholder for '=='" "T1" "eq" + $ \(eqAction:_) -> do + executeCodeAction eqAction + , glodenTest "Creates a placeholder for '/='" "T1" "ne" + $ \(_:neAction:_) -> do + executeCodeAction neAction + , glodenTest "Creates a placeholder for 'fmap'" "T2" "fmap" + $ \(_:_:fmapAction:_) -> do + executeCodeAction fmapAction + , glodenTest "Creates a placeholder for multiple methods 1" "T3" "1" + $ \(mmAction:_) -> do + executeCodeAction mmAction + , glodenTest "Creates a placeholder for multiple methods 2" "T3" "2" + $ \(_:mmAction:_) -> do + executeCodeAction mmAction + ] + +_CACodeAction :: Prism' CAResult CodeAction +_CACodeAction = prism' CACodeAction $ \case + CACodeAction action -> Just action + _ -> Nothing + +classPath :: FilePath +classPath = "test" "testdata" "class" + +glodenTest :: String -> FilePath -> FilePath -> ([CodeAction] -> Session ()) -> TestTree +glodenTest name fp deco execute + = goldenVsStringDiff name goldenGitDiff (classPath fp <.> deco <.> "expected" <.> "hs") + $ runSession hlsCommand fullCaps classPath + $ do + doc <- openDoc (fp <.> "hs") "haskell" + _ <- waitForDiagnosticsFromSource doc "typecheck" + actions <- concatMap (^.. _CACodeAction) <$> getAllCodeActions doc + execute actions + BS.fromStrict . T.encodeUtf8 <$> getDocumentEdit doc + +goldenGitDiff :: FilePath -> FilePath -> [String] +goldenGitDiff fRef fNew = ["git", "diff", "--no-index", "--text", "--exit-code", fRef, fNew] diff --git a/test/functional/Main.hs b/test/functional/Main.hs index de8f136797..29c7c4785e 100644 --- a/test/functional/Main.hs +++ b/test/functional/Main.hs @@ -1,5 +1,6 @@ module Main where +import Class import Command import Completion import Config @@ -36,7 +37,8 @@ main = [antXMLRunner, rerunningTests [listingTests, consoleTestReporter]] $ testGroup "haskell-language-server" - [ Command.tests + [ Class.tests + , Command.tests , Completion.tests , Config.tests , Deferred.tests diff --git a/test/testdata/class/T1.eq.expected.hs b/test/testdata/class/T1.eq.expected.hs new file mode 100644 index 0000000000..c3d7d09c03 --- /dev/null +++ b/test/testdata/class/T1.eq.expected.hs @@ -0,0 +1,6 @@ +module T1 where + +data X = X + +instance Eq X where + (==) = _ diff --git a/test/testdata/class/T1.hs b/test/testdata/class/T1.hs new file mode 100644 index 0000000000..9f611ecc05 --- /dev/null +++ b/test/testdata/class/T1.hs @@ -0,0 +1,5 @@ +module T1 where + +data X = X + +instance Eq X where diff --git a/test/testdata/class/T1.ne.expected.hs b/test/testdata/class/T1.ne.expected.hs new file mode 100644 index 0000000000..dc7fe80899 --- /dev/null +++ b/test/testdata/class/T1.ne.expected.hs @@ -0,0 +1,6 @@ +module T1 where + +data X = X + +instance Eq X where + (/=) = _ diff --git a/test/testdata/class/T2.fmap.expected.hs b/test/testdata/class/T2.fmap.expected.hs new file mode 100644 index 0000000000..8e3a4194b2 --- /dev/null +++ b/test/testdata/class/T2.fmap.expected.hs @@ -0,0 +1,13 @@ +module T2 where + +data X a + = A a + | B + +instance + (Eq a) => Eq (X a) + where + +instance + Functor X where + fmap = _ diff --git a/test/testdata/class/T2.hs b/test/testdata/class/T2.hs new file mode 100644 index 0000000000..c929d7df69 --- /dev/null +++ b/test/testdata/class/T2.hs @@ -0,0 +1,12 @@ +module T2 where + +data X a + = A a + | B + +instance + (Eq a) => Eq (X a) + where + +instance + Functor X diff --git a/test/testdata/class/T3.1.expected.hs b/test/testdata/class/T3.1.expected.hs new file mode 100644 index 0000000000..829ce7506c --- /dev/null +++ b/test/testdata/class/T3.1.expected.hs @@ -0,0 +1,13 @@ +module T3 where + +class Test a where + f :: a + f = h + g :: a + h :: a + h = f + {-# MINIMAL f, g | g, h #-} + +instance Test [a] where + f = _ + g = _ diff --git a/test/testdata/class/T3.2.expected.hs b/test/testdata/class/T3.2.expected.hs new file mode 100644 index 0000000000..5872122fc0 --- /dev/null +++ b/test/testdata/class/T3.2.expected.hs @@ -0,0 +1,13 @@ +module T3 where + +class Test a where + f :: a + f = h + g :: a + h :: a + h = f + {-# MINIMAL f, g | g, h #-} + +instance Test [a] where + g = _ + h = _ diff --git a/test/testdata/class/T3.hs b/test/testdata/class/T3.hs new file mode 100644 index 0000000000..72290c9cd9 --- /dev/null +++ b/test/testdata/class/T3.hs @@ -0,0 +1,11 @@ +module T3 where + +class Test a where + f :: a + f = h + g :: a + h :: a + h = f + {-# MINIMAL f, g | g, h #-} + +instance Test [a] where