@@ -1358,28 +1358,11 @@ object Parsers {
13581358 * @param offset The source offset where the string literal begins
13591359 * @return The dedented string, or str if errors were reported
13601360 */
1361- private def dedentString (str : String , offset : Offset ): String = {
1362- if (str.isEmpty) return str
1363-
1364- // Find the last line (should be just whitespace before closing delimiter)
1365- val lastNewlineIdx = str.lastIndexOf('\n ' )
1366-
1367- if (lastNewlineIdx < 0 ) {
1368- syntaxError(
1369- em " dedented string literal must start with newline after opening quotes " ,
1370- offset
1371- )
1372- return str
1373- }
1374-
1375- val closingIndent = str.substring(lastNewlineIdx + 1 )
1376- if (! closingIndent.forall(_.isWhitespace)) {
1377- syntaxError(
1378- em " last line of dedented string literal must contain only whitespace before closing delimiter " ,
1379- offset
1380- )
1381- return str
1382- }
1361+ private def dedentString (str : String ,
1362+ offset : Offset ,
1363+ closingIndent : String ,
1364+ isFirstPart : Boolean ,
1365+ isLastPart : Boolean ): String = {
13831366
13841367 // Check for mixed tabs and spaces in closing indent
13851368 val hasTabs = closingIndent.contains('\t ' )
@@ -1395,10 +1378,8 @@ object Parsers {
13951378 // Split into lines
13961379 val linesAndWithSeps = (str.linesIterator.zip(str.linesWithSeparators)).toSeq
13971380
1398- // Process all lines except the first (which is empty before the first newline)
1399- // and the last (which is just the closing indentation)
14001381 var lineOffset = offset
1401- val dedented = linesAndWithSeps.drop( 1 ).dropRight( 1 ).map { case ( line, lineWithSep) =>
1382+ def dedentLine ( line : String , lineWithSep : String ) = {
14021383 val result =
14031384 if (line.startsWith(closingIndent)) line.substring(closingIndent.length)
14041385 else if (line.trim.isEmpty) " " // Empty or whitespace-only lines
@@ -1424,7 +1405,30 @@ object Parsers {
14241405 result
14251406 }
14261407
1427- dedented.mkString(" \n " )
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)
1417+ }
1418+
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+ }
1423+
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 " )
14281432 }
14291433
14301434 /** Literal ::= SimpleLiteral
@@ -1463,7 +1467,7 @@ object Parsers {
14631467 // For non-interpolated dedented strings, check if the token starts with '''
14641468 val str = in.strVal
14651469 if (token == STRINGLIT && ! inStringInterpolation && isDedentedStringLiteral(negOffset)) {
1466- dedentString(str, negOffset)
1470+ dedentString(str, negOffset, extractClosingIndent(str, negOffset), true , true )
14671471 } else str
14681472 case TRUE => true
14691473 case FALSE => false
@@ -1545,14 +1549,11 @@ object Parsers {
15451549 in.buf(in.charOffset + 1 ) == '"'
15461550 val isDedented =
15471551 in.charOffset + 2 < in.buf.length &&
1552+ in.buf(in.charOffset - 1 ) == '\' ' &&
15481553 in.buf(in.charOffset) == '\' ' &&
1549- in.buf(in.charOffset + 1 ) == '\' ' &&
1550- in.buf(in.charOffset + 2 ) == '\' '
1551-
1554+ in.buf(in.charOffset + 1 ) == '\' '
15521555 in.nextToken()
15531556
1554- // For dedented strings, we need to collect all string parts first,
1555- // then dedent them all based on the closing indentation
15561557 if (isDedented) {
15571558 // Collect all string parts and their offsets
15581559 val stringParts = new ListBuffer [(String , Offset )]
@@ -1599,11 +1600,11 @@ object Parsers {
15991600 // Now dedent all string parts based on the last one's closing indentation
16001601 if (stringParts.nonEmpty) {
16011602 val lastPart = stringParts.last._1
1602- val closingIndent = extractClosingIndent(lastPart)
1603+ val closingIndent = extractClosingIndent(lastPart, in.offset )
16031604
16041605 // Dedent all parts
1605- val dedentedParts = stringParts.map { case (str, offset) =>
1606- (dedentStringPart (str, closingIndent), offset)
1606+ val dedentedParts = stringParts.zipWithIndex. map { case (( str, offset), index ) =>
1607+ (dedentString (str, in.offset, closingIndent, index == 0 , index == stringParts.length - 1 ), offset)
16071608 }
16081609
16091610 // Build the segments with dedented strings
@@ -1658,30 +1659,16 @@ object Parsers {
16581659 }
16591660
16601661 /** Extract the closing indentation from the last line of a string */
1661- private def extractClosingIndent (str : String ): String = {
1662- val lastNewlineIdx = str.lastIndexOf('\n ' )
1663- if (lastNewlineIdx < 0 ) " " else str.substring(lastNewlineIdx + 1 )
1664- }
1665-
1666- /** Dedent a string part by removing the specified indentation from each line */
1667- private def dedentStringPart (str : String , closingIndent : String ): String = {
1668- if (str.isEmpty || closingIndent.isEmpty) return str
1669-
1670- val lines = str.split(" \n " , - 1 ) // -1 to keep trailing empty strings
1671-
1672- val dedented = lines.map { line =>
1673- if (line.startsWith(closingIndent)) {
1674- line.substring(closingIndent.length)
1675- } else if (line.trim.isEmpty) {
1676- // Empty or whitespace-only lines
1677- " "
1678- } else {
1679- // Line doesn't start with the closing indentation, keep as-is
1680- line
1681- }
1662+ private def extractClosingIndent (str : String , offset : Offset ): String = {
1663+ val closingIndent = str.linesIterator.toSeq.last
1664+ if (! closingIndent.forall(_.isWhitespace)) {
1665+ syntaxError(
1666+ em " last line of dedented string literal must contain only whitespace before closing delimiter " ,
1667+ offset
1668+ )
1669+ return str
16821670 }
1683-
1684- dedented.mkString(" \n " )
1671+ closingIndent
16851672 }
16861673
16871674/* ------------- NEW LINES ------------------------------------------------- */
0 commit comments