Commit ef5e1de2 authored by Andrew Gibiansky's avatar Andrew Gibiansky

Merge pull request #313 from razzius/allow_pragmas

Implement pragma handling
parents 8d2ccf25 34acef3c
...@@ -67,7 +67,7 @@ data ParseOutput a ...@@ -67,7 +67,7 @@ data ParseOutput a
-- | Store locations along with a value. -- | Store locations along with a value.
data Located a = Located { data Located a = Located {
line :: LineNumber, -- Where this element is located. line :: LineNumber, -- Where this element is located.
unloc :: a -- Located element. unloc :: a -- Located element.
} deriving (Eq, Show, Functor) } deriving (Eq, Show, Functor)
...@@ -101,17 +101,17 @@ runParser flags (Parser parserType parser) str = ...@@ -101,17 +101,17 @@ runParser flags (Parser parserType parser) str =
toParseOut $ unP parser parseState toParseOut $ unP parser parseState
where where
toParseOut :: ParseResult a -> ParseOutput a toParseOut :: ParseResult a -> ParseOutput a
toParseOut (PFailed span@(RealSrcSpan realSpan) err) = toParseOut (PFailed span@(RealSrcSpan realSpan) err) =
let errMsg = printErrorBag $ unitBag $ mkPlainErrMsg flags span err let errMsg = printErrorBag $ unitBag $ mkPlainErrMsg flags span err
line = srcLocLine $ realSrcSpanStart realSpan line = srcLocLine $ realSrcSpanStart realSpan
col = srcLocCol $ realSrcSpanStart realSpan col = srcLocCol $ realSrcSpanStart realSpan
in Failure errMsg $ Loc line col in Failure errMsg $ Loc line col
toParseOut (PFailed span err) = toParseOut (PFailed span err) =
let errMsg = printErrorBag $ unitBag $ mkPlainErrMsg flags span err let errMsg = printErrorBag $ unitBag $ mkPlainErrMsg flags span err
in Failure errMsg $ Loc 0 0 in Failure errMsg $ Loc 0 0
toParseOut (POk parseState result) = toParseOut (POk parseState result) =
let parseEnd = realSrcSpanStart $ last_loc parseState let parseEnd = realSrcSpanStart $ last_loc parseState
endLine = srcLocLine parseEnd endLine = srcLocLine parseEnd
endCol = srcLocCol parseEnd endCol = srcLocCol parseEnd
...@@ -126,7 +126,7 @@ runParser flags (Parser parserType parser) str = ...@@ -126,7 +126,7 @@ runParser flags (Parser parserType parser) str =
-- | Split a string at a given line and column. The column is included in -- | Split a string at a given line and column. The column is included in
-- the second part of the split. -- the second part of the split.
splitAtLoc :: LineNumber -> ColumnNumber -> String -> (String, String) splitAtLoc :: LineNumber -> ColumnNumber -> String -> (String, String)
splitAtLoc line col string = splitAtLoc line col string =
if line > length (lines string) if line > length (lines string)
then (string, "") then (string, "")
else (before, after) else (before, after)
...@@ -145,7 +145,7 @@ joinLines = intercalate "\n" ...@@ -145,7 +145,7 @@ joinLines = intercalate "\n"
-- | Split an input string into chunks based on indentation. -- | Split an input string into chunks based on indentation.
-- A chunk is a line and all lines immediately following that are indented -- A chunk is a line and all lines immediately following that are indented
-- beyond the indentation of the first line. This parses Haskell layout -- beyond the indentation of the first line. This parses Haskell layout
-- rules properly, and allows using multiline expressions via indentation. -- rules properly, and allows using multiline expressions via indentation.
layoutChunks :: String -> [Located String] layoutChunks :: String -> [Located String]
layoutChunks = go 1 layoutChunks = go 1
where where
...@@ -164,16 +164,16 @@ layoutChunks = go 1 ...@@ -164,16 +164,16 @@ layoutChunks = go 1
layoutLines _ [] = [] layoutLines _ [] = []
-- Use the indent of the first line to find the end of the first block. -- Use the indent of the first line to find the end of the first block.
layoutLines lineIdx all@(firstLine:rest) = layoutLines lineIdx all@(firstLine:rest) =
let firstIndent = indentLevel firstLine let firstIndent = indentLevel firstLine
blockEnded line = indentLevel line <= firstIndent in blockEnded line = indentLevel line <= firstIndent in
case findIndex blockEnded rest of case findIndex blockEnded rest of
-- If the first block doesn't end, return the whole string, since -- If the first block doesn't end, return the whole string, since
-- that just means the block takes up the entire string. -- that just means the block takes up the entire string.
Nothing -> [Located lineIdx $ intercalate "\n" all] Nothing -> [Located lineIdx $ intercalate "\n" all]
-- We found the end of the block. Split this bit out and recurse. -- We found the end of the block. Split this bit out and recurse.
Just idx -> Just idx ->
let (before, after) = splitAt idx rest in let (before, after) = splitAt idx rest in
Located lineIdx (joinLines $ firstLine:before) : go (lineIdx + idx + 1) (joinLines after) Located lineIdx (joinLines $ firstLine:before) : go (lineIdx + idx + 1) (joinLines after)
...@@ -183,7 +183,7 @@ layoutChunks = go 1 ...@@ -183,7 +183,7 @@ layoutChunks = go 1
-- Count a tab as two spaces. -- Count a tab as two spaces.
indentLevel ('\t':str) = 2 + indentLevel str indentLevel ('\t':str) = 2 + indentLevel str
-- Count empty lines as a large indent level, so they're always with the previous expression. -- Count empty lines as a large indent level, so they're always with the previous expression.
indentLevel "" = 100000 indentLevel "" = 100000
...@@ -192,7 +192,7 @@ layoutChunks = go 1 ...@@ -192,7 +192,7 @@ layoutChunks = go 1
-- | Drop comments from Haskell source. -- | Drop comments from Haskell source.
-- Simply gets rid of them, does not replace them in any way. -- Simply gets rid of them, does not replace them in any way.
removeComments :: String -> String removeComments :: String -> String
removeComments = removeOneLineComments . removeMultilineComments 0 removeComments = removeOneLineComments . removeMultilineComments 0 0
where where
removeOneLineComments str = removeOneLineComments str =
case str of case str of
...@@ -200,7 +200,7 @@ removeComments = removeOneLineComments . removeMultilineComments 0 ...@@ -200,7 +200,7 @@ removeComments = removeOneLineComments . removeMultilineComments 0
':':'!':remaining ->":!" ++ takeLine remaining ++ dropLine remaining ':':'!':remaining ->":!" ++ takeLine remaining ++ dropLine remaining
-- Handle strings. -- Handle strings.
'"':remaining -> '"':remaining ->
let quoted = takeString remaining let quoted = takeString remaining
len = length quoted in len = length quoted in
'"':quoted ++ removeOneLineComments (drop len remaining) '"':quoted ++ removeOneLineComments (drop len remaining)
...@@ -211,31 +211,40 @@ removeComments = removeOneLineComments . removeMultilineComments 0 ...@@ -211,31 +211,40 @@ removeComments = removeOneLineComments . removeMultilineComments 0
where where
dropLine = removeOneLineComments . dropWhile (/= '\n') dropLine = removeOneLineComments . dropWhile (/= '\n')
removeMultilineComments nesting str = removeMultilineComments nesting pragmaNesting str =
case str of case str of
-- Don't remove comments after cmd directives -- Don't remove comments after cmd directives
':':'!':remaining ->":!" ++ takeLine remaining ++ ':':'!':remaining ->":!" ++ takeLine remaining ++
removeMultilineComments nesting (dropWhile (/= '\n') remaining) removeMultilineComments nesting pragmaNesting (dropWhile (/= '\n') remaining)
-- Handle strings. -- Handle strings.
'"':remaining -> '"':remaining ->
if nesting == 0 if nesting == 0
then then
let quoted = takeString remaining let quoted = takeString remaining
len = length quoted in len = length quoted in
'"':quoted ++ removeMultilineComments nesting (drop len remaining) '"':quoted ++ removeMultilineComments nesting pragmaNesting (drop len remaining)
else else
removeMultilineComments nesting remaining removeMultilineComments nesting pragmaNesting remaining
'{':'-':'#':remaining ->
'{':'-':remaining -> removeMultilineComments (nesting + 1) remaining if nesting == 0
'-':'}':remaining -> then "{-#" ++ removeMultilineComments nesting (pragmaNesting + 1) remaining
else removeMultilineComments nesting pragmaNesting remaining
'#':'-':'}':remaining ->
if nesting == 0
then if pragmaNesting > 0
then '#':'-':'}':removeMultilineComments nesting (pragmaNesting - 1) remaining
else '#':'-':'}':removeMultilineComments nesting pragmaNesting remaining
else removeMultilineComments nesting pragmaNesting remaining
'{':'-':remaining -> removeMultilineComments (nesting + 1) pragmaNesting remaining
'-':'}':remaining ->
if nesting > 0 if nesting > 0
then removeMultilineComments (nesting - 1) remaining then removeMultilineComments (nesting - 1) pragmaNesting remaining
else '-':'}':removeMultilineComments nesting remaining else '-':'}':removeMultilineComments nesting pragmaNesting remaining
x:xs -> x:xs ->
if nesting > 0 if nesting > 0
then removeMultilineComments nesting xs then removeMultilineComments nesting pragmaNesting xs
else x:removeMultilineComments nesting xs else x:removeMultilineComments nesting pragmaNesting xs
[] -> [] [] -> []
takeLine = takeWhile (/= '\n') takeLine = takeWhile (/= '\n')
......
...@@ -13,7 +13,7 @@ import ClassyPrelude hiding (init, last, liftIO, head, hGetContents, tail, try) ...@@ -13,7 +13,7 @@ import ClassyPrelude hiding (init, last, liftIO, head, hGetContents, tail, try)
import Control.Concurrent (forkIO, threadDelay) import Control.Concurrent (forkIO, threadDelay)
import Prelude (putChar, head, tail, last, init, (!!)) import Prelude (putChar, head, tail, last, init, (!!))
import Data.List.Utils import Data.List.Utils
import Data.List(findIndex, and) import Data.List (findIndex, and)
import Data.String.Utils import Data.String.Utils
import Text.Printf import Text.Printf
import Data.Char as Char import Data.Char as Char
...@@ -258,7 +258,7 @@ evaluate kernelState code output = do ...@@ -258,7 +258,7 @@ evaluate kernelState code output = do
-- Merge them with normal display outputs. -- Merge them with normal display outputs.
dispsIO <- extractValue "IHaskell.Display.displayFromChan" dispsIO <- extractValue "IHaskell.Display.displayFromChan"
dispsMay <- liftIO dispsIO dispsMay <- liftIO dispsIO
let result = let result =
case dispsMay of case dispsMay of
Nothing -> evalResult evalOut Nothing -> evalResult evalOut
Just disps -> evalResult evalOut <> disps Just disps -> evalResult evalOut <> disps
...@@ -415,7 +415,7 @@ evalCommand output (Directive SetDynFlag flags) state = ...@@ -415,7 +415,7 @@ evalCommand output (Directive SetDynFlag flags) state =
-- For -XNoImplicitPrelude, remove the Prelude import. -- For -XNoImplicitPrelude, remove the Prelude import.
-- For -XImplicitPrelude, add it back in. -- For -XImplicitPrelude, add it back in.
case flag of case flag of
"-XNoImplicitPrelude" -> "-XNoImplicitPrelude" ->
evalImport "import qualified Prelude as Prelude" evalImport "import qualified Prelude as Prelude"
"-XImplicitPrelude" -> do "-XImplicitPrelude" -> do
importDecl <- parseImportDecl "import Prelude" importDecl <- parseImportDecl "import Prelude"
...@@ -881,6 +881,13 @@ evalCommand _ (ParseError loc err) state = do ...@@ -881,6 +881,13 @@ evalCommand _ (ParseError loc err) state = do
evalComms = [] evalComms = []
} }
evalCommand _ (Pragma (PragmaUnsupported pragmaType) pragmas) state = wrapExecution state $
return $ displayError $ "Pragmas of type " ++ pragmaType ++
"\nare not supported."
evalCommand output (Pragma PragmaLanguage pragmas) state = do
write $ "Got LANGUAGE pragma " ++ show pragmas
evalCommand output (Directive SetExtension $ unwords pragmas) state
hoogleResults :: KernelState -> [Hoogle.HoogleResult] -> EvalOut hoogleResults :: KernelState -> [Hoogle.HoogleResult] -> EvalOut
hoogleResults state results = EvalOut { hoogleResults state results = EvalOut {
...@@ -986,7 +993,7 @@ keepingItVariable act = do ...@@ -986,7 +993,7 @@ keepingItVariable act = do
var name = name ++ rand var name = name ++ rand
goStmt s = runStmt s RunToCompletion goStmt s = runStmt s RunToCompletion
itVariable = var "it_var_temp_" itVariable = var "it_var_temp_"
goStmt $ printf "let %s = it" itVariable goStmt $ printf "let %s = it" itVariable
val <- act val <- act
goStmt $ printf "let it = %s" itVariable goStmt $ printf "let it = %s" itVariable
......
...@@ -11,6 +11,7 @@ module IHaskell.Eval.Parser ( ...@@ -11,6 +11,7 @@ module IHaskell.Eval.Parser (
parseDirective, parseDirective,
getModuleName, getModuleName,
Located(..), Located(..),
PragmaType(..),
) where ) where
-- Hide 'unlines' to use our own 'joinLines' instead. -- Hide 'unlines' to use our own 'joinLines' instead.
...@@ -20,6 +21,7 @@ import Data.List (findIndex, maximumBy, maximum, inits) ...@@ -20,6 +21,7 @@ import Data.List (findIndex, maximumBy, maximum, inits)
import Data.String.Utils (startswith, strip, split) import Data.String.Utils (startswith, strip, split)
import Data.List.Utils (subIndex) import Data.List.Utils (subIndex)
import Prelude (init, last, head, tail) import Prelude (init, last, head, tail)
import Control.Monad (msum)
import Bag import Bag
import ErrUtils hiding (ErrMsg) import ErrUtils hiding (ErrMsg)
...@@ -48,6 +50,7 @@ data CodeBlock ...@@ -48,6 +50,7 @@ data CodeBlock
| Directive DirectiveType String -- ^ An IHaskell directive. | Directive DirectiveType String -- ^ An IHaskell directive.
| Module String -- ^ A full Haskell module, to be compiled and loaded. | Module String -- ^ A full Haskell module, to be compiled and loaded.
| ParseError StringLoc ErrMsg -- ^ An error indicating that parsing the code block failed. | ParseError StringLoc ErrMsg -- ^ An error indicating that parsing the code block failed.
| Pragma PragmaType [String] -- ^ A list of GHC pragmas (from a {-# LANGUAGE ... #-} block)
deriving (Show, Eq) deriving (Show, Eq)
-- | Directive types. Each directive is associated with a string in the -- | Directive types. Each directive is associated with a string in the
...@@ -66,6 +69,13 @@ data DirectiveType ...@@ -66,6 +69,13 @@ data DirectiveType
| GetKind -- ^ Get the kind of a type via ':kind'. | GetKind -- ^ Get the kind of a type via ':kind'.
deriving (Show, Eq) deriving (Show, Eq)
-- | Pragma types. Only LANGUAGE pragmas are currently supported.
-- Other pragma types are kept around as a string for error reporting.
data PragmaType
= PragmaLanguage
| PragmaUnsupported String
deriving (Show, Eq)
-- | Parse a string into code blocks. -- | Parse a string into code blocks.
parseString :: String -> Ghc [Located CodeBlock] parseString :: String -> Ghc [Located CodeBlock]
parseString codeString = do parseString codeString = do
...@@ -86,10 +96,12 @@ parseString codeString = do ...@@ -86,10 +96,12 @@ parseString codeString = do
return result return result
where where
parseChunk :: GhcMonad m => String -> LineNumber -> m (Located CodeBlock) parseChunk :: GhcMonad m => String -> LineNumber -> m (Located CodeBlock)
parseChunk chunk line = Located line <$> parseChunk chunk line = Located line <$> handleChunk chunk line
if isDirective chunk where
then return $ parseDirective chunk line handleChunk chunk line
else parseCodeChunk chunk line | isDirective chunk = return $ parseDirective chunk line
| isPragma chunk = return $ parsePragma chunk line
| otherwise = parseCodeChunk chunk line
processChunks :: GhcMonad m => [Located CodeBlock] -> [Located String] -> m [Located CodeBlock] processChunks :: GhcMonad m => [Located CodeBlock] -> [Located String] -> m [Located CodeBlock]
processChunks accum remaining = processChunks accum remaining =
...@@ -100,27 +112,37 @@ parseString codeString = do ...@@ -100,27 +112,37 @@ parseString codeString = do
-- If we have more remaining, parse the current chunk and recurse. -- If we have more remaining, parse the current chunk and recurse.
Located line chunk:remaining -> do Located line chunk:remaining -> do
block <- parseChunk chunk line block <- parseChunk chunk line
activateParsingExtensions $ unloc block activateExtensions $ unloc block
processChunks (block : accum) remaining processChunks (block : accum) remaining
-- Test wither a given chunk is a directive. -- Test whether a given chunk is a directive.
isDirective :: String -> Bool isDirective :: String -> Bool
isDirective = startswith ":" . strip isDirective = startswith ":" . strip
-- Test if a chunk is a pragma.
isPragma :: String -> Bool
isPragma = startswith "{-#" . strip
-- Number of lines in this string. -- Number of lines in this string.
nlines :: String -> Int nlines :: String -> Int
nlines = length . lines nlines = length . lines
activateParsingExtensions :: GhcMonad m => CodeBlock -> m () activateExtensions :: GhcMonad m => CodeBlock -> m ()
activateParsingExtensions (Directive SetExtension ext) = void $ setExtension ext activateExtensions (Directive SetExtension ext) = void $ setExtension ext
activateParsingExtensions (Directive SetDynFlag flags) = activateExtensions (Directive SetDynFlag flags) =
case stripPrefix "-X" flags of case stripPrefix "-X" flags of
Just ext -> void $ setExtension ext Just ext -> void $ setExtension ext
Nothing -> return () Nothing -> return ()
activateParsingExtensions _ = return () activateExtensions (Pragma PragmaLanguage extensions) = void $ setAll extensions
where
setAll :: GhcMonad m => [String] -> m (Maybe String)
setAll exts = do
errs <- mapM setExtension exts
return $ msum errs
activateExtensions _ = return ()
-- | Parse a single chunk of code, as indicated by the layout of the code. -- | Parse a single chunk of code, as indicated by the layout of the code.
parseCodeChunk :: GhcMonad m => String -> LineNumber -> m CodeBlock parseCodeChunk :: GhcMonad m => String -> LineNumber -> m CodeBlock
parseCodeChunk code startLine = do parseCodeChunk code startLine = do
flags <- getSessionDynFlags flags <- getSessionDynFlags
let let
...@@ -191,11 +213,11 @@ parseCodeChunk code startLine = do ...@@ -191,11 +213,11 @@ parseCodeChunk code startLine = do
-- signature, which is also joined with the subsequent declarations. -- signature, which is also joined with the subsequent declarations.
joinFunctions :: [Located CodeBlock] -> [Located CodeBlock] joinFunctions :: [Located CodeBlock] -> [Located CodeBlock]
joinFunctions [] = [] joinFunctions [] = []
joinFunctions blocks = joinFunctions blocks =
if signatureOrDecl $ unloc $ head blocks if signatureOrDecl $ unloc $ head blocks
then Located lnum (conjoin $ map unloc decls) : joinFunctions rest then Located lnum (conjoin $ map unloc decls) : joinFunctions rest
else head blocks : joinFunctions (tail blocks) else head blocks : joinFunctions (tail blocks)
where where
decls = takeWhile (signatureOrDecl . unloc) blocks decls = takeWhile (signatureOrDecl . unloc) blocks
rest = drop (length decls) blocks rest = drop (length decls) blocks
lnum = line $ head decls lnum = line $ head decls
...@@ -211,6 +233,21 @@ joinFunctions blocks = ...@@ -211,6 +233,21 @@ joinFunctions blocks =
conjoin :: [CodeBlock] -> CodeBlock conjoin :: [CodeBlock] -> CodeBlock
conjoin = Declaration . intercalate "\n" . map str conjoin = Declaration . intercalate "\n" . map str
-- | Parse a pragma of the form {-# LANGUAGE ... #-}
parsePragma :: String -- ^ Pragma string.
-> Int -- ^ Line number at which the directive appears.
-> CodeBlock -- ^ Pragma code block or a parse error.
parsePragma ('{':'-':'#':pragma) line =
let commaToSpace :: Char -> Char
commaToSpace ',' = ' '
commaToSpace x = x
pragmas = words $ takeWhile (/= '#') $ map commaToSpace pragma in
case pragmas of
[] -> Pragma (PragmaUnsupported "") [] --empty string pragmas are unsupported
"LANGUAGE":xs -> Pragma PragmaLanguage xs
x:xs -> Pragma (PragmaUnsupported x) xs
-- | Parse a directive of the form :directiveName. -- | Parse a directive of the form :directiveName.
parseDirective :: String -- ^ Directive string. parseDirective :: String -- ^ Directive string.
-> Int -- ^ Line number at which the directive appears. -> Int -- ^ Line number at which the directive appears.
......
...@@ -52,7 +52,7 @@ data ExtFlag ...@@ -52,7 +52,7 @@ data ExtFlag
-- If no such extension exist, yield @Nothing@. -- If no such extension exist, yield @Nothing@.
extensionFlag :: String -- Extension name, such as @"DataKinds"@ extensionFlag :: String -- Extension name, such as @"DataKinds"@
-> Maybe ExtFlag -> Maybe ExtFlag
extensionFlag ext = extensionFlag ext =
case find (flagMatches ext) xFlags of case find (flagMatches ext) xFlags of
Just (_, flag, _) -> Just $ SetFlag flag Just (_, flag, _) -> Just $ SetFlag flag
-- If it doesn't match an extension name, try matching against -- If it doesn't match an extension name, try matching against
...@@ -68,7 +68,7 @@ extensionFlag ext = ...@@ -68,7 +68,7 @@ extensionFlag ext =
-- Check if a FlagSpec matches "No<ExtensionName>". -- Check if a FlagSpec matches "No<ExtensionName>".
-- In that case, we disable the extension. -- In that case, we disable the extension.
flagMatchesNo ext (name, _, _) = ext == "No" ++ name flagMatchesNo ext (name, _, _) = ext == "No" ++ name
-- | Set an extension and update flags. -- | Set an extension and update flags.
-- Return @Nothing@ on success. On failure, return an error message. -- Return @Nothing@ on success. On failure, return an error message.
...@@ -78,7 +78,7 @@ setExtension ext = do ...@@ -78,7 +78,7 @@ setExtension ext = do
case extensionFlag ext of case extensionFlag ext of
Nothing -> return $ Just $ "Could not parse extension name: " ++ ext Nothing -> return $ Just $ "Could not parse extension name: " ++ ext
Just flag -> do Just flag -> do
setSessionDynFlags $ setSessionDynFlags $
case flag of case flag of
SetFlag ghcFlag -> xopt_set flags ghcFlag SetFlag ghcFlag -> xopt_set flags ghcFlag
UnsetFlag ghcFlag -> xopt_unset flags ghcFlag UnsetFlag ghcFlag -> xopt_unset flags ghcFlag
...@@ -101,7 +101,7 @@ setFlags ext = do ...@@ -101,7 +101,7 @@ setFlags ext = do
-- Create the parse errors. -- Create the parse errors.
let noParseErrs = map (("Could not parse: " ++) . unLoc) unrecognized let noParseErrs = map (("Could not parse: " ++) . unLoc) unrecognized
allWarns = map unLoc warnings ++ allWarns = map unLoc warnings ++
["-package not supported yet" | packageFlags flags /= packageFlags flags'] ["-package not supported yet" | packageFlags flags /= packageFlags flags']
warnErrs = map ("Warning: " ++) allWarns warnErrs = map ("Warning: " ++) allWarns
return $ noParseErrs ++ warnErrs return $ noParseErrs ++ warnErrs
...@@ -178,7 +178,7 @@ evalImport imports = do ...@@ -178,7 +178,7 @@ evalImport imports = do
-- Check whether an import is the same as another import (same module). -- Check whether an import is the same as another import (same module).
importOf :: ImportDecl RdrName -> InteractiveImport -> Bool importOf :: ImportDecl RdrName -> InteractiveImport -> Bool
importOf _ (IIModule _) = False importOf _ (IIModule _) = False
importOf imp (IIDecl decl) = importOf imp (IIDecl decl) =
((==) `on` (unLoc . ideclName)) decl imp && not (ideclQualified decl) ((==) `on` (unLoc . ideclName)) decl imp && not (ideclQualified decl)
-- Check whether an import is an *implicit* import of something. -- Check whether an import is an *implicit* import of something.
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment