Skip to content

Commit 11265bc

Browse files
authored
Merge pull request #11794 from alexarchambault/custom-completions
Make the completion logic easier to extend
2 parents e60ef35 + c3f59a5 commit 11265bc

File tree

3 files changed

+312
-8
lines changed

3 files changed

+312
-8
lines changed

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

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ object Completion {
5959
*
6060
* Otherwise, provide no completion suggestion.
6161
*/
62-
private def completionMode(path: List[Tree], pos: SourcePosition): Mode =
62+
def completionMode(path: List[Tree], pos: SourcePosition): Mode =
6363
path match {
6464
case (ref: RefTree) :: _ =>
6565
if (ref.name.isTermName) Mode.Term
@@ -81,7 +81,7 @@ object Completion {
8181
* Inspect `path` to determine the completion prefix. Only symbols whose name start with the
8282
* returned prefix should be considered.
8383
*/
84-
private def completionPrefix(path: List[untpd.Tree], pos: SourcePosition): String =
84+
def completionPrefix(path: List[untpd.Tree], pos: SourcePosition): String =
8585
path match {
8686
case (sel: untpd.ImportSelector) :: _ =>
8787
completionPrefix(sel.imported :: Nil, pos)
@@ -100,7 +100,7 @@ object Completion {
100100
}
101101

102102
/** Inspect `path` to determine the offset where the completion result should be inserted. */
103-
private def completionOffset(path: List[Tree]): Int =
103+
def completionOffset(path: List[Tree]): Int =
104104
path match {
105105
case (ref: RefTree) :: _ => ref.span.point
106106
case _ => 0
@@ -134,7 +134,7 @@ object Completion {
134134
* If several denotations share the same name, the type denotations appear before term denotations inside
135135
* the same `Completion`.
136136
*/
137-
private def describeCompletions(completions: CompletionMap)(using Context): List[Completion] = {
137+
def describeCompletions(completions: CompletionMap)(using Context): List[Completion] = {
138138
completions
139139
.toList.groupBy(_._1.toTermName) // don't distinguish between names of terms and types
140140
.toList.map { (name, namedDenots) =>
@@ -153,7 +153,7 @@ object Completion {
153153
*
154154
* When there are multiple denotations, show their kinds.
155155
*/
156-
private def description(denots: List[SingleDenotation])(using Context): String =
156+
def description(denots: List[SingleDenotation])(using Context): String =
157157
denots match {
158158
case denot :: Nil =>
159159
if (denot.isType) denot.symbol.showFullName
@@ -174,7 +174,7 @@ object Completion {
174174
* For the results of all `xyzCompletions` methods term names and type names are always treated as different keys in the same map
175175
* and they never conflict with each other.
176176
*/
177-
private class Completer(val mode: Mode, val prefix: String, pos: SourcePosition) {
177+
class Completer(val mode: Mode, val prefix: String, pos: SourcePosition) {
178178
/** Completions for terms and types that are currently in scope:
179179
* the members of the current class, local definitions and the symbols that have been imported,
180180
* recursively adding completions from outer scopes.
@@ -442,11 +442,11 @@ object Completion {
442442
* The completion mode: defines what kinds of symbols should be included in the completion
443443
* results.
444444
*/
445-
private class Mode(val bits: Int) extends AnyVal {
445+
class Mode(val bits: Int) extends AnyVal {
446446
def is(other: Mode): Boolean = (bits & other.bits) == other.bits
447447
def |(other: Mode): Mode = new Mode(bits | other.bits)
448448
}
449-
private object Mode {
449+
object Mode {
450450
/** No symbol should be included */
451451
val None: Mode = new Mode(0)
452452

Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
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.NameOps._
9+
import dotty.tools.dotc.core.Names.{Name, termName}
10+
import dotty.tools.dotc.core.StdNames.nme
11+
import dotty.tools.dotc.core.Symbols.{Symbol, defn}
12+
import dotty.tools.dotc.core.TypeError
13+
import dotty.tools.dotc.util.Chars.{isOperatorPart, isScalaLetter}
14+
import dotty.tools.dotc.util.SourcePosition
15+
16+
object CustomCompletion {
17+
18+
def completions(
19+
pos: SourcePosition,
20+
dependencyCompleteOpt: Option[String => (Int, Seq[String])],
21+
enableDeep: Boolean
22+
)(using Context): (Int, List[Completion]) = {
23+
val path = Interactive.pathTo(ctx.compilationUnit.tpdTree, pos.span)
24+
computeCompletions(pos, path, dependencyCompleteOpt, enableDeep)(using Interactive.contextOfPath(path))
25+
}
26+
27+
def computeCompletions(
28+
pos: SourcePosition,
29+
path: List[Tree],
30+
dependencyCompleteOpt: Option[String => (Int, Seq[String])],
31+
enableDeep: Boolean
32+
)(using Context): (Int, List[Completion]) = {
33+
val mode = Completion.completionMode(path, pos)
34+
val prefix = Completion.completionPrefix(path, pos)
35+
val completer = new DeepCompleter(mode, prefix, pos)
36+
37+
var extra = List.empty[Completion]
38+
39+
val completions = path match {
40+
case Select(qual, _) :: _ => completer.selectionCompletions(qual)
41+
case Import(Ident(name), _) :: _ if name.decode.toString == "$ivy" && dependencyCompleteOpt.nonEmpty =>
42+
val complete = dependencyCompleteOpt.get
43+
val (pos, completions) = complete(prefix)
44+
val input0 = prefix.take(pos)
45+
extra ++= completions.distinct.toList
46+
.map(s => Completion(label(termName(input0 + s)), "", Nil))
47+
Map.empty
48+
case Import(expr, _) :: _ => completer.directMemberCompletions(expr)
49+
case (_: untpd.ImportSelector) :: Import(expr, _) :: _ => completer.directMemberCompletions(expr)
50+
case _ =>
51+
completer.scopeCompletions ++ {
52+
if (enableDeep) completer.deepCompletions
53+
else Nil
54+
}
55+
}
56+
57+
val describedCompletions = extra ++ describeCompletions(completions)
58+
val offset = Completion.completionOffset(path)
59+
60+
(pos.span.start - prefix.length, describedCompletions)
61+
}
62+
63+
private type CompletionMap = Map[Name, Seq[SingleDenotation]]
64+
65+
private def describeCompletions(completions: CompletionMap)(using Context): List[Completion] = {
66+
completions
67+
.toList.groupBy(_._1.toTermName) // don't distinguish between names of terms and types
68+
.toList.map { (name, namedDenots) =>
69+
val denots = namedDenots.flatMap(_._2)
70+
val typesFirst = denots.sortWith((d1, d2) => d1.isType && !d2.isType)
71+
val desc = Completion.description(typesFirst)
72+
Completion(label(name), desc, typesFirst.map(_.symbol))
73+
}
74+
}
75+
76+
class DeepCompleter(mode: Completion.Mode, prefix: String, pos: SourcePosition) extends Completion.Completer(mode, prefix, pos):
77+
def deepCompletions(using Context): Map[Name, Seq[SingleDenotation]] = {
78+
79+
def allMembers(s: Symbol) =
80+
try s.info.allMembers
81+
catch {
82+
case _: dotty.tools.dotc.core.TypeError => Nil
83+
}
84+
def rec(t: Symbol): Seq[Symbol] = {
85+
val children =
86+
if (t.is(Package) || t.is(PackageVal) || t.is(PackageClass)) {
87+
allMembers(t).map(_.symbol).filter(_ != t).flatMap(rec)
88+
} else Nil
89+
90+
t +: children.toSeq
91+
}
92+
93+
val syms = for {
94+
member <- allMembers(defn.RootClass).map(_.symbol).toList
95+
sym <- rec(member)
96+
if sym.name.toString.startsWith(prefix)
97+
} yield sym
98+
99+
syms.map(sym => (sym.fullName, List(sym: SingleDenotation))).toMap
100+
}
101+
102+
private val bslash = '\\'
103+
private val specialChars = Set('[', ']', '(', ')', '{', '}', '.', ',', ';')
104+
105+
def label(name: Name): String = {
106+
107+
def maybeQuote(name: Name, recurse: Boolean): String =
108+
if (recurse && name.isTermName)
109+
name.asTermName.qualToString(maybeQuote(_, true), maybeQuote(_, false))
110+
else {
111+
// initially adapted from
112+
// https://github.com/scala/scala/blob/decbd53f1bde4600c8ff860f30a79f028a8e431d/src/reflect/scala/reflect/internal/Printers.scala#L573-L584
113+
val decName = name.decode.toString
114+
val hasSpecialChar = decName.exists { ch =>
115+
specialChars(ch) || ch.isWhitespace
116+
}
117+
def isOperatorLike = (name.isOperatorName || decName.exists(isOperatorPart)) &&
118+
decName.exists(isScalaLetter) &&
119+
!decName.contains(bslash)
120+
lazy val term = name.toTermName
121+
122+
val needsBackTicks = hasSpecialChar ||
123+
isOperatorLike ||
124+
nme.keywords(term) && term != nme.USCOREkw
125+
126+
if (needsBackTicks) s"`$decName`"
127+
else decName
128+
}
129+
130+
maybeQuote(name, true)
131+
}
132+
}
133+
Lines changed: 171 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,171 @@
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+
deep = true // Enables CustomCompleter
122+
)
123+
val labels = completions0.map(_.label)
124+
125+
assert(offset == prefix.length)
126+
assert(labels.contains("a1"))
127+
assert(labels.contains("`a-b`"))
128+
129+
@Test def backTicksDependencies(): Unit =
130+
val prefix = "import $ivy."
131+
val input = prefix + "`org.scala-lang:scala-`"
132+
133+
val dependencies = Seq(
134+
"org.scala-lang:scala-compiler",
135+
"org.scala-lang:scala-library",
136+
"other"
137+
)
138+
val (offset, completions0) = completions(
139+
input,
140+
dependencyCompleter = Some { dep =>
141+
val matches = dependencies.filter(_.startsWith(dep))
142+
(0, matches)
143+
}
144+
)
145+
val labels = completions0.map(_.label)
146+
147+
// Seems backticks mess with that for now...
148+
// assert(offset == prefix.length)
149+
assert(labels.contains("`org.scala-lang:scala-compiler`"))
150+
assert(labels.contains("`org.scala-lang:scala-library`"))
151+
assert(labels.length == 2)
152+
153+
@Test def deep(): Unit =
154+
val prefix = ""
155+
val input = prefix + "ListBuf"
156+
157+
val (offset, completions0) = completions(input, deep = true)
158+
val labels = completions0.map(_.label)
159+
160+
assert(offset == prefix.length)
161+
assert(labels.contains("scala.collection.mutable.ListBuffer"))
162+
163+
@Test def deepType(): Unit =
164+
val prefix = ""
165+
val input = prefix + "Function2"
166+
167+
val (offset, completions0) = completions(input, deep = true)
168+
val labels = completions0.map(_.label)
169+
170+
assert(offset == prefix.length)
171+
assert(labels.contains("scala.Function2"))

0 commit comments

Comments
 (0)