diff --git a/compiler/src/dotty/tools/dotc/config/Feature.scala b/compiler/src/dotty/tools/dotc/config/Feature.scala index fa262a5880ff..2cdd1ceeb1ce 100644 --- a/compiler/src/dotty/tools/dotc/config/Feature.scala +++ b/compiler/src/dotty/tools/dotc/config/Feature.scala @@ -120,6 +120,9 @@ object Feature: def fewerBracesEnabled(using Context) = sourceVersion.isAtLeast(`3.3`) || enabled(fewerBraces) + def indentAfterOperatorEnabled(using Context) = + enabled(fewerBraces) + /** If current source migrates to `version`, issue given warning message * and return `true`, otherwise return `false`. */ diff --git a/compiler/src/dotty/tools/dotc/parsing/Parsers.scala b/compiler/src/dotty/tools/dotc/parsing/Parsers.scala index aec3ad42feef..d47fb7bc298b 100644 --- a/compiler/src/dotty/tools/dotc/parsing/Parsers.scala +++ b/compiler/src/dotty/tools/dotc/parsing/Parsers.scala @@ -1003,6 +1003,10 @@ object Parsers { */ def nextCanFollowOperator(leadingOperandTokens: BitSet): Boolean = leadingOperandTokens.contains(in.lookahead.token) + || in.indentAfterOperatorEnabled + && in.lineOffset >= 0 // operator is on its own line + && in.lookahead.lineOffset >= 0 // and next line is indented + && in.currentRegion.indentWidth < in.indentWidth(in.lookahead.offset) || in.postfixOpsEnabled || in.lookahead.token == COLONop || in.lookahead.token == EOF // important for REPL completions diff --git a/compiler/src/dotty/tools/dotc/parsing/Scanners.scala b/compiler/src/dotty/tools/dotc/parsing/Scanners.scala index 44b0c43e545b..078897cd588d 100644 --- a/compiler/src/dotty/tools/dotc/parsing/Scanners.scala +++ b/compiler/src/dotty/tools/dotc/parsing/Scanners.scala @@ -84,10 +84,6 @@ object Scanners { /** Is current token first one after a newline? */ def isAfterLineEnd: Boolean = lineOffset >= 0 - def isOperator = - token == BACKQUOTED_IDENT - || token == IDENTIFIER && isOperatorPart(name(name.length - 1)) - def isArrow = token == ARROW || token == CTXARROW } @@ -160,9 +156,9 @@ object Scanners { strVal = litBuf.toString litBuf.clear() - @inline def isNumberSeparator(c: Char): Boolean = c == '_' + inline def isNumberSeparator(c: Char): Boolean = c == '_' - @inline def removeNumberSeparators(s: String): String = if (s.indexOf('_') == -1) s else s.replace("_", "") + inline def removeNumberSeparators(s: String): String = if (s.indexOf('_') == -1) s else s.replace("_", "") // disallow trailing numeric separator char, but continue lexing def checkNoTrailingSeparator(): Unit = @@ -194,7 +190,6 @@ object Scanners { ((if (Config.defaultIndent) !noindentSyntax else ctx.settings.indent.value) || rewriteNoIndent) && allowIndent - if (rewrite) { val s = ctx.settings val rewriteTargets = List(s.newSyntax, s.oldSyntax, s.indent, s.noindent) @@ -209,6 +204,7 @@ object Scanners { def featureEnabled(name: TermName) = Feature.enabled(name)(using languageImportContext) def erasedEnabled = featureEnabled(Feature.erasedDefinitions) + def indentAfterOperatorEnabled = featureEnabled(Feature.fewerBraces) private var postfixOpsEnabledCache = false private var postfixOpsEnabledCtx: Context = NoContext @@ -387,9 +383,10 @@ object Scanners { def nextToken(): Unit = val lastToken = token val lastName = name + val lastLineOffset = lineOffset adjustSepRegions(lastToken) getNextToken(lastToken) - if isAfterLineEnd then handleNewLine(lastToken) + if isAfterLineEnd then handleNewLine(lastToken, lastName, lastLineOffset) postProcessToken(lastToken, lastName) profile.recordNewToken() printState() @@ -406,6 +403,10 @@ object Scanners { this.token = token } + def isOperator(token: Int, name: SimpleName): Boolean = + token == BACKQUOTED_IDENT + || token == IDENTIFIER && isOperatorPart(name(name.length - 1)) + /** A leading symbolic or backquoted identifier is treated as an infix operator if * - it does not follow a blank line, and * - it is followed by at least one whitespace character and a @@ -416,7 +417,7 @@ object Scanners { */ def isLeadingInfixOperator(nextWidth: IndentWidth = indentWidth(offset), inConditional: Boolean = true) = allowLeadingInfixOperators - && isOperator + && isOperator(token, name) && (isWhitespace(ch) || ch == LF) && !pastBlankLine && { @@ -434,15 +435,17 @@ object Scanners { // leading infix operator. def assumeStartsExpr(lexeme: TokenData) = (canStartExprTokens.contains(lexeme.token) || lexeme.token == COLONeol) - && (!lexeme.isOperator || nme.raw.isUnary(lexeme.name)) + && (!isOperator(lexeme.token, lexeme.name) || nme.raw.isUnary(lexeme.name)) val lookahead = LookaheadScanner() lookahead.allowLeadingInfixOperators = false // force a NEWLINE a after current token if it is on its own line lookahead.nextToken() assumeStartsExpr(lookahead) || lookahead.token == NEWLINE - && assumeStartsExpr(lookahead.next) && indentWidth(offset) <= indentWidth(lookahead.next.offset) + && (assumeStartsExpr(lookahead.next) + || indentAfterOperatorEnabled + && indentWidth(offset) < indentWidth(lookahead.next.offset)) } && { currentRegion match @@ -547,7 +550,7 @@ object Scanners { * I.e. `a <= b` iff `b.startsWith(a)`. If indentation is significant it is considered an error * if the current indentation width and the indentation of the current token are incomparable. */ - def handleNewLine(lastToken: Token) = + def handleNewLine(lastToken: Token, lastName: SimpleName, lastLineOffset: Offset) = var indentIsSignificant = false var newlineIsSeparating = false var lastWidth = IndentWidth.Zero @@ -576,7 +579,11 @@ object Scanners { */ inline def isContinuing = lastWidth < nextWidth - && (openParensTokens.contains(token) || lastToken == RETURN) + && ( openParensTokens.contains(token) + || lastToken == RETURN + || indentAfterOperatorEnabled + && isOperator(lastToken, lastName) && lastLineOffset >= 0 + ) && !pastBlankLine && !migrateTo3 && !noindentSyntax @@ -631,7 +638,12 @@ object Scanners { else if lastWidth < nextWidth || lastWidth == nextWidth && (lastToken == MATCH || lastToken == CATCH) && token == CASE then - if canStartIndentTokens.contains(lastToken) then + if canStartIndentTokens.contains(lastToken) + || indentAfterOperatorEnabled + && isOperator(lastToken, lastName) + && lastLineOffset >= 0 + && canStartStatTokens3.contains(token) + then currentRegion = Indented(nextWidth, lastToken, currentRegion) insert(INDENT, offset) else if lastToken == SELFARROW then @@ -1088,7 +1100,7 @@ object Scanners { next class LookaheadScanner(val allowIndent: Boolean = false) extends Scanner(source, offset, allowIndent = allowIndent) { - override protected def initialCharBufferSize = 8 + override protected def initialCharBufferSize = 16 override def languageImportContext = Scanner.this.languageImportContext } diff --git a/docs/_docs/reference/changed-features/operators.md b/docs/_docs/reference/changed-features/operators.md index 0cf25d77bc11..4e55951a9653 100644 --- a/docs/_docs/reference/changed-features/operators.md +++ b/docs/_docs/reference/changed-features/operators.md @@ -167,6 +167,20 @@ Another example: This code is recognized as three different statements. `???` is syntactically a symbolic identifier, but neither of its occurrences is followed by a space and a token that can start an expression. +Indentation is significant after an operator that appears on its own line. +For instance, in +```scala +someCondition +|| + val helper = helperDef + anotherCondition(helper) +``` +an `` token is inserted [^1] after the `||`. Since `` can start as an expression, the `||` operator is classified as a leading infix operator. +``` + +[^1]: Currently only enabled with an `experimental.fewerBraces` language import or setting. + + ## Unary operators A unary operator must not have explicit parameter lists even if they are empty. diff --git a/docs/_docs/reference/other-new-features/indentation.md b/docs/_docs/reference/other-new-features/indentation.md index 9963d1ee7577..7665201a2a49 100644 --- a/docs/_docs/reference/other-new-features/indentation.md +++ b/docs/_docs/reference/other-new-features/indentation.md @@ -68,8 +68,10 @@ There are two rules: = => ?=> <- catch do else finally for if match return then throw try while yield ``` + , or - - after the closing `)` of a condition in an old-style `if` or `while`. + - after an operator that appears on its own line [^1], or + - after the closing `)` of a condition in an old-style `if` or `while`, or - after the closing `)` or `}` of the enumerations of an old-style `for` loop without a `do`. If an `` is inserted, the indentation width of the token on the next line @@ -143,6 +145,8 @@ else d ``` is parsed as `if x then a + b + c else d`. +[^1]: Currently only enabled with an `experimental.fewerBraces` language import or setting. + ## Optional Braces Around Template Bodies The Scala grammar uses the term _template body_ for the definitions of a class, trait, or object that are normally enclosed in braces. The braces around a template body can also be omitted by means of the following rule. @@ -199,6 +203,7 @@ Refinement ::= :<<< [RefineDcl] {semi [RefineDcl]} >>> Packaging ::= ‘package’ QualId :<<< TopStats >>> ``` + ## Optional Braces for Method Arguments Starting with Scala 3.3, a `` token is also recognized where a function argument would be expected. Examples: diff --git a/library/src/scala/runtime/stdLibPatches/language.scala b/library/src/scala/runtime/stdLibPatches/language.scala index c2a12cec2ecc..ccacad5259e2 100644 --- a/library/src/scala/runtime/stdLibPatches/language.scala +++ b/library/src/scala/runtime/stdLibPatches/language.scala @@ -51,7 +51,7 @@ object language: /** Experimental support for using indentation for arguments */ @compileTimeOnly("`fewerBraces` can only be used at compile time in import statements") - @deprecated("`fewerBraces` is now standard, no language import is needed", since = "3.3") + //@deprecated("`fewerBraces` is now standard, no language import is needed", since = "3.3") object fewerBraces /** Experimental support for typechecked exception capabilities diff --git a/tests/pos/indent-ops.scala b/tests/pos/indent-ops.scala new file mode 100644 index 000000000000..b5e4ca62d1b0 --- /dev/null +++ b/tests/pos/indent-ops.scala @@ -0,0 +1,28 @@ +import language.experimental.fewerBraces + +def test(y: Int) = + val firstValue = y + * y + val secondValue = + firstValue + + + if firstValue < 0 then 1 else 0 + + + if y < 0 then y else -y + + val result = + firstValue < secondValue + || + val thirdValue = firstValue * secondValue + thirdValue > 100 + || + def avg(x: Double, y: Double) = (x + y)/2 + avg(firstValue, secondValue) > 0.0 + || + firstValue + * secondValue + * + val firstSquare = firstValue * firstValue + firstSquare + firstSquare + <= + firstValue `max` secondValue