diff --git a/build.sbt b/build.sbt index 61842c40c..0975b7d88 100644 --- a/build.sbt +++ b/build.sbt @@ -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", diff --git a/main/src/main/scala/sbt/Defaults.scala b/main/src/main/scala/sbt/Defaults.scala index 7041fe6de..d40467dff 100755 --- a/main/src/main/scala/sbt/Defaults.scala +++ b/main/src/main/scala/sbt/Defaults.scala @@ -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 @@ -498,6 +498,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, diff --git a/main/src/main/scala/sbt/Keys.scala b/main/src/main/scala/sbt/Keys.scala index 211b46b1b..3df25679d 100644 --- a/main/src/main/scala/sbt/Keys.scala +++ b/main/src/main/scala/sbt/Keys.scala @@ -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) diff --git a/main/src/main/scala/sbt/Main.scala b/main/src/main/scala/sbt/Main.scala index 61fe64cd0..09d668c4b 100644 --- a/main/src/main/scala/sbt/Main.scala +++ b/main/src/main/scala/sbt/Main.scala @@ -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) diff --git a/main/src/main/scala/sbt/internal/server/Definition.scala b/main/src/main/scala/sbt/internal/server/Definition.scala new file mode 100644 index 000000000..4459861db --- /dev/null +++ b/main/src/main/scala/sbt/internal/server/Definition.scala @@ -0,0 +1,329 @@ +/* + * 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 + +private[sbt] 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 + } + } + + private def findInBackticks(line: String, point: Int): Option[String] = { + val (even, odd) = line.zipWithIndex + .collect { + case (char, backtickIndex) if char == '`' => + backtickIndex + } + .zipWithIndex + .partition { bs => + val (_, index) = bs + index % 2 == 0 + } + even + .collect { + case (backtickIndex, _) => backtickIndex + } + .zip { + odd.collect { + case (backtickIndex, _) => backtickIndex + 1 + } + } + .collectFirst { + case (from, to) if from <= point && point < to => line.slice(from, to) + } + } + + def identifier(line: String, point: Int): Option[String] = findInBackticks(line, point).orElse { + 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.toSeq match { + case '`' +: body :+ '`' => body.mkString + case noBackticked => noBackticked.mkString + }) + 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\\s+${quote(sym)}".r, + s"trait\\s+${quote(sym)} *\\[?".r, + s"class\\s+${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 + } + } +} diff --git a/main/src/main/scala/sbt/internal/server/LanguageServerProtocol.scala b/main/src/main/scala/sbt/internal/server/LanguageServerProtocol.scala index 776078ae6..5c18b926b 100644 --- a/main/src/main/scala/sbt/internal/server/LanguageServerProtocol.scala +++ b/main/src/main/scala/sbt/internal/server/LanguageServerProtocol.scala @@ -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) } } diff --git a/main/src/test/scala/sbt/internal/server/DefinitionTest.scala b/main/src/test/scala/sbt/internal/server/DefinitionTest.scala new file mode 100644 index 000000000..534d77083 --- /dev/null +++ b/main/src/test/scala/sbt/internal/server/DefinitionTest.scala @@ -0,0 +1,177 @@ +/* + * 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 { + import scalacache.caffeine._ + import scalacache.modes.sync._ + "cache data in cache" in { + val cache = CaffeineCache[Any] + val cacheFile = "Test.scala" + val useBinary = true + + Definition.updateCache(cache)(cacheFile, useBinary) + + val actual = cache.get(Definition.AnalysesKey) + + actual.collect { + case s => s.asInstanceOf[Set[((String, Boolean), Option[Analysis])]] + }.get should contain("Test.scala" -> true -> None) + } + "replace cache data in cache" in { + val cache = CaffeineCache[Any] + val cacheFile = "Test.scala" + val useBinary = true + val falseUseBinary = false + + Definition.updateCache(cache)(cacheFile, falseUseBinary) + Definition.updateCache(cache)(cacheFile, useBinary) + + val actual = cache.get(Definition.AnalysesKey) + + actual.collect { + case s => s.asInstanceOf[Set[((String, Boolean), Option[Analysis])]] + }.get should contain("Test.scala" -> true -> None) + } + "cache more data in cache" in { + val cache = CaffeineCache[Any] + val cacheFile = "Test.scala" + val useBinary = true + val otherCacheFile = "OtherTest.scala" + val otherUseBinary = false + + Definition.updateCache(cache)(otherCacheFile, otherUseBinary) + Definition.updateCache(cache)(cacheFile, useBinary) + + val actual = cache.get(Definition.AnalysesKey) + + actual.collect { + case s => s.asInstanceOf[Set[((String, Boolean), Option[Analysis])]] + }.get should contain("Test.scala" -> true -> None, "OtherTest.scala" -> false -> None) + } + } +} diff --git a/project/Dependencies.scala b/project/Dependencies.scala index c9f430e17..9a0339a1e 100644 --- a/project/Dependencies.scala +++ b/project/Dependencies.scala @@ -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" } diff --git a/protocol/src/main/contraband-scala/sbt/internal/langserver/ServerCapabilities.scala b/protocol/src/main/contraband-scala/sbt/internal/langserver/ServerCapabilities.scala index c40b895e5..802428214 100644 --- a/protocol/src/main/contraband-scala/sbt/internal/langserver/ServerCapabilities.scala +++ b/protocol/src/main/contraband-scala/sbt/internal/langserver/ServerCapabilities.scala @@ -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)) } diff --git a/protocol/src/main/contraband-scala/sbt/internal/langserver/TextDocumentIdentifier.scala b/protocol/src/main/contraband-scala/sbt/internal/langserver/TextDocumentIdentifier.scala new file mode 100644 index 000000000..bb29b3d7f --- /dev/null +++ b/protocol/src/main/contraband-scala/sbt/internal/langserver/TextDocumentIdentifier.scala @@ -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) +} diff --git a/protocol/src/main/contraband-scala/sbt/internal/langserver/TextDocumentPositionParams.scala b/protocol/src/main/contraband-scala/sbt/internal/langserver/TextDocumentPositionParams.scala new file mode 100644 index 000000000..5d7d3edd8 --- /dev/null +++ b/protocol/src/main/contraband-scala/sbt/internal/langserver/TextDocumentPositionParams.scala @@ -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) +} diff --git a/protocol/src/main/contraband-scala/sbt/internal/langserver/codec/JsonProtocol.scala b/protocol/src/main/contraband-scala/sbt/internal/langserver/codec/JsonProtocol.scala index f946496ec..7898d1d72 100644 --- a/protocol/src/main/contraband-scala/sbt/internal/langserver/codec/JsonProtocol.scala +++ b/protocol/src/main/contraband-scala/sbt/internal/langserver/codec/JsonProtocol.scala @@ -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 \ No newline at end of file diff --git a/protocol/src/main/contraband-scala/sbt/internal/langserver/codec/ServerCapabilitiesFormats.scala b/protocol/src/main/contraband-scala/sbt/internal/langserver/codec/ServerCapabilitiesFormats.scala index 4bac79256..028865f7f 100644 --- a/protocol/src/main/contraband-scala/sbt/internal/langserver/codec/ServerCapabilitiesFormats.scala +++ b/protocol/src/main/contraband-scala/sbt/internal/langserver/codec/ServerCapabilitiesFormats.scala @@ -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() } } diff --git a/protocol/src/main/contraband-scala/sbt/internal/langserver/codec/TextDocumentIdentifierFormats.scala b/protocol/src/main/contraband-scala/sbt/internal/langserver/codec/TextDocumentIdentifierFormats.scala new file mode 100644 index 000000000..fd093e0cf --- /dev/null +++ b/protocol/src/main/contraband-scala/sbt/internal/langserver/codec/TextDocumentIdentifierFormats.scala @@ -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() + } +} +} diff --git a/protocol/src/main/contraband-scala/sbt/internal/langserver/codec/TextDocumentPositionParamsFormats.scala b/protocol/src/main/contraband-scala/sbt/internal/langserver/codec/TextDocumentPositionParamsFormats.scala new file mode 100644 index 000000000..87f231e91 --- /dev/null +++ b/protocol/src/main/contraband-scala/sbt/internal/langserver/codec/TextDocumentPositionParamsFormats.scala @@ -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() + } +} +} diff --git a/protocol/src/main/contraband/lsp.contra b/protocol/src/main/contraband/lsp.contra index 18bd0a42c..a46d27d1d 100644 --- a/protocol/src/main/contraband/lsp.contra +++ b/protocol/src/main/contraband/lsp.contra @@ -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! +}