-
-
Notifications
You must be signed in to change notification settings - Fork 401
Add Code Action for adding a module to your project's cabal file #4617
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
VeryMilkyJoe
wants to merge
1
commit into
haskell:master
Choose a base branch
from
VeryMilkyJoe:feature/cabal-add-module
base: master
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
+1,342
−693
Open
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Large diffs are not rendered by default.
Oops, something went wrong.
326 changes: 0 additions & 326 deletions
326
plugins/hls-cabal-plugin/src/Ide/Plugin/Cabal/CabalAdd.hs
This file was deleted.
Oops, something went wrong.
343 changes: 343 additions & 0 deletions
343
plugins/hls-cabal-plugin/src/Ide/Plugin/Cabal/CabalAdd/CodeAction.hs
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,343 @@ | ||
{-# LANGUAGE AllowAmbiguousTypes #-} | ||
{-# LANGUAGE DeriveAnyClass #-} | ||
{-# LANGUAGE DerivingStrategies #-} | ||
{-# LANGUAGE ExplicitNamespaces #-} | ||
{-# LANGUAGE LambdaCase #-} | ||
{-# LANGUAGE OverloadedStrings #-} | ||
{-# LANGUAGE PartialTypeSignatures #-} | ||
{-# LANGUAGE RecordWildCards #-} | ||
|
||
module Ide.Plugin.Cabal.CabalAdd.CodeAction where | ||
|
||
import Control.Monad.IO.Class (MonadIO, liftIO) | ||
import Control.Monad.Trans.Except | ||
import Data.Aeson.Types (toJSON) | ||
import Data.Foldable (asum) | ||
import Data.Maybe (mapMaybe) | ||
import qualified Data.Text as T | ||
import Development.IDE.Core.PluginUtils (uriToFilePathE) | ||
import Development.IDE.Types.Location (Uri) | ||
import Distribution.PackageDescription | ||
import Distribution.PackageDescription.Configuration (flattenPackageDescription) | ||
import qualified Distribution.Pretty as CabalPretty | ||
import Distribution.Simple.BuildTarget (BuildTarget, | ||
buildTargetComponentName, | ||
readBuildTargets) | ||
import Distribution.Utils.Path (getSymbolicPath) | ||
import Distribution.Verbosity (silent, | ||
verboseNoStderr) | ||
import Ide.Logger | ||
import Ide.Plugin.Cabal.CabalAdd.Types | ||
import Ide.Plugin.Cabal.Completion.Completer.Module (fpToExposedModulePath) | ||
import Ide.Plugin.Cabal.Orphans () | ||
import Ide.Plugin.Error | ||
import Ide.PluginUtils (mkLspCommand) | ||
import Ide.Types (CommandId (CommandId), | ||
PluginId) | ||
|
||
import Control.Lens ((^.)) | ||
import qualified Language.LSP.Protocol.Lens as JL | ||
import Language.LSP.Protocol.Types (CodeActionKind (..), | ||
VersionedTextDocumentIdentifier) | ||
import qualified Language.LSP.Protocol.Types as J | ||
import System.FilePath | ||
import Text.PrettyPrint (render) | ||
import Text.Regex.TDFA | ||
|
||
-------------------------------------------- | ||
-- Add module to cabal file | ||
-------------------------------------------- | ||
|
||
{- | Takes a path to a cabal file, a module path in exposed module syntax | ||
and the contents of the cabal file and generates all possible | ||
code actions for inserting the module into the cabal file | ||
with the given contents. | ||
-} | ||
collectModuleInsertionOptions :: | ||
(MonadIO m) => | ||
Recorder (WithPriority Log) -> | ||
PluginId -> | ||
VersionedTextDocumentIdentifier -> | ||
J.Diagnostic -> | ||
-- | The file path of the cabal file to insert the new module into | ||
FilePath -> | ||
-- | The generic package description of the cabal file to insert the new module into. | ||
GenericPackageDescription -> | ||
-- | The URI of the unknown haskell file/new module to insert into the cabal file. | ||
Uri -> | ||
ExceptT PluginError m [J.CodeAction] | ||
collectModuleInsertionOptions _ plId txtDocIdentifier diag cabalFilePath gpd haskellFilePathURI = do | ||
haskellFilePath <- uriToFilePathE haskellFilePathURI | ||
let configs = concatMap (mkModuleInsertionConfig txtDocIdentifier cabalFilePath haskellFilePath) (makeStanzaItems gpd) | ||
pure $ map (mkCodeActionForModulePath plId diag) configs | ||
where | ||
makeStanzaItems :: GenericPackageDescription -> [StanzaItem] | ||
makeStanzaItems gpd = | ||
mainLibItem pd | ||
++ libItems pd | ||
++ executableItems pd | ||
++ testSuiteItems pd | ||
++ benchmarkItems pd | ||
where | ||
pd = flattenPackageDescription gpd | ||
|
||
{- | Takes a buildInfo of a cabal file component as defined in the generic package description, | ||
and translates it to filepaths of the component's hsSourceDirs, | ||
to be processed for adding modules to exposed-, or other-modules fields in a cabal file. | ||
-} | ||
buildInfoToHsSourceDirs :: BuildInfo -> [FilePath] | ||
buildInfoToHsSourceDirs buildInfo = map getSymbolicPath hsSourceDirs' | ||
where | ||
hsSourceDirs' = hsSourceDirs buildInfo | ||
|
||
{- | Takes the path to the cabal file to insert the module into, | ||
the module path to be inserted, and a stanza representation. | ||
Returns a list of module insertion configs, where each config | ||
represents a possible place to insert the module. | ||
-} | ||
mkModuleInsertionConfig :: VersionedTextDocumentIdentifier -> FilePath -> FilePath -> StanzaItem -> [ModuleInsertionConfig] | ||
mkModuleInsertionConfig txtDocIdentifier cabalFilePath haskellFilePath (StanzaItem{..}) = do | ||
case mkRelativeModulePathM siHsSourceDirs cabalFilePath haskellFilePath of | ||
Just processedModPath -> | ||
[modInsertItem processedModPath "other-modules"] | ||
++ [modInsertItem processedModPath "exposed-modules" | CLibName _ <- [siComponent]] | ||
_ -> [] | ||
where | ||
modInsertItem :: T.Text -> T.Text -> ModuleInsertionConfig | ||
modInsertItem modPath label = | ||
ModuleInsertionConfig | ||
{ targetFile = cabalFilePath | ||
, moduleToInsert = modPath | ||
, modVerTxtDocId = txtDocIdentifier | ||
, insertionStanza = siComponent | ||
, insertionLabel = label | ||
} | ||
|
||
mkCodeActionForModulePath :: PluginId -> J.Diagnostic -> ModuleInsertionConfig -> J.CodeAction | ||
mkCodeActionForModulePath plId diag insertionConfig = | ||
J.CodeAction | ||
{ _title = "Add to " <> label <> " as " <> fieldName | ||
, _kind = Just CodeActionKind_Refactor | ||
, _diagnostics = Just [diag] | ||
, _isPreferred = Nothing | ||
, _disabled = Nothing | ||
, _edit = Nothing | ||
, _command = Just command | ||
, _data_ = Nothing | ||
} | ||
where | ||
fieldName = insertionLabel insertionConfig | ||
command = mkLspCommand plId (CommandId cabalAddModuleCommandId) "Add missing module" (Just [toJSON insertionConfig]) | ||
label = T.pack $ CabalPretty.prettyShow $ insertionStanza insertionConfig | ||
|
||
{- | Takes a list of source subdirectories, a cabal source path and a haskell filepath | ||
and returns a path to the module in exposed module syntax. | ||
The path will be relative to one of the subdirectories, in case the module is contained within one of them. | ||
-} | ||
mkRelativeModulePathM :: [FilePath] -> FilePath -> FilePath -> Maybe T.Text | ||
mkRelativeModulePathM hsSourceDirs cabalSrcPath' haskellFilePath = | ||
asum $ | ||
map | ||
( \srcDir -> do | ||
let relMP = makeRelative (normalise (cabalSrcPath </> srcDir)) haskellFilePath | ||
if relMP == haskellFilePath then Nothing else Just $ fpToExposedModulePath cabalSrcPath relMP | ||
) | ||
hsSourceDirs | ||
where | ||
cabalSrcPath = takeDirectory cabalSrcPath' | ||
|
||
isUnknownModuleDiagnostic :: J.Diagnostic -> Bool | ||
isUnknownModuleDiagnostic diag = (msg =~ regex) | ||
where | ||
msg :: T.Text | ||
msg = diag ^. JL.message | ||
regex :: T.Text | ||
regex = "Loading the module [\8216'][^\8217']*[\8217'] failed." | ||
|
||
-------------------------- | ||
-- Below are several utility functions which create a StanzaItem for each of the possible Stanzas, | ||
-- these all have specific constructors we need to match, so we can't generalise this process well. | ||
-------------------------- | ||
|
||
benchmarkItems :: PackageDescription -> [StanzaItem] | ||
benchmarkItems pd = | ||
map | ||
( \benchmark -> | ||
StanzaItem | ||
{ siComponent = CBenchName $ benchmarkName benchmark | ||
, siHsSourceDirs = buildInfoToHsSourceDirs $ benchmarkBuildInfo benchmark | ||
} | ||
) | ||
(benchmarks pd) | ||
|
||
testSuiteItems :: PackageDescription -> [StanzaItem] | ||
testSuiteItems pd = | ||
map | ||
( \testSuite -> | ||
StanzaItem | ||
{ siComponent = CTestName $ testName testSuite | ||
, siHsSourceDirs = buildInfoToHsSourceDirs $ testBuildInfo testSuite | ||
} | ||
) | ||
(testSuites pd) | ||
|
||
executableItems :: PackageDescription -> [StanzaItem] | ||
executableItems pd = | ||
map | ||
( \executable -> | ||
StanzaItem | ||
{ siComponent = CExeName $ exeName executable | ||
, siHsSourceDirs = buildInfoToHsSourceDirs $ buildInfo executable | ||
} | ||
) | ||
(executables pd) | ||
|
||
libItems :: PackageDescription -> [StanzaItem] | ||
libItems pd = | ||
mapMaybe | ||
( \subLib -> | ||
case libName subLib of | ||
LSubLibName compName -> | ||
Just | ||
StanzaItem | ||
{ siComponent = CLibName $ LSubLibName compName | ||
, siHsSourceDirs = buildInfoToHsSourceDirs $ libBuildInfo subLib | ||
} | ||
_ -> Nothing | ||
) | ||
(subLibraries pd) | ||
|
||
mainLibItem :: PackageDescription -> [StanzaItem] | ||
mainLibItem pd = | ||
case library pd of | ||
Just lib -> | ||
[ StanzaItem | ||
{ siComponent = CLibName LMainLibName | ||
, siHsSourceDirs = buildInfoToHsSourceDirs $ libBuildInfo lib | ||
} | ||
] | ||
Nothing -> [] | ||
|
||
-------------------------------------------- | ||
-- Add dependency to a cabal file | ||
-------------------------------------------- | ||
|
||
{- | 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 and cabal file paths to create a relative path | ||
to the haskell file, which is used to get a `BuildTarget`. | ||
-} | ||
addDependencySuggestCodeAction :: | ||
PluginId -> | ||
-- | Cabal's versioned text identifier | ||
VersionedTextDocumentIdentifier -> | ||
-- | A dependency-version suggestion pairs | ||
[(T.Text, T.Text)] -> | ||
-- | Path to the haskell file (source of diagnostics) | ||
FilePath -> | ||
-- | Path to the cabal file (that will be edited) | ||
FilePath -> | ||
GenericPackageDescription -> | ||
IO [J.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 the `cabal-add` command with default behaviour | ||
[] -> pure $ mkCodeActionForDependency cabalFilePath Nothing <$> suggestions | ||
-- Otherwise provide actions for all found targets | ||
targets -> | ||
pure $ | ||
concat | ||
[ mkCodeActionForDependency cabalFilePath (Just $ buildTargetToStringRepr target) | ||
<$> suggestions | ||
| target <- targets | ||
] | ||
where | ||
{- | Note the use of the `pretty` function. | ||
It converts the `BuildTarget` to an acceptable string representation. | ||
It will be used as the input for `cabal-add`'s `executeConfig`. | ||
-} | ||
buildTargetToStringRepr target = render $ CabalPretty.pretty $ buildTargetComponentName target | ||
|
||
{- | Finds the build targets that are used in `cabal-add`. | ||
Note the unorthodox usage of `readBuildTargets`: | ||
If the relative path to the haskell file is provided, | ||
`readBuildTargets` will return the build targets, this | ||
module is mentioned in (either 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] | ||
|
||
mkCodeActionForDependency :: FilePath -> Maybe String -> (T.Text, T.Text) -> J.CodeAction | ||
mkCodeActionForDependency 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 = | ||
CabalAddDependencyCommandParams | ||
{ depCabalPath = cabalFilePath | ||
, depVerTxtDocId = verTxtDocId | ||
, depBuildTarget = target | ||
, depDependency = suggestedDep | ||
, depVersion = version | ||
} | ||
command = mkLspCommand plId (CommandId cabalAddDependencyCommandId) "Add dependency" (Just [toJSON params]) | ||
in | ||
J.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 :: J.Diagnostic -> [(T.Text, T.Text)] | ||
hiddenPackageSuggestion diag = getMatch (msg =~ regex) | ||
where | ||
msg :: T.Text | ||
msg = diag ^. JL.message | ||
regex :: T.Text | ||
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 (_, _, _, _) = [] |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.