Skip to content

Commit fbc17c5

Browse files
committed
properly instantiate type vars for completion labels
1 parent 4ed3838 commit fbc17c5

File tree

10 files changed

+357
-238
lines changed

10 files changed

+357
-238
lines changed

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

Lines changed: 99 additions & 83 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@ import dotty.tools.dotc.core.TypeError
1919
import dotty.tools.dotc.core.Phases
2020
import dotty.tools.dotc.core.Types.{AppliedType, ExprType, MethodOrPoly, NameFilter, NoType, RefinedType, TermRef, Type, TypeProxy}
2121
import dotty.tools.dotc.parsing.Tokens
22+
import dotty.tools.dotc.typer.Implicits.SearchSuccess
23+
import dotty.tools.dotc.typer.Inferencing
2224
import dotty.tools.dotc.util.Chars
2325
import dotty.tools.dotc.util.SourcePosition
2426

@@ -28,6 +30,7 @@ import dotty.tools.dotc.core.ContextOps.localContext
2830
import dotty.tools.dotc.core.Names
2931
import dotty.tools.dotc.core.Types
3032
import dotty.tools.dotc.core.Symbols
33+
import dotty.tools.dotc.core.Constants
3134

3235
/**
3336
* One of the results of a completion query.
@@ -49,8 +52,31 @@ object Completion:
4952
* @return offset and list of symbols for possible completions
5053
*/
5154
def completions(pos: SourcePosition)(using Context): (Int, List[Completion]) =
52-
val path: List[Tree] = Interactive.pathTo(ctx.compilationUnit.tpdTree, pos.span)
53-
computeCompletions(pos, path)(using Interactive.contextOfPath(path).withPhase(Phases.typerPhase))
55+
val tpdPath = Interactive.pathTo(ctx.compilationUnit.tpdTree, pos.span)
56+
val completionContext = Interactive.contextOfPath(tpdPath).withPhase(Phases.typerPhase)
57+
inContext(completionContext):
58+
val untpdPath = Interactive.resolveTypedOrUntypedPath(tpdPath, pos)
59+
val mode = completionMode(untpdPath, pos)
60+
val rawPrefix = completionPrefix(untpdPath, pos)
61+
val completions = rawCompletions(pos, mode, rawPrefix, tpdPath, untpdPath)
62+
63+
postProcessCompletions(untpdPath, completions, rawPrefix)
64+
65+
66+
/** Get possible completions from tree at `pos`
67+
* This method requires manually computing the mode, prefix and paths.
68+
*
69+
* @return completion map of name to list of denotations
70+
*/
71+
def rawCompletions(
72+
pos: SourcePosition,
73+
mode: Mode,
74+
rawPrefix: String,
75+
tpdPath: List[Tree],
76+
untpdPath: List[untpd.Tree]
77+
)(using Context): CompletionMap =
78+
val adjustedPath = typeCheckExtensionConstructPath(untpdPath, tpdPath, pos)
79+
computeCompletions(pos, mode, rawPrefix, adjustedPath)
5480

5581
/**
5682
* Inspect `path` to determine what kinds of symbols should be considered.
@@ -63,90 +89,69 @@ object Completion:
6389
* Otherwise, provide no completion suggestion.
6490
*/
6591
def completionMode(path: List[untpd.Tree], pos: SourcePosition): Mode =
66-
path match
67-
case untpd.Ident(_) :: untpd.Import(_, _) :: _ => Mode.ImportOrExport
68-
case untpd.Ident(_) :: (_: untpd.ImportSelector) :: _ => Mode.ImportOrExport
69-
case (ref: untpd.RefTree) :: _ =>
70-
if (ref.name.isTermName) Mode.Term
71-
else if (ref.name.isTypeName) Mode.Type
72-
else Mode.None
7392

