Skip to content

Commit 45fddb0

Browse files
Make the completion logic easier to extend
1 parent a4fd829 commit 45fddb0

File tree

3 files changed

+297
-8
lines changed

3 files changed

+297
-8
lines changed

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

Lines changed: 40 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import dotty.tools.dotc.core.TypeComparer
2121
import dotty.tools.dotc.core.TypeError
2222
import dotty.tools.dotc.core.Types.{ExprType, MethodOrPoly, NameFilter, NamedType, NoType, PolyType, TermRef, Type}
2323
import dotty.tools.dotc.printing.Texts._
24+
import dotty.tools.dotc.util.Chars.{isOperatorPart, isScalaLetter}
2425
import dotty.tools.dotc.util.{NameTransformer, NoSourcePosition, SourcePosition}
2526

2627
import scala.collection.mutable
@@ -59,7 +60,7 @@ object Completion {
5960
*
6061
* Otherwise, provide no completion suggestion.
6162
*/
62-
private def completionMode(path: List[Tree], pos: SourcePosition): Mode =
63+
def completionMode(path: List[Tree], pos: SourcePosition): Mode =
6364
path match {
6465
case (ref: RefTree) :: _ =>
6566
if (ref.name.isTermName) Mode.Term
@@ -81,7 +82,7 @@ object Completion {
8182
* Inspect `path` to determine the completion prefix. Only symbols whose name start with the
8283
* returned prefix should be considered.
8384
*/
84-
private def completionPrefix(path: List[untpd.Tree], pos: SourcePosition): String =
85+
def completionPrefix(path: List[untpd.Tree], pos: SourcePosition): String =
8586
path match {
8687
case (sel: untpd.ImportSelector) :: _ =>
8788
completionPrefix(sel.imported :: Nil, pos)
@@ -100,7 +101,7 @@ object Completion {
100101
}
101102

102103
/** Inspect `path` to determine the offset where the completion result should be inserted. */
103-
private def completionOffset(path: List[Tree]): Int =
104+
def completionOffset(path: List[Tree]): Int =
104105
path match {
105106
case (ref: RefTree) :: _ => ref.span.point
106107
case _ => 0
@@ -134,14 +135,14 @@ object Completion {
134135
* If several denotations share the same name, the type denotations appear before term denotations inside
135136
* the same `Completion`.
136137
*/
137-
private def describeCompletions(completions: CompletionMap)(using Context): List[Completion] = {
138+
def describeCompletions(completions: CompletionMap)(using Context): List[Completion] = {
138139
completions
139140
.toList.groupBy(_._1.toTermName) // don't distinguish between names of terms and types
140141
.toList.map { (name, namedDenots) =>
141142
val denots = namedDenots.flatMap(_._2)
142143
val typesFirst = denots.sortWith((d1, d2) => d1.isType && !d2.isType)
143144
val desc = description(typesFirst)
144-
Completion(name.show, desc, typesFirst.map(_.symbol))
145+
Completion(label(name), desc, typesFirst.map(_.symbol))
145146
}
146147
}
147148

@@ -174,7 +175,7 @@ object Completion {
174175
* For the results of all `xyzCompletions` methods term names and type names are always treated as different keys in the same map
175176
* and they never conflict with each other.
176177
*/
177-
private class Completer(val mode: Mode, val prefix: String, pos: SourcePosition) {
178+
class Completer(val mode: Mode, val prefix: String, pos: SourcePosition) {
178179
/** Completions for terms and types that are currently in scope:
179180
* the members of the current class, local definitions and the symbols that have been imported,
180181
* recursively adding completions from outer scopes.
@@ -442,11 +443,11 @@ object Completion {
442443
* The completion mode: defines what kinds of symbols should be included in the completion
443444
* results.
444445
*/
445-
private class Mode(val bits: Int) extends AnyVal {
446+
class Mode(val bits: Int) extends AnyVal {
446447
def is(other: Mode): Boolean = (bits & other.bits) == other.bits
447448
def |(other: Mode): Mode = new Mode(bits | other.bits)
448449
}
449-
private object Mode {
450+
object Mode {
450451
/** No symbol should be included */
451452
val None: Mode = new Mode(0)
452453

@@ -459,5 +460,36 @@ object Completion {
459460
/** Both term and type symbols are allowed */
460461
val Import: Mode = new Mode(4) | Term | Type
461462
}
463+
464+
private val bslash = '\\'
465+
private val specialChars = Set('[', ']', '(', ')', '{', '}', '.', ',', ';')
466+
467+
def label(name: Name): String = {
468+
469+
def maybeQuote(name: Name, recurse: Boolean): String =
470+
if (recurse && name.isTermName)
471+
name.asTermName.qualToString(maybeQuote(_, true), maybeQuote(_, false))
472+
else {
473+
// initially adapted from
474+
// https://github.com/scala/scala/blob/decbd53f1bde4600c8ff860f30a79f028a8e431d/src/reflect/scala/reflect/internal/Printers.scala#L573-L584
475+
val decName = name.decode.toString
476+
val hasSpecialChar = decName.exists { ch =>
477+
specialChars(ch) || ch.isWhitespace
478+
}
479+
def isOperatorLike = (name.isOperatorName || decName.exists(isOperatorPart)) &&
480+
decName.exists(isScalaLetter) &&
481+
!decName.contains(bslash)
482+
lazy val term = name.toTermName
483+
484+
val needsBackTicks = hasSpecialChar ||
485+
isOperatorLike ||
486+
nme.keywords(term) && term != nme.USCOREkw
487+
488+
if (needsBackTicks) s"`$decName`"
489+
else decName
490+
}
491+
492+
maybeQuote(name, true)
493+
}
462494
}
463495

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
package dotty.tools.dotc.interactive
2+
3+
import dotty.tools.dotc.ast.tpd._
4+
import dotty.tools.dotc.ast.untpd
5+
import dotty.tools.dotc.core.Contexts._
6+
import dotty.tools.dotc.core.Denotations.SingleDenotation
7+
import dotty.tools.dotc.core.Flags._
8+
import dotty.tools.dotc.core.Names.{Name, termName}
9+
import dotty.tools.dotc.core.Symbols.{Symbol, defn}
10+
import dotty.tools.dotc.core.TypeError
11+
import dotty.tools.dotc.util.SourcePosition
12+
13+
object CustomCompletion {
14+
15+
def completions(
16+
pos: SourcePosition,
17+
dependencyCompleteOpt: Option[String => (Int, Seq[String])],
18+
enableDeep: Boolean
19+
)(using Context): (Int, List[Completion]) = {
20+
val path = Interactive.pathTo(ctx.compilationUnit.tpdTree, pos.span)
21+
computeCompletions(pos, path, dependencyCompleteOpt, enableDeep)(using Interactive.contextOfPath(path))
22+
}
23+
24+
def computeCompletions(
25+
pos: SourcePosition,
26+
path: List[Tree],
27+
dependencyCompleteOpt: Option[String => (Int, Seq[String])],
28+
enableDeep: Boolean
29+
)(using Context): (Int, List[Completion]) = {
30+
val mode = Completion.completionMode(path, pos)
31+
val prefix = Completion.completionPrefix(path, pos)
32+
val completer = new DeepCompleter(mode, prefix, pos)
33+
34+
var extra = List.empty[Completion]
35+
36+
val completions = path match {
37+
case Select(qual, _) :: _ => completer.selectionCompletions(qual)
38+
case Import(Ident(name), _) :: _ if name.decode.toString == "$ivy" && dependencyCompleteOpt.nonEmpty =>
39+
val complete = dependencyCompleteOpt.get
40+
val (pos, completions) = complete(prefix)
41+
val input0 = prefix.take(pos)
42+
extra ++= completions.distinct.toList
43+
.map(s => Completion(Completion.label(termName(input0 + s)), "", Nil))
44+
Map.empty
45+
case Import(expr, _) :: _ => completer.directMemberCompletions(expr)
46+
case (_: untpd.ImportSelector) :: Import(expr, _) :: _ => completer.directMemberCompletions(expr)
47+
case _ =>
48+
completer.scopeCompletions ++ {
49+
if (enableDeep) completer.deepCompletions
50+
else Nil
51+
}
52+
}
53+
54+
val describedCompletions = extra ++ Completion.describeCompletions(completions)
55+
val offset = Completion.completionOffset(path)
56+
57+
(pos.span.start - prefix.length, describedCompletions)
58+
}
59+
60+
class DeepCompleter(mode: Completion.Mode, prefix: String, pos: SourcePosition) extends Completion.Completer(mode, prefix, pos):
61+
def deepCompletions(using Context): Map[Name, Seq[SingleDenotation]] = {
62+
63+
def allMembers(s: Symbol) =
64+
try s.info.allMembers
65+
catch {
66+
case _: dotty.tools.dotc.core.TypeError => Nil
67+
}
68+
def rec(t: Symbol): Seq[Symbol] = {
69+
val children =
70+
if (t.is(Package) || t.is(PackageVal) || t.is(PackageClass)) {
71+
allMembers(t).map(_.symbol).filter(_ != t).flatMap(rec)
72+
} else Nil
73+
74+
t +: children.toSeq
75+
}
76+
77+
val syms = for {
78+
member <- allMembers(defn.RootClass).map(_.symbol).toList
79+
sym <- rec(member)
80+
if sym.name.toString.startsWith(prefix)
81+
} yield sym
82+
83+
syms.map(sym => (sym.fullName, List(sym: SingleDenotation))).toMap
84+
}
85+
86+
}
87+
Lines changed: 170 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,170 @@
1+
package dotty.tools
2+
package dotc.interactive
3+
4+
import dotc.ast.tpd
5+
import dotc.{CompilationUnit, Compiler, Run}
6+
import dotc.core.Contexts.Context
7+
import dotc.core.Mode
8+
import dotc.reporting.StoreReporter
9+
import dotc.util.{SourceFile, SourcePosition}
10+
import dotc.util.Spans.Span
11+
12+
import org.junit.Assert._
13+
import org.junit.Test
14+
15+
class CustomCompletionTests extends DottyTest:
16+
17+
private def completions(
18+
input: String,
19+
dependencyCompleter: Option[String => (Int, Seq[String])] = None,
20+
deep: Boolean = false,
21+
extraDefinitions: String = ""
22+
): (Int, Seq[Completion]) =
23+
val prefix = extraDefinitions + """
24+
object Wrapper {
25+
val expr = {
26+
"""
27+
val suffix = """
28+
}
29+
}
30+
"""
31+
32+
val allCode = prefix + input + suffix
33+
val index = prefix.length + input.length
34+
35+
val run = new Run(
36+
new Compiler,
37+
initialCtx.fresh
38+
.addMode(Mode.ReadPositions | Mode.Interactive)
39+
// discard errors - comment out this line to print them in the console
40+
.setReporter(new StoreReporter(null))
41+
.setSetting(initialCtx.settings.YstopAfter, List("typer"))
42+
)
43+
val file = SourceFile.virtual("<completions>", allCode, maybeIncomplete = true)
44+
given ctx: Context = run.runContext.withSource(file)
45+
val unit = CompilationUnit(file)
46+
ctx
47+
.run
48+
.compileUnits(unit :: Nil, ctx)
49+
50+
// ignoring compilation errors here - the input code
51+
// to complete likely doesn't compile
52+
53+
unit.tpdTree = {
54+
import tpd._
55+
unit.tpdTree match {
56+
case PackageDef(_, p) =>
57+
p.reverseIterator.collectFirst {
58+
case TypeDef(_, tmpl: Template) =>
59+
tmpl.body
60+
.collectFirst { case dd: ValDef if dd.name.show == "expr" => dd }
61+
.getOrElse(sys.error("Unexpected tree shape"))
62+
}
63+
.getOrElse(sys.error("Unexpected tree shape"))
64+
case _ => sys.error("Unexpected tree shape")
65+
}
66+
}
67+
val ctx1 = ctx.fresh.setCompilationUnit(unit)
68+
val srcPos = SourcePosition(file, Span(index))
69+
val (offset0, completions) =
70+
if (deep || dependencyCompleter.nonEmpty)
71+
CustomCompletion.completions(srcPos, dependencyCompleteOpt = dependencyCompleter, enableDeep = deep)(using ctx1)
72+
else
73+
Completion.completions(srcPos)(using ctx1)
74+
val offset = offset0 - prefix.length
75+
(offset, completions)
76+
77+
78+
@Test def simple(): Unit =
79+
val prefix = "scala.collection.immutable."
80+
val input = prefix + "Ma"
81+
82+
val (offset, completions0) = completions(input)
83+
val labels = completions0.map(_.label)
84+
85+
assert(offset == prefix.length)
86+
assert(labels.contains("Map"))
87+
88+
@Test def custom(): Unit =
89+
val prefix = "import $ivy."
90+
val input = prefix + "scala"
91+
92+
val dependencies = Seq(
93+
"scalaCompiler",
94+
"scalaLibrary",
95+
"other"
96+
)
97+
val (offset, completions0) = completions(
98+
input,
99+
dependencyCompleter = Some { dep =>
100+
val matches = dependencies.filter(_.startsWith(dep))
101+
(0, matches)
102+
}
103+
)
104+
val labels = completions0.map(_.label)
105+
106+
assert(offset == prefix.length)
107+
assert(labels.contains("scalaCompiler"))
108+
assert(labels.contains("scalaLibrary"))
109+
assert(labels.length == 2)
110+
111+
@Test def backTicks(): Unit =
112+
val prefix = "Foo."
113+
val input = prefix + "a"
114+
115+
val extraDefinitions =
116+
"""object Foo { def a1 = 2; def `a-b` = 3 }
117+
|""".stripMargin
118+
val (offset, completions0) = completions(
119+
input,
120+
extraDefinitions = extraDefinitions
121+
)
122+
val labels = completions0.map(_.label)
123+
124+
assert(offset == prefix.length)
125+
assert(labels.contains("a1"))
126+
assert(labels.contains("`a-b`"))
127+
128+
@Test def backTicksDependencies(): Unit =
129+
val prefix = "import $ivy."
130+
val input = prefix + "`org.scala-lang:scala-`"
131+
132+
val dependencies = Seq(
133+
"org.scala-lang:scala-compiler",
134+
"org.scala-lang:scala-library",
135+
"other"
136+
)
137+
val (offset, completions0) = completions(
138+
input,
139+
dependencyCompleter = Some { dep =>
140+
val matches = dependencies.filter(_.startsWith(dep))
141+
(0, matches)
142+
}
143+
)
144+
val labels = completions0.map(_.label)
145+
146+
// Seems backticks mess with that for now...
147+
// assert(offset == prefix.length)
148+
assert(labels.contains("`org.scala-lang:scala-compiler`"))
149+
assert(labels.contains("`org.scala-lang:scala-library`"))
150+
assert(labels.length == 2)
151+
152+
@Test def deep(): Unit =
153+
val prefix = ""
154+
val input = prefix + "ListBuf"
155+
156+
val (offset, completions0) = completions(input, deep = true)
157+
val labels = completions0.map(_.label)
158+
159+
assert(offset == prefix.length)
160+
assert(labels.contains("scala.collection.mutable.ListBuffer"))
161+
162+
@Test def deepType(): Unit =
163+
val prefix = ""
164+
val input = prefix + "Function2"
165+
166+
val (offset, completions0) = completions(input, deep = true)
167+
val labels = completions0.map(_.label)
168+
169+
assert(offset == prefix.length)
170+
assert(labels.contains("scala.Function2"))

0 commit comments

Comments
 (0)