Skip to content

Commit 7f28de1

Browse files
committed
Stabilise returned completions by improving deduplication + extra completions for constructors (#19976)
This PR doesn't address all issues. In the future we need to get rid of https://github.com/scala/scala3/compare/main...rochala:improved-deduplication-and-constructors-search?expand=1#diff-035851592480495dfdb20da6b615ec7dd77b3db70cda46aba56230d8cd690773R157-R167 as it is not working as intended. The future PR of shortened type printer refactor includes a proper way to split extension params. `CompletionValue.Interpolator` should also be removed, and instead we should return workspace / completions as it is now hard to sort those completions. Next refactor is reusing completion affix for other kinds of completions such as case completions, so prefix / suffix is handled in single place. This PR will unblock fuzzy search in the compiler because of stabilizing returned completions. [Cherry-picked 97313ed][modified]
1 parent cb761cb commit 7f28de1

23 files changed

+1252
-524
lines changed

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

+45-45
Original file line numberDiff line numberDiff line change
@@ -86,26 +86,21 @@ object Completion:
8686
*
8787
* Otherwise, provide no completion suggestion.
8888
*/
89-
def completionMode(path: List[untpd.Tree], pos: SourcePosition): Mode =
90-
91-
val completionSymbolKind: Mode =
92-
path match
93-
case GenericImportSelector(sel) =>
94-
if sel.imported.span.contains(pos.span) then Mode.ImportOrExport // import scala.@@
95-
else if sel.isGiven && sel.bound.span.contains(pos.span) then Mode.ImportOrExport
96-
else Mode.None // import scala.{util => u@@}
97-
case GenericImportOrExport(_) => Mode.ImportOrExport | Mode.Scope // import TrieMa@@
98-
case untpd.Literal(Constants.Constant(_: String)) :: _ => Mode.Term | Mode.Scope // literal completions
99-
case (ref: untpd.RefTree) :: _ =>
100-
val maybeSelectMembers = if ref.isInstanceOf[untpd.Select] then Mode.Member else Mode.Scope
101-
102-
if (ref.name.isTermName) Mode.Term | maybeSelectMembers
103-
else if (ref.name.isTypeName) Mode.Type | maybeSelectMembers
104-
else Mode.None
105-
106-
case _ => Mode.None
107-
108-
completionSymbolKind
89+
def completionMode(path: List[untpd.Tree], pos: SourcePosition): Mode = path match
90+
case GenericImportSelector(sel) =>
91+
if sel.imported.span.contains(pos.span) then Mode.ImportOrExport // import scala.@@
92+
else if sel.isGiven && sel.bound.span.contains(pos.span) then Mode.ImportOrExport
93+
else Mode.None // import scala.{util => u@@}
94+
case GenericImportOrExport(_) => Mode.ImportOrExport | Mode.Scope // import TrieMa@@
95+
case untpd.Literal(Constants.Constant(_: String)) :: _ => Mode.Term | Mode.Scope // literal completions
96+
case (ref: untpd.RefTree) :: _ =>
97+
val maybeSelectMembers = if ref.isInstanceOf[untpd.Select] then Mode.Member else Mode.Scope
98+
99+
if (ref.name.isTermName) Mode.Term | maybeSelectMembers
100+
else if (ref.name.isTypeName) Mode.Type | maybeSelectMembers
101+
else Mode.None
102+
103+
case _ => Mode.None
109104

110105
/** When dealing with <errors> in varios palces we check to see if they are
111106
* due to incomplete backticks. If so, we ensure we get the full prefix
@@ -130,7 +125,7 @@ object Completion:
130125
def completionPrefix(path: List[untpd.Tree], pos: SourcePosition)(using Context): String =
131126
def fallback: Int =
132127
var i = pos.point - 1
133-
while i >= 0 && Chars.isIdentifierPart(pos.source.content()(i)) do i -= 1
128+
while i >= 0 && Character.isUnicodeIdentifierPart(pos.source.content()(i)) do i -= 1
134129
i + 1
135130

136131
path match
@@ -278,6 +273,32 @@ object Completion:
278273
if denot.isType then denot.symbol.showFullName
279274
else denot.info.widenTermRefExpr.show
280275

276+
/** Include in completion sets only symbols that
277+
* 1. is not absent (info is not NoType)
278+
* 2. are not a primary constructor,
279+
* 3. have an existing source symbol,
280+
* 4. are the module class in case of packages,
281+
* 5. are mutable accessors, to exclude setters for `var`,
282+
* 6. symbol is not a package object
283+
* 7. symbol is not an artifact of the compiler
284+
* 8. symbol is not a constructor proxy module when in type completion mode
285+
* 9. have same term/type kind as name prefix given so far
286+
*/
287+
def isValidCompletionSymbol(sym: Symbol, completionMode: Mode)(using Context): Boolean =
288+
sym.exists &&
289+
!sym.isAbsent() &&
290+
!sym.isPrimaryConstructor &&
291+
sym.sourceSymbol.exists &&
292+
(!sym.is(Package) || sym.is(ModuleClass)) &&
293+
!sym.isAllOf(Mutable | Accessor) &&
294+
!sym.isPackageObject &&
295+
!sym.is(Artifact) &&
296+
!(completionMode.is(Mode.Type) && sym.isAllOf(ConstructorProxyModule)) &&
297+
(
298+
(completionMode.is(Mode.Term) && (sym.isTerm || sym.is(ModuleClass))
299+
|| (completionMode.is(Mode.Type) && (sym.isType || sym.isStableMember)))
300+
)
301+
281302
given ScopeOrdering(using Context): Ordering[Seq[SingleDenotation]] with
282303
val order =
283304
List(defn.ScalaPredefModuleClass, defn.ScalaPackageClass, defn.JavaLangPackageClass)
@@ -531,34 +552,13 @@ object Completion:
531552
extMethodsWithAppliedReceiver.groupByName
532553

533554
/** Include in completion sets only symbols that
534-
* 1. start with given name prefix, and
535-
* 2. is not absent (info is not NoType)
536-
* 3. are not a primary constructor,
537-
* 4. have an existing source symbol,
538-
* 5. are the module class in case of packages,
539-
* 6. are mutable accessors, to exclude setters for `var`,
540-
* 7. symbol is not a package object
541-
* 8. symbol is not an artifact of the compiler
542-
* 9. have same term/type kind as name prefix given so far
555+
* 1. match the filter method,
556+
* 2. satisfy [[Completion.isValidCompletionSymbol]]
543557
*/
544558
private def include(denot: SingleDenotation, nameInScope: Name)(using Context): Boolean =
545-
val sym = denot.symbol
546-
547-
548559
nameInScope.startsWith(prefix) &&
549-
sym.exists &&
550560
completionsFilter(NoType, nameInScope) &&
551-
!sym.isAbsent() &&
552-
!sym.isPrimaryConstructor &&
553-
sym.sourceSymbol.exists &&
554-
(!sym.is(Package) || sym.is(ModuleClass)) &&
555-
!sym.isAllOf(Mutable | Accessor) &&
556-
!sym.isPackageObject &&
557-
!sym.is(Artifact) &&
558-
(
559-
(mode.is(Mode.Term) && (sym.isTerm || sym.is(ModuleClass))
560-
|| (mode.is(Mode.Type) && (sym.isType || sym.isStableMember)))
561-
)
561+
isValidCompletionSymbol(denot.symbol, mode)
562562

563563
private def extractRefinements(site: Type)(using Context): Seq[SingleDenotation] =
564564
site match

language-server/test/dotty/tools/languageserver/CompletionTest.scala

+6-23
Original file line numberDiff line numberDiff line change
@@ -954,14 +954,8 @@ class CompletionTest {
954954
.noCompletions()
955955

956956
@Test def i13624_annotType: Unit =
957-
val expected1 = Set(
958-
("MyAnnotation", Class, "MyAnnotation"),
959-
("MyAnnotation", Module, "MyAnnotation"),
960-
)
961-
val expected2 = Set(
962-
("MyAnnotation", Class, "Foo.MyAnnotation"),
963-
("MyAnnotation", Module, "Foo.MyAnnotation"),
964-
)
957+
val expected1 = Set(("MyAnnotation", Class, "MyAnnotation"))
958+
val expected2 = Set(("MyAnnotation", Class, "Foo.MyAnnotation"))
965959
code"""object Foo{
966960
| class MyAnnotation extends annotation.StaticAnnotation
967961
|}
@@ -984,14 +978,8 @@ class CompletionTest {
984978
@Test def i13624_annotation : Unit =
985979
code"""@annotation.implicitNot${m1}
986980
|@annotation.implicitNotFound @mai${m2}"""
987-
.completion(m1,
988-
("implicitNotFound", Class, "scala.annotation.implicitNotFound"),
989-
("implicitNotFound", Module, "scala.annotation.implicitNotFound"),
990-
)
991-
.completion(m2,
992-
("main", Class, "main"),
993-
("main", Module, "main"),
994-
)
981+
.completion(m1, ("implicitNotFound", Class, "scala.annotation.implicitNotFound"))
982+
.completion(m2, ("main", Class, "main"))
995983

996984
@Test def i13623_annotation : Unit =
997985
code"""import annot${m1}"""
@@ -1489,7 +1477,6 @@ class CompletionTest {
14891477
("xDef", Method, "=> Int"),
14901478
("xVal", Field, "Int"),
14911479
("xObject", Module, "Foo.xObject"),
1492-
("xClass", Module, "Foo.xClass"),
14931480
("xClass", Class, "Foo.xClass")))
14941481
}
14951482

@@ -1557,9 +1544,7 @@ class CompletionTest {
15571544
|object T:
15581545
| extension (x: Test.TestSel$m1)
15591546
|"""
1560-
.completion(m1, Set(
1561-
("TestSelect", Module, "Test.TestSelect"), ("TestSelect", Class, "Test.TestSelect")
1562-
))
1547+
.completion(m1, Set(("TestSelect", Class, "Test.TestSelect")))
15631548

15641549
@Test def extensionDefinitionCompletionsSelectNested: Unit =
15651550
code"""|object Test:
@@ -1568,9 +1553,7 @@ class CompletionTest {
15681553
|object T:
15691554
| extension (x: Test.Test2.TestSel$m1)
15701555
|"""
1571-
.completion(m1, Set(
1572-
("TestSelect", Module, "Test.Test2.TestSelect"), ("TestSelect", Class, "Test.Test2.TestSelect")
1573-
))
1556+
.completion(m1, Set(("TestSelect", Class, "Test.Test2.TestSelect")))
15741557

15751558
@Test def extensionDefinitionCompletionsSelectInside: Unit =
15761559
code"""|object Test:

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

+1-1
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import scala.annotation.tailrec
55

66
import dotc.*
77
import ast.*, tpd.*
8-
import core.*, Contexts.*, Decorators.*, Flags.*, Names.*, Symbols.*, Types.*
8+
import core.*, Contexts.*, Flags.*, Names.*, Symbols.*, Types.*
99
import interactive.*
1010
import util.*
1111
import util.SourcePosition

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

-1
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,6 @@ import dotty.tools.dotc.interactive.InteractiveDriver
2424
import dotty.tools.dotc.util.SourceFile
2525
import dotty.tools.dotc.util.SourcePosition
2626
import dotty.tools.dotc.util.Spans.Span
27-
import dotty.tools.pc.IndexedContext
2827

2928
import org.eclipse.lsp4j.InlayHint
3029
import org.eclipse.lsp4j.InlayHintKind

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

-6
Original file line numberDiff line numberDiff line change
@@ -6,20 +6,14 @@ import dotty.tools.dotc.core.Flags
66
import dotty.tools.dotc.core.Symbols.*
77
import dotty.tools.dotc.interactive.Interactive
88
import dotty.tools.dotc.interactive.InteractiveDriver
9-
import dotty.tools.dotc.parsing.Tokens.closingRegionTokens
10-
import dotty.tools.dotc.reporting.ErrorMessageID
11-
import dotty.tools.dotc.reporting.ExpectedTokenButFound
129
import dotty.tools.dotc.util.Signatures
1310
import dotty.tools.dotc.util.SourceFile
14-
import dotty.tools.dotc.util.Spans
15-
import dotty.tools.dotc.util.Spans.Span
1611
import dotty.tools.pc.printer.ShortenedTypePrinter
1712
import dotty.tools.pc.printer.ShortenedTypePrinter.IncludeDefaultParam
1813
import dotty.tools.pc.utils.MtagsEnrichments.*
1914
import org.eclipse.lsp4j as l
2015

2116
import scala.jdk.CollectionConverters.*
22-
import scala.jdk.OptionConverters.*
2317
import scala.meta.internal.metals.ReportContext
2418
import scala.meta.pc.OffsetParams
2519
import scala.meta.pc.SymbolDocumentation
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
package dotty.tools.pc.completions
2+
3+
import org.eclipse.lsp4j.Position
4+
import org.eclipse.lsp4j.Range
5+
6+
/**
7+
* @param suffixes which we should insert
8+
* @param prefixes which we should insert
9+
* @param snippet which suffix should we insert the snippet $0
10+
*/
11+
case class CompletionAffix(
12+
suffixes: Set[Suffix],
13+
prefixes: Set[Prefix],
14+
snippet: Suffix,
15+
currentPrefix: Option[String],
16+
):
17+
def addLabelSnippet = suffixes.exists(_.kind == SuffixKind.Bracket)
18+
def hasSnippet = snippet.kind != SuffixKind.NoSuffix
19+
def chain(copyFn: CompletionAffix => CompletionAffix) = copyFn(this)
20+
def withNewSuffix(kind: Suffix) = this.copy(suffixes = suffixes + kind)
21+
def withNewPrefix(kind: Prefix) = this.copy(prefixes = prefixes + kind)
22+
def withCurrentPrefix(currentPrefix: String) = this.copy(currentPrefix = Some(currentPrefix))
23+
def withNewSuffixSnippet(suffix: Suffix) =
24+
this.copy(suffixes = suffixes + suffix, snippet = suffix)
25+
26+
def nonEmpty: Boolean = suffixes.nonEmpty || prefixes.nonEmpty
27+
28+
def toSuffix: String =
29+
def loop(suffixes: List[SuffixKind]): String =
30+
def cursor = if suffixes.head == snippet.kind then "$0" else ""
31+
suffixes match
32+
case SuffixKind.Brace :: tail => s"($cursor)" + loop(tail)
33+
case SuffixKind.Bracket :: tail => s"[$cursor]" + loop(tail)
34+
case SuffixKind.Template :: tail => s" {$cursor}" + loop(tail)
35+
case _ => ""
36+
loop(suffixes.toList.map(_.kind))
37+
38+
def toSuffixOpt: Option[String] =
39+
val edit = toSuffix
40+
if edit.nonEmpty then Some(edit) else None
41+
42+
43+
given Ordering[Position] = Ordering.by(elem => (elem.getLine, elem.getCharacter))
44+
45+
def toInsertRange: Option[Range] =
46+
import scala.language.unsafeNulls
47+
48+
val ranges = prefixes.collect:
49+
case Affix(_, Some(range)) => range
50+
.toList
51+
for
52+
startPos <- ranges.map(_.getStart).minOption
53+
endPos <- ranges.map(_.getEnd).maxOption
54+
yield Range(startPos, endPos)
55+
56+
private def loopPrefix(prefixes: List[PrefixKind]): String =
57+
prefixes match
58+
case PrefixKind.New :: tail => "new " + loopPrefix(tail)
59+
case _ => ""
60+
61+
/**
62+
* We need to insert previous prefix, but we don't want to display it in the label i.e.
63+
* ```scala
64+
* scala.util.Tr@@
65+
* ````
66+
* should return `new Try[T]: Try[T]`
67+
* but insert `new scala.util.Try`
68+
*
69+
*/
70+
def toInsertPrefix: String =
71+
loopPrefix(prefixes.toList.map(_.kind)) + currentPrefix.getOrElse("")
72+
73+
def toPrefix: String =
74+
loopPrefix(prefixes.toList.map(_.kind))
75+
76+
end CompletionAffix
77+
78+
object CompletionAffix:
79+
val empty = CompletionAffix(
80+
suffixes = Set.empty,
81+
prefixes = Set.empty,
82+
snippet = Affix(SuffixKind.NoSuffix),
83+
currentPrefix = None,
84+
)
85+
86+
enum SuffixKind:
87+
case Brace, Bracket, Template, NoSuffix
88+
89+
enum PrefixKind:
90+
case New
91+
92+
type Suffix = Affix[SuffixKind]
93+
type Prefix = Affix[PrefixKind]
94+
95+
private case class Affix[+T](kind: T, insertRange: Option[Range] = None)

0 commit comments

Comments
 (0)