From bee65d9ee8627f5b1616d7acbeef9b9208446555 Mon Sep 17 00:00:00 2001 From: Som Snytt Date: Tue, 8 Apr 2025 08:47:27 -0700 Subject: [PATCH] Improve dentation at EOF Check trailing blank line at EOF for OUTDENT If EOF is preceded by only spaces on the last line, do not outdent because it will complain about alignment during an edit. Preserve EOF token when probing arrow EOL For REPL, `: x =>` at EOF sees an EOF in order to detect lambda eol. --- .../dotty/tools/dotc/parsing/Scanners.scala | 26 +++++++++++++++---- .../src/dotty/tools/dotc/util/Chars.scala | 2 +- .../dotty/tools/repl/ReplCompilerTests.scala | 10 +++++++ 3 files changed, 32 insertions(+), 6 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/parsing/Scanners.scala b/compiler/src/dotty/tools/dotc/parsing/Scanners.scala index ed20c189796b..6ae2f7999d33 100644 --- a/compiler/src/dotty/tools/dotc/parsing/Scanners.scala +++ b/compiler/src/dotty/tools/dotc/parsing/Scanners.scala @@ -603,6 +603,20 @@ object Scanners { lastWidth = r.knownWidth newlineIsSeparating = r.isInstanceOf[InBraces] + // can emit OUTDENT if line is not non-empty blank line at EOF + inline def isTrailingBlankLine: Boolean = + token == EOF && { + val end = buf.length - 1 // take terminal NL as empty last line + val prev = buf.lastIndexWhere(!isWhitespace(_), end = end) + prev < 0 || end - prev > 0 && isLineBreakChar(buf(prev)) + } + + inline def canDedent: Boolean = + lastToken != INDENT + && !isLeadingInfixOperator(nextWidth) + && !statCtdTokens.contains(lastToken) + && !isTrailingBlankLine + if newlineIsSeparating && canEndStatTokens.contains(lastToken) && canStartStatTokens.contains(token) @@ -615,9 +629,8 @@ object Scanners { || nextWidth == lastWidth && (indentPrefix == MATCH || indentPrefix == CATCH) && token != CASE then if currentRegion.isOutermost then if nextWidth < lastWidth then currentRegion = topLevelRegion(nextWidth) - else if !isLeadingInfixOperator(nextWidth) && !statCtdTokens.contains(lastToken) && lastToken != INDENT then + else if canDedent then currentRegion match - case _ if token == EOF => // no OUTDENT at EOF case r: Indented => insert(OUTDENT, offset) handleNewIndentWidth(r.enclosing, ir => @@ -671,13 +684,16 @@ object Scanners { reset() if atEOL then token = COLONeol - // consume => and insert if applicable + // consume => and insert if applicable. Used to detect colon arrow: x => def observeArrowIndented(): Unit = if isArrow && indentSyntax then peekAhead() - val atEOL = isAfterLineEnd || token == EOF + val atEOL = isAfterLineEnd + val atEOF = token == EOF reset() - if atEOL then + if atEOF then + token = EOF + else if atEOL then val nextWidth = indentWidth(next.offset) val lastWidth = currentRegion.indentWidth if lastWidth < nextWidth then diff --git a/compiler/src/dotty/tools/dotc/util/Chars.scala b/compiler/src/dotty/tools/dotc/util/Chars.scala index 916bdfa9dca3..e68c48903a63 100644 --- a/compiler/src/dotty/tools/dotc/util/Chars.scala +++ b/compiler/src/dotty/tools/dotc/util/Chars.scala @@ -50,7 +50,7 @@ object Chars: } /** Is character a whitespace character (but not a new line)? */ - def isWhitespace(c: Char): Boolean = + inline def isWhitespace(c: Char): Boolean = c == ' ' || c == '\t' || c == CR /** Can character form part of a doc comment variable $xxx? */ diff --git a/compiler/test/dotty/tools/repl/ReplCompilerTests.scala b/compiler/test/dotty/tools/repl/ReplCompilerTests.scala index d32b28647c32..0592cbbed1be 100644 --- a/compiler/test/dotty/tools/repl/ReplCompilerTests.scala +++ b/compiler/test/dotty/tools/repl/ReplCompilerTests.scala @@ -511,6 +511,16 @@ class ReplCompilerTests extends ReplTest: val all = lines() assertTrue(hints.forall(hint => all.exists(_.contains(hint)))) + @Test def `i22844 regression colon eol`: Unit = initially: + run: + """|println: + | "hello, world" + |""".stripMargin // outdent, but this test does not exercise the bug + assertEquals(List("hello, world"), lines()) + + @Test def `i22844b regression colon arrow eol`: Unit = contextually: + assertTrue(ParseResult.isIncomplete("List(42).map: x =>")) + object ReplCompilerTests: private val pattern = Pattern.compile("\\r[\\n]?|\\n");