diff --git a/metals/src/main/scala/scala/meta/internal/metals/WorkspaceLspService.scala b/metals/src/main/scala/scala/meta/internal/metals/WorkspaceLspService.scala index 088ca6a5dc9..0cc568c7374 100644 --- a/metals/src/main/scala/scala/meta/internal/metals/WorkspaceLspService.scala +++ b/metals/src/main/scala/scala/meta/internal/metals/WorkspaceLspService.scala @@ -1211,7 +1211,7 @@ class WorkspaceLspService( capabilities.setCompletionProvider( new lsp4j.CompletionOptions( clientConfig.isCompletionItemResolve(), - List(".", "*", "$").asJava, + List(".", "*", "$", "`").asJava, ) ) capabilities.setCallHierarchyProvider(true) diff --git a/mtags/src/main/scala-2/scala/meta/internal/pc/CompletionProvider.scala b/mtags/src/main/scala-2/scala/meta/internal/pc/CompletionProvider.scala index 2d6139c1c45..8c7c2fbfd2d 100644 --- a/mtags/src/main/scala-2/scala/meta/internal/pc/CompletionProvider.scala +++ b/mtags/src/main/scala-2/scala/meta/internal/pc/CompletionProvider.scala @@ -57,10 +57,15 @@ class CompletionProvider( val pos = unit.position(params.offset) val isSnippet = isSnippetEnabled(pos, params.text()) - val (i, completion, editRange, query) = safeCompletionsAt(pos, params.uri()) - - val start = inferIdentStart(pos, params.text()) - val end = inferIdentEnd(pos, params.text()) + val (i, completion, identOffsets, editRange, query) = + safeCompletionsAt(pos, params.uri()) + + val InferredIdentOffsets( + start, + end, + hadLeadingBacktick, + hadTrailingBacktick + ) = identOffsets val oldText = params.text().substring(start, end) val stripSuffix = pos.withStart(start).withEnd(end).toLsp @@ -275,7 +280,12 @@ class CompletionProvider( if (item.getTextEdit == null) { val editText = member match { case _: NamedArgMember => item.getLabel - case _ => ident + case _ => + val ident0 = + if (hadLeadingBacktick) ident.stripPrefix("`") else ident + val ident1 = + if (hadTrailingBacktick) ident0.stripSuffix("`") else ident0 + ident1 } item.setTextEdit(textEdit(editText)) } @@ -502,9 +512,16 @@ class CompletionProvider( private def safeCompletionsAt( pos: Position, source: URI - ): (InterestingMembers, CompletionPosition, l.Range, String) = { + ): ( + InterestingMembers, + CompletionPosition, + InferredIdentOffsets, + l.Range, + String + ) = { + lazy val inferredIdentOffsets = inferIdentOffsets(pos, params.text()) lazy val editRange = pos - .withStart(inferIdentStart(pos, params.text())) + .withStart(inferredIdentOffsets.start) .withEnd(pos.point) .toLsp val noQuery = "$a" @@ -522,6 +539,7 @@ class CompletionProvider( ( InterestingMembers(Nil, SymbolSearch.Result.COMPLETE), NoneCompletion, + inferredIdentOffsets, editRange, noQuery ) @@ -532,6 +550,7 @@ class CompletionProvider( SymbolSearch.Result.COMPLETE ), completion, + inferredIdentOffsets, editRange, noQuery ) @@ -579,7 +598,7 @@ class CompletionProvider( params.text() ) params.checkCanceled() - (items, completion, editRange, query) + (items, completion, inferredIdentOffsets, editRange, query) } catch { case e: CyclicReference if e.getMessage.contains("illegal cyclic reference") => diff --git a/mtags/src/main/scala-2/scala/meta/internal/pc/completions/Completions.scala b/mtags/src/main/scala-2/scala/meta/internal/pc/completions/Completions.scala index 8958f03b787..3e9a15c8700 100644 --- a/mtags/src/main/scala-2/scala/meta/internal/pc/completions/Completions.scala +++ b/mtags/src/main/scala-2/scala/meta/internal/pc/completions/Completions.scala @@ -905,19 +905,71 @@ trait Completions { this: MetalsGlobal => result } - def inferStart( + case class InferredIdentOffsets( + start: Int, + end: Int, + strippedLeadingBacktick: Boolean, + strippedTrailingBacktick: Boolean + ) + + def inferIdentOffsets( pos: Position, - text: String, - charPred: Char => Boolean - ): Int = { - def fallback: Int = { + text: String + ): InferredIdentOffsets = { + + // If we fail to find a tree, approximate with a heurstic about ident characters + def fallbackStart: Int = { var i = pos.point - 1 - while (i >= 0 && charPred(text.charAt(i))) { + while (i >= 0 && Chars.isIdentifierPart(text.charAt(i))) { i -= 1 } i + 1 } - def loop(enclosing: List[Tree]): Int = + def fallbackEnd: Int = { + findEnd(false) + } + + def findEnd(hasBacktick: Boolean): Int = { + val predicate: Char => Boolean = if (hasBacktick) { (ch: Char) => + !Chars.isLineBreakChar(ch) && ch != '`' + } else { + Chars.isIdentifierPart(_) + } + + var i = pos.point + while (i < text.length && predicate(text.charAt(i))) { + i += 1 + } + i + } + def fallback = + InferredIdentOffsets(fallbackStart, fallbackEnd, false, false) + + def refTreePos(refTree: RefTree): InferredIdentOffsets = { + val refTreePos = treePos(refTree) + var startPos = refTreePos.point + var strippedLeadingBacktick = false + if (text.charAt(startPos) == '`') { + startPos += 1 + strippedLeadingBacktick = true + } + + val endPos = findEnd(strippedLeadingBacktick) + var strippedTrailingBacktick = false + if (endPos < text.length) { + if (text.charAt(endPos) == '`') { + strippedTrailingBacktick = true + } + } + InferredIdentOffsets( + Math.min(startPos, pos.point), + endPos, + strippedLeadingBacktick, + strippedTrailingBacktick + ) + } + + def loop(enclosing: List[Tree]): InferredIdentOffsets = enclosing match { case Nil => fallback case head :: tl => @@ -925,37 +977,21 @@ trait Completions { this: MetalsGlobal => else { head match { case i: Ident => - treePos(i).point - case Select(qual, _) if !treePos(qual).includes(pos) => - treePos(head).point + refTreePos(i) + case sel @ Select(qual, _) if !treePos(qual).includes(pos) => + refTreePos(sel) case _ => fallback } } } - val start = loop(lastVisitedParentTrees) - Math.min(start, pos.point) + loop(lastVisitedParentTrees) } - /** Can character form part of an alphanumeric Scala identifier? */ - private def isIdentifierPart(c: Char) = - (c == '$') || Character.isUnicodeIdentifierPart(c) - /** * Returns the start offset of the identifier starting as the given offset position. */ def inferIdentStart(pos: Position, text: String): Int = - inferStart(pos, text, isIdentifierPart) - - /** - * Returns the end offset of the identifier starting as the given offset position. - */ - def inferIdentEnd(pos: Position, text: String): Int = { - var i = pos.point - while (i < text.length && Chars.isIdentifierPart(text.charAt(i))) { - i += 1 - } - i - } + inferIdentOffsets(pos, text).start def isSnippetEnabled(pos: Position, text: String): Boolean = { pos.point < text.length() && { diff --git a/tests/cross/src/test/scala/tests/pc/CompletionSuite.scala b/tests/cross/src/test/scala/tests/pc/CompletionSuite.scala index 08883bc6e68..af448508761 100644 --- a/tests/cross/src/test/scala/tests/pc/CompletionSuite.scala +++ b/tests/cross/src/test/scala/tests/pc/CompletionSuite.scala @@ -2091,4 +2091,63 @@ class CompletionSuite extends BaseCompletionSuite { } ) + val BacktickCompletionsTag = + IgnoreScala3.and(IgnoreScalaVersion.forLessThan("2.13.17")) + + checkEdit( + "add-backticks-around-identifier".tag(BacktickCompletionsTag), + """|object Main { + | def `Foo Bar` = 123 + | Foo@@ + |} + |""".stripMargin, + """|object Main { + | def `Foo Bar` = 123 + | `Foo Bar` + |} + |""".stripMargin + ) + + checkEdit( + "complete-inside-backticks".tag(BacktickCompletionsTag), + """|object Main { + | def `Foo Bar` = 123 + | `Foo@@` + |} + |""".stripMargin, + """|object Main { + | def `Foo Bar` = 123 + | `Foo Bar` + |} + |""".stripMargin + ) + + checkEdit( + "complete-inside-backticks-after-space".tag(BacktickCompletionsTag), + """|object Main { + | def `Foo Bar` = 123 + | `Foo B@@` + |} + |""".stripMargin, + """|object Main { + | def `Foo Bar` = 123 + | `Foo Bar` + |} + |""".stripMargin + ) + + checkEdit( + "complete-inside-empty-backticks".tag(BacktickCompletionsTag), + """|object Main { + | def `Foo Bar` = 123 + | `@@` + |} + |""".stripMargin, + """|object Main { + | def `Foo Bar` = 123 + | `Foo Bar` + |} + |""".stripMargin, + filter = _ == "`Foo Bar`: Int" + ) }