74-
case (sel: untpd.ImportSelector) :: _ =>
75-
if sel.imported.span.contains(pos.span) then Mode.ImportOrExport
76-
else Mode.None // Can't help completing the renaming
93+
val completionSymbolKind: Mode =
94+
path match
95+
case untpd.Ident(_) :: untpd.Import(_, _) :: _ => Mode.ImportOrExport
96+
case untpd.Ident(_) :: (_: untpd.ImportSelector) :: _ => Mode.ImportOrExport
97+
case Literal(Constants.Constant(_: String)) :: _ => Mode.Term // literal completions
98+
case (ref: untpd.RefTree) :: _ =>
99+
if (ref.name.isTermName) Mode.Term
100+
else if (ref.name.isTypeName) Mode.Type
101+
else Mode.None
77102

78-
case (_: untpd.ImportOrExport) :: _ => Mode.ImportOrExport
79-
case _ => Mode.None
103+
case (sel: untpd.ImportSelector) :: _ =>
104+
if sel.imported.span.contains(pos.span) then Mode.ImportOrExport
105+
else Mode.None // Can't help completing the renaming
80106

81-
/** When dealing with <errors> in varios palces we check to see if they are
82-
* due to incomplete backticks. If so, we ensure we get the full prefix
83-
* including the backtick.
84-
*
85-
* @param content The source content that we'll check the positions for the prefix
86-
* @param start The start position we'll start to look for the prefix at
87-
* @param end The end position we'll look for the prefix at
88-
* @return Either the full prefix including the ` or an empty string
89-
*/
90-
private def checkBacktickPrefix(content: Array[Char], start: Int, end: Int): String =
91-
content.lift(start) match
92-
case Some(char) if char == '`' =>
93-
content.slice(start, end).mkString
94-
case _ =>
95-
""
107+
case (_: untpd.ImportOrExport) :: _ => Mode.ImportOrExport
108+
case _ => Mode.None
109+
110+
val completionKind: Mode =
111+
path match
112+
case Nil | (_: PackageDef) :: _ => Mode.None
113+
case untpd.Ident(_) :: (_: untpd.ImportSelector) :: _ => Mode.Member
114+
case (_: Select) :: _ => Mode.Member
115+
case _ => Mode.Scope
116+
117+
completionSymbolKind | completionKind
96118

97119
/**
98120
* Inspect `path` to determine the completion prefix. Only symbols whose name start with the
99121
* returned prefix should be considered.
100122
*/
101123
def completionPrefix(path: List[untpd.Tree], pos: SourcePosition)(using Context): String =
124+
def fallback: Int =
125+
var i = pos.point - 1
126+
while i >= 0 && Chars.isIdentifierPart(pos.source.content()(i)) do i -= 1
127+
i + 1
128+
102129
path match
103130
case (sel: untpd.ImportSelector) :: _ =>
104131
completionPrefix(sel.imported :: Nil, pos)
105132

106133
case untpd.Ident(_) :: (sel: untpd.ImportSelector) :: _ if !sel.isGiven =>
107-
completionPrefix(sel.imported :: Nil, pos)
134+
if sel.isWildcard then pos.source.content()(pos.point - 1).toString
135+
else completionPrefix(sel.imported :: Nil, pos)
108136

109137
case (tree: untpd.ImportOrExport) :: _ =>
110138
tree.selectors.find(_.span.contains(pos.span)).map: selector =>
111139
completionPrefix(selector :: Nil, pos)
112140
.getOrElse("")
113141

114-
// Foo.`se<TAB> will result in Select(Ident(Foo), <error>)
115-
case (select: untpd.Select) :: _ if select.name == nme.ERROR =>
116-
checkBacktickPrefix(select.source.content(), select.nameSpan.start, select.span.end)
142+
case (tree: untpd.RefTree) :: _ if tree.name != nme.ERROR =>
143+
tree.name.toString.take(pos.span.point - tree.span.point)
117144

118-
// import scala.util.chaining.`s<TAB> will result in a Ident(<error>)
119-
case (ident: untpd.Ident) :: _ if ident.name == nme.ERROR =>
120-
checkBacktickPrefix(ident.source.content(), ident.span.start, ident.span.end)
145+
case _ => pos.source.content.slice(fallback, pos.point).mkString
121146

122-
case (ref: untpd.RefTree) :: _ =>
123-
if (ref.name == nme.ERROR) ""
124-
else ref.name.toString.take(pos.span.point - ref.span.point)
125-
126-
case _ => ""
127147

