diff --git a/cabal-parser/CHANGELOG.md b/cabal-parser/CHANGELOG.md new file mode 100644 index 0000000000..c295fbd53d --- /dev/null +++ b/cabal-parser/CHANGELOG.md @@ -0,0 +1,5 @@ +# Revision history for cabal-parser + +## 0.1.0.0 -- YYYY-mm-dd + +* First version. Released on an unsuspecting world. diff --git a/cabal-parser/LICENSE b/cabal-parser/LICENSE new file mode 100644 index 0000000000..addfa4f37e --- /dev/null +++ b/cabal-parser/LICENSE @@ -0,0 +1,20 @@ +Copyright (c) 2023 Jana Chadt + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be included +in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/cabal-parser/cabal-parser.cabal b/cabal-parser/cabal-parser.cabal new file mode 100644 index 0000000000..1be8b71b60 --- /dev/null +++ b/cabal-parser/cabal-parser.cabal @@ -0,0 +1,71 @@ +cabal-version: 3.0 +name: cabal-parser +version: 0.1.0.0 +synopsis: Parses cabal files to AST, annotated with source positions +license: MIT +license-file: LICENSE +author: VeryMilkyJoe +maintainer: jana.chadt@nets.at +category: Parser +build-type: Simple +extra-doc-files: CHANGELOG.md + +common warnings + ghc-options: -Wall + +library + import: warnings + exposed-modules: + Text.Cabal.Data + Text.Cabal.Parser + Text.Cabal.Types + Text.Cabal.Value + + build-depends: + , base + , containers + , lsp + , lsp-types + , megaparsec >= 8 && < 10 + , prettyprinter >= 1.7 + , text + hs-source-dirs: src + default-language: Haskell2010 + +executable cabal-parser-cli + import: warnings + default-language: Haskell2010 + hs-source-dirs: exe + main-is: Main.hs + other-modules: Options + build-depends: + , base + , cabal-parser + , optparse-applicative + , filepath + , directory + , text + , transformers + , megaparsec + , prettyprinter + +test-suite tests + import: warnings + default-language: Haskell2010 + type: exitcode-stdio-1.0 + hs-source-dirs: test + main-is: Main.hs + other-modules: Parser + build-depends: + , base + , Cabal-syntax + , cabal-parser + , directory + , filepath + , lsp-types + , megaparsec + , prettyprinter + , tasty + , tasty-expected-failure + , tasty-hunit + , text diff --git a/cabal-parser/exe/Main.hs b/cabal-parser/exe/Main.hs new file mode 100644 index 0000000000..9bb126847d --- /dev/null +++ b/cabal-parser/exe/Main.hs @@ -0,0 +1,92 @@ +module Main where + +import Control.Monad +import Control.Monad.IO.Class +import Control.Monad.Trans.State.Strict +import qualified Data.Text.IO as T +import Options +import Options.Applicative +import Prettyprinter +import System.Directory (canonicalizePath, + doesDirectoryExist, + listDirectory) +import System.FilePath +import System.IO +import Text.Cabal.Parser +import Text.Megaparsec (errorBundlePretty) + +main :: IO () +main = do + options <- execParser opts + case optCommand options of + Parse parseOpts -> do + analytics <- flip execStateT defParseAnalytics $ forM_ (parseFiles parseOpts) $ \fileTarget' -> do + fileTarget <- liftIO $ canonicalizePath fileTarget' + isDirectory <- liftIO $ doesDirectoryExist fileTarget + + if isDirectory + then do + targets <- liftIO $ listDirectory fileTarget + forM_ targets $ \target -> + parseFileAndReport parseOpts (fileTarget target) + else do + parseFileAndReport parseOpts fileTarget + + putStrLn $ showAnalytics analytics + +parseFileAndReport :: MonadIO m => ParseOptions -> FilePath -> StateT ParseAnalytics m () +parseFileAndReport parseOpts cabalFile = do + cabalFileContents <- liftIO $ T.readFile cabalFile + liftIO $ putStr $ "Parsing \"" <> cabalFile <> "\"" + liftIO $ hFlush stdout + case parseCabalFile cabalFile cabalFileContents of + Left err -> do + addFailure + liftIO $ putStrLn $ " failed" + when (not $ silent parseOpts) $ do + liftIO $ putStrLn $ errorBundlePretty err + Right ast -> do + addSuccess + liftIO $ putStrLn $ " succeeded" + when (showAst parseOpts) $ do + liftIO $ putStrLn $ show $ pretty ast + +data ParseAnalytics = ParseAnalytics + { parsedSuccessfully :: !Int + , parsedFailed :: !Int + , parsedNum :: !Int + } + deriving (Show, Eq, Ord) + +defParseAnalytics :: ParseAnalytics +defParseAnalytics = + ParseAnalytics + { parsedSuccessfully = 0 + , parsedFailed = 0 + , parsedNum = 0 + } + +addSuccess :: (Monad m) => StateT ParseAnalytics m () +addSuccess = do + modify' + ( \pa -> + pa + { parsedSuccessfully = parsedSuccessfully pa + 1 + , parsedNum = parsedNum pa + 1 + } + ) + +addFailure :: (Monad m) => StateT ParseAnalytics m () +addFailure = do + modify' + ( \pa -> + pa + { parsedFailed = parsedFailed pa + 1 + , parsedNum = parsedNum pa + 1 + } + ) + +showAnalytics :: ParseAnalytics -> String +showAnalytics a + | parsedFailed a == 0 && parsedSuccessfully a > 0 = "Successfully parsed all cabal files (" <> show (parsedNum a) <> ")" + | otherwise = "Parsed " <> show (parsedSuccessfully a) <> " out of " <> show (parsedNum a) <> " cabal files" diff --git a/cabal-parser/exe/Options.hs b/cabal-parser/exe/Options.hs new file mode 100644 index 0000000000..9850c09c2b --- /dev/null +++ b/cabal-parser/exe/Options.hs @@ -0,0 +1,55 @@ +module Options + ( Options(..) + , Command(..) + , ParseOptions(..) + , opts + ) where + +import Options.Applicative + +data Options = Options + { optCommand :: Command + } + deriving (Show, Eq, Ord) + +data Command + = Parse ParseOptions + deriving (Show, Eq, Ord) + +data ParseOptions = ParseOptions + { parseFiles :: [FilePath] + , silent :: Bool + , showAst :: Bool + } + deriving (Show, Eq, Ord) + +opts :: ParserInfo Options +opts = info (options <**> helper) + ( fullDesc + <> progDesc "Command Line Interface for cabal-parse" + <> header "Parse your cabal files" + ) + +options :: Parser Options +options = + Options + <$> parseCommands + +parseCommands :: Parser Command +parseCommands = + subparser + ( command "parse" (info (Parse <$> parseCommand) $ progDesc "Check cabal-parse can parse your cabal file") + ) + +parseCommand :: Parser ParseOptions +parseCommand = + ParseOptions + <$> some (argument str (metavar "FILES")) + <*> flag False True + ( long "silent" + <> help "Don't show parse errors on stdout" + ) + <*> flag False True + ( long "ast" + <> help "Show the parsed AST in a human readable form" + ) diff --git a/cabal-parser/src/Text/Cabal/Data.hs b/cabal-parser/src/Text/Cabal/Data.hs new file mode 100644 index 0000000000..542a6daa07 --- /dev/null +++ b/cabal-parser/src/Text/Cabal/Data.hs @@ -0,0 +1,188 @@ +{-# LANGUAGE OverloadedStrings #-} + +module Text.Cabal.Data where + +import Data.Map (Map) +import qualified Data.Map as Map +import qualified Data.Text as T +import Text.Cabal.Types +import Text.Cabal.Value + +{- | Top level keywords of a cabal file and the parsers + to be used to parse their corresponding values. +-} +cabalKeywords :: Map T.Text (Bool -> Parser [ValueItem]) +cabalKeywords = + Map.fromList + [ ("cabal-version:", defaultValueParser) + , ("name:", defaultValueParser) + , ("version:", defaultValueParser) + , ("build-type:", defaultValueParser) + , ("license:", defaultValueParser) + , ("license-file:", defaultValueParser) + , ("license-files:", defaultValueParser) + , ("copyright:", defaultValueParser) + , ("author:", defaultValueParser) + , ("maintainer:", defaultValueParser) + , ("stability:", defaultValueParser) + , ("homepage:", defaultValueParser) + , ("bug-reports:", defaultValueParser) + , ("package-url:", defaultValueParser) + , ("synopsis:", defaultValueParser) + , ("description:", defaultValueParser) + , ("category:", defaultValueParser) + , ("tested-with:", defaultValueParser) + , ("data-files:", defaultValueParser) + , ("data-dir:", defaultValueParser) + , ("extra-source-files:", defaultValueParser) + , ("extra-doc-files:", defaultValueParser) + , ("extra-tmp-files:", defaultValueParser) + ] + +{- | Map, containing all stanzas in a cabal file as keys + and lists of their possible nested keywords as values + and the parsers to use to parse the keywords' corresponding values. +-} +stanzaKeywordMap :: Map T.Text (Map T.Text (Bool -> Parser [ValueItem])) +stanzaKeywordMap = + Map.fromList + [ + ( "library" + , Map.fromList $ + [ ("exposed-modules:", moduleParser) + , ("virtual-modules:", moduleParser) + , ("exposed:", defaultValueParser) + , ("import:", defaultValueParser) + , ("visibility:", defaultValueParser) + , ("reexported-modules:", defaultValueParser) + , ("signatures:", defaultValueParser) + , ("other-modules:", moduleParser) + ] + ++ libExecTestBenchCommons + ) + , + ( "executable" + , Map.fromList $ + [ ("main-is:", defaultValueParser) + , ("import:", defaultValueParser) + , ("scope:", defaultValueParser) + , ("other-modules:", moduleParser) + ] + ++ libExecTestBenchCommons + ) + , + ( "test-suite" + , Map.fromList $ + [ ("type:", defaultValueParser) + , ("import:", defaultValueParser) + , ("main-is:", defaultValueParser) + , ("other-modules:", moduleParser) + ] + ++ libExecTestBenchCommons + ) + , + ( "benchmark" + , Map.fromList $ + [ ("type:", defaultValueParser) + , ("import:", defaultValueParser) + , ("main-is:", defaultValueParser) + , ("other-modules:", moduleParser) + ] + ++ libExecTestBenchCommons + ) + , + ( "foreign-library" + , Map.fromList + [ ("type:", defaultValueParser) + , ("options:", defaultValueParser) + , ("import:", defaultValueParser) + , ("mod-def-file:", defaultValueParser) + , ("lib-version-info:", defaultValueParser) + , ("lib-version-linux:", defaultValueParser) + ] + ) + , + ( "flag" + , Map.fromList + [ ("description:", defaultValueParser) + , ("default:", defaultValueParser) + , ("manual:", defaultValueParser) + , ("lib-def-file:", defaultValueParser) + , ("lib-version-info:", defaultValueParser) + , ("lib-version-linux:", defaultValueParser) + ] + ) + , + ( "source-repository" + , Map.fromList + [ + ( "type:" + , defaultValueParser + ) + , ("location:", defaultValueParser) + , ("module:", defaultValueParser) + , ("branch:", defaultValueParser) + , ("tag:", defaultValueParser) + , ("subdir:", defaultValueParser) + ] + ) + , + ( "custom-setup" + , Map.fromList + [("setup-depends:", defaultValueParser)] + ) + , + ( "common" + , Map.fromList + libExecTestBenchCommons + ) + ] + where + libExecTestBenchCommons = + [ ("autogen-modules:", moduleParser) + , ("build-tools:", defaultValueParser) + , ("build-depends:", defaultValueParser) + , ("hs-source-dirs:", filepathParser) + , ("extensions:", defaultValueParser) + , ("default-extensions:", defaultValueParser) + , ("other-extensions:", defaultValueParser) + , ("default-language:", defaultValueParser) + , ("other-languages:", defaultValueParser) + , ("build-tool-depends:", defaultValueParser) + , ("buildable:", defaultValueParser) + , ("ghc-options:", defaultValueParser) + , ("ghc-prof-options:", defaultValueParser) + , ("ghc-shared-options:", defaultValueParser) + , ("ghcjs-options:", defaultValueParser) + , ("ghcjs-prof-options:", defaultValueParser) + , ("ghcjs-shared-options:", defaultValueParser) + , ("includes:", defaultValueParser) + , ("install-includes:", defaultValueParser) + , ("include-dirs:", defaultValueParser) + , ("c-sources:", defaultValueParser) + , ("cxx-sources:", defaultValueParser) + , ("asm-sources:", defaultValueParser) + , ("cmm-sources:", defaultValueParser) + , ("js-sources:", defaultValueParser) + , ("extra-libraries:", defaultValueParser) + , ("extra-ghci-libraries:", defaultValueParser) + , ("extra-bundled-libraries:", defaultValueParser) + , ("extra-lib-dirs:", defaultValueParser) + , ("cc-options:", defaultValueParser) + , ("cpp-options:", defaultValueParser) + , ("cxx-options:", defaultValueParser) + , ("cmm-options:", defaultValueParser) + , ("asm-options:", defaultValueParser) + , ("ld-options:", defaultValueParser) + , ("pkgconfig-depends:", defaultValueParser) + , ("frameworks:", defaultValueParser) + , ("extra-framework-dirs:", defaultValueParser) + , ("mixins:", defaultValueParser) + ] + +{- | Returns a list of all possible keywords that may occur in a cabal file +regardless of which contexts these keywords occur in +-} +allKeywords :: Map T.Text (Bool -> Parser [ValueItem]) +allKeywords = + Map.unions $ [cabalKeywords] <> Map.elems stanzaKeywordMap diff --git a/cabal-parser/src/Text/Cabal/Parser.hs b/cabal-parser/src/Text/Cabal/Parser.hs new file mode 100644 index 0000000000..0292bd22e4 --- /dev/null +++ b/cabal-parser/src/Text/Cabal/Parser.hs @@ -0,0 +1,331 @@ +{-# LANGUAGE OverloadedStrings #-} +{-# LANGUAGE ScopedTypeVariables #-} + +module Text.Cabal.Parser where + +import Data.Functor (void) +import qualified Data.Map as M +import Data.Maybe (fromMaybe) +import qualified Data.Text as T +import Text.Cabal.Data +import Text.Cabal.Types +import Text.Cabal.Value +import Text.Megaparsec +import Text.Megaparsec.Char + +-------------------------------- +-- Parser +-------------------------------- + +parseCabalFile :: FilePath -> T.Text -> Either ErrorBundle CabalAST +parseCabalFile srcFile contents = do + let parsed = parse cabalParser srcFile contents + parsed + +{- | Parses a cabal file into an AST structure of the cabal file's fields and stanzas. + + Requires the cabal file to be valid, otherwise the parser's behaviour is undefined. + Ignores any comments and empty lines. +-} +cabalParser :: Parser CabalAST +cabalParser = do + (items, loc) <- annotateSrcLoc $ do + throwAwayLines + items <- + sepBy + (stanzaParser <|> topLevelFieldParser) + throwAwayLines + throwAwayLines + eof + pure items + pure $ CabalAST items (Annotation Nothing loc) + +{- | Parses a top-level field which is a field with no indentation. +-} +topLevelFieldParser :: Parser ASTItem +topLevelFieldParser = do + (_, field) <- fieldParser $ Just 0 + pure $ FieldItem field + +{- | Parses a field with a possible indentation. + + Returns the indentation of the parsed field and the field. + In order to parse a field, first a keyword has to be parsed + then an arbitrary number of values which are parsed + according to the keyword's value parser. +-} +fieldParser :: Maybe Int -> Parser (Maybe Int, FieldItem) +fieldParser indentation = do + indentNum <- + case indentation of + Nothing -> do + indents <- many $ char ' ' + pure $ length indents + Just indentNum -> do + indentationParser indentNum + pure indentNum + hspace + ((kw, vals), fieldLoc) <- annotateSrcLoc $ do + (keyword, kwLoc) <- annotateSrcLoc $ choice [keywordParser, externalKeywordParser] + let valParser = fromMaybe defaultValueParser $ M.lookup keyword allKeywords + values <- valuesParser valParser indentNum + pure (KeyWord keyword (Annotation Nothing kwLoc), values) + pure (Just indentNum, Field kw vals (Annotation Nothing fieldLoc)) + where + -- any keyword can also be written in the cabal file, + -- when prefixed with a 'x-', indicating it to be external + externalKeywordParser :: Parser T.Text + externalKeywordParser = do + prefix <- string' "x-" + kw <- keywordParser + pure $ prefix <> kw + + -- a keyword is some word ending with a colon + keywordParser :: Parser T.Text + keywordParser = do + kw <- many $ noneOf [':', '\r', '\n'] + suffix <- string ":" + pure $ T.pack kw <> suffix + + -- tries to apply the value parser in braces or without them, + -- the boolean signifies whether the called parser is in a braces context, + -- i.e. whether indentation should be respected. + -- when no values can be parsed with the first two options, we assume + -- that there are no values + valuesParser :: (Bool -> Parser [ValueItem]) -> Int -> Parser ValueItems + valuesParser valParser' indentNum' = + choice + [ label "braces enclosed " $ try $ do + ((parsedValues, valsLoc), braces) <- annotateBraces $ annotateSrcLoc $ valueLinesParser Nothing $ valParser' True + pure $ Values parsedValues (Annotation (Just braces) valsLoc) + , label "not enclosed" $ try $ do + (parsedValues, valsLoc) <- annotateSrcLoc $ valueLinesParser (Just indentNum') $ valParser' False + pure $ Values parsedValues (Annotation Nothing valsLoc) + , do + (_, loc) <- annotateSrcLoc $ pure () + pure $ Values [] (Annotation Nothing loc) + ] + +{- | Parses a field with a possible indentation. + + Returns the indentation of the parsed field and the field. + In order to parse a field, first a known keyword has to be parsed + and then a number of values which are parsed according to the keyword's value parser. +-} +stanzaFieldItemParser :: Maybe Int -> Parser (Maybe Int, StanzaElement) +stanzaFieldItemParser indentation = do + (indentNum, sItem) <- + choice + [ try $ stanzaConditionalParser indentation + , do + (iNum, fI) <- fieldParser indentation + pure (iNum, StanzaField fI) + ] + pure (indentNum, sItem) + +{- | Parses a conditional block inside a stanza with a possible indentation. + + Returns the indentation of the parsed block and the conditional block itself. + Parses either an if, ifelse or else block. +-} +stanzaConditionalParser :: Maybe Int -> Parser (Maybe Int, StanzaElement) +stanzaConditionalParser indentation = do + indentNum <- + case indentation of + Nothing -> do + indents <- many $ char ' ' + pure $ length indents + Just indentNum -> do + indentationParser indentNum + pure indentNum + hspace + cond <- + choice + [ conditionalParser ifConditionParser + , conditionalParser elifConditionParser + , conditionalParser elseConditionParser + ] + pure (Just indentNum, StanzaConditional cond) + +{- | Parses the conditional block corresponding to the given parser. + + A conditional block consists of a conditional declaration and possible condition + (defined by the given parser) and a number of fields guarded by the condition. +-} +conditionalParser :: Parser ConditionItem -> Parser ConditionalItem +conditionalParser condParser = do + ((condDecl, condFields), loc) <- annotateSrcLoc $ do + condItem <- condParser + throwAwayLines + fields <- stanzaFieldsParser + pure (condItem, fields) + throwAwayLines + pure $ Conditional condDecl condFields (Annotation Nothing loc) + +-- | Parses an if declaration and condition with possible trailing spaces. +ifConditionParser :: Parser ConditionItem +ifConditionParser = do + ((ifTxt, ifCond), loc) <- annotateSrcLoc $ do + ifTxt' <- ifTextParser + hspace + ifCond' <- conditionParser + pure (ifTxt', ifCond') + pure $ IfCondition ifTxt ifCond (Annotation Nothing loc) + +-- | Parses an elif declaration and condition with possible trailing spaces. +elifConditionParser :: Parser ConditionItem +elifConditionParser = do + ((elifTxt, elifCond), loc) <- annotateSrcLoc $ do + elifTxt' <- elifTextParser + hspace + elifCond' <- conditionParser + pure (elifTxt', elifCond') + pure $ ElIfCondition elifTxt elifCond (Annotation Nothing loc) + +-- | Parses an else declaration with possible trailing spaces. +elseConditionParser :: Parser ConditionItem +elseConditionParser = do + (txt, loc) <- annotateSrcLoc $ do + txt' <- elseTextParser + hspace + pure txt' + pure $ ElseCondition txt (Annotation Nothing loc) + +{- | No white spaces before are parsed, +preserves capitalization of else in original file. +-} +elseTextParser :: Parser ElseConditionItem +elseTextParser = do + (elifTxt, loc) <- annotateSrcLoc $ string' "else" + pure $ Else elifTxt (Annotation Nothing loc) + +{- | No white spaces before are parsed, +preserves capitalization of elif in original file. +-} +elifTextParser :: Parser ElIfConditionItem +elifTextParser = do + -- preserves case of if in original file + (elifTxt, loc) <- annotateSrcLoc $ string' "elif" + pure $ ElIf elifTxt (Annotation Nothing loc) + +{- | No white spaces before are parsed, +preserves capitalization of if in original file. +-} +ifTextParser :: Parser IfConditionItem +ifTextParser = do + -- + (ifTxt, loc) <- annotateSrcLoc $ string' "if" + pure $ If ifTxt (Annotation Nothing loc) + +conditionParser :: Parser Condition +conditionParser = do + (cond, loc) <- + annotateSrcLoc $ many $ noneOf ['\n', '{','}'] + pure $ Cond (T.pack cond) (Annotation Nothing loc) + +{- | Parses a stanza into a StanzaItem. + + First a stanza declaration is parsed and then + an arbitrary number of fields with an indentation that is + determined when parsing the first line of fields. +-} +stanzaParser :: Parser ASTItem +stanzaParser = do + ((stanzaDecl, fieldItems), loc) <- annotateSrcLoc $ do + stanzaDecl' <- stanzaDeclarationParser + throwAwayLines + fields <- stanzaFieldsParser + pure (stanzaDecl', fields) + pure $ StanzaItem (Stanza stanzaDecl fieldItems (Annotation Nothing loc)) + +stanzaFieldsParser :: Parser StanzaElements +stanzaFieldsParser = + choice + [ label "braces enclosed " $ try $ do + ((parsedValues, valsLoc), braces) <- annotateBraces $ annotateSrcLoc $ stanzaFieldItemsParserIndentM Nothing + throwAwayLines + pure $ StanzaElements parsedValues (Annotation (Just braces) valsLoc) + , label "not enclosed" $ try $ do + (parsedValues, valsLoc) <- annotateSrcLoc indentedStanzaFieldItemsParser + pure $ StanzaElements parsedValues (Annotation Nothing valsLoc) + ] + +{- | Parses a list of field items in a stanza. + + First the field parser is called with undecided indentation + in order to figure out the indentation for fields in the stanza, + after the indentation is determined, an arbitrary number of fields + with this indentation is parsed. +-} +indentedStanzaFieldItemsParser :: Parser [StanzaElement] +indentedStanzaFieldItemsParser = do + (indentNum, firstField) <- stanzaFieldItemParser Nothing + stanzaFields <- stanzaFieldItemsParserIndentM indentNum + pure $ [firstField] <> stanzaFields + +stanzaFieldItemsParserIndentM :: Maybe Int -> Parser [StanzaElement] +stanzaFieldItemsParserIndentM indent = do + stanzaFields <- many $ try $ do + throwAwayLines + (_, field) <- stanzaFieldItemParser indent + pure field + throwAwayLines + pure stanzaFields + +{- | Parses a stanza context declaration, which consists of + a stanza type and a possible stanza name + with an arbitrary number of spaces in-between. +-} +stanzaDeclarationParser :: Parser StanzaDecl +stanzaDeclarationParser = do + ((sType, sName), loc) <- annotateSrcLoc $ do + sType' <- stanzaTypeParser + hspace + sName' <- stanzaNameParser + pure (sType', sName') + throwAwayLines + pure $ StanzaDecl sType sName (Annotation Nothing loc) + where + -- Parses a stanza type which has to match the list of existing stanza types + -- based on the cabal specification, does not take care of spaces + stanzaTypeParser :: Parser StanzaTypeItem + stanzaTypeParser = do + (sType, loc) <- annotateSrcLoc $ choice (map string' $ M.keys stanzaKeywordMap) + pure $ StanzaType sType (Annotation Nothing loc) + + -- Parses a stanza name. + stanzaNameParser :: Parser (Maybe StanzaNameItem) + stanzaNameParser = do + (sNameM, loc) <- + annotateSrcLoc $ many (alphaNumChar <|> char '-' <|> char '_') + case sNameM of + "" -> pure Nothing + _ -> pure $ Just $ StanzaName (T.pack sNameM) (Annotation Nothing loc) + +{- | Parses an arbitrary number of lines with the given indentation. +-} +valueLinesParser :: Maybe Int -> Parser [ValueItem] -> Parser [ValueItem] +valueLinesParser indentNumM p = do + r <- optional p + res <- valueLineParser + throwAwayLines + pure $ fromMaybe [] r <> res + where + -- Parses a single line with possibly multiple values. + valueLineParser :: Parser [ValueItem] + valueLineParser = do + res <- many $ try $ do + throwAwayLines + case indentNumM of + Just indentNum -> do + indentationParser indentNum + void $ char ' ' + Nothing -> pure () + r <- p + pure r + pure $ concat res + +-- | Discards the given number of spaces +indentationParser :: Int -> Parser () +indentationParser indentNum = do + void $ string $ T.replicate indentNum " " diff --git a/cabal-parser/src/Text/Cabal/Types.hs b/cabal-parser/src/Text/Cabal/Types.hs new file mode 100644 index 0000000000..5fb1a17772 --- /dev/null +++ b/cabal-parser/src/Text/Cabal/Types.hs @@ -0,0 +1,331 @@ +{-# LANGUAGE OverloadedStrings #-} + +module Text.Cabal.Types where + +import qualified Data.Text as T +import Data.Void (Void) +import Language.LSP.Protocol.Types +import qualified Language.LSP.Protocol.Types as LSP (Position (..), Range (..)) +import Prettyprinter +import Text.Megaparsec (Parsec) +import Text.Megaparsec.Error (ParseErrorBundle, + errorBundlePretty) + +----------------------------------------------- +-- Parser +----------------------------------------------- + +type Parser = Parsec Void T.Text + +type ErrorBundle = ParseErrorBundle T.Text Void + +----------------------------------------------- +-- Cabal File Structure Representation +----------------------------------------------- + +{- | An abstract syntax tree representing a cabal file. + + The cabal file is represented by a list of AST items + and the source range of the file (0,0 up until the end of the file). +-} +data CabalAST = CabalAST [ASTItem] Annotation + deriving (Show, Eq, Ord) + +{- | Represents an annotation any element in the ast can have. + + Meaning: + * whether the element is wrapped in braces and, if so, + where these braces are located, + * the range of the element, i.e. where in the cabal file is it + located. +-} +data Annotation = Annotation (Maybe Braces) LSP.Range + deriving (Show, Eq, Ord) + +data Braces = Braces + { openingBrace :: LSP.Position + , closingBrace :: LSP.Position + } + deriving (Show, Eq, Ord) + +{- | An AST item which can either represent a field or + stanza. +-} +data ASTItem = FieldItem FieldItem | StanzaItem StanzaItem + deriving (Show, Eq, Ord) + +{- | Represents a stanza in a cabal file and consists + of the stanza's declaration and a list of its elements + which can be conditionals or fields. + + Range: From the start of the stanza declaration + up until the end of the last field item. +-} +data StanzaItem = Stanza StanzaDecl StanzaElements Annotation + deriving (Show, Eq, Ord) + +{- | Represents a list of elements in a stanza, these are either + conditional blocks or fields and can be wrapped in braces. +-} +data StanzaElements = StanzaElements [StanzaElement] Annotation + deriving (Show, Eq, Ord) + +{- | Represents a stanza declaration, which consists of + the stanza's type and possibly the stanza's name, in case + it is named. + + Range: From the start of the stanza type until the and of the + stanza name or until the end of the stanza type in case the + stanza is unnamed. +-} +data StanzaDecl = StanzaDecl StanzaTypeItem (Maybe StanzaNameItem) Annotation + deriving (Show, Eq, Ord) + +{- | Represents a field in a cabal file which consists of + a key word item and the values of the field. + + Range: From the start of the key word item until the + end of the last value. +-} +data FieldItem = Field KeyWordItem ValueItems Annotation + deriving (Show, Eq, Ord) + +{- | Represents either a stanza's field or a conditional block + inside the stanza. +-} +data StanzaElement = StanzaField FieldItem | StanzaConditional ConditionalItem + deriving (Show, Eq, Ord) + +{- | Represents a conditional block, containing the conditional's + keyword and condition and the list of stanza elements that apply + if the conditional is satisfied. + + Range: From the start of the conditional's keyword until the end of + the last stanza element in the list. +-} +data ConditionalItem = Conditional ConditionItem StanzaElements Annotation + deriving (Show, Eq, Ord) + +{- | Represents either an if, elif or else keyword and the corresponding + condition. + + Range: From the start of the keyword until the end of the condition + formula. +-} +data ConditionItem + = IfCondition IfConditionItem Condition Annotation + | ElIfCondition ElIfConditionItem Condition Annotation + | ElseCondition ElseConditionItem Annotation + deriving (Show, Eq, Ord) + +data IfConditionItem = If T.Text Annotation + deriving (Show, Eq, Ord) + +data ElIfConditionItem = ElIf T.Text Annotation + deriving (Show, Eq, Ord) + +data ElseConditionItem = Else T.Text Annotation + deriving (Show, Eq, Ord) + +{- | Represents a condition formula in a conditional + block. + + Range: From the start of the formula until the end. +-} +data Condition = Cond T.Text Annotation + deriving (Show, Eq, Ord) + +{- | Represents the values of a field in a cabal file + in the form of a list of value items. + + Can be wrapped in braces. + Range: From the start of the first value up until + the end of the last value in the list. +-} +data ValueItems = Values [ValueItem] Annotation + deriving (Show, Eq, Ord) + +{- | Represents a keyword in a cabal file, + can be a key word in a stanza or in the top + level. + + Range: From the start of the keyword up until + after the semi-colon. +-} +data KeyWordItem = KeyWord T.Text Annotation + deriving (Show, Eq, Ord) + +{- | Represents a single value in a field. + + The values may contain some commas and white spaces, + so these might need to be stripped when one wants to + read the actual values. + + Range: From the start of the value up until the end. +-} +data ValueItem = Value T.Text Annotation + deriving (Show, Eq, Ord) + +{- | Represents the name of a named stanza. + + Range: From the start of the name up until the end. +-} +data StanzaNameItem = StanzaName T.Text Annotation + deriving (Show, Ord, Eq) + +{- | Represents the declaration of a stanza type. + + Range: From the start of the declared type up until the end. +-} +data StanzaTypeItem = StanzaType T.Text Annotation + deriving (Show, Ord, Eq) + +--------------------------------------------- +-- Pretty implementations +--------------------------------------------- + +instance Pretty ASTItem where + pretty (FieldItem item) = pretty item + pretty (StanzaItem item) = pretty item + +instance Pretty Annotation where + pretty (Annotation (Just bracesInfo) range) = + vcat + [ pretty bracesInfo + , indent 2 $ prettyRange range + ] + pretty (Annotation Nothing range) = + indent 2 $ prettyRange range + +instance Pretty Braces where + pretty bracesInfo = + "In braces:" <+> (prettyRange $ rangeFromBraces bracesInfo) + +instance Pretty StanzaItem where + pretty (Stanza decl fields anno) = + vcat + [ pretty decl + , indent 2 $ pretty fields + , indent 4 $ pretty anno + ] + +instance Pretty StanzaElements where + pretty (StanzaElements elems anno) = + (vcat $ map pretty elems) + <+> (indent 2 $ pretty anno) + + +instance Pretty StanzaElement where + pretty (StanzaField fI) = pretty fI + pretty (StanzaConditional sC) = pretty sC + +instance Pretty StanzaDecl where + pretty (StanzaDecl sType sNameM anno) = + pretty sType + <+> maybe mempty pretty sNameM + <+> pretty anno + +instance Pretty FieldItem where + pretty (Field kwItem values _) = + vcat + [ pretty kwItem + , indent 2 $ pretty values + ] + +instance Pretty ConditionalItem where + pretty (Conditional cond siItems r) = + vcat $ + [pretty cond] + <> [pretty siItems] + <> [indent 2 $ pretty r] +instance Pretty ConditionItem where + pretty (IfCondition cI c r) = + vcat $ + [pretty cI <+> pretty c] + <> [indent 2 $ pretty r] + pretty (ElIfCondition cI c r) = + vcat $ + [pretty cI <+> pretty c] + <> [indent 2 $ pretty r] + pretty (ElseCondition cI r) = + vcat $ + [pretty cI] + <> [indent 2 $ pretty r] + +instance Pretty IfConditionItem where + pretty (If txt r) = + viaShow txt + <+> pretty r + +instance Pretty ElIfConditionItem where + pretty (ElIf txt r) = + viaShow txt + <+> (indent 2 $ pretty r) + +instance Pretty ElseConditionItem where + pretty (Else txt r) = + viaShow txt + <+> (indent 2 $ pretty r) + +instance Pretty Condition where + pretty (Cond c r) = + viaShow c + <+> (indent 2 $ pretty r) + +instance Pretty ValueItems where + pretty (Values values anno) = + vcat + [ "[" + , vsep $ fmap (indent 2 . pretty) values + , "]" <+> pretty anno + ] + +instance Pretty KeyWordItem where + pretty (KeyWord kwName anno) = + pretty kwName <+> pretty anno + +instance Pretty ValueItem where + pretty (Value val anno) = + pretty val <+> pretty anno + +instance Pretty CabalAST where + pretty (CabalAST items anno) = + vcat + [ vcat $ map pretty items + , indent 2 $ pretty anno + ] + +instance Pretty StanzaNameItem where + pretty (StanzaName name anno) = + pretty name <+> pretty anno + +instance Pretty StanzaTypeItem where + pretty (StanzaType t anno) = + pretty t <+> pretty anno + +rangeFromBraces :: Braces -> LSP.Range +rangeFromBraces braces' = LSP.Range (openingBrace braces') (closingBrace braces') + +prettyRange :: Range -> Doc ann +prettyRange (LSP.Range start end) = parens (prettyPos start <> colon <> prettyPos end) + +prettyPos :: Position -> Doc ann +prettyPos (Position l ch) = parens (viaShow l <> comma <> viaShow ch) + +myPretty :: (Pretty a) => Either ErrorBundle a -> String +myPretty (Right ast) = show (pretty ast) +myPretty (Left err) = errorBundlePretty err + +----------------------------- +-- Utils +----------------------------- + +{- | Returns whether the field item is a keyword of the given type. + + Independent of capitalization. + + Note that the given text should not have a semicolon at the end. +-} +hasFieldType :: T.Text -> FieldItem -> Bool +hasFieldType type' (Field (KeyWord kwTxt _) _ _) = + T.isPrefixOf (T.toLower type') (T.toLower kwTxt) diff --git a/cabal-parser/src/Text/Cabal/Value.hs b/cabal-parser/src/Text/Cabal/Value.hs new file mode 100644 index 0000000000..e7ff861ca5 --- /dev/null +++ b/cabal-parser/src/Text/Cabal/Value.hs @@ -0,0 +1,161 @@ +{-# LANGUAGE OverloadedStrings #-} + +module Text.Cabal.Value where + +import Data.Functor (void) +import qualified Data.Text as T +import qualified Language.LSP.Protocol.Types as LSP (Position (..), Range (..)) +import Text.Cabal.Types +import Text.Megaparsec +import Text.Megaparsec.Char + +{- Note [Value Parser Implementation Guidelines] + Any value parser needs to fulfill the following functionality: + * Takes a boolean which signifies whether the parsing takes place in a braces context, + if we are in braces context we do not consider indentation. + * Parses a single line which contains at least one value but may contain multiple. + * Must stop when it encounters an unescaped '}', but must not parse it. + * Fails when called on an empty line. + * The parser must not attempt to parse a fixed indentation. + * Is only ever invoked to parse values, keywords are taken care of beforehand +-} + +{- | Default parser which can be used to parse values for a field. + + Here we assume that the rest of the line contains exactly one value + and simply parse anything up until the newline into the value. +-} +defaultValueParser :: Bool -> Parser [ValueItem] +defaultValueParser inBraces = do + hspace + (val, loc) <- annotateSrcLoc $ some $ noneOf endChars + choice [eof, void eol, void $ lookAhead $ string "}"] + pure [Value (T.pack val) (Annotation Nothing loc)] + where + endChars = ['\r', '\n'] ++ if inBraces then ['{', '}'] else [] + +{- | Parser to parse module paths. + + The module paths may start with a comma, + then an arbitrary number of white spaces + then a module path which may be wrapped in apostrophes + and consists of words separated by dots, + after the module path may be a comma before which there + could be an arbitrary number of white spaces. +-} +moduleParser :: Bool -> Parser [ValueItem] +moduleParser inBraces = do + vals <- some $ try $ do + void $ many " " + (val, loc) <- annotateSrcLoc $ do + c1 <- option "" $ string "," + s1 <- many $ char ' ' + val' <- between (char '\"') (char '\"') moduleNameParser <|> moduleNameParser + s2 <- many $ char ' ' + c2 <- option "" $ string "," + let val = c1 <> T.pack s1 <> T.pack val' <> T.pack s2 <> c2 + pure val + pure $ Value val (Annotation Nothing loc) + hspace + choice endParsers + pure vals + where + moduleNameParser = some (alphaNumChar <|> char '.' <|> char '_') + endParsers = [eof, void eol] ++ ([void $ lookAhead $ string "}" | inBraces]) + +{- | Parser to parse file paths. + + The file paths may start with a comma, + then an arbitrary number of white spaces + then a file path which may be wrapped in apostrophes, + after the file path may be a comma before which there + could be an arbitrary number of white spaces. +-} +filepathParser :: Bool -> Parser [ValueItem] +filepathParser inBraces = do + vals <- some $ try $ do + void $ many " " + (val, loc) <- annotateSrcLoc $ do + c1 <- option "" $ string "," + s1 <- many $ char ' ' + val' <- wrappedFpParser <|> filePathParser + s2 <- many $ char ' ' + c2 <- option "" $ string "," + let val = c1 <> T.pack s1 <> T.pack val' <> T.pack s2 <> c2 + pure val + pure $ Value val (Annotation Nothing loc) + hspace + choice endParsers + pure vals + where + wrappedFpParser = between (char '\"') (char '\"') $ many printChar + filePathParser = some $ noneOf $ [' ', ',', '\r', '\n'] ++ if inBraces then ['{', '}'] else [] + endParsers = [eof, void eol] ++ ([void $ lookAhead $ string "}" | inBraces]) + +parseSourcePosToRange :: SourcePos -> SourcePos -> LSP.Range +parseSourcePosToRange start end = + LSP.Range + { LSP._start = parseSourcePosToPosition start + , LSP._end = parseSourcePosToPosition end + } + +parseSourcePosToPosition :: SourcePos -> LSP.Position +parseSourcePosToPosition (SourcePos _ srcLine srcCol) = + LSP.Position + { LSP._line = fromIntegral $ unPos srcLine - 1 + , LSP._character = fromIntegral $ unPos srcCol - 1 + } + +{- | Calls a given parser and returns the value returned by the parser + and the range from the start of the parsed value to the end of the parsed value. +-} +annotateSrcLoc :: Parser a -> Parser (a, LSP.Range) +annotateSrcLoc a = do + start <- getSourcePos + parsed <- a + end <- getSourcePos + pure (parsed, parseSourcePosToRange start end) + +{- | Calls a given parser and returns the value returned by the parser +with the positions of the braces around the parsed value. +-} +annotateBraces :: Parser a -> Parser (a, Braces) +annotateBraces a = do + throwAwayLines + hspace + opening <- getSourcePos + parsed <- between (char '{') (char '}') $ do + throwAwayLines + parsed' <- a + throwAwayLines + hspace + pure parsed' + closing <- getSourcePos + pure (parsed, Braces{openingBrace = parseSourcePosToPosition opening, closingBrace = parseSourcePosToPosition closing}) + +{- | Discards an arbitrary number of consecutive lines we + want to ignore when parsing a cabal file. + + This can be empty lines or a comment. +-} +throwAwayLines :: Parser () +throwAwayLines = do + void $ many $ try $ do + emptyLineParser + void $ optional commentParser + eol + void $ optional $ try $ do + emptyLineParser + void $ optional commentParser + eof + where + -- Discards white spaces + emptyLineParser :: Parser () + emptyLineParser = label "Empty Line parser" $ do + hspace + + -- Discards a comment + commentParser :: Parser () + commentParser = label "comment parser " $ do + void $ string "--" + void $ many (anySingleBut '\n') diff --git a/cabal-parser/test/Main.hs b/cabal-parser/test/Main.hs new file mode 100644 index 0000000000..42ce3b7ea6 --- /dev/null +++ b/cabal-parser/test/Main.hs @@ -0,0 +1,21 @@ +module Main (main) where + +import Parser +import System.Directory (getCurrentDirectory) +import System.FilePath (addTrailingPathSeparator, ()) +import Test.Tasty + + +main :: IO () +main = do + testDir <- getTestDir + defaultMain $ + testGroup + "Cabal Plugin Tests" + [ parserTests testDir + ] + +getTestDir :: IO FilePath +getTestDir = do + cwd <- getCurrentDirectory + pure $ addTrailingPathSeparator $ cwd "test" "testdata" diff --git a/cabal-parser/test/Parser.hs b/cabal-parser/test/Parser.hs new file mode 100644 index 0000000000..9b5af6ad41 --- /dev/null +++ b/cabal-parser/test/Parser.hs @@ -0,0 +1,160 @@ +{-# LANGUAGE LambdaCase #-} +{-# LANGUAGE OverloadedStrings #-} + +module Parser where + +import Data.Maybe (isJust) +import qualified Data.Text as T +import qualified Data.Text.IO as T +import Distribution.Utils.Generic (safeHead) +import System.FilePath (()) +import Test.Tasty +import Test.Tasty.ExpectedFailure +import Test.Tasty.HUnit +import Text.Cabal.Parser +import Text.Cabal.Types +import Text.Megaparsec + +parserTests :: FilePath -> TestTree +parserTests testDir = + testGroup + "CabalAST Parser Tests" + [ parserTests' testDir + ] + +parserTests' :: FilePath -> TestTree +parserTests' testDir = + testGroup + "Parser Tests" + [ testCase "hls cabal file" $ do + ast <- callCabalParser (testDir "parser" "hls-real.cabal") + let nsM = getStanzaWithTypeAndName "common" (Just "importLens") ast + case nsM of + Just (StanzaItem (Stanza _ (StanzaElements elems _) _)) -> + assertBool "importLens contains conditional" $ any + (\case + (StanzaConditional _) -> True + _ -> False + ) + elems + _ -> assertFailure "no import lens common section" + , testCase "cabal cabal file" $ do + ast <- callCabalParser (testDir "parser" "cabal-real.cabal") + let nsM = getStanzaWithTypeAndName "source-repository" (Just "head") ast + assertBool "source repo stanza head exists" $ isJust nsM + , testCase "haskell gi cabal file" $ do + ast <- callCabalParser (testDir "parser" "haskellgi-real.cabal") + let nsM = getStanzaWithTypeAndName "test-suite" (Just "doctests") ast + case nsM of + Just (StanzaItem (Stanza _ (StanzaElements elems _) _)) -> + assertBool "test-suite doctests contains build-depends" $ any + (\case + (StanzaField (Field (KeyWord "build-depends:" _) _ _)) -> True + _ -> False + ) + elems + _ -> assertFailure "no test-suite doctests" + , testCase "gi cairo connector cabal file" $ do + _ast <- callCabalParser (testDir "parser" "gicc-real.cabal") + pure () + , testCase "aeson cabal file" $ do + ast <- callCabalParser (testDir "parser" "aeson-real.cabal") + let nsM = getStanzaWithTypeAndName "library" Nothing ast + case nsM of + Just (StanzaItem (Stanza _ (StanzaElements elems _) _)) -> + assertBool "library contains conditional" $ any + (\case + (StanzaConditional _) -> True + _ -> False + ) + elems + _ -> assertFailure "no library stanza" + let nsM' = getStanzaWithTypeAndName "test-suite" (Just "aeson-tests") ast + case nsM' of + Just (StanzaItem (Stanza _ (StanzaElements elems _) _)) -> + assertBool "test-suite contains conditional at very end" $ any + (\case + (StanzaConditional _) -> True + _ -> False + ) + elems + _ -> assertFailure "no test-suite aeson-tests stanza" + , testCase "nested ifs" $ do + ast <- callCabalParser (testDir "parser" "nested-ifs-real.cabal") + let nsM = getStanzaWithTypeAndName "Library" Nothing ast + case nsM of + Just (StanzaItem (Stanza _ (StanzaElements elems _) _)) -> + assertBool "library contains nested conditional" $ any + (\case + (StanzaConditional (Conditional _ (StanzaElements elems' _) _)) -> + any (\case + (StanzaConditional _) -> True + _ -> False) + elems' + _ -> False + ) + elems + _ -> assertFailure "no library stanza" + , testCase "values in braces" $ do + _ast <- callCabalParser (testDir "parser" "value-braces.cabal") + pure () + , testCase "edit distance" $ do + _ast <- callCabalParser (testDir "parser" "edit-dist.cabal") + pure () + , testCase "stanza with fields in braces" $ do + _ast <- callCabalParser (testDir "parser" "stanza-braces.cabal") + pure () + , testCase "top level braces" $ do + _ast <- callCabalParser (testDir "parser" "top-level-braces.cabal") + pure () + , expectFailBecause "We do not support stanza elements directly after closing braces since it violates indentation rules" $ testCase "conditional braces - sameline" $ do + _ast <- callCabalParser (testDir "parser" "nested-ifs-braces-simple.cabal") + pure () + , testCase "conditional braces" $ do + _ast <- callCabalParser (testDir "parser" "nested-ifs-braces-complex.cabal") + pure () + , testCase "empty hs-source-dirs" $ do + ast <- callCabalParser (testDir "parser" "empty-field.cabal") + let nsM = getStanzaWithTypeAndName "library" Nothing ast + case nsM of + Just (StanzaItem (Stanza _ (StanzaElements elems _) _)) -> + assertBool "library contains exposed-modules as field" $ any + (\case + (StanzaField (Field (KeyWord "exposed-modules:" _) _ _)) -> True + _ -> False + ) + elems + _ -> assertFailure "no library stanza" + ] + where + callCabalParser :: FilePath -> IO CabalAST + callCabalParser fp = do + contents <- T.readFile fp + case parseCabalFile fp contents of + Left err -> do + putStrLn $ errorBundlePretty err + assertFailure "Must parse" + Right ast -> do + pure ast + + getStanzaWithTypeAndName :: T.Text -> Maybe T.Text -> CabalAST -> Maybe ASTItem + getStanzaWithTypeAndName type' (Just name) (CabalAST items _) = + safeHead $ filter + (\case + (StanzaItem (Stanza (StanzaDecl (StanzaType t _) sNameM _) _ _)) -> + case sNameM of + Just (StanzaName n _) -> n == name && t == type' + _ -> False + _ -> False + ) + items + getStanzaWithTypeAndName type' Nothing (CabalAST items _) = + safeHead $ filter + (\case + (StanzaItem (Stanza (StanzaDecl (StanzaType t _) sNameM _) _ _)) -> + case sNameM of + Nothing -> t == type' + _ -> False + _ -> False + ) + items diff --git a/cabal-parser/test/testdata/parser/aeson-real.cabal b/cabal-parser/test/testdata/parser/aeson-real.cabal new file mode 100644 index 0000000000..99c918e9bf --- /dev/null +++ b/cabal-parser/test/testdata/parser/aeson-real.cabal @@ -0,0 +1,235 @@ +name: aeson +version: 2.2.0.0 +license: BSD3 +license-file: LICENSE +category: Text, Web, JSON +copyright: + (c) 2011-2016 Bryan O'Sullivan + (c) 2011 MailRank, Inc. + +author: Bryan O'Sullivan +maintainer: Adam Bergmark +stability: experimental +tested-with: + GHC ==8.2.2 + || ==8.4.4 + || ==8.6.5 + || ==8.8.4 + || ==8.10.7 + || ==9.0.2 + || ==9.2.8 + || ==9.4.5 + || ==9.6.2 + +synopsis: Fast JSON parsing and encoding +cabal-version: 1.12 +homepage: https://github.com/haskell/aeson +bug-reports: https://github.com/haskell/aeson/issues +build-type: Simple +description: + A JSON parsing and encoding library optimized for ease of use + and high performance. + . + To get started, see the documentation for the @Data.Aeson@ module + below. + . + (A note on naming: in Greek mythology, Aeson was the father of Jason.) + +extra-source-files: + *.yaml + benchmarks/json-data/*.json + changelog.md + README.markdown + tests/golden/*.expected + tests/JSONTestSuite/results/*.tok + tests/JSONTestSuite/results/*.txt + tests/JSONTestSuite/test_parsing/*.json + tests/JSONTestSuite/test_transform/*.json + +flag ordered-keymap + description: Use ordered @Data.Map.Strict@ for KeyMap implementation. + default: True + manual: True + +library + default-language: Haskell2010 + hs-source-dirs: src + exposed-modules: + Data.Aeson + Data.Aeson.Decoding + Data.Aeson.Decoding.ByteString + Data.Aeson.Decoding.ByteString.Lazy + Data.Aeson.Decoding.Tokens + Data.Aeson.Encoding + Data.Aeson.Encoding.Internal + Data.Aeson.Key + Data.Aeson.KeyMap + Data.Aeson.QQ.Simple + Data.Aeson.Text + Data.Aeson.TH + Data.Aeson.Types + + other-modules: + Data.Aeson.Decoding.Conversion + Data.Aeson.Decoding.Internal + Data.Aeson.Encoding.Builder + Data.Aeson.Internal.ByteString + Data.Aeson.Internal.Functions + Data.Aeson.Internal.Prelude + Data.Aeson.Internal.Scientific + Data.Aeson.Internal.Text + Data.Aeson.Internal.TH + Data.Aeson.Internal.Unescape + Data.Aeson.Internal.Word8 + Data.Aeson.Parser.Time + Data.Aeson.Types.Class + Data.Aeson.Types.FromJSON + Data.Aeson.Types.Generic + Data.Aeson.Types.Internal + Data.Aeson.Types.ToJSON + + -- GHC bundled libs + build-depends: + base >=4.10.0.0 && <5 + , bytestring >=0.10.8.2 && <0.12 + , containers >=0.5.10.2 && <0.7 + , deepseq >=1.4.3.0 && <1.5 + , exceptions >=0.10.4 && <0.11 + , ghc-prim >=0.5.0.0 && <0.11 + , template-haskell >=2.12.0.0 && <2.21 + , text >=1.2.3.0 && <1.3 || >=2.0 && <2.1 + , time >=1.8.0.2 && <1.13 + + -- Compat + build-depends: + generically >=0.1 && <0.2 + , time-compat >=1.9.6 && <1.10 + + if !impl(ghc >=8.6) + build-depends: contravariant >=1.4.1 && <1.6 + + -- Other dependencies + build-depends: + data-fix >=0.3.2 && <0.4 + , dlist >=1.0 && <1.1 + , hashable >=1.4.2.0 && <1.5 + , indexed-traversable >=0.1.2 && <0.2 + , integer-conversion >=0.1 && <0.2 + , network-uri >=2.6.4.1 && <2.7 + , OneTuple >=0.4.1.1 && <0.5 + , primitive >=0.8.0.0 && <0.9 + , QuickCheck >=2.14.3 && <2.15 + , scientific >=0.3.7.0 && <0.4 + , semialign >=1.3 && <1.4 + , strict >=0.5 && <0.6 + , tagged >=0.8.7 && <0.9 + , text-iso8601 >=0.1 && <0.2 + , text-short >=0.1.5 && <0.2 + , th-abstraction >=0.5.0.0 && <0.6 + , these >=1.2 && <1.3 + , unordered-containers >=0.2.10.0 && <0.3 + , uuid-types >=1.0.5 && <1.1 + , vector >=0.13.0.0 && <0.14 + , witherable >=0.4.2 && <0.5 + + ghc-options: -Wall + + -- String unescaping + + if flag(ordered-keymap) + cpp-options: -DUSE_ORDEREDMAP=1 + +test-suite aeson-tests + default-language: Haskell2010 + type: exitcode-stdio-1.0 + hs-source-dirs: tests + main-is: Tests.hs + ghc-options: -Wall -threaded -rtsopts + other-modules: + DataFamilies.Encoders + DataFamilies.Instances + DataFamilies.Properties + DataFamilies.Types + Encoders + ErrorMessages + Functions + Instances + JSONTestSuite + Options + Properties + PropertyGeneric + PropertyKeys + PropertyQC + PropertyRoundTrip + PropertyRTFunctors + PropertyTH + PropUtils + Regression.Issue351 + Regression.Issue571 + Regression.Issue687 + Regression.Issue967 + SerializationFormatSpec + Types + UnitTests + UnitTests.FromJSONKey + UnitTests.Hashable + UnitTests.KeyMapInsertWith + UnitTests.MonadFix + UnitTests.NullaryConstructors + UnitTests.OmitNothingFieldsNote + UnitTests.OptionalFields + UnitTests.OptionalFields.Common + UnitTests.OptionalFields.Generics + UnitTests.OptionalFields.Manual + UnitTests.OptionalFields.TH + UnitTests.UTCTime + UnitTests.NoThunks + + build-depends: + aeson + , base + , base-compat + , deepseq + , base-orphans >=0.5.3 && <0.10 + , base16-bytestring + , bytestring + , containers + , data-fix + , Diff >=0.4 && <0.5 + , directory + , dlist + , filepath + , generic-deriving >=1.10 && <1.15 + , generically + , ghc-prim >=0.2 + , hashable + , indexed-traversable + , integer-logarithms >=1 && <1.1 + , network-uri + , OneTuple + , primitive + , QuickCheck >=2.14.2 && <2.15 + , quickcheck-instances >=0.3.29 && <0.4 + , scientific + , strict + , tagged + , tasty + , tasty-golden + , tasty-hunit + , tasty-quickcheck + , template-haskell + , text + , text-short + , these + , time + , time-compat + , unordered-containers + , uuid-types + , vector + + if impl(ghc >=9.2 && <9.7) + build-depends: nothunks >=0.1.4 && <0.2 + +source-repository head + type: git + location: git://github.com/haskell/aeson.git diff --git a/cabal-parser/test/testdata/parser/cabal-real.cabal b/cabal-parser/test/testdata/parser/cabal-real.cabal new file mode 100644 index 0000000000..48615f1a81 --- /dev/null +++ b/cabal-parser/test/testdata/parser/cabal-real.cabal @@ -0,0 +1,379 @@ +cabal-version: 2.2 +name: Cabal +version: 3.11.0.0 +copyright: 2003-2023, Cabal Development Team (see AUTHORS file) +license: BSD-3-Clause +license-file: LICENSE +author: Cabal Development Team +maintainer: cabal-devel@haskell.org +homepage: http://www.haskell.org/cabal/ +bug-reports: https://github.com/haskell/cabal/issues +synopsis: A framework for packaging Haskell software +description: + The Haskell Common Architecture for Building Applications and + Libraries: a framework defining a common interface for authors to more + easily build their Haskell applications in a portable way. + . + The Haskell Cabal is part of a larger infrastructure for distributing, + organizing, and cataloging Haskell libraries and tools. +category: Distribution +build-type: Simple +-- If we use a new Cabal feature, this needs to be changed to Custom so +-- we can bootstrap. + +extra-source-files: + README.md ChangeLog.md + +source-repository head + type: git + location: https://github.com/haskell/cabal/ + subdir: Cabal + +library + default-language: Haskell2010 + hs-source-dirs: src + + build-depends: + Cabal-syntax ^>= 3.11, + array >= 0.4.0.1 && < 0.6, + base >= 4.9 && < 5, + bytestring >= 0.10.0.0 && < 0.12, + containers >= 0.5.0.0 && < 0.7, + deepseq >= 1.3.0.1 && < 1.6, + directory >= 1.2 && < 1.4, + filepath >= 1.3.0.1 && < 1.5, + pretty >= 1.1.1 && < 1.2, + process >= 1.2.1.0 && < 1.7, + time >= 1.4.0.1 && < 1.13 + + if os(windows) + build-depends: Win32 >= 2.3.0.0 && < 2.14 + else + build-depends: unix >= 2.6.0.0 && < 2.9 + + ghc-options: -Wall -fno-ignore-asserts -fwarn-tabs -fwarn-incomplete-uni-patterns -fwarn-incomplete-record-updates + + if impl(ghc >= 8.0) + ghc-options: -Wcompat -Wnoncanonical-monad-instances + + if impl(ghc >= 8.0) && impl(ghc < 8.8) + ghc-options: -Wnoncanonical-monadfail-instances + + exposed-modules: + Distribution.Backpack.Configure + Distribution.Backpack.ComponentsGraph + Distribution.Backpack.ConfiguredComponent + Distribution.Backpack.DescribeUnitId + Distribution.Backpack.FullUnitId + Distribution.Backpack.LinkedComponent + Distribution.Backpack.ModSubst + Distribution.Backpack.ModuleShape + Distribution.Backpack.PreModuleShape + Distribution.Utils.IOData + Distribution.Utils.LogProgress + Distribution.Utils.MapAccum + Distribution.Compat.CreatePipe + Distribution.Compat.Directory + Distribution.Compat.Environment + Distribution.Compat.FilePath + Distribution.Compat.Internal.TempFile + Distribution.Compat.ResponseFile + Distribution.Compat.Prelude.Internal + Distribution.Compat.Process + Distribution.Compat.Stack + Distribution.Compat.Time + Distribution.Make + Distribution.PackageDescription.Check + Distribution.ReadE + Distribution.Simple + Distribution.Simple.Bench + Distribution.Simple.Build + Distribution.Simple.Build.Macros + Distribution.Simple.Build.PackageInfoModule + Distribution.Simple.Build.PathsModule + Distribution.Simple.BuildPaths + Distribution.Simple.BuildTarget + Distribution.Simple.BuildToolDepends + Distribution.Simple.CCompiler + Distribution.Simple.Command + Distribution.Simple.Compiler + Distribution.Simple.Configure + Distribution.Simple.Errors + Distribution.Simple.Flag + Distribution.Simple.GHC + Distribution.Simple.GHCJS + Distribution.Simple.Haddock + Distribution.Simple.Glob + Distribution.Simple.HaskellSuite + Distribution.Simple.Hpc + Distribution.Simple.Install + Distribution.Simple.InstallDirs + Distribution.Simple.InstallDirs.Internal + Distribution.Simple.LocalBuildInfo + Distribution.Simple.PackageDescription + Distribution.Simple.PackageIndex + Distribution.Simple.PreProcess + Distribution.Simple.PreProcess.Unlit + Distribution.Simple.Program + Distribution.Simple.Program.Ar + Distribution.Simple.Program.Builtin + Distribution.Simple.Program.Db + Distribution.Simple.Program.Find + Distribution.Simple.Program.GHC + Distribution.Simple.Program.HcPkg + Distribution.Simple.Program.Hpc + Distribution.Simple.Program.Internal + Distribution.Simple.Program.Ld + Distribution.Simple.Program.ResponseFile + Distribution.Simple.Program.Run + Distribution.Simple.Program.Script + Distribution.Simple.Program.Strip + Distribution.Simple.Program.Types + Distribution.Simple.Register + Distribution.Simple.Setup + Distribution.Simple.ShowBuildInfo + Distribution.Simple.SrcDist + Distribution.Simple.Test + Distribution.Simple.Test.ExeV10 + Distribution.Simple.Test.LibV09 + Distribution.Simple.Test.Log + Distribution.Simple.UHC + Distribution.Simple.UserHooks + Distribution.Simple.Utils + Distribution.TestSuite + Distribution.Types.AnnotatedId + Distribution.Types.ComponentInclude + Distribution.Types.DumpBuildInfo + Distribution.Types.PackageName.Magic + Distribution.Types.ComponentLocalBuildInfo + Distribution.Types.LocalBuildInfo + Distribution.Types.TargetInfo + Distribution.Types.GivenComponent + Distribution.Types.ParStrat + Distribution.Utils.Json + Distribution.Utils.NubList + Distribution.Utils.Progress + Distribution.Utils.TempTestDir + Distribution.Verbosity + Distribution.Verbosity.Internal + + -- We reexport all of Cabal-syntax to aid in compatibility for downstream + -- users. In the future we may opt to deprecate some or all of these exports. + -- See haskell/Cabal#7974. + reexported-modules: + Distribution.Backpack, + Distribution.CabalSpecVersion, + Distribution.Compat.Binary, + Distribution.Compat.CharParsing, + Distribution.Compat.DList, + Distribution.Compat.Exception, + Distribution.Compat.Graph, + Distribution.Compat.Lens, + Distribution.Compat.MonadFail, + Distribution.Compat.Newtype, + Distribution.Compat.NonEmptySet, + Distribution.Compat.Parsing, + Distribution.Compat.Prelude, + Distribution.Compat.Semigroup, + Distribution.Compat.Typeable, + Distribution.Compiler, + Distribution.FieldGrammar, + Distribution.FieldGrammar.Class, + Distribution.FieldGrammar.FieldDescrs, + Distribution.FieldGrammar.Newtypes, + Distribution.FieldGrammar.Parsec, + Distribution.FieldGrammar.Pretty, + Distribution.Fields, + Distribution.Fields.ConfVar, + Distribution.Fields.Field, + Distribution.Fields.Lexer, + Distribution.Fields.LexerMonad, + Distribution.Fields.ParseResult, + Distribution.Fields.Parser, + Distribution.Fields.Pretty, + Distribution.InstalledPackageInfo, + Distribution.License, + Distribution.ModuleName, + Distribution.Package, + Distribution.PackageDescription, + Distribution.PackageDescription.Configuration, + Distribution.PackageDescription.FieldGrammar, + Distribution.PackageDescription.Parsec, + Distribution.PackageDescription.PrettyPrint, + Distribution.PackageDescription.Quirks, + Distribution.PackageDescription.Utils, + Distribution.Parsec, + Distribution.Parsec.Error, + Distribution.Parsec.FieldLineStream, + Distribution.Parsec.Position, + Distribution.Parsec.Warning, + Distribution.Pretty, + Distribution.SPDX, + Distribution.SPDX.License, + Distribution.SPDX.LicenseExceptionId, + Distribution.SPDX.LicenseExpression, + Distribution.SPDX.LicenseId, + Distribution.SPDX.LicenseListVersion, + Distribution.SPDX.LicenseReference, + Distribution.System, + Distribution.Text, + Distribution.Types.AbiDependency, + Distribution.Types.AbiHash, + Distribution.Types.Benchmark, + Distribution.Types.Benchmark.Lens, + Distribution.Types.BenchmarkInterface, + Distribution.Types.BenchmarkType, + Distribution.Types.BuildInfo, + Distribution.Types.BuildInfo.Lens, + Distribution.Types.BuildType, + Distribution.Types.Component, + Distribution.Types.ComponentId, + Distribution.Types.ComponentName, + Distribution.Types.ComponentRequestedSpec, + Distribution.Types.CondTree, + Distribution.Types.Condition, + Distribution.Types.ConfVar, + Distribution.Types.Dependency, + Distribution.Types.DependencyMap, + Distribution.Types.ExeDependency, + Distribution.Types.Executable, + Distribution.Types.Executable.Lens, + Distribution.Types.ExecutableScope, + Distribution.Types.ExposedModule, + Distribution.Types.Flag, + Distribution.Types.ForeignLib, + Distribution.Types.ForeignLib.Lens, + Distribution.Types.ForeignLibOption, + Distribution.Types.ForeignLibType, + Distribution.Types.GenericPackageDescription, + Distribution.Types.GenericPackageDescription.Lens, + Distribution.Types.HookedBuildInfo, + Distribution.Types.IncludeRenaming, + Distribution.Types.InstalledPackageInfo, + Distribution.Types.InstalledPackageInfo.Lens, + Distribution.Types.InstalledPackageInfo.FieldGrammar, + Distribution.Types.LegacyExeDependency, + Distribution.Types.Lens, + Distribution.Types.Library, + Distribution.Types.Library.Lens, + Distribution.Types.LibraryName, + Distribution.Types.LibraryVisibility, + Distribution.Types.Mixin, + Distribution.Types.Module, + Distribution.Types.ModuleReexport, + Distribution.Types.ModuleRenaming, + Distribution.Types.MungedPackageId, + Distribution.Types.MungedPackageName, + Distribution.Types.PackageDescription, + Distribution.Types.PackageDescription.Lens, + Distribution.Types.PackageId, + Distribution.Types.PackageId.Lens, + Distribution.Types.PackageName, + Distribution.Types.PackageVersionConstraint, + Distribution.Types.PkgconfigDependency, + Distribution.Types.PkgconfigName, + Distribution.Types.PkgconfigVersion, + Distribution.Types.PkgconfigVersionRange, + Distribution.Types.SetupBuildInfo, + Distribution.Types.SetupBuildInfo.Lens, + Distribution.Types.SourceRepo, + Distribution.Types.SourceRepo.Lens, + Distribution.Types.TestSuite, + Distribution.Types.TestSuite.Lens, + Distribution.Types.TestSuiteInterface, + Distribution.Types.TestType, + Distribution.Types.UnitId, + Distribution.Types.UnqualComponentName, + Distribution.Types.Version, + Distribution.Types.VersionInterval, + Distribution.Types.VersionInterval.Legacy, + Distribution.Types.VersionRange, + Distribution.Types.VersionRange.Internal, + Distribution.Utils.Base62, + Distribution.Utils.Generic, + Distribution.Utils.MD5, + Distribution.Utils.Path, + Distribution.Utils.ShortText, + Distribution.Utils.String, + Distribution.Utils.Structured, + Distribution.Version, + Language.Haskell.Extension + + -- Parsec parser-related modules + build-depends: + -- transformers-0.4.0.0 doesn't have record syntax e.g. for Identity + -- See also https://github.com/ekmett/transformers-compat/issues/35 + transformers (>= 0.3 && < 0.4) || (>=0.4.1.0 && <0.7), + mtl >= 2.1 && < 2.4, + text (>= 1.2.3.0 && < 1.3) || (>= 2.0 && < 2.1), + parsec >= 3.1.13.0 && < 3.2 + + other-modules: + Distribution.Backpack.PreExistingComponent + Distribution.Backpack.ReadyComponent + Distribution.Backpack.MixLink + Distribution.Backpack.ModuleScope + Distribution.Backpack.UnifyM + Distribution.Backpack.Id + Distribution.Utils.UnionFind + Distribution.Compat.Async + Distribution.Compat.CopyFile + Distribution.Compat.GetShortPathName + Distribution.Compat.SnocList + Distribution.GetOpt + Distribution.Lex + Distribution.Simple.Build.Macros.Z + Distribution.Simple.Build.PackageInfoModule.Z + Distribution.Simple.Build.PathsModule.Z + Distribution.Simple.GHC.EnvironmentParser + Distribution.Simple.GHC.Internal + Distribution.Simple.GHC.ImplInfo + Distribution.Simple.ConfigureScript + Distribution.Simple.Setup.Benchmark + Distribution.Simple.Setup.Build + Distribution.Simple.Setup.Clean + Distribution.Simple.Setup.Common + Distribution.Simple.Setup.Config + Distribution.Simple.Setup.Copy + Distribution.Simple.Setup.Global + Distribution.Simple.Setup.Haddock + Distribution.Simple.Setup.Hscolour + Distribution.Simple.Setup.Install + Distribution.Simple.Setup.Register + Distribution.Simple.Setup.Repl + Distribution.Simple.Setup.SDist + Distribution.Simple.Setup.Test + Distribution.ZinzaPrelude + Paths_Cabal + + autogen-modules: + Paths_Cabal + + other-extensions: + BangPatterns + CPP + DefaultSignatures + DeriveDataTypeable + DeriveFoldable + DeriveFunctor + DeriveGeneric + DeriveTraversable + ExistentialQuantification + FlexibleContexts + FlexibleInstances + GeneralizedNewtypeDeriving + ImplicitParams + KindSignatures + LambdaCase + NondecreasingIndentation + OverloadedStrings + PatternSynonyms + RankNTypes + RecordWildCards + ScopedTypeVariables + StandaloneDeriving + Trustworthy + TypeFamilies + TypeOperators + TypeSynonymInstances + UndecidableInstances diff --git a/cabal-parser/test/testdata/parser/edit-dist.cabal b/cabal-parser/test/testdata/parser/edit-dist.cabal new file mode 100644 index 0000000000..c13ab2ac06 --- /dev/null +++ b/cabal-parser/test/testdata/parser/edit-dist.cabal @@ -0,0 +1,73 @@ +Name: edit-distance +Version: 0.2.1 +x-revision: 1 +Cabal-Version: >= 1.2 +Category: Algorithms +Synopsis: Levenshtein and restricted Damerau-Levenshtein edit distances +Description: Optimized edit distances for fuzzy matching, including Levenshtein and restricted Damerau-Levenshtein algorithms. +License: BSD3 +License-File: LICENSE +Extra-Source-Files: README.textile +Author: Max Bolingbroke +Maintainer: batterseapower@hotmail.com +Homepage: http://github.com/batterseapower/edit-distance +Build-Type: Simple + +Flag Tests + Description: Enable building the tests + Default: False + +Flag Benchmark + Description: Enable building the benchmark suite + Default: False + +Flag SplitBase + Description: Choose the new smaller, split-up base package + Default: True + + +Library + Exposed-Modules: Text.EditDistance + Other-Modules: Text.EditDistance.EditCosts + Text.EditDistance.SquareSTUArray + Text.EditDistance.STUArray + Text.EditDistance.Bits + Text.EditDistance.MonadUtilities + + if flag(splitBase) + Build-Depends: base >= 3 && < 4.6, array >= 0.1, random >= 1.0, containers >= 0.1.0.1 + else + Build-Depends: base < 3 + + Ghc-Options: -O2 + +Executable edit-distance-tests + Main-Is: Text/EditDistance/Tests.hs + + Extensions: PatternGuards, PatternSignatures, + ScopedTypeVariables + Ghc-Options: -O2 + + if !flag(tests) + Buildable: False + else + Build-Depends: test-framework >= 0.1.1, QuickCheck >= 1.1 && < 2.0, test-framework-quickcheck + if flag(splitBase) + Build-Depends: base >= 3 && < 4.6, array >= 0.1, random >= 1.0 + else + Build-Depends: base < 3 + +Executable edit-distance-benchmark + Main-Is: Text/EditDistance/Benchmark.hs + + if !flag(benchmark) + Buildable: False + else + if flag(splitBase) + Build-Depends: base >= 3 && < 4.6, array >= 0.1, random >= 1.0, old-time >= 1.0, process >= 1.0, + parallel >= 1.0, unix >= 2.3 + else + Build-Depends: base < 3, + parallel >= 1.0, unix >= 2.3 + + Ghc-Options: -O2 diff --git a/cabal-parser/test/testdata/parser/empty-field.cabal b/cabal-parser/test/testdata/parser/empty-field.cabal new file mode 100644 index 0000000000..1741533b7f --- /dev/null +++ b/cabal-parser/test/testdata/parser/empty-field.cabal @@ -0,0 +1,13 @@ +cabal-version: 3.4 +name: test-hls +version: 0.1.0.0 +maintainer: milky +synopsis: example cabal file :) +license: Apache-2.0 +build-type: Simple + +library + hs-source-dirs: + exposed-modules: + build-depends: base + default-language: Haskell2010 diff --git a/cabal-parser/test/testdata/parser/gicc-real.cabal b/cabal-parser/test/testdata/parser/gicc-real.cabal new file mode 100644 index 0000000000..6e5d9b6d08 --- /dev/null +++ b/cabal-parser/test/testdata/parser/gicc-real.cabal @@ -0,0 +1,31 @@ +Name: gi-cairo-connector +Version: 0.1.1 +License: LGPL-2.1 +License-file: LICENSE +Copyright: (c) 2018 Kilian Kilger, Iñaki García Etxebarria +Author: Iñaki García Etxebarria, + Kilian Kilger +Maintainer: Kilian Kilger (kkilger@gmail.com) +Build-Type: Simple +Cabal-Version: 2.0 +Stability: experimental +homepage: https://github.com/cohomology/gi-cairo-render +bug-reports: https://github.com/cohomology/gi-cairo-render/issues +Synopsis: GI friendly Binding to the Cairo library. +Description: This library contains glue code used to interconnect Haskell GI and Cairo +Category: Graphics +Tested-With: GHC == 8.10.2, GHC == 8.6.2 +extra-source-files: ChangeLog.md + +Source-Repository head + type: git + location: https://github.com/cohomology/gi-cairo-render + subdir: gi-cairo-connector + +Library + build-depends: base >= 4 && < 5, mtl >= 2.2 && <2.4, + haskell-gi-base >=0.24.0 && <0.27, + gi-cairo >= 1.0 && <2, + gi-cairo-render == 0.1.* + exposed-modules: GI.Cairo.Render.Connector + default-language: Haskell2010 diff --git a/cabal-parser/test/testdata/parser/haskellgi-real.cabal b/cabal-parser/test/testdata/parser/haskellgi-real.cabal new file mode 100644 index 0000000000..7f863e868d --- /dev/null +++ b/cabal-parser/test/testdata/parser/haskellgi-real.cabal @@ -0,0 +1,127 @@ +name: haskell-gi +version: 0.26.7 +synopsis: Generate Haskell bindings for GObject Introspection capable libraries +description: Generate Haskell bindings for GObject Introspection capable libraries. This includes most notably + Gtk+, but many other libraries in the GObject ecosystem provide introspection data too. +homepage: https://github.com/haskell-gi/haskell-gi +license: LGPL-2.1 + -- or above +license-file: LICENSE +author: Will Thompson and Iñaki García Etxebarria +maintainer: Iñaki García Etxebarria (github@the.blueleaf.cc) +stability: Experimental +category: Development +build-type: Custom +tested-with: GHC == 8.4.1, GHC == 8.6.1, GHC == 8.8.1, GHC == 8.10.1, GHC == 9.0.1, GHC == 9.2.1 +cabal-version: 2.0 + +extra-source-files: ChangeLog.md + +custom-setup + setup-depends: + base >= 4 && <5, + Cabal >= 1.24 && < 4, + cabal-doctest >= 1 + +source-repository head + type: git + location: git://github.com/haskell-gi/haskell-gi.git + +Library + default-language: Haskell2010 + pkgconfig-depends: gobject-introspection-1.0 >= 1.32, gobject-2.0 >= 2.32 + build-depends: base >= 4.11 && < 5, + haskell-gi-base >= 0.26.4 && <0.27, + Cabal >= 1.24, + attoparsec >= 0.13, + containers, + directory, + filepath, + mtl >= 2.2, + transformers >= 0.3, + pretty-show, + ansi-terminal >= 0.10, + process, + safe, + bytestring, + xdg-basedir, + xml-conduit >= 1.3, + regex-tdfa >= 1.2, + text >= 1.0 + + default-extensions: CPP, ForeignFunctionInterface, DoAndIfThenElse, LambdaCase, RankNTypes, OverloadedStrings + ghc-options: -Wall -fwarn-incomplete-patterns -fno-warn-name-shadowing -Wcompat + + c-sources: lib/c/enumStorage.c + build-tool-depends: hsc2hs:hsc2hs + + hs-source-dirs: lib + exposed-modules: Data.GI.GIR.Alias, + Data.GI.GIR.Allocation, + Data.GI.GIR.Arg, + Data.GI.GIR.BasicTypes, + Data.GI.GIR.Callable, + Data.GI.GIR.Callback, + Data.GI.GIR.Constant, + Data.GI.GIR.Deprecation, + Data.GI.GIR.Documentation, + Data.GI.GIR.Enum, + Data.GI.GIR.Field, + Data.GI.GIR.Flags, + Data.GI.GIR.Function, + Data.GI.GIR.Interface, + Data.GI.GIR.Method, + Data.GI.GIR.Object, + Data.GI.GIR.Parser, + Data.GI.GIR.Property, + Data.GI.GIR.Repository, + Data.GI.GIR.Signal, + Data.GI.GIR.Struct, + Data.GI.GIR.Type, + Data.GI.GIR.Union, + Data.GI.GIR.XMLUtils, + Data.GI.CodeGen.API, + Data.GI.CodeGen.Cabal, + Data.GI.CodeGen.CabalHooks, + Data.GI.CodeGen.Callable, + Data.GI.CodeGen.Code, + Data.GI.CodeGen.CodeGen, + Data.GI.CodeGen.Config, + Data.GI.CodeGen.Constant, + Data.GI.CodeGen.Conversions, + Data.GI.CodeGen.CtoHaskellMap, + Data.GI.CodeGen.EnumFlags, + Data.GI.CodeGen.Fixups, + Data.GI.CodeGen.GObject, + Data.GI.CodeGen.GtkDoc, + Data.GI.CodeGen.GType, + Data.GI.CodeGen.Haddock, + Data.GI.CodeGen.Inheritance, + Data.GI.CodeGen.LibGIRepository, + Data.GI.CodeGen.ModulePath, + Data.GI.CodeGen.OverloadedSignals, + Data.GI.CodeGen.OverloadedMethods, + Data.GI.CodeGen.Overrides, + Data.GI.CodeGen.PkgConfig, + Data.GI.CodeGen.ProjectInfo, + Data.GI.CodeGen.Properties, + Data.GI.CodeGen.Signal, + Data.GI.CodeGen.Struct, + Data.GI.CodeGen.SymbolNaming, + Data.GI.CodeGen.Transfer, + Data.GI.CodeGen.Type, + Data.GI.CodeGen.Util + + other-modules: Paths_haskell_gi + + autogen-modules: Paths_haskell_gi + +test-suite doctests + type: exitcode-stdio-1.0 + default-language: Haskell2010 + ghc-options: -threaded -Wall + main-is: DocTests.hs + build-depends: base + , process + , doctest >= 0.8 + , haskell-gi diff --git a/cabal-parser/test/testdata/parser/hls-real.cabal b/cabal-parser/test/testdata/parser/hls-real.cabal new file mode 100644 index 0000000000..aec1e399d6 --- /dev/null +++ b/cabal-parser/test/testdata/parser/hls-real.cabal @@ -0,0 +1,668 @@ +cabal-version: 3.0 +category: Development +name: haskell-language-server +version: 2.1.0.0 +synopsis: LSP server for GHC +description: + Please see the README on GitHub at + +homepage: https://github.com/haskell/haskell-language-server#readme +bug-reports: https://github.com/haskell/haskell-language-server/issues +author: The Haskell IDE Team +maintainer: alan.zimm@gmail.com +copyright: The Haskell IDE Team +license: Apache-2.0 +license-file: LICENSE +build-type: Simple +tested-with: GHC == 8.10.7 || == 9.0.2 || ==9.2.5 +extra-source-files: + README.md + ChangeLog.md + test/testdata/**/*.project + test/testdata/**/*.cabal + test/testdata/**/*.yaml + test/testdata/**/*.hs + bindist/wrapper.in + +flag pedantic + description: Enable -Werror + default: False + manual: True + +source-repository head + type: git + location: https://github.com/haskell/haskell-language-server + +common common-deps + build-depends: + , base >=4.12 && <5 + , directory + , extra + , filepath + , text + , prettyprinter >= 1.7 + +-- Default warnings in HLS +common warnings + ghc-options: -Wall -Wredundant-constraints -Wno-name-shadowing -Wno-unticked-promoted-constructors + +-- Allow compiling in pedantic mode +common pedantic + if flag(pedantic) + ghc-options: -Werror + +-- Plugin flags are designed for 'cabal install haskell-language-server': +-- - Bulk flags should be default:False +-- - Individual flags should be default:True + +-- The intent of this flag is being able to keep the ghc condition for hackage +-- but skip it via flags in cabal.project as plugins for new ghcs usually +-- are buildable using cabal.project tweaks +flag ignore-plugins-ghc-bounds + description: Force the inclusion of plugins even if they are not buildable by default with a specific ghc version + default: False + manual: True + + +flag cabal + description: Enable cabal plugin + default: True + manual: True + +flag class + description: Enable class plugin + default: True + manual: True + +flag callHierarchy + description: Enable call hierarchy plugin + default: True + manual: True + +flag haddockComments + description: Enable haddockComments plugin + default: True + manual: True + +flag eval + description: Enable eval plugin + default: True + manual: True + +flag importLens + description: Enable importLens plugin + default: True + manual: True + +flag refineImports + description: Enable refineImports plugin + default: True + manual: True + +flag rename + description: Enable rename plugin + default: True + manual: True + +flag retrie + description: Enable retrie plugin + default: True + manual: True + +flag tactic + description: Enable tactic plugin + default: True + manual: True + +flag hlint + description: Enable hlint plugin + default: True + manual: True + +flag stan + description: Enable stan plugin + default: True + manual: True + +flag moduleName + description: Enable moduleName plugin + default: True + manual: True + +flag pragmas + description: Enable pragmas plugin + default: True + manual: True + +flag splice + description: Enable splice plugin + default: True + manual: True + +flag alternateNumberFormat + description: Enable Alternate Number Format plugin + default: True + manual: True + +flag qualifyImportedNames + description: Enable qualifyImportedNames plugin + default: True + manual: True + +flag codeRange + description: Enable Code Range plugin + default: True + manual: True + +flag changeTypeSignature + description: Enable changeTypeSignature plugin + default: True + manual: True + +flag gadt + description: Enable gadt plugin + default: True + manual: True + +flag explicitFixity + description: Enable explicitFixity plugin + default: True + manual: True + +flag explicitFields + description: Enable explicitFields plugin + default: True + manual: True + +flag overloadedRecordDot + description: Enable overloadedRecordDot plugin + default: True + manual: True + +-- formatters + +flag floskell + description: Enable floskell plugin + default: True + manual: True + +flag fourmolu + description: Enable fourmolu plugin + default: True + manual: True + +flag ormolu + description: Enable ormolu plugin + default: True + manual: True + +flag stylishHaskell + description: Enable stylishHaskell plugin + default: True + manual: True + +flag refactor + description: Enable refactor plugin + default: True + manual: True + +flag dynamic + description: Build with the dyn rts + default: True + manual: True + +flag cabalfmt + description: Enable cabal-fmt plugin + default: True + manual: True + +common cabalfmt + if flag(cabalfmt) + build-depends: hls-cabal-fmt-plugin == 2.1.0.0 + cpp-options: -Dhls_cabalfmt + +common cabal + if flag(cabal) + build-depends: hls-cabal-plugin == 2.1.0.0 + cpp-options: -Dhls_cabal + +common class + if flag(class) + build-depends: hls-class-plugin == 2.1.0.0 + cpp-options: -Dhls_class + +common callHierarchy + if flag(callHierarchy) + build-depends: hls-call-hierarchy-plugin == 2.1.0.0 + cpp-options: -Dhls_callHierarchy + +common haddockComments + if flag(haddockComments) && (impl(ghc < 9.2.1) || flag(ignore-plugins-ghc-bounds)) + build-depends: hls-haddock-comments-plugin == 2.1.0.0 + cpp-options: -Dhls_haddockComments + +common eval + if flag(eval) + build-depends: hls-eval-plugin == 2.1.0.0 + cpp-options: -Dhls_eval + +common importLens + if flag(importLens) + build-depends: hls-explicit-imports-plugin == 2.1.0.0 + cpp-options: -Dhls_importLens + +common refineImports + if flag(refineImports) + build-depends: hls-refine-imports-plugin == 2.1.0.0 + cpp-options: -Dhls_refineImports + +common rename + if flag(rename) + build-depends: hls-rename-plugin == 2.1.0.0 + cpp-options: -Dhls_rename + +common retrie + if flag(retrie) + build-depends: hls-retrie-plugin == 2.1.0.0 + cpp-options: -Dhls_retrie + +common tactic + if flag(tactic) && (impl(ghc < 9.2.1) || flag(ignore-plugins-ghc-bounds)) + build-depends: hls-tactics-plugin == 2.1.0.0 + cpp-options: -Dhls_tactic + +common hlint + if flag(hlint) && impl(ghc < 9.5) + build-depends: hls-hlint-plugin == 2.1.0.0 + cpp-options: -Dhls_hlint + +common stan + if flag(stan) && (impl(ghc >= 8.10) && impl(ghc < 9.0)) + build-depends: hls-stan-plugin == 2.1.0.0 + cpp-options: -Dhls_stan + +common moduleName + if flag(moduleName) + build-depends: hls-module-name-plugin == 2.1.0.0 + cpp-options: -Dhls_moduleName + +common pragmas + if flag(pragmas) + build-depends: hls-pragmas-plugin == 2.1.0.0 + cpp-options: -Dhls_pragmas + +common splice + if flag(splice) + build-depends: hls-splice-plugin == 2.1.0.0 + cpp-options: -Dhls_splice + +common alternateNumberFormat + if flag(alternateNumberFormat) + build-depends: hls-alternate-number-format-plugin == 2.1.0.0 + cpp-options: -Dhls_alternateNumberFormat + +common qualifyImportedNames + if flag(qualifyImportedNames) + build-depends: hls-qualify-imported-names-plugin == 2.1.0.0 + cpp-options: -Dhls_qualifyImportedNames + +common codeRange + if flag(codeRange) + build-depends: hls-code-range-plugin == 2.1.0.0 + cpp-options: -Dhls_codeRange + +common changeTypeSignature + if flag(changeTypeSignature) + build-depends: hls-change-type-signature-plugin == 2.1.0.0 + cpp-options: -Dhls_changeTypeSignature + +common gadt + if flag(gadt) + build-depends: hls-gadt-plugin == 2.1.0.0 + cpp-options: -Dhls_gadt + +common explicitFixity + if flag(explicitFixity) + build-depends: hls-explicit-fixity-plugin == 2.1.0.0 + cpp-options: -DexplicitFixity + +common explicitFields + if flag(explicitFields) + build-depends: hls-explicit-record-fields-plugin == 2.1.0.0 + cpp-options: -DexplicitFields + +common overloadedRecordDot + if flag(overloadedRecordDot) && (impl(ghc >= 9.2.0) || flag(ignore-plugins-ghc-bounds)) + build-depends: hls-overloaded-record-dot-plugin == 2.1.0.0 + cpp-options: -Dhls_overloaded_record_dot + +-- formatters + +common floskell + if flag(floskell) && impl(ghc < 9.5) + build-depends: hls-floskell-plugin == 2.1.0.0 + cpp-options: -Dhls_floskell + +common fourmolu + if flag(fourmolu) + build-depends: hls-fourmolu-plugin == 2.1.0.0 + cpp-options: -Dhls_fourmolu + +common ormolu + if flag(ormolu) && impl(ghc < 9.5) + build-depends: hls-ormolu-plugin == 2.1.0.0 + cpp-options: -Dhls_ormolu + +common stylishHaskell + if flag(stylishHaskell) && impl(ghc < 9.5) + build-depends: hls-stylish-haskell-plugin == 2.1.0.0 + cpp-options: -Dhls_stylishHaskell + +common refactor + if flag(refactor) + build-depends: hls-refactor-plugin == 2.1.0.0 + cpp-options: -Dhls_refactor + +library + import: common-deps + -- configuration + , warnings + , pedantic + -- plugins + , cabal + , callHierarchy + , cabalfmt + , changeTypeSignature + , class + , haddockComments + , eval + , importLens + , refineImports + , rename + , retrie + , tactic + , hlint + , stan + , moduleName + , pragmas + , splice + , alternateNumberFormat + , qualifyImportedNames + , codeRange + , gadt + , explicitFixity + , explicitFields + , floskell + , fourmolu + , ormolu + , stylishHaskell + , refactor + , overloadedRecordDot + + exposed-modules: + Ide.Arguments + Ide.Main + Ide.Version + HlsPlugins + + other-modules: Paths_haskell_language_server + autogen-modules: Paths_haskell_language_server + hs-source-dirs: src + build-depends: + , async + , base16-bytestring + , bytestring + , containers + , cryptohash-sha1 + , data-default + , ghc + , ghcide == 2.1.0.0 + , githash >=0.1.6.1 + , lsp >= 2.0.0.0 + , hie-bios + , hiedb + , hls-plugin-api == 2.1.0.0 + , optparse-applicative + , optparse-simple + , process + , hls-graph + , safe-exceptions + , sqlite-simple + , unordered-containers + , aeson-pretty + + default-language: Haskell2010 + default-extensions: DataKinds, TypeOperators + +executable haskell-language-server + import: common-deps + -- configuration + , warnings + , pedantic + main-is: Main.hs + hs-source-dirs: exe + + ghc-options: + -threaded + -- allow user RTS overrides + -rtsopts + -- disable idle GC + -- increase nursery size + -- Enable collection of heap statistics + "-with-rtsopts=-I0 -A128M -T" + -Wno-unticked-promoted-constructors + if flag(pedantic) + ghc-options: -Werror + if !os(windows) && flag(dynamic) + -- We want to link against the dyn rts just like official GHC binaries do; + -- the linked rts determines how external libs are loaded dynamically by TH. + -- The standard way of doing this is via the --enable-dynamic-executables Cabal option + -- Unfortunately it doesnt' work, see https://github.com/haskell/haskell-language-server/issues/2659 + -- One can use --ghc-options=-dynamic but this gets applied to the dependencies as well, + -- which results in massive rebuilds and incompatibilities with profiling. + -- So instead we set the -dynamic flag diretly here. + ghc-options: -dynamic + + build-depends: + , aeson + , async + , base16-bytestring + , binary + , bytestring + , containers + , cryptohash-sha1 + , deepseq + , ghc + , ghc-boot-th + , ghcide + , hashable + , haskell-language-server + , lsp + , hie-bios + , hiedb + , lens + , regex-tdfa + , optparse-applicative + , hls-plugin-api + , lens + , mtl + , regex-tdfa + , safe-exceptions + , hls-graph + , sqlite-simple + , stm + , temporary + , transformers + , unordered-containers + + default-language: Haskell2010 + default-extensions: DataKinds, TypeOperators + +executable haskell-language-server-wrapper + import: common-deps + , warnings + , pedantic + main-is: Wrapper.hs + hs-source-dirs: exe + other-modules: Paths_haskell_language_server + autogen-modules: Paths_haskell_language_server + ghc-options: + -threaded + -- allow user RTS overrides + -rtsopts + -- disable idle GC + -- increase nursery size + "-with-rtsopts=-I0 -A128M" + + build-depends: + , data-default + , ghc + , ghc-paths + , ghcide + , gitrev + , haskell-language-server + , hie-bios + , hls-plugin-api + , lsp + , lsp-types + , mtl + , optparse-applicative + , optparse-simple + , process + , transformers + , unliftio-core + if !os(windows) + build-depends: + unix + , containers + + default-language: Haskell2010 + + +test-suite func-test + import: common-deps + , warnings + , pedantic + , refactor + type: exitcode-stdio-1.0 + default-language: Haskell2010 + build-tool-depends: + haskell-language-server:haskell-language-server -any, + ghcide:ghcide-test-preprocessor -any + + build-depends: + , bytestring + , data-default + , deepseq + , hashable + , hspec-expectations + , lens + , lens-aeson + , ghcide + , ghcide-test-utils + , hls-test-utils == 2.1.0.0 + , lsp-types + , aeson + , hls-plugin-api + , lsp-test + , containers + , unordered-containers + , row-types + + hs-source-dirs: test/functional test/utils + + main-is: Main.hs + other-modules: + Command + Completion + Config + Deferred + Definition + Diagnostic + Format + FunctionalBadProject + FunctionalCodeAction + HieBios + Highlight + Progress + Reference + Symbol + TypeDefinition + Test.Hls.Command + Test.Hls.Flags + + default-extensions: OverloadedStrings + ghc-options: + -threaded -rtsopts -with-rtsopts=-N + +-- Duplicating inclusion plugin conditions until tests are moved to their own packages + if flag(eval) + cpp-options: -Dhls_eval +-- formatters + if flag(floskell) && (impl(ghc < 9.2.1) || flag(ignore-plugins-ghc-bounds)) + cpp-options: -Dhls_floskell + if flag(fourmolu) + cpp-options: -Dhls_fourmolu + if flag(ormolu) + cpp-options: -Dhls_ormolu + +test-suite wrapper-test + import: common-deps + , warnings + , pedantic + type: exitcode-stdio-1.0 + build-tool-depends: + haskell-language-server:haskell-language-server-wrapper -any, + haskell-language-server:haskell-language-server -any + + default-language: Haskell2010 + build-depends: + process + , hls-test-utils + + hs-source-dirs: test/wrapper + main-is: Main.hs + +benchmark benchmark + type: exitcode-stdio-1.0 + default-language: Haskell2010 + ghc-options: -Wall -Wno-name-shadowing -threaded + main-is: Main.hs + hs-source-dirs: bench + build-tool-depends: + ghcide-bench:ghcide-bench, + hp2pretty:hp2pretty, + implicit-hie:gen-hie + default-extensions: + BangPatterns + DeriveFunctor + DeriveGeneric + FlexibleContexts + GeneralizedNewtypeDeriving + LambdaCase + NamedFieldPuns + RecordWildCards + ScopedTypeVariables + StandaloneDeriving + TupleSections + TypeApplications + ViewPatterns + + build-depends: + aeson, + base == 4.*, + containers, + data-default, + directory, + extra, + filepath, + ghcide-bench, + haskell-language-server, + hls-plugin-api, + lens, + lens-aeson, + optparse-applicative, + shake, + shake-bench == 0.2.*, + text, + yaml diff --git a/cabal-parser/test/testdata/parser/nested-ifs-braces-complex.cabal b/cabal-parser/test/testdata/parser/nested-ifs-braces-complex.cabal new file mode 100644 index 0000000000..2dd13e10ec --- /dev/null +++ b/cabal-parser/test/testdata/parser/nested-ifs-braces-complex.cabal @@ -0,0 +1,57 @@ +name: unix-compat +version: 0.7 +synopsis: Portable POSIX-compatibility layer. +description: This package provides portable implementations of parts + of the {unix package. This package} re-exports the unix + package when available. When it isn't available, + portable implementations are used. + +Library + default-language: Haskell2010 + hs-source-dirs: src + ghc-options: -Wall + build-depends: base == 4.* + + exposed-modules: + System.PosixCompat + System.PosixCompat.Extensions + System.PosixCompat.Files + System.PosixCompat.Temp + System.PosixCompat.Time + System.PosixCompat.Types + System.PosixCompat.Unistd + + if os(windows) { + c-sources: + cbits/HsUname.c + cbits/mktemp.c + + extra-libraries: msvcrt + build-depends: Win32 >= 2.5.0.0 + build-depends: filepath >= 1.0 && < 1.5 + if flag(old-time) { + build-depends: old-time >= 1.0.0.0 && < 1.2.0.0 + cpp-options: -DOLD_TIME + + if impl(ghc < 7) + build-depends: directory == 1.0.* + cpp-options: -DDIRECTORY_1_0 + else + build-depends: directory == 1.1.* + } + else + build-depends: time >= 1.0 && < 1.13 + build-depends: directory >= 1.3.1 && < 1.4 + + other-modules: + System.PosixCompat.Internal.Time + + } + else + build-depends: unix >= 2.6 && < 2.9 + include-dirs: include + includes: HsUnixCompat.h + install-includes: HsUnixCompat.h + c-sources: cbits/HsUnixCompat.c + if os(solaris) + cc-options: -DSOLARIS diff --git a/cabal-parser/test/testdata/parser/nested-ifs-braces-simple.cabal b/cabal-parser/test/testdata/parser/nested-ifs-braces-simple.cabal new file mode 100644 index 0000000000..e9cb632ff4 --- /dev/null +++ b/cabal-parser/test/testdata/parser/nested-ifs-braces-simple.cabal @@ -0,0 +1,5 @@ +cabal-version: 3.4 +library + if os(windows) { + build-depends: directory + } build-depends: directory diff --git a/cabal-parser/test/testdata/parser/nested-ifs-real.cabal b/cabal-parser/test/testdata/parser/nested-ifs-real.cabal new file mode 100644 index 0000000000..58b18c4de1 --- /dev/null +++ b/cabal-parser/test/testdata/parser/nested-ifs-real.cabal @@ -0,0 +1,147 @@ +name: unix-compat +version: 0.7 +synopsis: Portable POSIX-compatibility layer. +description: This package provides portable implementations of parts + of the unix package. This package re-exports the unix + package when available. When it isn't available, + portable implementations are used. + +homepage: http://github.com/haskell-pkg-janitors/unix-compat +license: BSD3 +license-file: LICENSE +author: Björn Bringert, Duncan Coutts, Jacob Stanley, Bryan O'Sullivan +maintainer: Mitchell Rosen +category: System +build-type: Simple +cabal-version: >= 1.10 + +extra-source-files: + CHANGELOG.md + +source-repository head + type: git + location: git@github.com:haskell-pkg-janitors/unix-compat.git + +flag old-time + description: build against old-time package + default: False + +Library + default-language: Haskell2010 + hs-source-dirs: src + ghc-options: -Wall + build-depends: base == 4.* + + exposed-modules: + System.PosixCompat + System.PosixCompat.Extensions + System.PosixCompat.Files + System.PosixCompat.Temp + System.PosixCompat.Time + System.PosixCompat.Types + System.PosixCompat.Unistd + + if os(windows) + c-sources: + cbits/HsUname.c + cbits/mktemp.c + + extra-libraries: msvcrt + build-depends: Win32 >= 2.5.0.0 + build-depends: filepath >= 1.0 && < 1.5 + + if flag(old-time) + build-depends: old-time >= 1.0.0.0 && < 1.2.0.0 + cpp-options: -DOLD_TIME + + if impl(ghc < 7) + build-depends: directory == 1.0.* + cpp-options: -DDIRECTORY_1_0 + else + build-depends: directory == 1.1.* + else + build-depends: time >= 1.0 && < 1.13 + build-depends: directory >= 1.3.1 && < 1.4 + + other-modules: + System.PosixCompat.Internal.Time + + else + build-depends: unix >= 2.6 && < 2.9 + include-dirs: include + includes: HsUnixCompat.h + install-includes: HsUnixCompat.h + c-sources: cbits/HsUnixCompat.c + if os(solaris) + cc-options: -DSOLARIS + +Test-Suite unix-compat-testsuite + default-language: Haskell2010 + type: exitcode-stdio-1.0 + hs-source-dirs: tests + ghc-options: -Wall + main-is: main.hs + + other-modules: + MkstempSpec + LinksSpec + + -- ghc-options: + -- -Wall + -- -fwarn-tabs + -- -funbox-strict-fields + -- -threaded + -- -fno-warn-unused-do-bind + -- -fno-warn-type-defaults + + -- extensions: + -- OverloadedStrings + -- ExtendedDefaultRules + + -- if flag(lifted) + -- cpp-options: -DLIFTED + + build-depends: + unix-compat + , base == 4.* + , monad-parallel + , hspec + , HUnit + , directory + , extra + , temporary + + if os(windows) + -- c-sources: + -- cbits/HsUname.c + -- cbits/mktemp.c + + -- extra-libraries: msvcrt + -- build-depends: Win32 >= 2.5.0.0 + + if flag(old-time) + build-depends: old-time >= 1.0.0.0 && < 1.2.0.0 + cpp-options: -DOLD_TIME + + if impl(ghc < 7) + build-depends: directory == 1.0.* + cpp-options: -DDIRECTORY_1_0 + else + build-depends: directory == 1.1.* + else + build-depends: time >= 1.0 && < 1.13 + build-depends: directory >= 1.3.1 && < 1.4 + + -- other-modules: + -- System.PosixCompat.Internal.Time + + else + -- build-depends: unix >= 2.4 && < 2.9 + -- include-dirs: include + -- includes: HsUnixCompat.h + -- install-includes: HsUnixCompat.h + -- c-sources: cbits/HsUnixCompat.c + if os(solaris) + cc-options: -DSOLARIS + + build-depends: directory >= 1.3.1 && < 1.4 diff --git a/cabal-parser/test/testdata/parser/stanza-braces.cabal b/cabal-parser/test/testdata/parser/stanza-braces.cabal new file mode 100644 index 0000000000..556b42baf6 --- /dev/null +++ b/cabal-parser/test/testdata/parser/stanza-braces.cabal @@ -0,0 +1,8 @@ +library { + hs-source-dirs: + exe, filepath, dir + build-depends: + dep1 + dep2 +} + diff --git a/cabal-parser/test/testdata/parser/top-level-braces.cabal b/cabal-parser/test/testdata/parser/top-level-braces.cabal new file mode 100644 index 0000000000..37820d7486 --- /dev/null +++ b/cabal-parser/test/testdata/parser/top-level-braces.cabal @@ -0,0 +1,18 @@ +cabal-version : 1.0 +description: + { + Hello my description is dark and + twisted. + Who dares call my name?!?!?!? + + } + +library + hs-source-dirs: + { exe, filepath, dir } + exposed-modules: + Dep1 + Dep2 + build-depends: + dep1 + dep2 diff --git a/cabal-parser/test/testdata/parser/value-braces.cabal b/cabal-parser/test/testdata/parser/value-braces.cabal new file mode 100644 index 0000000000..5a70495dcb --- /dev/null +++ b/cabal-parser/test/testdata/parser/value-braces.cabal @@ -0,0 +1,16 @@ +library + hs-source-dirs: + { exe, filepath +, dir } + build-depends: + --commmmmment + { dep1 + -- comment + dep2 + } + import: + + + + + ghc-options: diff --git a/cabal.project b/cabal.project index 971fa012b8..e3fe80c47e 100644 --- a/cabal.project +++ b/cabal.project @@ -36,6 +36,7 @@ packages: ./plugins/hls-explicit-record-fields-plugin ./plugins/hls-refactor-plugin ./plugins/hls-overloaded-record-dot-plugin + ./cabal-parser -- Standard location for temporary packages needed for particular environments -- For example it is used in the project gitlab mirror to help in the MAcOS M1 build script diff --git a/ghcide/ghcide.cabal b/ghcide/ghcide.cabal index a0044d14d0..09874466c4 100644 --- a/ghcide/ghcide.cabal +++ b/ghcide/ghcide.cabal @@ -193,6 +193,7 @@ library Development.IDE.LSP.Outline Development.IDE.LSP.Server Development.IDE.Session + Development.IDE.Session.Diagnostics Development.IDE.Spans.Common Development.IDE.Spans.Documentation Development.IDE.Spans.AtPoint diff --git a/ghcide/session-loader/Development/IDE/Session.hs b/ghcide/session-loader/Development/IDE/Session.hs index 6dfb9a7b01..f2c43de220 100644 --- a/ghcide/session-loader/Development/IDE/Session.hs +++ b/ghcide/session-loader/Development/IDE/Session.hs @@ -34,14 +34,12 @@ import Data.Aeson hiding (Error) import Data.Bifunctor import qualified Data.ByteString.Base16 as B16 import qualified Data.ByteString.Char8 as B -import Data.Char (isLower) import Data.Default import Data.Either.Extra import Data.Function import Data.Hashable hiding (hash) import qualified Data.HashMap.Strict as HM import Data.List -import Data.List.Extra (dropPrefix, split) import qualified Data.Map.Strict as Map import Data.Maybe import Data.Proxy @@ -69,7 +67,6 @@ import Development.IDE.Types.Location import Development.IDE.Types.Options import GHC.Check import qualified HIE.Bios as HieBios -import qualified HIE.Bios.Cradle as HieBios import HIE.Bios.Environment hiding (getCacheDir) import HIE.Bios.Types hiding (Log) import qualified HIE.Bios.Types as HieBios @@ -103,6 +100,7 @@ import Data.HashSet (HashSet) import qualified Data.HashSet as Set import Database.SQLite.Simple import Development.IDE.Core.Tracing (withTrace) +import Development.IDE.Session.Diagnostics (renderCradleError) import Development.IDE.Types.Shake (WithHieDb) import HieDb.Create import HieDb.Types @@ -685,7 +683,7 @@ loadSessionWithOptions recorder SessionLoadingOptions{..} dir = do Left err -> do dep_info <- getDependencyInfo (maybeToList hieYaml) let ncfp = toNormalizedFilePath' cfp - let res = (map (renderCradleError cradle ncfp) err, Nothing) + let res = (map (\err' -> renderCradleError err' cradle ncfp) err, Nothing) void $ modifyVar' fileToFlags $ Map.insertWith HM.union hieYaml (HM.singleton ncfp (res, dep_info)) void $ modifyVar' filesMap $ HM.insert ncfp hieYaml @@ -925,72 +923,6 @@ setCacheDirs recorder CacheDirs{..} dflags = do & maybe id setHieDir hieCacheDir & maybe id setODir oCacheDir - -renderCradleError :: Cradle a -> NormalizedFilePath -> CradleError -> FileDiagnostic -renderCradleError cradle nfp (CradleError _ _ec ms) = - ideErrorWithSource (Just "cradle") (Just DiagnosticSeverity_Error) nfp $ T.unlines $ map T.pack userFriendlyMessage - where - - userFriendlyMessage :: [String] - userFriendlyMessage - | HieBios.isCabalCradle cradle = fromMaybe ms fileMissingMessage - | otherwise = ms - - fileMissingMessage :: Maybe [String] - fileMissingMessage = - multiCradleErrMessage <$> parseMultiCradleErr ms - --- | Information included in Multi Cradle error messages -data MultiCradleErr = MultiCradleErr - { mcPwd :: FilePath - , mcFilePath :: FilePath - , mcPrefixes :: [(FilePath, String)] - } deriving (Show) - --- | Attempt to parse a multi-cradle message -parseMultiCradleErr :: [String] -> Maybe MultiCradleErr -parseMultiCradleErr ms = do - _ <- lineAfter "Multi Cradle: " - wd <- lineAfter "pwd: " - fp <- lineAfter "filepath: " - ps <- prefixes - pure $ MultiCradleErr wd fp ps - - where - lineAfter :: String -> Maybe String - lineAfter pre = listToMaybe $ mapMaybe (stripPrefix pre) ms - - prefixes :: Maybe [(FilePath, String)] - prefixes = do - pure $ mapMaybe tuple ms - - tuple :: String -> Maybe (String, String) - tuple line = do - line' <- surround '(' line ')' - [f, s] <- pure $ split (==',') line' - pure (f, s) - - -- extracts the string surrounded by required characters - surround :: Char -> String -> Char -> Maybe String - surround start s end = do - guard (listToMaybe s == Just start) - guard (listToMaybe (reverse s) == Just end) - pure $ drop 1 $ take (length s - 1) s - -multiCradleErrMessage :: MultiCradleErr -> [String] -multiCradleErrMessage e = - [ "Loading the module '" <> moduleFileName <> "' failed. It may not be listed in your .cabal file!" - , "Perhaps you need to add `"<> moduleName <> "` to other-modules or exposed-modules." - , "For more information, visit: https://cabal.readthedocs.io/en/3.4/developing-packages.html#modules-included-in-the-package" - , "" - ] <> map prefix (mcPrefixes e) - where - localFilePath f = dropWhile (==pathSeparator) $ dropPrefix (mcPwd e) f - moduleFileName = localFilePath $ mcFilePath e - moduleName = intercalate "." $ map dropExtension $ dropWhile isSourceFolder $ splitDirectories moduleFileName - isSourceFolder p = all isLower $ take 1 p - prefix (f, r) = f <> " - " <> r - -- See Note [Multi Cradle Dependency Info] type DependencyInfo = Map.Map FilePath (Maybe UTCTime) type HieMap = Map.Map (Maybe FilePath) (HscEnv, [RawComponentInfo]) diff --git a/ghcide/session-loader/Development/IDE/Session/Diagnostics.hs b/ghcide/session-loader/Development/IDE/Session/Diagnostics.hs new file mode 100644 index 0000000000..60697af455 --- /dev/null +++ b/ghcide/session-loader/Development/IDE/Session/Diagnostics.hs @@ -0,0 +1,86 @@ +module Development.IDE.Session.Diagnostics where +import Control.Monad +import qualified Data.Aeson as Aeson +import Data.Char (isLower) +import Data.List +import Data.List.Extra (dropPrefix, split) +import Data.Maybe +import qualified Data.Text as T +import qualified Data.Vector as Vector +import Development.IDE.Types.Diagnostics +import Development.IDE.Types.Location +import qualified HIE.Bios.Cradle as HieBios +import HIE.Bios.Types hiding (Log) +import System.FilePath + +{- | Takes a cradle error, the corresponding cradle and the file path where + the cradle error occurred (of the file we attempted to load). + Depicts the cradle error in a user-friendly way. +-} +renderCradleError :: CradleError -> Cradle a -> NormalizedFilePath -> FileDiagnostic +renderCradleError (CradleError deps _ec ms) cradle nfp + | HieBios.isCabalCradle cradle && any (isInfixOf "Error: cabal: Failed extracting script block:") ms = + let (fp, showDiag, diag) = ideErrorWithSource (Just "cradle") (Just DiagnosticSeverity_Error) nfp $ T.unlines $ map T.pack userFriendlyMessage in + (fp, showDiag, diag{_data_ = Just (Aeson.Array $ Vector.fromList $ map (Aeson.String . T.pack) absDeps)}) + | otherwise = ideErrorWithSource (Just "cradle") (Just DiagnosticSeverity_Error) nfp $ T.unlines $ map T.pack userFriendlyMessage + where + absDeps = fmap (cradleRootDir cradle ) deps + userFriendlyMessage :: [String] + userFriendlyMessage + | HieBios.isCabalCradle cradle = fromMaybe ms fileMissingMessage + | otherwise = ms + + fileMissingMessage :: Maybe [String] + fileMissingMessage = + multiCradleErrMessage <$> parseMultiCradleErr ms + +-- | Information included in Multi Cradle error messages +data MultiCradleErr = MultiCradleErr + { mcPwd :: FilePath + , mcFilePath :: FilePath + , mcPrefixes :: [(FilePath, String)] + } deriving (Show) + +-- | Attempt to parse a multi-cradle message +parseMultiCradleErr :: [String] -> Maybe MultiCradleErr +parseMultiCradleErr ms = do + _ <- lineAfter "Multi Cradle: " + wd <- lineAfter "pwd: " + fp <- lineAfter "filepath: " + ps <- prefixes + pure $ MultiCradleErr wd fp ps + + where + lineAfter :: String -> Maybe String + lineAfter pre = listToMaybe $ mapMaybe (stripPrefix pre) ms + + prefixes :: Maybe [(FilePath, String)] + prefixes = do + pure $ mapMaybe tuple ms + + tuple :: String -> Maybe (String, String) + tuple line = do + line' <- surround '(' line ')' + [f, s] <- pure $ split (==',') line' + pure (f, s) + + -- extracts the string surrounded by required characters + surround :: Char -> String -> Char -> Maybe String + surround start s end = do + guard (listToMaybe s == Just start) + guard (listToMaybe (reverse s) == Just end) + pure $ drop 1 $ take (length s - 1) s + +multiCradleErrMessage :: MultiCradleErr -> [String] +multiCradleErrMessage e = + [ "Loading the module '" <> moduleFileName <> "' failed. It may not be listed in your .cabal file!" + , "Perhaps you need to add `"<> moduleName <> "` to other-modules or exposed-modules." + , "For more information, visit: https://cabal.readthedocs.io/en/3.4/developing-packages.html#modules-included-in-the-package" + , "" + ] <> map prefix (mcPrefixes e) + where + localFilePath f = dropWhile (==pathSeparator) $ dropPrefix (mcPwd e) f + moduleFileName = localFilePath $ mcFilePath e + moduleName = intercalate "." $ map dropExtension $ dropWhile isSourceFolder $ splitDirectories moduleFileName + isSourceFolder p = all isLower $ take 1 p + prefix (f, r) = f <> " - " <> r diff --git a/plugins/hls-cabal-plugin/hls-cabal-plugin.cabal b/plugins/hls-cabal-plugin/hls-cabal-plugin.cabal index c21f2f2639..0f6b7df22e 100644 --- a/plugins/hls-cabal-plugin/hls-cabal-plugin.cabal +++ b/plugins/hls-cabal-plugin/hls-cabal-plugin.cabal @@ -26,7 +26,8 @@ library import: warnings exposed-modules: Ide.Plugin.Cabal - Ide.Plugin.Cabal.Diagnostics + Ide.Plugin.Cabal.Cabal + Ide.Plugin.Cabal.CodeActions Ide.Plugin.Cabal.Completion.Completer.FilePath Ide.Plugin.Cabal.Completion.Completer.Module Ide.Plugin.Cabal.Completion.Completer.Paths @@ -36,14 +37,16 @@ library Ide.Plugin.Cabal.Completion.Completions Ide.Plugin.Cabal.Completion.Data Ide.Plugin.Cabal.Completion.Types + Ide.Plugin.Cabal.Diagnostics Ide.Plugin.Cabal.LicenseSuggest Ide.Plugin.Cabal.Parse - build-depends: + , aeson , base >=4.12 && <5 , bytestring , Cabal-syntax >= 3.7 + , cabal-parser , containers , deepseq , directory @@ -54,15 +57,18 @@ library , hls-plugin-api == 2.1.0.0 , hls-graph == 2.1.0.0 , lens - , lsp ^>=2.1.0.0 - , lsp-types ^>=2.0.1.0 + , lsp + , lsp-types + , megaparsec + , mtl , regex-tdfa ^>=1.3.1 + , safe , stm , text , text-rope , transformers , unordered-containers >=0.2.10.0 - , containers + hs-source-dirs: src default-language: Haskell2010 @@ -73,13 +79,17 @@ test-suite tests hs-source-dirs: test main-is: Main.hs other-modules: + CodeActions Completer Context Utils + build-depends: , base , bytestring , Cabal-syntax >= 3.7 + , cabal-parser + , containers , directory , filepath , ghcide @@ -88,6 +98,7 @@ test-suite tests , lens , lsp , lsp-types + , megaparsec , tasty-hunit , text , text-rope diff --git a/plugins/hls-cabal-plugin/src/Ide/Plugin/Cabal.hs b/plugins/hls-cabal-plugin/src/Ide/Plugin/Cabal.hs index 61c6f5df52..26624b58f2 100644 --- a/plugins/hls-cabal-plugin/src/Ide/Plugin/Cabal.hs +++ b/plugins/hls-cabal-plugin/src/Ide/Plugin/Cabal.hs @@ -9,40 +9,52 @@ {-# LANGUAGE TupleSections #-} {-# LANGUAGE TypeFamilies #-} -module Ide.Plugin.Cabal (descriptor, Log (..)) where +module Ide.Plugin.Cabal (descriptor, hsDescriptor, Log (..)) where import Control.Concurrent.STM import Control.Concurrent.Strict import Control.DeepSeq import Control.Lens ((^.)) -import Control.Monad.Extra -import Control.Monad.IO.Class -import Control.Monad.Trans.Class (lift) +import Control.Monad.Except (MonadError (throwError), + MonadIO (liftIO), + MonadTrans (lift), + join, void, when) +import Control.Monad.Extra (whenJust) import Control.Monad.Trans.Maybe (runMaybeT) +import qualified Data.Aeson as Aeson import qualified Data.ByteString as BS +import Data.Foldable import Data.Hashable import Data.HashMap.Strict (HashMap) import qualified Data.HashMap.Strict as HashMap import qualified Data.List.NonEmpty as NE +import qualified Data.Text as T import qualified Data.Text.Encoding as Encoding import Data.Typeable import Development.IDE as D +import Development.IDE.Core.PluginUtils (runActionE) +import Development.IDE.Core.Rules (getSourceFileSource) import Development.IDE.Core.Shake (restartShakeSession) import qualified Development.IDE.Core.Shake as Shake import Development.IDE.Graph (alwaysRerun) import GHC.Generics +import qualified Ide.Plugin.Cabal.CodeActions as CA import qualified Ide.Plugin.Cabal.Completion.Completer.Types as CompleterTypes import qualified Ide.Plugin.Cabal.Completion.Completions as Completions import qualified Ide.Plugin.Cabal.Completion.Types as Types import qualified Ide.Plugin.Cabal.Diagnostics as Diagnostics import qualified Ide.Plugin.Cabal.LicenseSuggest as LicenseSuggest import qualified Ide.Plugin.Cabal.Parse as Parse +import Ide.Plugin.Error (PluginError (..)) import Ide.Types import qualified Language.LSP.Protocol.Lens as JL import qualified Language.LSP.Protocol.Message as LSP import Language.LSP.Protocol.Types import Language.LSP.Server (getVirtualFile) import qualified Language.LSP.VFS as VFS +import Text.Cabal.Parser (parseCabalFile) +import Text.Cabal.Types (ErrorBundle) +import Text.Megaparsec (errorBundlePretty) data Log = LogModificationTime NormalizedFilePath FileVersion @@ -54,6 +66,8 @@ data Log | LogFOI (HashMap NormalizedFilePath FileOfInterestStatus) | LogCompletionContext Types.Context Position | LogCompletions Types.Log + | LogCabalParserError ErrorBundle + | LogCodeAction CA.Log deriving (Show) instance Pretty Log where @@ -77,6 +91,20 @@ instance Pretty Log where <+> "for cursor position:" <+> viaShow position LogCompletions logs -> pretty logs + LogCodeAction logs -> pretty logs + LogCabalParserError errorBundle -> "Parsing cabal file failed with error:" <+> pretty (errorBundlePretty errorBundle) + + +hsDescriptor :: Recorder (WithPriority Log) -> PluginId -> PluginDescriptor IdeState +hsDescriptor recorder plId = + (defaultPluginDescriptor plId) + { pluginHandlers = + mconcat + [ mkPluginHandler LSP.SMethod_TextDocumentCodeAction $ addModuleCodeAction recorder + ] + , pluginRules = pure () + , pluginNotificationHandlers = mempty + } descriptor :: Recorder (WithPriority Log) -> PluginId -> PluginDescriptor IdeState descriptor recorder plId = @@ -189,8 +217,49 @@ kick = do -- Code Actions -- ---------------------------------------------------------------- +addModuleCodeAction :: Recorder (WithPriority Log) -> PluginMethodHandler IdeState 'LSP.Method_TextDocumentCodeAction +addModuleCodeAction recorder ideState _ (CodeActionParams _ _ (TextDocumentIdentifier uri) _range CodeActionContext{_diagnostics = diags}) = do + diag <- case find (\diag -> T.isInfixOf "Error: cabal: Failed extracting script block:" (diag ^. JL.message)) diags of + Nothing -> throwError $ PluginRequestRefused "Not an unknown module diagnostic" + Just diag -> pure diag + + cabalFiles <- case diag ^. JL.data_ of + Just (Aeson.Array val) -> pure val + _ -> throwError $ PluginRequestRefused "Missing cradle dependencies" + + cabalFP <- case findCabalFile $ toList cabalFiles of + Nothing -> throwError $ PluginRequestRefused "No .cabal file found in cradle dependencies" + Just cabalFP -> + pure cabalFP + + fileContent <- runActionE "cabal-plugin.readFileContent" ideState $ lift $ do + sources <- getSourceFileSource $ toNormalizedFilePath cabalFP + pure $ Encoding.decodeUtf8 sources + + let cabalAST = parseCabalFile cabalFP fileContent -- todo read the cabal file + case cabalAST of + Right ast -> do + codeActions <- CA.collectModuleInsertionOptions (cmapWithPrio LogCodeAction recorder) cabalFP uri ast + pure $ InL (fmap InR codeActions) + Left errorBundle -> do + logWith recorder Debug $ LogCabalParserError errorBundle + pure $ InL [] + where + findCabalFile :: [Aeson.Value] -> Maybe FilePath + findCabalFile vals = + asum $ map + (\case + Aeson.String val -> do + if T.isSuffixOf ".cabal" val + then Just $ T.unpack val + else Nothing + _ -> Nothing + ) + vals + + licenseSuggestCodeAction :: PluginMethodHandler IdeState 'LSP.Method_TextDocumentCodeAction -licenseSuggestCodeAction _ _ (CodeActionParams _ _ (TextDocumentIdentifier uri) _range CodeActionContext{_diagnostics=diags}) = +licenseSuggestCodeAction _ _ (CodeActionParams _ _ (TextDocumentIdentifier uri) _range CodeActionContext{_diagnostics = diags}) = pure $ InL $ diags >>= (fmap InR . LicenseSuggest.licenseErrorAction uri) -- ---------------------------------------------------------------- @@ -290,16 +359,17 @@ completion recorder ide _ complParams = do Just ctx -> do logWith recorder Debug $ LogCompletionContext ctx pos let completer = Completions.contextToCompleter ctx - let completerData = CompleterTypes.CompleterData - { getLatestGPD = do - mGPD <- runIdeAction "cabal-plugin.modulesCompleter.gpd" (shakeExtras ide) $ useWithStaleFast Types.ParseCabal $ toNormalizedFilePath fp - pure $ fmap fst mGPD - , cabalPrefixInfo = prefInfo - , stanzaName = - case fst ctx of - Types.Stanza _ name -> name - _ -> Nothing - } + let completerData = + CompleterTypes.CompleterData + { getLatestGPD = do + mGPD <- runIdeAction "cabal-plugin.modulesCompleter.gpd" (shakeExtras ide) $ useWithStaleFast Types.ParseCabal $ toNormalizedFilePath fp + pure $ fmap fst mGPD + , cabalPrefixInfo = prefInfo + , stanzaName = + case fst ctx of + Types.Stanza _ name -> name + _ -> Nothing + } completions <- completer completerRecorder completerData pure completions where diff --git a/plugins/hls-cabal-plugin/src/Ide/Plugin/Cabal/Cabal.hs b/plugins/hls-cabal-plugin/src/Ide/Plugin/Cabal/Cabal.hs new file mode 100644 index 0000000000..59a65c81d9 --- /dev/null +++ b/plugins/hls-cabal-plugin/src/Ide/Plugin/Cabal/Cabal.hs @@ -0,0 +1,19 @@ +module Ide.Plugin.Cabal.Cabal where + +import qualified Data.Map as Map +import Data.Maybe (isJust) +import qualified Data.Text as T +import Ide.Plugin.Cabal.Completion.Completer.Types +import Ide.Plugin.Cabal.Completion.Data +import qualified Language.LSP.Protocol.Types as P (Position (..)) + +import Ide.Plugin.Cabal.Completion.Types + +stanzaMapFrom :: T.Text -> Maybe (Map.Map KeyWordName Completer) +stanzaMapFrom s = Map.lookup s stanzaKeywordMap + +isStanzaType :: T.Text -> Bool +isStanzaType s = isJust $ Map.lookup s stanzaKeywordMap + +mkPosition :: Int -> Int -> P.Position +mkPosition lineNumber charNumber = P.Position{P._line = fromIntegral lineNumber, P._character = fromIntegral charNumber} diff --git a/plugins/hls-cabal-plugin/src/Ide/Plugin/Cabal/CodeActions.hs b/plugins/hls-cabal-plugin/src/Ide/Plugin/Cabal/CodeActions.hs new file mode 100644 index 0000000000..6af41c0995 --- /dev/null +++ b/plugins/hls-cabal-plugin/src/Ide/Plugin/Cabal/CodeActions.hs @@ -0,0 +1,354 @@ +{-# LANGUAGE DataKinds #-} +{-# LANGUAGE LambdaCase #-} +{-# LANGUAGE OverloadedStrings #-} + +module Ide.Plugin.Cabal.CodeActions where + +import Control.Lens ((^.)) +import Data.Foldable (Foldable (foldl')) +import Data.Foldable.Extra (asum) +import qualified Data.Map as Map +import Data.Maybe (fromMaybe, + mapMaybe) +import qualified Data.Text as T +import Ide.Logger +import Ide.Plugin.Cabal.Completion.Completer.Module (fpToExposedModulePath) +import qualified Language.LSP.Protocol.Lens as L + +import qualified Language.LSP.Protocol.Types as CA (CodeAction (..), + CodeActionKind (..)) +import qualified Language.LSP.Protocol.Types as J +import qualified Language.LSP.Protocol.Types as P (Position (..), + Range (..)) +import qualified Language.LSP.Protocol.Types as TE (TextEdit (..)) +import qualified Language.LSP.Protocol.Types as WE (WorkspaceEdit (..)) +import Safe (headMay) + +import Control.Monad.IO.Class +import Control.Monad.Trans.Except +import Data.Char (isUpper) +import Data.List.Extra (notNull) +import Development.IDE +import Development.IDE.Core.PluginUtils +import Ide.Plugin.Error +import qualified System.FilePath as FP +import Text.Cabal.Types as Parser + +------------------------------------------ +-- Types +------------------------------------------ + +data Log = LogString + deriving (Ord, Eq, Show) + +instance Pretty Log where + pretty = \case + LogString -> "" + +{- | Contains information on how the new module path + should be inserted into the cabal file. +-} +data ModulePathInsertionInfo = ModulePathInsertionInfo + { insertionPosition :: Maybe P.Position + -- ^ The position to insert the path at. + , insertionMode :: InsertionMode + -- ^ Whether we are inserting directly in front or after another value. + } + deriving (Ord, Eq, Show) + +{- | Whether the module path is going to be inserted + in front or after another value. +-} +data InsertionMode = Before | After + deriving (Ord, Eq, Show) + +{- | Whether the module path is going to be inserted into + its own line or in the same line as the other module paths + in the field. + + If the path is to be inserted into its own line, + the indentation of the path to be inserted is also stored. +-} +data LineInformation = OwnLine Indentation | SameLine + deriving (Ord, Eq, Show) + +type Indentation = Int + +-- | Relevant data needed to add a module to a cabal file. +data ModuleInsertionConfig = ModuleInsertionConfig + { targetFile :: FilePath + -- ^ The file we want to insert information about the new module into. + , moduleToInsert :: T.Text + -- ^ The text to insert into the targetFile at the insertionPosition. + , insertionPos :: P.Position + -- ^ The position where the module path will be inserted into the targetFile. + , insertionStanza :: T.Text + -- ^ A label which describes which stanza the module will be inserted into. + , insertionField :: T.Text + -- ^ A label which describes which field the module will be inserted into. + } + deriving (Show, Eq, Ord) + +-------------------------------------------- +-- Implementation +-------------------------------------------- + +{- | 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 path into the cabal file + with the given contents. +-} +collectModuleInsertionOptions :: + (MonadIO m) => + Recorder (WithPriority Log) -> + FilePath -> + Uri -> + CabalAST -> + ExceptT PluginError m [J.CodeAction] +collectModuleInsertionOptions _ srcPath modulePathUri ast = do + modulePath <- uriToFilePathE modulePathUri + let configs = concatMap (mkModuleInsertionConfig srcPath modulePath) (stanzas ast) + pure $ map makeCodeActionForModulePath configs + +{- | 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 position the module path could be inserted in the + given stanza. +-} +mkModuleInsertionConfig :: FilePath -> FilePath -> StanzaItem -> [ModuleInsertionConfig] +mkModuleInsertionConfig srcPath modulePath (Parser.Stanza (StanzaDecl (StanzaType sType _) sNameM _) (StanzaElements fields _) _) = do + case getModulePathForStanza fields srcPath modulePath of + Just processedModPath -> + mapMaybe + ( \sField -> + valuesToInsertionConfig + ( case sField of + (StanzaField field) -> + if isModuleField field then Just field else Nothing + _ -> Nothing + -- TODO: some conditionals may apply so we may want to + -- choose these fields as well in the future + ) + srcPath + processedModPath + label + ) + fields + _ -> [] + where + label = + sType <> case sNameM of + Just (StanzaName sName _) -> " " <> sName + Nothing -> + "" + +{- | Takes a list of stanza elements, a source path and the module path to add + and returns a module path to insert which is relative to some source directory in the + given stanza. + If the module is contained in one of the stanza's fields' source directories, + then a module path to insert is returned. + + For each source directory occurring in the given stanza we try create the module path + using the source directory and if it matches we return the first matching one, in + module path syntax and relative to the source directory. +-} +getModulePathForStanza :: [StanzaElement] -> FilePath -> FilePath -> Maybe T.Text +getModulePathForStanza stanzaElements sourceDir modPath = + case mkModulePathM stanzaSourceDirs sourceDir modPath of + Just fp -> + if all (maybe False isUpper . headMay) $ FP.splitDirectories fp + then Just $ fpToExposedModulePath sourceDir fp + else Nothing + Nothing -> Nothing + where + -- a list of all possible source directories of the stanza, + -- either all values in the hs-source-dir field + -- or simple the directory of the cabal file in case there are no + -- hs-source-dir-fields + stanzaSourceDirs = + if notNull $ filterSourceDirs stanzaElements + then map (\(Value val _) -> T.unpack val) $ concat $ filterSourceDirs stanzaElements + else [FP.takeDirectory sourceDir] + + -- gathers all source directory value value items in the given stanza elements + filterSourceDirs :: [StanzaElement] -> [[ValueItem]] + filterSourceDirs sElems = + mapMaybe + ( \case + (StanzaField field@(Field _ (Values vals _) _)) -> if hasFieldType "hs-source-dirs" field then Just vals else Nothing + _ -> Nothing + ) + sElems + +{- | Takes a list of source subdirectories, a source path and a module path + and returns a file path to the module relative to one of the subdirectories in + case the module is contained within one of them. +-} +mkModulePathM :: [FilePath] -> FilePath -> FilePath -> Maybe FilePath +mkModulePathM filepaths srcPath' modPath = + asum $ + map + ( \srcDir -> do + let relMP = FP.makeRelative (FP.normalise (srcPath FP. srcDir)) modPath + if relMP == modPath then Nothing else Just relMP + ) + filepaths + where + srcPath = FP.takeDirectory srcPath' + +{- | Takes a possible field item, a cabal file path where the module is to be inserted, + a module path to insert and a label to use to label the text edit and constructs the + ModuleInsertionConfig. +-} +valuesToInsertionConfig :: + Maybe FieldItem -> + FilePath -> + T.Text -> + T.Text -> + Maybe ModuleInsertionConfig +valuesToInsertionConfig Nothing _ _ _ = Nothing +valuesToInsertionConfig + (Just (Field (Parser.KeyWord kw (Annotation _ kwRange)) (Values vals (Annotation _ range)) _)) + fp + modPath + label' = + Just $ + ModuleInsertionConfig + { targetFile = fp + , moduleToInsert = paddedModulePath + , insertionPos = + case insertionPosition mps of + Just pos -> pos + Nothing -> range ^. L.start + , insertionStanza = label' + , insertionField = kw + } + where + paddedModulePath = + case mkLineInformation vals kwRange of + SameLine -> + case insertionMode mps of + Before -> modPath <> " " + After -> " " <> modPath + OwnLine indents -> + case insertionMode mps of + Before -> modPath <> "\n" <> genIndentation indents + After -> "\n" <> genIndentation indents <> modPath + + mps = modulePathInsertionPosition vals modPath + + genIndentation :: Int -> T.Text + genIndentation = (`T.replicate` " ") . fromIntegral + +-- | Returns a list of all stanza items in the given AST. +stanzas :: CabalAST -> [StanzaItem] +stanzas (CabalAST ast _) = + mapMaybe + ( \case + (StanzaItem x) -> Just x + _ -> Nothing + ) + ast + +{- | Takes a list of value items and the range of the values' keyword item + and returns the line information for that list. + + The line information is set to OwnLine iff all values in the list are in + different lines and the indentation is set to the indentation of the first + value in the list. +-} +mkLineInformation :: [ValueItem] -> P.Range -> LineInformation +mkLineInformation vs kwRange + | snd allDiffLines = + case headMay vs of + Just (Value _ (Annotation _ r)) -> OwnLine (fromIntegral $ r ^. (L.start . L.character)) + Nothing -> OwnLine $ 2 + fromIntegral (kwRange ^. (L.start . L.character)) + | otherwise = SameLine + where + allDiffLines = + foldl' + ( \(sPos, allDiff) (Value _ (Annotation _ r1)) -> + let curLine = r1 ^. (L.start . L.line) + in (curLine, allDiff && (curLine /= sPos)) + ) + (-1, True) + vs + +{- | Takes a list of value items and a module path and finds the correct position in the list + to add the path in ascending alphabetical order and whether to add the value directly before + the next value in the list or directly after the previous value in the list. + + In most cases the value will be added after the previous value in the list, unless + it is the first value in the list, then it will be added directly before the + second value in the list. + + Assumes the existing list of values is already sorted in ascending alphabetical order, + if not, the value is just added before the first found item which is alphabetically larger. +-} +modulePathInsertionPosition :: [ValueItem] -> T.Text -> ModulePathInsertionInfo +modulePathInsertionPosition [] _ = ModulePathInsertionInfo{insertionPosition = Nothing, insertionMode = After} +modulePathInsertionPosition [v1@(Value val1 _)] modPath + | getModuleName val1 <= modPath = mkModulePathInsertionInfo v1 After + | otherwise = mkModulePathInsertionInfo v1 Before +modulePathInsertionPosition (v1@(Value val1 _) : v2@(Value val2 _) : xs) modPath + | modPath <= getModuleName val1 = mkModulePathInsertionInfo v1 Before + | getModuleName val1 <= modPath && modPath < getModuleName val2 = mkModulePathInsertionInfo v1 After + | otherwise = modulePathInsertionPosition (v2 : xs) modPath + +{- | Builds a Module Path Insertion Info from the received value item and insertion mode. + + Depending on the insertion mode, will set the insertion position right before the received + value or right after the received value. +-} +mkModulePathInsertionInfo :: ValueItem -> InsertionMode -> ModulePathInsertionInfo +mkModulePathInsertionInfo (Value _ (Annotation _ r)) iMode = + ModulePathInsertionInfo + { insertionPosition = + case iMode of + Before -> Just $ r ^. L.start + After -> Just $ r ^. L.end + , insertionMode = iMode + } + +{- | Removes anything that is not the module name from a given value + which can contain a comma at the start or end and spaces. +-} +getModuleName :: T.Text -> T.Text +getModuleName mPath = T.strip $ (T.dropWhile (== ',') . T.dropWhileEnd (== ',')) mPath + +-- | Returns whether the given field item represents a field where a module path can be inserted. +isModuleField :: FieldItem -> Bool +isModuleField f = + any (\modKw -> hasFieldType modKw f) ["exposed-modules", "other-modules"] + +makeCodeActionForModulePath :: ModuleInsertionConfig -> J.CodeAction +makeCodeActionForModulePath insertionConfig = + J.CodeAction + { CA._title = "Add to " <> label <> " as " <> fieldDescription <> " in " <> (T.pack $ FP.takeFileName cabalFilePath) + , CA._kind = Just CA.CodeActionKind_Refactor + , CA._diagnostics = Nothing + , CA._isPreferred = Nothing + , CA._disabled = Nothing + , CA._edit = Just workSpaceEdit + , CA._command = Nothing + , CA._data_ = Nothing + } + where + fieldName = insertionField insertionConfig + fieldDescription = fromMaybe fieldName $ T.stripSuffix "s:" fieldName + cabalFilePath = targetFile insertionConfig + pos = insertionPos insertionConfig + label = insertionStanza insertionConfig + moduleToAdd = moduleToInsert insertionConfig + workSpaceEdit = + WE.WorkspaceEdit + { WE._changes = + Just $ + Map.singleton + (J.filePathToUri cabalFilePath) + [TE.TextEdit{TE._range = P.Range pos pos, TE._newText = moduleToAdd}] + , WE._documentChanges = Nothing + , WE._changeAnnotations = Nothing + } diff --git a/plugins/hls-cabal-plugin/src/Ide/Plugin/Cabal/Completion/Completer/FilePath.hs b/plugins/hls-cabal-plugin/src/Ide/Plugin/Cabal/Completion/Completer/FilePath.hs index c7aa59f125..4a6896ccda 100644 --- a/plugins/hls-cabal-plugin/src/Ide/Plugin/Cabal/Completion/Completer/FilePath.hs +++ b/plugins/hls-cabal-plugin/src/Ide/Plugin/Cabal/Completion/Completer/FilePath.hs @@ -20,8 +20,9 @@ import qualified System.FilePath as FP import qualified System.FilePath.Posix as Posix import qualified Text.Fuzzy.Parallel as Fuzzy --- | Completer to be used when a file path can be completed for a field. --- Completes file paths as well as directories. +{- | Completer to be used when a file path can be completed for a field. + Completes file paths as well as directories. +-} filePathCompleter :: Completer filePathCompleter recorder cData = do let prefInfo = cabalPrefixInfo cData @@ -72,9 +73,9 @@ mainIsCompleter extractionFunction recorder cData = do sName = stanzaName cData prefInfo = cabalPrefixInfo cData - --- | Completer to be used when a directory can be completed for the field. --- Only completes directories. +{- | Completer to be used when a directory can be completed for the field. + Only completes directories. +-} directoryCompleter :: Completer directoryCompleter recorder cData = do let prefInfo = cabalPrefixInfo cData @@ -107,13 +108,14 @@ directoryCompleter recorder cData = do be used for file path completions to be written to the cabal file. -} --- | Takes a PathCompletionInfo and returns the list of files and directories --- in the directory which match the path completion info in posix style. --- --- The directories end with a posix trailing path separator. --- Since this is used for completions to be written to the cabal file, --- we use posix separators here. --- See Note [Using correct file path separators]. +{- | Takes a PathCompletionInfo and returns the list of files and directories + in the directory which match the path completion info in posix style. + + The directories end with a posix trailing path separator. + Since this is used for completions to be written to the cabal file, + we use posix separators here. + See Note [Using correct file path separators]. +-} listFileCompletions :: Recorder (WithPriority Log) -> PathCompletionInfo -> IO [FilePath] listFileCompletions recorder complInfo = do let complDir = mkCompletionDirectory complInfo @@ -126,60 +128,65 @@ listFileCompletions recorder complInfo = do logWith recorder Warning $ LogFilePathCompleterIOError complDir err pure [] --- | Returns a list of all (and only) directories in the --- directory described by path completion info. +{- | Returns a list of all (and only) directories in the + directory described by path completion info. +-} listDirectoryCompletions :: Recorder (WithPriority Log) -> PathCompletionInfo -> IO [FilePath] listDirectoryCompletions recorder complInfo = do filepaths <- listFileCompletions recorder complInfo filterM (doesDirectoryExist . mkDirFromCWD complInfo) filepaths --- | Returns the directory where files and directories can be queried from --- for the passed PathCompletionInfo. --- --- Returns the full path to the directory pointed to by the path prefix --- by combining it with the working directory. --- --- Since this is used for querying paths we use platform --- compatible separators here. --- See Note [Using correct file path separators]. +{- | Returns the directory where files and directories can be queried from + for the passed PathCompletionInfo. + + Returns the full path to the directory pointed to by the path prefix + by combining it with the working directory. + + Since this is used for querying paths we use platform + compatible separators here. + See Note [Using correct file path separators]. +-} mkCompletionDirectory :: PathCompletionInfo -> FilePath mkCompletionDirectory complInfo = FP.addTrailingPathSeparator $ workingDirectory complInfo FP. (FP.normalise $ queryDirectory complInfo) --- | Returns the full path for the given path segment --- by combining the working directory with the path prefix --- and the path segment. --- --- Since this is used for querying paths we use platform --- compatible separators here. --- See Note [Using correct file path separators]. +{- | Returns the full path for the given path segment + by combining the working directory with the path prefix + and the path segment. + + Since this is used for querying paths we use platform + compatible separators here. + See Note [Using correct file path separators]. +-} mkDirFromCWD :: PathCompletionInfo -> FilePath -> FilePath mkDirFromCWD complInfo fp = FP.addTrailingPathSeparator $ mkCompletionDirectory complInfo FP. FP.normalise fp --- | Takes a PathCompletionInfo and a directory and --- returns the complete cabal path to be written on completion action --- by combining the previously written path prefix and the completed --- path segment. --- --- Since this is used for completions we use posix separators here. --- See Note [Using correct file path separators]. +{- | Takes a PathCompletionInfo and a directory and + returns the complete cabal path to be written on completion action + by combining the previously written path prefix and the completed + path segment. + + Since this is used for completions we use posix separators here. + See Note [Using correct file path separators]. +-} mkPathCompletionDir :: PathCompletionInfo -> T.Text -> T.Text mkPathCompletionDir complInfo completion = T.pack $ queryDirectory complInfo Posix. T.unpack completion --- | Takes a PathCompletionInfo and a completed path segment and --- generates the whole filepath to be completed. --- --- The returned text combines the completion with a relative path --- generated from a possible previously written path prefix and --- is relative to the cabal file location. --- --- If the completion results in a filepath, we know this is a --- completed path and can thus apply wrapping of apostrophes if needed. +{- | Takes a PathCompletionInfo and a completed path segment and + generates the whole filepath to be completed. + + The returned text combines the completion with a relative path + generated from a possible previously written path prefix and + is relative to the cabal file location. + + If the completion results in a filepath, we know this is a + completed path and can thus apply wrapping of apostrophes if needed. +-} mkFilePathCompletion :: PathCompletionInfo -> T.Text -> IO T.Text mkFilePathCompletion complInfo completion = do let combinedPath = mkPathCompletionDir complInfo completion diff --git a/plugins/hls-cabal-plugin/src/Ide/Plugin/Cabal/Completion/Completer/Module.hs b/plugins/hls-cabal-plugin/src/Ide/Plugin/Cabal/Completion/Completer/Module.hs index 21dfbb9e1f..a33f856e65 100644 --- a/plugins/hls-cabal-plugin/src/Ide/Plugin/Cabal/Completion/Completer/Module.hs +++ b/plugins/hls-cabal-plugin/src/Ide/Plugin/Cabal/Completion/Completer/Module.hs @@ -5,6 +5,7 @@ module Ide.Plugin.Cabal.Completion.Completer.Module where import Control.Monad (filterM) import Control.Monad.Extra (concatForM, forM) +import Data.Char (isUpper) import Data.List (stripPrefix) import Data.Maybe (fromMaybe) import qualified Data.Text as T @@ -19,14 +20,16 @@ import Ide.Plugin.Cabal.Completion.Completer.Paths import Ide.Plugin.Cabal.Completion.Completer.Simple import Ide.Plugin.Cabal.Completion.Completer.Types import Ide.Plugin.Cabal.Completion.Types +import Safe (headMay) import System.Directory (doesFileExist) import qualified System.FilePath as FP import qualified Text.Fuzzy.Parallel as Fuzzy --- | Completer to be used when module paths can be completed for the field. --- --- Takes an extraction function which extracts the source directories --- to be used by the completer. +{- | Completer to be used when module paths can be completed for the field. + +Takes an extraction function which extracts the source directories +to be used by the completer. +-} modulesCompleter :: (Maybe StanzaName -> GenericPackageDescription -> [FilePath]) -> Completer modulesCompleter extractionFunction recorder cData = do mGPD <- getLatestGPD cData @@ -39,9 +42,9 @@ modulesCompleter extractionFunction recorder cData = do Nothing -> do logWith recorder Debug LogUseWithStaleFastNoResult pure [] - where - sName = stanzaName cData - prefInfo = cabalPrefixInfo cData + where + sName = stanzaName cData + prefInfo = cabalPrefixInfo cData -- | Takes a list of source directories and returns a list of path completions -- relative to any of the passed source directories which fit the passed prefix info. @@ -53,7 +56,7 @@ filePathsForExposedModules recorder srcDirs prefInfo = do let dir = FP.normalise dir' pathInfo = pathCompletionInfoFromCabalPrefixInfo dir modPrefInfo completions <- listFileCompletions recorder pathInfo - validExposedCompletions <- filterM (isValidExposedModulePath pathInfo) completions + validExposedCompletions <- filterM (isValidExposedModulePath (mkCompletionDirectory pathInfo)) completions let toMatch = pathSegment pathInfo scored = Fuzzy.simpleFilter Fuzzy.defChunkSize @@ -77,28 +80,37 @@ filePathsForExposedModules recorder srcDirs prefInfo = do -- to filepath syntax, since it is in exposed module syntax modPrefInfo = prefInfo{completionPrefix=prefix} - -- Takes a PathCompletionInfo and a path segment and checks whether - -- the path segment can be completed for an exposed module. - -- - -- This is the case if the segment represents either a directory or a Haskell file. - isValidExposedModulePath :: PathCompletionInfo -> FilePath -> IO Bool - isValidExposedModulePath pInfo path = do - let dir = mkCompletionDirectory pInfo - fileExists <- doesFileExist (dir FP. path) - pure $ not fileExists || FP.takeExtension path `elem` [".hs", ".lhs"] +{- | Takes a PathCompletionInfo and a path segment and checks whether + the path segment can be completed for an exposed module. + + This is the case if the segment represents either a directory or a Haskell + file and all directories in the path and the possible filename must start + with uppercase letters. +-} +isValidExposedModulePath :: FilePath -> FilePath -> IO Bool +isValidExposedModulePath dir path = do + let fpM = dir FP. path + allCapitalised = all isCapitalised (FP.splitDirectories path) + fileExists <- doesFileExist fpM + pure $ allCapitalised && (not fileExists || FP.takeExtension path `elem` [".hs", ".lhs"]) + where + -- Returns whether the given string starts with an uppercase letter + isCapitalised :: String -> Bool + isCapitalised x = maybe False isUpper (headMay x) + +{- | Takes a PathCompletionInfo and a path segment and generates the whole + filepath to be written on completion including a possibly already written prefix; + using the cabal syntax for exposed modules. + + Examples: + When the partial directory path @Dir.Dir2.@ is stored in the PathCompletionInfo + and the completed file @HaskellFile.hs@ is passed along with that PathCompletionInfo, + the result would be @Dir1.Dir2.HaskellFile@ --- | Takes a pathCompletionInfo and a path segment and generates the whole --- filepath to be written on completion including a possibly already written prefix; --- using the cabal syntax for exposed modules. --- --- Examples: --- When the partial directory path `Dir.Dir2.` is stored in the PathCompletionInfo --- and the completed file `HaskellFile.hs` is passed along with that PathCompletionInfo, --- the result would be `Dir1.Dir2.HaskellFile` --- --- When the partial directory path `Dir.` is stored in the PathCompletionInfo --- and the completed directory `Dir2` is passed along with that PathCompletionInfo, --- the result would be `Dir1.Dir2.` + When the partial directory path @Dir.@ is stored in the PathCompletionInfo + and the completed directory @Dir2@ is passed along with that PathCompletionInfo, + the result would be @Dir1.Dir2.@. +-} mkExposedModulePathCompletion :: PathCompletionInfo -> FilePath -> IO T.Text mkExposedModulePathCompletion complInfo completion = do let combinedPath = queryDirectory complInfo FP. completion @@ -107,17 +119,19 @@ mkExposedModulePathCompletion complInfo completion = do let exposedPath = FP.makeRelative "." combinedPath pure $ addTrailingDot $ fpToExposedModulePath "" exposedPath --- | Takes a source directory path and a module path and returns --- the module path relative to the source directory --- in exposed module syntax where the separators are '.' --- and the file ending is removed. --- --- Synopsis: @'fpToExposedModulePath' sourceDir modPath@. +{- | Takes a source directory path and a module path and returns + the module path relative to the source directory + in exposed module syntax where the separators are '.' + and the file ending is removed if the path is a valid module path, + i.e. all directories start with capital letters. + +Synopsis: @'fpToExposedModulePath' sourceDir modPath@. +-} fpToExposedModulePath :: FilePath -> FilePath -> T.Text fpToExposedModulePath sourceDir modPath = T.intercalate "." $ fmap T.pack $ FP.splitDirectories $ FP.dropExtension fp - where - fp = fromMaybe modPath $ stripPrefix sourceDir modPath + where + fp = fromMaybe modPath $ stripPrefix sourceDir modPath -- | Takes a path in the exposed module syntax and translates it to a platform-compatible file path. exposedModulePathToFp :: T.Text -> FilePath diff --git a/plugins/hls-cabal-plugin/src/Ide/Plugin/Cabal/Completion/Completer/Paths.hs b/plugins/hls-cabal-plugin/src/Ide/Plugin/Cabal/Completion/Completer/Paths.hs index b067fa9e49..045df01cf3 100644 --- a/plugins/hls-cabal-plugin/src/Ide/Plugin/Cabal/Completion/Completer/Paths.hs +++ b/plugins/hls-cabal-plugin/src/Ide/Plugin/Cabal/Completion/Completer/Paths.hs @@ -36,7 +36,7 @@ data PathCompletionInfo = PathCompletionInfo { pathSegment :: T.Text, -- ^ Partly written segment of the next part of the path. queryDirectory :: FilePath, - -- ^ Written part of path, in posix format. + -- ^ written part of path, platform dependent. workingDirectory :: FilePath, -- ^ Directory relative to which relative paths are interpreted, platform dependent. isStringNotationPath :: Maybe Apostrophe diff --git a/plugins/hls-cabal-plugin/src/Ide/Plugin/Cabal/Completion/Completer/Simple.hs b/plugins/hls-cabal-plugin/src/Ide/Plugin/Cabal/Completion/Completer/Simple.hs index d4fb54bb5c..93971f920b 100644 --- a/plugins/hls-cabal-plugin/src/Ide/Plugin/Cabal/Completion/Completer/Simple.hs +++ b/plugins/hls-cabal-plugin/src/Ide/Plugin/Cabal/Completion/Completer/Simple.hs @@ -21,20 +21,23 @@ import qualified Language.LSP.Protocol.Types as Compls (Completi import qualified Language.LSP.Protocol.Types as LSP import qualified Text.Fuzzy.Parallel as Fuzzy --- | Completer to be used when no completion suggestions --- are implemented for the field +{- | Completer to be used when no completion suggestions + are implemented for the field +-} noopCompleter :: Completer noopCompleter _ _ = pure [] --- | Completer to be used when no completion suggestions --- are implemented for the field and a log message should be emitted. +{- | Completer to be used when no completion suggestions + are implemented for the field and a log message should be emitted. +-} errorNoopCompleter :: Log -> Completer errorNoopCompleter l recorder _ = do logWith recorder Warning l pure [] --- | Completer to be used when a simple set of values --- can be completed for a field. +{- | Completer to be used when a simple set of values + can be completed for a field. +-} constantCompleter :: [T.Text] -> Completer constantCompleter completions _ cData = do let prefInfo = cabalPrefixInfo cData @@ -42,11 +45,12 @@ constantCompleter completions _ cData = do range = completionRange prefInfo pure $ map (mkSimpleCompletionItem range . Fuzzy.original) scored --- | Completer to be used for the field @name:@ value. --- --- This is almost always the name of the cabal file. However, --- it is not forbidden by the specification to have a different name, --- it is just forbidden on hackage. +{- | Completer to be used for the field @name:@ value. + + This is almost always the name of the cabal file. However, + it is not forbidden by the specification to have a different name, + it is just forbidden on hackage. +-} nameCompleter :: Completer nameCompleter _ cData = do let scored = Fuzzy.simpleFilter Fuzzy.defChunkSize Fuzzy.defMaxResults (completionPrefix prefInfo) [completionFileName prefInfo] @@ -54,13 +58,14 @@ nameCompleter _ cData = do range = completionRange prefInfo pure $ map (mkSimpleCompletionItem range . Fuzzy.original) scored --- | Completer to be used when a set of values with priority weights --- attached to some values are to be completed for a field. --- --- The higher the weight, the higher the priority to show --- the value in the completion suggestion. --- --- If the value does not occur in the weighted map its weight is defaulted to zero. +{- | Completer to be used when a set of values with priority weights + attached to some values are to be completed for a field. + + The higher the weight, the higher the priority to show + the value in the completion suggestion. + + If the value does not occur in the weighted map its weight is defaulted to zero. +-} weightedConstantCompleter :: [T.Text] -> Map T.Text Double -> Completer weightedConstantCompleter completions weights _ cData = do let scored = @@ -71,67 +76,72 @@ weightedConstantCompleter completions weights _ cData = do else topTenByWeight range = completionRange prefInfo pure $ map (mkSimpleCompletionItem range) scored - where - prefInfo = cabalPrefixInfo cData - prefix = completionPrefix prefInfo - -- The perfect score is the score of the word matched with itself - -- this should never return Nothing since we match the word with itself - perfectScore = fromMaybe (error "match is broken") $ Fuzzy.match prefix prefix - -- \| Since the best score is cut off at the perfect score, we use a custom match - -- which allows for the score to be larger than the perfect score. - -- - -- This is necessary since the weight is multiplied with the originally matched - -- score and thus the calculated score may be larger than the perfect score. - customMatch :: (T.Text -> T.Text -> Maybe Int) - customMatch toSearch searchSpace = do - matched <- Fuzzy.match toSearch searchSpace - let weight = fromMaybe 0 $ Map.lookup searchSpace weights - let score = - min - perfectScore - (round (fromIntegral matched * (1 + weight))) - pure score - -- \| Sorts the list in descending order based on the map of weights and then - -- returns the top ten items in the list - topTenByWeight :: [T.Text] - topTenByWeight = take 10 $ map fst $ List.sortOn (Down . snd) $ Map.assocs weights + where + prefInfo = cabalPrefixInfo cData + prefix = completionPrefix prefInfo + + -- The perfect score is the score of the word matched with itself + -- this should never return Nothing since we match the word with itself + perfectScore = fromMaybe (error "match is broken") $ Fuzzy.match prefix prefix + + -- Since the best score is cut off at the perfect score, we use a custom match + -- which allows for the score to be larger than the perfect score. + -- + -- This is necessary since the weight is multiplied with the originally matched + -- score and thus the calculated score may be larger than the perfect score. + customMatch :: (T.Text -> T.Text -> Maybe Int) + customMatch toSearch searchSpace = do + matched <- Fuzzy.match toSearch searchSpace + let weight = fromMaybe 0 $ Map.lookup searchSpace weights + let score = + min + perfectScore + (round (fromIntegral matched * (1 + weight))) + pure score + -- Sorts the list in descending order based on the map of weights and then + -- returns the top ten items in the list + topTenByWeight :: [T.Text] + topTenByWeight = take 10 $ map fst $ List.sortOn (Down . snd) $ Map.assocs weights --- | Creates a CompletionItem with the given text as the label --- where the completion item kind is keyword. +{- | Creates a CompletionItem with the given text as the label + where the completion item kind is keyword. +-} mkDefaultCompletionItem :: T.Text -> LSP.CompletionItem mkDefaultCompletionItem label = LSP.CompletionItem - { Compls._label = label, - Compls._labelDetails = Nothing, - Compls._kind = Just LSP.CompletionItemKind_Keyword, - Compls._tags = Nothing, - Compls._detail = Nothing, - Compls._documentation = Nothing, - Compls._deprecated = Nothing, - Compls._preselect = Nothing, - Compls._sortText = Nothing, - Compls._filterText = Nothing, - Compls._insertText = Nothing, - Compls._insertTextFormat = Nothing, - Compls._insertTextMode = Nothing, - Compls._textEdit = Nothing, - Compls._textEditText = Nothing, - Compls._additionalTextEdits = Nothing, - Compls._commitCharacters = Nothing, - Compls._command = Nothing, - Compls._data_ = Nothing + { Compls._label = label + , Compls._labelDetails = Nothing + , Compls._kind = Just LSP.CompletionItemKind_Keyword + , Compls._tags = Nothing + , Compls._detail = Nothing + , Compls._documentation = Nothing + , Compls._deprecated = Nothing + , Compls._preselect = Nothing + , Compls._sortText = Nothing + , Compls._filterText = Nothing + , Compls._insertText = Nothing + , Compls._insertTextFormat = Nothing + , Compls._insertTextMode = Nothing + , Compls._textEdit = Nothing + , Compls._textEditText = Nothing + , Compls._additionalTextEdits = Nothing + , Compls._commitCharacters = Nothing + , Compls._command = Nothing + , Compls._data_ = Nothing } --- | Returns a CompletionItem with the given starting position --- and text to be inserted, where the displayed text is the same as the --- inserted text. +{- | Returns a CompletionItem with the given starting position + and text to be inserted, where the displayed text is the same as the + inserted text. +-} mkSimpleCompletionItem :: LSP.Range -> T.Text -> LSP.CompletionItem mkSimpleCompletionItem range txt = mkDefaultCompletionItem txt & JL.textEdit ?~ LSP.InL (LSP.TextEdit range txt) --- | Returns a completionItem with the given starting position, --- text to be inserted and text to be displayed in the completion suggestion. +{- | Returns a completionItem with the given starting position, + text to be inserted and text to be displayed in the completion suggestion. +-} mkCompletionItem :: LSP.Range -> T.Text -> T.Text -> LSP.CompletionItem mkCompletionItem range insertTxt displayTxt = mkDefaultCompletionItem displayTxt diff --git a/plugins/hls-cabal-plugin/src/Ide/Plugin/Cabal/Completion/Completer/Snippet.hs b/plugins/hls-cabal-plugin/src/Ide/Plugin/Cabal/Completion/Completer/Snippet.hs index 800a39bfbc..8d7eb0cbb1 100644 --- a/plugins/hls-cabal-plugin/src/Ide/Plugin/Cabal/Completion/Completer/Snippet.hs +++ b/plugins/hls-cabal-plugin/src/Ide/Plugin/Cabal/Completion/Completer/Snippet.hs @@ -33,15 +33,15 @@ snippetCompleter recorder cData = do pure $ Just $ mkSnippetCompletion completion matched ) scored - where - snippets = snippetMap prefInfo - prefInfo = cabalPrefixInfo cData - mkSnippetCompletion :: T.Text -> T.Text -> LSP.CompletionItem - mkSnippetCompletion insertText toDisplay = - mkDefaultCompletionItem toDisplay - & JL.kind ?~ LSP.CompletionItemKind_Snippet - & JL.insertText ?~ insertText - & JL.insertTextFormat ?~ LSP.InsertTextFormat_Snippet + where + snippets = snippetMap prefInfo + prefInfo = cabalPrefixInfo cData + mkSnippetCompletion :: T.Text -> T.Text -> LSP.CompletionItem + mkSnippetCompletion insertText toDisplay = + mkDefaultCompletionItem toDisplay + & JL.kind ?~ LSP.CompletionItemKind_Snippet + & JL.insertText ?~ insertText + & JL.insertTextFormat ?~ LSP.InsertTextFormat_Snippet type TriggerWord = T.Text @@ -49,60 +49,76 @@ snippetMap :: CabalPrefixInfo -> Map TriggerWord T.Text snippetMap prefInfo = fmap T.unlines $ Map.fromList - [ ( "library-snippet", - [ "library", - " hs-source-dirs: $1", - " exposed-modules: $2", - " build-depends: base", - " default-language: Haskell2010" + [ + ( "library-snippet" + , + [ "library" + , " hs-source-dirs: $1" + , " exposed-modules: $2" + , " build-depends: base" + , " default-language: Haskell2010" ] - ), - ( "recommended-fields", - [ "cabal-version: $1", - "name: " <> completionFileName prefInfo, - "version: 0.1.0.0", - "maintainer: $4", - "category: $5", - "synopsis: $6", - "license: $7", - "build-type: Simple" + ) + , + ( "recommended-fields" + , + [ "cabal-version: $1" + , "name: " <> completionFileName prefInfo + , "version: 0.1.0.0" + , "maintainer: $4" + , "category: $5" + , "synopsis: $6" + , "license: $7" + , "build-type: Simple" ] - ), - ( "executable-snippet", - [ "executable $1", - " main-is: ${2:Main.hs}", - " build-depends: base" + ) + , + ( "executable-snippet" + , + [ "executable $1" + , " main-is: ${2:Main.hs}" + , " build-depends: base" ] - ), - ( "benchmark-snippet", - [ "benchmark $1", - " type: exitcode-stdio-1.0", - " main-is: ${3:Main.hs}", - " build-depends: base" + ) + , + ( "benchmark-snippet" + , + [ "benchmark $1" + , " type: exitcode-stdio-1.0" + , " main-is: ${3:Main.hs}" + , " build-depends: base" ] - ), - ( "testsuite-snippet", - [ "test-suite $1", - " type: exitcode-stdio-1.0", - " main-is: ${3:Main.hs}", - " build-depends: base" + ) + , + ( "testsuite-snippet" + , + [ "test-suite $1" + , " type: exitcode-stdio-1.0" + , " main-is: ${3:Main.hs}" + , " build-depends: base" ] - ), - ( "common-warnings", - [ "common warnings", - " ghc-options: -Wall" + ) + , + ( "common-warnings" + , + [ "common warnings" + , " ghc-options: -Wall" ] - ), - ( "source-repo-github-snippet", - [ "source-repository head", - " type: git", - " location: git://github.com/$2" + ) + , + ( "source-repo-github-snippet" + , + [ "source-repository head" + , " type: git" + , " location: git://github.com/$2" ] - ), - ( "source-repo-git-snippet", - [ "source-repository head", - " type: git", - " location: $1" + ) + , + ( "source-repo-git-snippet" + , + [ "source-repository head" + , " type: git" + , " location: $1" ] ) ] diff --git a/plugins/hls-cabal-plugin/src/Ide/Plugin/Cabal/Completion/Completer/Types.hs b/plugins/hls-cabal-plugin/src/Ide/Plugin/Cabal/Completion/Completer/Types.hs index c39ad2d953..a4c478a42a 100644 --- a/plugins/hls-cabal-plugin/src/Ide/Plugin/Cabal/Completion/Completer/Types.hs +++ b/plugins/hls-cabal-plugin/src/Ide/Plugin/Cabal/Completion/Completer/Types.hs @@ -8,18 +8,19 @@ import Distribution.PackageDescription (GenericPackageDescription) import Ide.Plugin.Cabal.Completion.Types import Language.LSP.Protocol.Types (CompletionItem) --- | Takes information needed to build possible completion items --- and returns the list of possible completion items +{- | Takes information needed to build possible completion items +and returns the list of possible completion items +-} type Completer = Recorder (WithPriority Log) -> CompleterData -> IO [CompletionItem] -- | Contains information to be used by completers. data CompleterData = CompleterData - { -- | Access to the latest available generic package description for the handled cabal file, - -- relevant for some completion actions which require the file's meta information - -- such as the module completers which require access to source directories - getLatestGPD :: IO (Maybe GenericPackageDescription), - -- | Prefix info to be used for constructing completion items - cabalPrefixInfo :: CabalPrefixInfo, - -- | The name of the stanza in which the completer is applied - stanzaName :: Maybe StanzaName + { getLatestGPD :: IO (Maybe GenericPackageDescription) + -- ^ Access to the latest available generic package description for the handled cabal file, + -- relevant for some completion actions which require the file's meta information + -- such as the module completers which require access to source directories + , cabalPrefixInfo :: CabalPrefixInfo + -- ^ Prefix info to be used for constructing completion items + , stanzaName :: Maybe StanzaName + -- ^ The name of the stanza in which the completer is applied } diff --git a/plugins/hls-cabal-plugin/src/Ide/Plugin/Cabal/Completion/Completions.hs b/plugins/hls-cabal-plugin/src/Ide/Plugin/Cabal/Completion/Completions.hs index 69c5fa6598..340f180b0e 100644 --- a/plugins/hls-cabal-plugin/src/Ide/Plugin/Cabal/Completion/Completions.hs +++ b/plugins/hls-cabal-plugin/src/Ide/Plugin/Cabal/Completion/Completions.hs @@ -14,6 +14,7 @@ import qualified Data.Text as T import Data.Text.Utf16.Rope (Rope) import qualified Data.Text.Utf16.Rope as Rope import Development.IDE as D +import Ide.Plugin.Cabal.Cabal import Ide.Plugin.Cabal.Completion.Completer.Simple import Ide.Plugin.Cabal.Completion.Completer.Snippet import Ide.Plugin.Cabal.Completion.Completer.Types (Completer) @@ -28,15 +29,16 @@ import System.FilePath (takeBaseName) -- Public API for Completions -- ---------------------------------------------------------------- --- | Takes information about the completion context within the file --- and finds the correct completer to be applied. +{- | Takes information about the completion context within the file + and finds the correct completer to be applied. +-} contextToCompleter :: Context -> Completer -- if we are in the top level of the cabal file and not in a keyword context, -- we can write any top level keywords or a stanza declaration contextToCompleter (TopLevel, None) = snippetCompleter <> ( constantCompleter $ - Map.keys (cabalVersionKeyword <> cabalKeywords) ++ Map.keys stanzaKeywordMap + Map.keys (cabalVersionKeyword <> cabalKeywords) ++ Map.keys stanzaKeywordMap ) -- if we are in a keyword context in the top level, -- we look up that keyword in the top level context and can complete its possible values @@ -47,23 +49,24 @@ contextToCompleter (TopLevel, KeyWord kw) = -- if we are in a stanza and not in a keyword context, -- we can write any of the stanza's keywords or a stanza declaration contextToCompleter (Stanza s _, None) = - case Map.lookup s stanzaKeywordMap of + case stanzaMapFrom s of Nothing -> errorNoopCompleter (LogUnknownStanzaNameInContextError s) Just l -> constantCompleter $ Map.keys l -- if we are in a stanza's keyword's context we can complete possible values of that keyword contextToCompleter (Stanza s _, KeyWord kw) = - case Map.lookup s stanzaKeywordMap of + case stanzaMapFrom s of Nothing -> errorNoopCompleter (LogUnknownStanzaNameInContextError s) Just m -> case Map.lookup kw m of Nothing -> errorNoopCompleter (LogUnknownKeyWordInContextError kw) Just l -> l --- | Takes prefix info about the previously written text --- and a rope (representing a file), returns the corresponding context. --- --- Can return Nothing if an error occurs. --- --- TODO: first line can only have cabal-version: keyword +{- | Takes prefix info about the previously written text + and a rope (representing a file), returns the corresponding context. + + Can return Nothing if an error occurs. + + TODO: first line can only have cabal-version: keyword +-} getContext :: (MonadIO m) => Recorder (WithPriority Log) -> CabalPrefixInfo -> Rope -> MaybeT m Context getContext recorder prefInfo ls = case prevLinesM of @@ -87,73 +90,75 @@ getContext recorder prefInfo ls = logWith recorder Warning $ LogFileSplitError pos -- basically returns nothing fail "Abort computation" - where - pos = completionCursorPosition prefInfo - prevLinesM = splitAtPosition pos ls - --- | Takes information about the current file's file path, --- and the cursor position in the file; and builds a CabalPrefixInfo --- with the prefix up to that cursor position. --- Checks whether a suffix needs to be completed --- and calculates the range in the document --- where the completion action should be applied. + where + pos = completionCursorPosition prefInfo + prevLinesM = splitAtPosition pos ls + +{- | Takes information about the current file's file path, + and the cursor position in the file; and builds a CabalPrefixInfo + with the prefix up to that cursor position. + Checks whether a suffix needs to be completed + and calculates the range in the document + where the completion action should be applied. +-} getCabalPrefixInfo :: FilePath -> VFS.PosPrefixInfo -> CabalPrefixInfo getCabalPrefixInfo fp prefixInfo = CabalPrefixInfo - { completionPrefix = completionPrefix', - isStringNotation = mkIsStringNotation separator afterCursorText, - completionCursorPosition = VFS.cursorPos prefixInfo, - completionRange = Range completionStart completionEnd, - completionWorkingDir = FP.takeDirectory fp, - completionFileName = T.pack $ takeBaseName fp + { completionPrefix = completionPrefix' + , isStringNotation = mkIsStringNotation separator afterCursorText + , completionCursorPosition = VFS.cursorPos prefixInfo + , completionRange = Range completionStart completionEnd + , completionWorkingDir = FP.takeDirectory fp + , completionFileName = T.pack $ takeBaseName fp } - where - completionEnd = VFS.cursorPos prefixInfo - completionStart = - Position - (_line completionEnd) - (_character completionEnd - (fromIntegral $ T.length completionPrefix')) - (beforeCursorText, afterCursorText) = T.splitAt cursorColumn $ VFS.fullLine prefixInfo - completionPrefix' = T.takeWhileEnd (not . (`elem` stopConditionChars)) beforeCursorText - separator = - -- if there is an opening apostrophe before the cursor in the line somewhere, - -- everything after that apostrophe is the completion prefix - if odd $ T.count "\"" beforeCursorText - then '\"' - else ' ' - cursorColumn = fromIntegral $ VFS.cursorPos prefixInfo ^. JL.character - stopConditionChars = separator : [',', ':'] - - -- \| Takes the character occurring exactly before, - -- and the text occurring after the item to be completed and - -- returns whether the item is already surrounded by apostrophes. - -- - -- Example: (@|@ indicates the cursor position) - -- - -- @"./src|@ would call @'\"'@ @""@ and result in Just LeftSide - -- - -- @"./src|"@ would call @'\"'@ @'\"'@ and result in Just Surrounded - -- - mkIsStringNotation :: Char -> T.Text -> Maybe Apostrophe - mkIsStringNotation '\"' restLine - | Just ('\"', _) <- T.uncons restLine = Just Surrounded - | otherwise = Just LeftSide - mkIsStringNotation _ _ = Nothing + where + completionEnd = VFS.cursorPos prefixInfo + completionStart = + Position + (_line completionEnd) + (_character completionEnd - (fromIntegral $ T.length completionPrefix')) + (beforeCursorText, afterCursorText) = T.splitAt cursorColumn $ VFS.fullLine prefixInfo + completionPrefix' = T.takeWhileEnd (not . (`elem` stopConditionChars)) beforeCursorText + separator = + -- if there is an opening apostrophe before the cursor in the line somewhere, + -- everything after that apostrophe is the completion prefix + if odd $ T.count "\"" beforeCursorText + then '\"' + else ' ' + cursorColumn = fromIntegral $ VFS.cursorPos prefixInfo ^. JL.character + stopConditionChars = separator : [',', ':'] + + -- Takes the character occurring exactly before, + -- and the text occurring after the item to be completed and + -- returns whether the item is already surrounded by apostrophes. + -- + -- Example: (@|@ indicates the cursor position) + -- + -- @"./src|@ would call @'\"'@ @""@ and result in Just LeftSide + -- + -- @"./src|"@ would call @'\"'@ @'\"'@ and result in Just Surrounded + -- + mkIsStringNotation :: Char -> T.Text -> Maybe Apostrophe + mkIsStringNotation '\"' restLine + | Just ('\"', _) <- T.uncons restLine = Just Surrounded + | otherwise = Just LeftSide + mkIsStringNotation _ _ = Nothing -- ---------------------------------------------------------------- -- Implementation Details -- ---------------------------------------------------------------- --- | Takes prefix info about the previously written text, --- a list of lines (representing a file) and a map of --- keywords and returns a keyword context if the --- previously written keyword matches one in the map. --- --- From a cursor position, we traverse the cabal file upwards to --- find the latest written keyword if there is any. --- Values may be written on subsequent lines, --- in order to allow for this we take the indentation of the current --- word to be completed into account to find the correct keyword context. +{- | Takes prefix info about the previously written text, + a list of lines (representing a file) and a map of + keywords and returns a keyword context if the + previously written keyword matches one in the map. + + From a cursor position, we traverse the cabal file upwards to + find the latest written keyword if there is any. + Values may be written on subsequent lines, + in order to allow for this we take the indentation of the current + word to be completed into account to find the correct keyword context. +-} getKeyWordContext :: CabalPrefixInfo -> [T.Text] -> Map KeyWordName a -> Maybe FieldContext getKeyWordContext prefInfo ls keywords = do case lastNonEmptyLineM of @@ -170,58 +175,61 @@ getKeyWordContext prefInfo ls keywords = do Nothing -> Just None Just kw -> Just $ KeyWord kw else Just None - where - lastNonEmptyLineM :: Maybe T.Text - lastNonEmptyLineM = do - (curLine, rest) <- List.uncons ls - -- represents the current line while disregarding the - -- currently written text we want to complete - let cur = stripPartiallyWritten curLine - List.find (not . T.null . T.stripEnd) $ - cur : rest - --- | Traverse the given lines (starting before current cursor position --- up to the start of the file) to find the nearest stanza declaration, --- if none is found we are in the top level context. --- --- TODO: this could be merged with getKeyWordContext in order to increase --- performance by reducing the number of times we have to traverse the cabal file. + where + lastNonEmptyLineM :: Maybe T.Text + lastNonEmptyLineM = do + (curLine, rest) <- List.uncons ls + -- represents the current line while disregarding the + -- currently written text we want to complete + let cur = stripPartiallyWritten curLine + List.find (not . T.null . T.stripEnd) $ + cur : rest + +{- | Traverse the given lines (starting before current cursor position + up to the start of the file) to find the nearest stanza declaration, + if none is found we are in the top level context. + + TODO: this could be merged with getKeyWordContext in order to increase + performance by reducing the number of times we have to traverse the cabal file. +-} currentLevel :: [T.Text] -> StanzaContext currentLevel [] = TopLevel currentLevel (cur : xs) | Just (s, n) <- stanza = Stanza s n | otherwise = currentLevel xs - where - stanza = asum $ map checkStanza (Map.keys stanzaKeywordMap) - checkStanza :: StanzaType -> Maybe (StanzaType, Maybe StanzaName) - checkStanza t = - case T.stripPrefix t (T.strip cur) of - Just n - | T.null n -> Just (t, Nothing) - | otherwise -> Just (t, Just $ T.strip n) - Nothing -> Nothing - --- | Get all lines before the given cursor position in the given file --- and reverse their order to traverse backwards starting from the given position. + where + stanza = asum $ map checkStanza (Map.keys stanzaKeywordMap) + checkStanza :: StanzaType -> Maybe (StanzaType, Maybe StanzaName) + checkStanza t = + case T.stripPrefix t (T.strip cur) of + Just n + | T.null n -> Just (t, Nothing) + | otherwise -> Just (t, Just $ T.strip n) + Nothing -> Nothing + +{- | Get all lines before the given cursor position in the given file + and reverse their order to traverse backwards starting from the given position. +-} splitAtPosition :: Position -> Rope -> Maybe [T.Text] splitAtPosition pos ls = do split <- splitFile pure $ reverse $ Rope.lines $ fst split - where - splitFile = Rope.splitAtPosition ropePos ls - ropePos = - Rope.Position - { Rope.posLine = fromIntegral $ pos ^. JL.line, - Rope.posColumn = fromIntegral $ pos ^. JL.character - } - --- | Takes a line of text and removes the last partially --- written word to be completed. + where + splitFile = Rope.splitAtPosition ropePos ls + ropePos = + Rope.Position + { Rope.posLine = fromIntegral $ pos ^. JL.line + , Rope.posColumn = fromIntegral $ pos ^. JL.character + } + +{- | Takes a line of text and removes the last partially +written word to be completed. +-} stripPartiallyWritten :: T.Text -> T.Text stripPartiallyWritten = T.dropWhileEnd (\y -> (y /= ' ') && (y /= ':')) -- | Calculates how many spaces the currently completed item is indented. completionIndentation :: CabalPrefixInfo -> Int completionIndentation prefInfo = fromIntegral (pos ^. JL.character) - (T.length $ completionPrefix prefInfo) - where - pos = completionCursorPosition prefInfo + where + pos = completionCursorPosition prefInfo diff --git a/plugins/hls-cabal-plugin/src/Ide/Plugin/Cabal/Completion/Data.hs b/plugins/hls-cabal-plugin/src/Ide/Plugin/Cabal/Completion/Data.hs index 24badfcfc5..1b84e7df28 100644 --- a/plugins/hls-cabal-plugin/src/Ide/Plugin/Cabal/Completion/Data.hs +++ b/plugins/hls-cabal-plugin/src/Ide/Plugin/Cabal/Completion/Data.hs @@ -1,8 +1,5 @@ {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE ScopedTypeVariables #-} -{-# OPTIONS_GHC -Wno-unrecognised-pragmas #-} - -{-# HLINT ignore "Redundant bracket" #-} module Ide.Plugin.Cabal.Completion.Data where @@ -33,61 +30,63 @@ cabalVersionKeyword = -- since we don't recommend using older ones. map (T.pack . showCabalSpecVersion) [CabalSpecV2_2 .. maxBound] --- | Top level keywords of a cabal file. --- --- TODO: we could add descriptions of field values and --- then show them when inside the field's context +{- | Top level keywords of a cabal file. + +TODO: we could add descriptions of field values and +then show them when inside the field's context +-} cabalKeywords :: Map KeyWordName Completer cabalKeywords = Map.fromList - [ ("name:", nameCompleter), - ("version:", noopCompleter), - ("build-type:", constantCompleter ["Simple", "Custom", "Configure", "Make"]), - ("license:", weightedConstantCompleter licenseNames weightedLicenseNames), - ("license-file:", filePathCompleter), - ("license-files:", filePathCompleter), - ("copyright:", noopCompleter), - ("author:", noopCompleter), - ("maintainer:", noopCompleter), -- email address, use git config? - ("stability:", noopCompleter), - ("homepage:", noopCompleter), - ("bug-reports:", noopCompleter), - ("package-url:", noopCompleter), - ("synopsis:", noopCompleter), - ("description:", noopCompleter), - ("category:", noopCompleter), - ("tested-with:", constantCompleter ["GHC"]), - ("data-files:", filePathCompleter), - ("data-dir:", directoryCompleter), - ("extra-source-files:", filePathCompleter), - ("extra-doc-files:", filePathCompleter), - ("extra-tmp-files:", filePathCompleter) + [ ("name:", nameCompleter) + , ("version:", noopCompleter) + , ("build-type:", constantCompleter ["Simple", "Custom", "Configure", "Make"]) + , ("license:", weightedConstantCompleter licenseNames weightedLicenseNames) + , ("license-file:", filePathCompleter) + , ("license-files:", filePathCompleter) + , ("copyright:", noopCompleter) + , ("author:", noopCompleter) + , ("maintainer:", noopCompleter) -- email address, use git config? + , ("stability:", noopCompleter) + , ("homepage:", noopCompleter) + , ("bug-reports:", noopCompleter) + , ("package-url:", noopCompleter) + , ("synopsis:", noopCompleter) + , ("description:", noopCompleter) + , ("category:", noopCompleter) + , ("tested-with:", constantCompleter ["GHC"]) + , ("data-files:", filePathCompleter) + , ("data-dir:", directoryCompleter) + , ("extra-source-files:", filePathCompleter) + , ("extra-doc-files:", filePathCompleter) + , ("extra-tmp-files:", filePathCompleter) ] --- | Map, containing all stanzas in a cabal file as keys --- and lists of their possible nested keywords as values. +{- | Map, containing all stanzas in a cabal file as keys + and lists of their possible nested keywords as values. +-} stanzaKeywordMap :: Map StanzaType (Map KeyWordName Completer) stanzaKeywordMap = Map.fromList - [ ("library", libraryFields <> libExecTestBenchCommons), - ("executable", executableFields <> libExecTestBenchCommons), - ("test-suite", testSuiteFields <> libExecTestBenchCommons), - ("benchmark", benchmarkFields <> libExecTestBenchCommons), - ("foreign-library", foreignLibraryFields <> libExecTestBenchCommons), - ("flag", flagFields), - ("source-repository", sourceRepositoryFields) + [ ("library", libraryFields <> libExecTestBenchCommons) + , ("executable", executableFields <> libExecTestBenchCommons) + , ("test-suite", testSuiteFields <> libExecTestBenchCommons) + , ("benchmark", benchmarkFields <> libExecTestBenchCommons) + , ("foreign-library", foreignLibraryFields <> libExecTestBenchCommons) + , ("flag", flagFields) + , ("source-repository", sourceRepositoryFields) ] libraryFields :: Map KeyWordName Completer libraryFields = Map.fromList - [ ("exposed-modules:", modulesCompleter sourceDirsExtractionLibrary), - ("virtual-modules:", noopCompleter), - ("exposed:", constantCompleter ["True", "False"]), - ("visibility:", constantCompleter ["private", "public"]), - ("reexported-modules:", noopCompleter), - ("signatures:", noopCompleter), - ("other-modules:", modulesCompleter sourceDirsExtractionLibrary) + [ ("exposed-modules:", modulesCompleter sourceDirsExtractionLibrary) + , ("virtual-modules:", noopCompleter) + , ("exposed:", constantCompleter ["True", "False"]) + , ("visibility:", constantCompleter ["private", "public"]) + , ("reexported-modules:", noopCompleter) + , ("signatures:", noopCompleter) + , ("other-modules:", modulesCompleter sourceDirsExtractionLibrary) ] executableFields :: Map KeyWordName Completer @@ -117,146 +116,148 @@ benchmarkFields = foreignLibraryFields :: Map KeyWordName Completer foreignLibraryFields = Map.fromList - [ ("type:", constantCompleter ["native-static", "native-shared"]), - ("options:", constantCompleter ["standalone"]), - ("mod-def-file:", filePathCompleter), - ("lib-version-info:", noopCompleter), - ("lib-version-linux:", noopCompleter) + [ ("type:", constantCompleter ["native-static", "native-shared"]) + , ("options:", constantCompleter ["standalone"]) + , ("mod-def-file:", filePathCompleter) + , ("lib-version-info:", noopCompleter) + , ("lib-version-linux:", noopCompleter) ] sourceRepositoryFields :: Map KeyWordName Completer sourceRepositoryFields = Map.fromList - [ ( "type:", - constantCompleter - [ "darcs", - "git", - "svn", - "cvs", - "mercurial", - "hg", - "bazaar", - "bzr", - "arch", - "monotone" + [ + ( "type:" + , constantCompleter + [ "darcs" + , "git" + , "svn" + , "cvs" + , "mercurial" + , "hg" + , "bazaar" + , "bzr" + , "arch" + , "monotone" ] - ), - ("location:", noopCompleter), - ("module:", noopCompleter), - ("branch:", noopCompleter), - ("tag:", noopCompleter), - ("subdir:", directoryCompleter) + ) + , ("location:", noopCompleter) + , ("module:", noopCompleter) + , ("branch:", noopCompleter) + , ("tag:", noopCompleter) + , ("subdir:", directoryCompleter) ] flagFields :: Map KeyWordName Completer flagFields = Map.fromList - [ ("description:", noopCompleter), - ("default:", constantCompleter ["True", "False"]), - ("manual:", constantCompleter ["False", "True"]), - ("lib-def-file:", noopCompleter), - ("lib-version-info:", noopCompleter), - ("lib-version-linux:", noopCompleter) + [ ("description:", noopCompleter) + , ("default:", constantCompleter ["True", "False"]) + , ("manual:", constantCompleter ["False", "True"]) + , ("lib-def-file:", noopCompleter) + , ("lib-version-info:", noopCompleter) + , ("lib-version-linux:", noopCompleter) ] libExecTestBenchCommons :: Map KeyWordName Completer libExecTestBenchCommons = Map.fromList - [ ("build-depends:", noopCompleter), - ("hs-source-dirs:", directoryCompleter), - ("default-extensions:", noopCompleter), - ("other-extensions:", noopCompleter), - ("default-language:", constantCompleter ["GHC2021", "Haskell2010", "Haskell98"]), - ("other-languages:", noopCompleter), - ("build-tool-depends:", noopCompleter), - ("buildable:", constantCompleter ["True", "False"]), - ("ghc-options:", constantCompleter ghcOptions), - ("ghc-prof-options:", constantCompleter ghcOptions), - ("ghc-shared-options:", constantCompleter ghcOptions), - ("ghcjs-options:", constantCompleter ghcOptions), - ("ghcjs-prof-options:", constantCompleter ghcOptions), - ("ghcjs-shared-options:", constantCompleter ghcOptions), - ("includes:", filePathCompleter), - ("install-includes:", filePathCompleter), - ("include-dirs:", directoryCompleter), - ("c-sources:", filePathCompleter), - ("cxx-sources:", filePathCompleter), - ("asm-sources:", filePathCompleter), - ("cmm-sources:", filePathCompleter), - ("js-sources:", filePathCompleter), - ("extra-libraries:", noopCompleter), - ("extra-ghci-libraries:", noopCompleter), - ("extra-bundled-libraries:", noopCompleter), - ("extra-lib-dirs:", directoryCompleter), - ("cc-options:", noopCompleter), - ("cpp-options:", noopCompleter), - ("cxx-options:", noopCompleter), - ("cmm-options:", noopCompleter), - ("asm-options:", noopCompleter), - ("ld-options:", noopCompleter), - ("pkgconfig-depends:", noopCompleter), - ("frameworks:", noopCompleter), - ("extra-framework-dirs:", directoryCompleter), - ("mixins:", noopCompleter) + [ ("build-depends:", noopCompleter) + , ("hs-source-dirs:", directoryCompleter) + , ("default-extensions:", noopCompleter) + , ("other-extensions:", noopCompleter) + , ("default-language:", constantCompleter ["GHC2021", "Haskell2010", "Haskell98"]) + , ("other-languages:", noopCompleter) + , ("build-tool-depends:", noopCompleter) + , ("buildable:", constantCompleter ["True", "False"]) + , ("ghc-options:", constantCompleter ghcOptions) + , ("ghc-prof-options:", constantCompleter ghcOptions) + , ("ghc-shared-options:", constantCompleter ghcOptions) + , ("ghcjs-options:", constantCompleter ghcOptions) + , ("ghcjs-prof-options:", constantCompleter ghcOptions) + , ("ghcjs-shared-options:", constantCompleter ghcOptions) + , ("includes:", filePathCompleter) + , ("install-includes:", filePathCompleter) + , ("include-dirs:", directoryCompleter) + , ("c-sources:", filePathCompleter) + , ("cxx-sources:", filePathCompleter) + , ("asm-sources:", filePathCompleter) + , ("cmm-sources:", filePathCompleter) + , ("js-sources:", filePathCompleter) + , ("extra-libraries:", noopCompleter) + , ("extra-ghci-libraries:", noopCompleter) + , ("extra-bundled-libraries:", noopCompleter) + , ("extra-lib-dirs:", directoryCompleter) + , ("cc-options:", noopCompleter) + , ("cpp-options:", noopCompleter) + , ("cxx-options:", noopCompleter) + , ("cmm-options:", noopCompleter) + , ("asm-options:", noopCompleter) + , ("ld-options:", noopCompleter) + , ("pkgconfig-depends:", noopCompleter) + , ("frameworks:", noopCompleter) + , ("extra-framework-dirs:", directoryCompleter) + , ("mixins:", noopCompleter) ] --- | Contains a map of the most commonly used licenses, weighted by their popularity. --- --- The data was extracted by Kleidukos from the alternative hackage frontend flora.pm. +{- | Contains a map of the most commonly used licenses, weighted by their popularity. + + The data was extracted by Kleidukos from the alternative hackage frontend flora.pm. +-} weightedLicenseNames :: Map T.Text Double weightedLicenseNames = fmap statisticsToWeight $ Map.fromList - [ ("BSD-3-Clause", 9955), - ("MIT", 3336), - ("GPL-3.0-only", 679), - ("LicenseRef-OtherLicense", 521), - ("Apache-2.0", 514), - ("LicenseRef-GPL", 443), - ("LicenseRef-PublicDomain", 318), - ("MPL-2.0", 288), - ("BSD-2-Clause", 174), - ("GPL-2.0-only", 160), - ("LicenseRef-LGPL", 146), - ("LGPL-2.1-only", 112), - ("LGPL-3.0-only", 100), - ("AGPL-3.0-only", 96), - ("ISC", 89), - ("LicenseRef-Apache", 45), - ("GPL-3.0-or-later", 43), - ("BSD-2-Clause-Patent", 33), - ("GPL-2.0-or-later", 21), - ("CC0-1.0", 16), - ("AGPL-3.0-or-later", 15), - ("LGPL-2.1-or-later", 12), - ("(BSD-2-Clause OR Apache-2.0)", 10), - ("(Apache-2.0 OR MPL-2.0)", 8), - ("LicenseRef-AGPL", 6), - ("(BSD-3-Clause OR Apache-2.0)", 4), - ("0BSD", 3), - ("BSD-4-Clause", 3), - ("LGPL-3.0-or-later", 3), - ("LicenseRef-LGPL-2", 2), - ("GPL-2.0-or-later AND BSD-3-Clause", 2), - ("NONE", 2), - ("Zlib", 2), - ("(Apache-2.0 OR BSD-3-Clause)", 2), - ("BSD-3-Clause AND GPL-2.0-or-later", 2), - ("BSD-3-Clause AND GPL-3.0-or-later", 2) + [ ("BSD-3-Clause", 9955) + , ("MIT", 3336) + , ("GPL-3.0-only", 679) + , ("LicenseRef-OtherLicense", 521) + , ("Apache-2.0", 514) + , ("LicenseRef-GPL", 443) + , ("LicenseRef-PublicDomain", 318) + , ("MPL-2.0", 288) + , ("BSD-2-Clause", 174) + , ("GPL-2.0-only", 160) + , ("LicenseRef-LGPL", 146) + , ("LGPL-2.1-only", 112) + , ("LGPL-3.0-only", 100) + , ("AGPL-3.0-only", 96) + , ("ISC", 89) + , ("LicenseRef-Apache", 45) + , ("GPL-3.0-or-later", 43) + , ("BSD-2-Clause-Patent", 33) + , ("GPL-2.0-or-later", 21) + , ("CC0-1.0", 16) + , ("AGPL-3.0-or-later", 15) + , ("LGPL-2.1-or-later", 12) + , ("(BSD-2-Clause OR Apache-2.0)", 10) + , ("(Apache-2.0 OR MPL-2.0)", 8) + , ("LicenseRef-AGPL", 6) + , ("(BSD-3-Clause OR Apache-2.0)", 4) + , ("0BSD", 3) + , ("BSD-4-Clause", 3) + , ("LGPL-3.0-or-later", 3) + , ("LicenseRef-LGPL-2", 2) + , ("GPL-2.0-or-later AND BSD-3-Clause", 2) + , ("NONE", 2) + , ("Zlib", 2) + , ("(Apache-2.0 OR BSD-3-Clause)", 2) + , ("BSD-3-Clause AND GPL-2.0-or-later", 2) + , ("BSD-3-Clause AND GPL-3.0-or-later", 2) ] - where - -- Add weights to each usage value from above, the weights are chosen - -- arbitrarily in order for completions to prioritize which licenses to - -- suggest in a sensible way - statisticsToWeight :: Int -> Double - statisticsToWeight stat - | stat < 10 = 0.1 - | stat < 20 = 0.3 - | stat < 50 = 0.4 - | stat < 100 = 0.5 - | stat < 500 = 0.6 - | stat < 650 = 0.7 - | otherwise = 0.9 + where + -- Add weights to each usage value from above, the weights are chosen + -- arbitrarily in order for completions to prioritize which licenses to + -- suggest in a sensible way + statisticsToWeight :: Int -> Double + statisticsToWeight stat + | stat < 10 = 0.1 + | stat < 20 = 0.3 + | stat < 50 = 0.4 + | stat < 100 = 0.5 + | stat < 500 = 0.6 + | stat < 650 = 0.7 + | otherwise = 0.9 ghcOptions :: [T.Text] ghcOptions = map T.pack $ flagsForCompletion False diff --git a/plugins/hls-cabal-plugin/src/Ide/Plugin/Cabal/Completion/Types.hs b/plugins/hls-cabal-plugin/src/Ide/Plugin/Cabal/Completion/Types.hs index 4783c2bbe3..dfba46f0f6 100644 --- a/plugins/hls-cabal-plugin/src/Ide/Plugin/Cabal/Completion/Types.hs +++ b/plugins/hls-cabal-plugin/src/Ide/Plugin/Cabal/Completion/Types.hs @@ -23,6 +23,7 @@ data Log | LogFilePathCompleterIOError FilePath IOError | LogUseWithStaleFastNoResult | LogMapLookUpOfKnownKeyFailed T.Text + | LogInvalidCabalFileStructure deriving (Show) instance Pretty Log where @@ -36,6 +37,7 @@ instance Pretty Log where "When trying to complete the file path:" <+> viaShow fp <+> "the following unexpected IO error occured" <+> viaShow ioErr LogUseWithStaleFastNoResult -> "Package description couldn't be read" LogMapLookUpOfKnownKeyFailed key -> "Lookup of key in map failed even though it should exist" <+> viaShow key + LogInvalidCabalFileStructure -> "Encountered modules field in cabal file outside of stanza" type instance RuleResult ParseCabal = Parse.GenericPackageDescription @@ -46,15 +48,17 @@ instance Hashable ParseCabal instance NFData ParseCabal --- | The context a cursor can be in within a cabal file. --- --- We can be in stanzas or the top level, --- and additionally we can be in a context where we have already --- written a keyword but no value for it yet +{- | The context a cursor can be in within a cabal file. + + We can be in stanzas or the top level, + and additionally we can be in a context where we have already + written a keyword but no value for it yet +-} type Context = (StanzaContext, FieldContext) --- | Context inside a cabal file. --- Used to decide which keywords to suggest. +{- | Context inside a cabal file. + Used to decide which keywords to suggest. +-} data StanzaContext = -- | Top level context in a cabal file such as 'author' TopLevel @@ -64,11 +68,12 @@ data StanzaContext -- Each stanza must be named, such as 'executable exe', -- except for the main library. Stanza StanzaType (Maybe StanzaName) - deriving (Eq, Show, Read) + deriving (Eq, Show, Read, Ord) + +{- | Keyword context in a cabal file. --- | Keyword context in a cabal file. --- --- Used to decide whether to suggest values or keywords. + Used to decide whether to suggest values or keywords. +-} data FieldContext = -- | Key word context, where a keyword -- occurs right before the current word @@ -85,56 +90,59 @@ type StanzaName = T.Text type StanzaType = T.Text --- | Information regarding the current completion status --- --- Example: @"dir1/fi@ having been written to the file --- would correspond to: --- --- @ --- completionPrefix = "dir1/fi" --- isStringNotation = LeftSide --- ... --- @ --- --- We define this type instead of simply using --- VFS.PosPrefixInfo since e.g. for filepaths we --- need more than just the word before the --- cursor (as can be seen above), --- since we want to capture the whole filepath --- before the cursor. --- --- We also use this type to wrap all information --- necessary to complete filepaths and other values --- in a cabal file. +{- | Information regarding the current completion status + + Example: @"dir1/fi@ having been written to the file + would correspond to: + + @ + completionPrefix = "dir1/fi" + isStringNotation = LeftSide + ... + @ + + We define this type instead of simply using + VFS.PosPrefixInfo since e.g. for filepaths we + need more than just the word before the + cursor (as can be seen above), + since we want to capture the whole filepath + before the cursor. + + We also use this type to wrap all information + necessary to complete filepaths and other values + in a cabal file. +-} data CabalPrefixInfo = CabalPrefixInfo - { -- | text prefix to complete - completionPrefix :: T.Text, - -- | Did the completion happen in the context of a string notation, - -- i.e. are there apostrophes around the item to be completed - isStringNotation :: Maybe Apostrophe, - -- | the current position of the cursor in the file - completionCursorPosition :: Position, - -- | range where completion is to be inserted - completionRange :: Range, - -- | directory of the handled cabal file - completionWorkingDir :: FilePath, - -- | filename of the handled cabal file - completionFileName :: T.Text + { completionPrefix :: T.Text + -- ^ text prefix to complete + , isStringNotation :: Maybe Apostrophe + -- ^ Did the completion happen in the context of a string notation, + -- i.e. are there apostrophes around the item to be completed + , completionCursorPosition :: Position + -- ^ the current position of the cursor in the file + , completionRange :: Range + -- ^ range where completion is to be inserted + , completionWorkingDir :: FilePath + -- ^ directory of the handled cabal file + , completionFileName :: T.Text + -- ^ filename of the handled cabal file } deriving (Eq, Show) --- | Where are the apostrophes around the item to be completed? --- --- 'Surrounded' means the item to complete already has the necessary apostrophes, --- while 'LeftSide' means, a closing apostrophe has to be added after the completion item. +{- | Where are the apostrophes around the item to be completed? + + 'Surrounded' means the item to complete already has the necessary apostrophes, + while 'LeftSide' means, a closing apostrophe has to be added after the completion item. +-} data Apostrophe = Surrounded | LeftSide deriving (Eq, Ord, Show) --- | Wraps a completion in apostrophes where appropriate. --- --- If a completion starts with an apostrophe we want to end it with an apostrophe. --- If a completed filepath contains a space, it can only be written in the cabal --- file if it is wrapped in apostrophes, thus we wrap it. +{- | Wraps a completion in apostrophes where appropriate. + + If a completion starts with an apostrophe we want to end it with an apostrophe. + If a completed filepath contains a space, it can only be written in the cabal + file if it is wrapped in apostrophes, thus we wrap it. +-} applyStringNotation :: Maybe Apostrophe -> T.Text -> T.Text applyStringNotation (Just Surrounded) compl = compl applyStringNotation (Just LeftSide) compl = compl <> "\"" diff --git a/plugins/hls-cabal-plugin/test/CodeActions.hs b/plugins/hls-cabal-plugin/test/CodeActions.hs new file mode 100644 index 0000000000..65217f450f --- /dev/null +++ b/plugins/hls-cabal-plugin/test/CodeActions.hs @@ -0,0 +1,104 @@ +{-# LANGUAGE OverloadedStrings #-} + +module CodeActions where + +import Control.Lens ((^.)) +import Control.Monad.Trans.Except (runExceptT) +import qualified Data.ByteString as BS +import qualified Data.Map as M +import Data.Maybe (fromJust) +import qualified Data.Text as T +import qualified Data.Text.Encoding as Encoding +import qualified Data.Text.IO as T +import Ide.Plugin.Cabal.CodeActions +import qualified Language.LSP.Protocol.Lens as JL +import qualified Language.LSP.Protocol.Types as J +import qualified Language.LSP.Protocol.Types as LSP +import System.FilePath (normalise, ()) +import Test.Hls (CodeAction, TestTree, TextEdit, + assertFailure, goldenGitDiff, + testCase, testGroup, (@?=)) +import qualified Text.Cabal.Parser as CP +import qualified Text.Cabal.Types as CP (myPretty) +import Utils + +moduleCodeActionTests :: TestTree +moduleCodeActionTests = + testGroup + "Module Code Action Tests" + [ collectModuleInsertionOptionsTests + ] + +collectModuleInsertionOptionsTests :: TestTree +collectModuleInsertionOptionsTests = + testGroup + "Collect Module Insertion Options Tests" + [ testCase "empty file" $ do + codeActions <- callCollectModuleInsertionOptions [""] + codeActions @?= [], + goldenGitDiff "library - empty exposed" (testDataDir "add-mods" "lib.golden.cabal") $ do + (codeActions, contents) <- callCollectModuleInsertionOptionsFromFp "lib.cabal" "./src/NewModule.hs" + let te = getTEWithLabel "library" codeActions + let newContents = LSP.applyTextEdit te contents + pure $ BS.fromStrict $ Encoding.encodeUtf8 newContents, + goldenGitDiff "test suite - existing modules in same line with commas" (testDataDir "add-mods" "test-suite.golden.cabal") $ do + (codeActions, contents) <- callCollectModuleInsertionOptionsFromFp "test-suite.cabal" "./src/NewModule.hs" + let te = getTEWithLabel "test-suite testiebestie" codeActions + let newContents = LSP.applyTextEdit te contents + pure $ BS.fromStrict $ Encoding.encodeUtf8 newContents, + goldenGitDiff "library - multiline modules" (testDataDir "add-mods" "lib-multiline.golden.cabal") $ do + (codeActions, contents) <- callCollectModuleInsertionOptionsFromFp "lib-multiline.cabal" "./src/NewModule.hs" + let te = getTEWithLabel "library" codeActions + let newContents = LSP.applyTextEdit te contents + pure $ BS.fromStrict $ Encoding.encodeUtf8 newContents, + goldenGitDiff "executable - multiline" (testDataDir "add-mods" "executable-multiline.golden.cabal") $ do + (codeActions, contents) <- callCollectModuleInsertionOptionsFromFp "executable-multiline.cabal" "test/preprocessor/NewModule.hs" + let te = getTEWithLabel "executable exeName" codeActions + let newContents = LSP.applyTextEdit te contents + pure $ BS.fromStrict $ Encoding.encodeUtf8 newContents, + goldenGitDiff "lib - multiline - first element" (testDataDir "add-mods" "lib-first-multiline.golden.cabal") $ do + (codeActions, contents) <- callCollectModuleInsertionOptionsFromFp "lib-first-multiline.cabal" "./src/NewModule.hs" + let te = getTEWithLabel "library" codeActions + let newContents = LSP.applyTextEdit te contents + pure $ BS.fromStrict $ Encoding.encodeUtf8 newContents, + goldenGitDiff "real file - lib" (testDataDir "add-mods" "real-cabal-lib.golden.cabal") $ do + (codeActions, contents) <- callCollectModuleInsertionOptionsFromFp "real-cabal.cabal" "./src/NewModule.hs" + let te = getTEWithLabel "library" codeActions + let newContents = LSP.applyTextEdit te contents + pure $ BS.fromStrict $ Encoding.encodeUtf8 newContents, + goldenGitDiff "real file - lib" (testDataDir "add-mods" "real-cabal-test-suite.golden.cabal") $ do + (codeActions, contents) <- callCollectModuleInsertionOptionsFromFp "./real-cabal.cabal" "./test/NewModule.hs" + let te = getTEWithLabel "test-suite tests" codeActions + let newContents = LSP.applyTextEdit te contents + pure $ BS.fromStrict $ Encoding.encodeUtf8 newContents, + testCase "empty source dirs - lowercase dir" $ do + (codeActions, _) <- callCollectModuleInsertionOptionsFromFp "./empty-source-dirs.cabal" "./test/NewModule.hs" + -- no code actions are found, since the test directory is lowercase + codeActions @?= [] + ] + where + callCollectModuleInsertionOptionsFromFp :: FilePath -> FilePath -> IO ([J.CodeAction], T.Text) + callCollectModuleInsertionOptionsFromFp relCabalFp relModulePath = do + let cabalFp = normalise $ testDataDir "add-mods" relCabalFp + moduleFp = normalise $ testDataDir "add-mods" relModulePath + contents <- T.readFile cabalFp + let eitherAST = CP.parseCabalFile cabalFp contents + case eitherAST of + Right ast -> do + Right options <- runExceptT $ collectModuleInsertionOptions mempty cabalFp (LSP.filePathToUri moduleFp) ast + pure (options, contents) + Left _ -> assertFailure $ "failed to parse cabal file to ast" <> show (CP.myPretty eitherAST) + + callCollectModuleInsertionOptions :: [T.Text] -> IO [J.CodeAction] + callCollectModuleInsertionOptions ls = do + let eitherAST = CP.parseCabalFile "./myFile.cabal" $ T.unlines ls + case eitherAST of + Right ast -> do + Right options <- runExceptT $ collectModuleInsertionOptions mempty "./myFile.cabal" (LSP.filePathToUri $ testDataDir "add-mods" "src" "NewModule.hs") ast + pure options + _ -> assertFailure "failed to parse cabal file to ast" + + getTEWithLabel :: T.Text -> [CodeAction] -> TextEdit + getTEWithLabel label codeActions = head $ head $ M.elems $ fromJust (fromJust (extractCa ^. JL.edit) ^. JL.changes) + where + extractCa = head $ filter (\x -> label `T.isInfixOf` (x ^. JL.title)) codeActions diff --git a/plugins/hls-cabal-plugin/test/Main.hs b/plugins/hls-cabal-plugin/test/Main.hs index 4ee8afac28..0b814a7bc5 100644 --- a/plugins/hls-cabal-plugin/test/Main.hs +++ b/plugins/hls-cabal-plugin/test/Main.hs @@ -8,6 +8,7 @@ module Main ( main, ) where +import CodeActions (moduleCodeActionTests) import Completer (completerTests) import Context (contextTests) import Control.Lens ((^.)) @@ -24,6 +25,7 @@ import System.FilePath import Test.Hls import Utils + main :: IO () main = do defaultTestRunner $ @@ -33,6 +35,7 @@ main = do , pluginTests , completerTests , contextTests + , moduleCodeActionTests ] -- ------------------------------------------------------------------------ diff --git a/plugins/hls-cabal-plugin/test/testdata/add-mods/empty-source-dirs.cabal b/plugins/hls-cabal-plugin/test/testdata/add-mods/empty-source-dirs.cabal new file mode 100644 index 0000000000..1741533b7f --- /dev/null +++ b/plugins/hls-cabal-plugin/test/testdata/add-mods/empty-source-dirs.cabal @@ -0,0 +1,13 @@ +cabal-version: 3.4 +name: test-hls +version: 0.1.0.0 +maintainer: milky +synopsis: example cabal file :) +license: Apache-2.0 +build-type: Simple + +library + hs-source-dirs: + exposed-modules: + build-depends: base + default-language: Haskell2010 diff --git a/plugins/hls-cabal-plugin/test/testdata/add-mods/executable-multiline.cabal b/plugins/hls-cabal-plugin/test/testdata/add-mods/executable-multiline.cabal new file mode 100644 index 0000000000..4560b162b8 --- /dev/null +++ b/plugins/hls-cabal-plugin/test/testdata/add-mods/executable-multiline.cabal @@ -0,0 +1,7 @@ +cabal-version: 3.0 +name: simple-cabal +executable exeName + default-language: Haskell2010 + hs-source-dirs: test/preprocessor + other-modules: + MyLib diff --git a/plugins/hls-cabal-plugin/test/testdata/add-mods/executable-multiline.golden.cabal b/plugins/hls-cabal-plugin/test/testdata/add-mods/executable-multiline.golden.cabal new file mode 100644 index 0000000000..b637381cec --- /dev/null +++ b/plugins/hls-cabal-plugin/test/testdata/add-mods/executable-multiline.golden.cabal @@ -0,0 +1,8 @@ +cabal-version: 3.0 +name: simple-cabal +executable exeName + default-language: Haskell2010 + hs-source-dirs: test/preprocessor + other-modules: + MyLib + NewModule diff --git a/plugins/hls-cabal-plugin/test/testdata/add-mods/lib-first-multiline.cabal b/plugins/hls-cabal-plugin/test/testdata/add-mods/lib-first-multiline.cabal new file mode 100644 index 0000000000..3b4abba3b7 --- /dev/null +++ b/plugins/hls-cabal-plugin/test/testdata/add-mods/lib-first-multiline.cabal @@ -0,0 +1,12 @@ +cabal-version: 3.0 +name: simple-cabal +executable exeName + default-language: Haskell2010 + hs-source-dirs: test/preprocessor + other-modules: MyLib +library + + hs-source-dirs: ./src + other-modules: XModule + YModule + ZModule diff --git a/plugins/hls-cabal-plugin/test/testdata/add-mods/lib-first-multiline.golden.cabal b/plugins/hls-cabal-plugin/test/testdata/add-mods/lib-first-multiline.golden.cabal new file mode 100644 index 0000000000..e40c6b4bf9 --- /dev/null +++ b/plugins/hls-cabal-plugin/test/testdata/add-mods/lib-first-multiline.golden.cabal @@ -0,0 +1,13 @@ +cabal-version: 3.0 +name: simple-cabal +executable exeName + default-language: Haskell2010 + hs-source-dirs: test/preprocessor + other-modules: MyLib +library + + hs-source-dirs: ./src + other-modules: NewModule + XModule + YModule + ZModule diff --git a/plugins/hls-cabal-plugin/test/testdata/add-mods/lib-multiline.cabal b/plugins/hls-cabal-plugin/test/testdata/add-mods/lib-multiline.cabal new file mode 100644 index 0000000000..59d073648d --- /dev/null +++ b/plugins/hls-cabal-plugin/test/testdata/add-mods/lib-multiline.cabal @@ -0,0 +1,11 @@ +cabal-version: 3.0 +name: simple-cabal +executable exeName + default-language: Haskell2010 + hs-source-dirs: test/preprocessor + other-modules: MyLib +library + + hs-source-dirs: ./src + other-modules: MyLib + Yir1.YourLib diff --git a/plugins/hls-cabal-plugin/test/testdata/add-mods/lib-multiline.golden.cabal b/plugins/hls-cabal-plugin/test/testdata/add-mods/lib-multiline.golden.cabal new file mode 100644 index 0000000000..2996631279 --- /dev/null +++ b/plugins/hls-cabal-plugin/test/testdata/add-mods/lib-multiline.golden.cabal @@ -0,0 +1,12 @@ +cabal-version: 3.0 +name: simple-cabal +executable exeName + default-language: Haskell2010 + hs-source-dirs: test/preprocessor + other-modules: MyLib +library + + hs-source-dirs: ./src + other-modules: MyLib + NewModule + Yir1.YourLib diff --git a/plugins/hls-cabal-plugin/test/testdata/add-mods/lib.cabal b/plugins/hls-cabal-plugin/test/testdata/add-mods/lib.cabal new file mode 100644 index 0000000000..732602d3ee --- /dev/null +++ b/plugins/hls-cabal-plugin/test/testdata/add-mods/lib.cabal @@ -0,0 +1,5 @@ +cabal-version: 3.0 +name: simple-cabal +library + hs-source-dirs: ./src + other-modules: diff --git a/plugins/hls-cabal-plugin/test/testdata/add-mods/lib.golden.cabal b/plugins/hls-cabal-plugin/test/testdata/add-mods/lib.golden.cabal new file mode 100644 index 0000000000..d43b25a2bb --- /dev/null +++ b/plugins/hls-cabal-plugin/test/testdata/add-mods/lib.golden.cabal @@ -0,0 +1,6 @@ +cabal-version: 3.0 +name: simple-cabal +library + hs-source-dirs: ./src + other-modules: + NewModule diff --git a/plugins/hls-cabal-plugin/test/testdata/add-mods/real-cabal-lib.golden.cabal b/plugins/hls-cabal-plugin/test/testdata/add-mods/real-cabal-lib.golden.cabal new file mode 100644 index 0000000000..8b354977ae --- /dev/null +++ b/plugins/hls-cabal-plugin/test/testdata/add-mods/real-cabal-lib.golden.cabal @@ -0,0 +1,105 @@ +cabal-version: 3.0 +name: hls-cabal-plugin +version: 2.1.0.0 +synopsis: Cabal integration plugin with Haskell Language Server +description: + Please see the README on GitHub at + +homepage: +license: MIT +license-file: LICENSE +author: Fendor +maintainer: fendor@posteo.de +category: Development +extra-source-files: + CHANGELOG.md + test/testdata/*.cabal + test/testdata/simple-cabal/A.hs + test/testdata/simple-cabal/cabal.project + test/testdata/simple-cabal/hie.yaml + test/testdata/simple-cabal/simple-cabal.cabal + +common warnings + ghc-options: -Wall + +library + import: warnings + exposed-modules: + Ide.Plugin.Cabal + Ide.Plugin.Cabal.Diagnostics + Ide.Plugin.Cabal.Cabal + Ide.Plugin.Cabal.CodeActions + Ide.Plugin.Cabal.Completion.Completer.FilePath + Ide.Plugin.Cabal.Completion.Completer.Module + Ide.Plugin.Cabal.Completion.Completer.Simple + Ide.Plugin.Cabal.Completion.Completer.Snippet + Ide.Plugin.Cabal.Completion.Completer.Types + Ide.Plugin.Cabal.Completion.Completions + Ide.Plugin.Cabal.Completion.Data + Ide.Plugin.Cabal.Completion.Types + Ide.Plugin.Cabal.LicenseSuggest + Ide.Plugin.Cabal.Parser.CabalParser + Ide.Plugin.Cabal.Parser.Data + Ide.Plugin.Cabal.Parser.Types + Ide.Plugin.Cabal.Parser.ValueParser + Ide.Plugin.Cabal.Parse + NewModule + + build-depends: + , base >=4.12 && <5 + , bytestring + , Cabal-syntax >= 3.7 + , containers + , deepseq + , directory + , filepath + , extra >=1.7.4 + , ghcide == 2.1.0.0 + , hashable + , hls-plugin-api == 2.1.0.0 + , hls-graph == 2.1.0.0 + , lens + , lsp ^>=2.0.0.0 + , lsp-types ^>=2.0.0.1 + , megaparsec + , regex-tdfa ^>=1.3.1 + , safe + , stm + , text + , text-rope + , transformers + , unordered-containers >=0.2.10.0 + , containers + + hs-source-dirs: src + default-language: Haskell2010 + +test-suite tests + import: warnings + default-language: Haskell2010 + type: exitcode-stdio-1.0 + hs-source-dirs: test + main-is: Main.hs + other-modules: + CodeActions + Completer + Context + Utils + build-depends: + , base + , bytestring + , Cabal-syntax >= 3.7 + , containers + , directory + , filepath + , ghcide + , hls-cabal-plugin + , hls-test-utils == 2.1.0.0 + , lens + , lsp + , lsp-types + , tasty-hunit + , text + , text-rope + , transformers + , row-types diff --git a/plugins/hls-cabal-plugin/test/testdata/add-mods/real-cabal-test-suite.golden.cabal b/plugins/hls-cabal-plugin/test/testdata/add-mods/real-cabal-test-suite.golden.cabal new file mode 100644 index 0000000000..63a05bcf58 --- /dev/null +++ b/plugins/hls-cabal-plugin/test/testdata/add-mods/real-cabal-test-suite.golden.cabal @@ -0,0 +1,105 @@ +cabal-version: 3.0 +name: hls-cabal-plugin +version: 2.1.0.0 +synopsis: Cabal integration plugin with Haskell Language Server +description: + Please see the README on GitHub at + +homepage: +license: MIT +license-file: LICENSE +author: Fendor +maintainer: fendor@posteo.de +category: Development +extra-source-files: + CHANGELOG.md + test/testdata/*.cabal + test/testdata/simple-cabal/A.hs + test/testdata/simple-cabal/cabal.project + test/testdata/simple-cabal/hie.yaml + test/testdata/simple-cabal/simple-cabal.cabal + +common warnings + ghc-options: -Wall + +library + import: warnings + exposed-modules: + Ide.Plugin.Cabal + Ide.Plugin.Cabal.Diagnostics + Ide.Plugin.Cabal.Cabal + Ide.Plugin.Cabal.CodeActions + Ide.Plugin.Cabal.Completion.Completer.FilePath + Ide.Plugin.Cabal.Completion.Completer.Module + Ide.Plugin.Cabal.Completion.Completer.Simple + Ide.Plugin.Cabal.Completion.Completer.Snippet + Ide.Plugin.Cabal.Completion.Completer.Types + Ide.Plugin.Cabal.Completion.Completions + Ide.Plugin.Cabal.Completion.Data + Ide.Plugin.Cabal.Completion.Types + Ide.Plugin.Cabal.LicenseSuggest + Ide.Plugin.Cabal.Parser.CabalParser + Ide.Plugin.Cabal.Parser.Data + Ide.Plugin.Cabal.Parser.Types + Ide.Plugin.Cabal.Parser.ValueParser + Ide.Plugin.Cabal.Parse + + build-depends: + , base >=4.12 && <5 + , bytestring + , Cabal-syntax >= 3.7 + , containers + , deepseq + , directory + , filepath + , extra >=1.7.4 + , ghcide == 2.1.0.0 + , hashable + , hls-plugin-api == 2.1.0.0 + , hls-graph == 2.1.0.0 + , lens + , lsp ^>=2.0.0.0 + , lsp-types ^>=2.0.0.1 + , megaparsec + , regex-tdfa ^>=1.3.1 + , safe + , stm + , text + , text-rope + , transformers + , unordered-containers >=0.2.10.0 + , containers + + hs-source-dirs: src + default-language: Haskell2010 + +test-suite tests + import: warnings + default-language: Haskell2010 + type: exitcode-stdio-1.0 + hs-source-dirs: test + main-is: Main.hs + other-modules: + CodeActions + Completer + Context + NewModule + Utils + build-depends: + , base + , bytestring + , Cabal-syntax >= 3.7 + , containers + , directory + , filepath + , ghcide + , hls-cabal-plugin + , hls-test-utils == 2.1.0.0 + , lens + , lsp + , lsp-types + , tasty-hunit + , text + , text-rope + , transformers + , row-types diff --git a/plugins/hls-cabal-plugin/test/testdata/add-mods/real-cabal.cabal b/plugins/hls-cabal-plugin/test/testdata/add-mods/real-cabal.cabal new file mode 100644 index 0000000000..22518fd289 --- /dev/null +++ b/plugins/hls-cabal-plugin/test/testdata/add-mods/real-cabal.cabal @@ -0,0 +1,104 @@ +cabal-version: 3.0 +name: hls-cabal-plugin +version: 2.1.0.0 +synopsis: Cabal integration plugin with Haskell Language Server +description: + Please see the README on GitHub at + +homepage: +license: MIT +license-file: LICENSE +author: Fendor +maintainer: fendor@posteo.de +category: Development +extra-source-files: + CHANGELOG.md + test/testdata/*.cabal + test/testdata/simple-cabal/A.hs + test/testdata/simple-cabal/cabal.project + test/testdata/simple-cabal/hie.yaml + test/testdata/simple-cabal/simple-cabal.cabal + +common warnings + ghc-options: -Wall + +library + import: warnings + exposed-modules: + Ide.Plugin.Cabal + Ide.Plugin.Cabal.Diagnostics + Ide.Plugin.Cabal.Cabal + Ide.Plugin.Cabal.CodeActions + Ide.Plugin.Cabal.Completion.Completer.FilePath + Ide.Plugin.Cabal.Completion.Completer.Module + Ide.Plugin.Cabal.Completion.Completer.Simple + Ide.Plugin.Cabal.Completion.Completer.Snippet + Ide.Plugin.Cabal.Completion.Completer.Types + Ide.Plugin.Cabal.Completion.Completions + Ide.Plugin.Cabal.Completion.Data + Ide.Plugin.Cabal.Completion.Types + Ide.Plugin.Cabal.LicenseSuggest + Ide.Plugin.Cabal.Parser.CabalParser + Ide.Plugin.Cabal.Parser.Data + Ide.Plugin.Cabal.Parser.Types + Ide.Plugin.Cabal.Parser.ValueParser + Ide.Plugin.Cabal.Parse + + build-depends: + , base >=4.12 && <5 + , bytestring + , Cabal-syntax >= 3.7 + , containers + , deepseq + , directory + , filepath + , extra >=1.7.4 + , ghcide == 2.1.0.0 + , hashable + , hls-plugin-api == 2.1.0.0 + , hls-graph == 2.1.0.0 + , lens + , lsp ^>=2.0.0.0 + , lsp-types ^>=2.0.0.1 + , megaparsec + , regex-tdfa ^>=1.3.1 + , safe + , stm + , text + , text-rope + , transformers + , unordered-containers >=0.2.10.0 + , containers + + hs-source-dirs: src + default-language: Haskell2010 + +test-suite tests + import: warnings + default-language: Haskell2010 + type: exitcode-stdio-1.0 + hs-source-dirs: test + main-is: Main.hs + other-modules: + CodeActions + Completer + Context + Utils + build-depends: + , base + , bytestring + , Cabal-syntax >= 3.7 + , containers + , directory + , filepath + , ghcide + , hls-cabal-plugin + , hls-test-utils == 2.1.0.0 + , lens + , lsp + , lsp-types + , tasty-hunit + , text + , text-rope + , transformers + , row-types diff --git a/plugins/hls-cabal-plugin/test/testdata/add-mods/test-suite.cabal b/plugins/hls-cabal-plugin/test/testdata/add-mods/test-suite.cabal new file mode 100644 index 0000000000..1e47db9aa2 --- /dev/null +++ b/plugins/hls-cabal-plugin/test/testdata/add-mods/test-suite.cabal @@ -0,0 +1,5 @@ +cabal-version: 3.0 +name: simple-cabal +test-suite testiebestie + hs-source-dirs: ./src + other-modules: M1, M2 diff --git a/plugins/hls-cabal-plugin/test/testdata/add-mods/test-suite.golden.cabal b/plugins/hls-cabal-plugin/test/testdata/add-mods/test-suite.golden.cabal new file mode 100644 index 0000000000..d4f31079f2 --- /dev/null +++ b/plugins/hls-cabal-plugin/test/testdata/add-mods/test-suite.golden.cabal @@ -0,0 +1,5 @@ +cabal-version: 3.0 +name: simple-cabal +test-suite testiebestie + hs-source-dirs: ./src + other-modules: M1, M2 NewModule diff --git a/src/HlsPlugins.hs b/src/HlsPlugins.hs index e12e5e9f35..49ebb897ed 100644 --- a/src/HlsPlugins.hs +++ b/src/HlsPlugins.hs @@ -148,6 +148,7 @@ idePlugins recorder = pluginDescToIdePlugins allPlugins allPlugins = #if hls_cabal let pId = "cabal" in Cabal.descriptor (pluginRecorder pId) pId : + let caId = "add-module" in Cabal.hsDescriptor (pluginRecorder caId) caId : #endif #if hls_pragmas Pragmas.suggestPragmaDescriptor "pragmas-suggest" :