Skip to content

Commit

Permalink
Implement textDocument/signatureHelper.
Browse files Browse the repository at this point in the history
This feature is only triggered when opening a parentheses at a function
call-site. The box disappears as soon as you write the first parameter.
We reuse the completion mechanism to find the method symbol and extract
the parameter lists.
  • Loading branch information
olafurpg committed Nov 22, 2017
1 parent 713db04 commit fe14433
Show file tree
Hide file tree
Showing 6 changed files with 163 additions and 35 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ class LanguageServer(inStream: InputStream, outStream: OutputStream) extends Laz
InitializeResult(initialize(pid, rootPath, capabilities))
case ("textDocument/completion", TextDocumentCompletionRequest(TextDocumentPositionParams(textDocument, position))) =>
completionRequest(textDocument, position)
case ("textDocument/signatureHelp", TextDocumentSignatureHelpRequest(TextDocumentPositionParams(textDocument, position))) =>
signatureHelpRequest(textDocument, position)
case ("textDocument/definition", TextDocumentDefinitionRequest(TextDocumentPositionParams(textDocument, position))) =>
gotoDefinitionRequest(textDocument, position)
case ("textDocument/hover", TextDocumentHoverRequest(TextDocumentPositionParams(textDocument, position))) =>
Expand Down Expand Up @@ -78,6 +80,10 @@ class LanguageServer(inStream: InputStream, outStream: OutputStream) extends Laz
ServerCapabilities(completionProvider = Some(CompletionOptions(false, Seq("."))))
}

def signatureHelpRequest(textDocument: TextDocumentIdentifier, position: Position): SignatureHelp = {
SignatureHelp(Nil, None, None)
}

