Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Presentation compiler: Add completions for named patterns #22251

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 29 additions & 19 deletions compiler/src/dotty/tools/dotc/interactive/Completion.scala
Original file line number Diff line number Diff line change
Expand Up @@ -89,21 +89,24 @@ object Completion:
*
* Otherwise, provide no completion suggestion.
*/
def completionMode(path: List[untpd.Tree], pos: SourcePosition): Mode = path match
case GenericImportSelector(sel) =>
if sel.imported.span.contains(pos.span) then Mode.ImportOrExport // import scala.@@
else if sel.isGiven && sel.bound.span.contains(pos.span) then Mode.ImportOrExport
else Mode.None // import scala.{util => u@@}
case GenericImportOrExport(_) => Mode.ImportOrExport | Mode.Scope // import TrieMa@@
case untpd.Literal(Constants.Constant(_: String)) :: _ => Mode.Term | Mode.Scope // literal completions
case (ref: untpd.RefTree) :: _ =>
val maybeSelectMembers = if ref.isInstanceOf[untpd.Select] then Mode.Member else Mode.Scope

if (ref.name.isTermName) Mode.Term | maybeSelectMembers
else if (ref.name.isTypeName) Mode.Type | maybeSelectMembers
else Mode.None

case _ => Mode.None
def completionMode(path: List[untpd.Tree], pos: SourcePosition): Mode =
path match
case GenericImportSelector(sel) =>
if sel.imported.span.contains(pos.span) then Mode.ImportOrExport // import scala.@@
else if sel.isGiven && sel.bound.span.contains(pos.span) then Mode.ImportOrExport
else Mode.None // import scala.{util => u@@}
case GenericImportOrExport() => Mode.ImportOrExport | Mode.Scope // import TrieMa@@
case BindMixedWithNamedPatterns() => Mode.None // case User(name = name, sur@@)
case untpd.Literal(Constants.Constant(_: String)) :: _ => Mode.Term | Mode.Scope // literal completions
// TODO case (_: tpd.Bind) :: _ => we should complete only when in backticks
case (ref: untpd.RefTree) :: _ =>
val maybeSelectMembers = if ref.isInstanceOf[untpd.Select] then Mode.Member else Mode.Scope

if (ref.name.isTermName) Mode.Term | maybeSelectMembers
else if (ref.name.isTypeName) Mode.Type | maybeSelectMembers
else Mode.None

case _ => Mode.None

/** When dealing with <errors> in varios palces we check to see if they are
* due to incomplete backticks. If so, we ensure we get the full prefix
Expand Down Expand Up @@ -161,12 +164,19 @@ object Completion:
case (sel: untpd.ImportSelector) :: _ => Some(sel)
case _ => None

private object BindMixedWithNamedPatterns:
def unapply(path: List[untpd.Tree]): Boolean =
path match
case (_: untpd.Ident) :: (fn0: untpd.Apply) :: untpd.CaseDef(fn1, _, _) :: _
if fn1 == fn0 && fn0.args.exists(_.isInstanceOf[untpd.NamedArg]) => true
Comment on lines +168 to +171
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What if it's nested:

case Some(User(nam@@)) =>

or is this covered by some other case?

case _ => false

private object GenericImportOrExport:
def unapply(path: List[untpd.Tree]): Option[untpd.ImportOrExport] =
def unapply(path: List[untpd.Tree]): Boolean =
path match
case untpd.Ident(_) :: (importOrExport: untpd.ImportOrExport) :: _ => Some(importOrExport)
case (importOrExport: untpd.ImportOrExport) :: _ => Some(importOrExport)
case _ => None
case untpd.Ident(_) :: (importOrExport: untpd.ImportOrExport) :: _ => true
case (importOrExport: untpd.ImportOrExport) :: _ => true
case _ => false

/** Inspect `path` to determine the offset where the completion result should be inserted. */
def completionOffset(untpdPath: List[untpd.Tree]): Int =
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
package dotty.tools.pc