128148
end completionPrefix
129149

130150
/** Inspect `path` to determine the offset where the completion result should be inserted. */
131151
def completionOffset(untpdPath: List[untpd.Tree]): Int =
132-
untpdPath match {
152+
untpdPath match
133153
case (ref: untpd.RefTree) :: _ => ref.span.point
134154
case _ => 0
135-
}
136-
137-
/** Some information about the trees is lost after Typer such as Extension method construct
138-
* is expanded into methods. In order to support completions in those cases
139-
* we have to rely on untyped trees and only when types are necessary use typed trees.
140-
*/
141-
def resolveTypedOrUntypedPath(tpdPath: List[Tree], pos: SourcePosition)(using Context): List[untpd.Tree] =
142-
lazy val untpdPath: List[untpd.Tree] = NavigateAST
143-
.pathTo(pos.span, List(ctx.compilationUnit.untpdTree), true).collect:
144-
case untpdTree: untpd.Tree => untpdTree
145-
146-
tpdPath match
147-
case (_: Bind) :: _ => tpdPath
148-
case (_: untpd.TypTree) :: _ => tpdPath
149-
case _ => untpdPath
150155

151156
/** Handle case when cursor position is inside extension method construct.
152157
* The extension method construct is then desugared into methods, and consturct parameters
@@ -170,18 +175,12 @@ object Completion:
170175
Interactive.pathTo(typedEnclosingParam, pos.span)
171176
.flatten.getOrElse(tpdPath)
172177

173-
private def computeCompletions(pos: SourcePosition, tpdPath: List[Tree])(using Context): (Int, List[Completion]) =
174-
val path0 = resolveTypedOrUntypedPath(tpdPath, pos)
175-
val mode = completionMode(path0, pos)
176-
val rawPrefix = completionPrefix(path0, pos)
177-
178+
private def computeCompletions(pos: SourcePosition, mode: Mode, rawPrefix: String, adjustedPath: List[Tree])(using Context): CompletionMap =
178179
val hasBackTick = rawPrefix.headOption.contains('`')
179180
val prefix = if hasBackTick then rawPrefix.drop(1) else rawPrefix
180-
181181
val completer = new Completer(mode, prefix, pos)
182182

183-
val adjustedPath = typeCheckExtensionConstructPath(path0, tpdPath, pos)
184-
val completions = adjustedPath match
183+
val result = adjustedPath match
185184
// Ignore synthetic select from `This` because in code it was `Ident`
186185
// See example in dotty.tools.languageserver.CompletionTest.syntheticThis
187186
case Select(qual @ This(_), _) :: _ if qual.span.isSynthetic => completer.scopeCompletions
@@ -191,17 +190,24 @@ object Completion:
191190
case (_: untpd.ImportSelector) :: Import(expr, _) :: _ => completer.directMemberCompletions(expr)
192191
case _ => completer.scopeCompletions
193192

193+
interactiv.println(i"""completion info with pos = $pos,
194+
| prefix = ${completer.prefix},
195+
| term = ${completer.mode.is(Mode.Term)},
196+
| type = ${completer.mode.is(Mode.Type)},
197+
| scope = ${completer.mode.is(Mode.Scope)},
198+
| member = ${completer.mode.is(Mode.Member)}""")
199+
200+
result
201+
202+
def postProcessCompletions(path: List[untpd.Tree], completions: CompletionMap, rawPrefix: String)(using Context): (Int, List[Completion]) =
194203
val describedCompletions = describeCompletions(completions)
204+
val hasBackTick = rawPrefix.headOption.contains('`')
195205
val backtickedCompletions =
196206
describedCompletions.map(completion => backtickCompletions(completion, hasBackTick))
197207

198-
val offset = completionOffset(path0)
208+
interactiv.println(i"""completion resutls = $backtickedCompletions%, %""")
199209

200-
interactiv.println(i"""completion with pos = $pos,
201-
| prefix = ${completer.prefix},
202-
| term = ${completer.mode.is(Mode.Term)},
203-
| type = ${completer.mode.is(Mode.Type)}
204-
| results = $backtickedCompletions%, %""")
210+
val offset = completionOffset(path)
205211
(offset, backtickedCompletions)
206212

207213
def backtickCompletions(completion: Completion, hasBackTick: Boolean) =
@@ -415,11 +421,22 @@ object Completion:
415421

416422
/** Completions from implicit conversions including old style extensions using implicit classes */
417423
private def implicitConversionMemberCompletions(qual: Tree)(using Context): CompletionMap =
424+
425+
def tryToInstantiateTypeVars(conversionTarget: SearchSuccess): Type =
426+
try
427+
val typingCtx = ctx.fresh
428+
inContext(typingCtx):
429+
val methodRefTree = ref(conversionTarget.ref, needLoad = false)
430+
val convertedTree = ctx.typer.typedAheadExpr(untpd.Apply(untpd.TypedSplice(methodRefTree), untpd.TypedSplice(qual) :: Nil))
431+
Inferencing.fullyDefinedType(convertedTree.tpe, "", pos)
432+
catch
433+
case error => conversionTarget.tree.tpe // fallback to not fully defined type
434+
418435
if qual.typeOpt.isExactlyNothing || qual.typeOpt.isNullType then
419436
Map.empty
420437
else
421438
implicitConversionTargets(qual)(using ctx.fresh.setExploreTyperState())
422-
.flatMap(accessibleMembers)
439+
.flatMap { conversionTarget => accessibleMembers(tryToInstantiateTypeVars(conversionTarget)) }
423440
.toSeq
424441
.groupByName
425442

