Skip to content

Commit

Permalink
Add named pattern completions
Browse files Browse the repository at this point in the history
  • Loading branch information
rochala committed Dec 19, 2024
1 parent 91063dd commit 8aa208f
Show file tree
Hide file tree
Showing 15 changed files with 441 additions and 40 deletions.
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
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 @@ -15,7 +14,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
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,104 @@
package dotty.tools.pc.completions

import dotty.tools.dotc.ast.tpd.*
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 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
.flatMap: selector =>
if selector.tpe.widenDealias.isNamedTupleType then
Some(selector.tpe.widenDealias.namedTupleElementTypes.toMap, unapply.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

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

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[Tree]
)(using Context): List[CompletionValue] =
val usedNames = patterns.collect:
case 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

0 comments on commit 8aa208f

Please sign in to comment.