import dotty.tools.dotc.ast.tpd
import dotty.tools.dotc.ast.tpd.*
import dotty.tools.dotc.core.Constants.Constant
import dotty.tools.dotc.core.Contexts.Context
Expand All @@ -16,7 +15,6 @@ import dotty.tools.dotc.typer.Applications.UnapplyArgs
import dotty.tools.dotc.util.NoSourcePosition
import dotty.tools.dotc.util.SourceFile
import dotty.tools.dotc.util.Spans.Span
import dotty.tools.pc.IndexedContext
import dotty.tools.pc.printer.ShortenedTypePrinter
import dotty.tools.pc.printer.ShortenedTypePrinter.IncludeDefaultParam
import dotty.tools.pc.utils.InteractiveEnrichments.*
Expand Down Expand Up @@ -95,7 +93,8 @@ object InterCompletionType:
case UnApply(fun, _, pats) :: _ =>
val ind = pats.indexWhere(_.span.contains(span))
if ind < 0 then None
else Some(UnapplyArgs(fun.tpe.finalResultType, fun, pats, NoSourcePosition).argTypes(ind))
else
UnapplyArgs(fun.tpe.finalResultType, fun, pats, NoSourcePosition).argTypes.get(ind)
// f(@@)
case (app: Apply) :: rest =>
val param =
Expand Down
Original file line number Diff line number Diff line change
@@ -1,30 +1,22 @@
package dotty.tools.pc

import java.nio.file.Paths

import dotty.tools.pc.PcSymbolSearch.*
import scala.meta.internal.metals.CompilerOffsetParams
import scala.meta.pc.OffsetParams
import scala.meta.pc.VirtualFileParams
import scala.meta as m

import dotty.tools.dotc.ast.NavigateAST
import dotty.tools.dotc.ast.Positioned
import dotty.tools.dotc.ast.tpd
import dotty.tools.dotc.ast.tpd.*
import dotty.tools.dotc.ast.untpd
import dotty.tools.dotc.ast.untpd.ExtMethods
import dotty.tools.dotc.ast.untpd.ImportSelector
import dotty.tools.dotc.core.Contexts.*
import dotty.tools.dotc.core.Flags
import dotty.tools.dotc.core.NameOps.*
import dotty.tools.dotc.core.Names.*
import dotty.tools.dotc.core.StdNames.*
import dotty.tools.dotc.core.Symbols.*
import dotty.tools.dotc.core.Types.*
import dotty.tools.dotc.interactive.Interactive
import dotty.tools.dotc.interactive.InteractiveDriver
import dotty.tools.dotc.util.SourceFile
import dotty.tools.dotc.util.SourcePosition
import dotty.tools.dotc.util.Spans.Span
import dotty.tools.pc.utils.InteractiveEnrichments.*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ import scala.meta.internal.metals.CompilerOffsetParams
import scala.meta.pc.ReferencesRequest
import scala.meta.pc.ReferencesResult

import dotty.tools.dotc.ast.tpd
import dotty.tools.dotc.ast.tpd.*
import dotty.tools.dotc.core.Symbols.*
import dotty.tools.dotc.interactive.InteractiveDriver
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,10 +30,8 @@ import scala.meta.pc.{PcSymbolInformation as IPcSymbolInformation}

import dotty.tools.dotc.reporting.StoreReporter
import dotty.tools.pc.completions.CompletionProvider
import dotty.tools.pc.InferExpectedType
import dotty.tools.pc.completions.OverrideCompletions
import dotty.tools.pc.buildinfo.BuildInfo
import dotty.tools.pc.SymbolInformationProvider
import dotty.tools.dotc.interactive.InteractiveDriver

