scripths 0.3.0.1 → 0.3.1.0
raw patch · 4 files changed
+331/−208 lines, 4 filesPVP ok
version bump matches the API change (PVP)
API changes (from Hackage documentation)
+ ScriptHs.Render: instance GHC.Classes.Eq ScriptHs.Render.Kind
+ ScriptHs.Render: instance GHC.Show.Show ScriptHs.Render.Kind
+ ScriptHs.Render: instance GHC.Show.Show ScriptHs.Render.Piece
Files
- CHANGELOG.md +4/−0
- scripths.cabal +1/−1
- src/ScriptHs/Render.hs +223/−122
- test/Test/Render.hs +103/−85
CHANGELOG.md view
@@ -1,5 +1,9 @@ # Revision history for scripths +## 0.3.1.0 -- 2026-04-23++* Better block parsing when there are function signatures and comments.+ ## 0.3.0.1 -- 2026-03-14 * improve block parsing
scripths.cabal view
@@ -1,6 +1,6 @@ cabal-version: 3.0 name: scripths-version: 0.3.0.1+version: 0.3.1.0 synopsis: GHCi scripts for standalone execution and Markdown documentation. description: GHCi scripts for standalone execution (with dependency resolution) and Markdown documentation (produces inline output). homepage: https://www.datahaskell.org/
src/ScriptHs/Render.hs view
@@ -1,15 +1,19 @@+{-# LANGUAGE OverloadedStrings #-}+ {- | Rendering 'ScriptHs.Parser.Line' sequences into GHCi-compatible scripts. GHCi imposes constraints that raw Haskell source does not: multi-line-definitions must be wrapped in @:{@ \/ @:}@ blocks, and monadic binds-(@<-@) or Template Haskell splices (@$(@) must be issued as individual-statements rather than grouped with pure definitions. 'toGhciScript'-handles all of this automatically+definitions must be wrapped in @:{@ \/ @:}@ blocks, and each+expression-statement or monadic bind (@<-@) must be a separate GHCi+statement. 'toGhciScript' groups input lines into logical units+(a lead non-indented line plus its indented continuations), classifies+each unit, then assembles blocks accordingly. -} module ScriptHs.Render ( toGhciScript, ) where +import Data.Char (isAsciiLower, isAsciiUpper, isDigit) import Data.Text (Text) import qualified Data.Text as T import ScriptHs.Parser (Line (..))@@ -21,142 +25,250 @@ {- | Render a list of 'Line's as a GHCi script. -Lines are grouped into blocks and wrapped in @:{@ \/ @:}@ where necessary.-Monadic bind expressions (@<-@) and Template Haskell splices (@$(@) are-always emitted as individual GHCi statements, since GHCi does not allow-them inside multi-line blocks.+Classification rules: -Example — a multi-line definition is wrapped in a single block:+* A run of @--@ comment lines forms a 'KComment' unit that attaches+ forward to the next non-comment unit.+* A type signature, value binding, or clause-head-with-guards forms a+ 'KDeclaration' unit; consecutive declarations merge into a single+ @:{ … :}@ block.+* A monadic bind (@<-@ on the lead line) forms a 'KIOBind' unit; each+ such unit is its own block.+* A Template Haskell splice (@$(…)@ or the rewritten @_ = (); …@ form)+ forms a 'KTHSplice' unit; each is its own block.+* Anything else is a 'KAction' (expression-statement, @do@-block, etc.);+ each action unit becomes its own block.+-}+toGhciScript :: [Line] -> Text+toGhciScript = T.unlines . concatMap renderBlock . piecesToBlocks . toPieces -@-toGhciScript- [ HaskellLine "double :: Int -> Int"- , HaskellLine "double = (*2)"- ]--- :{--- double :: Int -> Int--- double = (*2)--- :}-@+---------------------------------------------------------------+-- Kind and Piece+--------------------------------------------------------------- -Example — an IO bind is kept as a standalone statement:+data Kind+ = KComment+ | KDeclaration+ | KAction+ | KIOBind+ | KTHSplice+ deriving (Show, Eq) -@-toGhciScript- [ HaskellLine "x <- getLine"- , HaskellLine "putStrLn x"- ]--- x <- getLine--- putStrLn x-@+data Piece+ = PBlank+ | PGhciCommand Text+ | PPragma Text+ | PImport Text+ | PUnit Kind [Line]+ deriving (Show)++---------------------------------------------------------------+-- Step 1: [Line] -> [Piece]+---------------------------------------------------------------++toPieces :: [Line] -> [Piece]+toPieces [] = []+toPieces (Blank : rest) = PBlank : toPieces rest+toPieces (GhciCommand t : rest) = PGhciCommand t : toPieces rest+toPieces (Pragma t : rest) = PPragma t : toPieces rest+toPieces (Import t : rest) = PImport t : toPieces rest+toPieces (HaskellLine t : rest)+ | isCommentText t =+ let (more, rest') = spanComments rest+ unit = HaskellLine t : more+ in PUnit KComment unit : toPieces rest'+ | otherwise =+ let (cont, rest') = takeContinuations rest+ unit = HaskellLine t : cont+ k = classify t (map lineText cont)+ in PUnit k unit : toPieces rest'++spanComments :: [Line] -> ([Line], [Line])+spanComments (HaskellLine t : rest)+ | isCommentText t =+ let (more, rest') = spanComments rest+ in (HaskellLine t : more, rest')+spanComments xs = ([], xs)++{- | Take indented continuations after a lead, absorbing interior blank+lines only when followed by more indented non-blank content. Trailing+blanks are left behind so they can serve as unit separators (which is+what makes the "double blank still breaks" case work). -}-toGhciScript :: [Line] -> Text-toGhciScript = T.unlines . concatMap renderBlock . groupBlocks+takeContinuations :: [Line] -> ([Line], [Line])+takeContinuations [] = ([], [])+takeContinuations xs@(l : rest)+ | isIndentedNonBlank l =+ let (more, rest') = takeContinuations rest+ in (l : more, rest')+ | isBlankLine l =+ let (blanks, afterBlanks) = span isBlankLine xs+ in case afterBlanks of+ (x : _)+ | isIndentedNonBlank x ->+ let (more, rest') = takeContinuations afterBlanks+ in (blanks ++ more, rest')+ _ -> ([], xs)+ | otherwise = ([], xs) -groupBlocks :: [Line] -> [Block]-groupBlocks = concatMap splitIOBinds . groupRaw+isIndentedNonBlank :: Line -> Bool+isIndentedNonBlank (HaskellLine t) = T.isPrefixOf " " t || T.isPrefixOf "\t" t+isIndentedNonBlank _ = False -groupRaw :: [Line] -> [Block]-groupRaw [] = []-groupRaw (Blank : rest) = SingleLine Blank : groupRaw rest-groupRaw (GhciCommand t : rest) = SingleLine (GhciCommand t) : groupRaw rest-groupRaw ls =- let (block, rest) = span isBlockLine ls- (block', rest') = takeIfIndented block rest- in classifyBlock block' : groupRaw rest'+isBlankLine :: Line -> Bool+isBlankLine Blank = True+isBlankLine _ = False -takeIfIndented :: [Line] -> [Line] -> ([Line], [Line])-takeIfIndented block rest = (block ++ takeWhile isIndented rest, dropWhile isIndented rest)+---------------------------------------------------------------+-- Classification+--------------------------------------------------------------- -isIndented :: Line -> Bool-isIndented Blank = True-isIndented (HaskellLine t) = T.isPrefixOf " " t || T.isPrefixOf "\t" t-isIndented _ = False+classify :: Text -> [Text] -> Kind+classify leadText contTexts+ | isTHSplice leadText = KTHSplice+ | isDeclaration leadText contTexts = KDeclaration+ | isIOBindLead leadText = KIOBind+ | otherwise = KAction -splitIOBinds :: Block -> [Block]-splitIOBinds (MultiLine ls) =- concatMap (splitDefIO . classifyBlock) (splitOn isIOLine ls)- where- isIOLine l = isIOorTH (lineText l)-splitIOBinds b = [b]+isTHSplice :: Text -> Bool+isTHSplice t =+ let s = T.stripStart t+ in "$(" `T.isPrefixOf` s || "_ = ();" `T.isPrefixOf` s -{- | Split a block that mixes definitions and IO actions.- Only splits when there's a clear boundary: a non-indented line- without @=@ or @::@ following a line that has @=@ or @::@, or vice versa.- Indented lines always attach to the preceding line's group.+isIOBindLead :: Text -> Bool+isIOBindLead = T.isInfixOf "<-"++isDeclaration :: Text -> [Text] -> Bool+isDeclaration leadText contTexts =+ startsWithDeclKeyword leadText+ || isTypeSig leadText+ || isValueBinding leadText+ || isClauseHead leadText && any contHasBinding contTexts++startsWithDeclKeyword :: Text -> Bool+startsWithDeclKeyword t =+ any+ (`T.isPrefixOf` T.stripStart t)+ ["data ", "newtype ", "type ", "class ", "instance ", "default "]++isTypeSig :: Text -> Bool+isTypeSig t = case afterLeadIdent t of+ Just rest -> "::" `T.isPrefixOf` T.stripStart rest+ Nothing -> False++isValueBinding :: Text -> Bool+isValueBinding t = maybe False hasTopLevelEquals (afterLeadIdent t)++{- | A line like @isPrime n@ — identifier + args, no @=@ / @::@ / @<-@ on+this line. Only counts as a declaration when accompanied by indented+continuations that supply the RHS (guards, a @where@ clause, etc.). -}-splitDefIO :: Block -> [Block]-splitDefIO (MultiLine ls)- | hasMix ls = map classifyBlock (groupByKind ls)- | otherwise = [MultiLine ls]-splitDefIO b = [b]+isClauseHead :: Text -> Bool+isClauseHead t = case afterLeadIdent t of+ Just rest ->+ let rest' = T.stripEnd (T.stripStart rest)+ in not (T.null rest')+ && not (hasTopLevelEquals rest)+ && not ("::" `T.isInfixOf` rest)+ && not ("<-" `T.isInfixOf` rest)+ Nothing -> False --- | Check if a block has both definitions and IO actions at the top level.-hasMix :: [Line] -> Bool-hasMix ls =- let topLevel = filter (not . isIndented) ls- defs = filter (isDef . lineText) topLevel- actions = filter (not . isDef . lineText) topLevel- in not (null defs) && not (null actions)+contHasBinding :: Text -> Bool+contHasBinding t =+ let s = T.stripStart t+ in "| " `T.isPrefixOf` s && hasTopLevelEquals s+ || hasTopLevelEquals t+ || "where" `T.isPrefixOf` s -groupByKind :: [Line] -> [[Line]]-groupByKind [] = []-groupByKind (l : ls) =- let kind = lineKindOf l- -- Collect lines of the same kind, plus any indented continuations- (same, rest) = spanSameKind kind ls- in (l : same) : groupByKind rest+{- | Parse a leading lowercase identifier. Returns the text that follows+it (with any leading whitespace) or 'Nothing' if the stripped line does+not begin with a lowercase identifier, or begins with a Haskell keyword.+-}+afterLeadIdent :: Text -> Maybe Text+afterLeadIdent t =+ let s = T.stripStart t+ in case T.uncons s of+ Just (c, _)+ | isIdentStart c ->+ let (ident, rest) = T.span isIdentCont s+ in if T.null ident || isHaskellKeyword ident+ then Nothing+ else Just rest+ _ -> Nothing -spanSameKind :: Bool -> [Line] -> ([Line], [Line])-spanSameKind _ [] = ([], [])-spanSameKind kind (l : ls)- | isIndented l =- -- Indented lines attach to the current group- let (more, rest) = spanSameKind kind ls- in (l : more, rest)- | lineKindOf l == kind =- let (more, rest) = spanSameKind kind ls- in (l : more, rest)- | otherwise = ([], l : ls)+isIdentStart :: Char -> Bool+isIdentStart c = c == '_' || isAsciiLower c --- | True if the line looks like a definition (has @=@ or @::@).-lineKindOf :: Line -> Bool-lineKindOf l = isDef (lineText l)+isIdentCont :: Char -> Bool+isIdentCont c =+ isIdentStart c+ || isAsciiUpper c+ || isDigit c+ || c == '\'' -isDef :: Text -> Bool-isDef t =- hasTopLevelEquals t || " :: " `T.isInfixOf` t+isHaskellKeyword :: Text -> Bool+isHaskellKeyword t =+ t+ `elem` [ "do"+ , "let"+ , "in"+ , "if"+ , "then"+ , "else"+ , "case"+ , "of"+ , "where"+ , "data"+ , "newtype"+ , "type"+ , "class"+ , "instance"+ , "module"+ , "import"+ , "default"+ , "deriving"+ , "infix"+ , "infixl"+ , "infixr"+ ] hasTopLevelEquals :: Text -> Bool-hasTopLevelEquals t =- " = " `T.isInfixOf` t || T.isSuffixOf " =" t+hasTopLevelEquals t = " = " `T.isInfixOf` t || T.isSuffixOf " =" t -splitOn :: (a -> Bool) -> [a] -> [[a]]-splitOn _ [] = []-splitOn p (x : xs)- | p x = [x] : splitOn p xs- | otherwise =- let (run, rest) = break p xs- in (x : run) : splitOn p rest+isCommentText :: Text -> Bool+isCommentText t = "--" `T.isPrefixOf` T.stripStart t -isBlockLine :: Line -> Bool-isBlockLine Blank = False-isBlockLine (GhciCommand _) = False-isBlockLine _ = True+---------------------------------------------------------------+-- Step 2: [Piece] -> [Block]+--------------------------------------------------------------- -classifyBlock :: [Line] -> Block-classifyBlock [l] = SingleLine l-classifyBlock ls = MultiLine ls+piecesToBlocks :: [Piece] -> [Block]+piecesToBlocks [] = []+piecesToBlocks (PBlank : rest) = SingleLine Blank : piecesToBlocks rest+piecesToBlocks (PGhciCommand t : rest) = SingleLine (GhciCommand t) : piecesToBlocks rest+piecesToBlocks (PPragma t : rest) = SingleLine (Pragma t) : piecesToBlocks rest+piecesToBlocks (PImport t : rest) = SingleLine (Import t) : piecesToBlocks rest+piecesToBlocks (PUnit KComment lines1 : PUnit k lines2 : rest)+ | k /= KComment =+ piecesToBlocks (PUnit k (lines1 ++ lines2) : rest)+piecesToBlocks (PUnit KDeclaration lines1 : PUnit KDeclaration lines2 : rest) =+ piecesToBlocks (PUnit KDeclaration (lines1 ++ lines2) : rest)+piecesToBlocks (PUnit _ ls : rest) = toBlock ls : piecesToBlocks rest+ where+ toBlock [l] = SingleLine l+ toBlock xs = MultiLine xs +---------------------------------------------------------------+-- Rendering+---------------------------------------------------------------+ renderBlock :: Block -> [Text] renderBlock (SingleLine Blank) = [""] renderBlock (SingleLine (GhciCommand t)) = [t] renderBlock (SingleLine (Pragma t)) = [t] renderBlock (SingleLine (Import t)) = [t] renderBlock (SingleLine (HaskellLine t)) = wrapMulti [t]-renderBlock (MultiLine ls)- | allIOorTH ls = concatMap (\l -> wrapMulti [lineText l]) ls- | otherwise = wrapMulti (map lineText ls)+renderBlock (MultiLine ls) = wrapMulti (map lineText ls) wrapMulti :: [Text] -> [Text] wrapMulti ls = [":{"] ++ ls ++ [":}"]@@ -167,14 +279,3 @@ lineText (Pragma t) = t lineText (Import t) = t lineText (HaskellLine t) = t--isIOorTH :: Text -> Bool-isIOorTH t =- not (T.isPrefixOf " " t)- && ( T.isInfixOf "<-" t- || T.isInfixOf "$(" t- || T.isPrefixOf "_ = ();" (T.stripStart t)- )--allIOorTH :: [Line] -> Bool-allIOorTH = all (isIOorTH . lineText)
test/Test/Render.hs view
@@ -13,7 +13,7 @@ testGroup "Render" [ testGroup- "Single lines"+ "Line-level rendering" [ testCase "plain expression stays unwrapped" $ do let result = toGhciScript [HaskellLine "print 42"] assertWrapped result ["print 42"]@@ -21,12 +21,11 @@ let result = toGhciScript [Import "import Data.Text"] assertNotWrapped result , testCase "pragma stays unwrapped" $ do- let result = toGhciScript [Import "{-# LANGUAGE GADTs #-}"]+ let result = toGhciScript [Pragma "{-# LANGUAGE GADTs #-}"] assertNotWrapped result , testCase "ghci command stays unwrapped" $ do let result = toGhciScript [GhciCommand ":set -XOverloadedStrings"]- let ls = nonEmpty result- ls @?= [":set -XOverloadedStrings"]+ nonEmpty result @?= [":set -XOverloadedStrings"] , testCase "IO bind gets wrapped" $ do let result = toGhciScript [HaskellLine "x <- getLine"] assertWrapped result ["x <- getLine"]@@ -35,17 +34,37 @@ assertBool "has blank" (T.isInfixOf "\n\n" result || result == "\n") ] , testGroup- "IO line isolation"+ "Isolation: one block per statement" [ testCase "consecutive IO binds each get own block" $ do let result = toGhciScript [ HaskellLine "x <- getLine" , HaskellLine "y <- getLine" ]- let blocks = splitBlocks result- assertBool- ("expected 2 blocks, got " ++ show (length blocks))- (length blocks == 2)+ length (splitBlocks result) @?= 2+ , testCase "consecutive IO expression-statements each get own block" $ do+ let result =+ toGhciScript+ [ HaskellLine "print 1"+ , HaskellLine "putStrLn \"hi\""+ , HaskellLine "displayLatex \"hi\""+ , HaskellLine "displayLatex \"hi\""+ ]+ length (splitBlocks result) @?= 4+ , testCase "inline type annotation does not flip statement into declaration" $ do+ let result =+ toGhciScript+ [ HaskellLine "print (1 :: Int)"+ , HaskellLine "print \"x\""+ ]+ length (splitBlocks result) @?= 2+ , testCase "TH splice gets its own block" $ do+ let result =+ toGhciScript+ [ HaskellLine "$(deriveJSON defaultOptions ''Foo)"+ , HaskellLine "print \"after\""+ ]+ length (splitBlocks result) @?= 2 , testCase "IO bind between pure code splits correctly" $ do let result = toGhciScript@@ -67,8 +86,7 @@ , HaskellLine " x = 5" , HaskellLine " y = 10" ]- let blocks = splitBlocks result- length blocks @?= 1+ length (splitBlocks result) @?= 1 , testCase "consecutive do-notation lines grouped" $ do let result = toGhciScript@@ -77,21 +95,18 @@ , HaskellLine " y <- pure 10" , HaskellLine " pure $ x + y" ]- let blocks = splitBlocks result- length blocks @?= 1+ length (splitBlocks result) @?= 1 , testCase "consecutive do-notation lines with space are grouped" $ do let result = toGhciScript [ HaskellLine "do" , HaskellLine " x <- pure 5"- , -- indentation should ignore these blank lines.- Blank , Blank+ , Blank , HaskellLine " y <- pure 10" , HaskellLine " pure $ x + y" ]- let blocks = splitBlocks result- length blocks @?= 1+ length (splitBlocks result) @?= 1 , testCase "blank separates blocks" $ do let result = toGhciScript@@ -121,7 +136,7 @@ assertBool "has print" (T.isInfixOf "print iris" result) ] , testGroup- "Continuation across blanks (don't break)"+ "Continuation across blanks" [ testCase "blank between independent IO statements still separates" $ do let result = toGhciScript@@ -129,10 +144,7 @@ , Blank , HaskellLine "putStrLn \"b\"" ]- let blocks = splitBlocks result- assertBool- ("expected 2 blocks, got " ++ show (length blocks))- (length blocks == 2)+ length (splitBlocks result) @?= 2 , testCase "blank between imports still separates" $ do let result = toGhciScript@@ -140,13 +152,9 @@ , Blank , Import "import Data.Map" ]- -- Both imports should appear assertBool "has Text" (T.isInfixOf "Data.Text" result) assertBool "has Map" (T.isInfixOf "Data.Map" result)- ]- , testGroup- "Continuation across blanks (new behavior)"- [ testCase "blank before where stays in same block" $ do+ , testCase "blank before where stays in same block" $ do let result = toGhciScript [ HaskellLine "foo x = bar x"@@ -154,14 +162,7 @@ , HaskellLine " where" , HaskellLine " bar = id" ]- let blocks = splitBlocks result- assertBool- ( "expected 1 block for where clause, got "- ++ show (length blocks)- ++ ": "- ++ show blocks- )- (length blocks == 1)+ length (splitBlocks result) @?= 1 , testCase "blank before guards stays in same block" $ do let result = toGhciScript@@ -170,14 +171,7 @@ , HaskellLine " | n > 0 = 1" , HaskellLine " | otherwise = 0" ]- let blocks = splitBlocks result- assertBool- ( "expected 1 block for guards, got "- ++ show (length blocks)- ++ ": "- ++ show blocks- )- (length blocks == 1)+ length (splitBlocks result) @?= 1 , testCase "blank before deriving stays in same block" $ do let result = toGhciScript@@ -185,14 +179,7 @@ , Blank , HaskellLine " deriving (Show, Eq)" ]- let blocks = splitBlocks result- assertBool- ( "expected 1 block for deriving, got "- ++ show (length blocks)- ++ ": "- ++ show blocks- )- (length blocks == 1)+ length (splitBlocks result) @?= 1 , testCase "double blank still breaks (intentional separation)" $ do let result = toGhciScript@@ -201,35 +188,25 @@ , Blank , HaskellLine "y = 2" ]- let blocks = splitBlocks result- assertBool- ("expected 2 blocks for double blank, got " ++ show (length blocks))- (length blocks == 2)+ length (splitBlocks result) @?= 2 ] , testGroup- "Mixed block splitting (don't break)"+ "Mixed block splitting" [ testCase "pure definitions stay grouped" $ do let result = toGhciScript [ HaskellLine "f x = x + 1" , HaskellLine "g y = y * 2" ]- let blocks = splitBlocks result- assertBool- ("expected 1 block for definitions, got " ++ show (length blocks))- (length blocks == 1)+ length (splitBlocks result) @?= 1 , testCase "indented continuation stays grouped" $ do let result = toGhciScript [ HaskellLine "f x =" , HaskellLine " x + 1" ]- let blocks = splitBlocks result- length blocks @?= 1- ]- , testGroup- "Mixed block splitting (new behavior)"- [ testCase "definition then IO splits into two blocks" $ do+ length (splitBlocks result) @?= 1+ , testCase "definition then IO splits into two blocks" $ do let result = toGhciScript [ HaskellLine "x = 5"@@ -274,29 +251,77 @@ [ HaskellLine "f :: Int -> Int" , HaskellLine "f x = x + 1" ]+ length (splitBlocks result) @?= 1+ , testCase "comment + type sig + guarded body + where stays together" $ do+ let result =+ toGhciScript+ [ HaskellLine "-- Helper function to check if a number is prime"+ , HaskellLine "isPrime :: Int -> Bool"+ , HaskellLine "isPrime n"+ , HaskellLine " | n < 2 = False"+ , HaskellLine " | n == 2 = True"+ , HaskellLine " | even n = False"+ , HaskellLine " | otherwise = all (\\k -> n `mod` k /= 0) [3, 5 .. isqrt n]"+ , HaskellLine " where isqrt = floor . sqrt . fromIntegral"+ ]+ length (splitBlocks result) @?= 1+ , testCase "multi-section script: commented + typed functions + do block" $ do+ let result =+ toGhciScript+ [ HaskellLine "-- Helper function to check if a number is prime"+ , HaskellLine "isPrime :: Int -> Bool"+ , HaskellLine "isPrime n"+ , HaskellLine " | n < 2 = False"+ , HaskellLine " | n == 2 = True"+ , HaskellLine " | even n = False"+ , HaskellLine " | otherwise = all (\\k -> n `mod` k /= 0) [3, 5 .. isqrt n]"+ , HaskellLine " where isqrt = floor . sqrt . fromIntegral"+ , Blank+ , HaskellLine "-- Generate list of primes up to n"+ , HaskellLine "primesUpTo :: Int -> [Int]"+ , HaskellLine "primesUpTo n = filter isPrime [2 .. n]"+ , Blank+ , HaskellLine "-- Compute gaps between consecutive primes"+ , HaskellLine "primeGaps :: [Int] -> [Int]"+ , HaskellLine "primeGaps ps = zipWith (-) (tail ps) ps"+ , Blank+ , HaskellLine "-- Test"+ , HaskellLine "do"+ , HaskellLine " let testPrimes = primesUpTo 10"+ ] let blocks = splitBlocks result assertBool- ("expected 1 block for sig+def, got " ++ show (length blocks))- (length blocks == 1)+ ( "expected 4 blocks (one per logical section), got "+ ++ show (length blocks)+ ++ ": "+ ++ show blocks+ )+ (length blocks == 4) ] , testGroup- "Bracket counting (don't break)"+ "Comments"+ [ testCase "comment attaches to following declaration" $ do+ let result =+ toGhciScript+ [ HaskellLine "-- doc comment"+ , HaskellLine "f x = x + 1"+ ]+ length (splitBlocks result) @?= 1+ assertWrapped result ["-- doc comment", "f x = x + 1"]+ ]+ , testGroup+ "Bracket counting" [ testCase "complete expression in parens" $ do let result = toGhciScript [HaskellLine "f x = (x + 1)"]- let blocks = splitBlocks result- length blocks @?= 1+ length (splitBlocks result) @?= 1 , testCase "balanced multiline already grouped by indentation" $ do let result = toGhciScript [ HaskellLine "f x =" , HaskellLine " (x + 1)" ]- let blocks = splitBlocks result- length blocks @?= 1- ]- , testGroup- "Bracket counting (new behavior)"- [ testCase "unclosed bracket extends block" $ do+ length (splitBlocks result) @?= 1+ , testCase "unclosed bracket extends block" $ do let result = toGhciScript [ HaskellLine "xs = ["@@ -305,14 +330,7 @@ , HaskellLine " 3" , HaskellLine " ]" ]- let blocks = splitBlocks result- assertBool- ( "expected 1 block for list literal, got "- ++ show (length blocks)- ++ ": "- ++ show blocks- )- (length blocks == 1)+ length (splitBlocks result) @?= 1 ] ]