Skip to content

Commit a94de7f

Browse files
committed
IDE: Support textDocument/signatureHelp
When the user starts writing a function application, the language client requests `textDocument/signatureHelp`. This is used to display help associated with the function application that is being written.
1 parent 52e6265 commit a94de7f

File tree

5 files changed

+605
-3
lines changed

5 files changed

+605
-3
lines changed

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

Lines changed: 68 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,71 @@ 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+
474+
val signatureInfos = alternatives.flatMap { denot =>
475+
val symbol = denot.symbol
476+
val docComment = ParsedComment.docOf(symbol)
477+
val classTree = symbol.topLevelClass.asClass.rootTree
478+
val isImplicit: TermName => Boolean = tpd.defPath(symbol, classTree).lastOption match {
479+
case Some(DefDef(_, _, paramss, _, _)) =>
480+
val flatParams = paramss.flatten
481+
name => flatParams.find(_.name == name).map(_.symbol.is(Implicit)).getOrElse(false)
482+
case _ =>
483+
_ => false
484+
}
485+
486+
denot.info.stripPoly match {
487+
case tpe: MethodType =>
488+
val infos = {
489+
tpe.paramInfoss.zip(tpe.paramNamess).map { (infos, names) =>
490+
infos.zip(names).map { (info, name) =>
491+
Signatures.Param(name.show,
492+
info.widenTermRefExpr.show,
493+
docComment.flatMap(_.paramDoc(name)),
494+
isImplicit = isImplicit(name))
495+
}
496+
}
497+
}
498+
499+
val typeParams = denot.info match {
500+
case poly: PolyType =>
501+
poly.paramNames.zip(poly.paramInfos).map((x, y) => x.show + y.show)
502+
case _ =>
503+
Nil
504+
}
505+
506+
val (name, returnType) =
507+
if (symbol.isConstructor) (symbol.owner.name.show, None)
508+
else (denot.name.show, Some(tpe.finalResultType.widenTermRefExpr.show))
509+
510+
val signature =
511+
Signatures.Signature(name,
512+
typeParams,
513+
infos,
514+
returnType,
515+
docComment.map(_.mainDoc))
516+
517+
Some(signature)
518+
519+
case other =>
520+
None
521+
}
522+
}
523+
524+
new SignatureHelp(signatureInfos.map(_.toSignatureInformation).asJava, callableN, paramN)
525+
}
526+
460527
override def getTextDocumentService: TextDocumentService = this
461528
override def getWorkspaceService: WorkspaceService = this
462529

@@ -469,7 +536,6 @@ class DottyLanguageServer extends LanguageServer
469536
override def onTypeFormatting(params: DocumentOnTypeFormattingParams) = null
470537
override def resolveCodeLens(params: CodeLens) = null
471538
override def resolveCompletionItem(params: CompletionItem) = null
472-
override def signatureHelp(params: TextDocumentPositionParams) = null
473539

