Skip to content

Commit 7824363

Browse files
authored
Merge pull request #5395 from dotty-staging/topic/ide-signature-help
IDE: Support `textDocument/signatureHelp`
2 parents bdd932f + 25d436b commit 7824363

File tree

8 files changed

+655
-9
lines changed

8 files changed

+655
-9
lines changed

compiler/src/dotty/tools/dotc/reporting/diagnostic/ErrorMessageID.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -140,7 +140,8 @@ public enum ErrorMessageID {
140140
TraitCompanionWithMutableStaticID,
141141
LazyStaticFieldID,
142142
StaticOverridingNonStaticMembersID,
143-
OverloadInRefinementID
143+
OverloadInRefinementID,
144+
NoMatchingOverloadID
144145
;
145146

146147
public int errorNumber() {

compiler/src/dotty/tools/dotc/reporting/diagnostic/messages.scala

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import dotty.tools.dotc.ast.Trees
2323
import dotty.tools.dotc.config.ScalaVersion
2424
import dotty.tools.dotc.core.Flags._
2525
import dotty.tools.dotc.core.SymDenotations.SymDenotation
26+
import dotty.tools.dotc.typer.ErrorReporting.Errors
2627
import scala.util.control.NonFatal
2728

2829
object messages {
@@ -1382,7 +1383,7 @@ object messages {
13821383
}
13831384

13841385
case class AmbiguousOverload(tree: tpd.Tree, alts: List[SingleDenotation], pt: Type)(
1385-
err: typer.ErrorReporting.Errors)(
1386+
err: Errors)(
13861387
implicit ctx: Context)
13871388
extends Message(AmbiguousOverloadID) {
13881389

@@ -1463,7 +1464,7 @@ object messages {
14631464
}
14641465

14651466
case class DoesNotConformToBound(tpe: Type, which: String, bound: Type)(
1466-
err: typer.ErrorReporting.Errors)(implicit ctx: Context)
1467+
err: Errors)(implicit ctx: Context)
14671468
extends Message(DoesNotConformToBoundID) {
14681469
val msg: String = hl"Type argument ${tpe} does not conform to $which bound $bound ${err.whyNoMatchStr(tpe, bound)}"
14691470
val kind: String = "Type Mismatch"
@@ -2181,4 +2182,14 @@ object messages {
21812182
hl"""The refinement `$rsym` introduces an overloaded definition.
21822183
|Refinements cannot contain overloaded definitions.""".stripMargin
21832184
}
2185+
2186+
case class NoMatchingOverload(alternatives: List[SingleDenotation], pt: Type)(
2187+
err: Errors)(implicit val ctx: Context)
2188+
extends Message(NoMatchingOverloadID) {
2189+
val msg: String =
2190+
hl"""None of the ${err.overloadedAltsStr(alternatives)}
2191+
|match ${err.expectedTypeStr(pt)}"""
2192+
val kind: String = "Type Mismatch"
2193+
val explanation: String = ""
2194+
}
21842195
}

compiler/src/dotty/tools/dotc/typer/Typer.scala

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2234,9 +2234,7 @@ class Typer extends Namer
22342234
readaptSimplified(tree.withType(alt))
22352235
case Nil =>
22362236
def noMatches =
2237-
errorTree(tree,
2238-
em"""none of the ${err.overloadedAltsStr(altDenots)}
2239-
|match ${err.expectedTypeStr(pt)}""")
2237+
errorTree(tree, NoMatchingOverload(altDenots, pt)(err))
22402238
def hasEmptyParams(denot: SingleDenotation) = denot.info.paramInfoss == ListOfNil
22412239
pt match {
22422240
case pt: FunProto =>
Lines changed: 196 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,196 @@
1+
package dotty.tools.dotc.util
2+
3+
import dotty.tools.dotc.ast.Trees._
4+
import dotty.tools.dotc.ast.tpd
5+
import dotty.tools.dotc.core.Constants.Constant
6+
import dotty.tools.dotc.core.Contexts.Context
7+
import dotty.tools.dotc.core.Denotations.SingleDenotation
8+
import dotty.tools.dotc.core.Flags.Implicit
9+
import dotty.tools.dotc.core.Names.TermName
10+
import dotty.tools.dotc.util.Positions.Position
11+
import dotty.tools.dotc.core.Types.{ErrorType, MethodType, PolyType}
12+
import dotty.tools.dotc.reporting.diagnostic.messages
13+
14+
import scala.collection.JavaConverters._
15+
16+
object Signatures {
17+
18+
/**
19+
* Represent a method signature.
20+
*
21+
* @param name The name of the method
22+
* @param tparams The type parameters and their bounds
23+
* @param paramss The parameter lists of this method
24+
* @param returnType The return type of this method, if this is not a constructor.
25+
* @param doc The documentation for this method.
26+
*/
27+
case class Signature(name: String, tparams: List[String], paramss: List[List[Param]], returnType: Option[String], doc: Option[String] = None) {
28+
}
29+
30+
/**
31+
* Represent a method's parameter.
32+
*
33+
* @param name The name of the parameter
34+
* @param tpe The type of the parameter
35+
* @param doc The documentation of this parameter
36+
* @param isImplicit Is this parameter implicit?
37+
*/
38+
case class Param(name: String, tpe: String, doc: Option[String] = None, isImplicit: Boolean = false) {
39+
def show: String =
40+
s"$name: $tpe"
41+
}
42+
43+
/**
44+
* Extract (current parameter index, function index, functions) out of a method call.
45+
*
46+
* @param path The path to the function application
47+
* @param pos The position of the cursor
48+
* @return A triple containing the index of the parameter being edited, the index of the function
49+
* being called, the list of overloads of this function).
50+
*/
51+
def callInfo(path: List[tpd.Tree], pos: Position)(implicit ctx: Context): (Int, Int, List[SingleDenotation]) = {
52+
path match {
53+
case Apply(fun, params) :: _ =>
54+
val alreadyAppliedCount = Signatures.countParams(fun)
55+
val paramIndex = params.indexWhere(_.pos.contains(pos)) match {
56+
case -1 => (params.length - 1 max 0) + alreadyAppliedCount
57+
case n => n + alreadyAppliedCount
58+
}
59+
60+
val (alternativeIndex, alternatives) = fun.tpe match {
61+
case err: ErrorType =>
62+
val (alternativeIndex, alternatives) = alternativesFromError(err, params)
63+
(alternativeIndex, alternatives)
64+
65+
case _ =>
66+
val funSymbol = fun.symbol
67+
val alternatives = funSymbol.owner.info.member(funSymbol.name).alternatives
68+
val alternativeIndex = alternatives.indexOf(funSymbol.denot) max 0
69+
(alternativeIndex, alternatives)
70+
}
71+
72+
(paramIndex, alternativeIndex, alternatives)
73+
74+
case _ =>
75+
(0, 0, Nil)
76+
}
77+
}
78+
79+
def toSignature(denot: SingleDenotation)(implicit ctx: Context): Option[Signature] = {
80+
val symbol = denot.symbol
81+
val docComment = ParsedComment.docOf(symbol)
82+
val classTree = symbol.topLevelClass.asClass.rootTree
83+
val isImplicit: TermName => Boolean = tpd.defPath(symbol, classTree).lastOption match {
84+
case Some(DefDef(_, _, paramss, _, _)) =>
85+
val flatParams = paramss.flatten
86+
name => flatParams.find(_.name == name).map(_.symbol.is(Implicit)).getOrElse(false)
87+
case _ =>
88+
_ => false
89+
}
90+
91+
denot.info.stripPoly match {
92+
case tpe: MethodType =>
93+
val infos = {
94+
tpe.paramInfoss.zip(tpe.paramNamess).map { case (infos, names) =>
95+
infos.zip(names).map { case (info, name) =>
96+
Signatures.Param(name.show,
97+
info.widenTermRefExpr.show,
98+
docComment.flatMap(_.paramDoc(name)),
99+
isImplicit = isImplicit(name))
100+
}
101+
}
102+
}
103+
104+
val typeParams = denot.info match {
105+
case poly: PolyType =>
106+
poly.paramNames.zip(poly.paramInfos).map { case (x, y) => x.show + y.show }
107+
case _ =>
108+
Nil
109+
}
110+
111+
val (name, returnType) =
112+
if (symbol.isConstructor) (symbol.owner.name.show, None)
113+
else (denot.name.show, Some(tpe.finalResultType.widenTermRefExpr.show))
114+
115+
val signature =
116+
Signatures.Signature(name,
117+
typeParams,
118+
infos,
119+
returnType,
120+
docComment.map(_.mainDoc))
121+
122+
Some(signature)
123+
124+
case other =>
125+
None
126+
}
127+
}
128+
129+
/**
130+
* The number of parameters that are applied in `tree`.
131+
*
132+
* This handles currying, so for an application such as `foo(1, 2)(3)`, the result of
133+
* `countParams` should be 3.
134+
*
135+
* @param tree The tree to inspect.
136+
* @return The number of parameters that are passed.
137+
*/
138+
private def countParams(tree: tpd.Tree): Int = {
139+
tree match {
140+
case Apply(fun, params) => countParams(fun) + params.length
141+
case _ => 0
142+
}
143+
}
144+
145+
/**
146+
* Inspect `err` to determine, if it is an error related to application of an overloaded
147+
* function, what were the possible alternatives.
148+
*
149+
* If several alternatives are found, determines what is the best suited alternatives
150+
* given the parameters `params`: The alternative that has the most formal parameters
151+
* matching the given arguments is chosen.
152+
*
153+
* @param err The error message to inspect.
154+
* @param params The parameters that were given at the call site.
155+
* @return A pair composed of the index of the best alternative (0 if no alternatives
156+
* were found), and the list of alternatives.
157+
*/
158+
private def alternativesFromError(err: ErrorType, params: List[tpd.Tree])(implicit ctx: Context): (Int, List[SingleDenotation]) = {
159+
val alternatives =
160+
err.msg match {
161+
case messages.AmbiguousOverload(_, alternatives, _) =>
162+
alternatives
163+
case messages.NoMatchingOverload(alternatives, _) =>
164+
alternatives
165+
case _ =>
166+
Nil
167+
}
168+
169+
// If the user writes `foo(bar, <cursor>)`, the typer will insert a synthetic
170+
// `null` parameter: `foo(bar, null)`. This may influence what's the "best"
171+
// alternative, so we discard it.
172+
val userParams = params match {
173+
case xs :+ (nul @ Literal(Constant(null))) if nul.pos.isZeroExtent => xs
174+
case _ => params
175+
}
176+
val userParamsTypes = userParams.map(_.tpe)
177+
178+
// Assign a score to each alternative (how many parameters are correct so far), and
179+
// use that to determine what is the current active signature.
180+
val alternativesScores = alternatives.map { alt =>
181+
alt.info.stripPoly match {
182+
case tpe: MethodType =>
183+
userParamsTypes.zip(tpe.paramInfos).takeWhile{ case (t0, t1) => t0 <:< t1 }.size
184+
case _ =>
185+
0
186+
}
187+
}
188+
val bestAlternative =
189+
if (alternativesScores.isEmpty) 0
190+
else alternativesScores.zipWithIndex.maxBy(_._1)._2
191+
192+
(bestAlternative, alternatives)
193+
}
194+
195+
}
196+

language-server/src/dotty/tools/languageserver/DottyLanguageServer.scala

Lines changed: 45 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ import scala.io.Codec
1919
import dotc._
2020
import ast.{Trees, tpd}
2121
import core._, core.Decorators.{sourcePos => _, _}
22-
import Comments._, Contexts._, Flags._, Names._, NameOps._, Symbols._, SymDenotations._, Trees._, Types._
22+
import Comments._, Constants._, Contexts._, Flags._, Names._, NameOps._, Symbols._, SymDenotations._, Trees._, Types._
2323
import classpath.ClassPathEntries
2424
import reporting._, reporting.diagnostic.{Message, MessageContainer, messages}
2525
import typer.Typer
@@ -198,6 +198,8 @@ class DottyLanguageServer extends LanguageServer
198198
c.setCompletionProvider(new CompletionOptions(
199199
/* resolveProvider = */ false,
200200
/* triggerCharacters = */ List(".").asJava))
201+
c.setSignatureHelpProvider(new SignatureHelpOptions(
202+
/* triggerCharacters = */ List("(").asJava))
201203

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

462+
override def signatureHelp(params: TextDocumentPositionParams) = computeAsync { canceltoken =>
463+
464+
val uri = new URI(params.getTextDocument.getUri)
465+
val driver = driverFor(uri)
466+
implicit val ctx = driver.currentCtx
467+
468+
val pos = sourcePosition(driver, uri, params.getPosition)
469+
val trees = driver.openedTrees(uri)
470+
val path = Interactive.pathTo(trees, pos).dropWhile(!_.isInstanceOf[Apply])
471+
472+
val (paramN, callableN, alternatives) = Signatures.callInfo(path, pos.pos)
473+
val signatureInfos = alternatives.flatMap(Signatures.toSignature)
474+
475+
new SignatureHelp(signatureInfos.map(signatureToSignatureInformation).asJava, callableN, paramN)
476+
}
477+
460478
override def getTextDocumentService: TextDocumentService = this
461479
override def getWorkspaceService: WorkspaceService = this
462480

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

474491
/**
475492
* Find the set of projects that have any of `definitions` on their classpath.
@@ -511,7 +528,6 @@ class DottyLanguageServer extends LanguageServer
511528
}
512529
}
513530

514-
515531
}
516532

517533
object DottyLanguageServer {
@@ -765,4 +781,30 @@ object DottyLanguageServer {
765781

766782
location(pos, positionMapper).map(l => new lsp4j.SymbolInformation(name, symbolKind(sym), l, containerName))
767783
}
784+
785+
/** Convert `signature` to a `SignatureInformation` */
786+
def signatureToSignatureInformation(signature: Signatures.Signature): lsp4j.SignatureInformation = {
787+
val paramInfoss = signature.paramss.map(_.map(paramToParameterInformation))
788+
val paramLists = signature.paramss.map { paramList =>
789+
val labels = paramList.map(_.show)
790+
val prefix = if (paramList.exists(_.isImplicit)) "implicit " else ""
791+
labels.mkString(prefix, ", ", "")
792+
}.mkString("(", ")(", ")")
793+
val tparamsLabel = if (signature.tparams.isEmpty) "" else signature.tparams.mkString("[", ", ", "]")
794+
val returnTypeLabel = signature.returnType.map(t => s": $t").getOrElse("")
795+
val label = s"${signature.name}$tparamsLabel$paramLists$returnTypeLabel"
796+
val documentation = signature.doc.map(DottyLanguageServer.hoverContent)
797+
val sig = new lsp4j.SignatureInformation(label)
798+
sig.setParameters(paramInfoss.flatten.asJava)
799+
documentation.foreach(sig.setDocumentation(_))
800+
sig
801+
}
802+
803+
/** Convert `param` to `ParameterInformation` */
804+
private def paramToParameterInformation(param: Signatures.Param): lsp4j.ParameterInformation = {
805+
val documentation = param.doc.map(DottyLanguageServer.hoverContent)
806+
val info = new lsp4j.ParameterInformation(param.show)
807+
documentation.foreach(info.setDocumentation(_))
808+
info
809+
}
768810
}

0 commit comments

Comments
 (0)