ormolu-0.8.0.0: src/Ormolu/Printer/Meat/Declaration/StringLiteral.hs
{-# LANGUAGE BlockArguments #-}
{-# LANGUAGE DerivingStrategies #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE RecordWildCards #-}
{-# LANGUAGE ViewPatterns #-}
module Ormolu.Printer.Meat.Declaration.StringLiteral (p_stringLit) where
import Control.Applicative (Alternative (..))
import Control.Category ((>>>))
import Control.Monad ((>=>))
import Data.Semigroup (Min (..))
import Data.Text (Text)
import Data.Text qualified as T
import GHC.Data.FastString
import GHC.Parser.CharClass (is_space)
import Ormolu.Printer.Combinators
import Ormolu.Utils
-- | Print the source text of a string literal while indenting gaps and newlines
-- correctly.
p_stringLit :: FastString -> R ()
p_stringLit src = case parseStringLiteral $ T.pack $ unpackFS src of
Nothing -> error $ "Internal Ormolu error: couldn't parse string literal: " <> show src
Just ParsedStringLiteral {..} -> sitcc do
txt startMarker
case stringLiteralKind of
RegularStringLiteral -> do
let singleLine =
txt $ T.concat segments
multiLine =
sep breakpoint f (attachRelativePos segments)
where
f :: (RelativePos, Text) -> R ()
f (pos, s) = case pos of
SinglePos -> txt s
FirstPos -> txt s *> txt "\\"
MiddlePos -> txt "\\" *> txt s *> txt "\\"
LastPos -> txt "\\" *> txt s
vlayout singleLine multiLine
MultilineStringLiteral ->
sep breakpoint' txt segments
txt endMarker
-- | The start/end marker of the literal, whether it is a regular or a multiline
-- literal, and the segments of the literals (separated by gaps for a regular
-- literal, and separated by newlines for a multiline literal).
data ParsedStringLiteral = ParsedStringLiteral
{ startMarker, endMarker :: Text,
stringLiteralKind :: StringLiteralKind,
segments :: [Text]
}
deriving stock (Show, Eq)
-- | A regular or a multiline string literal.
data StringLiteralKind = RegularStringLiteral | MultilineStringLiteral
deriving stock (Show, Eq)
-- | Turn a string literal (as it exists in the source) into a more structured
-- form for printing. This should never return 'Nothing' for literals that the
-- GHC parser accepted.
parseStringLiteral :: Text -> Maybe ParsedStringLiteral
parseStringLiteral = \s -> do
psl <-
(stripStartEndMarker MultilineStringLiteral "\"\"\"" s)
<|> (stripStartEndMarker RegularStringLiteral "\"" s)
let splitSegments = case stringLiteralKind psl of
RegularStringLiteral -> splitGaps
MultilineStringLiteral -> splitMultilineString
pure psl {segments = concatMap splitSegments $ segments psl}
where
-- Remove the given marker from the start and the end (at the end,
-- optionally also remove a #).
stripStartEndMarker ::
StringLiteralKind -> Text -> Text -> Maybe ParsedStringLiteral
stripStartEndMarker stringLiteralKind marker s = do
let startMarker = marker
suffix <- T.stripPrefix startMarker s
let markerWithHash = marker <> "#"
(endMarker, infix_) <-
((markerWithHash,) <$> T.stripSuffix markerWithHash suffix)
<|> ((marker,) <$> T.stripSuffix marker suffix)
pure ParsedStringLiteral {segments = [infix_], ..}
-- Split a string on gaps (backslash delimited whitespaces).
--
-- > splitGaps "bar\\ \\fo\\&o" == ["bar", "fo\\&o"]
splitGaps :: Text -> [Text]
splitGaps s = go $ T.breakOnAll "\\" s
where
go [] = [s]
go ((pre, suf) : bs) = case T.uncons suf of
Just ('\\', T.uncons -> Just (c, s'))
| is_space c,
let rest = T.drop 1 $ T.dropWhile (/= '\\') s' ->
pre : splitGaps rest
| otherwise -> go $ (if c == '\\' then drop 1 else id) bs
_ -> go bs
-- See the the MultilineStrings GHC proposal and 'lexMultilineString' from
-- "GHC.Parser.String" for reference.
--
-- https://github.com/ghc-proposals/ghc-proposals/blob/master/proposals/0569-multiline-strings.rst#proposed-change-specification
splitMultilineString :: Text -> [Text]
splitMultilineString =
splitGaps
-- There is no reason to use gaps with multiline string literals, so
-- we collapse them.
>>> T.concat
>>> splitNewlines
>>> fmap expandLeadingTabs
>>> rmCommonWhitespacePrefixAndBlank
-- See the definition of newlines on
-- <https://www.haskell.org/onlinereport/haskell2010/haskellch10.html#x17-17800010.3>.
splitNewlines :: Text -> [Text]
splitNewlines = T.splitOn "\r\n" >=> T.split isNewlineish
where
isNewlineish c = c == '\n' || c == '\r' || c == '\f'
-- See GHC's 'lexMultilineString'.
expandLeadingTabs :: Text -> Text
expandLeadingTabs = T.concat . go 0
where
go :: Int -> Text -> [Text]
go col s = case T.breakOn "\t" s of
(pre, T.uncons -> Just (_, suf)) ->
let col' = col + T.length pre
fill = 8 - (col' `mod` 8)
in pre : T.replicate fill " " : go (col' + fill) suf
_ -> [s]
-- Don't touch the first line, and remove common whitespace from all
-- remaining lines as well as convert those consisting only of whitespace to
-- empty lines.
rmCommonWhitespacePrefixAndBlank :: [Text] -> [Text]
rmCommonWhitespacePrefixAndBlank = \case
[] -> []
hd : tl -> hd : tl'
where
(leadingSpaces, tl') = unzip $ countLeadingAndBlank <$> tl
commonWs :: Int
commonWs = maybe 0 getMin $ mconcat leadingSpaces
countLeadingAndBlank :: Text -> (Maybe (Min Int), Text)
countLeadingAndBlank l
| T.all is_space l = (Nothing, "")
| otherwise = (Just $ Min leadingSpace, T.drop commonWs l)
where
leadingSpace = T.length $ T.takeWhile is_space l