Skip to content

Commit 1a831d1

Browse files
authored
Preserve leading whitespace in multi-line strings (#1550)
… by moving any leading whitespace on the _last_ line into a string interpolation. This ensures that the parser can find the correct indentation level, no matter what the other lines contain. The additional null-check in prettyChunks is necessary to preserve formatting idempotence. Otherwise "\n " is first formatted as '' ${" "}'' but turns into "\n${" "}" on re-formatting. Fixes #1545.
1 parent 145b7b8 commit 1a831d1

File tree

8 files changed

+79
-11
lines changed

8 files changed

+79
-11
lines changed

dhall/src/Dhall/Pretty/Internal.hs

Lines changed: 66 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1089,7 +1089,7 @@ prettyCharacterSet characterSet expression =
10891089
prettyChunks :: Pretty a => Chunks Src a -> Doc Ann
10901090
prettyChunks chunks@(Chunks a b)
10911091
| anyText (== '\n') =
1092-
if anyText (/= '\n')
1092+
if not (null a) || anyText (/= '\n')
10931093
then long
10941094
else Pretty.flatAlt long short
10951095
| otherwise =
@@ -1139,24 +1139,80 @@ prettyCharacterSet characterSet expression =
11391139
prettyText t = literal (Pretty.pretty (escapeText_ t))
11401140

11411141

1142-
-- | Prepare 'Chunks' for multi-line formatting by interpolating characters that
1143-
-- may not appear in multi-line strings directly.
1142+
-- | Prepare 'Chunks' for multi-line formatting by escaping problematic
1143+
-- character sequences via string interpolations
11441144
--
1145+
-- >>> multilineChunks (Chunks [] "\n \tx")
1146+
-- Chunks [("\n",TextLit (Chunks [] " \t"))] "x"
11451147
-- >>> multilineChunks (Chunks [] "\n\NUL\b\f\t")
11461148
-- Chunks [("\n",TextLit (Chunks [] "\NUL\b\f"))] "\t"
1147-
multilineChunks :: Chunks Src a -> Chunks Src a
1148-
multilineChunks (Chunks as0 b0) = Chunks as1 b1
1149+
multilineChunks :: Chunks s a -> Chunks s a
1150+
multilineChunks = escapeControlCharacters . escapeLastLineLeadingWhitespace
1151+
1152+
-- | Escape leading whitespace on the last line by moving it into a string
1153+
-- string interpolation
1154+
--
1155+
-- Unescaped leading whitespace on the last line would otherwise be removed
1156+
-- by the parser's dedentation logic.
1157+
--
1158+
-- >>> escapeLastLineLeadingWhitespace (Chunks [] "\n \tx")
1159+
-- Chunks [("\n",TextLit (Chunks [] " \t"))] "x"
1160+
-- >>> escapeLastLineLeadingWhitespace (Chunks [("\n",Var (V "x" 0))] " ")
1161+
-- Chunks [("\n",Var (V "x" 0))] " "
1162+
-- >>> escapeLastLineLeadingWhitespace (Chunks [("\n ",Var (V "x" 0))] "")
1163+
-- Chunks [("\n",TextLit (Chunks [] " ")),("",Var (V "x" 0))] ""
1164+
-- >>> escapeLastLineLeadingWhitespace (Chunks [("\n ",Var (V "x" 0))] "\n")
1165+
-- Chunks [("\n ",Var (V "x" 0))] "\n"
1166+
--
1167+
-- We assume that there's at least one newline and may therefore ignore leading
1168+
-- whitespace on the first line:
1169+
--
1170+
-- >>> escapeLastLineLeadingWhitespace (Chunks [] " ")
1171+
-- Chunks [] " "
1172+
escapeLastLineLeadingWhitespace :: Chunks s a -> Chunks s a
1173+
escapeLastLineLeadingWhitespace (Chunks as0 b0) =
1174+
case escape1 b0 of
1175+
Nothing -> Chunks (escapeChunks as0) b0
1176+
Just (Chunks cs b1) -> Chunks (as0 ++ cs) b1
1177+
where
1178+
-- Nothing: No newline found
1179+
-- Just chunks: Newline was found!
1180+
escape1 :: Text -> Maybe (Chunks s a)
1181+
escape1 t = case Text.breakOnEnd "\n" t of
1182+
("", _) -> Nothing
1183+
(a , b) -> Just $ case Text.span predicate b of
1184+
("", _) -> Chunks [] t
1185+
(c , d) -> Chunks [(a, TextLit (Chunks [] c))] d
1186+
1187+
predicate c = c == ' ' || c == '\t'
1188+
1189+
escapeChunks = snd . foldr f (NotDone, [])
1190+
1191+
f chunk (Done , chunks) = (Done, chunk : chunks)
1192+
f (t, e) (NotDone, chunks) = case escape1 t of
1193+
Nothing -> (NotDone, (t, e) : chunks)
1194+
Just (Chunks as b) -> (Done, as ++ (b, e) : chunks)
1195+
1196+
data Done = NotDone | Done
1197+
1198+
-- | Escape control characters by moving them into string interpolations
1199+
--
1200+
-- >>> escapeControlCharacters (Chunks [] "\n\NUL\b\f\t")
1201+
-- Chunks [("\n",TextLit (Chunks [] "\NUL\b\f"))] "\t"
1202+
escapeControlCharacters :: Chunks s a -> Chunks s a
1203+
escapeControlCharacters (Chunks as0 b0) = Chunks as1 b1
11491204
where
1150-
as1 = foldr f (map toPair bs) as0
1205+
as1 = foldr f (map toChunk bs) as0
11511206

11521207
(bs, b1) = splitOnPredicate predicate b0
11531208

1154-
predicate c = Data.Char.isControl c && c /= ' ' && c /= '\t' && c /= '\n'
1209+
f (t0, e) chunks = map toChunk ts1 ++ (t1, e) : chunks
1210+
where
1211+
(ts1, t1) = splitOnPredicate predicate t0
11551212

1156-
f (t0, e) pairs = case splitOnPredicate predicate t0 of
1157-
(ts1, t1) -> map toPair ts1 ++ (t1, e) : pairs
1213+
predicate c = Data.Char.isControl c && c /= ' ' && c /= '\t' && c /= '\n'
11581214

1159-
toPair (t0, t1) = (t0, TextLit (Chunks [] t1))
1215+
toChunk (t0, t1) = (t0, TextLit (Chunks [] t1))
11601216

11611217
-- | Split `Text` on a predicate, preserving all parts of the original string.
11621218
--
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
"foo\n${a} bar"
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
''
2+
foo
3+
${a} bar''

dhall/tests/format/issue1545-1A.dhall

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
"\n\tx"

dhall/tests/format/issue1545-1B.dhall

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
''
2+
3+
${"\t"}x''

dhall/tests/format/issue1545-2A.dhall

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
"\n x"

dhall/tests/format/issue1545-2B.dhall

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
''
2+
3+
${" "}x''
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
''
22
${"\u0000"} $ \
3-
''
3+
${" "}''

0 commit comments

Comments
 (0)