Skip to content

Commit 7cc0f9d

Browse files
committed
feature: completions inside of backticks
Scala 2 only - the [corresponding Scala 3 change][0] has already been merged. Completions can now be triggered from inside of backticks. In addition, the backtick itself is added as a completion character. Especially when the editor is configured to auto-close backticks, this plays nicely with completions now producing useful results inside of backticks. Closes [this feature request][1] [0]: scala/scala3#22555 [1]: scalameta/metals-feature-requests#418
1 parent 34e995e commit 7cc0f9d

File tree

4 files changed

+151
-37
lines changed

4 files changed

+151
-37
lines changed

metals/src/main/scala/scala/meta/internal/metals/WorkspaceLspService.scala

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1211,7 +1211,7 @@ class WorkspaceLspService(
12111211
capabilities.setCompletionProvider(
12121212
new lsp4j.CompletionOptions(
12131213
clientConfig.isCompletionItemResolve(),
1214-
List(".", "*", "$").asJava,
1214+
List(".", "*", "$", "`").asJava,
12151215
)
12161216
)
12171217
capabilities.setCallHierarchyProvider(true)

mtags/src/main/scala-2/scala/meta/internal/pc/CompletionProvider.scala

Lines changed: 27 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -57,10 +57,15 @@ class CompletionProvider(
5757
val pos = unit.position(params.offset)
5858
val isSnippet = isSnippetEnabled(pos, params.text())
5959

60-
val (i, completion, editRange, query) = safeCompletionsAt(pos, params.uri())
61-
62-
val start = inferIdentStart(pos, params.text())
63-
val end = inferIdentEnd(pos, params.text())
60+
val (i, completion, identOffsets, editRange, query) =
61+
safeCompletionsAt(pos, params.uri())
62+
63+
val InferredIdentOffsets(
64+
start,
65+
end,
66+
hadLeadingBacktick,
67+
hadTrailingBacktick
68+
) = identOffsets
6469
val oldText = params.text().substring(start, end)
6570
val stripSuffix = pos.withStart(start).withEnd(end).toLsp
6671

@@ -275,7 +280,12 @@ class CompletionProvider(
275280
if (item.getTextEdit == null) {
276281
val editText = member match {
277282
case _: NamedArgMember => item.getLabel
278-
case _ => ident
283+
case _ =>
284+
val ident0 =
285+
if (hadLeadingBacktick) ident.stripPrefix("`") else ident
286+
val ident1 =
287+
if (hadTrailingBacktick) ident0.stripSuffix("`") else ident0
288+
ident1
279289
}
280290
item.setTextEdit(textEdit(editText))
281291
}
@@ -502,9 +512,16 @@ class CompletionProvider(
502512
private def safeCompletionsAt(
503513
pos: Position,
504514
source: URI
505-
): (InterestingMembers, CompletionPosition, l.Range, String) = {
515+
): (
516+
InterestingMembers,
517+
CompletionPosition,
518+
InferredIdentOffsets,
519+
l.Range,
520+
String
521+
) = {
522+
lazy val inferredIdentOffsets = inferIdentOffsets(pos, params.text())
506523
lazy val editRange = pos
507-
.withStart(inferIdentStart(pos, params.text()))
524+
.withStart(inferredIdentOffsets.start)
508525
.withEnd(pos.point)
509526
.toLsp
510527
val noQuery = "$a"
@@ -522,6 +539,7 @@ class CompletionProvider(
522539
(
523540
InterestingMembers(Nil, SymbolSearch.Result.COMPLETE),
524541
NoneCompletion,
542+
inferredIdentOffsets,
525543
editRange,
526544
noQuery
527545
)
@@ -532,6 +550,7 @@ class CompletionProvider(
532550
SymbolSearch.Result.COMPLETE
533551
),
534552
completion,
553+
inferredIdentOffsets,
535554
editRange,
536555
noQuery
537556
)
@@ -579,7 +598,7 @@ class CompletionProvider(
579598
params.text()
580599
)
581600
params.checkCanceled()
582-
(items, completion, editRange, query)
601+
(items, completion, inferredIdentOffsets, editRange, query)
583602
} catch {
584603
case e: CyclicReference
585604
if e.getMessage.contains("illegal cyclic reference") =>

mtags/src/main/scala-2/scala/meta/internal/pc/completions/Completions.scala

Lines changed: 64 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -905,57 +905,93 @@ trait Completions { this: MetalsGlobal =>
905905
result
906906
}
907907

908-
def inferStart(
908+
case class InferredIdentOffsets(
909+
start: Int,
910+
end: Int,
911+
strippedLeadingBacktick: Boolean,
912+
strippedTrailingBacktick: Boolean
913+
)
914+
915+
def inferIdentOffsets(
909916
pos: Position,
910-
text: String,
911-
charPred: Char => Boolean
912-
): Int = {
913-
def fallback: Int = {
917+
text: String
918+
): InferredIdentOffsets = {
919+
920+
// If we fail to find a tree, approximate with a heurstic about ident characters
921+
def fallbackStart: Int = {
914922
var i = pos.point - 1
915-
while (i >= 0 && charPred(text.charAt(i))) {
923+
while (i >= 0 && Chars.isIdentifierPart(text.charAt(i))) {
916924
i -= 1
917925
}
918926
i + 1
919927
}
920-
def loop(enclosing: List[Tree]): Int =
928+
def fallbackEnd: Int = {
929+
findEnd(false)
930+
}
931+
932+
def findEnd(hasBacktick: Boolean): Int = {
933+
val predicate: Char => Boolean = if (hasBacktick) { (ch: Char) =>
934+
!Chars.isLineBreakChar(ch) && ch != '`'
935+
} else {
936+
Chars.isIdentifierPart(_)
937+
}
938+
939+
var i = pos.point
940+
while (i < text.length && predicate(text.charAt(i))) {
941+
i += 1
942+
}
943+
i
944+
}
945+
def fallback =
946+
InferredIdentOffsets(fallbackStart, fallbackEnd, false, false)
947+
948+
def refTreePos(refTree: RefTree): InferredIdentOffsets = {
949+
val refTreePos = treePos(refTree)
950+
var startPos = refTreePos.point
951+
var strippedLeadingBacktick = false
952+
if (text.charAt(startPos) == '`') {
953+
startPos += 1
954+
strippedLeadingBacktick = true
955+
}
956+
957+
val endPos = findEnd(strippedLeadingBacktick)
958+
var strippedTrailingBacktick = false
959+
if (endPos < text.length) {
960+
if (text.charAt(endPos) == '`') {
961+
strippedTrailingBacktick = true
962+
}
963+
}
964+
InferredIdentOffsets(
965+
Math.min(startPos, pos.point),
966+
endPos,
967+
strippedLeadingBacktick,
968+
strippedTrailingBacktick
969+
)
970+
}
971+
972+
def loop(enclosing: List[Tree]): InferredIdentOffsets =
921973
enclosing match {
922974
case Nil => fallback
923975
case head :: tl =>
924976
if (!treePos(head).includes(pos)) loop(tl)
925977
else {
926978
head match {
927979
case i: Ident =>
928-
treePos(i).point
929-
case Select(qual, _) if !treePos(qual).includes(pos) =>
930-
treePos(head).point
980+
refTreePos(i)
981+
case sel @ Select(qual, _) if !treePos(qual).includes(pos) =>
982+
refTreePos(sel)
931983
case _ => fallback
932984
}
933985
}
934986
}
935-
val start = loop(lastVisitedParentTrees)
936-
Math.min(start, pos.point)
987+
loop(lastVisitedParentTrees)
937988
}
938989

939-
/** Can character form part of an alphanumeric Scala identifier? */
940-
private def isIdentifierPart(c: Char) =
941-
(c == '$') || Character.isUnicodeIdentifierPart(c)
942-
943990
/**
944991
* Returns the start offset of the identifier starting as the given offset position.
945992
*/
946993
def inferIdentStart(pos: Position, text: String): Int =
947-
inferStart(pos, text, isIdentifierPart)
948-
949-
/**
950-
* Returns the end offset of the identifier starting as the given offset position.
951-
*/
952-
def inferIdentEnd(pos: Position, text: String): Int = {
953-
var i = pos.point
954-
while (i < text.length && Chars.isIdentifierPart(text.charAt(i))) {
955-
i += 1
956-
}
957-
i
958-
}
994+
inferIdentOffsets(pos, text).start
959995

960996
def isSnippetEnabled(pos: Position, text: String): Boolean = {
961997
pos.point < text.length() && {

tests/cross/src/test/scala/tests/pc/CompletionSuite.scala

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2091,4 +2091,63 @@ class CompletionSuite extends BaseCompletionSuite {
20912091
}
20922092
)
20932093

2094+
val BacktickCompletionsTag =
2095+
IgnoreScala3.and(IgnoreScalaVersion.forLessThan("2.13.17"))
2096+
2097+
checkEdit(
2098+
"add-backticks-around-identifier".tag(BacktickCompletionsTag),
2099+
"""|object Main {
2100+
| def `Foo Bar` = 123
2101+
| Foo@@
2102+
|}
2103+
|""".stripMargin,
2104+
"""|object Main {
2105+
| def `Foo Bar` = 123
2106+
| `Foo Bar`
2107+
|}
2108+
|""".stripMargin
2109+
)
2110+
2111+
checkEdit(
2112+
"complete-inside-backticks".tag(BacktickCompletionsTag),
2113+
"""|object Main {
2114+
| def `Foo Bar` = 123
2115+
| `Foo@@`
2116+
|}
2117+
|""".stripMargin,
2118+
"""|object Main {
2119+
| def `Foo Bar` = 123
2120+
| `Foo Bar`
2121+
|}
2122+
|""".stripMargin
2123+
)
2124+
2125+
checkEdit(
2126+
"complete-inside-backticks-after-space".tag(BacktickCompletionsTag),
2127+
"""|object Main {
2128+
| def `Foo Bar` = 123
2129+
| `Foo B@@`
2130+
|}
2131+
|""".stripMargin,
2132+
"""|object Main {
2133+
| def `Foo Bar` = 123
2134+
| `Foo Bar`
2135+
|}
2136+
|""".stripMargin
2137+
)
2138+
2139+
checkEdit(
2140+
"complete-inside-empty-backticks".tag(BacktickCompletionsTag),
2141+
"""|object Main {
2142+
| def `Foo Bar` = 123
2143+
| `@@`
2144+
|}
2145+
|""".stripMargin,
2146+
"""|object Main {
2147+
| def `Foo Bar` = 123
2148+
| `Foo Bar`
2149+
|}
2150+
|""".stripMargin,
2151+
filter = _ == "`Foo Bar`: Int"
2152+
)
20942153
}

0 commit comments

Comments
 (0)