packages feed

hls-eval-plugin-0.1.0.2: src/Ide/Plugin/Eval/Parse/Token.hs

{-# OPTIONS_GHC -Wwarn #-}

-- | Parse source code into a list of line Tokens.
module Ide.Plugin.Eval.Parse.Token (
    Token (..),
    TokenS,
    tokensFrom,
    unsafeContent,
    isStatement,
    isTextLine,
    isPropLine,
    isCodeLine,
    isBlockOpen,
    isBlockClose,
) where

import Control.Monad.Combinators (
    many,
    optional,
    skipManyTill,
    (<|>),
 )
import Data.Functor (($>))
import Data.List (foldl')
import Ide.Plugin.Eval.Parse.Parser (
    Parser,
    alphaNumChar,
    char,
    letterChar,
    runParser,
    satisfy,
    space,
    string,
    tillEnd,
 )
import Ide.Plugin.Eval.Types (
    Format (..),
    Language (..),
    Loc,
    Located (Located),
 )
import Maybes (fromJust, fromMaybe)

type TParser = Parser Char (State, [TokenS])

data State = InCode | InSingleComment | InMultiComment deriving (Eq, Show)

commentState :: Bool -> State
commentState True = InMultiComment
commentState False = InSingleComment

type TokenS = Token String

data Token s
    = -- | Text, without prefix "(--)? >>>"
      Statement s
    | -- | Text, without prefix "(--)? prop>"
      PropLine s
    | -- | Text inside a comment
      TextLine s
    | -- | Line of code (outside comments)
      CodeLine
    | -- | Open of comment
      BlockOpen {blockName :: Maybe s, blockLanguage :: Language, blockFormat :: Format}
    | -- | Close of multi-line comment
      BlockClose
    deriving (Eq, Show)

isStatement :: Token s -> Bool
isStatement (Statement _) = True
isStatement _ = False

isTextLine :: Token s -> Bool
isTextLine (TextLine _) = True
isTextLine _ = False

isPropLine :: Token s -> Bool
isPropLine (PropLine _) = True
isPropLine _ = False

isCodeLine :: Token s -> Bool
isCodeLine CodeLine = True
isCodeLine _ = False

isBlockOpen :: Token s -> Bool
isBlockOpen (BlockOpen _ _ _) = True
isBlockOpen _ = False

isBlockClose :: Token s -> Bool
isBlockClose BlockClose = True
isBlockClose _ = False

unsafeContent :: Token a -> a
unsafeContent = fromJust . contentOf

contentOf :: Token a -> Maybe a
contentOf (Statement c) = Just c
contentOf (PropLine c) = Just c
contentOf (TextLine c) = Just c
contentOf _ = Nothing

{- | Parse source code and return a list of located Tokens
>>> import           Ide.Plugin.Eval.Types        (unLoc)
>>> tks src = map unLoc . tokensFrom  <$> readFile src

>>> tks "test/testdata/eval/T1.hs"
[CodeLine,CodeLine,CodeLine,CodeLine,BlockOpen {blockName = Nothing, blockLanguage = Plain, blockFormat = SingleLine},Statement " unwords example",CodeLine,CodeLine]

>>> tks "test/testdata/eval/TLanguageOptions.hs"
[BlockOpen {blockName = Nothing, blockLanguage = Plain, blockFormat = SingleLine},TextLine "Support for language options",CodeLine,CodeLine,CodeLine,CodeLine,BlockOpen {blockName = Nothing, blockLanguage = Plain, blockFormat = SingleLine},TextLine "Language options set in the module source (ScopedTypeVariables)",TextLine "also apply to tests so this works fine",Statement " f = (\\(c::Char) -> [c])",CodeLine,BlockOpen {blockName = Nothing, blockLanguage = Plain, blockFormat = MultiLine},TextLine "Multiple options can be set with a single `:set`",TextLine "",Statement " :set -XMultiParamTypeClasses -XFlexibleInstances",Statement " class Z a b c",BlockClose,CodeLine,BlockOpen {blockName = Nothing, blockLanguage = Plain, blockFormat = MultiLine},TextLine "",TextLine "",TextLine "Options apply only in the section where they are defined (unless they are in the setup section), so this will fail:",TextLine "",Statement " class L a b c",BlockClose,CodeLine,CodeLine,BlockOpen {blockName = Nothing, blockLanguage = Plain, blockFormat = MultiLine},TextLine "",TextLine "Options apply to all tests in the same section after their declaration.",TextLine "",TextLine "Not set yet:",TextLine "",Statement " class D",TextLine "",TextLine "Now it works:",TextLine "",Statement ":set -XMultiParamTypeClasses",Statement " class C",TextLine "",TextLine "It still works",TextLine "",Statement " class F",BlockClose,CodeLine,BlockOpen {blockName = Nothing, blockLanguage = Plain, blockFormat = MultiLine},TextLine "Wrong option names are reported.",Statement " :set -XWrong",BlockClose]
-}
tokensFrom :: String -> [Loc (Token String)]
tokensFrom = tokens . lines . filter (/= '\r')

{- |
>>> tokens ["-- |$setup >>> 4+7","x=11"]
[Located {location = 0, located = BlockOpen {blockName = Just "setup", blockLanguage = Haddock, blockFormat = SingleLine}},Located {location = 0, located = Statement " 4+7"},Located {location = 1, located = CodeLine}]

>>> tokens ["-- $start"]
[Located {location = 0, located = BlockOpen {blockName = Just "start", blockLanguage = Plain, blockFormat = SingleLine}},Located {location = 0, located = TextLine ""}]

>>> tokens ["--","-- >>> 4+7"]
[Located {location = 0, located = BlockOpen {blockName = Nothing, blockLanguage = Plain, blockFormat = SingleLine}},Located {location = 0, located = TextLine ""},Located {location = 1, located = Statement " 4+7"}]

>>> tokens ["-- |$setup  44","-- >>> 4+7"]
[Located {location = 0, located = BlockOpen {blockName = Just "setup", blockLanguage = Haddock, blockFormat = SingleLine}},Located {location = 0, located = TextLine "44"},Located {location = 1, located = Statement " 4+7"}]

>>> tokens ["{"++"- |$doc",">>> 2+2","4","prop> x-x==0","--minus","-"++"}"]
[Located {location = 0, located = BlockOpen {blockName = Just "doc", blockLanguage = Haddock, blockFormat = MultiLine}},Located {location = 0, located = TextLine ""},Located {location = 1, located = Statement " 2+2"},Located {location = 2, located = TextLine "4"},Located {location = 3, located = PropLine " x-x==0"},Located {location = 4, located = TextLine "--minus"},Located {location = 5, located = BlockClose}]

Multi lines, closed on following line:

>>> tokens ["{"++"-","-"++"}"]
[Located {location = 0, located = BlockOpen {blockName = Nothing, blockLanguage = Plain, blockFormat = MultiLine}},Located {location = 0, located = TextLine ""},Located {location = 1, located = BlockClose}]

>>> tokens [" {"++"-","-"++"} "]
[Located {location = 0, located = BlockOpen {blockName = Nothing, blockLanguage = Plain, blockFormat = MultiLine}},Located {location = 0, located = TextLine ""},Located {location = 1, located = BlockClose}]

>>> tokens ["{"++"- SOME TEXT "," MORE -"++"}"]
[Located {location = 0, located = BlockOpen {blockName = Nothing, blockLanguage = Plain, blockFormat = MultiLine}},Located {location = 0, located = TextLine "SOME TEXT "},Located {location = 1, located = BlockClose}]

Multi lines, closed on the same line:

>>> tokens $ ["{","--"++"}"]
[Located {location = 0, located = CodeLine}]

>>> tokens $ ["  {","- IGNORED -","}  "]
[Located {location = 0, located = CodeLine}]

>>> tokens ["{","-# LANGUAGE TupleSections","#-","}"]
[Located {location = 0, located = CodeLine},Located {location = 1, located = CodeLine}]

>>> tokens []
[]
-}
tokens :: [String] -> [Loc TokenS]
tokens = concatMap (\(l, vs) -> map (Located l) vs) . zip [0 ..] . reverse . snd . foldl' next (InCode, [])
  where
    next (st, tokens) ln = case runParser (aline st) ln of
        Right (st', tokens') -> (st', tokens' : tokens)
        Left err -> error $ unwords ["Tokens.next failed to parse", ln, err]

-- | Parse a line of input
aline :: State -> TParser
aline InCode = optionStart <|> multi <|> singleOpen <|> codeLine
aline InSingleComment = optionStart <|> multi <|> commentLine False <|> codeLine
aline InMultiComment = multiClose <|> commentLine True

multi :: TParser
multi = multiOpenClose <|> multiOpen

codeLine :: TParser
codeLine = (InCode, [CodeLine]) <$ tillEnd

{- | A multi line comment that starts and ends on the same line.

>>> runParser multiOpenClose $ concat ["{","--","}"]
Right (InCode,[CodeLine])

>>> runParser multiOpenClose $ concat [" {","-| >>> IGNORED -","} "]
Right (InCode,[CodeLine])
-}
multiOpenClose :: TParser
multiOpenClose = (multiStart >> multiClose) $> (InCode, [CodeLine])

{- | Parses the opening of a multi line comment.
>>> runParser multiOpen $ "{"++"- $longSection this is also parsed"
Right (InMultiComment,[BlockOpen {blockName = Just "longSection", blockLanguage = Plain, blockFormat = MultiLine},TextLine "this is also parsed"])

>>> runParser multiOpen $ "{"++"- $longSection >>> 2+3"
Right (InMultiComment,[BlockOpen {blockName = Just "longSection", blockLanguage = Plain, blockFormat = MultiLine},Statement " 2+3"])
-}
multiOpen :: TParser
multiOpen =
    ( \() (maybeLanguage, maybeName) tk ->
        (InMultiComment, [BlockOpen maybeName (defLang maybeLanguage) MultiLine, tk])
    )
        <$> multiStart
        <*> languageAndName
        <*> commentRest

{- | Parse the first line of a sequence of single line comments
>>> runParser singleOpen "-- |$doc >>>11"
Right (InSingleComment,[BlockOpen {blockName = Just "doc", blockLanguage = Haddock, blockFormat = SingleLine},Statement "11"])
-}
singleOpen :: TParser
singleOpen =
    ( \() (maybeLanguage, maybeName) tk ->
        (InSingleComment, [BlockOpen maybeName (defLang maybeLanguage) SingleLine, tk])
    )
        <$> singleStart
        <*> languageAndName
        <*> commentRest

{- | Parse a line in a comment
>>> runParser (commentLine False) "x=11"
Left "No match"

>>> runParser (commentLine False) "-- >>>11"
Right (InSingleComment,[Statement "11"])

>>> runParser (commentLine True) "-- >>>11"
Right (InMultiComment,[TextLine "-- >>>11"])
-}
commentLine :: Bool -> TParser
commentLine noPrefix =
    (\tk -> (commentState noPrefix, [tk])) <$> (optLineStart noPrefix *> commentBody)

commentRest :: Parser Char (Token [Char])
commentRest = many space *> commentBody

commentBody :: Parser Char (Token [Char])
commentBody = stmt <|> prop <|> txt
  where
    txt = TextLine <$> tillEnd
    stmt = Statement <$> (string ">>>" *> tillEnd)
    prop = PropLine <$> (string "prop>" *> tillEnd)

-- | Remove comment line prefix, if needed
optLineStart :: Bool -> Parser Char ()
optLineStart noPrefix
    | noPrefix = pure ()
    | otherwise = singleStart

singleStart :: Parser Char ()
singleStart = (string "--" *> optional space) $> ()

multiStart :: Parser Char ()
multiStart = sstring "{-" $> ()

{- Parse the close of a multi-line comment
>>> runParser multiClose $ "-"++"}"
Right (InCode,[BlockClose])

>>> runParser multiClose $ "-"++"}  "
Right (InCode,[BlockClose])

As there is currently no way of handling tests in the final line of a multi line comment, it ignores anything that precedes the closing marker:

>>> runParser multiClose $ "IGNORED -"++"}  "
Right (InCode,[BlockClose])
-}
multiClose :: TParser
multiClose = skipManyTill (satisfy (const True)) (string "-}" *> many space) >> return (InCode, [BlockClose])

optionStart :: Parser Char (State, [Token s])
optionStart = (string "{-#" *> tillEnd) $> (InCode, [CodeLine])

name :: Parser Char [Char]
name = (:) <$> letterChar <*> many (alphaNumChar <|> char '_')

sstring :: String -> Parser Char [Char]
sstring s = many space *> string s *> many space

{- |
>>>runParser languageAndName "|$"
Right (Just Haddock,Just "")

>>>runParser languageAndName "|$start"
Right (Just Haddock,Just "start")

>>>runParser languageAndName "| $start"
Right (Just Haddock,Just "start")

>>>runParser languageAndName "^"
Right (Just Haddock,Nothing)

>>>runParser languageAndName "$start"
Right (Nothing,Just "start")
-}
languageAndName :: Parser Char (Maybe Language, Maybe String)
languageAndName =
    (,) <$> optional ((char '|' <|> char '^') >> pure Haddock)
        <*> optional
            (char '$' *> (fromMaybe "" <$> optional name))

defLang :: Maybe Language -> Language
defLang = fromMaybe Plain