import org.eclipse.lsp4j.DocumentHighlight
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@ import dotty.tools.dotc.core.Names.*
import dotty.tools.dotc.core.StdNames.nme
import dotty.tools.dotc.core.Symbols.*
import dotty.tools.pc.utils.InteractiveEnrichments.deepDealias
import dotty.tools.pc.SemanticdbSymbols
import dotty.tools.pc.utils.InteractiveEnrichments.allSymbols
import dotty.tools.pc.utils.InteractiveEnrichments.stripBackticks
import scala.meta.internal.pc.PcSymbolInformation
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -250,7 +250,7 @@ object CompletionValue:
denotation: Denotation
) extends Symbolic:
override def insertText: Option[String] = Some(label.replace("$", "$$").nn)
override def completionItemDataKind: Integer = CompletionSource.OverrideKind.ordinal
override def completionItemDataKind: Integer = CompletionSource.NamedArgKind.ordinal
override def completionItemKind(using Context): CompletionItemKind =
CompletionItemKind.Field
override def description(printer: ShortenedTypePrinter)(using Context): String =
Expand All @@ -265,7 +265,7 @@ object CompletionValue:
) extends CompletionValue:
override def completionItemKind(using Context): CompletionItemKind =
CompletionItemKind.Enum
override def completionItemDataKind: Integer = CompletionSource.OverrideKind.ordinal
override def completionItemDataKind: Integer = CompletionSource.AutoFillKind.ordinal
override def insertText: Option[String] = Some(value)
override def label: String = "Autofill with default values"

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -336,6 +336,8 @@ class Completions(
val values = ScaladocCompletions.contribute(pos, text, config)
(values, true)

case NamedPatternCompletions(namedPatternCompletions) => (namedPatternCompletions(completionPos), false)

case MatchCaseExtractor.MatchExtractor(selector) =>
(
CaseKeywordCompletion.matchContribute(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,6 @@ import dotty.tools.dotc.core.Types.NoType
import dotty.tools.dotc.core.Types.OrType
import dotty.tools.dotc.core.Types.Type
import dotty.tools.dotc.core.Types.TypeRef
import dotty.tools.dotc.core.Types.AppliedType
import dotty.tools.dotc.typer.Applications.UnapplyArgs
import dotty.tools.dotc.util.SourcePosition
import dotty.tools.pc.AutoImports.AutoImportsGenerator
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
package dotty.tools.pc.completions

import dotty.tools.dotc.ast.tpd.*
import dotty.tools.dotc.ast.untpd
import dotty.tools.dotc.core.Flags
import dotty.tools.dotc.core.Names.Name
import dotty.tools.dotc.core.StdNames.*
import dotty.tools.dotc.core.SymDenotations.NoDenotation
import dotty.tools.dotc.core.Symbols
import dotty.tools.dotc.core.Symbols.defn
import dotty.tools.dotc.core.Contexts.*
import dotty.tools.dotc.core.Types.*
import dotty.tools.dotc.util.SourcePosition
import dotty.tools.dotc.ast.NavigateAST

import scala.meta.internal.pc.CompletionFuzzy

object NamedPatternCompletions:

def isInsideParams(sourcePos: SourcePosition, start: Int): Boolean =
sourcePos.source.content().slice(sourcePos.start, start).foldLeft(0): (count, char) =>
if char == '(' then count + 1
else if char == ')' then count - 1
else count
> 0

def unapply(path: List[Tree])(using Context): Option[CompletionPos => List[CompletionValue]] =
val result = path match
// case (nam@@
// but not case nam@@
case (bind: Bind) :: (caseDef: CaseDef) :: Match(selector, _) :: _
if isInsideParams(caseDef.sourcePos, bind.sourcePos.end) =>
if selector.tpe.widenDealias.isNamedTupleType then
Some(selector.tpe.widenDealias.namedTupleElementTypes.toMap, Nil)
else None

// case (name = supername, na@@
// case (nam@@, surname = test) =>
case (_: Bind) :: (rest @ (unapply: UnApply) :: _)
if defn.isTupleClass(unapply.fun.symbol.owner.companionClass) =>
rest.collectFirst: // We can't complete names without knowing the type of selector
case Match(selector, _) => selector
Comment on lines +41 to +42
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Won't this fail for nested named tuples?

.flatMap: selector =>
// Named patterns are desugared to normal binds without original arg name info
val patterns = NavigateAST.untypedPath(unapply).collectFirst:
case untpd.Tuple(elems) => elems
.getOrElse(Nil)

if selector.tpe.widenDealias.isNamedTupleType then
Some(selector.tpe.widenDealias.namedTupleElementTypes.toMap, patterns)
else None

// case User(nam@@
// case User(nam@@, surname = test) =>
case (_: Bind) :: (rest @ (unapply: UnApply) :: _) =>
Some(unapplyResultNamesToTypes(unapply.fun), unapply.patterns)

// This case is happening because nam@@ is removed at desugaring as it is illegal unnamed bind mixed with named one
// case User(surname = test, nam@@) =>
// case User(surname = test, nam@@
case UnApply(fun, _, patterns) :: _ => Some(unapplyResultNamesToTypes(fun), patterns)
case _ => None

result.map: (namesToArgs, patterns) =>
contribute(_, namesToArgs, patterns)
end unapply

private object NamedTupleUnapplyResultType:
def unapply(tree: Type)(using Context): Option[Type] = tree match
case AppliedType(TypeRef(_, cls), (namedTuple @ defn.NamedTuple(_, _)) :: Nil)
if (cls == ctx.definitions.OptionClass || cls == ctx.definitions.SomeClass) => Some(namedTuple)
case _ => None
Comment on lines +68 to +72
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this used?


private def unapplyResultNamesToTypes(tree: Tree)(using Context): Map[Name, Type] =
tree.tpe.widenDealias.finalResultType match
// result type is named tuple, we can directly extract names
case AppliedType(TypeRef(_, cls), (namedTuple @ defn.NamedTuple(_, _)) :: Nil)
if (cls == ctx.definitions.OptionClass || cls == ctx.definitions.SomeClass) =>
namedTuple.namedTupleElementTypes.toMap
// unapplies generated for case classes have synthetic names and result type is not a named tuple
case _ if tree.symbol.flags.is(Flags.Synthetic) =>
val apply = tree.symbol.owner.info.member(nme.apply)
val maybeApplied = tree match
// The check for case flag is necessary to filter introduced type bounds T$1..n
case tpeApply @ TypeApply(_, args) if !args.exists(_.symbol.flags.is(Flags.Case)) =>
apply.info.appliedTo(args.map(_.tpe))
case _ => apply.info
Comment on lines +83 to +87
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you simplify and always do:

apply.info.appliedTo(args.map(_.tpe))


val unapplyParamList = maybeApplied.paramNamess.indexWhere(_.forall(_.isTermName))
if unapplyParamList < 0 then Map.empty
else
val paramNames= maybeApplied.paramNamess(unapplyParamList)
val paramInfos = maybeApplied.paramInfoss(unapplyParamList)
(paramNames zip paramInfos).toMap
case _ => Map.empty // we can't help complete non synthetic non named tuple extractors

def contribute(
completionPos: CompletionPos,
namesToArgs: Map[Name, Type],
patterns: List[untpd.Tree]
)(using Context): List[CompletionValue] =
val usedNames = patterns.collect:
case untpd.NamedArg(name, _) => name.asTermName

val remainingParams = namesToArgs -- usedNames
remainingParams
.toList
.filter: (name, _) =>
CompletionFuzzy.matchesSubCharacters(completionPos.query, name.toString)
.map: (name, tpe) =>
CompletionValue.NamedArg(name.show + " = ", tpe, NoDenotation)

Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ import scala.meta.internal.metals.CompilerOffsetParams
import scala.meta.pc.OffsetParams
import scala.concurrent.Future
import scala.concurrent.Await
import scala.meta.pc.VirtualFileParams
import scala.concurrent.duration.*

import java.util.Collections
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ import dotty.tools.pc.ScalaPresentationCompiler
import scala.meta.internal.mtags.CommonMtagsEnrichments.*

import org.junit.Test
import org.junit.Ignore

class InferExpectedTypeSuite extends BasePCSuite:
def check(
Expand Down
Loading
Loading