haskell-language-server-2.10.0.0: plugins/hls-semantic-tokens-plugin/test/SemanticTokensTest.hs
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE DisambiguateRecordFields #-}
{-# LANGUAGE OverloadedStrings #-}
import Control.Lens ((^.), (^?))
import Data.Aeson (KeyValue (..), Object)
import qualified Data.Aeson.KeyMap as KV
import Data.Default
import Data.Functor (void)
import qualified Data.List as T
import Data.Map.Strict as Map hiding (map)
import Data.String (fromString)
import Data.Text (Text)
import qualified Data.Text as Text
import qualified Data.Text.Utf16.Rope.Mixed as Rope
import Data.Version (Version (..))
import Development.IDE (Pretty)
import Development.IDE.Plugin.Test (WaitForIdeRuleResult (..))
import Ide.Plugin.SemanticTokens
import Ide.Plugin.SemanticTokens.Mappings
import Ide.Plugin.SemanticTokens.Types
import Ide.Types
import qualified Language.LSP.Protocol.Lens as L
import Language.LSP.Protocol.Types
import qualified Language.LSP.Test as Test
import Language.LSP.VFS (VirtualFile (..))
import System.FilePath
import System.Info (compilerVersion)
import Test.Hls
import qualified Test.Hls.FileSystem as FS
import Test.Hls.FileSystem (file, text)
testDataDir :: FilePath
testDataDir = "plugins" </> "hls-semantic-tokens-plugin" </> "test" </> "testdata" </> testVersionDir
mkFs :: [FS.FileTree] -> FS.VirtualFileTree
mkFs = FS.mkVirtualFileTree testDataDir
semanticTokensPlugin :: Test.Hls.PluginTestDescriptor SemanticLog
semanticTokensPlugin = Test.Hls.mkPluginTestDescriptor enabledSemanticDescriptor "SemanticTokens"
where
enabledSemanticDescriptor recorder plId =
let semanticDescriptor = Ide.Plugin.SemanticTokens.descriptor recorder plId
in semanticDescriptor
{ pluginConfigDescriptor =
(pluginConfigDescriptor semanticDescriptor)
{ configInitialGenericConfig =
(configInitialGenericConfig (pluginConfigDescriptor semanticDescriptor))
{ plcGlobalOn = True
}
}
}
-- if 9_10 and after we change the directory to the testdata/before_9_10 directory
-- if 9_10 and after we change the directory to the testdata/after_9_10 directory
testVersionDir :: FilePath
testVersionDir
| compilerVersion >= Version [9, 10] [] = "after_9_10"
| otherwise = "before_9_10"
goldenWithHaskellAndCapsOutPut :: (Pretty b) => Config -> PluginTestDescriptor b -> TestName -> FS.VirtualFileTree -> FilePath -> String -> (TextDocumentIdentifier -> Session String) -> TestTree
goldenWithHaskellAndCapsOutPut config plugin title tree path desc act =
goldenGitDiff title (FS.vftOriginalRoot tree </> path <.> desc) $
fromString
<$> ( runSessionWithServerInTmpDir config plugin tree $
do
doc <- openDoc (path <.> "hs") "haskell"
void waitForBuildQueue
act doc
)
goldenWithSemanticTokensWithDefaultConfig :: TestName -> FilePath -> TestTree
goldenWithSemanticTokensWithDefaultConfig title path =
goldenWithHaskellAndCapsOutPut
def
semanticTokensPlugin
title
(mkFs $ FS.directProject (path <.> "hs"))
path
"expected"
(docSemanticTokensString def)
docSemanticTokensString :: SemanticTokensConfig -> TextDocumentIdentifier -> Session String
docSemanticTokensString cf doc = do
xs <- map (lspTokenHsToken cf) <$> docLspSemanticTokensString doc
return $ unlines . map show $ xs
docLspSemanticTokensString :: (HasCallStack) => TextDocumentIdentifier -> Session [SemanticTokenOriginal Language.LSP.Protocol.Types.SemanticTokenTypes]
docLspSemanticTokensString doc = do
res <- Test.getSemanticTokens doc
textContent <- documentContents doc
let vfs = VirtualFile 0 0 (Rope.fromText textContent)
case res ^? Language.LSP.Protocol.Types._L of
Just tokens -> do
either (error . show) pure $ recoverLspSemanticTokens vfs tokens
_noTokens -> error "No tokens found"
-- | Pass a param and return the response from `semanticTokensFull`
-- getSemanticTokensFullDelta :: TextDocumentIdentifier -> Session _
getSemanticTokensFullDelta :: TextDocumentIdentifier -> Text -> Session (SemanticTokens |? (SemanticTokensDelta |? Null))
getSemanticTokensFullDelta doc lastResultId = do
let params = SemanticTokensDeltaParams Nothing Nothing doc lastResultId
rsp <- request SMethod_TextDocumentSemanticTokensFullDelta params
case rsp ^. L.result of
Right x -> return x
_ -> error "No tokens found"
semanticTokensClassTests :: TestTree
semanticTokensClassTests =
testGroup
"type class"
[ goldenWithSemanticTokensWithDefaultConfig "golden type class" "TClass",
goldenWithSemanticTokensWithDefaultConfig "imported class method InstanceClassMethodBind" "TInstanceClassMethodBind",
goldenWithSemanticTokensWithDefaultConfig "imported class method TInstanceClassMethodUse" "TInstanceClassMethodUse",
goldenWithSemanticTokensWithDefaultConfig "imported deriving" "TClassImportedDeriving"
]
semanticTokensValuePatternTests :: TestTree
semanticTokensValuePatternTests =
testGroup
"value and patterns "
[ goldenWithSemanticTokensWithDefaultConfig "value bind" "TValBind",
goldenWithSemanticTokensWithDefaultConfig "pattern match" "TPatternMatch",
goldenWithSemanticTokensWithDefaultConfig "pattern bind" "TPatternbind"
]
mkSemanticConfig :: Object -> Config
mkSemanticConfig setting = def {plugins = Map.insert "SemanticTokens" conf (plugins def)}
where
conf = def {plcConfig = setting}
directFile :: FilePath -> Text -> [FS.FileTree]
directFile fp content =
[ FS.directCradle [Text.pack fp],
file fp (text content)
]
semanticTokensConfigTest :: TestTree
semanticTokensConfigTest =
testGroup
"semantic token config test"
[ testCase "function to variable" $ do
let content = Text.unlines ["module Hello where", "go _ = 1"]
let fs = mkFs $ directFile "Hello.hs" content
let funcVar = KV.fromList ["functionToken" .= var]
var :: String
var = "variable"
Test.Hls.runSessionWithTestConfig def
{ testPluginDescriptor = semanticTokensPlugin
, testConfigSession = def
{ ignoreConfigurationRequests = False
}
, testConfigCaps = fullLatestClientCaps
, testDirLocation = Right fs
, testLspConfig = mkSemanticConfig funcVar
}
$ const $ do
-- modifySemantic funcVar
void waitForBuildQueue
doc <- openDoc "Hello.hs" "haskell"
void waitForBuildQueue
result1 <- docLspSemanticTokensString doc
liftIO $ unlines (map show result1) @?=
T.unlines (["1:8-13 SemanticTokenTypes_Namespace \"Hello\"" | compilerVersion >= Version [9, 10] []]
++ ["2:1-3 SemanticTokenTypes_Variable \"go\""])
]
semanticTokensFullDeltaTests :: TestTree
semanticTokensFullDeltaTests =
testGroup "semanticTokensFullDeltaTests"
[ testCase "null delta since unchanged" $ do
let file1 = "TModuleA.hs"
let expectDelta = InR (InL (SemanticTokensDelta (Just "1") []))
Test.Hls.runSessionWithServerInTmpDir def semanticTokensPlugin (mkFs $ FS.directProjectMulti [file1]) $ do
doc1 <- openDoc file1 "haskell"
_ <- waitForAction "TypeCheck" doc1
_ <- Test.getSemanticTokens doc1
delta <- getSemanticTokensFullDelta doc1 "0"
liftIO $ delta @?= expectDelta,
testCase "add tokens" $ do
let file1 = "TModuleA.hs"
let expectDelta
| compilerVersion >= Version [9, 10] [] = InR (InL (SemanticTokensDelta (Just "1") [SemanticTokensEdit 25 0 (Just [2, 0, 3, 8, 0])]))
| otherwise = InR (InL (SemanticTokensDelta (Just "1") [SemanticTokensEdit 20 0 (Just [2, 0, 3, 8, 0])]))
-- r c l t m
-- where r = row, c = column, l = length, t = token, m = modifier
Test.Hls.runSessionWithServerInTmpDir def semanticTokensPlugin (mkFs $ FS.directProjectMulti [file1]) $ do
doc1 <- openDoc file1 "haskell"
_ <- waitForAction "TypeCheck" doc1
_ <- Test.getSemanticTokens doc1
-- open the file and append a line to it
let change = TextDocumentContentChangeEvent $ InL TextDocumentContentChangePartial
{ _range = Range (Position 4 0) (Position 4 6)
, _rangeLength = Nothing
, _text = "foo = 1"
}
changeDoc doc1 [change]
_ <- waitForAction "TypeCheck" doc1
delta <- getSemanticTokensFullDelta doc1 "0"
liftIO $ delta @?= expectDelta,
testCase "remove tokens" $ do
let file1 = "TModuleA.hs"
let expectDelta
| compilerVersion >= Version [9, 10] [] = InR (InL (SemanticTokensDelta (Just "1") [SemanticTokensEdit 5 20 (Just [])]))
| otherwise = InR (InL (SemanticTokensDelta (Just "1") [SemanticTokensEdit 0 20 (Just [])]))
-- delete all tokens
Test.Hls.runSessionWithServerInTmpDir def semanticTokensPlugin (mkFs $ FS.directProjectMulti [file1]) $ do
doc1 <- openDoc file1 "haskell"
_ <- waitForAction "TypeCheck" doc1
_ <- Test.getSemanticTokens doc1
-- open the file and append a line to it
let change = TextDocumentContentChangeEvent $ InL TextDocumentContentChangePartial
{ _range = Range (Position 2 0) (Position 2 28)
, _rangeLength = Nothing
, _text = Text.replicate 28 " "
}
changeDoc doc1 [change]
_ <- waitForAction "TypeCheck" doc1
delta <- getSemanticTokensFullDelta doc1 "0"
liftIO $ delta @?= expectDelta
]
semanticTokensTests :: TestTree
semanticTokensTests =
testGroup "other semantic Token test"
[ testCase "module import test" $ do
let file1 = "TModuleA.hs"
let file2 = "TModuleB.hs"
Test.Hls.runSessionWithServerInTmpDir def semanticTokensPlugin (mkFs $ FS.directProjectMulti [file1, file2]) $ do
doc1 <- openDoc file1 "haskell"
doc2 <- openDoc file2 "haskell"
check1 <- waitForAction "TypeCheck" doc1
check2 <- waitForAction "TypeCheck" doc2
case check1 of
Right (WaitForIdeRuleResult _) -> return ()
Left _ -> error "TypeCheck1 failed"
case check2 of
Right (WaitForIdeRuleResult _) -> return ()
Left _ -> error "TypeCheck2 failed"
result <- docSemanticTokensString def doc2
let expect =
unlines
(
-- > 9.10 have module name in the token
(["1:8-16 TModule \"TModuleB\"" | compilerVersion >= Version [9, 10] []])
++
[
"3:8-16 TModule \"TModuleA\"",
"4:18-26 TModule \"TModuleA\"",
"6:1-3 TVariable \"go\"",
"6:6-10 TDataConstructor \"Game\"",
"8:1-5 TVariable \"a\\66560bb\"",
"8:8-17 TModule \"TModuleA.\"",
"8:17-20 TRecordField \"a\\66560b\"",
"8:21-23 TVariable \"go\""
])
liftIO $ result @?= expect,
goldenWithSemanticTokensWithDefaultConfig "mixed constancy test result generated from one ghc version" "T1",
goldenWithSemanticTokensWithDefaultConfig "pattern bind" "TPatternSynonym",
goldenWithSemanticTokensWithDefaultConfig "type family" "TTypefamily",
goldenWithSemanticTokensWithDefaultConfig "TUnicodeSyntax" "TUnicodeSyntax",
goldenWithSemanticTokensWithDefaultConfig "TQualifiedName" "TQualifiedName",
goldenWithSemanticTokensWithDefaultConfig "TDoc" "TDoc"
]
semanticTokensDataTypeTests :: TestTree
semanticTokensDataTypeTests =
testGroup
"get semantic Tokens"
[ goldenWithSemanticTokensWithDefaultConfig "simple datatype" "TDataType",
goldenWithSemanticTokensWithDefaultConfig "record" "TRecord",
goldenWithSemanticTokensWithDefaultConfig "record With DuplicateRecordFields" "TRecordDuplicateRecordFields",
goldenWithSemanticTokensWithDefaultConfig "datatype import" "TDatatypeImported",
goldenWithSemanticTokensWithDefaultConfig "datatype family" "TDataFamily",
goldenWithSemanticTokensWithDefaultConfig "GADT" "TGADT"
]
semanticTokensFunctionTests :: TestTree
semanticTokensFunctionTests =
testGroup
"get semantic of functions"
[ goldenWithSemanticTokensWithDefaultConfig "functions" "TFunction",
goldenWithSemanticTokensWithDefaultConfig "local functions" "TFunctionLocal",
goldenWithSemanticTokensWithDefaultConfig "functions under type synonym" "TFunctionUnderTypeSynonym",
goldenWithSemanticTokensWithDefaultConfig "function in let binding" "TFunctionLet",
goldenWithSemanticTokensWithDefaultConfig "negative case non-function with constraint" "TNoneFunctionWithConstraint",
goldenWithSemanticTokensWithDefaultConfig "TOperator" "TOperator"
]
main :: IO ()
main =
defaultTestRunner $
testGroup
"Semantic tokens"
[ semanticTokensTests,
semanticTokensClassTests,
semanticTokensDataTypeTests,
semanticTokensValuePatternTests,
semanticTokensFunctionTests,
semanticTokensConfigTest,
semanticTokensFullDeltaTests
]