@@ -551,19 +568,14 @@ object Completion:
551568
* @param qual The argument to which the implicit conversion should be applied.
552569
* @return The set of types after `qual` implicit conversion.
553570
*/
554-
private def implicitConversionTargets(qual: Tree)(using Context): Set[Type] =
571+
private def implicitConversionTargets(qual: Tree)(using Context): Set[SearchSuccess] = {
555572
val typer = ctx.typer
556-
val targets = try {
557-
val conversions = new typer.ImplicitSearch(defn.AnyType, qual, pos.span).allImplicits
558-
conversions.map(_.tree.typeOpt)
559-
} catch {
560-
case _ =>
561-
interactiv.println(i"implicit conversion targets failed: ${qual.show}")
562-
Set.empty
563-
}
573+
val conversions = new typer.ImplicitSearch(defn.AnyType, qual, pos.span).allImplicits
574+
conversions.map(_.tree.typeOpt)
564575

565-
interactiv.println(i"implicit conversion targets considered: ${targets.toList}%, %")
566-
targets
576+
interactiv.println(i"implicit conversion targets considered: ${conversions.toList}%, %")
577+
conversions
578+
}
567579

568580
/** Filter for names that should appear when looking for completions. */
569581
private object completionsFilter extends NameFilter:
@@ -606,3 +618,7 @@ object Completion:
606618
/** Both term and type symbols are allowed */
607619
val ImportOrExport: Mode = new Mode(4) | Term | Type
608620

621+
val Scope: Mode = new Mode(8)
622+
623+
val Member: Mode = new Mode(16)
624+

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

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -420,6 +420,21 @@ object Interactive {
420420
false
421421
}
422422

423+
424+
/** Some information about the trees is lost after Typer such as Extension method construct
425+
* is expanded into methods. In order to support completions in those cases
426+
* we have to rely on untyped trees and only when types are necessary use typed trees.
427+
*/
428+
def resolveTypedOrUntypedPath(tpdPath: List[Tree], pos: SourcePosition)(using Context): List[untpd.Tree] =
429+
lazy val untpdPath: List[untpd.Tree] = NavigateAST
430+
.pathTo(pos.span, List(ctx.compilationUnit.untpdTree), true).collect:
431+
case untpdTree: untpd.Tree => untpdTree
432+
433+
tpdPath match
434+
case (_: Bind) :: _ => tpdPath
435+
case (_: untpd.TypTree) :: _ => tpdPath
436+
case _ => untpdPath
437+
423438
/**
424439
* Is this tree using a renaming introduced by an import statement or an alias for `this`?
425440
*

0 commit comments

Comments
 (0)