Skip to content

IDE: Support textDocument/signatureHelp #5395

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

Merged
merged 4 commits into from
Nov 13, 2018
Merged
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
Original file line number Diff line number Diff line change
Expand Up @@ -140,7 +140,8 @@ public enum ErrorMessageID {
TraitCompanionWithMutableStaticID,
LazyStaticFieldID,
StaticOverridingNonStaticMembersID,
OverloadInRefinementID
OverloadInRefinementID,
NoMatchingOverloadID
;

public int errorNumber() {
Expand Down
15 changes: 13 additions & 2 deletions compiler/src/dotty/tools/dotc/reporting/diagnostic/messages.scala
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import dotty.tools.dotc.ast.Trees
import dotty.tools.dotc.config.ScalaVersion
import dotty.tools.dotc.core.Flags._
import dotty.tools.dotc.core.SymDenotations.SymDenotation
import dotty.tools.dotc.typer.ErrorReporting.Errors
import scala.util.control.NonFatal

object messages {
Expand Down Expand Up @@ -1382,7 +1383,7 @@ object messages {
}

case class AmbiguousOverload(tree: tpd.Tree, alts: List[SingleDenotation], pt: Type)(
err: typer.ErrorReporting.Errors)(
err: Errors)(
implicit ctx: Context)
extends Message(AmbiguousOverloadID) {

Expand Down Expand Up @@ -1463,7 +1464,7 @@ object messages {
}

case class DoesNotConformToBound(tpe: Type, which: String, bound: Type)(
err: typer.ErrorReporting.Errors)(implicit ctx: Context)
err: Errors)(implicit ctx: Context)
extends Message(DoesNotConformToBoundID) {
val msg: String = hl"Type argument ${tpe} does not conform to $which bound $bound ${err.whyNoMatchStr(tpe, bound)}"
val kind: String = "Type Mismatch"
Expand Down Expand Up @@ -2181,4 +2182,14 @@ object messages {
hl"""The refinement `$rsym` introduces an overloaded definition.
|Refinements cannot contain overloaded definitions.""".stripMargin
}

case class NoMatchingOverload(alternatives: List[SingleDenotation], pt: Type)(
err: Errors)(implicit val ctx: Context)
extends Message(NoMatchingOverloadID) {
val msg: String =
hl"""None of the ${err.overloadedAltsStr(alternatives)}
|match ${err.expectedTypeStr(pt)}"""
val kind: String = "Type Mismatch"
val explanation: String = ""
}
}
4 changes: 1 addition & 3 deletions compiler/src/dotty/tools/dotc/typer/Typer.scala
Original file line number Diff line number Diff line change
Expand Up @@ -2234,9 +2234,7 @@ class Typer extends Namer
readaptSimplified(tree.withType(alt))
case Nil =>
def noMatches =
errorTree(tree,
em"""none of the ${err.overloadedAltsStr(altDenots)}
|match ${err.expectedTypeStr(pt)}""")
errorTree(tree, NoMatchingOverload(altDenots, pt)(err))
def hasEmptyParams(denot: SingleDenotation) = denot.info.paramInfoss == ListOfNil
pt match {
case pt: FunProto =>
Expand Down
196 changes: 196 additions & 0 deletions compiler/src/dotty/tools/dotc/util/Signatures.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,196 @@
package dotty.tools.dotc.util

import dotty.tools.dotc.ast.Trees._
import dotty.tools.dotc.ast.tpd
import dotty.tools.dotc.core.Constants.Constant
import dotty.tools.dotc.core.Contexts.Context
import dotty.tools.dotc.core.Denotations.SingleDenotation
import dotty.tools.dotc.core.Flags.Implicit
import dotty.tools.dotc.core.Names.TermName
import dotty.tools.dotc.util.Positions.Position
import dotty.tools.dotc.core.Types.{ErrorType, MethodType, PolyType}
import dotty.tools.dotc.reporting.diagnostic.messages

import scala.collection.JavaConverters._

object Signatures {

/**
* Represent a method signature.
*
* @param name The name of the method
* @param tparams The type parameters and their bounds
* @param paramss The parameter lists of this method
* @param returnType The return type of this method, if this is not a constructor.
* @param doc The documentation for this method.
*/
case class Signature(name: String, tparams: List[String], paramss: List[List[Param]], returnType: Option[String], doc: Option[String] = None) {
}

/**
* Represent a method's parameter.
*
* @param name The name of the parameter
* @param tpe The type of the parameter
* @param doc The documentation of this parameter
* @param isImplicit Is this parameter implicit?
*/
case class Param(name: String, tpe: String, doc: Option[String] = None, isImplicit: Boolean = false) {
def show: String =
s"$name: $tpe"
}

/**
* Extract (current parameter index, function index, functions) out of a method call.
*
* @param path The path to the function application
* @param pos The position of the cursor
* @return A triple containing the index of the parameter being edited, the index of the function
* being called, the list of overloads of this function).
*/
def callInfo(path: List[tpd.Tree], pos: Position)(implicit ctx: Context): (Int, Int, List[SingleDenotation]) = {
path match {
case Apply(fun, params) :: _ =>
val alreadyAppliedCount = Signatures.countParams(fun)
val paramIndex = params.indexWhere(_.pos.contains(pos)) match {
case -1 => (params.length - 1 max 0) + alreadyAppliedCount
case n => n + alreadyAppliedCount
}

val (alternativeIndex, alternatives) = fun.tpe match {
case err: ErrorType =>
val (alternativeIndex, alternatives) = alternativesFromError(err, params)
(alternativeIndex, alternatives)

case _ =>
val funSymbol = fun.symbol
val alternatives = funSymbol.owner.info.member(funSymbol.name).alternatives
val alternativeIndex = alternatives.indexOf(funSymbol.denot) max 0
(alternativeIndex, alternatives)
}

(paramIndex, alternativeIndex, alternatives)

case _ =>
(0, 0, Nil)
}
}

def toSignature(denot: SingleDenotation)(implicit ctx: Context): Option[Signature] = {
val symbol = denot.symbol
val docComment = ParsedComment.docOf(symbol)
val classTree = symbol.topLevelClass.asClass.rootTree
val isImplicit: TermName => Boolean = tpd.defPath(symbol, classTree).lastOption match {
case Some(DefDef(_, _, paramss, _, _)) =>
val flatParams = paramss.flatten
name => flatParams.find(_.name == name).map(_.symbol.is(Implicit)).getOrElse(false)
case _ =>
_ => false
}

denot.info.stripPoly match {
case tpe: MethodType =>
val infos = {
tpe.paramInfoss.zip(tpe.paramNamess).map { case (infos, names) =>
infos.zip(names).map { case (info, name) =>
Signatures.Param(name.show,
info.widenTermRefExpr.show,
docComment.flatMap(_.paramDoc(name)),
isImplicit = isImplicit(name))
}
}
}

val typeParams = denot.info match {
case poly: PolyType =>
poly.paramNames.zip(poly.paramInfos).map { case (x, y) => x.show + y.show }
case _ =>
Nil
}

val (name, returnType) =
if (symbol.isConstructor) (symbol.owner.name.show, None)
else (denot.name.show, Some(tpe.finalResultType.widenTermRefExpr.show))

val signature =
Signatures.Signature(name,
typeParams,
infos,
returnType,
docComment.map(_.mainDoc))

Some(signature)

case other =>
None
}
}

/**
* The number of parameters that are applied in `tree`.
*
* This handles currying, so for an application such as `foo(1, 2)(3)`, the result of
* `countParams` should be 3.
*
* @param tree The tree to inspect.
* @return The number of parameters that are passed.
*/
private def countParams(tree: tpd.Tree): Int = {
tree match {
case Apply(fun, params) => countParams(fun) + params.length
case _ => 0
}
}

/**
* Inspect `err` to determine, if it is an error related to application of an overloaded
* function, what were the possible alternatives.
*
* If several alternatives are found, determines what is the best suited alternatives
* given the parameters `params`: The alternative that has the most formal parameters
* matching the given arguments is chosen.
*
* @param err The error message to inspect.
* @param params The parameters that were given at the call site.
* @return A pair composed of the index of the best alternative (0 if no alternatives
* were found), and the list of alternatives.
*/
private def alternativesFromError(err: ErrorType, params: List[tpd.Tree])(implicit ctx: Context): (Int, List[SingleDenotation]) = {
val alternatives =
err.msg match {
case messages.AmbiguousOverload(_, alternatives, _) =>
alternatives
case messages.NoMatchingOverload(alternatives, _) =>
alternatives
case _ =>
Nil
}

// If the user writes `foo(bar, <cursor>)`, the typer will insert a synthetic
// `null` parameter: `foo(bar, null)`. This may influence what's the "best"
// alternative, so we discard it.
val userParams = params match {
case xs :+ (nul @ Literal(Constant(null))) if nul.pos.isZeroExtent => xs
case _ => params
}
val userParamsTypes = userParams.map(_.tpe)

// Assign a score to each alternative (how many parameters are correct so far), and
// use that to determine what is the current active signature.
val alternativesScores = alternatives.map { alt =>
alt.info.stripPoly match {
case tpe: MethodType =>
userParamsTypes.zip(tpe.paramInfos).takeWhile{ case (t0, t1) => t0 <:< t1 }.size
case _ =>
0
}
}
val bestAlternative =
if (alternativesScores.isEmpty) 0
else alternativesScores.zipWithIndex.maxBy(_._1)._2

(bestAlternative, alternatives)
}

}

Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ import scala.io.Codec
import dotc._
import ast.{Trees, tpd}
import core._, core.Decorators.{sourcePos => _, _}
import Comments._, Contexts._, Flags._, Names._, NameOps._, Symbols._, SymDenotations._, Trees._, Types._
import Comments._, Constants._, Contexts._, Flags._, Names._, NameOps._, Symbols._, SymDenotations._, Trees._, Types._
import classpath.ClassPathEntries
import reporting._, reporting.diagnostic.{Message, MessageContainer, messages}
import typer.Typer
Expand Down Expand Up @@ -198,6 +198,8 @@ class DottyLanguageServer extends LanguageServer
c.setCompletionProvider(new CompletionOptions(
/* resolveProvider = */ false,
/* triggerCharacters = */ List(".").asJava))
c.setSignatureHelpProvider(new SignatureHelpOptions(
/* triggerCharacters = */ List("(").asJava))

// Do most of the initialization asynchronously so that we can return early
// from this method and thus let the client know our capabilities.
Expand Down Expand Up @@ -457,6 +459,22 @@ class DottyLanguageServer extends LanguageServer
implementations.flatten.asJava
}

override def signatureHelp(params: TextDocumentPositionParams) = computeAsync { canceltoken =>

val uri = new URI(params.getTextDocument.getUri)
val driver = driverFor(uri)
implicit val ctx = driver.currentCtx

val pos = sourcePosition(driver, uri, params.getPosition)
val trees = driver.openedTrees(uri)
val path = Interactive.pathTo(trees, pos).dropWhile(!_.isInstanceOf[Apply])

val (paramN, callableN, alternatives) = Signatures.callInfo(path, pos.pos)
val signatureInfos = alternatives.flatMap(Signatures.toSignature)

new SignatureHelp(signatureInfos.map(signatureToSignatureInformation).asJava, callableN, paramN)
}

override def getTextDocumentService: TextDocumentService = this
override def getWorkspaceService: WorkspaceService = this

Expand All @@ -469,7 +487,6 @@ class DottyLanguageServer extends LanguageServer
override def onTypeFormatting(params: DocumentOnTypeFormattingParams) = null
override def resolveCodeLens(params: CodeLens) = null
override def resolveCompletionItem(params: CompletionItem) = null
override def signatureHelp(params: TextDocumentPositionParams) = null

/**
* Find the set of projects that have any of `definitions` on their classpath.
Expand Down Expand Up @@ -511,7 +528,6 @@ class DottyLanguageServer extends LanguageServer
}
}


}

object DottyLanguageServer {
Expand Down Expand Up @@ -765,4 +781,30 @@ object DottyLanguageServer {

location(pos, positionMapper).map(l => new lsp4j.SymbolInformation(name, symbolKind(sym), l, containerName))
}

/** Convert `signature` to a `SignatureInformation` */
def signatureToSignatureInformation(signature: Signatures.Signature): lsp4j.SignatureInformation = {
val paramInfoss = signature.paramss.map(_.map(paramToParameterInformation))
val paramLists = signature.paramss.map { paramList =>
val labels = paramList.map(_.show)
val prefix = if (paramList.exists(_.isImplicit)) "implicit " else ""
labels.mkString(prefix, ", ", "")
}.mkString("(", ")(", ")")
val tparamsLabel = if (signature.tparams.isEmpty) "" else signature.tparams.mkString("[", ", ", "]")
val returnTypeLabel = signature.returnType.map(t => s": $t").getOrElse("")
val label = s"${signature.name}$tparamsLabel$paramLists$returnTypeLabel"
val documentation = signature.doc.map(DottyLanguageServer.hoverContent)
val sig = new lsp4j.SignatureInformation(label)
sig.setParameters(paramInfoss.flatten.asJava)
documentation.foreach(sig.setDocumentation(_))
sig
}

/** Convert `param` to `ParameterInformation` */
private def paramToParameterInformation(param: Signatures.Param): lsp4j.ParameterInformation = {
val documentation = param.doc.map(DottyLanguageServer.hoverContent)
val info = new lsp4j.ParameterInformation(param.show)
documentation.foreach(info.setDocumentation(_))
info
}
}
Loading