Skip to content

Commit a42892a

Browse files
committed
fix(completions): add backticks when needed in completions
This PR adds in the functionality to add in backticks when needed when giving back completions and also correctly returning completions when the user has already started typing a backtick. Fixes: scala#4406, scala#14006
1 parent 29e4b05 commit a42892a

File tree

5 files changed

+175
-7
lines changed

5 files changed

+175
-7
lines changed

compiler/src/dotty/tools/dotc/interactive/Completion.scala

Lines changed: 49 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,9 @@ import dotty.tools.dotc.core.StdNames.nme
1515
import dotty.tools.dotc.core.SymDenotations.SymDenotation
1616
import dotty.tools.dotc.core.TypeError
1717
import dotty.tools.dotc.core.Types.{ExprType, MethodOrPoly, NameFilter, NoType, TermRef, Type}
18+
import dotty.tools.dotc.parsing.Scanners
19+
import dotty.tools.dotc.parsing.Tokens
20+
import dotty.tools.dotc.util.Chars
1821
import dotty.tools.dotc.util.SourcePosition
1922

2023
import scala.collection.mutable
@@ -78,8 +81,8 @@ object Completion {
7881
* Inspect `path` to determine the completion prefix. Only symbols whose name start with the
7982
* returned prefix should be considered.
8083
*/
81-
def completionPrefix(path: List[untpd.Tree], pos: SourcePosition): String =
82-
path match {
84+
def completionPrefix(path: List[untpd.Tree], pos: SourcePosition)(using Context): String =
85+
path match
8386
case (sel: untpd.ImportSelector) :: _ =>
8487
completionPrefix(sel.imported :: Nil, pos)
8588

@@ -88,13 +91,22 @@ object Completion {
8891
completionPrefix(selector :: Nil, pos)
8992
}.getOrElse("")
9093

94+
// We special case Select here because we want to determine if the name
95+
// is an error due to an unclosed backtick.
96+
case (select: untpd.Select) :: _ if (select.name == nme.ERROR) =>
97+
val content = select.source.content()
98+
content.lift(select.nameSpan.start) match
99+
case Some(char) if char == '`' =>
100+
content.slice(select.nameSpan.start, select.span.end).mkString
101+
case _ =>
102+
""
91103
case (ref: untpd.RefTree) :: _ =>
92104
if (ref.name == nme.ERROR) ""
93105
else ref.name.toString.take(pos.span.point - ref.span.point)
94106

95107
case _ =>
96108
""
97-
}
109+
end completionPrefix
98110

99111
/** Inspect `path` to determine the offset where the completion result should be inserted. */
100112
def completionOffset(path: List[Tree]): Int =
@@ -105,7 +117,10 @@ object Completion {
105117

106118
private def computeCompletions(pos: SourcePosition, path: List[Tree])(using Context): (Int, List[Completion]) = {
107119
val mode = completionMode(path, pos)
108-
val prefix = completionPrefix(path, pos)
120+
val rawPrefix = completionPrefix(path, pos)
121+
val hasBackTick = rawPrefix.headOption.contains('`')
122+
val prefix = if hasBackTick then rawPrefix.drop(1) else rawPrefix
123+
109124
val completer = new Completer(mode, prefix, pos)
110125

111126
val completions = path match {
@@ -120,16 +135,44 @@ object Completion {
120135
}
121136

122137
val describedCompletions = describeCompletions(completions)
138+
val backtickedCompletions = describedCompletions.map(backtickCompletions)
139+
123140
val offset = completionOffset(path)
124141

125142
interactiv.println(i"""completion with pos = $pos,
126143
| prefix = ${completer.prefix},
127144
| term = ${completer.mode.is(Mode.Term)},
128145
| type = ${completer.mode.is(Mode.Type)}
129-
| results = $describedCompletions%, %""")
130-
(offset, describedCompletions)
146+
| results = $backtickCompletions%, %""")
147+
(offset, backtickedCompletions)
131148
}
132149

150+
def backtickCompletions(completion: Completion) =
151+
if needsBacktick(completion.label) then
152+
completion.copy(label = s"`${completion.label}`")
153+
else
154+
completion
155+
156+
private def needsBacktick(s: String) =
157+
val chunks = s.split("_", -1)
158+
159+
val validChunks = chunks.zipWithIndex.forall { case (chunk, index) =>
160+
chunk.forall(Chars.isIdentifierPart) ||
161+
(chunk.forall(Chars.isOperatorPart) &&
162+
index == chunks.length - 1 &&
163+
!(chunks.lift(index - 1).contains("") && index - 1 == 0))
164+
}
165+
166+
val validStart =
167+
Chars.isIdentifierStart(s(0)) || chunks(0).forall(Chars.isOperatorPart)
168+
169+
val valid = validChunks && validStart && !keywords.contains(s)
170+
171+
!valid
172+
end needsBacktick
173+
174+
private lazy val keywords = Tokens.keywords.map(Tokens.tokenString)
175+
133176
/**
134177
* Return the list of code completions with descriptions based on a mapping from names to the denotations they refer to.
135178
* If several denotations share the same name, each denotation will be transformed into a separate completion item.

compiler/src/dotty/tools/repl/JLineTerminal.scala

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,8 @@ final class JLineTerminal extends java.io.Closeable {
118118
def currentToken: TokenData /* | Null */ = {
119119
val source = SourceFile.virtual("<completions>", input)
120120
val scanner = new Scanner(source)(using ctx.fresh.setReporter(Reporter.NoReporter))
121+
var lastBacktickErrorStart: Option[Int] = None
122+
121123
while (scanner.token != EOF) {
122124
val start = scanner.offset
123125
val token = scanner.token
@@ -126,7 +128,14 @@ final class JLineTerminal extends java.io.Closeable {
126128

127129
val isCurrentToken = cursor >= start && cursor <= end
128130
if (isCurrentToken)
129-
return TokenData(token, start, end)
131+
return TokenData(token, lastBacktickErrorStart.getOrElse(start), end)
132+
133+
134+
// we need to enclose the last backtick, which unclosed produces ERROR token
135+
if (token == ERROR && input(start) == '`') then
136+
lastBacktickErrorStart = Some(start)
137+
else
138+
lastBacktickErrorStart = None
130139
}
131140
null
132141
}

compiler/src/dotty/tools/repl/ReplDriver.scala

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,8 @@ import org.jline.reader._
3535
import scala.annotation.tailrec
3636
import scala.collection.JavaConverters._
3737
import scala.util.Using
38+
import dotty.tools.dotc.util.Chars
39+
import dotty.tools.dotc.parsing.Tokens
3840

3941
/** The state of the REPL contains necessary bindings instead of having to have
4042
* mutation
@@ -199,6 +201,7 @@ class ReplDriver(settings: Array[String],
199201
/** Extract possible completions at the index of `cursor` in `expr` */
200202
protected final def completions(cursor: Int, expr: String, state0: State): List[Candidate] = {
201203
def makeCandidate(label: String) = {
204+
202205
new Candidate(
203206
/* value = */ label,
204207
/* displ = */ label, // displayed value

compiler/test/dotty/tools/repl/TabcompleteTests.scala

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -131,4 +131,62 @@ class TabcompleteTests extends ReplTest {
131131
tabComplete("import quoted.* ; def fooImpl(using Quotes): Expr[Int] = { import quotes.reflect.* ; TypeRepr.of[Int].s"))
132132
}
133133

134+
@Test def backticked = initially {
135+
assertEquals(
136+
List(
137+
"!=",
138+
"##",
139+
"->",
140+
"==",
141+
"__system",
142+
"`back-tick`",
143+
"`match`",
144+
"asInstanceOf",
145+
"dot_product_*",
146+
"ensuring",
147+
"eq",
148+
"equals",
149+
"foo",
150+
"formatted",
151+
"fromOrdinal",
152+
"getClass",
153+
"hashCode",
154+
"isInstanceOf",
155+
"ne",
156+
"nn",
157+
"notify",
158+
"notifyAll",
159+
"synchronized",
160+
"toString",
161+
"valueOf",
162+
"values",
163+
"wait",
164+
""
165+
),
166+
tabComplete("""|enum Foo:
167+
| case `back-tick`
168+
| case `match`
169+
| case foo
170+
| case dot_product_*
171+
| case __system
172+
|
173+
|Foo."""stripMargin))
174+
}
175+
176+
177+
@Test def backtickedAlready = initially {
178+
assertEquals(
179+
List(
180+
"`back-tick`"
181+
),
182+
tabComplete("""|enum Foo:
183+
| case `back-tick`
184+
| case `match`
185+
| case foo
186+
| case dot_product_*
187+
| case __system
188+
|
189+
|Foo.`bac"""stripMargin))
190+
}
191+
134192
}

language-server/test/dotty/tools/languageserver/CompletionTest.scala

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1023,4 +1023,59 @@ class CompletionTest {
10231023
|class Foo[A]{ self: Futu${m1} => }""".withSource
10241024
.completion(m1, expected)
10251025
}
1026+
1027+
@Test def backticks: Unit = {
1028+
val expected = Set(
1029+
("getClass", Method, "[X0 >: Foo.Bar.type](): Class[? <: X0]"),
1030+
("ensuring", Method, "(cond: Boolean): A"),
1031+
("##", Method, "=> Int"),
1032+
("nn", Method, "=> Foo.Bar.type"),
1033+
("==", Method, "(x$0: Any): Boolean"),
1034+
("ensuring", Method, "(cond: Boolean, msg: => Any): A"),
1035+
("ne", Method, "(x$0: Object): Boolean"),
1036+
("valueOf", Method, "($name: String): Foo.Bar"),
1037+
("equals", Method, "(x$0: Any): Boolean"),
1038+
("wait", Method, "(x$0: Long): Unit"),
1039+
("hashCode", Method, "(): Int"),
1040+
("notifyAll", Method, "(): Unit"),
1041+
("values", Method, "=> Array[Foo.Bar]"),
1042+
("", Method, "[B](y: B): (A, B)"),
1043+
("!=", Method, "(x$0: Any): Boolean"),
1044+
("fromOrdinal", Method, "(ordinal: Int): Foo.Bar"),
1045+
("asInstanceOf", Method, "[X0] => X0"),
1046+
("->", Method, "[B](y: B): (A, B)"),
1047+
("wait", Method, "(x$0: Long, x$1: Int): Unit"),
1048+
("`back-tick`", Field, "Foo.Bar"),
1049+
("notify", Method, "(): Unit"),
1050+
("formatted", Method, "(fmtstr: String): String"),
1051+
("ensuring", Method, "(cond: A => Boolean, msg: => Any): A"),
1052+
("wait", Method, "(): Unit"),
1053+
("isInstanceOf", Method, "[X0] => Boolean"),
1054+
("`match`", Field, "Foo.Bar"),
1055+
("toString", Method, "(): String"),
1056+
("ensuring", Method, "(cond: A => Boolean): A"),
1057+
("eq", Method, "(x$0: Object): Boolean"),
1058+
("synchronized", Method, "[X0](x$0: X0): X0")
1059+
)
1060+
code"""object Foo:
1061+
| enum Bar:
1062+
| case `back-tick`
1063+
| case `match`
1064+
|
1065+
| val x = Bar.${m1}"""
1066+
.withSource.completion(m1, expected)
1067+
}
1068+
1069+
@Test def backticksPrefix: Unit = {
1070+
val expected = Set(
1071+
("`back-tick`", Field, "Foo.Bar"),
1072+
)
1073+
code"""object Foo:
1074+
| enum Bar:
1075+
| case `back-tick`
1076+
| case `match`
1077+
|
1078+
| val x = Bar.`back${m1}"""
1079+
.withSource.completion(m1, expected)
1080+
}
10261081
}

0 commit comments

Comments
 (0)