Skip to content

Commit 43455e4

Browse files
committed
Fix #5460: Improve completion of import nodes
1 parent 30b8b1c commit 43455e4

File tree

5 files changed

+168
-18
lines changed

5 files changed

+168
-18
lines changed

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

Lines changed: 45 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -166,31 +166,64 @@ object Interactive {
166166
private def computeCompletions(pos: SourcePosition, path: List[Tree])(implicit ctx: Context): (Int, List[Symbol]) = {
167167
val completions = Scopes.newScope.openForMutations
168168

169-
val (completionPos, prefix, termOnly, typeOnly) = path match {
169+
/**
170+
* Extract basic info about completion location and the kind of symbols to include.
171+
*
172+
* @param path The path to the position where completion happens
173+
* @param inImport If set, indicates that this is the completion of an import node. When
174+
* completing imports, both types and terms are always included.
175+
* @return The point where to insert completion, whether terms should be included in results,
176+
* whether types should be included, and whether we're completing an import.
177+
*/
178+
def completionInfo(path: List[Tree], inImport: Boolean): (Int, String, Boolean, Boolean, Boolean) = path match {
170179
case (ref: RefTree) :: _ =>
171180
if (ref.name == nme.ERROR)
172-
(ref.pos.point, "", false, false)
181+
(ref.pos.point, "", false, false, inImport)
173182
else
174183
(ref.pos.point,
175184
ref.name.toString.take(pos.pos.point - ref.pos.point),
176-
ref.name.isTermName,
177-
ref.name.isTypeName)
185+
!inImport && ref.name.isTermName, // Types and terms are always accepted in imports
186+
!inImport && ref.name.isTypeName,
187+
inImport)
178188
case _ =>
179-
(0, "", false, false)
189+
(0, "", false, false, false)
190+
}
191+
192+
val (completionPos, prefix, termOnly, typeOnly, inImport) = path match {
193+
case (imp: Import) :: _ =>
194+
imp.selectors.find(_.pos.contains(pos.pos)) match {
195+
case None => (imp.expr.pos.point, "", false, false, true)
196+
case Some(sel) => completionInfo(sel.asInstanceOf[tpd.Tree] :: Nil, /* inImport = */ true)
197+
}
198+
case other =>
199+
completionInfo(other, /* inImport = */ false)
180200
}
181201

182202
/** Include in completion sets only symbols that
183203
* 1. start with given name prefix, and
184204
* 2. do not contain '$' except in prefix where it is explicitly written by user, and
185-
* 3. have same term/type kind as name prefix given so far
205+
* 3. are not a primary constructor,
206+
* 4. have an existing source symbol,
207+
* 5. are the module class in case of packages,
208+
* 6. are mutable accessors, to exclude setters for `var`,
209+
* 7. have same term/type kind as name prefix given so far
186210
*
187211
* The reason for (2) is that we do not want to present compiler-synthesized identifiers
188212
* as completion results. However, if a user explicitly writes all '$' characters in an
189213
* identifier, we should complete the rest.
214+
*
215+
* The reason for (4) is that we want to filter, for instance, non-existnet `Module`
216+
* symbols that accompany class symbols. We can't simply return only the source symbols,
217+
* because this would discard some synthetic symbols such as the copy method of case
218+
* classes.
190219
*/
191220
def include(sym: Symbol) =
192221
sym.name.startsWith(prefix) &&
193222
!sym.name.toString.drop(prefix.length).contains('$') &&
223+
!sym.isPrimaryConstructor &&
224+
sym.sourceSymbol.exists &&
225+
(!sym.is(Package) || !sym.moduleClass.exists) &&
226+
!sym.is(allOf(Mutable, Accessor)) &&
194227
(!termOnly || sym.isTerm) &&
195228
(!typeOnly || sym.isType)
196229

@@ -270,12 +303,16 @@ object Interactive {
270303

271304
def getMemberCompletions(qual: Tree): Unit = {
272305
addAccessibleMembers(qual.tpe)
273-
implicitConversionTargets(qual)(ctx.fresh.setExploreTyperState())
274-
.foreach(addAccessibleMembers(_))
306+
if (!inImport) {
307+
// Implicit conversions do not kick in when importing
308+
implicitConversionTargets(qual)(ctx.fresh.setExploreTyperState())
309+
.foreach(addAccessibleMembers(_))
310+
}
275311
}
276312

277313
path match {
278314
case (sel @ Select(qual, _)) :: _ => getMemberCompletions(qual)
315+
case (imp @ Import(expr, _)) :: _ => getMemberCompletions(expr)
279316
case _ => getScopeCompletions(ctx)
280317
}
281318

language-server/src/dotty/tools/languageserver/DottyLanguageServer.scala

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -779,7 +779,7 @@ object DottyLanguageServer {
779779
def completionItemKind(sym: Symbol)(implicit ctx: Context): lsp4j.CompletionItemKind = {
780780
import lsp4j.{CompletionItemKind => CIK}
781781

782-
if (sym.is(Package))
782+
if (sym.is(Package) || sym.is(Module))
783783
CIK.Module // No CompletionItemKind.Package (https://github.com/Microsoft/language-server-protocol/issues/155)
784784
else if (sym.isConstructor)
785785
CIK.Constructor
Lines changed: 100 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,119 @@
11
package dotty.tools.languageserver
22

3+
import org.junit.Assert.{assertTrue, assertFalse}
34
import org.junit.Test
4-
import org.eclipse.lsp4j.CompletionItemKind
5+
import org.eclipse.lsp4j.CompletionItemKind._
56

67
import dotty.tools.languageserver.util.Code._
78

89
class CompletionTest {
910

1011
@Test def completion0: Unit = {
1112
code"class Foo { val xyz: Int = 0; def y: Int = xy$m1 }".withSource
12-
.completion(m1, Set(("xyz", CompletionItemKind.Field, "Int")))
13+
.completion(m1, Set(("xyz", Field, "Int")))
1314
}
1415

1516
@Test def completionWithImplicitConversion: Unit = {
1617
withSources(
1718
code"object Foo { implicit class WithBaz(bar: Bar) { def baz = 0 } }",
1819
code"class Bar",
1920
code"object Main { import Foo._; val bar: Bar = new Bar; bar.b${m1} }"
20-
) .completion(m1, Set(("baz", CompletionItemKind.Method, "=> Int")))
21+
) .completion(m1, Set(("baz", Method, "=> Int")))
22+
}
23+
24+
@Test def importCompleteClassWithPrefix: Unit = {
25+
withSources(
26+
code"""object Foo { class MyClass }""",
27+
code"""import Foo.My${m1}"""
28+
).completion(m1, Set(("MyClass", Class, "Object{...}")))
29+
}
30+
31+
@Test def ImportCompleteClassNoPrefix: Unit = {
32+
withSources(
33+
code"""object Foo { class MyClass }""",
34+
code"""import Foo.${m1}"""
35+
).completion(m1, results => {
36+
val myClass = ("MyClass", Class, "Object{...}")
37+
assertTrue(results.contains(("MyClass", Class, "Object{...}")))
38+
39+
// Verify that apart from `MyClass`, we only have the methods that exists on `Foo`
40+
assertTrue((results - myClass).forall { case (_, kind, _) => kind == Method })
41+
42+
// Verify that we don't have things coming from an implicit conversion, such as ensuring
43+
assertFalse(results.exists { case (name, _, _) => name == "ensuring" })
44+
})
45+
}
46+
47+
@Test def importCompleteFromPackage: Unit = {
48+
withSources(
49+
code"""package a
50+
class MyClass""",
51+
code"""package b
52+
import a.My${m1}"""
53+
).completion(m1, Set(("MyClass", Class, "Object{...}")))
54+
}
55+
56+
@Test def importCompleteFromClass: Unit = {
57+
withSources(
58+
code"""class Foo { val x: Int = 0 }""",
59+
code"""import Foo.${m1}"""
60+
).completion(m1, Set())
61+
}
62+
63+
@Test def importCompleteIncludesSynthetic: Unit = {
64+
code"""case class MyCaseClass(foobar: Int)
65+
object O {
66+
val x = MyCaseClass(0)
67+
import x.c${m1}
68+
}""".withSource
69+
.completion(
70+
m1,
71+
Set(("clone", Method, "(): Object"),
72+
("copy", Method, "(foobar: Int): MyCaseClass"),
73+
("canEqual", Method, "(that: Any): Boolean")))
74+
}
75+
76+
@Test def importCompleteIncludeModule: Unit = {
77+
withSources(
78+
code"""object O { object MyObject }""",
79+
code"""import O.My${m1}"""
80+
).completion(m1, Set(("MyObject", Module, "O.MyObject")))
81+
}
82+
83+
@Test def importCompleteIncludeClassAndCompanion: Unit = {
84+
withSources(
85+
code"""package pkg0
86+
class Foo
87+
object Foo""",
88+
code"""package pgk1
89+
import pkg0.F${m1}"""
90+
).completion(m1, Set(("Foo", Class, "Object{...}"), ("Foo", Module, "pkg0.Foo")))
91+
}
92+
93+
@Test def importCompleteIncludePackage: Unit = {
94+
withSources(
95+
code"""package foo.bar
96+
class Fizz""",
97+
code"""import foo.b${m1}"""
98+
).completion(m1, Set(("bar", Module, "{...}")))
99+
}
100+
101+
@Test def importCompleteIncludeMembers: Unit = {
102+
withSources(
103+
code"""object MyObject {
104+
val myVal = 0
105+
def myDef = 0
106+
var myVar = 0
107+
object myObject
108+
class myClass
109+
trait myTrait
110+
}""",
111+
code"""import MyObject.my${m1}"""
112+
).completion(m1, Set(("myVal", Field, "Int"),
113+
("myDef", Method, "=> Int"),
114+
("myVar", Variable, "Int"),
115+
("myObject", Module, "MyObject.myObject"),
116+
("myClass", Class, "Object{...}"),
117+
("myTrait", Class, "Object{...}")))
21118
}
22119
}

language-server/test/dotty/tools/languageserver/util/CodeTester.scala

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@ import org.eclipse.lsp4j.{ CompletionItemKind, DocumentHighlightKind, Diagnostic
1212

1313
import org.junit.Assert.assertEquals
1414

15+
import org.junit.Assert.assertEquals
16+
1517
/**
1618
* Simulates an LSP client for test in a project defined by `sources`.
1719
*
@@ -114,7 +116,19 @@ class CodeTester(projects: List[Project]) {
114116
* @see dotty.tools.languageserver.util.actions.CodeCompletion
115117
*/
116118
def completion(marker: CodeMarker, expected: Set[(String, CompletionItemKind, String)]): this.type =
117-
doAction(new CodeCompletion(marker, expected))
119+
completion(marker, assertEquals(expected, _))
120+
121+
/**
122+
* Requests completion at the position defined by `marker`, and pass the results to
123+
* `checkResults`.
124+
*
125+
* @param marker The position from which to ask for completions.
126+
* @param checkResults A function that verifies that the results of completion are correct.
127+
*
128+
* @see dotty.tools.languageserver.util.actions.CodeCompletion
129+
*/
130+
def completion(marker: CodeMarker, checkResults: Set[(String, CompletionItemKind, String)] => Unit): this.type =
131+
doAction(new CodeCompletion(marker, checkResults))
118132

119133
/**
120134
* Performs a workspace-wide renaming of the symbol under `marker`, verifies that the positions to

language-server/test/dotty/tools/languageserver/util/actions/CodeCompletion.scala

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -13,11 +13,13 @@ import scala.collection.JavaConverters._
1313
* An action requesting for code completion at `marker`, expecting `expected`.
1414
* This action corresponds to the `textDocument/completion` method of the Language Server Protocol.
1515
*
16-
* @param marker The marker indicating the position where completion should be requested.
17-
* @param expected The expected results from the language server.
16+
* @param marker The marker indicating the position where completion should be requested.
17+
* @param checkResults A function that takes the results and verifies that they match
18+
* expectations.
1819
*/
1920
class CodeCompletion(override val marker: CodeMarker,
20-
expected: Set[(String, CompletionItemKind, String)]) extends ActionOnMarker {
21+
checkResults: Set[(String, CompletionItemKind, String)] => Unit)
22+
extends ActionOnMarker {
2123

2224
override def execute(): Exec[Unit] = {
2325
val result = server.completion(marker.toCompletionParams).get()
@@ -26,9 +28,9 @@ class CodeCompletion(override val marker: CodeMarker,
2628
val completionResults = result.getRight.getItems.asScala.toSet.map { item =>
2729
(item.getLabel, item.getKind, item.getDetail)
2830
}
29-
assertEquals(expected, completionResults)
31+
checkResults(completionResults)
3032
}
3133

3234
override def show: PositionContext.PosCtx[String] =
33-
s"CodeCompletion(${marker.show}, $expected)"
35+
s"CodeCompletion(${marker.show}, $checkResults)"
3436
}

0 commit comments

Comments
 (0)