rzk-0.11.0: test/Rzk/SemanticTokensSpec.hs
{-# LANGUAGE CPP #-}
{-# LANGUAGE OverloadedStrings #-}
-- | Tests for the LSP presentation layer: the merged semantic tokens
-- (AST-based plus lexer-based) and the hover signature formatting.
module Rzk.SemanticTokensSpec (spec) where
import Test.Hspec
#ifdef LSP_ENABLED
import qualified Data.Text as T
import Language.LSP.Protocol.Types (SemanticTokenAbsolute (..),
SemanticTokenModifiers (..),
SemanticTokenTypes (..))
import Language.Rzk.Syntax (parseModule, parseTerm)
import Language.Rzk.VSCode.Handlers (formatSignature, useSiteTokens)
import qualified Language.Rzk.VSCode.ReferenceIndex as RI
import Language.Rzk.VSCode.Tokenize (mergeTokens, tokenizeModule,
tokenizeSyntaxSymbols)
import Rzk.TypeCheck (Context (..), Verbosity (..),
checkedModules, declViews,
emptyContext)
-- | Merged tokens of a module, as 'provideSemanticTokens' produces them.
tokensOf :: T.Text -> [SemanticTokenAbsolute]
tokensOf src = case parseModule src of
Left err -> error ("parse error: " <> T.unpack err)
Right m -> mergeTokens (tokenizeModule m) (tokenizeSyntaxSymbols src)
-- | The token type starting at a (0-based) position, if any.
tokenAt :: [SemanticTokenAbsolute] -> (Int, Int) -> Maybe SemanticTokenTypes
tokenAt toks (l, c) = case
[ _tokenType t
| t <- toks
, _line t == fromIntegral l, _startChar t == fromIntegral c
] of
(tt : _) -> Just tt
[] -> Nothing
exampleModule :: T.Text
exampleModule = T.unlines
[ "#lang rzk-1" -- 0
, "#define weird uses (A) (x : A) : A := x" -- 1
, "#check TOP : TOPE" -- 2
]
-- | The use-site overlay of a one-file project: typechecked declarations
-- plus the reference index, as 'provideSemanticTokens' combines them.
useSiteTokensOf :: T.Text -> [SemanticTokenAbsolute]
useSiteTokensOf src = case parseModule src of
Left err -> error ("parse error: " <> T.unpack err)
Right m -> case checkedModules [(path, m)] silentContext of
Left _err -> error "unexpected type error in use-site fixture"
Right (checked, _holes) ->
useSiteTokens (declViews checked) (RI.indexModules [(path, m)]) path
where
path = "use-site.rzk"
silentContext = emptyContext { ctxVerbosity = Silent }
-- | The token type and modifiers starting at a (0-based) position, if any.
useTokenAt :: [SemanticTokenAbsolute] -> (Int, Int) -> Maybe (SemanticTokenTypes, [SemanticTokenModifiers])
useTokenAt toks (l, c) = case
[ (_tokenType t, _tokenModifiers t)
| t <- toks
, _line t == fromIntegral l, _startChar t == fromIntegral c
] of
(tt : _) -> Just tt
[] -> Nothing
dataModule :: T.Text
dataModule = T.unlines
[ "#lang rzk-1" -- 0
, "#data bool := false | true" -- 1
, "#define not (b : bool) : bool" -- 2
, " := rec-bool bool true false b" -- 3
, "#define shadow (false : bool) : bool := false" -- 4
]
postulateModule :: T.Text
postulateModule = T.unlines
[ "#lang rzk-1" -- 0
, "#postulate ax (A : U) (a : A) : A" -- 1
, "#define use-ax (A : U) (a : A) : A := ax A a" -- 2
, "#assume hyp : U" -- 3
, "#define use-hyp : U := hyp" -- 4
, "#section local" -- 5
, "#assume sec : U" -- 6
, "#define use-sec : U := sec" -- 7
, "#end local" -- 8
]
spec :: Spec
spec = do
describe "use-site tokens" $ do
let toks = useSiteTokensOf dataModule
it "colour a constructor occurrence as an enum member" $
useTokenAt toks (3, 19) `shouldBe` Just (SemanticTokenTypes_EnumMember, [])
it "colour a data type occurrence as a class" $ do
useTokenAt toks (3, 14) `shouldBe` Just (SemanticTokenTypes_Class, [])
useTokenAt toks (2, 17) `shouldBe` Just (SemanticTokenTypes_Class, [])
it "colour a generated eliminator occurrence as a library function" $
useTokenAt toks (3, 5)
`shouldBe` Just (SemanticTokenTypes_Function, [SemanticTokenModifiers_DefaultLibrary])
it "leave a local that shadows a constructor plain" $
-- the body of shadow uses its parameter, not the constructor
useTokenAt toks (4, 40) `shouldBe` Nothing
it "leave plain definitions to the other token sources" $
useTokenAt toks (3, 30) `shouldBe` Nothing -- b, a binder use
it "colour a postulate occurrence as an abstract static function" $ do
let ptoks = useSiteTokensOf postulateModule
useTokenAt ptoks (2, 38) -- ax, in use-ax
`shouldBe` Just ( SemanticTokenTypes_Function
, [SemanticTokenModifiers_Abstract, SemanticTokenModifiers_Static] )
it "colour a top-level-assumption occurrence as an abstract function" $ do
let ptoks = useSiteTokensOf postulateModule
useTokenAt ptoks (4, 23) -- hyp, in use-hyp
`shouldBe` Just (SemanticTokenTypes_Function, [SemanticTokenModifiers_Abstract])
it "colour an in-section-assumption occurrence as an abstract parameter" $ do
let ptoks = useSiteTokensOf postulateModule
useTokenAt ptoks (7, 23) -- sec, in use-sec
`shouldBe` Just (SemanticTokenTypes_Parameter, [SemanticTokenModifiers_Abstract])
describe "semantic tokens" $ do
let toks = tokensOf exampleModule
positions = [ (_line t, _startChar t) | t <- toks ]
it "are sorted by position, as the LSP delta encoding requires" $
-- Regression: tokens of a uses-clause used to be emitted before the
-- declaration name.
and (zipWith (<) positions (drop 1 positions)) `shouldBe` True
it "classify commands, keywords, and operators from the lexer" $ do
tokenAt toks (1, 0) `shouldBe` Just SemanticTokenTypes_Macro -- #define
tokenAt toks (1, 14) `shouldBe` Just SemanticTokenTypes_Keyword -- uses
tokenAt toks (1, 26) `shouldBe` Just SemanticTokenTypes_Operator -- :
tokenAt toks (1, 35) `shouldBe` Just SemanticTokenTypes_Operator -- :=
it "let AST-based tokens win over lexer tokens on overlap" $ do
tokenAt toks (1, 8) `shouldBe` Just SemanticTokenTypes_Function -- weird (declaration)
tokenAt toks (2, 7) `shouldBe` Just SemanticTokenTypes_String -- TOP (tope literal, not keyword)
it "skip plain brackets" $
tokenAt toks (1, 23) `shouldBe` Nothing -- (
it "survive files that do not parse" $
tokenizeSyntaxSymbols "#lang rzk-1\n#define broken (x : A) :=\n"
`shouldNotBe` []
it "mark holes, including named ones" $ do
let toks' = tokensOf $ T.unlines
[ "#lang rzk-1" -- 0
, "#define gap (A : U) : A := ?" -- 1
, "#define named (A : U) : A := ?goal" -- 2
]
tokenAt toks' (1, 27) `shouldBe` Just SemanticTokenTypes_Regexp -- ?
tokenAt toks' (2, 29) `shouldBe` Just SemanticTokenTypes_Regexp -- ?goal
it "mark postulate and assume declarations as abstract, by severity" $ do
let ptoks = tokensOf postulateModule
useTokenAt ptoks (1, 11) -- ax, declared
`shouldBe` Just ( SemanticTokenTypes_Function
, [ SemanticTokenModifiers_Declaration
, SemanticTokenModifiers_Abstract
, SemanticTokenModifiers_Static ] )
useTokenAt ptoks (3, 8) -- hyp, assumed
`shouldBe` Just ( SemanticTokenTypes_Function
, [SemanticTokenModifiers_Declaration, SemanticTokenModifiers_Abstract] )
useTokenAt ptoks (6, 8) -- sec, assumed in a section
`shouldBe` Just ( SemanticTokenTypes_Parameter
, [SemanticTokenModifiers_Declaration, SemanticTokenModifiers_Abstract] )
it "mark holes even in files that do not parse" $
tokenizeSyntaxSymbols "#lang rzk-1\n#define broken (x : A) := ?\n"
`shouldSatisfy` any ((== SemanticTokenTypes_Regexp) . _tokenType)
describe "formatSignature" $ do
let fmt name src = case parseTerm (T.pack src) of
Left err -> error ("parse error: " <> T.unpack err)
Right t -> formatSignature name t
it "keeps short types on one line" $
fmt "x" "A -> B" `shouldBe` "x : A -> B"
it "splits long function types one parameter per line" $
fmt "long" "(A : U) -> (B : A -> U) -> (C : (x : A) -> B x -> U) -> ((x : A) -> B x) -> U"
`shouldBe` unlines'
[ "long"
, " : (A : U)"
, " → (B : A -> U)"
, " → (C : (x : A) -> B x -> U)"
, " → ((x : A) -> B x)"
, " → U"
]
it "keeps long non-function types on one line" $ do
let sigma = "Sigma (f : (x : A) -> (y : A) -> hom A x y -> hom A x y), (x : A) -> U"
fmt "s" sigma `shouldBe` ("s : " <> sigma)
where
unlines' = foldr1 (\l r -> l <> "\n" <> r)
#else
spec :: Spec
spec = describe "semantic tokens" (pure ())
#endif