def completionRequest(textDocument: TextDocumentIdentifier, position: Position): ResultResponse = {
CompletionList(isIncomplete = false, Nil)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,7 @@ object MessageActionItem {
case class TextDocumentPositionParams(textDocument: TextDocumentIdentifier, position: Position)
case class DocumentSymbolParams(textDocument: TextDocumentIdentifier) extends ServerCommand

case class TextDocumentSignatureHelpRequest(params: TextDocumentPositionParams) extends ServerCommand
case class TextDocumentCompletionRequest(params: TextDocumentPositionParams) extends ServerCommand
case class TextDocumentDefinitionRequest(params: TextDocumentPositionParams) extends ServerCommand
case class TextDocumentHoverRequest(params: TextDocumentPositionParams) extends ServerCommand
Expand All @@ -195,6 +196,7 @@ object ServerCommand extends CommandCompanion[ServerCommand] {
"initialize" -> Json.format[InitializeParams],
"shutdown" -> Shutdown.format,
"textDocument/completion" -> valueFormat(TextDocumentCompletionRequest)(_.params),
"textDocument/signatureHelp" -> valueFormat(TextDocumentSignatureHelpRequest)(_.params),
"textDocument/definition" -> valueFormat(TextDocumentDefinitionRequest)(_.params),
"textDocument/hover" -> valueFormat(TextDocumentHoverRequest)(_.params),
"textDocument/documentSymbol" -> Json.format[DocumentSymbolParams],
Expand Down Expand Up @@ -271,6 +273,7 @@ object ResultResponse extends ResponseCompanion[Any] {
override val ResponseFormats = Message.MessageFormats(
"initialize" -> Json.format[InitializeResult],
"textDocument/completion" -> Json.format[CompletionList],
"textDocument/signatureHelp" -> Json.format[SignatureHelp],
"textDocument/definition" -> valueFormat(DefinitionResult)(_.params),
"textDocument/hover" -> Json.format[Hover],
"textDocument/documentSymbol" -> valueFormat(DocumentSymbolResult)(_.params),
Expand Down
9 changes: 9 additions & 0 deletions languageserver/src/main/scala/langserver/types/types.scala
Original file line number Diff line number Diff line change
Expand Up @@ -146,8 +146,14 @@ object MarkedString {


case class ParameterInformation(label: String, documentation: Option[String])
object ParameterInformation {
implicit val format: OFormat[ParameterInformation] = Json.format[ParameterInformation]
}

case class SignatureInformation(label: String, documentation: Option[String], parameters: Seq[ParameterInformation])
object SignatureInformation {
implicit val format: OFormat[SignatureInformation] = Json.format[SignatureInformation]
}

/**
* Signature help represents the signature of something
Expand All @@ -163,6 +169,9 @@ case class SignatureHelp(

/** The active parameter of the active signature. */
activeParameter: Option[Int])
object SignatureHelp {
implicit var format: OFormat[SignatureHelp] = Json.format[SignatureHelp]
}

/**
* Value-object that contains additional information when
Expand Down
132 changes: 97 additions & 35 deletions metaserver/src/main/scala/scala/meta/languageserver/Compiler.scala
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,13 @@ import org.langmeta.internal.semanticdb.schema.Database
import org.langmeta.io.AbsolutePath
import org.langmeta.internal.semanticdb.schema.Document
import ScalametaLanguageServer.cacheDirectory
import scala.reflect.internal.util.Position
import langserver.types.SignatureHelp
import langserver.types.SymbolInformation
import Compiler.ask
import scala.collection.immutable
import langserver.types.ParameterInformation
import langserver.types.SignatureInformation

class Compiler(
serverConfig: ServerConfig,
Expand Down Expand Up @@ -47,41 +54,76 @@ class Compiler(
)
}

def signatureHelp(
path: AbsolutePath,
line: Int,
column: Int
): SignatureHelp = {
getCompiler(path, line, column - 1).fold(noSignatureHelp) {
case (compiler, position) =>
logger.info(s"Signature help at $path:$line:$column")
val signatureInformations = for {
member <- compiler.completionsAt(position).matchingResults().distinct
if member.sym.isMethod
} yield {
val sym = member.sym.asMethod
val parameterInfos = sym.paramLists.headOption.map { params =>
params.map { param =>
ParameterInformation(
label = param.nameString,
documentation =
Some(s"${param.nameString}: ${param.info.toLongString}")
)
}
}
SignatureInformation(
label = sym.nameString,
documentation = Some(sym.info.toLongString),
parameters = parameterInfos.getOrElse(Nil)
)
}
SignatureHelp(signatureInformations, None, None)
}
}

def autocomplete(
path: AbsolutePath,
line: Int,
column: Int
): List[(String, String)] = {
logger.info(s"Completion request at $path:$line:$column")
val code = buffers.read(path)
val offset = lineColumnToOffset(code, line, column)
compilerByPath.get(path).fold(noCompletions) { compiler =>
compiler.reporter.reset()
val source = code.take(offset) + "_CURSOR_" + code.drop(offset)
val unit = compiler.newCompilationUnit(source, path.toString())
val richUnit = new compiler.RichCompilationUnit(unit.source)
compiler.unitOfFile(richUnit.source.file) = richUnit
val position = richUnit.position(offset)
logger.info(s"Completion request at position $position")
val results = compiler.completionsAt(position).matchingResults()
results
.map(r => (r.sym.signatureString, r.symNameDropLocal.decoded))
.distinct
getCompiler(path, line, column).fold(noCompletions) {
case (compiler, position) =>
logger.info(s"Completion request at position $position")
val results = compiler.completionsAt(position).matchingResults()
results
.map(r => (r.sym.signatureString, r.symNameDropLocal.decoded))
.distinct
}
}

def typeAt(path: AbsolutePath, line: Int, column: Int): Option[String] = {
getCompiler(path, line, column).flatMap {
case (compiler, position) =>
val response = ask[compiler.Tree](r => compiler.askTypeAt(position, r))
val typedTree = response.get.swap
typedTree.toOption.flatMap(t => typeOfTree(compiler)(t))
}
}

def getCompiler(
path: AbsolutePath,
line: Int,
column: Int
): Option[(Global, Position)] = {
val code = buffers.read(path)
val offset = lineColumnToOffset(code, line, column)
compilerByPath.get(path).flatMap { compiler =>
compilerByPath.get(path).map { compiler =>
compiler.reporter.reset()
val unit = compiler.newCompilationUnit(code, path.toString())
val richUnit = new compiler.RichCompilationUnit(unit.source)
compiler.unitOfFile(richUnit.source.file) = richUnit
val richUnit =
Compiler.addCompilationUnit(compiler, code, path.toString())
val position = richUnit.position(offset)
val response = ask[compiler.Tree](r => compiler.askTypeAt(position, r))
val typedTree = response.get.swap
typedTree.toOption.flatMap(t => typeOfTree(compiler)(t))
compiler -> position
}
}

Expand All @@ -90,14 +132,7 @@ class Compiler(
config: CompilerConfig
): Effects.InstallPresentationCompiler = {
logger.info(s"Loading new compiler from config $config")
val vd = new io.VirtualDirectory("(memory)", None)
val settings = new Settings
settings.outputDirs.setSingleOutput(vd)
settings.classpath.value = config.classpath
settings.processArgumentString(
("-Ypresentation-any-thread" :: config.scalacOptions).mkString(" ")
)
val compiler = new Global(settings, new StoreReporter)
val compiler = Compiler.newCompiler(config.classpath, config.scalacOptions)
config.sources.foreach { path =>
// TODO(olafur) garbage collect compilers from removed files.
compilerByPath(path) = compiler
Expand Down Expand Up @@ -138,6 +173,7 @@ class Compiler(
Effects.IndexSourcesClasspath
}

private def noSignatureHelp: SignatureHelp = SignatureHelp(Nil, None, None)
private def noCompletions: List[(String, String)] = {
connection.showMessage(
MessageType.Warning,
Expand All @@ -160,12 +196,6 @@ class Compiler(
i + column
}

private def ask[A](f: Response[A] => Unit): Response[A] = {
val r = new Response[A]
f(r)
r
}

private def typeOfTree(c: Global)(t: c.Tree): Option[String] = {
import c._

Expand All @@ -180,3 +210,35 @@ class Compiler(
}

}

object Compiler {
def addCompilationUnit(
global: Global,
code: String,
filename: String
): global.RichCompilationUnit = {
val unit = global.newCompilationUnit(code, filename)
val richUnit = new global.RichCompilationUnit(unit.source)
global.unitOfFile(richUnit.source.file) = richUnit
richUnit
}
def newCompiler(classpath: String, scalacOptions: List[String]): Global = {
val vd = new io.VirtualDirectory("(memory)", None)
val settings = new Settings
settings.outputDirs.setSingleOutput(vd)
settings.classpath.value = classpath
if (classpath.isEmpty) {
settings.usejavacp.value = true
}
settings.processArgumentString(
("-Ypresentation-any-thread" :: scalacOptions).mkString(" ")
)
val compiler = new Global(settings, new StoreReporter)
compiler
}
def ask[A](f: Response[A] => Unit): Response[A] = {
val r = new Response[A]
f(r)
r
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import langserver.messages.MessageType
import langserver.messages.ResultResponse
import langserver.messages.ServerCapabilities
import langserver.messages.ShutdownResult
import langserver.messages.SignatureHelpOptions
import langserver.types._
import monix.execution.Cancelable
import monix.execution.Scheduler
Expand Down Expand Up @@ -113,6 +114,11 @@ class ScalametaLanguageServer(
triggerCharacters = "." :: Nil
)
),
signatureHelpProvider = Some(
SignatureHelpOptions(
triggerCharacters = "(" :: Nil
)
),
definitionProvider = true,
documentSymbolProvider = true,
documentFormattingProvider = true,
Expand Down Expand Up @@ -259,6 +265,16 @@ class ScalametaLanguageServer(

override def onSaveTextDocument(td: TextDocumentIdentifier): Unit = {}

override def signatureHelpRequest(
td: TextDocumentIdentifier,
position: Position
): SignatureHelp = {
compiler.signatureHelp(
Uri.toPath(td.uri).get,
position.line,
position.character
)
}
override def completionRequest(
td: TextDocumentIdentifier,
position: Position
Expand Down
32 changes: 32 additions & 0 deletions metaserver/src/test/scala/tests/compiler/CompilerTest.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package tests.compiler

import scala.meta.languageserver.Compiler
import scala.tools.nsc.interactive.Global
import tests.MegaSuite
import scala.concurrent.ExecutionContext.Implicits.global

object CompilerTest extends MegaSuite {
test("signatureHelp") {
val compiler: Global = Compiler.newCompiler("", Nil)
val TIMEOUT = 100
// very bad test, we should be calling our wrapper around the compiler
// instead, see https://github.com/scalameta/language-server/issues/47
val code =
"""
|object a {
| Predef.assert(xxx)
| def xxx = 42
|}
""".stripMargin
val unit = Compiler.addCompilationUnit(compiler, code, "a.scala")
val cursor = code.indexOf("(")
val pos = unit.position(cursor)
val completions = compiler.completionsAt(pos).matchingResults().distinct
assert(completions.nonEmpty)
val assertName = completions.head.sym.nameString
assert(assertName == "assert")
val assertParams =
completions.head.sym.asMethod.paramLists.head.head.nameString
assert(assertParams == "assertion")
}
}

0 comments on commit fe14433

Please sign in to comment.