From 470228313ac1f58a9ace23d37b00ec95147628c7 Mon Sep 17 00:00:00 2001 From: Tomasz Godzik Date: Mon, 29 Jul 2024 09:48:29 +0200 Subject: [PATCH] bugfix: Switch implementation of on type formatting --- .../metals/formatting/MultilineString.scala | 214 ++++++++++-------- .../tests/feature/SyntaxErrorLspSuite.scala | 8 +- 2 files changed, 127 insertions(+), 95 deletions(-) diff --git a/metals/src/main/scala/scala/meta/internal/metals/formatting/MultilineString.scala b/metals/src/main/scala/scala/meta/internal/metals/formatting/MultilineString.scala index 86d7e9e9b9e..20d18535328 100644 --- a/metals/src/main/scala/scala/meta/internal/metals/formatting/MultilineString.scala +++ b/metals/src/main/scala/scala/meta/internal/metals/formatting/MultilineString.scala @@ -3,6 +3,7 @@ package scala.meta.internal.metals.formatting import scala.annotation.tailrec import scala.meta +import scala.meta.XtensionClassifiable import scala.meta.internal.metals.UserConfiguration import scala.meta.internal.mtags.MtagsEnrichments._ import scala.meta.tokens.Token @@ -88,43 +89,6 @@ case class MultilineString(userConfig: () => UserConfiguration) if (lastQuote != -1) Some((lastQuote, quoteClosed)) else None } - private def getIndexOfLastOpenTripleQuote( - closedFromPreviousLines: Boolean, - line: String, - ): Option[(Int, Boolean)] = { - var lastTripleQuote = -1 - var tripleQuoteClosed = closedFromPreviousLines - var quoteNum = 0 - for (i <- 0 until line.size) { - val char = line(i) - if (char == '"') { - quoteNum = quoteNum + 1 - if (quoteNum == 3) { - lastTripleQuote = i - tripleQuoteClosed = !tripleQuoteClosed - quoteNum = 0 - } - } else { - quoteNum = 0 - } - } - if (lastTripleQuote != -1) Some((lastTripleQuote, tripleQuoteClosed)) - else None - } - - private def onlyFourQuotes( - splitLines: Array[String], - position: Position, - ): Boolean = { - - val currentLine = splitLines(position.getLine) - val pos = position.getCharacter - val onlyFour = 4 - hasNQuotes(pos - 3, currentLine, onlyFour) && currentLine.count( - _ == quote - ) == onlyFour - } - private def hasNQuotes(start: Int, text: String, n: Int): Boolean = (start until start + n).forall(i => if (i < 0 || i >= text.length) false else text(i) == quote @@ -248,35 +212,6 @@ case class MultilineString(userConfig: () => UserConfiguration) pipeBetweenLastLineAndPos != -1 || pipeBetweenSelection != -1 } - private def doubleQuoteNotClosed( - splitLines: Array[String], - position: Position, - ): Boolean = { - val lineBefore = splitLines(position.getLine - 1) - getIndexOfLastOpenQuote(lineBefore).exists { case (_, quoteClosed) => - !quoteClosed - } - } - - private def wasTripleQuoted( - splitLines: Array[String], - position: Position, - ): Boolean = { - var closedFromPreviousLines = true - var existed = false - for (i <- 0 until position.getLine()) { - val currentLine = splitLines(i) - getIndexOfLastOpenTripleQuote(closedFromPreviousLines, currentLine) - .foreach { case (_, quoteClosed) => - closedFromPreviousLines = quoteClosed - existed = true - } - } - if (existed) - !closedFromPreviousLines - else false - } - private def fixStringNewline( position: Position, splitLines: Array[String], @@ -412,37 +347,130 @@ case class MultilineString(userConfig: () => UserConfiguration) override def contribute( params: OnTypeFormatterParams + ): Option[List[TextEdit]] = { + params.triggerChar.head match { + case '"' => contributeQuotes(params) + case '\n' => contributeNewline(params) + } + } + + private def contributeQuotes( + params: OnTypeFormatterParams + ): Option[List[TextEdit]] = { + val range = new Range(params.position, params.position) + params.tokens + .getOrElse(Nil) + .sliding(3) + .collectFirst { + // `s""""` <- four quotes interpolation + case Seq( + start: Token.Interpolation.Start, + part: Token.Interpolation.Part, + _: Token.Invalid, + ) + if part.pos.encloses(range) && start.pos.text + .startsWith("\"\"\"") && part.pos.text.startsWith("\"") => + Some(replaceWithSixQuotes(params.position)) + // `""""` <- simple four quotes + case Seq(token: Token.Constant.String, _: Token.Invalid, _) + if token.pos + .encloses(range) && token.pos.text.startsWith("\"\"\"\"") => + Some(replaceWithSixQuotes(params.position)) + } + .flatten + } + + private def contributeNewline( + params: OnTypeFormatterParams ): Option[List[TextEdit]] = { val splitLines = params.splitLines val position = params.position - val triggerChar = params.triggerChar - (params.tokens, triggerChar) match { - case (Some(tokens), "\n") => - getStringLiterals(tokens, params, true) - .map { expr => - if (expr.hasStripMargin) - indent(splitLines, position, expr) - else - indentWhenNoStripMargin( - expr, - splitLines, - position, - ) - } - .find(_.nonEmpty) - case (None, "\"") if onlyFourQuotes(splitLines, position) => - Some(replaceWithSixQuotes(position)) - case (None, "\n") - if wasTripleQuoted( - splitLines, - position, - ) => - Some(addTripleQuote(position)) - case (None, "\n") if doubleQuoteNotClosed(splitLines, position) => - Some(fixStringNewline(position, splitLines)) - - case _ => None + val range = new Range(params.position, params.position) + val sourceText = params.sourceText + + def isUnfinishedTripleQuote(token: Token) = { + token.pos.startLine != token.pos.endLine && + token.end - token.start >= 6 && + (sourceText.subSequence(token.start, token.start + 3) != "\"\"\"" || + sourceText.subSequence(token.end - 3, token.end) != "\"\"\"") + } + + def isUnfinishedDoubleQuote(token: Token.Constant.String) = { + token.start != token.end && ( + sourceText(token.start) != '"' || + sourceText(token.end - 1) != '"' + ) + } + params.tokens + .getOrElse(Nil) + .sliding(3) + .collectFirst { + /* + *``` + * """some text without ending triple quotes + *``` + */ + case Seq(token: Token.Constant.String, _: Token.Invalid, _) + if token.pos.encloses(range) && + isUnfinishedTripleQuote(token) => + Some(addTripleQuote(position)) + /* + *``` + * sql"""some interpolated text without ending triple quotes + *``` + */ + case Seq( + start: Token.Interpolation.Start, + part: Token.Interpolation.Part, + _: Token.Invalid, + ) + if part.pos.encloses(range) && + start.pos.text.startsWith("\"\"\"") => + Some(addTripleQuote(position)) + /* + *``` + * s"some interpolated text without ending quote + * some textwithout starting quote" + *``` + */ + case Seq( + _: Token.Interpolation.Part, + token: Token.Invalid, + _, + ) if token.pos.endLine == position.getLine - 1 => // we added enter + Some(fixStringNewline(position, splitLines)) + /* + *``` + * "some text without ending quote + * some textwithout starting quote" + *``` + */ + case Seq(token: Token.Constant.String, _, _) + if isUnfinishedDoubleQuote(token) => + Some(fixStringNewline(position, splitLines)) + + /* + * Otherwise, we are inside a multiline string or interpolation. + */ + case Seq(token, _, _) + if (token.is[Token.Constant.String] || token + .is[Token.Interpolation.Part]) && + token.pos.encloses(range) && params.tokens.nonEmpty => + getStringLiterals(params.tokens.get, params, true) + .map { expr => + if (expr.hasStripMargin) + indent(splitLines, position, expr) + else + indentWhenNoStripMargin( + expr, + splitLines, + position, + ) + } + .find(_.nonEmpty) + } + .flatten } override def contribute( diff --git a/tests/slow/src/test/scala/tests/feature/SyntaxErrorLspSuite.scala b/tests/slow/src/test/scala/tests/feature/SyntaxErrorLspSuite.scala index a7abf26f2f9..f8b2bfea5aa 100644 --- a/tests/slow/src/test/scala/tests/feature/SyntaxErrorLspSuite.scala +++ b/tests/slow/src/test/scala/tests/feature/SyntaxErrorLspSuite.scala @@ -209,10 +209,14 @@ class SyntaxErrorLspSuite extends BaseLspSuite("syntax-error") { _ <- server.didChange("a/src/main/scala/A.scala")( _.replace("\"\"", "\"") ) - // assert that a tokenization error results in a single diagnostic, hides type errors. _ = assertNoDiff( client.workspaceDiagnostics, - """|a/src/main/scala/A.scala:2:16: error: unclosed string literal + """|a/src/main/scala/A.scala:2:16: error: type mismatch; + | found : String("") + | required: Int + | val x: Int = " + | ^ + |a/src/main/scala/A.scala:2:16: error: unclosed string literal | val x: Int = " | ^ |""".stripMargin,