Skip to content

Commit a28d1a6

Browse files
authored
improvement: Support completions for implicit classes (#19314)
Previously, we would only support extension methods and not the old style implicit classes. Now, those are supported also. Introduced in scalameta/metals#5904
2 parents 58b8108 + 3c5e216 commit a28d1a6

File tree

6 files changed

+230
-6
lines changed

6 files changed

+230
-6
lines changed

presentation-compiler/src/main/dotty/tools/pc/CompilerSearchVisitor.scala

+8-1
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import scala.meta.pc.*
99
import scala.util.control.NonFatal
1010

1111
import dotty.tools.dotc.core.Contexts.*
12+
import dotty.tools.dotc.core.Flags
1213
import dotty.tools.dotc.core.Names.*
1314
import dotty.tools.dotc.core.Symbols.*
1415

@@ -19,8 +20,14 @@ class CompilerSearchVisitor(
1920

2021
val logger: Logger = Logger.getLogger(classOf[CompilerSearchVisitor].getName().nn).nn
2122

23+
private def isAccessibleImplicitClass(sym: Symbol) =
24+
val owner = sym.maybeOwner
25+
owner != NoSymbol && owner.isClass &&
26+
owner.is(Flags.Implicit) &&
27+
owner.isStatic && owner.isPublic
28+
2229
private def isAccessible(sym: Symbol): Boolean = try
23-
sym != NoSymbol && sym.isPublic && sym.isStatic
30+
sym != NoSymbol && sym.isPublic && sym.isStatic || isAccessibleImplicitClass(sym)
2431
catch
2532
case err: AssertionError =>
2633
logger.log(Level.WARNING, err.getMessage())

presentation-compiler/src/main/dotty/tools/pc/SemanticdbSymbols.scala

+11
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,16 @@ object SemanticdbSymbols:
4949
// however in scalac this method is defined only in `module Files`
5050
if typeSym.is(JavaDefined) then
5151
typeSym :: owner.info.decl(termName(value)).symbol :: Nil
52+
/**
53+
* Looks like decl doesn't work for:
54+
* package a:
55+
* implicit class <<A>> (i: Int):
56+
* def inc = i + 1
57+
*/
58+
else if typeSym == NoSymbol then
59+
owner.info.member(typeName(value)).symbol :: Nil
5260
else typeSym :: Nil
61+
end if
5362
case Descriptor.Term(value) =>
5463
val outSymbol = owner.info.decl(termName(value)).symbol
5564
if outSymbol.exists
@@ -92,6 +101,8 @@ object SemanticdbSymbols:
92101
.map(_.symbol)
93102
.filter(sym => symbolName(sym) == s)
94103
.toList
104+
end match
105+
end tryMember
95106

96107
parentSymbol.flatMap(tryMember)
97108
try

presentation-compiler/src/main/dotty/tools/pc/completions/CompletionProvider.scala

+2-2
Original file line numberDiff line numberDiff line change
@@ -215,7 +215,7 @@ class CompletionProvider(
215215

216216
def mkItemWithImports(
217217
v: CompletionValue.Workspace | CompletionValue.Extension |
218-
CompletionValue.Interpolator
218+
CompletionValue.Interpolator | CompletionValue.ImplicitClass
219219
) =
220220
val sym = v.symbol
221221
path match
@@ -260,7 +260,7 @@ class CompletionProvider(
260260
end mkItemWithImports
261261

262262
completion match
263-
case v: (CompletionValue.Workspace | CompletionValue.Extension) =>
263+
case v: (CompletionValue.Workspace | CompletionValue.Extension | CompletionValue.ImplicitClass) =>
264264
mkItemWithImports(v)
265265
case v: CompletionValue.Interpolator if v.isWorkspace || v.isExtension =>
266266
mkItemWithImports(v)

presentation-compiler/src/main/dotty/tools/pc/completions/CompletionValue.scala

+14
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,20 @@ object CompletionValue:
143143
override def isFromWorkspace: Boolean = true
144144
override def completionItemDataKind: Integer = CompletionSource.WorkspaceKind.ordinal
145145

146+
/**
147+
* CompletionValue for old implicit classes methods via SymbolSearch
148+
*/
149+
case class ImplicitClass(
150+
label: String,
151+
symbol: Symbol,
152+
override val snippetSuffix: CompletionSuffix,
153+
override val importSymbol: Symbol,
154+
) extends Symbolic:
155+
override def completionItemKind(using Context): CompletionItemKind =
156+
CompletionItemKind.Method
157+
override def description(printer: ShortenedTypePrinter)(using Context): String =
158+
s"${printer.completionSymbol(symbol)} (implicit)"
159+
146160
/**
147161
* CompletionValue for extension methods via SymbolSearch
148162
*/

presentation-compiler/src/main/dotty/tools/pc/completions/Completions.scala

+26-2
Original file line numberDiff line numberDiff line change
@@ -520,14 +520,38 @@ class Completions(
520520
Some(search.search(query, buildTargetIdentifier, visitor).nn)
521521
case CompletionKind.Members =>
522522
val visitor = new CompilerSearchVisitor(sym =>
523-
if sym.is(ExtensionMethod) &&
523+
def isExtensionMethod = sym.is(ExtensionMethod) &&
524524
qualType.widenDealias <:< sym.extensionParam.info.widenDealias
525-
then
525+
def isImplicitClass(owner: Symbol) =
526+
val constructorParam =
527+
owner.info
528+
.membersBasedOnFlags(
529+
Flags.ParamAccessor,
530+
Flags.EmptyFlags,
531+
)
532+
.headOption
533+
.map(_.info)
534+
owner.isClass && owner.is(Flags.Implicit) &&
535+
constructorParam.exists(p =>
536+
qualType.widenDealias <:< p.widenDealias
537+
)
538+
end isImplicitClass
539+
540+
def isImplicitClassMethod = sym.is(Flags.Method) && !sym.isConstructor &&
541+
isImplicitClass(sym.maybeOwner)
542+
543+
if isExtensionMethod then
526544
completionsWithSuffix(
527545
sym,
528546
sym.decodedName,
529547
CompletionValue.Extension(_, _, _)
530548
).map(visit).forall(_ == true)
549+
else if isImplicitClassMethod then
550+
completionsWithSuffix(
551+
sym,
552+
sym.decodedName,
553+
CompletionValue.ImplicitClass(_, _, _, sym.maybeOwner),
554+
).map(visit).forall(_ == true)
531555
else false,
532556
)
533557
Some(search.searchMethods(query, buildTargetIdentifier, visitor).nn)

presentation-compiler/test/dotty/tools/pc/tests/completion/CompletionExtensionSuite.scala

+169-1
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,20 @@ class CompletionExtensionSuite extends BaseCompletionSuite:
2020
|""".stripMargin
2121
)
2222

23+
@Test def `simple-old-syntax` =
24+
check(
25+
"""|package example
26+
|
27+
|object Test:
28+
| implicit class TestOps(a: Int):
29+
| def testOps(b: Int): String = ???
30+
|
31+
|def main = 100.test@@
32+
|""".stripMargin,
33+
"""|testOps(b: Int): String (implicit)
34+
|""".stripMargin
35+
)
36+
2337
@Test def `simple2` =
2438
check(
2539
"""|package example
@@ -35,6 +49,21 @@ class CompletionExtensionSuite extends BaseCompletionSuite:
3549
filter = _.contains("(extension)")
3650
)
3751

52+
@Test def `simple2-old-syntax` =
53+
check(
54+
"""|package example
55+
|
56+
|object enrichments:
57+
| implicit class TestOps(a: Int):
58+
| def testOps(b: Int): String = ???
59+
|
60+
|def main = 100.t@@
61+
|""".stripMargin,
62+
"""|testOps(b: Int): String (implicit)
63+
|""".stripMargin,
64+
filter = _.contains("(implicit)")
65+
)
66+
3867
@Test def `filter-by-type` =
3968
check(
4069
"""|package example
@@ -52,6 +81,22 @@ class CompletionExtensionSuite extends BaseCompletionSuite:
5281
filter = _.contains("(extension)")
5382
)
5483

84+
@Test def `filter-by-type-old` =
85+
check(
86+
"""|package example
87+
|
88+
|object enrichments:
89+
| implicit class A(num: Int):
90+
| def identity2: Int = num + 1
91+
| implicit class B(str: String):
92+
| def identity: String = str
93+
|
94+
|def main = "foo".iden@@
95+
|""".stripMargin,
96+
"""|identity: String (implicit)
97+
|""".stripMargin // identity2 won't be available
98+
)
99+
55100
@Test def `filter-by-type-subtype` =
56101
check(
57102
"""|package example
@@ -70,6 +115,24 @@ class CompletionExtensionSuite extends BaseCompletionSuite:
70115
filter = _.contains("(extension)")
71116
)
72117

118+
@Test def `filter-by-type-subtype-old` =
119+
check(
120+
"""|package example
121+
|
122+
|class A
123+
|class B extends A
124+
|
125+
|object enrichments:
126+
| implicit class Test(a: A):
127+
| def doSomething: A = a
128+
|
129+
|def main = (new B).do@@
130+
|""".stripMargin,
131+
"""|doSomething: A (implicit)
132+
|""".stripMargin,
133+
filter = _.contains("(implicit)")
134+
)
135+
73136
@Test def `simple-edit` =
74137
checkEdit(
75138
"""|package example
@@ -92,6 +155,28 @@ class CompletionExtensionSuite extends BaseCompletionSuite:
92155
|""".stripMargin
93156
)
94157

158+
@Test def `simple-edit-old` =
159+
checkEdit(
160+
"""|package example
161+
|
162+
|object enrichments:
163+
| implicit class A (num: Int):
164+
| def incr: Int = num + 1
165+
|
166+
|def main = 100.inc@@
167+
|""".stripMargin,
168+
"""|package example
169+
|
170+
|import example.enrichments.A
171+
|
172+
|object enrichments:
173+
| implicit class A (num: Int):
174+
| def incr: Int = num + 1
175+
|
176+
|def main = 100.incr
177+
|""".stripMargin
178+
)
179+
95180
@Test def `simple-edit-suffix` =
96181
checkEdit(
97182
"""|package example
@@ -114,6 +199,28 @@ class CompletionExtensionSuite extends BaseCompletionSuite:
114199
|""".stripMargin
115200
)
116201

202+
@Test def `simple-edit-suffix-old` =
203+
checkEdit(
204+
"""|package example
205+
|
206+
|object enrichments:
207+
| implicit class A (val num: Int):
208+
| def plus(other: Int): Int = num + other
209+
|
210+
|def main = 100.pl@@
211+
|""".stripMargin,
212+
"""|package example
213+
|
214+
|import example.enrichments.A
215+
|
216+
|object enrichments:
217+
| implicit class A (val num: Int):
218+
| def plus(other: Int): Int = num + other
219+
|
220+
|def main = 100.plus($0)
221+
|""".stripMargin
222+
)
223+
117224
@Test def `simple-empty` =
118225
check(
119226
"""|package example
@@ -129,6 +236,21 @@ class CompletionExtensionSuite extends BaseCompletionSuite:
129236
filter = _.contains("(extension)")
130237
)
131238

239+
@Test def `simple-empty-old` =
240+
check(
241+
"""|package example
242+
|
243+
|object enrichments:
244+
| implicit class TestOps(a: Int):
245+
| def testOps(b: Int): String = ???
246+
|
247+
|def main = 100.@@
248+
|""".stripMargin,
249+
"""|testOps(b: Int): String (implicit)
250+
|""".stripMargin,
251+
filter = _.contains("(implicit)")
252+
)
253+
132254
@Test def `directly-in-pkg1` =
133255
check(
134256
"""|
@@ -143,6 +265,20 @@ class CompletionExtensionSuite extends BaseCompletionSuite:
143265
|""".stripMargin
144266
)
145267

268+
@Test def `directly-in-pkg1-old` =
269+
check(
270+
"""|
271+
|package examples:
272+
| implicit class A(num: Int):
273+
| def incr: Int = num + 1
274+
|
275+
|package examples2:
276+
| def main = 100.inc@@
277+
|""".stripMargin,
278+
"""|incr: Int (implicit)
279+
|""".stripMargin
280+
)
281+
146282
@Test def `directly-in-pkg2` =
147283
check(
148284
"""|package example:
@@ -157,6 +293,20 @@ class CompletionExtensionSuite extends BaseCompletionSuite:
157293
|""".stripMargin
158294
)
159295

296+
@Test def `directly-in-pkg2-old` =
297+
check(
298+
"""|package examples:
299+
| object X:
300+
| def fooBar(num: Int) = num + 1
301+
| implicit class A (num: Int) { def incr: Int = num + 1 }
302+
|
303+
|package examples2:
304+
| def main = 100.inc@@
305+
|""".stripMargin,
306+
"""|incr: Int (implicit)
307+
|""".stripMargin
308+
)
309+
160310
@Test def `nested-pkg` =
161311
check(
162312
"""|package a: // some comment
@@ -175,7 +325,25 @@ class CompletionExtensionSuite extends BaseCompletionSuite:
175325
|""".stripMargin
176326
)
177327

178-
@Test def `name-conflict` =
328+
@Test def `nested-pkg-old` =
329+
check(
330+
"""|package aa: // some comment
331+
| package cc:
332+
| implicit class A (num: Int):
333+
| def increment2 = num + 2
334+
| implicit class A (num: Int):
335+
| def increment = num + 1
336+
|
337+
|
338+
|package bb:
339+
| def main: Unit = 123.incre@@
340+
|""".stripMargin,
341+
"""|increment: Int (implicit)
342+
|increment2: Int (implicit)
343+
|""".stripMargin
344+
)
345+
346+
@Test def `name-conflict` =
179347
checkEdit(
180348
"""
181349
|package example

0 commit comments

Comments
 (0)