packages feed

hls-cabal-plugin-2.2.0.0: test/Context.hs

{-# LANGUAGE DisambiguateRecordFields #-}
{-# LANGUAGE LambdaCase               #-}
{-# LANGUAGE OverloadedStrings        #-}

module Context where

import           Control.Monad.Trans.Maybe                   (runMaybeT)
import qualified Data.Text                                   as T
import qualified Data.Text.Utf16.Rope                        as Rope
import           Ide.Plugin.Cabal
import           Ide.Plugin.Cabal.Completion.Completer.Paths
import           Ide.Plugin.Cabal.Completion.Completions
import           Ide.Plugin.Cabal.Completion.Types           (Context,
                                                              FieldContext (KeyWord, None),
                                                              StanzaContext (Stanza, TopLevel))
import           Test.Hls
import           Utils                                       as T

cabalPlugin :: PluginTestDescriptor Ide.Plugin.Cabal.Log
cabalPlugin = mkPluginTestDescriptor descriptor "cabal context"

contextTests :: TestTree
contextTests =
    testGroup
        "Context Tests "
        [ pathCompletionInfoFromCompletionContextTests
        , getContextTests
        ]

pathCompletionInfoFromCompletionContextTests :: TestTree
pathCompletionInfoFromCompletionContextTests =
    testGroup
        "Completion Info to Completion Context Tests"
        [ testCase "Current Directory" $ do
            let complInfo = pathCompletionInfoFromCabalPrefixInfo "" $ simpleCabalPrefixInfoFromFp "" testDataDir
            queryDirectory complInfo @?= "./"
        , testCase "Current Directory - partly written next" $ do
            let complInfo = pathCompletionInfoFromCabalPrefixInfo "" $ simpleCabalPrefixInfoFromFp "di" testDataDir
            queryDirectory complInfo @?= "./"
            pathSegment complInfo @?= "di"
        , testCase "Current Directory - alternative writing" $ do
            let complInfo = pathCompletionInfoFromCabalPrefixInfo "" $ simpleCabalPrefixInfoFromFp "./" testDataDir
            queryDirectory complInfo @?= "./"
        , testCase "Subdirectory" $ do
            let complInfo = pathCompletionInfoFromCabalPrefixInfo "" $ simpleCabalPrefixInfoFromFp "dir1/" testDataDir
            queryDirectory complInfo @?= "dir1/"
            pathSegment complInfo @?= ""
        , testCase "Subdirectory - partly written next" $ do
            let complInfo = pathCompletionInfoFromCabalPrefixInfo "" $ simpleCabalPrefixInfoFromFp "dir1/d" testDataDir
            queryDirectory complInfo @?= "dir1/"
            pathSegment complInfo @?= "d"
        , testCase "Subdirectory - partly written next" $ do
            let complInfo = pathCompletionInfoFromCabalPrefixInfo "" $ simpleCabalPrefixInfoFromFp "dir1/dir2/d" testDataDir
            queryDirectory complInfo @?= "dir1/dir2/"
            pathSegment complInfo @?= "d"
        ]

getContextTests :: TestTree
getContextTests =
    testGroup
        "Context Tests"
        [ testCase "Empty File - Start" $ do
            -- for a completely empty file, the context needs to
            -- be top level without a specified keyword
            ctx <- callGetContext (Position 0 0) "" [""]
            ctx @?= (TopLevel, None)
        , testCase "Cabal version keyword - no value, no space after :" $ do
            -- on a file, where the keyword is already written
            -- the context should still be toplevel but the keyword should be recognized
            ctx <- callGetContext (Position 0 14) "" ["cabal-version:"]
            ctx @?= (TopLevel, KeyWord "cabal-version:")
        , testCase "Cabal version keyword - cursor in keyword" $ do
            -- on a file, where the keyword is already written
            -- but the cursor is in the middle of the keyword,
            -- we are not in a keyword context
            ctx <- callGetContext (Position 0 5) "cabal" ["cabal-version:"]
            ctx @?= (TopLevel, None)
        , testCase "Cabal version keyword - no value, many spaces" $ do
            -- on a file, where the "cabal-version:" keyword is already written
            -- the context should still be top level but the keyword should be recognized
            ctx <- callGetContext (Position 0 45) ("") ["cabal-version:" <> T.replicate 50 " "]
            ctx @?= (TopLevel, KeyWord "cabal-version:")
        , testCase "Cabal version keyword - keyword partly written" $ do
            -- in the first line of the file, if the keyword
            -- has not been written completely, the keyword context
            -- should still be None
            ctx <- callGetContext (Position 0 5) "cabal" ["cabal"]
            ctx @?= (TopLevel, None)
        , testCase "Cabal version keyword - value partly written" $ do
            -- in the first line of the file, if the keyword
            -- has not been written completely, the keyword context
            -- should still be None
            ctx <- callGetContext (Position 0 17) "1." ["cabal-version: 1."]
            ctx @?= (TopLevel, KeyWord "cabal-version:")
        , testCase "Inside Stanza - no keyword" $ do
            -- on a file, where the library stanza has been defined
            -- but no keyword is defined afterwards, the stanza context should be recognized
            ctx <- callGetContext (Position 3 2) "" libraryStanzaData
            ctx @?= (Stanza "library" Nothing, None)
        , testCase "Inside Stanza - keyword, no value" $ do
            -- on a file, where the library stanza and a keyword
            -- has been defined, the keyword and stanza should be recognized
            ctx <- callGetContext (Position 4 21) "" libraryStanzaData
            ctx @?= (Stanza "library" Nothing, KeyWord "build-depends:")
        , expectFailBecause "While not valid, it is not that important to make the code more complicated for this" $
            testCase "Cabal version keyword - no value, next line" $ do
                -- if the cabal version keyword has been written but without a value,
                -- in the next line we still should be in top level context with no keyword
                -- since the cabal version keyword and value pair need to be in the same line
                ctx <- callGetContext (Position 1 2) "" ["cabal-version:", ""]
                ctx @?= (TopLevel, None)
        , testCase "Non-cabal-version keyword - no value, next line indentented position" $ do
            -- if a keyword, other than the cabal version keyword has been written
            -- with no value, in the next line we still should be in top level keyword context
            -- of the keyword with no value, since its value may be written in the next line
            ctx <- callGetContext (Position 2 4) "" topLevelData
            ctx @?= (TopLevel, KeyWord "name:")
        , testCase "Non-cabal-version keyword - no value, next line at start" $ do
            -- if a keyword, other than the cabal version keyword has been written
            -- with no value, in the next line we still should be in top level context
            -- but not the keyword's, since it is not viable to write a value for a
            -- keyword a the start of the next line
            ctx <- callGetContext (Position 2 0) "" topLevelData
            ctx @?= (TopLevel, None)
        , testCase "Toplevel after stanza partially written" $ do
            ctx <- callGetContext (Position 6 2) "ma" libraryStanzaData
            ctx @?= (TopLevel, None)
        , testCase "Non-cabal-version keyword - no value, multiple lines between" $ do
            -- if a keyword, other than the cabal version keyword has been written
            -- with no value, even with multiple lines in between we can still write the
            -- value corresponding to the keyword
            ctx <- callGetContext (Position 5 4) "" topLevelData
            ctx @?= (TopLevel, KeyWord "name:")
        , testCase "Keyword inside stanza - cursor indented more than keyword in next line" $ do
            -- if a keyword, other than the cabal version keyword has been written
            -- in a stanza context with no value, then the value may be written in the next line,
            -- when the cursor is indented more than the keyword
            ctx <- callGetContext (Position 5 8) "" libraryStanzaData
            ctx @?= (Stanza "library" Nothing, KeyWord "build-depends:")
        , testCase "Keyword inside stanza - cursor indented less than keyword in next line" $ do
            -- if a keyword, other than the cabal version keyword has been written
            -- in a stanza context with no value, then the value may not be written in the next line,
            -- when the cursor is indented less than the keyword
            ctx <- callGetContext (Position 5 2) "" libraryStanzaData
            ctx @?= (Stanza "library" Nothing, None)
        , testCase "Keyword inside stanza - cursor at start of next line" $ do
            -- in a stanza context with no value the value may not be written in the next line,
            -- when the cursor is not indented and we are in the top level context
            ctx <- callGetContext (Position 5 0) "" libraryStanzaData
            ctx @?= (TopLevel, None)
        , testCase "Top level - cursor in later line with partially written value" $ do
            ctx <- callGetContext (Position 5 13) "eee" topLevelData
            ctx @?= (TopLevel, KeyWord "name:")
        , testCase "Named Stanza" $ do
            ctx <- callGetContext (Position 2 18) "" executableStanzaData
            ctx @?= (Stanza "executable" (Just "exeName"), None)
        ]
  where
    callGetContext :: Position -> T.Text -> [T.Text] -> IO Context
    callGetContext pos pref ls = do
        runMaybeT (getContext mempty (simpleCabalPrefixInfoFromPos pos pref) (Rope.fromText $ T.unlines ls))
            >>= \case
                Nothing -> assertFailure "Context must be found"
                Just ctx -> pure ctx

-- ------------------------------------------------------------------------
-- Test Data
-- ------------------------------------------------------------------------

libraryStanzaData :: [T.Text]
libraryStanzaData =
    [ "cabal-version:      3.0"
    , "name:               simple-cabal"
    , "library "
    , "    default-language: Haskell98"
    , "    build-depends:    "
    , "           "
    , "ma  "
    ]

executableStanzaData :: [T.Text]
executableStanzaData =
    [ "cabal-version:      3.0"
    , "name:               simple-cabal"
    , "executable exeName"
    , "    default-language: Haskell2010"
    , "    hs-source-dirs: test/preprocessor"
    ]

topLevelData :: [T.Text]
topLevelData =
    [ "cabal-version:      3.0"
    , "name:"
    , ""
    , ""
    , ""
    , "          eee"
    ]