Skip to content

Commit f101de1

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 06590c5 commit f101de1

File tree

5 files changed

+152
-37
lines changed

5 files changed

+152
-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

@@ -271,7 +276,12 @@ class CompletionProvider(
271276
if (item.getTextEdit == null) {
272277
val editText = member match {
273278
case _: NamedArgMember => item.getLabel
274-
case _ => ident
279+
case _ =>
280+
val ident0 =
281+
if (hadLeadingBacktick) ident.stripPrefix("`") else ident
282+
val ident1 =
283+
if (hadTrailingBacktick) ident0.stripSuffix("`") else ident0
284+
ident1
275285
}
276286
item.setTextEdit(textEdit(editText))
277287
}
@@ -498,9 +508,16 @@ class CompletionProvider(
498508
private def safeCompletionsAt(
499509
pos: Position,
500510
source: URI
501-
): (InterestingMembers, CompletionPosition, l.Range, String) = {
511+
): (
512+
InterestingMembers,
513+
CompletionPosition,
514+
InferredIdentOffsets,
515+
l.Range,
516+
String
517+
) = {
518+
lazy val inferredIdentOffsets = inferIdentOffsets(pos, params.text())
502519
lazy val editRange = pos
503-
.withStart(inferIdentStart(pos, params.text()))
520+
.withStart(inferredIdentOffsets.start)
504521
.withEnd(pos.point)
505522
.toLsp
506523
val noQuery = "$a"
@@ -518,6 +535,7 @@ class CompletionProvider(
518535
(
519536
InterestingMembers(Nil, SymbolSearch.Result.COMPLETE),
520537
NoneCompletion,
538+
inferredIdentOffsets,
521539
editRange,
522540
noQuery
523541
)
@@ -528,6 +546,7 @@ class CompletionProvider(
528546
SymbolSearch.Result.COMPLETE
529547
),
530548
completion,
549+
inferredIdentOffsets,
531550
editRange,
532551
noQuery
533552
)
@@ -575,7 +594,7 @@ class CompletionProvider(
575594
params.text()
576595
)
577596
params.checkCanceled()
578-
(items, completion, editRange, query)
597+
(items, completion, inferredIdentOffsets, editRange, query)
579598
} catch {
580599
case e: CyclicReference
581600
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() && {

mtags/src/main/scala-3/scala/meta/internal/pc/completions/CompletionPos.scala

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,7 @@ object CompletionPos:
110110

111111
loop(path)
112112

113+
// TODO: update this
113114
/**
114115
* Returns the start offset of the identifier starting as the given offset position.
115116
*/

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

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2069,4 +2069,63 @@ class CompletionSuite extends BaseCompletionSuite {
20692069
assertSingleItem = false
20702070
)
20712071

2072+
val BacktickCompletionsTag =
2073+
IgnoreScala3.and(IgnoreScalaVersion.forLessThan("2.13.17"))
2074+
2075+
checkEdit(
2076+
"add-backticks-around-identifier".tag(BacktickCompletionsTag),
2077+
"""|object Main {
2078+
| def `Foo Bar` = 123
2079+
| Foo@@
2080+
|}
2081+
|""".stripMargin,
2082+
"""|object Main {
2083+
| def `Foo Bar` = 123
2084+
| `Foo Bar`
2085+
|}
2086+
|""".stripMargin
2087+
)
2088+
2089+
checkEdit(
2090+
"complete-inside-backticks".tag(BacktickCompletionsTag),
2091+
"""|object Main {
2092+
| def `Foo Bar` = 123
2093+
| `Foo@@`
2094+
|}
2095+
|""".stripMargin,
2096+
"""|object Main {
2097+
| def `Foo Bar` = 123
2098+
| `Foo Bar`
2099+
|}
2100+
|""".stripMargin
2101+
)
2102+
2103+
checkEdit(
2104+
"complete-inside-backticks-after-space".tag(BacktickCompletionsTag),
2105+
"""|object Main {
2106+
| def `Foo Bar` = 123
2107+
| `Foo B@@`
2108+
|}
2109+
|""".stripMargin,
2110+
"""|object Main {
2111+
| def `Foo Bar` = 123
2112+
| `Foo Bar`
2113+
|}
2114+
|""".stripMargin
2115+
)
2116+
2117+
checkEdit(
2118+
"complete-inside-empty-backticks".tag(BacktickCompletionsTag),
2119+
"""|object Main {
2120+
| def `Foo Bar` = 123
2121+
| `@@`
2122+
|}
2123+
|""".stripMargin,
2124+
"""|object Main {
2125+
| def `Foo Bar` = 123
2126+
| `Foo Bar`
2127+
|}
2128+
|""".stripMargin,
2129+
filter = _ == "`Foo Bar`: Int"
2130+
)
20722131
}

0 commit comments

Comments
 (0)