textDocument/definition for LSP / VS Code

This is an implementation of `textDocument/definition` request.
Supports types only, and only in case when type is found in Zinc Analysis. When source(s) are found then editor opens potential source(s).
This simple implementation does not use semantic data.

During the processing of `textDocument/didSave`, we will start collecting the location of Analysis files via `lspCollectAnalyses`.
Later on, when the user asked for `textDocument/definition`, sbt server will invoke a Future call to lspDefinition, which direct reads the files to locate the definition of a class.
This commit is contained in:
wpopielarski 2017-10-20 17:26:23 +02:00 committed by Eugene Yokota
parent 297fd5d24b
commit 73b0034cfc
17 changed files with 688 additions and 19 deletions

View File

@ -396,7 +396,7 @@ lazy val mainProj = (project in file("main"))
.settings(
testedBaseSettings,
name := "Main",
libraryDependencies ++= scalaXml.value ++ Seq(launcherInterface) ++ log4jDependencies,
libraryDependencies ++= scalaXml.value ++ Seq(launcherInterface) ++ log4jDependencies ++ Seq(scalaCacheCaffeine),
managedSourceDirectories in Compile +=
baseDirectory.value / "src" / "main" / "contraband-scala",
sourceManaged in (Compile, generateContrabands) := baseDirectory.value / "src" / "main" / "contraband-scala",

View File

@ -26,7 +26,7 @@ import sbt.internal.librarymanagement.mavenint.{
PomExtraDependencyAttributes,
SbtPomExtraProperties
}
import sbt.internal.server.LanguageServerReporter
import sbt.internal.server.{ LanguageServerReporter, Definition }
import sbt.internal.testing.TestLogger
import sbt.internal.util._
import sbt.internal.util.Attributed.data
@ -132,6 +132,7 @@ object Defaults extends BuildCommon {
Seq(
managedDirectory := baseDirectory.value / "lib_managed"
))
import Keys.test
private[sbt] lazy val globalCore: Seq[Setting[_]] = globalDefaults(
defaultTestTasks(test) ++ defaultTestTasks(testOnly) ++ defaultTestTasks(testQuick) ++ Seq(
excludeFilter :== HiddenFileFilter
@ -498,6 +499,7 @@ object Defaults extends BuildCommon {
},
compileIncSetup := compileIncSetupTask.value,
console := consoleTask.value,
collectAnalyses := Definition.collectAnalysesTask.value,
consoleQuick := consoleQuickTask.value,
discoveredMainClasses := (compile map discoverMainClasses storeAs discoveredMainClasses xtriggeredBy compile).value,
discoveredSbtPlugins := discoverSbtPluginNames.value,
@ -643,6 +645,7 @@ object Defaults extends BuildCommon {
testResultLogger :== TestResultLogger.Default,
testFilter in testOnly :== (selectedFilter _)
))
import Configurations.Test
lazy val testTasks
: Seq[Setting[_]] = testTaskOptions(test) ++ testTaskOptions(testOnly) ++ testTaskOptions(
testQuick) ++ testDefaults ++ Seq(
@ -1032,6 +1035,7 @@ object Defaults extends BuildCommon {
art.value)).asFile
}
import Configurations._
def artifactSetting: Initialize[Artifact] =
Def.setting {
val a = artifact.value
@ -1390,6 +1394,7 @@ object Defaults extends BuildCommon {
}
analysisResult.analysis
}
def compileIncrementalTask = Def.task {
// TODO - Should readAnalysis + saveAnalysis be scoped by the compile task too?
compileIncrementalTaskImpl(streams.value, (compileInputs in compile).value)
@ -1674,6 +1679,7 @@ object Classpaths {
}
def defaultPackageKeys = Seq(packageBin, packageSrc, packageDoc)
import Configurations.Test
lazy val defaultPackages: Seq[TaskKey[File]] =
for (task <- defaultPackageKeys; conf <- Seq(Compile, Test)) yield (task in conf)
lazy val defaultArtifactTasks: Seq[TaskKey[File]] = makePom +: defaultPackages
@ -1707,6 +1713,7 @@ object Classpaths {
pkgTasks: Seq[TaskKey[_]]): Initialize[Seq[T]] =
pkgTasks.map(pkg => key in pkg.scope in pkg).join
import Configurations.Test
private[this] def publishGlobalDefaults =
Defaults.globalDefaults(
Seq(
@ -3200,6 +3207,7 @@ trait BuildExtra extends BuildCommon with DefExtra {
* Disables post-compilation hook for determining tests for tab-completion (such as for 'test-only').
* This is useful for reducing test:compile time when not running test.
*/
import Configurations.Test
def noTestCompletion(config: Configuration = Test): Setting[_] =
inConfig(config)(Seq(definedTests := detectTests.value)).head

View File

@ -449,6 +449,9 @@ object Keys {
val interactionService = taskKey[InteractionService]("Service used to ask for user input through the current user interface(s).").withRank(CTask)
val insideCI = SettingKey[Boolean]("insideCI", "Determines if the SBT is running in a Continuous Integration environment", AMinusSetting)
// sbt server internal
val collectAnalyses = taskKey[Unit]("Collect analysis file locations for later use.")
// special
val sessionVars = AttributeKey[SessionVar.Map]("sessionVars", "Bindings that exist for the duration of the session.", Invisible)
val parallelExecution = settingKey[Boolean]("Enables (true) or disables (false) parallel execution of tasks.").withRank(BMinusSetting)

View File

@ -101,6 +101,9 @@ final class ConsoleMain extends xsbti.AppMain {
object StandardMain {
private[sbt] lazy val exchange = new CommandExchange()
import scalacache._
import scalacache.caffeine._
private[sbt] val cache: Cache[Any] = CaffeineCache[Any]
def runManaged(s: State): xsbti.MainResult = {
val previous = TrapExit.installManager()
@ -132,6 +135,9 @@ object StandardMain {
Exec(x, None)
}
val initAttrs = BuiltinCommands.initialAttributes
import scalacache.modes.scalaFuture._
import scala.concurrent.ExecutionContext.Implicits.global
cache.removeAll()
val s = State(
configuration,
initialDefinitions,
@ -157,7 +163,7 @@ import TemplateCommandUtil.templateCommand
object BuiltinCommands {
def initialAttributes = AttributeMap.empty
import BasicCommands.exit
def ConsoleCommands: Seq[Command] =
Seq(ignore, exit, IvyConsole.command, setLogLevel, early, act, nop)

View File

@ -0,0 +1,301 @@
/*
* sbt
* Copyright 2011 - 2017, Lightbend, Inc.
* Copyright 2008 - 2010, Mark Harrah
* Licensed under BSD-3-Clause license (see LICENSE)
*/
package sbt
package internal
package server
import sbt.internal.inc.MixedAnalyzingCompiler
import sbt.internal.langserver.ErrorCodes
import sbt.util.Logger
import scala.annotation.tailrec
import scala.concurrent.{ ExecutionContext, Future }
import scala.concurrent.duration.Duration.Inf
import scala.util.matching.Regex.MatchIterator
import java.nio.file.{ Files, Paths }
import sbt.StandardMain
object Definition {
import java.net.URI
import Keys._
import sbt.internal.inc.Analysis
import sbt.internal.inc.JavaInterfaceUtil._
val AnalysesKey = "lsp.definition.analyses.key"
import sjsonnew.JsonFormat
def send[A: JsonFormat](source: CommandSource, execId: String)(params: A): Unit = {
for {
channel <- StandardMain.exchange.channels.collectFirst {
case c if c.name == source.channelName => c
}
} yield {
channel.publishEvent(params, Option(execId))
}
}
object textProcessor {
private val isIdentifier = {
import scala.tools.reflect.{ ToolBox, ToolBoxError }
lazy val tb =
scala.reflect.runtime.universe
.runtimeMirror(this.getClass.getClassLoader)
.mkToolBox()
import tb._
lazy val check = parse _ andThen compile _
(identifier: String) =>
try {
check(s"val $identifier = 0; val ${identifier}${identifier} = $identifier")
true
} catch {
case _: ToolBoxError => false
}
}
def identifier(line: String, point: Int): Option[String] = {
val whiteSpaceReg = "\\s+".r
val (zero, end) = fold(Seq.empty)(whiteSpaceReg.findAllIn(line))
.collect {
case (white, ind) => (ind, ind + white.length)
}
.fold((0, line.length)) { (z, e) =>
val (from, to) = e
val (left, right) = z
(if (to > left && to <= point) to else left,
if (from < right && from >= point) from else right)
}
val ranges = for {
from <- zero to point
to <- point to end
} yield (from -> to)
ranges
.sortBy {
case (from, to) => -(to - from)
}
.foldLeft(Seq.empty[String]) { (z, e) =>
val (from, to) = e
val fragment = line.slice(from, to).trim
z match {
case Nil if fragment.nonEmpty && isIdentifier(fragment) => fragment +: z
case h +: _ if h.length < fragment.length && isIdentifier(fragment) =>
Seq(fragment)
case h +: _ if h.length == fragment.length && isIdentifier(fragment) =>
fragment +: z
case z => z
}
}
.headOption
}
private def asClassObjectIdentifier(sym: String) =
Seq(s".$sym", s".$sym$$", s"$$$sym", s"$$$sym$$")
def potentialClsOrTraitOrObj(sym: String): PartialFunction[String, String] = {
import scala.reflect.NameTransformer
val encodedSym = NameTransformer.encode(sym)
val action: PartialFunction[String, String] = {
case potentialClassOrTraitOrObject
if asClassObjectIdentifier(encodedSym).exists(potentialClassOrTraitOrObject.endsWith) ||
encodedSym == potentialClassOrTraitOrObject ||
s"$encodedSym$$" == potentialClassOrTraitOrObject =>
potentialClassOrTraitOrObject
}
action
}
@tailrec
private def fold(z: Seq[(String, Int)])(it: MatchIterator): Seq[(String, Int)] = {
if (!it.hasNext) z
else fold(z :+ (it.next() -> it.start))(it)
}
def classTraitObjectInLine(sym: String)(line: String): Seq[(String, Int)] = {
import scala.util.matching.Regex.quote
val potentials =
Seq(s"object +${quote(sym)}".r,
s"trait +${quote(sym)} *\\[?".r,
s"class +${quote(sym)} *\\[?".r)
potentials
.flatMap { reg =>
fold(Seq.empty)(reg.findAllIn(line))
}
.collect {
case (name, pos) =>
(if (name.endsWith("[")) name.init.trim else name.trim) -> pos
}
}
import java.io.File
def markPosition(file: File, sym: String): Seq[(File, Long, Long, Long)] = {
import java.nio.file._
import scala.collection.JavaConverters._
val findInLine = classTraitObjectInLine(sym)(_)
Files
.lines(file.toPath)
.iterator
.asScala
.zipWithIndex
.flatMap {
case (line, lineNumber) =>
findInLine(line)
.collect {
case (sym, from) =>
(file, lineNumber.toLong, from.toLong, from.toLong + sym.length)
}
}
.toSeq
.distinct
}
}
import sbt.internal.langserver.TextDocumentPositionParams
import sjsonnew.shaded.scalajson.ast.unsafe.JValue
private def getDefinition(jsonDefinition: JValue): Option[TextDocumentPositionParams] = {
import sbt.internal.langserver.codec.JsonProtocol._
import sjsonnew.support.scalajson.unsafe.Converter
Converter.fromJson[TextDocumentPositionParams](jsonDefinition).toOption
}
import java.io.File
private def storeAnalysis(cacheFile: File, useBinary: Boolean): Option[Analysis] =
MixedAnalyzingCompiler
.staticCachedStore(cacheFile, !useBinary)
.get
.toOption
.collect {
case contents =>
contents.getAnalysis
}
.collect {
case a: Analysis => a
}
import scalacache._
private[sbt] def updateCache[F[_]](cache: Cache[Any])(cacheFile: String, useBinary: Boolean)(
implicit
mode: Mode[F],
flags: Flags): F[Any] = {
mode.M.flatMap(cache.get(AnalysesKey)) {
case None =>
cache.put(AnalysesKey)(Set(cacheFile -> useBinary -> None), Option(Inf))
case Some(set) =>
cache.put(AnalysesKey)(
set.asInstanceOf[Set[((String, Boolean), Option[Analysis])]].filterNot {
case ((file, _), _) => file == cacheFile
} + (cacheFile -> useBinary -> None),
Option(Inf))
case _ => mode.M.pure(())
}
}
def collectAnalysesTask = Def.task {
val cacheFile = compileIncSetup.value.cacheFile.getAbsolutePath
val useBinary = enableBinaryCompileAnalysis.value
val s = state.value
s.log.debug(s"analysis location ${(cacheFile -> useBinary)}")
import scalacache.modes.sync._
updateCache(StandardMain.cache)(cacheFile, useBinary)
}
private[sbt] def getAnalyses(log: Logger): Future[Seq[Analysis]] = {
import scalacache.modes.scalaFuture._
import scala.concurrent.ExecutionContext.Implicits.global
StandardMain.cache
.get(AnalysesKey)
.collect {
case Some(a) => a.asInstanceOf[Set[((String, Boolean), Option[Analysis])]]
}
.map { caches =>
val (working, uninitialized) = caches.partition { cacheAnalysis =>
cacheAnalysis._2 != None
}
val addToCache = uninitialized.collect {
case (title @ (file, useBinary), _) if Files.exists(Paths.get(file)) =>
(title, storeAnalysis(Paths.get(file).toFile, !useBinary))
}
val validCaches = working ++ addToCache
if (addToCache.nonEmpty)
StandardMain.cache.put(AnalysesKey)(validCaches, Option(Inf))
validCaches.toSeq.collect {
case (_, Some(analysis)) =>
analysis
}
}
}
def lspDefinition(jsonDefinition: JValue,
requestId: String,
commandSource: CommandSource,
log: Logger)(implicit ec: ExecutionContext): Future[Unit] = Future {
val LspDefinitionLogHead = "lsp-definition"
import sjsonnew.support.scalajson.unsafe.CompactPrinter
log.debug(s"$LspDefinitionLogHead json request: ${CompactPrinter(jsonDefinition)}")
lazy val analyses = getAnalyses(log)
val definition = getDefinition(jsonDefinition)
definition
.flatMap { definition =>
val uri = URI.create(definition.textDocument.uri)
import java.nio.file._
Files
.lines(Paths.get(uri))
.skip(definition.position.line)
.findFirst
.toOption
.flatMap { line =>
log.debug(s"$LspDefinitionLogHead found line: $line")
textProcessor
.identifier(line, definition.position.character.toInt)
}
}
.map { sym =>
log.debug(s"symbol $sym")
analyses
.map { analyses =>
val locations = analyses.par.flatMap { analysis =>
val selectPotentials = textProcessor.potentialClsOrTraitOrObj(sym)
val classes =
(analysis.apis.allInternalClasses ++ analysis.apis.allExternals).collect {
selectPotentials
}
log.debug(s"$LspDefinitionLogHead potentials: $classes")
classes
.flatMap { className =>
analysis.relations.definesClass(className) ++ analysis.relations
.libraryDefinesClass(className)
}
.flatMap { classFile =>
textProcessor.markPosition(classFile, sym).collect {
case (file, line, from, to) =>
import sbt.internal.langserver.{ Location, Position, Range }
Location(file.toURI.toURL.toString,
Range(Position(line, from), Position(line, to)))
}
}
}.seq
log.debug(s"$LspDefinitionLogHead locations ${locations}")
import sbt.internal.langserver.codec.JsonProtocol._
send(commandSource, requestId)(locations.toArray)
}
.recover {
case anyException @ _ =>
log.warn(
s"Problem with processing analyses $anyException for ${CompactPrinter(jsonDefinition)}")
import sbt.internal.protocol.JsonRpcResponseError
import sbt.internal.protocol.codec.JsonRPCProtocol._
send(commandSource, requestId)(
JsonRpcResponseError(ErrorCodes.InternalError,
"Problem with processing analyses.",
None))
}
}
.orElse {
log.info(s"Symbol not found in definition request ${CompactPrinter(jsonDefinition)}")
import sbt.internal.langserver.Location
import sbt.internal.langserver.codec.JsonProtocol._
send(commandSource, requestId)(Array.empty[Location])
None
}
}
}

View File

@ -35,10 +35,8 @@ private[sbt] trait LanguageServerProtocol extends CommandChannel {
protected def onSettingQuery(execId: Option[String], req: Q): Unit
protected def onRequestMessage(request: JsonRpcRequestMessage): Unit = {
import sbt.internal.langserver.codec.JsonProtocol._
import internalJsonProtocol._
def json =
request.params.getOrElse(
throw LangServerError(ErrorCodes.InvalidParams,
@ -57,9 +55,13 @@ private[sbt] trait LanguageServerProtocol extends CommandChannel {
else throw LangServerError(ErrorCodes.InvalidRequest, "invalid token")
} else ()
setInitialized(true)
append(Exec(s"collectAnalyses", Some(request.id), Some(CommandSource(name))))
langRespond(InitializeResult(serverCapabilities), Option(request.id))
case "textDocument/didSave" =>
append(Exec("compile", Some(request.id), Some(CommandSource(name))))
append(Exec(";compile; collectAnalyses", Some(request.id), Some(CommandSource(name))))
case "textDocument/definition" =>
import scala.concurrent.ExecutionContext.Implicits.global
Definition.lspDefinition(json, request.id, CommandSource(name), log)
case "sbt/exec" =>
val param = Converter.fromJson[SbtExecParams](json).get
append(Exec(param.commandLine, Some(request.id), Some(CommandSource(name))))
@ -68,7 +70,7 @@ private[sbt] trait LanguageServerProtocol extends CommandChannel {
val param = Converter.fromJson[Q](json).get
onSettingQuery(Option(request.id), param)
}
case _ => ()
case unhandledRequest => log.debug(s"Unhandled request received: $unhandledRequest")
}
}
@ -149,6 +151,7 @@ private[sbt] trait LanguageServerProtocol extends CommandChannel {
private[sbt] lazy val serverCapabilities: ServerCapabilities = {
ServerCapabilities(textDocumentSync =
TextDocumentSyncOptions(true, 0, false, false, SaveOptions(false)),
hoverProvider = false)
hoverProvider = false,
definitionProvider = true)
}
}

View File

@ -0,0 +1,187 @@
/*
* sbt
* Copyright 2011 - 2017, Lightbend, Inc.
* Copyright 2008 - 2010, Mark Harrah
* Licensed under BSD-3-Clause license (see LICENSE)
*/
package sbt
package internal
package server
import sbt.internal.inc.Analysis
class DefinitionTest extends org.specs2.mutable.Specification {
import Definition.textProcessor
"text processor" should {
"find valid standard scala identifier when caret is set at the start of it" in {
textProcessor.identifier("val identifier = 0", 4) must beSome("identifier")
}
"not find valid standard scala identifier because it is '='" in {
textProcessor.identifier("val identifier = 0", 15) must beNone
}
"find valid standard scala identifier when caret is set in the middle of it" in {
textProcessor.identifier("val identifier = 0", 11) must beSome("identifier")
}
"find valid standard scala identifier with comma" in {
textProcessor.identifier("def foo(a: identifier, b: other) = ???", 13) must beSome(
"identifier")
}
"find valid standard short scala identifier when caret is set at the start of it" in {
textProcessor.identifier("val a = 0", 4) must beSome("a")
}
"find valid standard short scala identifier when caret is set at the end of it" in {
textProcessor.identifier("def foo(f: Int) = Foo(f)", 19) must beSome("Foo")
}
"find valid non-standard short scala identifier when caret is set at the start of it" in {
textProcessor.identifier("val == = 0", 4) must beSome("==")
}
"find valid non-standard short scala identifier when caret is set in the middle of it" in {
textProcessor.identifier("val == = 0", 5) must beSome("==")
}
"find valid non-standard short scala identifier when caret is set at the end of it" in {
textProcessor.identifier("val == = 0", 6) must beSome("==")
}
"choose longest valid standard scala identifier from scala keyword when caret is set at the start of it" in {
textProcessor.identifier("val k = 0", 0) must beSome("va") or beSome("al")
}
"choose longest valid standard scala identifier from scala keyword when caret is set in the middle of it" in {
textProcessor.identifier("val k = 0", 1) must beSome("va") or beSome("al")
}
"match symbol as class name" in {
textProcessor.potentialClsOrTraitOrObj("A")("com.acme.A") must be_==("com.acme.A")
}
"match symbol as object name" in {
textProcessor.potentialClsOrTraitOrObj("A")("com.acme.A$") must be_==("com.acme.A$")
}
"match symbol as inner class name" in {
textProcessor.potentialClsOrTraitOrObj("A")("com.acme.A$A") must be_==("com.acme.A$A")
}
"match symbol as inner object name" in {
textProcessor.potentialClsOrTraitOrObj("A")("com.acme.A$A$") must be_==("com.acme.A$A$")
}
"match symbol as default package class name" in {
textProcessor.potentialClsOrTraitOrObj("A")("A") must be_==("A")
}
"match symbol as default package object name" in {
textProcessor.potentialClsOrTraitOrObj("A")("A$") must be_==("A$")
}
"match object in line version 1" in {
textProcessor.classTraitObjectInLine("A")(" object A ") must contain(("object A", 3))
}
"match object in line version 2" in {
textProcessor.classTraitObjectInLine("A")(" object A ") must contain(("object A", 3))
}
"match object in line version 3" in {
textProcessor.classTraitObjectInLine("A")("object A {") must contain(("object A", 0))
}
"not match object in line" in {
textProcessor.classTraitObjectInLine("B")("object A ") must be empty
}
"match class in line version 1" in {
textProcessor.classTraitObjectInLine("A")(" class A ") must contain(("class A", 3))
}
"match class in line version 2" in {
textProcessor.classTraitObjectInLine("A")(" class A ") must contain(("class A", 3))
}
"match class in line version 3" in {
textProcessor.classTraitObjectInLine("A")("class A {") must contain(("class A", 0))
}
"match class in line version 4" in {
textProcessor.classTraitObjectInLine("A")(" class A[A] ") must contain(
("class A", 3))
}
"match class in line version 5" in {
textProcessor.classTraitObjectInLine("A")(" class A [A] ") must contain(
("class A", 3))
}
"match class in line version 6" in {
textProcessor.classTraitObjectInLine("A")("class A[A[_]] {") must contain(("class A", 0))
}
"not match class in line" in {
textProcessor.classTraitObjectInLine("B")("class A ") must be empty
}
"match trait in line version 1" in {
textProcessor.classTraitObjectInLine("A")(" trait A ") must contain(("trait A", 3))
}
"match trait in line version 2" in {
textProcessor.classTraitObjectInLine("A")(" trait A ") must contain(("trait A", 3))
}
"match trait in line version 3" in {
textProcessor.classTraitObjectInLine("A")("trait A {") must contain(("trait A", 0))
}
"match trait in line version 4" in {
textProcessor.classTraitObjectInLine("A")(" trait A[A] ") must contain(
("trait A", 3))
}
"match trait in line version 5" in {
textProcessor.classTraitObjectInLine("A")(" trait A [A] ") must contain(
("trait A", 3))
}
"match trait in line version 6" in {
textProcessor.classTraitObjectInLine("A")("trait A[A[_]] {") must contain(("trait A", 0))
}
"not match trait in line" in {
textProcessor.classTraitObjectInLine("B")("trait A ") must be empty
}
}
"definition" should {
"cache data in cache" in {
import scalacache.caffeine._
val cache = CaffeineCache[Any]
val cacheFile = "Test.scala"
val useBinary = true
import scalacache.modes.scalaFuture._
val actual = Definition
.updateCache(cache)(cacheFile, useBinary)
.flatMap(_ => cache.get(Definition.AnalysesKey))
actual.collect {
case Some(s) => s.asInstanceOf[Set[((String, Boolean), Option[Analysis])]]
} should contain[((String, Boolean), Option[Analysis])]("Test.scala" -> true -> None).await
}
"replace cache data in cache" in {
import scalacache.caffeine._
val cache = CaffeineCache[Any]
val cacheFile = "Test.scala"
val useBinary = true
val falseUseBinary = false
import scalacache.modes.scalaFuture._
val actual = Definition
.updateCache(cache)(cacheFile, falseUseBinary)
.flatMap { _ =>
Definition.updateCache(cache)(cacheFile, useBinary)
}
.flatMap(_ => cache.get(Definition.AnalysesKey))
actual.collect {
case Some(s) => s.asInstanceOf[Set[((String, Boolean), Option[Analysis])]]
} should contain[((String, Boolean), Option[Analysis])]("Test.scala" -> true -> None).await
}
"cache more data in cache" in {
import scalacache.caffeine._
val cache = CaffeineCache[Any]
val cacheFile = "Test.scala"
val useBinary = true
val otherCacheFile = "OtherTest.scala"
val otherUseBinary = false
import scalacache.modes.scalaFuture._
val actual = Definition
.updateCache(cache)(otherCacheFile, otherUseBinary)
.flatMap { _ =>
Definition.updateCache(cache)(cacheFile, useBinary)
}
.flatMap(_ => cache.get(Definition.AnalysesKey))
actual.collect {
case Some(s) => s.asInstanceOf[Set[((String, Boolean), Option[Analysis])]]
} should contain[((String, Boolean), Option[Analysis])](
"Test.scala" -> true -> None,
"OtherTest.scala" -> false -> None).await
}
}
}

View File

@ -161,11 +161,11 @@ object SettingQueryTest extends org.specs2.mutable.Specification {
val settings: Seq[Setting[_]] = finalTransforms(
buildConfigurations(loadedBuild, getRootProject(units), config.injectSettings))
val delegates: Scope => Seq[Scope] = defaultDelegates(loadedBuild)
val delegates = defaultDelegates(loadedBuild)
val scopeLocal: ScopeLocal = EvaluateTask.injectStreams
val display: Show[ScopedKey[_]] = Project showLoadingKey loadedBuild
val data: Settings[Scope] = Def.make(settings)(delegates, scopeLocal, display)
val data = Def.make(settings)(delegates, scopeLocal, display)
val extra: KeyIndex => BuildUtil[_] = index => BuildUtil(baseUri, units, index, data)
val index: StructureIndex = structureIndex(data, settings, extra, units)

View File

@ -125,4 +125,6 @@ object Dependencies {
val log4jSlf4jImpl = "org.apache.logging.log4j" % "log4j-slf4j-impl" % log4jVersion
// specify all of log4j modules to prevent misalignment
val log4jDependencies = Vector(log4jApi, log4jCore, log4jSlf4jImpl)
val scalaCacheCaffeine = "com.github.cb372" %% "scalacache-caffeine" % "0.20.0"
}

View File

@ -7,22 +7,24 @@ package sbt.internal.langserver
final class ServerCapabilities private (
val textDocumentSync: Option[sbt.internal.langserver.TextDocumentSyncOptions],
/** The server provides hover support. */
val hoverProvider: Option[Boolean]) extends Serializable {
val hoverProvider: Option[Boolean],
/** Goto definition */
val definitionProvider: Option[Boolean]) extends Serializable {
override def equals(o: Any): Boolean = o match {
case x: ServerCapabilities => (this.textDocumentSync == x.textDocumentSync) && (this.hoverProvider == x.hoverProvider)
case x: ServerCapabilities => (this.textDocumentSync == x.textDocumentSync) && (this.hoverProvider == x.hoverProvider) && (this.definitionProvider == x.definitionProvider)
case _ => false
}
override def hashCode: Int = {
37 * (37 * (37 * (17 + "sbt.internal.langserver.ServerCapabilities".##) + textDocumentSync.##) + hoverProvider.##)
37 * (37 * (37 * (37 * (17 + "sbt.internal.langserver.ServerCapabilities".##) + textDocumentSync.##) + hoverProvider.##) + definitionProvider.##)
}
override def toString: String = {
"ServerCapabilities(" + textDocumentSync + ", " + hoverProvider + ")"
"ServerCapabilities(" + textDocumentSync + ", " + hoverProvider + ", " + definitionProvider + ")"
}
protected[this] def copy(textDocumentSync: Option[sbt.internal.langserver.TextDocumentSyncOptions] = textDocumentSync, hoverProvider: Option[Boolean] = hoverProvider): ServerCapabilities = {
new ServerCapabilities(textDocumentSync, hoverProvider)
protected[this] def copy(textDocumentSync: Option[sbt.internal.langserver.TextDocumentSyncOptions] = textDocumentSync, hoverProvider: Option[Boolean] = hoverProvider, definitionProvider: Option[Boolean] = definitionProvider): ServerCapabilities = {
new ServerCapabilities(textDocumentSync, hoverProvider, definitionProvider)
}
def withTextDocumentSync(textDocumentSync: Option[sbt.internal.langserver.TextDocumentSyncOptions]): ServerCapabilities = {
copy(textDocumentSync = textDocumentSync)
@ -36,9 +38,15 @@ final class ServerCapabilities private (
def withHoverProvider(hoverProvider: Boolean): ServerCapabilities = {
copy(hoverProvider = Option(hoverProvider))
}
def withDefinitionProvider(definitionProvider: Option[Boolean]): ServerCapabilities = {
copy(definitionProvider = definitionProvider)
}
def withDefinitionProvider(definitionProvider: Boolean): ServerCapabilities = {
copy(definitionProvider = Option(definitionProvider))
}
}
object ServerCapabilities {
def apply(textDocumentSync: Option[sbt.internal.langserver.TextDocumentSyncOptions], hoverProvider: Option[Boolean]): ServerCapabilities = new ServerCapabilities(textDocumentSync, hoverProvider)
def apply(textDocumentSync: sbt.internal.langserver.TextDocumentSyncOptions, hoverProvider: Boolean): ServerCapabilities = new ServerCapabilities(Option(textDocumentSync), Option(hoverProvider))
def apply(textDocumentSync: Option[sbt.internal.langserver.TextDocumentSyncOptions], hoverProvider: Option[Boolean], definitionProvider: Option[Boolean]): ServerCapabilities = new ServerCapabilities(textDocumentSync, hoverProvider, definitionProvider)
def apply(textDocumentSync: sbt.internal.langserver.TextDocumentSyncOptions, hoverProvider: Boolean, definitionProvider: Boolean): ServerCapabilities = new ServerCapabilities(Option(textDocumentSync), Option(hoverProvider), Option(definitionProvider))
}

View File

@ -0,0 +1,34 @@
/**
* This code is generated using [[http://www.scala-sbt.org/contraband/ sbt-contraband]].
*/
// DO NOT EDIT MANUALLY
package sbt.internal.langserver
/** Text documents are identified using a URI. On the protocol level, URIs are passed as strings. */
final class TextDocumentIdentifier private (
/** The text document's URI. */
val uri: String) extends Serializable {
override def equals(o: Any): Boolean = o match {
case x: TextDocumentIdentifier => (this.uri == x.uri)
case _ => false
}
override def hashCode: Int = {
37 * (37 * (17 + "sbt.internal.langserver.TextDocumentIdentifier".##) + uri.##)
}
override def toString: String = {
"TextDocumentIdentifier(" + uri + ")"
}
protected[this] def copy(uri: String = uri): TextDocumentIdentifier = {
new TextDocumentIdentifier(uri)
}
def withUri(uri: String): TextDocumentIdentifier = {
copy(uri = uri)
}
}
object TextDocumentIdentifier {
def apply(uri: String): TextDocumentIdentifier = new TextDocumentIdentifier(uri)
}

View File

@ -0,0 +1,39 @@
/**
* This code is generated using [[http://www.scala-sbt.org/contraband/ sbt-contraband]].
*/
// DO NOT EDIT MANUALLY
package sbt.internal.langserver
/** Goto definition params model */
final class TextDocumentPositionParams private (
/** The text document. */
val textDocument: sbt.internal.langserver.TextDocumentIdentifier,
/** The position inside the text document. */
val position: sbt.internal.langserver.Position) extends Serializable {
override def equals(o: Any): Boolean = o match {
case x: TextDocumentPositionParams => (this.textDocument == x.textDocument) && (this.position == x.position)
case _ => false
}
override def hashCode: Int = {
37 * (37 * (37 * (17 + "sbt.internal.langserver.TextDocumentPositionParams".##) + textDocument.##) + position.##)
}
override def toString: String = {
"TextDocumentPositionParams(" + textDocument + ", " + position + ")"
}
protected[this] def copy(textDocument: sbt.internal.langserver.TextDocumentIdentifier = textDocument, position: sbt.internal.langserver.Position = position): TextDocumentPositionParams = {
new TextDocumentPositionParams(textDocument, position)
}
def withTextDocument(textDocument: sbt.internal.langserver.TextDocumentIdentifier): TextDocumentPositionParams = {
copy(textDocument = textDocument)
}
def withPosition(position: sbt.internal.langserver.Position): TextDocumentPositionParams = {
copy(position = position)
}
}
object TextDocumentPositionParams {
def apply(textDocument: sbt.internal.langserver.TextDocumentIdentifier, position: sbt.internal.langserver.Position): TextDocumentPositionParams = new TextDocumentPositionParams(textDocument, position)
}

View File

@ -19,4 +19,6 @@ trait JsonProtocol extends sjsonnew.BasicJsonProtocol
with sbt.internal.langserver.codec.LogMessageParamsFormats
with sbt.internal.langserver.codec.PublishDiagnosticsParamsFormats
with sbt.internal.langserver.codec.SbtExecParamsFormats
with sbt.internal.langserver.codec.TextDocumentIdentifierFormats
with sbt.internal.langserver.codec.TextDocumentPositionParamsFormats
object JsonProtocol extends JsonProtocol

View File

@ -13,8 +13,9 @@ implicit lazy val ServerCapabilitiesFormat: JsonFormat[sbt.internal.langserver.S
unbuilder.beginObject(js)
val textDocumentSync = unbuilder.readField[Option[sbt.internal.langserver.TextDocumentSyncOptions]]("textDocumentSync")
val hoverProvider = unbuilder.readField[Option[Boolean]]("hoverProvider")
val definitionProvider = unbuilder.readField[Option[Boolean]]("definitionProvider")
unbuilder.endObject()
sbt.internal.langserver.ServerCapabilities(textDocumentSync, hoverProvider)
sbt.internal.langserver.ServerCapabilities(textDocumentSync, hoverProvider, definitionProvider)
case None =>
deserializationError("Expected JsObject but found None")
}
@ -23,6 +24,7 @@ implicit lazy val ServerCapabilitiesFormat: JsonFormat[sbt.internal.langserver.S
builder.beginObject()
builder.addField("textDocumentSync", obj.textDocumentSync)
builder.addField("hoverProvider", obj.hoverProvider)
builder.addField("definitionProvider", obj.definitionProvider)
builder.endObject()
}
}

View File

@ -0,0 +1,27 @@
/**
* This code is generated using [[http://www.scala-sbt.org/contraband/ sbt-contraband]].
*/
// DO NOT EDIT MANUALLY
package sbt.internal.langserver.codec
import _root_.sjsonnew.{ Unbuilder, Builder, JsonFormat, deserializationError }
trait TextDocumentIdentifierFormats { self: sjsonnew.BasicJsonProtocol =>
implicit lazy val TextDocumentIdentifierFormat: JsonFormat[sbt.internal.langserver.TextDocumentIdentifier] = new JsonFormat[sbt.internal.langserver.TextDocumentIdentifier] {
override def read[J](jsOpt: Option[J], unbuilder: Unbuilder[J]): sbt.internal.langserver.TextDocumentIdentifier = {
jsOpt match {
case Some(js) =>
unbuilder.beginObject(js)
val uri = unbuilder.readField[String]("uri")
unbuilder.endObject()
sbt.internal.langserver.TextDocumentIdentifier(uri)
case None =>
deserializationError("Expected JsObject but found None")
}
}
override def write[J](obj: sbt.internal.langserver.TextDocumentIdentifier, builder: Builder[J]): Unit = {
builder.beginObject()
builder.addField("uri", obj.uri)
builder.endObject()
}
}
}

View File

@ -0,0 +1,29 @@
/**
* This code is generated using [[http://www.scala-sbt.org/contraband/ sbt-contraband]].
*/
// DO NOT EDIT MANUALLY
package sbt.internal.langserver.codec
import _root_.sjsonnew.{ Unbuilder, Builder, JsonFormat, deserializationError }
trait TextDocumentPositionParamsFormats { self: sbt.internal.langserver.codec.TextDocumentIdentifierFormats with sbt.internal.langserver.codec.PositionFormats with sjsonnew.BasicJsonProtocol =>
implicit lazy val TextDocumentPositionParamsFormat: JsonFormat[sbt.internal.langserver.TextDocumentPositionParams] = new JsonFormat[sbt.internal.langserver.TextDocumentPositionParams] {
override def read[J](jsOpt: Option[J], unbuilder: Unbuilder[J]): sbt.internal.langserver.TextDocumentPositionParams = {
jsOpt match {
case Some(js) =>
unbuilder.beginObject(js)
val textDocument = unbuilder.readField[sbt.internal.langserver.TextDocumentIdentifier]("textDocument")
val position = unbuilder.readField[sbt.internal.langserver.Position]("position")
unbuilder.endObject()
sbt.internal.langserver.TextDocumentPositionParams(textDocument, position)
case None =>
deserializationError("Expected JsObject but found None")
}
}
override def write[J](obj: sbt.internal.langserver.TextDocumentPositionParams, builder: Builder[J]): Unit = {
builder.beginObject()
builder.addField("textDocument", obj.textDocument)
builder.addField("position", obj.position)
builder.endObject()
}
}
}

View File

@ -83,6 +83,9 @@ type ServerCapabilities {
## The server provides hover support.
hoverProvider: Boolean
## Goto definition
definitionProvider: Boolean
}
type TextDocumentSyncOptions {
@ -127,3 +130,18 @@ type PublishDiagnosticsParams {
type SbtExecParams {
commandLine: String!
}
## Goto definition params model
type TextDocumentPositionParams {
## The text document.
textDocument: sbt.internal.langserver.TextDocumentIdentifier!
## The position inside the text document.
position: sbt.internal.langserver.Position!
}
## Text documents are identified using a URI. On the protocol level, URIs are passed as strings.
type TextDocumentIdentifier {
## The text document's URI.
uri: String!
}