@@ -1364,71 +1364,76 @@ object Parsers {
13641364 isFirstPart : Boolean ,
13651365 isLastPart : Boolean ): String = {
13661366
1367- // Check for mixed tabs and spaces in closing indent
1368- val hasTabs = closingIndent.contains('\t ' )
1369- val hasSpaces = closingIndent.contains(' ' )
1370- if (hasTabs && hasSpaces) {
1371- syntaxError(
1372- em " dedented string literal cannot mix tabs and spaces in indentation " ,
1373- offset
1374- )
1375- return str
1376- }
1367+ if (closingIndent == " " ) str
1368+ else {
1369+ // Check for mixed tabs and spaces in closing indent
1370+
1371+ val hasTabs = closingIndent.contains('\t ' )
1372+ val hasSpaces = closingIndent.contains(' ' )
1373+ if (hasTabs && hasSpaces) {
1374+ syntaxError(
1375+ em " dedented string literal cannot mix tabs and spaces in indentation " ,
1376+ offset
1377+ )
1378+ return str
1379+ }
13771380
1378- // Split into lines
1379- val linesAndWithSeps = (str.linesIterator.zip(str.linesWithSeparators)).toSeq
1381+ // Split into lines
1382+ val linesAndWithSeps = (str.linesIterator.zip(str.linesWithSeparators)).toSeq
13801383
1381- var lineOffset = offset
1382- def dedentLine (line : String , lineWithSep : String ) = {
1383- val result =
1384- if (line.startsWith(closingIndent)) line.substring(closingIndent.length)
1385- else if (line.trim.isEmpty) " " // Empty or whitespace-only lines
1386- else {
1387- // Check if this line has mixed tabs/spaces that don't match closing indent
1388- val lineIndent = line.takeWhile(_.isWhitespace)
1389- val lineHasTabs = lineIndent.contains('\t ' )
1390- val lineHasSpaces = lineIndent.contains(' ' )
1391- if ((hasTabs && lineHasSpaces && ! lineHasTabs) || (hasSpaces && lineHasTabs && ! lineHasSpaces)) {
1392- syntaxError(
1393- em " dedented string literal cannot mix tabs and spaces in indentation " ,
1394- offset
1395- )
1396- } else {
1397- syntaxError(
1398- em " line in dedented string literal must be indented at least as much as the closing delimiter " ,
1399- lineOffset
1400- )
1384+ var lineOffset = offset
1385+
1386+ def dedentLine (line : String , lineWithSep : String ) = {
1387+ val result =
1388+ if (line.startsWith(closingIndent)) line.substring(closingIndent.length)
1389+ else if (line.trim.isEmpty) " " // Empty or whitespace-only lines
1390+ else {
1391+ // Check if this line has mixed tabs/spaces that don't match closing indent
1392+ val lineIndent = line.takeWhile(_.isWhitespace)
1393+ val lineHasTabs = lineIndent.contains('\t ' )
1394+ val lineHasSpaces = lineIndent.contains(' ' )
1395+ if ((hasTabs && lineHasSpaces && ! lineHasTabs) || (hasSpaces && lineHasTabs && ! lineHasSpaces)) {
1396+ syntaxError(
1397+ em " dedented string literal cannot mix tabs and spaces in indentation " ,
1398+ offset
1399+ )
1400+ } else {
1401+ syntaxError(
1402+ em " line in dedented string literal must be indented at least as much as the closing delimiter " ,
1403+ lineOffset
1404+ )
1405+ }
1406+ line
14011407 }
1402- line
1408+ lineOffset += lineWithSep.length // Make sure to include any \n, \r, \r\n, or \n\r
1409+ result
1410+ }
1411+
1412+ // If this is the first part of a string, then the first line is the empty string following
1413+ // the opening `'''` delimiter, so we skip it. If not, then the first line is immediately
1414+ // following an interpolated value, and should be used raw without indenting
1415+ val firstLine =
1416+ if (isFirstPart) Nil
1417+ else {
1418+ val (line, lineWithSep) = linesAndWithSeps.head
1419+ lineOffset += lineWithSep.length
1420+ Seq (line)
14031421 }
1404- lineOffset += lineWithSep.length // Make sure to include any \n, \r, \r\n, or \n\r
1405- result
1406- }
14071422
1408- // If this is the first part of a string, then the first line is the empty string following
1409- // the opening `'''` delimiter, so we skip it. If not, then the first line is immediately
1410- // following an interpolated value, and should be used raw without indenting
1411- val firstLine =
1412- if (isFirstPart) Nil
1413- else {
1414- val (line, lineWithSep) = linesAndWithSeps.head
1415- lineOffset += lineWithSep.length
1416- Seq (line)
1423+ // Process all lines except the first and last, which require special handling
1424+ val dedented = linesAndWithSeps.drop(1 ).dropRight(1 ).map { case (line, lineWithSep) =>
1425+ dedentLine(line, lineWithSep)
14171426 }
14181427
1419- // Process all lines except the first and last, which require special handling
1420- val dedented = linesAndWithSeps.drop(1 ).dropRight(1 ).map { case (line, lineWithSep) =>
1421- dedentLine(line, lineWithSep)
1422- }
1428+ // If this is the last part of the string, then the last line is the indentation-only
1429+ // line preceding the closing delimiter, and should be ignored. If not, then the last line
1430+ // also needs to be de-dented
1431+ val lastLine =
1432+ if (isLastPart) Nil
1433+ else Seq (dedentLine(linesAndWithSeps.last._1, linesAndWithSeps.last._2))
14231434
1424- // If this is the last part of the string, then the last line is the indentation-only
1425- // line preceding the closing delimiter, and should be ignored. If not, then the last line
1426- // also needs to be de-dented
1427- val lastLine =
1428- if (isLastPart) Nil
1429- else Seq (dedentLine(linesAndWithSeps.last._1, linesAndWithSeps.last._2))
1430-
1431- (firstLine ++ dedented ++ lastLine).mkString(" \n " )
1435+ (firstLine ++ dedented ++ lastLine).mkString(" \n " )
1436+ }
14321437 }
14331438
14341439 /** Literal ::= SimpleLiteral
@@ -1597,21 +1602,22 @@ object Parsers {
15971602 } else false
15981603
15991604 val dedentedParts =
1600- if (! isDedented) stringParts
1605+ if (! isDedented || stringParts.isEmpty ) stringParts
16011606 else {
16021607 val lastPart = stringParts.last._1
16031608 val closingIndent = extractClosingIndent(lastPart, in.offset)
16041609 stringParts.zipWithIndex.map { case ((str, offset), index) =>
1605- (dedentString(str, in.offset, closingIndent, index == 0 , index == stringParts.length - 1 ), offset)
1610+ val dedented = dedentString(str, in.offset, closingIndent, index == 0 , index == stringParts.length - 1 )
1611+ (dedented, offset)
16061612 }
16071613 }
16081614
16091615 // Build the segments with dedented strings
1610- for (i <- 0 until dedentedParts.size - 1 ) {
1611- val (dedentedStr, offset) = dedentedParts(i)
1616+ for ((str, expr) <- dedentedParts.zip(interpolatedExprs) ) {
1617+ val (dedentedStr, offset) = str
16121618 segmentBuf += Thicket (
16131619 atSpan(offset, offset, offset + dedentedStr.length) { Literal (Constant (dedentedStr)) },
1614- interpolatedExprs(i)
1620+ expr
16151621 )
16161622 }
16171623
@@ -1626,14 +1632,26 @@ object Parsers {
16261632
16271633 /** Extract the closing indentation from the last line of a string */
16281634 private def extractClosingIndent (str : String , offset : Offset ): String = {
1629- val closingIndent = str.linesIterator.toSeq.last
1630- if (! closingIndent.forall(_.isWhitespace)) {
1635+ // If the last line is empty, `linesIterator` and `linesWithSeparators` skips
1636+ // the empty string, so we must recognize that case and explicitly default to ""
1637+ // otherwise things will blow up
1638+ val closingIndent = str
1639+ .linesIterator
1640+ .zip(str.linesWithSeparators)
1641+ .toSeq
1642+ .lastOption
1643+ .filter((line, lineWithSep) => line == lineWithSep)
1644+ .map(_._1)
1645+ .getOrElse(" " )
1646+
1647+ if (closingIndent.exists(! _.isWhitespace)) {
16311648 syntaxError(
16321649 em " last line of dedented string literal must contain only whitespace before closing delimiter " ,
16331650 offset
16341651 )
16351652 return str
16361653 }
1654+
16371655 closingIndent
16381656 }
16391657
0 commit comments