Skip to content

Commit a079acb

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 3f1b92c commit a079acb

File tree

5 files changed

+605
-2
lines changed

5 files changed

+605
-2
lines changed

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

Lines changed: 68 additions & 2 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
@@ -171,6 +171,8 @@ class DottyLanguageServer extends LanguageServer
171171
c.setCompletionProvider(new CompletionOptions(
172172
/* resolveProvider = */ false,
173173
/* triggerCharacters = */ List(".").asJava))
174+
c.setSignatureHelpProvider(new SignatureHelpOptions(
175+
/* triggerCharacters = */ List("(").asJava))
174176

175177
// Do most of the initialization asynchronously so that we can return early
176178
// from this method and thus let the client know our capabilities.
@@ -389,6 +391,71 @@ class DottyLanguageServer extends LanguageServer
389391
}.asJava
390392
}
391393

394+
override def signatureHelp(params: TextDocumentPositionParams) = computeAsync { canceltoken =>
395+
396+
val uri = new URI(params.getTextDocument.getUri)
397+
val driver = driverFor(uri)
398+
implicit val ctx = driver.currentCtx
399+
400+
val pos = sourcePosition(driver, uri, params.getPosition)
401+
val trees = driver.openedTrees(uri)
402+
val path = Interactive.pathTo(trees, pos).dropWhile(!_.isInstanceOf[Apply])
403+
404+
val (paramN, callableN, alternatives) = Signatures.callInfo(path, pos.pos)
405+
406+
val signatureInfos = alternatives.flatMap { denot =>
407+
val symbol = denot.symbol
408+
val docComment = ParsedComment.docOf(symbol)
409+
val classTree = symbol.topLevelClass.asClass.rootTree
410+
val isImplicit: TermName => Boolean = tpd.defPath(symbol, classTree).lastOption match {
411+
case Some(DefDef(_, _, paramss, _, _)) =>
412+
val flatParams = paramss.flatten
413+
name => flatParams.find(_.name == name).map(_.symbol.is(Implicit)).getOrElse(false)
414+
case _ =>
415+
_ => false
416+
}
417+
418+
denot.info.stripPoly match {
419+
case tpe: MethodType =>
420+
val infos = {
421+
tpe.paramInfoss.zip(tpe.paramNamess).map { (infos, names) =>
422+
infos.zip(names).map { (info, name) =>
423+
Signatures.Param(name.show,
424+
info.widenTermRefExpr.show,
425+
docComment.flatMap(_.paramDoc(name)),
426+
isImplicit = isImplicit(name))
427+
}
428+
}
429+
}
430+
431+
val typeParams = denot.info match {
432+
case poly: PolyType =>
433+
poly.paramNames.zip(poly.paramInfos).map((x, y) => x.show + y.show)
434+
case _ =>
435+
Nil
436+
}
437+
438+
val (name, returnType) =
439+
if (symbol.isConstructor) (symbol.owner.name.show, None)
440+
else (denot.name.show, Some(tpe.finalResultType.widenTermRefExpr.show))
441+
442+
val signature =
443+
Signatures.Signature(name,
444+
typeParams,
445+
infos,
446+
returnType,
447+
docComment.map(_.mainDoc))
448+
449+
Some(signature)
450+
451+
case other =>
452+
None
453+
}
454+
}
455+
456+
new SignatureHelp(signatureInfos.map(_.toSignatureInformation).asJava, callableN, paramN)
457+
}
458+
392459
override def getTextDocumentService: TextDocumentService = this
393460
override def getWorkspaceService: WorkspaceService = this
394461

@@ -401,7 +468,6 @@ class DottyLanguageServer extends LanguageServer
401468
override def onTypeFormatting(params: DocumentOnTypeFormattingParams) = null
402469
override def resolveCodeLens(params: CodeLens) = null
403470
override def resolveCompletionItem(params: CompletionItem) = null
404-
override def signatureHelp(params: TextDocumentPositionParams) = null
405471
}
406472

407473
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)