-
Notifications
You must be signed in to change notification settings - Fork 1.1k
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
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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 | ||
|
@@ -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
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 = | ||
|
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
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can you simplify and always do:
|
||
|
||
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) | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is this intentionally left as TODO?