Skip to content

Commit 9441ab6

Browse files
Make the completion logic easier to extend
1 parent a4fd829 commit 9441ab6

File tree

3 files changed

+282
-8
lines changed

3 files changed

+282
-8
lines changed

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

Lines changed: 43 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,39 @@ 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 isDot = (x: Char) => x == '.'
466+
private val brackets = List('[',']','(',')','{','}')
467+
468+
def label(name: Name): String = {
469+
470+
def maybeQuote(name: Name, recurse: Boolean): String =
471+
if (recurse && name.isTermName)
472+
name.asTermName.qualToString(maybeQuote(_, true), maybeQuote(_, false))
473+
// initially adapted from
474+
// https://github.com/scala/scala/blob/decbd53f1bde4600c8ff860f30a79f028a8e431d/
475+
// src/reflect/scala/reflect/internal/Printers.scala#L573-L584
476+
else if (name == nme.CONSTRUCTOR) "this"
477+
else {
478+
val decName = name.decode.toString
479+
val hasSpecialChar = decName.exists { ch =>
480+
brackets.contains(ch) || ch.isWhitespace || isDot(ch)
481+
}
482+
def isOperatorLike = (name.isOperatorName || decName.exists(isOperatorPart)) &&
483+
decName.exists(isScalaLetter) &&
484+
!decName.contains(bslash)
485+
lazy val term = name.toTermName
486+
487+
val needsBackTicks = hasSpecialChar ||
488+
isOperatorLike ||
489+
nme.keywords(term) && term != nme.USCOREkw
490+
491+
if (needsBackTicks) s"`$decName`"
492+
else decName
493+
}
494+
495+
maybeQuote(name, true)
496+
}
462497
}
463498

Lines changed: 152 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,152 @@
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 CompletionTests extends DottyTest:
16+
17+
private def completions(
18+
input: String,
19+
dependencyCompleter: Option[String => (Int, Seq[String])] = None,
20+
deep: Boolean = false
21+
): (Int, Seq[Completion]) =
22+
val prefix = """
23+
object Wrapper {
24+
val expr = {
25+
"""
26+
val suffix = """
27+
}
28+
}
29+
"""
30+
31+
val allCode = prefix + input + suffix
32+
val index = prefix.length + input.length
33+
34+
val run = new Run(
35+
new Compiler,
36+
initialCtx.fresh
37+
.addMode(Mode.ReadPositions | Mode.Interactive)
38+
// discard errors - comment out this line to print them in the console
39+
.setReporter(new StoreReporter(null))
40+
.setSetting(initialCtx.settings.YstopAfter, List("typer"))
41+
)
42+
val file = SourceFile.virtual("<completions>", allCode, maybeIncomplete = true)
43+
given ctx: Context = run.runContext.withSource(file)
44+
val unit = CompilationUnit(file)
45+
ctx
46+
.run
47+
.compileUnits(unit :: Nil, ctx)
48+
49+
// ignoring compilation errors here - the input code
50+
// to complete likely doesn't compile
51+
52+
unit.tpdTree = {
53+
import tpd._
54+
unit.tpdTree match {
55+
case PackageDef(_, p) =>
56+
p.collectFirst {
57+
case TypeDef(_, tmpl: Template) =>
58+
tmpl.body
59+
.collectFirst { case dd: ValDef if dd.name.show == "expr" => dd }
60+
.getOrElse(sys.error("Unexpected tree shape"))
61+
}
62+
.getOrElse(sys.error("Unexpected tree shape"))
63+
case _ => sys.error("Unexpected tree shape")
64+
}
65+
}
66+
val ctx1 = ctx.fresh.setCompilationUnit(unit)
67+
val srcPos = SourcePosition(file, Span(index))
68+
val (offset0, completions) =
69+
if (deep || dependencyCompleter.nonEmpty)
70+
CustomCompletion.completions(srcPos, dependencyCompleteOpt = dependencyCompleter, enableDeep = deep)(using ctx1)
71+
else
72+
Completion.completions(srcPos)(using ctx1)
73+
val offset = offset0 - prefix.length
74+
(offset, completions)
75+
76+
77+
@Test def simple(): Unit =
78+
val prefix = "scala.collection.immutable."
79+
val input = prefix + "Ma"
80+
81+
val (offset, completions0) = completions(input)
82+
val labels = completions0.map(_.label)
83+
84+
assert(offset == prefix.length)
85+
assert(labels.contains("Map"))
86+
87+
@Test def custom(): Unit =
88+
val prefix = "import $ivy."
89+
val input = prefix + "scala"
90+
91+
val dependencies = Seq(
92+
"scalaCompiler",
93+
"scalaLibrary",
94+
"other"
95+
)
96+
val (offset, completions0) = completions(
97+
input,
98+
dependencyCompleter = Some { dep =>
99+
val matches = dependencies.filter(_.startsWith(dep))
100+
(0, matches)
101+
}
102+
)
103+
val labels = completions0.map(_.label)
104+
105+
assert(offset == prefix.length)
106+
assert(labels.contains("scalaCompiler"))
107+
assert(labels.contains("scalaLibrary"))
108+
assert(labels.length == 2)
109+
110+
@Test def backTicks(): Unit =
111+
val prefix = "import $ivy."
112+
val input = prefix + "`org.scala-lang:scala-`"
113+
114+
val dependencies = Seq(
115+
"org.scala-lang:scala-compiler",
116+
"org.scala-lang:scala-library",
117+
"other"
118+
)
119+
val (offset, completions0) = completions(
120+
input,
121+
dependencyCompleter = Some { dep =>
122+
val matches = dependencies.filter(_.startsWith(dep))
123+
(0, matches)
124+
}
125+
)
126+
val labels = completions0.map(_.label)
127+
128+
// Seems backticks mess with that for now...
129+
// assert(offset == prefix.length)
130+
assert(labels.contains("`org.scala-lang:scala-compiler`"))
131+
assert(labels.contains("`org.scala-lang:scala-library`"))
132+
assert(labels.length == 2)
133+
134+
@Test def deep(): Unit =
135+
val prefix = ""
136+
val input = prefix + "ListBuf"
137+
138+
val (offset, completions0) = completions(input, deep = true)
139+
val labels = completions0.map(_.label)
140+
141+
assert(offset == prefix.length)
142+
assert(labels.contains("scala.collection.mutable.ListBuffer"))
143+
144+
@Test def deepType(): Unit =
145+
val prefix = ""
146+
val input = prefix + "Function2"
147+
148+
val (offset, completions0) = completions(input, deep = true)
149+
val labels = completions0.map(_.label)
150+
151+
assert(offset == prefix.length)
152+
assert(labels.contains("scala.Function2"))
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+

0 commit comments

Comments
 (0)