From 03c78b2c4f0be19e5db3e55618dc96b09ef6bb88 Mon Sep 17 00:00:00 2001 From: rochala Date: Tue, 1 Aug 2023 15:52:10 +0200 Subject: [PATCH 01/11] Add support for completions for extension definition --- .../tools/dotc/interactive/Completion.scala | 22 +++++++++++++++---- .../tools/languageserver/CompletionTest.scala | 9 ++++++++ .../tools/pc/completions/Completions.scala | 2 +- .../pc/tests/completion/CompletionSuite.scala | 20 +++++++++++++++++ 4 files changed, 48 insertions(+), 5 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/interactive/Completion.scala b/compiler/src/dotty/tools/dotc/interactive/Completion.scala index e4d0cce9f6f9..7ee3e48f0b68 100644 --- a/compiler/src/dotty/tools/dotc/interactive/Completion.scala +++ b/compiler/src/dotty/tools/dotc/interactive/Completion.scala @@ -3,6 +3,7 @@ package dotty.tools.dotc.interactive import scala.language.unsafeNulls import dotty.tools.dotc.ast.untpd +import dotty.tools.dotc.ast.NavigateAST import dotty.tools.dotc.config.Printers.interactiv import dotty.tools.dotc.core.Contexts._ import dotty.tools.dotc.core.Decorators._ @@ -129,16 +130,29 @@ object Completion { case _ => 0 } + /** + * Inspect `path` to deterimine whether enclosing tree is a result of tree extension. + * If so, completion should use untyped path containing tree before extension to get proper results. + */ + def pathBeforeDesugaring(path: List[Tree], pos: SourcePosition)(using Context): List[Tree] = + val hasUntypedTree = path.headOption.forall(NavigateAST.untypedPath(_, exactMatch = true).nonEmpty) + if hasUntypedTree then + path + else + NavigateAST.untypedPath(pos.span).collect: + case tree: untpd.Tree => tree + private def computeCompletions(pos: SourcePosition, path: List[Tree])(using Context): (Int, List[Completion]) = { - val mode = completionMode(path, pos) - val rawPrefix = completionPrefix(path, pos) + val path0 = pathBeforeDesugaring(path, pos) + val mode = completionMode(path0, pos) + val rawPrefix = completionPrefix(path0, pos) val hasBackTick = rawPrefix.headOption.contains('`') val prefix = if hasBackTick then rawPrefix.drop(1) else rawPrefix val completer = new Completer(mode, prefix, pos) - val completions = path match { + val completions = path0 match { // Ignore synthetic select from `This` because in code it was `Ident` // See example in dotty.tools.languageserver.CompletionTest.syntheticThis case Select(qual @ This(_), _) :: _ if qual.span.isSynthetic => completer.scopeCompletions @@ -153,7 +167,7 @@ object Completion { val backtickedCompletions = describedCompletions.map(completion => backtickCompletions(completion, hasBackTick)) - val offset = completionOffset(path) + val offset = completionOffset(path0) interactiv.println(i"""completion with pos = $pos, | prefix = ${completer.prefix}, diff --git a/language-server/test/dotty/tools/languageserver/CompletionTest.scala b/language-server/test/dotty/tools/languageserver/CompletionTest.scala index 4dd9c276b8b4..85c4fc63ff6f 100644 --- a/language-server/test/dotty/tools/languageserver/CompletionTest.scala +++ b/language-server/test/dotty/tools/languageserver/CompletionTest.scala @@ -1528,4 +1528,13 @@ class CompletionTest { ) ) } + + @Test def desugaredErrorStatement: Unit = + code"""|trait Foo + |object T: + | extension (x: Fo$m1) + |""" + .completion(m1, Set( + ("Foo",Class,"Foo") + )) } diff --git a/presentation-compiler/src/main/dotty/tools/pc/completions/Completions.scala b/presentation-compiler/src/main/dotty/tools/pc/completions/Completions.scala index 0574aa2cdb7d..6de37e9707a4 100644 --- a/presentation-compiler/src/main/dotty/tools/pc/completions/Completions.scala +++ b/presentation-compiler/src/main/dotty/tools/pc/completions/Completions.scala @@ -125,7 +125,7 @@ class Completions( end CursorPos private lazy val cursorPos = - calculateTypeInstanceAndNewPositions(path) + calculateTypeInstanceAndNewPositions(Completion.pathBeforeDesugaring(path, pos)) private def calculateTypeInstanceAndNewPositions( path: List[Tree] diff --git a/presentation-compiler/test/dotty/tools/pc/tests/completion/CompletionSuite.scala b/presentation-compiler/test/dotty/tools/pc/tests/completion/CompletionSuite.scala index 313013c34de1..862773abc7ed 100644 --- a/presentation-compiler/test/dotty/tools/pc/tests/completion/CompletionSuite.scala +++ b/presentation-compiler/test/dotty/tools/pc/tests/completion/CompletionSuite.scala @@ -1326,6 +1326,26 @@ class CompletionSuite extends BaseCompletionSuite: """|AClass[A <: Int] test.O |AClass test.O |AbstractTypeClassManifest - scala.reflect.ClassManifestFactory + """.stripMargin + ) + + @Test def `extension-definition-scope` = + check( + """|object T: + | extension (x: ListBuffe@@) + |""".stripMargin, + """|ListBuffer[T] - scala.collection.mutable + |ListBuffer - scala.collection.mutable + |""".stripMargin, + ) + + @Test def `extension-definition-symbol-search` = + check( + """|trait Foo + |object T: + | extension (x: Fo@@) + |""".stripMargin, + """|Foo test |""".stripMargin ) From 13aa7899e5cd95ee705607d6eb5b20d7fd473d6f Mon Sep 17 00:00:00 2001 From: rochala Date: Wed, 2 Aug 2023 16:57:54 +0200 Subject: [PATCH 02/11] refactor cursorPos to reuse compiler completion mode --- .../tools/languageserver/CompletionTest.scala | 2 +- .../tools/pc/completions/Completions.scala | 167 ++++++------------ .../pc/tests/completion/CompletionSuite.scala | 12 +- .../completion/CompletionWorkspaceSuite.scala | 6 +- 4 files changed, 60 insertions(+), 127 deletions(-) diff --git a/language-server/test/dotty/tools/languageserver/CompletionTest.scala b/language-server/test/dotty/tools/languageserver/CompletionTest.scala index 85c4fc63ff6f..ced7f19749de 100644 --- a/language-server/test/dotty/tools/languageserver/CompletionTest.scala +++ b/language-server/test/dotty/tools/languageserver/CompletionTest.scala @@ -1529,7 +1529,7 @@ class CompletionTest { ) } - @Test def desugaredErrorStatement: Unit = + @Test def extensionDefinitionCompletions: Unit = code"""|trait Foo |object T: | extension (x: Fo$m1) diff --git a/presentation-compiler/src/main/dotty/tools/pc/completions/Completions.scala b/presentation-compiler/src/main/dotty/tools/pc/completions/Completions.scala index 6de37e9707a4..1212b98a2289 100644 --- a/presentation-compiler/src/main/dotty/tools/pc/completions/Completions.scala +++ b/presentation-compiler/src/main/dotty/tools/pc/completions/Completions.scala @@ -12,6 +12,7 @@ import scala.meta.internal.pc.{IdentifierComparator, MemberOrdering} import scala.meta.pc.* import dotty.tools.dotc.ast.tpd.* +import dotty.tools.dotc.ast.NavigateAST import dotty.tools.dotc.core.Comments.Comment import dotty.tools.dotc.core.Constants.Constant import dotty.tools.dotc.core.Contexts.* @@ -24,6 +25,7 @@ import dotty.tools.dotc.core.StdNames.* import dotty.tools.dotc.core.Symbols.* import dotty.tools.dotc.core.Types.* import dotty.tools.dotc.interactive.Completion +import dotty.tools.dotc.interactive.Completion.Mode import dotty.tools.dotc.transform.SymUtils.* import dotty.tools.dotc.util.SourcePosition import dotty.tools.dotc.util.Spans @@ -54,6 +56,14 @@ class Completions( val coursierComplete = new CoursierComplete(BuildInfo.scalaVersion) + private lazy val completionMode = + val adjustedPath = Completion.pathBeforeDesugaring(path, pos) + val mode = Completion.completionMode(adjustedPath, pos) + path match + case Literal(Constant(_: String)) :: _ => Mode.Term // literal completions + case _ => mode + + private lazy val shouldAddSnippet = path match /* In case of `method@@()` we should not add snippets and the path @@ -69,113 +79,41 @@ class Completions( case (_: Ident) :: (_: SeqLiteral) :: _ => false case _ => true - enum CursorPos: - case Type(hasTypeParams: Boolean, hasNewKw: Boolean) - case Term - case Import - - def include(sym: Symbol)(using Context): Boolean = - def hasSyntheticCursorSuffix: Boolean = - if !sym.name.endsWith(Cursor.value) then false - else - val realNameLength = sym.decodedName.length - Cursor.value.length - sym.source == pos.source && - sym.span.start + realNameLength == pos.span.end - - val generalExclude = - isUninterestingSymbol(sym) || - !isNotLocalForwardReference(sym) || - sym.isPackageObject || - hasSyntheticCursorSuffix - - def isWildcardParam(sym: Symbol) = - if sym.isTerm && sym.owner.isAnonymousFunction then - sym.name match - case DerivedName(under, _) => - under.isEmpty - case _ => false - else false + private lazy val allowTemplateSuffix: Boolean = + path match + case _ :: New(selectOrIdent: (Select | Ident)) :: _ => true + case _ => false - if generalExclude then false + def includeSymbol(sym: Symbol)(using Context): Boolean = + def hasSyntheticCursorSuffix: Boolean = + if !sym.name.endsWith(Cursor.value) then false else - this match - case Type(_, _) => true - case Term if isWildcardParam(sym) => false - case Term if sym.isTerm || sym.is(Package) => true - case Import => true + val realNameLength = sym.decodedName.length - Cursor.value.length + sym.source == pos.source && + sym.span.start + realNameLength == pos.span.end + + val generalExclude = + isUninterestingSymbol(sym) || + !isNotLocalForwardReference(sym) || + sym.isPackageObject || + hasSyntheticCursorSuffix + + def isWildcardParam(sym: Symbol) = + if sym.isTerm && sym.owner.isAnonymousFunction then + sym.name match + case DerivedName(under, _) => + under.isEmpty case _ => false - end if - end include - - def allowBracketSuffix: Boolean = - this match - case Type(hasTypeParams, _) => !hasTypeParams - case _ => false - - def allowTemplateSuffix: Boolean = - this match - case Type(_, hasNewKw) => hasNewKw - case _ => false + else false - def allowApplicationSuffix: Boolean = - this match - case Term => true - case _ => false + if generalExclude then false + else if completionMode.is(Mode.ImportOrExport) then true + else if completionMode.is(Mode.Term) && isWildcardParam(sym) then false + else if completionMode.is(Mode.Term) && (sym.isTerm || sym.is(Package)) then true + else !completionMode.is(Mode.Term) + end if + end includeSymbol - end CursorPos - - private lazy val cursorPos = - calculateTypeInstanceAndNewPositions(Completion.pathBeforeDesugaring(path, pos)) - - private def calculateTypeInstanceAndNewPositions( - path: List[Tree] - ): CursorPos = - path match - case (_: Import) :: _ => CursorPos.Import - case _ :: (_: Import) :: _ => CursorPos.Import - case (head: (Select | Ident)) :: tail => - // https://github.com/lampepfl/dotty/issues/15750 - // due to this issue in dotty, because of which trees after typer lose information, - // we have to calculate hasNoSquareBracket manually: - val hasSquareBracket = - val span: Span = head.srcPos.span - if span.exists then - var i = span.end - while i < (text.length() - 1) && text(i).isWhitespace do i = i + 1 - - if i < text.length() then text(i) == '[' - else false - else false - - def typePos = CursorPos.Type(hasSquareBracket, hasNewKw = false) - def newTypePos = - CursorPos.Type(hasSquareBracket, hasNewKw = true) - - tail match - case (v: ValOrDefDef) :: _ if v.tpt.sourcePos.contains(pos) => - typePos - case New(selectOrIdent: (Select | Ident)) :: _ - if selectOrIdent.sourcePos.contains(pos) => - newTypePos - case (a @ AppliedTypeTree(_, args)) :: _ - if args.exists(_.sourcePos.contains(pos)) => - typePos - case (templ @ Template(constr, _, self, _)) :: _ - if (constr :: self :: templ.parents).exists( - _.sourcePos.contains(pos) - ) => - typePos - case _ => - CursorPos.Term - end match - - case (_: TypeTree) :: TypeApply(Select(newQualifier: New, _), _) :: _ - if newQualifier.sourcePos.contains(pos) => - CursorPos.Type(hasTypeParams = false, hasNewKw = true) - - case _ => CursorPos.Term - end match - end calculateTypeInstanceAndNewPositions def completions(): (List[CompletionValue], SymbolSearch.Result) = val (advanced, exclusive) = advancedCompletions(path, pos, completionPos) @@ -206,7 +144,7 @@ class Completions( end match val application = CompletionApplication.fromPath(path) - val ordering = completionOrdering(application, cursorPos) + val ordering = completionOrdering(application) val values = application.postProcess(all.sorted(ordering)) (values, result) end completions @@ -256,8 +194,7 @@ class Completions( private def findSuffix(symbol: Symbol): CompletionSuffix = CompletionSuffix.empty .chain { suffix => // for [] suffix - if shouldAddSnippet && - cursorPos.allowBracketSuffix && symbol.info.typeParams.nonEmpty + if shouldAddSnippet && symbol.info.typeParams.nonEmpty then suffix.withNewSuffixSnippet(SuffixKind.Bracket) else suffix } @@ -285,7 +222,7 @@ class Completions( else suffix } .chain { suffix => // for {} suffix - if shouldAddSnippet && cursorPos.allowTemplateSuffix + if shouldAddSnippet && allowTemplateSuffix && isAbstractType(symbol) then if suffix.hasSnippet then suffix.withNewSuffix(SuffixKind.Template) @@ -307,7 +244,7 @@ class Completions( val methodSymbols = if shouldAddSnippet && (sym.is(Flags.Module) || sym.isClass && !sym.is(Flags.Trait)) && - !sym.is(Flags.JavaDefined) && cursorPos.allowApplicationSuffix + !sym.is(Flags.JavaDefined) && completionMode.is(Mode.Term) then val info = /* Companion will be added even for normal classes now, @@ -635,7 +572,7 @@ class Completions( val suffix = if symOnly.snippetSuffix.addLabelSnippet then "[]" else "" val id = nameId + suffix - val include = cursorPos.include(sym) + val include = includeSymbol(sym) (id, include) case kw: CompletionValue.Keyword => (kw.label, true) case mc: CompletionValue.MatchCompletion => (mc.label, true) @@ -695,7 +632,6 @@ class Completions( private def computeRelevancePenalty( completion: CompletionValue, application: CompletionApplication, - cursorPos: CursorPos, ): Int = import scala.meta.internal.pc.MemberOrdering.* @@ -741,10 +677,8 @@ class Completions( relevance |= IsSynthetic if sym.isDeprecated then relevance |= IsDeprecated if isEvilMethod(sym.name) then relevance |= IsEvilMethod - cursorPos match - case CursorPos.Type(_, _) if !sym.isType => - relevance |= IsNotTypeInTypePos - case _ => + if !completionMode.is(Mode.ImportOrExport) && + completionMode.is(Mode.Type) && !sym.isType then relevance |= IsNotTypeInTypePos relevance end symbolRelevance @@ -822,8 +756,7 @@ class Completions( end CompletionApplication private def completionOrdering( - application: CompletionApplication, - cursorPos: CursorPos, + application: CompletionApplication ): Ordering[CompletionValue] = new Ordering[CompletionValue]: val queryLower = completionPos.query.toLowerCase() @@ -838,8 +771,8 @@ class Completions( def compareByRelevance(o1: CompletionValue, o2: CompletionValue): Int = Integer.compare( - computeRelevancePenalty(o1, application, cursorPos), - computeRelevancePenalty(o2, application, cursorPos), + computeRelevancePenalty(o1, application), + computeRelevancePenalty(o2, application), ) def fuzzyScore(o: CompletionValue.Symbolic): Int = diff --git a/presentation-compiler/test/dotty/tools/pc/tests/completion/CompletionSuite.scala b/presentation-compiler/test/dotty/tools/pc/tests/completion/CompletionSuite.scala index 862773abc7ed..7ab759c55dca 100644 --- a/presentation-compiler/test/dotty/tools/pc/tests/completion/CompletionSuite.scala +++ b/presentation-compiler/test/dotty/tools/pc/tests/completion/CompletionSuite.scala @@ -673,8 +673,8 @@ class CompletionSuite extends BaseCompletionSuite: |} |""".stripMargin, """|Some[?] scala - |Seq scala.collection.immutable - |Set scala.collection.immutable + |SafeVarargs java.lang + |ScalaReflectionException scala |""".stripMargin, topLines = Some(3) ) @@ -709,8 +709,8 @@ class CompletionSuite extends BaseCompletionSuite: |} |""".stripMargin, """|Number: Regex - |Nil scala.collection.immutable - |NoManifest scala.reflect + |NegativeArraySizeException java.lang + |NoClassDefFoundError java.lang |""".stripMargin, topLines = Option(3) ) @@ -724,8 +724,8 @@ class CompletionSuite extends BaseCompletionSuite: |} |""".stripMargin, """|Number: Regex - |Nil scala.collection.immutable - |NoManifest scala.reflect + |NegativeArraySizeException java.lang + |NoClassDefFoundError java.lang |""".stripMargin, topLines = Option(3) ) diff --git a/presentation-compiler/test/dotty/tools/pc/tests/completion/CompletionWorkspaceSuite.scala b/presentation-compiler/test/dotty/tools/pc/tests/completion/CompletionWorkspaceSuite.scala index 500b083137e1..c51b66bd5f2e 100644 --- a/presentation-compiler/test/dotty/tools/pc/tests/completion/CompletionWorkspaceSuite.scala +++ b/presentation-compiler/test/dotty/tools/pc/tests/completion/CompletionWorkspaceSuite.scala @@ -293,7 +293,6 @@ class CompletionWorkspaceSuite extends BaseCompletionSuite: |""".stripMargin ) - // Ignore for Scala 3, since we don't provide completions for null @Test def `match-typed` = checkEdit( """|object Main { @@ -305,11 +304,12 @@ class CompletionWorkspaceSuite extends BaseCompletionSuite: """|import java.util.ArrayDeque |object Main { | def foo(): Unit = null match { - | case x: ArrayDeque => + | case x: ArrayDeque[$0] => | } |} |""".stripMargin, - filter = _.contains("java.util") + filter = _.contains("java.util"), + assertSingleItem = false, ) @Test def `type` = From 247af6d3e663c63b828173bb3df248f090e0c137 Mon Sep 17 00:00:00 2001 From: rochala Date: Thu, 3 Aug 2023 14:54:01 +0200 Subject: [PATCH 03/11] Set untpdTree in repl compilation unit for completions --- .../tools/dotc/interactive/Completion.scala | 8 +- .../src/dotty/tools/repl/ReplCompiler.scala | 35 ++++-- .../src/dotty/tools/repl/ReplDriver.scala | 5 +- .../dotty/tools/repl/TabcompleteTests.scala | 5 + .../pc/tests/completion/CompletionSuite.scala | 105 +++++++++++++++++- 5 files changed, 138 insertions(+), 20 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/interactive/Completion.scala b/compiler/src/dotty/tools/dotc/interactive/Completion.scala index 7ee3e48f0b68..0b9265ae2635 100644 --- a/compiler/src/dotty/tools/dotc/interactive/Completion.scala +++ b/compiler/src/dotty/tools/dotc/interactive/Completion.scala @@ -136,11 +136,9 @@ object Completion { */ def pathBeforeDesugaring(path: List[Tree], pos: SourcePosition)(using Context): List[Tree] = val hasUntypedTree = path.headOption.forall(NavigateAST.untypedPath(_, exactMatch = true).nonEmpty) - if hasUntypedTree then - path - else - NavigateAST.untypedPath(pos.span).collect: - case tree: untpd.Tree => tree + if hasUntypedTree then path + else NavigateAST.untypedPath(pos.span).collect: + case tree: untpd.Tree => tree private def computeCompletions(pos: SourcePosition, path: List[Tree])(using Context): (Int, List[Completion]) = { val path0 = pathBeforeDesugaring(path, pos) diff --git a/compiler/src/dotty/tools/repl/ReplCompiler.scala b/compiler/src/dotty/tools/repl/ReplCompiler.scala index 764695e8479b..d3a5561b6080 100644 --- a/compiler/src/dotty/tools/repl/ReplCompiler.scala +++ b/compiler/src/dotty/tools/repl/ReplCompiler.scala @@ -93,9 +93,9 @@ class ReplCompiler extends Compiler: end compile final def typeOf(expr: String)(using state: State): Result[String] = - typeCheck(expr).map { tree => + typeCheck(expr).map { (_, tpdTree) => given Context = state.context - tree.rhs match { + tpdTree.rhs match { case Block(xs, _) => xs.last.tpe.widen.show case _ => """Couldn't compute the type of your expression, so sorry :( @@ -129,7 +129,7 @@ class ReplCompiler extends Compiler: Iterator(sym) ++ sym.allOverriddenSymbols } - typeCheck(expr).map { + typeCheck(expr).map { (_, tpdTree) => tpdTree match case ValDef(_, _, Block(stats, _)) if stats.nonEmpty => val stat = stats.last.asInstanceOf[tpd.Tree] if (stat.tpe.isError) stat.tpe.show @@ -152,7 +152,7 @@ class ReplCompiler extends Compiler: } } - final def typeCheck(expr: String, errorsAllowed: Boolean = false)(using state: State): Result[tpd.ValDef] = { + final def typeCheck(expr: String, errorsAllowed: Boolean = false)(using state: State): Result[(untpd.ValDef, tpd.ValDef)] = { def wrapped(expr: String, sourceFile: SourceFile, state: State)(using Context): Result[untpd.PackageDef] = { def wrap(trees: List[untpd.Tree]): untpd.PackageDef = { @@ -181,22 +181,32 @@ class ReplCompiler extends Compiler: } } - def unwrapped(tree: tpd.Tree, sourceFile: SourceFile)(using Context): Result[tpd.ValDef] = { - def error: Result[tpd.ValDef] = - List(new Diagnostic.Error(s"Invalid scala expression", - sourceFile.atSpan(Span(0, sourceFile.content.length)))).errors + def error[Tree <: untpd.Tree](sourceFile: SourceFile): Result[Tree] = + List(new Diagnostic.Error(s"Invalid scala expression", + sourceFile.atSpan(Span(0, sourceFile.content.length)))).errors + def unwrappedTypeTree(tree: tpd.Tree, sourceFile0: SourceFile)(using Context): Result[tpd.ValDef] = { import tpd._ tree match { case PackageDef(_, List(TypeDef(_, tmpl: Template))) => tmpl.body .collectFirst { case dd: ValDef if dd.name.show == "expr" => dd.result } - .getOrElse(error) + .getOrElse(error[tpd.ValDef](sourceFile0)) case _ => - error + error[tpd.ValDef](sourceFile0) } } + def unwrappedUntypedTree(tree: untpd.Tree, sourceFile0: SourceFile)(using Context): Result[untpd.ValDef] = + import untpd._ + tree match { + case PackageDef(_, List(TypeDef(_, tmpl: Template))) => + tmpl.body + .collectFirst { case dd: ValDef if dd.name.show == "expr" => dd.result } + .getOrElse(error[untpd.ValDef](sourceFile0)) + case _ => + error[untpd.ValDef](sourceFile0) + } val src = SourceFile.virtual("", expr) inContext(state.context.fresh @@ -209,7 +219,10 @@ class ReplCompiler extends Compiler: ctx.run.nn.compileUnits(unit :: Nil, ctx) if (errorsAllowed || !ctx.reporter.hasErrors) - unwrapped(unit.tpdTree, src) + for + tpdTree <- unwrappedTypeTree(unit.tpdTree, src) + untpdTree <- unwrappedUntypedTree(unit.untpdTree, src) + yield untpdTree -> tpdTree else ctx.reporter.removeBufferedMessages.errors } diff --git a/compiler/src/dotty/tools/repl/ReplDriver.scala b/compiler/src/dotty/tools/repl/ReplDriver.scala index 905f4f06de08..2471f6bece42 100644 --- a/compiler/src/dotty/tools/repl/ReplDriver.scala +++ b/compiler/src/dotty/tools/repl/ReplDriver.scala @@ -251,10 +251,11 @@ class ReplDriver(settings: Array[String], given state: State = newRun(state0) compiler .typeCheck(expr, errorsAllowed = true) - .map { tree => + .map { (untpdTree, tpdTree) => val file = SourceFile.virtual("", expr, maybeIncomplete = true) val unit = CompilationUnit(file)(using state.context) - unit.tpdTree = tree + unit.untpdTree = untpdTree + unit.tpdTree = tpdTree given Context = state.context.fresh.setCompilationUnit(unit) val srcPos = SourcePosition(file, Span(cursor)) val completions = try Completion.completions(srcPos)._2 catch case NonFatal(_) => Nil diff --git a/compiler/test/dotty/tools/repl/TabcompleteTests.scala b/compiler/test/dotty/tools/repl/TabcompleteTests.scala index 910584a9b5e7..0bce525e1469 100644 --- a/compiler/test/dotty/tools/repl/TabcompleteTests.scala +++ b/compiler/test/dotty/tools/repl/TabcompleteTests.scala @@ -32,6 +32,11 @@ class TabcompleteTests extends ReplTest { assertEquals(List("apply"), comp) } + @Test def tabCompleteInExtensionDefinition = initially { + val comp = tabComplete("extension (x: Lis") + assertEquals(List("List"), comp) + } + @Test def tabCompleteTwiceIn = { val src1 = "class Foo { def bar(xs: List[Int]) = xs.map" val src2 = "class Foo { def bar(xs: List[Int]) = xs.mapC" diff --git a/presentation-compiler/test/dotty/tools/pc/tests/completion/CompletionSuite.scala b/presentation-compiler/test/dotty/tools/pc/tests/completion/CompletionSuite.scala index 7ab759c55dca..110c2c6eccb7 100644 --- a/presentation-compiler/test/dotty/tools/pc/tests/completion/CompletionSuite.scala +++ b/presentation-compiler/test/dotty/tools/pc/tests/completion/CompletionSuite.scala @@ -1330,6 +1330,16 @@ class CompletionSuite extends BaseCompletionSuite: ) @Test def `extension-definition-scope` = + check( + """|trait Foo + |object T: + | extension (x: Fo@@) + |""".stripMargin, + """|Foo test + |""".stripMargin + ) + + @Test def `extension-definition-symbol-search` = check( """|object T: | extension (x: ListBuffe@@) @@ -1339,11 +1349,102 @@ class CompletionSuite extends BaseCompletionSuite: |""".stripMargin, ) - @Test def `extension-definition-symbol-search` = + @Test def `extension-definition-type-parameter` = check( """|trait Foo |object T: - | extension (x: Fo@@) + | extension [A <: Fo@@] + |""".stripMargin, + """|Foo test + |""".stripMargin + ) + + @Test def `extension-definition-type-parameter-symbol-search` = + check( + """|object T: + | extension [A <: ListBuffe@@] + |""".stripMargin, + """|ListBuffer[T] - scala.collection.mutable + |ListBuffer - scala.collection.mutable + |""".stripMargin + ) + + @Test def `extension-definition-using-param-clause` = + check( + """|trait Foo + |object T: + | extension (using Fo@@) + |""".stripMargin, + """|Foo test + |""".stripMargin + ) + + + @Test def `extension-definition-mix-1` = + check( + """|trait Foo + |object T: + | extension (x: Int)(using Fo@@) + |""".stripMargin, + """|Foo test + |""".stripMargin + ) + + @Test def `extension-definition-mix-2` = + check( + """|trait Foo + |object T: + | extension (using Fo@@)(x: Int)(using Foo) + |""".stripMargin, + """|Foo test + |""".stripMargin + ) + + @Test def `extension-definition-mix-3` = + check( + """|trait Foo + |object T: + | extension (using Foo)(x: Int)(using Fo@@) + |""".stripMargin, + """|Foo test + |""".stripMargin + ) + + @Test def `extension-definition-mix-4` = + check( + """|trait Foo + |object T: + | extension [A](x: Fo@@) + |""".stripMargin, + """|Foo test + |""".stripMargin + ) + + @Test def `extension-definition-mix-5` = + check( + """|trait Foo + |object T: + | extension [A](using Fo@@)(x: Int) + |""".stripMargin, + """|Foo test + |""".stripMargin + ) + + @Test def `extension-definition-mix-6` = + check( + """|trait Foo + |object T: + | extension [A](using Foo)(x: Fo@@) + |""".stripMargin, + """|Foo test + |""".stripMargin + ) + + @Test def `extension-definition-mix-7` = + check( + """|trait Foo + |object T: + | extension [A](using Foo)(x: Fo@@)(using Fo@@) |""".stripMargin, """|Foo test |""".stripMargin From fcde5478f766f45764f51fe30299d97e0125b3f5 Mon Sep 17 00:00:00 2001 From: rochala Date: Thu, 24 Aug 2023 18:08:53 +0200 Subject: [PATCH 04/11] remove duplicated code --- .../tools/pc/completions/Completions.scala | 7 ++--- .../pc/printer/ShortenedTypePrinter.scala | 15 ++++++++--- .../pc/tests/completion/CompletionSuite.scala | 26 ++++++++++++++----- 3 files changed, 32 insertions(+), 16 deletions(-) diff --git a/presentation-compiler/src/main/dotty/tools/pc/completions/Completions.scala b/presentation-compiler/src/main/dotty/tools/pc/completions/Completions.scala index 1212b98a2289..a55f19e2ab68 100644 --- a/presentation-compiler/src/main/dotty/tools/pc/completions/Completions.scala +++ b/presentation-compiler/src/main/dotty/tools/pc/completions/Completions.scala @@ -63,7 +63,6 @@ class Completions( case Literal(Constant(_: String)) :: _ => Mode.Term // literal completions case _ => mode - private lazy val shouldAddSnippet = path match /* In case of `method@@()` we should not add snippets and the path @@ -114,7 +113,6 @@ class Completions( end if end includeSymbol - def completions(): (List[CompletionValue], SymbolSearch.Result) = val (advanced, exclusive) = advancedCompletions(path, pos, completionPos) val (all, result) = @@ -242,9 +240,8 @@ class Completions( def companionSynthetic = sym.companion.exists && sym.companion.is(Synthetic) // find the apply completion that would need a snippet val methodSymbols = - if shouldAddSnippet && - (sym.is(Flags.Module) || sym.isClass && !sym.is(Flags.Trait)) && - !sym.is(Flags.JavaDefined) && completionMode.is(Mode.Term) + if shouldAddSnippet && completionMode.is(Mode.Term) && + (sym.is(Flags.Module) || sym.isClass && !sym.is(Flags.Trait)) && !sym.is(Flags.JavaDefined) then val info = /* Companion will be added even for normal classes now, diff --git a/presentation-compiler/src/main/dotty/tools/pc/printer/ShortenedTypePrinter.scala b/presentation-compiler/src/main/dotty/tools/pc/printer/ShortenedTypePrinter.scala index 088ecd6c3a0c..5652fd0d9bcc 100644 --- a/presentation-compiler/src/main/dotty/tools/pc/printer/ShortenedTypePrinter.scala +++ b/presentation-compiler/src/main/dotty/tools/pc/printer/ShortenedTypePrinter.scala @@ -234,18 +234,25 @@ class ShortenedTypePrinter( end match end hoverSymbol + def isImportedByDefault(sym: Symbol): Boolean = + import dotty.tools.dotc.core.Symbols.defn + lazy val effectiveOwner = sym.effectiveOwner + sym.isType && (effectiveOwner == defn.ScalaPackageClass || effectiveOwner == defn.ScalaPredefModuleClass) + def completionSymbol(sym: Symbol): String = val info = sym.info.widenTermRefExpr val typeSymbol = info.typeSymbol - if sym.is(Flags.Package) || sym.isClass then " " + fullNameString(sym.effectiveOwner) - else if sym.is(Flags.Module) || typeSymbol.is(Flags.Module) then + lazy val typeEffectiveOwner = if typeSymbol != NoSymbol then " " + fullNameString(typeSymbol.effectiveOwner) else " " + fullNameString(sym.effectiveOwner) + + if isImportedByDefault(sym) then typeEffectiveOwner + else if sym.is(Flags.Package) || sym.isClass then " " + fullNameString(sym.effectiveOwner) + else if sym.is(Flags.Module) || typeSymbol.is(Flags.Module) then typeEffectiveOwner else if sym.is(Flags.Method) then defaultMethodSignature(sym, info, onlyMethodParams = true) - else if sym.isType - then + else if sym.isType then info match case TypeAlias(t) => " = " + tpe(t.resultType) case t => tpe(t.resultType) diff --git a/presentation-compiler/test/dotty/tools/pc/tests/completion/CompletionSuite.scala b/presentation-compiler/test/dotty/tools/pc/tests/completion/CompletionSuite.scala index 110c2c6eccb7..64cb5e6d3adb 100644 --- a/presentation-compiler/test/dotty/tools/pc/tests/completion/CompletionSuite.scala +++ b/presentation-compiler/test/dotty/tools/pc/tests/completion/CompletionSuite.scala @@ -673,8 +673,8 @@ class CompletionSuite extends BaseCompletionSuite: |} |""".stripMargin, """|Some[?] scala - |SafeVarargs java.lang |ScalaReflectionException scala + |Seq[A] scala.collection.immutable |""".stripMargin, topLines = Some(3) ) @@ -709,8 +709,8 @@ class CompletionSuite extends BaseCompletionSuite: |} |""".stripMargin, """|Number: Regex - |NegativeArraySizeException java.lang - |NoClassDefFoundError java.lang + |NoSuchElementException java.util + |NoSuchFieldError java.lang |""".stripMargin, topLines = Option(3) ) @@ -724,12 +724,24 @@ class CompletionSuite extends BaseCompletionSuite: |} |""".stripMargin, """|Number: Regex - |NegativeArraySizeException java.lang - |NoClassDefFoundError java.lang + |NoSuchElementException java.util + |NoSuchFieldError java.lang |""".stripMargin, topLines = Option(3) ) + @Test def `no-methods-on-case-type` = + check( + s"""|object Main { + | val Number = "".r + | "" match { + | case _: NotImpl@@ + |} + |""".stripMargin, + """|NotImplementedError scala + |""".stripMargin, + ) + @Test def underscore = check( s"""|object Main { @@ -1344,7 +1356,7 @@ class CompletionSuite extends BaseCompletionSuite: """|object T: | extension (x: ListBuffe@@) |""".stripMargin, - """|ListBuffer[T] - scala.collection.mutable + """|ListBuffer[A] - scala.collection.mutable |ListBuffer - scala.collection.mutable |""".stripMargin, ) @@ -1364,7 +1376,7 @@ class CompletionSuite extends BaseCompletionSuite: """|object T: | extension [A <: ListBuffe@@] |""".stripMargin, - """|ListBuffer[T] - scala.collection.mutable + """|ListBuffer[A] - scala.collection.mutable |ListBuffer - scala.collection.mutable |""".stripMargin ) From 40855741c8ef02f5663e513d49f127f8698765de Mon Sep 17 00:00:00 2001 From: rochala Date: Thu, 24 Aug 2023 20:15:58 +0200 Subject: [PATCH 05/11] make tests less jvm dependent --- .../pc/tests/completion/CompletionSuite.scala | 30 ++++--------------- 1 file changed, 5 insertions(+), 25 deletions(-) diff --git a/presentation-compiler/test/dotty/tools/pc/tests/completion/CompletionSuite.scala b/presentation-compiler/test/dotty/tools/pc/tests/completion/CompletionSuite.scala index 64cb5e6d3adb..4c1254a063a4 100644 --- a/presentation-compiler/test/dotty/tools/pc/tests/completion/CompletionSuite.scala +++ b/presentation-compiler/test/dotty/tools/pc/tests/completion/CompletionSuite.scala @@ -669,14 +669,12 @@ class CompletionSuite extends BaseCompletionSuite: check( s"""|object Main { | Option(1) match { - | case _: S@@ + | case _: Som@@ |} |""".stripMargin, """|Some[?] scala - |ScalaReflectionException scala - |Seq[A] scala.collection.immutable |""".stripMargin, - topLines = Some(3) + topLines = Some(1) ) @Test def adt3 = @@ -695,9 +693,8 @@ class CompletionSuite extends BaseCompletionSuite: |""".stripMargin, """|NotString: Int |Number: Regex - |Nil scala.collection.immutable |""".stripMargin, - topLines = Option(3) + topLines = Some(2) ) @Test def adt4 = @@ -705,29 +702,12 @@ class CompletionSuite extends BaseCompletionSuite: s"""|object Main { | val Number = "".r | "" match { - | case _: N@@ - |} - |""".stripMargin, - """|Number: Regex - |NoSuchElementException java.util - |NoSuchFieldError java.lang - |""".stripMargin, - topLines = Option(3) - ) - - @Test def adt5 = - check( - s"""|object Main { - | val Number = "".r - | "" match { - | case _: N@@ + | case _: Numb@@ |} |""".stripMargin, """|Number: Regex - |NoSuchElementException java.util - |NoSuchFieldError java.lang |""".stripMargin, - topLines = Option(3) + topLines = Some(1) ) @Test def `no-methods-on-case-type` = From 94a3cb53e099d520c42f4a23e0275975ad5b643b Mon Sep 17 00:00:00 2001 From: rochala Date: Tue, 19 Sep 2023 13:07:22 +0200 Subject: [PATCH 06/11] refactor Completions, remove unsafeNulls from Completion.scala --- .../tools/dotc/interactive/Completion.scala | 122 +++++++++--------- .../tools/languageserver/CompletionTest.scala | 25 +++- .../tools/pc/completions/Completions.scala | 2 +- .../pc/tests/completion/CompletionSuite.scala | 8 +- 4 files changed, 84 insertions(+), 73 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/interactive/Completion.scala b/compiler/src/dotty/tools/dotc/interactive/Completion.scala index 0b9265ae2635..80cba66f60ed 100644 --- a/compiler/src/dotty/tools/dotc/interactive/Completion.scala +++ b/compiler/src/dotty/tools/dotc/interactive/Completion.scala @@ -1,7 +1,5 @@ package dotty.tools.dotc.interactive -import scala.language.unsafeNulls - import dotty.tools.dotc.ast.untpd import dotty.tools.dotc.ast.NavigateAST import dotty.tools.dotc.config.Printers.interactiv @@ -38,7 +36,7 @@ import scala.util.control.NonFatal */ case class Completion(label: String, description: String, symbols: List[Symbol]) -object Completion { +object Completion: import dotty.tools.dotc.ast.tpd._ @@ -46,10 +44,9 @@ object Completion { * * @return offset and list of symbols for possible completions */ - def completions(pos: SourcePosition)(using Context): (Int, List[Completion]) = { - val path = Interactive.pathTo(ctx.compilationUnit.tpdTree, pos.span) + def completions(pos: SourcePosition)(using Context): (Int, List[Completion]) = + val path: List[Tree] = Interactive.pathTo(ctx.compilationUnit.tpdTree, pos.span) computeCompletions(pos, path)(using Interactive.contextOfPath(path).withPhase(Phases.typerPhase)) - } /** * Inspect `path` to determine what kinds of symbols should be considered. @@ -61,10 +58,11 @@ object Completion { * * Otherwise, provide no completion suggestion. */ - def completionMode(path: List[Tree], pos: SourcePosition): Mode = - path match { - case Ident(_) :: Import(_, _) :: _ => Mode.ImportOrExport - case (ref: RefTree) :: _ => + def completionMode(path: List[untpd.Tree], pos: SourcePosition): Mode = + path match + case untpd.Ident(_) :: untpd.Import(_, _) :: _ => Mode.ImportOrExport + case untpd.Ident(_) :: (_: untpd.ImportSelector) :: _ => Mode.ImportOrExport + case (ref: untpd.RefTree) :: _ => if (ref.name.isTermName) Mode.Term else if (ref.name.isTypeName) Mode.Type else Mode.None @@ -73,9 +71,8 @@ object Completion { if sel.imported.span.contains(pos.span) then Mode.ImportOrExport else Mode.None // Can't help completing the renaming - case (_: ImportOrExport) :: _ => Mode.ImportOrExport + case (_: untpd.ImportOrExport) :: _ => Mode.ImportOrExport case _ => Mode.None - } /** When dealing with in varios palces we check to see if they are * due to incomplete backticks. If so, we ensure we get the full prefix @@ -97,15 +94,18 @@ object Completion { * Inspect `path` to determine the completion prefix. Only symbols whose name start with the * returned prefix should be considered. */ - def completionPrefix(path: List[untpd.Tree], pos: SourcePosition)(using Context): String = + def completionPrefix(path: List[untpd.Tree], pos: SourcePosition)(using Context): String = path match case (sel: untpd.ImportSelector) :: _ => completionPrefix(sel.imported :: Nil, pos) + case untpd.Ident(_) :: (sel: untpd.ImportSelector) :: _ if !sel.isGiven => + completionPrefix(sel.imported :: Nil, pos) + case (tree: untpd.ImportOrExport) :: _ => - tree.selectors.find(_.span.contains(pos.span)).map { selector => + tree.selectors.find(_.span.contains(pos.span)).map: selector => completionPrefix(selector :: Nil, pos) - }.getOrElse("") + .getOrElse("") // Foo.`se will result in Select(Ident(Foo), ) case (select: untpd.Select) :: _ if select.name == nme.ERROR => @@ -119,29 +119,34 @@ object Completion { if (ref.name == nme.ERROR) "" else ref.name.toString.take(pos.span.point - ref.span.point) - case _ => - "" + case _ => "" + end completionPrefix /** Inspect `path` to determine the offset where the completion result should be inserted. */ - def completionOffset(path: List[Tree]): Int = - path match { - case (ref: RefTree) :: _ => ref.span.point + def completionOffset(untpdPath: List[untpd.Tree]): Int = + untpdPath match { + case (ref: untpd.RefTree) :: _ => ref.span.point case _ => 0 } - /** - * Inspect `path` to deterimine whether enclosing tree is a result of tree extension. - * If so, completion should use untyped path containing tree before extension to get proper results. + /** Some information about the trees is lost after Typer such as Extension method definitions + * are expanded into methods. In order to support completions in those cases + * we have to rely on untyped trees and only when types are necessary use typed trees. */ - def pathBeforeDesugaring(path: List[Tree], pos: SourcePosition)(using Context): List[Tree] = - val hasUntypedTree = path.headOption.forall(NavigateAST.untypedPath(_, exactMatch = true).nonEmpty) - if hasUntypedTree then path - else NavigateAST.untypedPath(pos.span).collect: - case tree: untpd.Tree => tree - - private def computeCompletions(pos: SourcePosition, path: List[Tree])(using Context): (Int, List[Completion]) = { - val path0 = pathBeforeDesugaring(path, pos) + def resolveTypedOrUntypedPath(tpdPath: List[Tree], pos: SourcePosition)(using Context): List[untpd.Tree] = + lazy val untpdPath: List[untpd.Tree] = NavigateAST + .pathTo(pos.span, List(ctx.compilationUnit.untpdTree), true).collect: + case untpdTree: untpd.Tree => untpdTree + + + tpdPath match + case (_: Bind) :: _ => tpdPath + case (_: untpd.TypTree) :: _ => tpdPath + case _ => untpdPath + + private def computeCompletions(pos: SourcePosition, tpdPath: List[Tree])(using Context): (Int, List[Completion]) = + val path0 = resolveTypedOrUntypedPath(tpdPath, pos) val mode = completionMode(path0, pos) val rawPrefix = completionPrefix(path0, pos) @@ -150,16 +155,15 @@ object Completion { val completer = new Completer(mode, prefix, pos) - val completions = path0 match { - // Ignore synthetic select from `This` because in code it was `Ident` - // See example in dotty.tools.languageserver.CompletionTest.syntheticThis - case Select(qual @ This(_), _) :: _ if qual.span.isSynthetic => completer.scopeCompletions - case Select(qual, _) :: _ if qual.tpe.hasSimpleKind => completer.selectionCompletions(qual) - case Select(qual, _) :: _ => Map.empty - case (tree: ImportOrExport) :: _ => completer.directMemberCompletions(tree.expr) - case (_: untpd.ImportSelector) :: Import(expr, _) :: _ => completer.directMemberCompletions(expr) - case _ => completer.scopeCompletions - } + val completions = tpdPath match + // Ignore synthetic select from `This` because in code it was `Ident` + // See example in dotty.tools.languageserver.CompletionTest.syntheticThis + case Select(qual @ This(_), _) :: _ if qual.span.isSynthetic => completer.scopeCompletions + case Select(qual, _) :: _ if qual.tpe.hasSimpleKind => completer.selectionCompletions(qual) + case Select(qual, _) :: _ => Map.empty + case (tree: ImportOrExport) :: _ => completer.directMemberCompletions(tree.expr) + case (_: untpd.ImportSelector) :: Import(expr, _) :: _ => completer.directMemberCompletions(expr) + case _ => completer.scopeCompletions val describedCompletions = describeCompletions(completions) val backtickedCompletions = @@ -173,7 +177,6 @@ object Completion { | type = ${completer.mode.is(Mode.Type)} | results = $backtickedCompletions%, %""") (offset, backtickedCompletions) - } def backtickCompletions(completion: Completion, hasBackTick: Boolean) = if hasBackTick || needsBacktick(completion.label) then @@ -186,17 +189,17 @@ object Completion { // https://github.com/scalameta/metals/blob/main/mtags/src/main/scala/scala/meta/internal/mtags/KeywordWrapper.scala // https://github.com/com-lihaoyi/Ammonite/blob/73a874173cd337f953a3edc9fb8cb96556638fdd/amm/util/src/main/scala/ammonite/util/Model.scala private def needsBacktick(s: String) = - val chunks = s.split("_", -1) + val chunks = s.split("_", -1).nn val validChunks = chunks.zipWithIndex.forall { case (chunk, index) => - chunk.forall(Chars.isIdentifierPart) || - (chunk.forall(Chars.isOperatorPart) && + chunk.nn.forall(Chars.isIdentifierPart) || + (chunk.nn.forall(Chars.isOperatorPart) && index == chunks.length - 1 && !(chunks.lift(index - 1).contains("") && index - 1 == 0)) } val validStart = - Chars.isIdentifierStart(s(0)) || chunks(0).forall(Chars.isOperatorPart) + Chars.isIdentifierStart(s(0)) || chunks(0).nn.forall(Chars.isOperatorPart) val valid = validChunks && validStart && !keywords.contains(s) @@ -228,7 +231,7 @@ object Completion { * For the results of all `xyzCompletions` methods term names and type names are always treated as different keys in the same map * and they never conflict with each other. */ - class Completer(val mode: Mode, val prefix: String, pos: SourcePosition) { + class Completer(val mode: Mode, val prefix: String, pos: SourcePosition): /** Completions for terms and types that are currently in scope: * the members of the current class, local definitions and the symbols that have been imported, * recursively adding completions from outer scopes. @@ -242,7 +245,7 @@ object Completion { * (even if the import follows it syntactically) * - a more deeply nested import shadowing a member or a local definition causes an ambiguity */ - def scopeCompletions(using context: Context): CompletionMap = { + def scopeCompletions(using context: Context): CompletionMap = val mappings = collection.mutable.Map.empty[Name, List[ScopedDenotations]].withDefaultValue(List.empty) def addMapping(name: Name, denots: ScopedDenotations) = mappings(name) = mappings(name) :+ denots @@ -314,7 +317,7 @@ object Completion { } resultMappings - } + end scopeCompletions /** Widen only those types which are applied or are exactly nothing */ @@ -347,16 +350,16 @@ object Completion { /** Completions introduced by imports directly in this context. * Completions from outer contexts are not included. */ - private def importedCompletions(using Context): CompletionMap = { + private def importedCompletions(using Context): CompletionMap = val imp = ctx.importInfo - def fromImport(name: Name, nameInScope: Name): Seq[(Name, SingleDenotation)] = - imp.site.member(name).alternatives - .collect { case denot if include(denot, nameInScope) => nameInScope -> denot } - if imp == null then Map.empty else + def fromImport(name: Name, nameInScope: Name): Seq[(Name, SingleDenotation)] = + imp.site.member(name).alternatives + .collect { case denot if include(denot, nameInScope) => nameInScope -> denot } + val givenImports = imp.importedImplicits .map { ref => (ref.implicitName: Name, ref.underlyingRef.denot.asSingleDenotation) } .filter((name, denot) => include(denot, name)) @@ -382,7 +385,7 @@ object Completion { }.toSeq.groupByName givenImports ++ wildcardMembers ++ explicitMembers - } + end importedCompletions /** Completions from implicit conversions including old style extensions using implicit classes */ private def implicitConversionMemberCompletions(qual: Tree)(using Context): CompletionMap = @@ -544,7 +547,6 @@ object Completion { extension [N <: Name](namedDenotations: Seq[(N, SingleDenotation)]) @annotation.targetName("groupByNameTupled") def groupByName: CompletionMap = namedDenotations.groupMap((name, denot) => name)((name, denot) => denot) - } private type CompletionMap = Map[Name, Seq[SingleDenotation]] @@ -557,11 +559,11 @@ object Completion { * The completion mode: defines what kinds of symbols should be included in the completion * results. */ - class Mode(val bits: Int) extends AnyVal { + class Mode(val bits: Int) extends AnyVal: def is(other: Mode): Boolean = (bits & other.bits) == other.bits def |(other: Mode): Mode = new Mode(bits | other.bits) - } - object Mode { + + object Mode: /** No symbol should be included */ val None: Mode = new Mode(0) @@ -573,6 +575,4 @@ object Completion { /** Both term and type symbols are allowed */ val ImportOrExport: Mode = new Mode(4) | Term | Type - } -} diff --git a/language-server/test/dotty/tools/languageserver/CompletionTest.scala b/language-server/test/dotty/tools/languageserver/CompletionTest.scala index ced7f19749de..daa0004e801c 100644 --- a/language-server/test/dotty/tools/languageserver/CompletionTest.scala +++ b/language-server/test/dotty/tools/languageserver/CompletionTest.scala @@ -1523,10 +1523,7 @@ class CompletionTest { |object Test: | def foo: ArrayBuffer[Fo${m1}] = ??? """ - .completion(m1, Set( - ("Foo",Class,"Foo") - ) - ) + .completion(m1, Set(("Foo",Class,"Foo"))) } @Test def extensionDefinitionCompletions: Unit = @@ -1534,7 +1531,21 @@ class CompletionTest { |object T: | extension (x: Fo$m1) |""" - .completion(m1, Set( - ("Foo",Class,"Foo") - )) + .completion(m1, Set(("Foo",Class,"Foo"))) + + @Test def selectDynamic: Unit = + code"""|import scala.language.dynamics + |class Foo extends Dynamic { + | def banana: Int = 42 + | def selectDynamic(field: String): Foo = this + | def applyDynamicNamed(name: String)(arg: (String, Int)): Foo = this + | def updateDynamic(name: String)(value: Int): Foo = this + |} + |object Test: + | val x = new Foo() + | x.sele$m1 + | x.bana$m2 + |""" + .completion(m1, Set(("selectDynamic", Method, "(field: String): Foo"))) + .completion(m2, Set(("banana", Method, "=> Int"))) } diff --git a/presentation-compiler/src/main/dotty/tools/pc/completions/Completions.scala b/presentation-compiler/src/main/dotty/tools/pc/completions/Completions.scala index a55f19e2ab68..8ed7792feaf2 100644 --- a/presentation-compiler/src/main/dotty/tools/pc/completions/Completions.scala +++ b/presentation-compiler/src/main/dotty/tools/pc/completions/Completions.scala @@ -57,7 +57,7 @@ class Completions( val coursierComplete = new CoursierComplete(BuildInfo.scalaVersion) private lazy val completionMode = - val adjustedPath = Completion.pathBeforeDesugaring(path, pos) + val adjustedPath = Completion.resolveTypedOrUntypedPath(path, pos) val mode = Completion.completionMode(adjustedPath, pos) path match case Literal(Constant(_: String)) :: _ => Mode.Term // literal completions diff --git a/presentation-compiler/test/dotty/tools/pc/tests/completion/CompletionSuite.scala b/presentation-compiler/test/dotty/tools/pc/tests/completion/CompletionSuite.scala index 4c1254a063a4..83783ba45a21 100644 --- a/presentation-compiler/test/dotty/tools/pc/tests/completion/CompletionSuite.scala +++ b/presentation-compiler/test/dotty/tools/pc/tests/completion/CompletionSuite.scala @@ -539,7 +539,7 @@ class CompletionSuite extends BaseCompletionSuite: | new Foo().bana@@ |} |""".stripMargin, - "selectDynamic(field: String): Foo" + "banana: Int" ) @Test def dynamic2 = @@ -549,7 +549,7 @@ class CompletionSuite extends BaseCompletionSuite: | val x = new Foo().foo.bana@@ |} |""".stripMargin, - "selectDynamic(field: String): Foo" + "banana: Int" ) @Test def dynamic3 = @@ -560,7 +560,7 @@ class CompletionSuite extends BaseCompletionSuite: | (foo.bar = 42).bana@@ |} |""".stripMargin, - "selectDynamic(field: String): Foo" + "banana: Int" ) @Test def dynamic4 = @@ -570,7 +570,7 @@ class CompletionSuite extends BaseCompletionSuite: | val foo = new Foo().foo(x = 42).bana@@ |} |""".stripMargin, - "selectDynamic(field: String): Foo" + "banana: Int" ) @Test def dynamic5 = From fe49708980e4aafbf04c3a2cd9461571e23c0e28 Mon Sep 17 00:00:00 2001 From: rochala Date: Thu, 21 Sep 2023 10:20:23 +0200 Subject: [PATCH 07/11] add test, remove additional whitespace --- .../src/dotty/tools/dotc/interactive/Completion.scala | 2 +- .../tools/pc/tests/completion/CompletionSuite.scala | 11 +++++++++++ 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/compiler/src/dotty/tools/dotc/interactive/Completion.scala b/compiler/src/dotty/tools/dotc/interactive/Completion.scala index 80cba66f60ed..1a0dbbe736e5 100644 --- a/compiler/src/dotty/tools/dotc/interactive/Completion.scala +++ b/compiler/src/dotty/tools/dotc/interactive/Completion.scala @@ -94,7 +94,7 @@ object Completion: * Inspect `path` to determine the completion prefix. Only symbols whose name start with the * returned prefix should be considered. */ - def completionPrefix(path: List[untpd.Tree], pos: SourcePosition)(using Context): String = + def completionPrefix(path: List[untpd.Tree], pos: SourcePosition)(using Context): String = path match case (sel: untpd.ImportSelector) :: _ => completionPrefix(sel.imported :: Nil, pos) diff --git a/presentation-compiler/test/dotty/tools/pc/tests/completion/CompletionSuite.scala b/presentation-compiler/test/dotty/tools/pc/tests/completion/CompletionSuite.scala index 83783ba45a21..9242db60a831 100644 --- a/presentation-compiler/test/dotty/tools/pc/tests/completion/CompletionSuite.scala +++ b/presentation-compiler/test/dotty/tools/pc/tests/completion/CompletionSuite.scala @@ -1442,3 +1442,14 @@ class CompletionSuite extends BaseCompletionSuite: |""".stripMargin ) + + @Test def `no-square-brackets` = + checkEdit( + """|object O: + | val a = List.appl@@ + |""".stripMargin, + """|object O: + | val a = List.apply($0) + |""".stripMargin, + ) + From b9ff46b83daa1480f674e1db72f18f3227226191 Mon Sep 17 00:00:00 2001 From: rochala Date: Thu, 21 Sep 2023 15:39:44 +0200 Subject: [PATCH 08/11] fix extension definitions with Select, add tests, simplify if --- .../tools/dotc/interactive/Completion.scala | 24 ++++++++------ .../tools/languageserver/CompletionTest.scala | 10 ++++++ .../tools/pc/completions/Completions.scala | 6 ++-- .../pc/tests/completion/CompletionSuite.scala | 33 +++++++++++++++++++ 4 files changed, 59 insertions(+), 14 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/interactive/Completion.scala b/compiler/src/dotty/tools/dotc/interactive/Completion.scala index 1a0dbbe736e5..dd4f30d6e4a2 100644 --- a/compiler/src/dotty/tools/dotc/interactive/Completion.scala +++ b/compiler/src/dotty/tools/dotc/interactive/Completion.scala @@ -139,7 +139,6 @@ object Completion: .pathTo(pos.span, List(ctx.compilationUnit.untpdTree), true).collect: case untpdTree: untpd.Tree => untpdTree - tpdPath match case (_: Bind) :: _ => tpdPath case (_: untpd.TypTree) :: _ => tpdPath @@ -155,15 +154,20 @@ object Completion: val completer = new Completer(mode, prefix, pos) - val completions = tpdPath match - // Ignore synthetic select from `This` because in code it was `Ident` - // See example in dotty.tools.languageserver.CompletionTest.syntheticThis - case Select(qual @ This(_), _) :: _ if qual.span.isSynthetic => completer.scopeCompletions - case Select(qual, _) :: _ if qual.tpe.hasSimpleKind => completer.selectionCompletions(qual) - case Select(qual, _) :: _ => Map.empty - case (tree: ImportOrExport) :: _ => completer.directMemberCompletions(tree.expr) - case (_: untpd.ImportSelector) :: Import(expr, _) :: _ => completer.directMemberCompletions(expr) - case _ => completer.scopeCompletions + val completions = path0 match + case untpd.Select(qual, _) :: _ :: untpd.ExtMethods(_, _) :: _ => + val tpdQual = ctx.typer.typedExpr(qual) + completer.selectionCompletions(tpdQual) + case _ => tpdPath match + + // Ignore synthetic select from `This` because in code it was `Ident` + // See example in dotty.tools.languageserver.CompletionTest.syntheticThis + case Select(qual @ This(_), _) :: _ if qual.span.isSynthetic => completer.scopeCompletions + case Select(qual, _) :: _ if qual.tpe.hasSimpleKind => completer.selectionCompletions(qual) + case Select(qual, _) :: _ => Map.empty + case (tree: ImportOrExport) :: _ => completer.directMemberCompletions(tree.expr) + case (_: untpd.ImportSelector) :: Import(expr, _) :: _ => completer.directMemberCompletions(expr) + case _ => completer.scopeCompletions val describedCompletions = describeCompletions(completions) val backtickedCompletions = diff --git a/language-server/test/dotty/tools/languageserver/CompletionTest.scala b/language-server/test/dotty/tools/languageserver/CompletionTest.scala index daa0004e801c..349c8615c5a1 100644 --- a/language-server/test/dotty/tools/languageserver/CompletionTest.scala +++ b/language-server/test/dotty/tools/languageserver/CompletionTest.scala @@ -1533,6 +1533,16 @@ class CompletionTest { |""" .completion(m1, Set(("Foo",Class,"Foo"))) + @Test def extensionDefinitionCompletionsSelect: Unit = + code"""|object Test: + | class TestSelect() + |object T: + | extension (x: Test.TestSel$m1) + |""" + .completion(m1, Set( + ("TestSelect", Module, "Test.TestSelect"), ("TestSelect", Class, "Test.TestSelect") + )) + @Test def selectDynamic: Unit = code"""|import scala.language.dynamics |class Foo extends Dynamic { diff --git a/presentation-compiler/src/main/dotty/tools/pc/completions/Completions.scala b/presentation-compiler/src/main/dotty/tools/pc/completions/Completions.scala index 8ed7792feaf2..64bbbb848289 100644 --- a/presentation-compiler/src/main/dotty/tools/pc/completions/Completions.scala +++ b/presentation-compiler/src/main/dotty/tools/pc/completions/Completions.scala @@ -106,10 +106,8 @@ class Completions( else false if generalExclude then false - else if completionMode.is(Mode.ImportOrExport) then true - else if completionMode.is(Mode.Term) && isWildcardParam(sym) then false - else if completionMode.is(Mode.Term) && (sym.isTerm || sym.is(Package)) then true - else !completionMode.is(Mode.Term) + else if completionMode.is(Mode.Type) then true + else !isWildcardParam(sym) && (sym.isTerm || sym.is(Package)) end if end includeSymbol diff --git a/presentation-compiler/test/dotty/tools/pc/tests/completion/CompletionSuite.scala b/presentation-compiler/test/dotty/tools/pc/tests/completion/CompletionSuite.scala index 9242db60a831..d97576505105 100644 --- a/presentation-compiler/test/dotty/tools/pc/tests/completion/CompletionSuite.scala +++ b/presentation-compiler/test/dotty/tools/pc/tests/completion/CompletionSuite.scala @@ -1442,6 +1442,39 @@ class CompletionSuite extends BaseCompletionSuite: |""".stripMargin ) + @Test def `extension-definition-select` = + check( + """|object Test: + | class TestSelect() + |object T: + | extension (x: Test.TestSel@@) + |""".stripMargin, + """|TestSelect test.Test + |""".stripMargin + ) + + @Test def `extension-definition-select-mix-1` = + check( + """|object Test: + | class TestSelect() + |object T: + | extension (using Int)(x: Test.TestSel@@) + |""".stripMargin, + """|TestSelect test.Test + |""".stripMargin + ) + + @Test def `extension-definition-select-mix-2` = + check( + """|object Test: + | class TestSelect[T]() + |object T: + | extension [T](x: Test.TestSel@@) + |""".stripMargin, + """|TestSelect[T] test.Test + |TestSelect test.Test + |""".stripMargin + ) @Test def `no-square-brackets` = checkEdit( From 694f5b00d25be1cad38a7b4963c8f26f3a7a664e Mon Sep 17 00:00:00 2001 From: rochala Date: Thu, 21 Sep 2023 15:52:11 +0200 Subject: [PATCH 09/11] small refactor --- .../src/dotty/tools/dotc/interactive/Completion.scala | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/interactive/Completion.scala b/compiler/src/dotty/tools/dotc/interactive/Completion.scala index dd4f30d6e4a2..41526f207a22 100644 --- a/compiler/src/dotty/tools/dotc/interactive/Completion.scala +++ b/compiler/src/dotty/tools/dotc/interactive/Completion.scala @@ -154,12 +154,11 @@ object Completion: val completer = new Completer(mode, prefix, pos) - val completions = path0 match - case untpd.Select(qual, _) :: _ :: untpd.ExtMethods(_, _) :: _ => - val tpdQual = ctx.typer.typedExpr(qual) - completer.selectionCompletions(tpdQual) - case _ => tpdPath match + val adjustedPath: List[Tree] = path0 match + case (sel: untpd.Select) :: _ :: untpd.ExtMethods(_, _) :: _ => List(ctx.typer.typedExpr(sel)) + case _ => tpdPath + val completions = adjustedPath match // Ignore synthetic select from `This` because in code it was `Ident` // See example in dotty.tools.languageserver.CompletionTest.syntheticThis case Select(qual @ This(_), _) :: _ if qual.span.isSynthetic => completer.scopeCompletions From a2f7cc923b38ce2fa8e1adda083dc1eae79ba7b4 Mon Sep 17 00:00:00 2001 From: rochala Date: Tue, 26 Sep 2023 17:43:55 +0200 Subject: [PATCH 10/11] improve extension consturct typechecking for completions --- .../tools/dotc/interactive/Completion.scala | 35 +++++++++++++++---- .../tools/languageserver/CompletionTest.scala | 33 +++++++++++++++++ 2 files changed, 62 insertions(+), 6 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/interactive/Completion.scala b/compiler/src/dotty/tools/dotc/interactive/Completion.scala index 41526f207a22..8fb844f1f333 100644 --- a/compiler/src/dotty/tools/dotc/interactive/Completion.scala +++ b/compiler/src/dotty/tools/dotc/interactive/Completion.scala @@ -24,6 +24,10 @@ import dotty.tools.dotc.util.SourcePosition import scala.collection.mutable import scala.util.control.NonFatal +import dotty.tools.dotc.core.ContextOps.localContext +import dotty.tools.dotc.core.Names +import dotty.tools.dotc.core.Types +import dotty.tools.dotc.core.Symbols /** * One of the results of a completion query. @@ -130,8 +134,8 @@ object Completion: case _ => 0 } - /** Some information about the trees is lost after Typer such as Extension method definitions - * are expanded into methods. In order to support completions in those cases + /** Some information about the trees is lost after Typer such as Extension method construct + * is expanded into methods. In order to support completions in those cases * we have to rely on untyped trees and only when types are necessary use typed trees. */ def resolveTypedOrUntypedPath(tpdPath: List[Tree], pos: SourcePosition)(using Context): List[untpd.Tree] = @@ -144,6 +148,28 @@ object Completion: case (_: untpd.TypTree) :: _ => tpdPath case _ => untpdPath + /** Handle case when cursor position is inside extension method construct. + * The extension method construct is then desugared into methods, and consturct parameters + * are no longer a part of a typed tree, but instead are prepended to method parameters. + * + * @param untpdPath The typed or untyped path to the tree that is being completed + * @param tpdPath The typed path that will be returned if no extension method construct is found + * @param pos The cursor position + * + * @return Typed path to the parameter of the extension construct if found or tpdPath + */ + private def typeCheckExtensionConstructPath( + untpdPath: List[untpd.Tree], tpdPath: List[Tree], pos: SourcePosition + )(using Context): List[Tree] = + untpdPath.collectFirst: + case untpd.ExtMethods(paramss, _) => + val enclosingParam = paramss.flatten.find(_.span.contains(pos.span)) + enclosingParam.map: param => + ctx.typer.index(paramss.flatten) + val typedEnclosingParam = ctx.typer.typed(param) + Interactive.pathTo(typedEnclosingParam, pos.span) + .flatten.getOrElse(tpdPath) + private def computeCompletions(pos: SourcePosition, tpdPath: List[Tree])(using Context): (Int, List[Completion]) = val path0 = resolveTypedOrUntypedPath(tpdPath, pos) val mode = completionMode(path0, pos) @@ -154,10 +180,7 @@ object Completion: val completer = new Completer(mode, prefix, pos) - val adjustedPath: List[Tree] = path0 match - case (sel: untpd.Select) :: _ :: untpd.ExtMethods(_, _) :: _ => List(ctx.typer.typedExpr(sel)) - case _ => tpdPath - + val adjustedPath = typeCheckExtensionConstructPath(path0, tpdPath, pos) val completions = adjustedPath match // Ignore synthetic select from `This` because in code it was `Ident` // See example in dotty.tools.languageserver.CompletionTest.syntheticThis diff --git a/language-server/test/dotty/tools/languageserver/CompletionTest.scala b/language-server/test/dotty/tools/languageserver/CompletionTest.scala index 349c8615c5a1..7a21fe15fbe5 100644 --- a/language-server/test/dotty/tools/languageserver/CompletionTest.scala +++ b/language-server/test/dotty/tools/languageserver/CompletionTest.scala @@ -1543,6 +1543,39 @@ class CompletionTest { ("TestSelect", Module, "Test.TestSelect"), ("TestSelect", Class, "Test.TestSelect") )) + @Test def extensionDefinitionCompletionsSelectNested: Unit = + code"""|object Test: + | object Test2: + | class TestSelect() + |object T: + | extension (x: Test.Test2.TestSel$m1) + |""" + .completion(m1, Set( + ("TestSelect", Module, "Test.Test2.TestSelect"), ("TestSelect", Class, "Test.Test2.TestSelect") + )) + + @Test def extensionDefinitionCompletionsSelectInside: Unit = + code"""|object Test: + | object Test2: + | class TestSelect() + |object T: + | extension (x: Test.Te$m1.TestSelect) + |""" + .completion(m1, Set(("Test2", Module, "Test.Test2"))) + + @Test def extensionDefinitionCompletionsTypeParam: Unit = + code"""|object T: + | extension [TypeParam](x: TypePar$m1) + |""" + .completion(m1, Set(("TypeParam", Field, "T.TypeParam"))) + + + @Test def typeParamCompletions: Unit = + code"""|object T: + | def xxx[TTT](x: TT$m1) + |""" + .completion(m1, Set(("TTT", Field, "T.TTT"))) + @Test def selectDynamic: Unit = code"""|import scala.language.dynamics |class Foo extends Dynamic { From 730020593a298a21bc9b221e18c4ea9706228ddd Mon Sep 17 00:00:00 2001 From: rochala Date: Tue, 3 Oct 2023 14:49:56 +0200 Subject: [PATCH 11/11] fix typo in test --- .../test/dotty/tools/pc/tests/completion/CompletionSuite.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/presentation-compiler/test/dotty/tools/pc/tests/completion/CompletionSuite.scala b/presentation-compiler/test/dotty/tools/pc/tests/completion/CompletionSuite.scala index d97576505105..bcd47259bb57 100644 --- a/presentation-compiler/test/dotty/tools/pc/tests/completion/CompletionSuite.scala +++ b/presentation-compiler/test/dotty/tools/pc/tests/completion/CompletionSuite.scala @@ -1436,7 +1436,7 @@ class CompletionSuite extends BaseCompletionSuite: check( """|trait Foo |object T: - | extension [A](using Foo)(x: Fo@@)(using Fo@@) + | extension [A](using Foo)(x: Fo@@)(using Foo) |""".stripMargin, """|Foo test |""".stripMargin