Skip to content

Commit 3a68af4

Browse files
committed
Add simple test and implementation for a cabal info parser
1 parent 4b922a6 commit 3a68af4

File tree

5 files changed

+220
-0
lines changed

5 files changed

+220
-0
lines changed

haskell-language-server.cabal

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -237,6 +237,7 @@ library hls-cabal-plugin
237237
buildable: False
238238
exposed-modules:
239239
Ide.Plugin.Cabal
240+
Ide.Plugin.Cabal.CabalInfoParser
240241
Ide.Plugin.Cabal.Diagnostics
241242
Ide.Plugin.Cabal.Completion.CabalFields
242243
Ide.Plugin.Cabal.Completion.Completer.FilePath
@@ -273,6 +274,7 @@ library hls-cabal-plugin
273274
, lens
274275
, lsp ^>=2.7
275276
, lsp-types ^>=2.3
277+
, megaparsec
276278
, regex-tdfa ^>=1.3.1
277279
, text
278280
, text-rope
@@ -296,6 +298,7 @@ test-suite hls-cabal-plugin-tests
296298
main-is: Main.hs
297299
other-modules:
298300
CabalAdd
301+
CabalInfoParser
299302
Completer
300303
Context
301304
Definition
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
{-# LANGUAGE OverloadedStrings #-}
2+
3+
-- | This module allows you to parse the output of @cabal info@.
4+
module Ide.Plugin.Cabal.CabalInfoParser (parseCabalInfo, cabalInfo) where
5+
6+
import Data.Map.Strict (Map)
7+
import Data.Text (Text)
8+
import Data.Void (Void)
9+
import Text.Megaparsec (MonadParsec (..), Parsec, chunk, many,
10+
parse, sepBy, single, (<|>))
11+
12+
import Control.Monad (void)
13+
import Data.Either.Extra (mapLeft)
14+
import qualified Data.Map.Strict as Map
15+
import qualified Data.Text as T
16+
17+
type Parser = Parsec Void Text
18+
19+
parseCabalInfo :: Text -> Either CabalInfoParserError (Map Text (Map Text [Text]))
20+
parseCabalInfo = mapLeft (T.pack . show) . parse cabalInfo ""
21+
22+
type CabalInfoParserError = Text
23+
24+
-- TODO: parse eof at the end to avoid early exits
25+
cabalInfo :: Parser (Map Text (Map Text [Text]))
26+
cabalInfo = do
27+
entries <- cabalInfoEntry `sepBy` chunk "\n\n"
28+
void $ takeWhileP (Just "trailing whitespace") (`elem` (" \t\r\n" :: String))
29+
eof
30+
31+
pure $ Map.fromList entries
32+
33+
cabalInfoEntry :: Parser (Text, Map Text [Text])
34+
cabalInfoEntry = do
35+
void $ single '*'
36+
void spaces
37+
38+
name <- takeWhileP (Just "package name") (/= ' ')
39+
40+
void restOfLine
41+
42+
pairs <- many $ try kvPair
43+
44+
pure (name, Map.fromList pairs)
45+
46+
kvPair :: Parser (Text, [Text])
47+
kvPair = do
48+
spacesBeforeKey <- spaces
49+
key <- takeWhileP (Just "field name") (/= ':')
50+
void $ single ':'
51+
spacesAfterKey <- spaces
52+
firstLine <- restOfLine
53+
54+
-- The first line of the field may be empty.
55+
-- In this case, we have to look at the second line to determine
56+
-- the indentation depth.
57+
if T.null firstLine then do
58+
spacesBeforeFirstLine <- spaces
59+
firstLine' <- restOfLine
60+
let indent = T.length spacesBeforeFirstLine
61+
lines <- trailingIndentedLines indent
62+
pure (key, firstLine' : lines)
63+
-- If the first line is *not* empty, we can determine the indentation
64+
-- depth by calculating how many characters came before it.
65+
else do
66+
let indent = T.length spacesBeforeKey + T.length key + 1 + T.length spacesAfterKey
67+
lines <- trailingIndentedLines indent
68+
pure (key, firstLine : lines)
69+
70+
where
71+
trailingIndentedLines :: Int -> Parser [Text]
72+
trailingIndentedLines indent = many $ try $ indentedLine indent
73+
74+
indentedLine :: Int -> Parser Text
75+
indentedLine indent = do
76+
void $ chunk $ T.replicate indent " "
77+
restOfLine
78+
79+
spaces :: Parser Text
80+
spaces = takeWhileP Nothing (== ' ')
81+
82+
-- | Parse until next @\n@, return text before that.
83+
restOfLine :: Parser Text
84+
restOfLine = do
85+
s <- takeWhileP (Just "rest of line") (/= '\n')
86+
eolOrEof
87+
pure s
88+
89+
eolOrEof :: Parser ()
90+
eolOrEof = void (single '\n') <|> eof
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
module CabalInfoParser (cabalInfoParserUnitTests) where
2+
3+
import System.FilePath ((</>))
4+
import Test.Hls (TestTree, testCase,
5+
testGroup, (@?))
6+
import Utils (testDataDir)
7+
8+
import qualified Data.Text.IO as TIO
9+
10+
import Data.Either (isRight)
11+
import Ide.Plugin.Cabal.CabalInfoParser (parseCabalInfo)
12+
13+
cabalInfoParserUnitTests :: TestTree
14+
cabalInfoParserUnitTests = testGroup "cabal info Parser Tests"
15+
[ simpleParsingWorks
16+
]
17+
where
18+
simpleParsingWorks = testCase "Simple parsing works" $ do
19+
res <- parseCabalInfo <$> TIO.readFile (testDataDir </> "cabal-info" </> "text.cabal-info")
20+
isRight res @? "Failed to parse well-formed input"

plugins/hls-cabal-plugin/test/Main.hs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ module Main (
77
) where
88

99
import CabalAdd (cabalAddTests)
10+
import CabalInfoParser (cabalInfoParserUnitTests)
1011
import Completer (completerTests)
1112
import Context (contextTests)
1213
import Control.Lens ((^.))
@@ -51,6 +52,7 @@ unitTests =
5152
"Unit Tests"
5253
[ cabalParserUnitTests
5354
, codeActionUnitTests
55+
, cabalInfoParserUnitTests
5456
]
5557

5658
cabalParserUnitTests :: TestTree
Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
* text (library)
2+
Synopsis: An efficient packed Unicode text type.
3+
djaksldjks
4+
Versions available: 0.11.3.1, 1.1.1.4, 1.2.4.1, 1.2.5.0, 2.0, 2.0.1, 2.0.2,
5+
2.1, 2.1.1 (and 70 others)
6+
Versions installed: 2.0.2
7+
Homepage: https://github.com/haskell/text
8+
Bug reports: https://github.com/haskell/text/issues
9+
Description: An efficient packed, immutable Unicode text type (both strict
10+
and lazy).
11+
12+
The 'Text' type represents Unicode character strings, in a
13+
time and space-efficient manner. This package provides text
14+
processing capabilities that are optimized for performance
15+
critical use, both in terms of large data quantities and high
16+
speed.
17+
18+
The 'Text' type provides character-encoding, type-safe case
19+
conversion via whole-string case conversion functions (see
20+
"Data.Text"). It also provides a range of functions for
21+
converting 'Text' values to and from 'ByteStrings', using
22+
several standard encodings (see "Data.Text.Encoding").
23+
24+
Efficient locale-sensitive support for text IO is also
25+
supported (see "Data.Text.IO").
26+
27+
These modules are intended to be imported qualified, to avoid
28+
name clashes with Prelude functions, e.g.
29+
30+
> import qualified Data.Text as T
31+
32+
== ICU Support
33+
34+
To use an extended and very rich family of functions for
35+
working with Unicode text (including normalization, regular
36+
expressions, non-standard encodings, text breaking, and
37+
locales), see the [text-icu
38+
package](https://hackage.haskell.org/package/text-icu) based
39+
on the well-respected and liberally licensed [ICU
40+
library](http://site.icu-project.org/).
41+
Category: Data, Text
42+
License: BSD-2-Clause
43+
Author: Bryan O'Sullivan <[email protected]>
44+
Maintainer: Haskell Text Team <[email protected]>, Core Libraries Committee
45+
Source repo: https://github.com/haskell/text
46+
Flags: developer, simdutf, pure-haskell
47+
Dependencies: array >=0.3 && <0.6, base >=4.10 && <5, binary >=0.5 && <0.9,
48+
bytestring >=0.10.4 && <0.13, deepseq >=1.1 && <1.6,
49+
ghc-prim >=0.2 && <0.12, template-haskell >=2.5 && <2.23,
50+
system-cxx-std-lib ==1.0, base <0,
51+
data-array-byte >=0.1 && <0.2, QuickCheck >=2.12.6 && <2.15,
52+
base <5, bytestring, deepseq, directory, ghc-prim, tasty,
53+
tasty-hunit, tasty-quickcheck, template-haskell,
54+
transformers, text, data-array-byte >=0.1 && <0.2,
55+
tasty-inspection-testing, base, bytestring >=0.10.4,
56+
containers, deepseq, directory, filepath, tasty-bench >=0.2,
57+
text, transformers
58+
Documentation: [ Not installed ]
59+
Cached: No
60+
Modules:
61+
Data.Text
62+
Data.Text.Array
63+
Data.Text.Encoding
64+
Data.Text.Encoding.Error
65+
Data.Text.Foreign
66+
Data.Text.IO
67+
Data.Text.Internal
68+
Data.Text.Internal.Builder
69+
Data.Text.Internal.Builder.Functions
70+
Data.Text.Internal.Builder.Int.Digits
71+
Data.Text.Internal.Builder.RealFloat.Functions
72+
Data.Text.Internal.ByteStringCompat
73+
Data.Text.Internal.Encoding
74+
Data.Text.Internal.Encoding.Fusion
75+
Data.Text.Internal.Encoding.Fusion.Common
76+
Data.Text.Internal.Encoding.Utf16
77+
Data.Text.Internal.Encoding.Utf32
78+
Data.Text.Internal.Encoding.Utf8
79+
Data.Text.Internal.Fusion
80+
Data.Text.Internal.Fusion.CaseMapping
81+
Data.Text.Internal.Fusion.Common
82+
Data.Text.Internal.Fusion.Size
83+
Data.Text.Internal.Fusion.Types
84+
Data.Text.Internal.IO
85+
Data.Text.Internal.Lazy
86+
Data.Text.Internal.Lazy.Encoding.Fusion
87+
Data.Text.Internal.Lazy.Fusion
88+
Data.Text.Internal.Lazy.Search
89+
Data.Text.Internal.PrimCompat
90+
Data.Text.Internal.Private
91+
Data.Text.Internal.Read
92+
Data.Text.Internal.Search
93+
Data.Text.Internal.StrictBuilder
94+
Data.Text.Internal.Unsafe
95+
Data.Text.Internal.Unsafe.Char
96+
Data.Text.Lazy
97+
Data.Text.Lazy.Builder
98+
Data.Text.Lazy.Builder.Int
99+
Data.Text.Lazy.Builder.RealFloat
100+
Data.Text.Lazy.Encoding
101+
Data.Text.Lazy.IO
102+
Data.Text.Lazy.Internal
103+
Data.Text.Lazy.Read
104+
Data.Text.Read
105+
Data.Text.Unsafe

0 commit comments

Comments
 (0)