474540
/**
475541
* Find the set of projects that have any of `definitions` on their classpath.
@@ -511,7 +577,6 @@ class DottyLanguageServer extends LanguageServer
511577
}
512578
}
513579

514-
515580
}
516581

517582
object DottyLanguageServer {
Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,143 @@
1+
package dotty.tools.languageserver
2+
3+
import dotty.tools.dotc.ast.tpd._
4+
import dotty.tools.dotc.core.Constants.Constant
5+
import dotty.tools.dotc.core.Contexts.Context
6+
import dotty.tools.dotc.core.Denotations.SingleDenotation
7+
import dotty.tools.dotc.util.Positions.Position
8+
import dotty.tools.dotc.core.Types.{ErrorType, MethodType}
9+
import dotty.tools.dotc.reporting.diagnostic.messages
10+
11+
import org.eclipse.lsp4j.{ParameterInformation, SignatureInformation}
12+
13+
import scala.collection.JavaConverters._
14+
15+
object Signatures {
16+
17+
def callInfo(path: List[Tree], pos: Position)(implicit ctx: Context): (Int, Int, List[SingleDenotation]) = {
18+
path match {
19+
case Apply(fun, params) :: _ =>
20+
val alreadyAppliedCount = Signatures.countParams(fun)
21+
val paramIndex = params.indexWhere(_.pos.contains(pos)) match {
22+
case -1 => (params.length - 1 max 0) + alreadyAppliedCount
23+
case n => n + alreadyAppliedCount
24+
}
25+
26+
val (alternativeIndex, alternatives) = fun.tpe match {
27+
case err: ErrorType =>
28+
val (alternativeIndex, alternatives) = alternativesFromError(err, params)
29+
(alternativeIndex, alternatives)
30+
31+
case _ =>
32+
val funSymbol = fun.symbol
33+
val alternatives = funSymbol.owner.info.member(funSymbol.name).alternatives
34+
val alternativeIndex = alternatives.indexOf(funSymbol.denot) max 0
35+
(alternativeIndex, alternatives)
36+
}
37+
38+
(paramIndex, alternativeIndex, alternatives)
39+
40+
case _ =>
41+
(0, 0, Nil)
42+
}
43+
}
44+
45+
/**
46+
* The number of parameters that are applied in `tree`.
47+
*
48+
* This handles currying, so for an application such as `foo(1, 2)(3)`, the result of
49+
* `countParams` should be 3.
50+
*
51+
* @param tree The tree to inspect.
52+
* @return The number of parameters that are passed.
53+
*/
54+
private def countParams(tree: Tree): Int = {
55+
tree match {
56+
case Apply(fun, params) => countParams(fun) + params.length
57+
case _ => 0
58+
}
59+
}
60+
61+
/**
62+
* Inspect `err` to determine, if it is an error related to application of an overloaded
63+
* function, what were the possible alternatives.
64+
*
65+
* If several alternatives are found, determines what is the best suited alternatives
66+
* given the parameters `params`: The alternative that has the most formal parameters
67+
* matching the given arguments is chosen.
68+
*
69+
* @param err The error message to inspect.
70+
* @param params The parameters that were given at the call site.
71+
* @return A pair composed of the index of the best alternative (0 if no alternatives
72+
* were found), and the list of alternatives.
73+
*/
74+
private def alternativesFromError(err: ErrorType, params: List[Tree])(implicit ctx: Context): (Int, List[SingleDenotation]) = {
75+
val alternatives =
76+
err.msg match {
77+
case messages.AmbiguousOverload(_, alternatives, _) =>
78+
alternatives
79+
case messages.NoMatchingOverload(alternatives, _) =>
80+
alternatives
81+
case _ =>
82+
Nil
83+
}
84+
85+
// If the user writes `foo(bar, <cursor>)`, the typer will insert a synthetic
86+
// `null` parameter: `foo(bar, null)`. This may influence what's the "best"
87+
// alternative, so we discard it.
88+
val userParams = params match {
89+
case xs :+ (nul @ Literal(Constant(null))) if nul.pos.isZeroExtent => xs
90+
case _ => params
91+
}
92+
val userParamsTypes = userParams.map(_.tpe)
93+
94+
// Assign a score to each alternative (how many parameters are correct so far), and
95+
// use that to determine what is the current active signature.
96+
val alternativesScores = alternatives.map { alt =>
97+
alt.info.stripPoly match {
98+
case tpe: MethodType =>
99+
userParamsTypes.zip(tpe.paramInfos).takeWhile(_ <:< _).size
100+
case _ =>
101+
0
102+
}
103+
}
104+
val bestAlternative =
105+
if (alternativesScores.isEmpty) 0
106+
else alternativesScores.zipWithIndex.maxBy(_._1)._2
107+
108+
(bestAlternative, alternatives)
109+
}
110+
111+
case class Signature(name: String, tparams: List[String], paramss: List[List[Param]], returnType: Option[String], doc: Option[String] = None) {
112+
def toSignatureInformation: SignatureInformation = {
113+
val paramInfoss = paramss.map(_.map(_.toParameterInformation))
114+
val paramLists = paramss.map { paramList =>
115+
val labels = paramList.map(_.show)
116+
val prefix = if (paramList.exists(_.isImplicit)) "implicit " else ""
117+
labels.mkString(prefix, ", ", "")
118+
}.mkString("(", ")(", ")")
119+
val tparamsLabel = if (tparams.isEmpty) "" else tparams.mkString("[", ", ", "]")
120+
val returnTypeLabel = returnType.map(t => s": $t").getOrElse("")
121+
val label = s"$name$tparamsLabel$paramLists$returnTypeLabel"
122+
val documentation = doc.map(DottyLanguageServer.hoverContent)
123+
val signature = new SignatureInformation(label)
124+
signature.setParameters(paramInfoss.flatten.asJava)
125+
documentation.foreach(signature.setDocumentation(_))
126+
signature
127+
}
128+
}
129+
130+
case class Param(name: String, tpe: String, doc: Option[String] = None, isImplicit: Boolean = false) {
131+
132+
def toParameterInformation: ParameterInformation = {
133+
val label = s"$name: $tpe"
134+
val documentation = doc.map(DottyLanguageServer.hoverContent)
135+
val info = new ParameterInformation(label)
136+
documentation.foreach(info.setDocumentation(_))
137+
info
138+
}
139+
140+
def show: String =
141+
s"$name: $tpe"
142+
}
143+
}

0 commit comments

Comments
 (0)