Skip to content

Commit 3c5dbc3

Browse files
authored
Merge pull request #14594 from ckipp01/backTickedOff
fix(completions): add backticks when needed in completions
2 parents 1080f1b + 5e5866b commit 3c5dbc3

File tree

5 files changed

+229
-8
lines changed

5 files changed

+229
-8
lines changed

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

+55-6
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@ import dotty.tools.dotc.core.StdNames.nme
1717
import dotty.tools.dotc.core.SymDenotations.SymDenotation
1818
import dotty.tools.dotc.core.TypeError
1919
import dotty.tools.dotc.core.Types.{ExprType, MethodOrPoly, NameFilter, NoType, TermRef, Type}
20+
import dotty.tools.dotc.parsing.Tokens
21+
import dotty.tools.dotc.util.Chars
2022
import dotty.tools.dotc.util.SourcePosition
2123

2224
import scala.collection.mutable
@@ -80,8 +82,8 @@ object Completion {
8082
* Inspect `path` to determine the completion prefix. Only symbols whose name start with the
8183
* returned prefix should be considered.
8284
*/
83-
def completionPrefix(path: List[untpd.Tree], pos: SourcePosition): String =
84-
path match {
85+
def completionPrefix(path: List[untpd.Tree], pos: SourcePosition)(using Context): String =
86+
path match
8587
case (sel: untpd.ImportSelector) :: _ =>
8688
completionPrefix(sel.imported :: Nil, pos)
8789

@@ -90,13 +92,22 @@ object Completion {
9092
completionPrefix(selector :: Nil, pos)
9193
}.getOrElse("")
9294

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

97108
case _ =>
98109
""
99-
}
110+
end completionPrefix
100111

101112
/** Inspect `path` to determine the offset where the completion result should be inserted. */
102113
def completionOffset(path: List[Tree]): Int =
@@ -107,7 +118,11 @@ object Completion {
107118

108119
private def computeCompletions(pos: SourcePosition, path: List[Tree])(using Context): (Int, List[Completion]) = {
109120
val mode = completionMode(path, pos)
110-
val prefix = completionPrefix(path, pos)
121+
val rawPrefix = completionPrefix(path, pos)
122+
123+
val hasBackTick = rawPrefix.headOption.contains('`')
124+
val prefix = if hasBackTick then rawPrefix.drop(1) else rawPrefix
125+
111126
val completer = new Completer(mode, prefix, pos)
112127

113128
val completions = path match {
@@ -122,16 +137,49 @@ object Completion {
122137
}
123138

124139
val describedCompletions = describeCompletions(completions)
140+
val backtickedCompletions =
141+
describedCompletions.map(completion => backtickCompletions(completion, hasBackTick))
142+
125143
val offset = completionOffset(path)
126144

127145
interactiv.println(i"""completion with pos = $pos,
128146
| prefix = ${completer.prefix},
129147
| term = ${completer.mode.is(Mode.Term)},
130148
| type = ${completer.mode.is(Mode.Type)}
131-
| results = $describedCompletions%, %""")
132-
(offset, describedCompletions)
149+
| results = $backtickCompletions%, %""")
150+
(offset, backtickedCompletions)
133151
}
134152

153+
def backtickCompletions(completion: Completion, hasBackTick: Boolean) =
154+
if hasBackTick || needsBacktick(completion.label) then
155+
completion.copy(label = s"`${completion.label}`")
156+
else
157+
completion
158+
159+
// This borrows from Metals, which itself borrows from Ammonite. This uses
160+
// the same approach, but some of the utils that already exist in Dotty.
161+
// https://github.com/scalameta/metals/blob/main/mtags/src/main/scala/scala/meta/internal/mtags/KeywordWrapper.scala
162+
// https://github.com/com-lihaoyi/Ammonite/blob/73a874173cd337f953a3edc9fb8cb96556638fdd/amm/util/src/main/scala/ammonite/util/Model.scala
163+
private def needsBacktick(s: String) =
164+
val chunks = s.split("_", -1)
165+
166+
val validChunks = chunks.zipWithIndex.forall { case (chunk, index) =>
167+
chunk.forall(Chars.isIdentifierPart) ||
168+
(chunk.forall(Chars.isOperatorPart) &&
169+
index == chunks.length - 1 &&
170+
!(chunks.lift(index - 1).contains("") && index - 1 == 0))
171+
}
172+
173+
val validStart =
174+
Chars.isIdentifierStart(s(0)) || chunks(0).forall(Chars.isOperatorPart)
175+
176+
val valid = validChunks && validStart && !keywords.contains(s)
177+
178+
!valid
179+
end needsBacktick
180+
181+
private lazy val keywords = Tokens.keywords.map(Tokens.tokenString)
182+
135183
/**
136184
* Return the list of code completions with descriptions based on a mapping from names to the denotations they refer to.
137185
* If several denotations share the same name, each denotation will be transformed into a separate completion item.
@@ -384,6 +432,7 @@ object Completion {
384432
private def include(denot: SingleDenotation, nameInScope: Name)(using Context): Boolean =
385433
val sym = denot.symbol
386434

435+
387436
nameInScope.startsWith(prefix) &&
388437
sym.exists &&
389438
completionsFilter(NoType, nameInScope) &&

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

+10-1
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,8 @@ final class JLineTerminal extends java.io.Closeable {
120120
def currentToken: TokenData /* | Null */ = {
121121
val source = SourceFile.virtual("<completions>", input)
122122
val scanner = new Scanner(source)(using ctx.fresh.setReporter(Reporter.NoReporter))
123+
var lastBacktickErrorStart: Option[Int] = None
124+
123125
while (scanner.token != EOF) {
124126
val start = scanner.offset
125127
val token = scanner.token
@@ -128,7 +130,14 @@ final class JLineTerminal extends java.io.Closeable {
128130

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

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

+8-1
Original file line numberDiff line numberDiff line change
@@ -198,12 +198,19 @@ class ReplDriver(settings: Array[String],
198198
state.copy(context = run.runContext)
199199
}
200200

201+
private def stripBackTicks(label: String) =
202+
if label.startsWith("`") && label.endsWith("`") then
203+
label.drop(1).dropRight(1)
204+
else
205+
label
206+
201207
/** Extract possible completions at the index of `cursor` in `expr` */
202208
protected final def completions(cursor: Int, expr: String, state0: State): List[Candidate] = {
203209
def makeCandidate(label: String) = {
210+
204211
new Candidate(
205212
/* value = */ label,
206-
/* displ = */ label, // displayed value
213+
/* displ = */ stripBackTicks(label), // displayed value
207214
/* group = */ null, // can be used to group completions together
208215
/* descr = */ null, // TODO use for documentation?
209216
/* suffix = */ null,

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

+58
Original file line numberDiff line numberDiff line change
@@ -133,4 +133,62 @@ class TabcompleteTests extends ReplTest {
133133
tabComplete("import quoted.* ; def fooImpl(using Quotes): Expr[Int] = { import quotes.reflect.* ; TypeRepr.of[Int].s"))
134134
}
135135

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

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

+98
Original file line numberDiff line numberDiff line change
@@ -1023,4 +1023,102 @@ 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+
}
1081+
1082+
@Test def backticksSpace: Unit = {
1083+
val expected = Set(
1084+
("`has space`", Field, "Foo.Bar"),
1085+
)
1086+
code"""object Foo:
1087+
| enum Bar:
1088+
| case `has space`
1089+
|
1090+
| val x = Bar.`has s${m1}"""
1091+
.withSource.completion(m1, expected)
1092+
}
1093+
1094+
@Test def backticksCompleteBoth: Unit = {
1095+
val expected = Set(
1096+
("formatted", Method, "(fmtstr: String): String"),
1097+
("`foo-bar`", Field, "Int"),
1098+
("foo", Field, "Int")
1099+
)
1100+
code"""object Foo:
1101+
| object Bar:
1102+
| val foo = 1
1103+
| val `foo-bar` = 2
1104+
| val `bar` = 3
1105+
|
1106+
| val x = Bar.fo${m1}"""
1107+
.withSource.completion(m1, expected)
1108+
}
1109+
1110+
@Test def backticksWhenNotNeeded: Unit = {
1111+
val expected = Set(
1112+
("`formatted`", Method, "(fmtstr: String): String"),
1113+
("`foo-bar`", Field, "Int"),
1114+
("`foo`", Field, "Int")
1115+
)
1116+
code"""object Foo:
1117+
| object Bar:
1118+
| val foo = 1
1119+
| val `foo-bar` = 2
1120+
|
1121+
| val x = Bar.`fo${m1}"""
1122+
.withSource.completion(m1, expected)
1123+
}
10261124
}

0 commit comments

Comments
 (0)