From 73b0034cfc7db7a03f84d6098829f08b0ec862db Mon Sep 17 00:00:00 2001 From: wpopielarski Date: Fri, 20 Oct 2017 17:26:23 +0200 Subject: [PATCH 1/2] 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. --- build.sbt | 2 +- main/src/main/scala/sbt/Defaults.scala | 10 +- main/src/main/scala/sbt/Keys.scala | 3 + main/src/main/scala/sbt/Main.scala | 8 +- .../sbt/internal/server/Definition.scala | 301 ++++++++++++++++++ .../server/LanguageServerProtocol.scala | 13 +- .../sbt/internal/server/DefinitionTest.scala | 187 +++++++++++ .../internal/server/SettingQueryTest.scala | 4 +- project/Dependencies.scala | 2 + .../langserver/ServerCapabilities.scala | 24 +- .../langserver/TextDocumentIdentifier.scala | 34 ++ .../TextDocumentPositionParams.scala | 39 +++ .../langserver/codec/JsonProtocol.scala | 2 + .../codec/ServerCapabilitiesFormats.scala | 4 +- .../codec/TextDocumentIdentifierFormats.scala | 27 ++ .../TextDocumentPositionParamsFormats.scala | 29 ++ protocol/src/main/contraband/lsp.contra | 18 ++ 17 files changed, 688 insertions(+), 19 deletions(-) create mode 100644 main/src/main/scala/sbt/internal/server/Definition.scala create mode 100644 main/src/test/scala/sbt/internal/server/DefinitionTest.scala create mode 100644 protocol/src/main/contraband-scala/sbt/internal/langserver/TextDocumentIdentifier.scala create mode 100644 protocol/src/main/contraband-scala/sbt/internal/langserver/TextDocumentPositionParams.scala create mode 100644 protocol/src/main/contraband-scala/sbt/internal/langserver/codec/TextDocumentIdentifierFormats.scala create mode 100644 protocol/src/main/contraband-scala/sbt/internal/langserver/codec/TextDocumentPositionParamsFormats.scala 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..06e6291df 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 @@ -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 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..52dc7e397 --- /dev/null +++ b/main/src/main/scala/sbt/internal/server/Definition.scala @@ -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 + } + } +} 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..6095c8ce7 --- /dev/null +++ b/main/src/test/scala/sbt/internal/server/DefinitionTest.scala @@ -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 + } + } +} diff --git a/main/src/test/scala/sbt/internal/server/SettingQueryTest.scala b/main/src/test/scala/sbt/internal/server/SettingQueryTest.scala index 7acf7955f..bcb2e0d6d 100644 --- a/main/src/test/scala/sbt/internal/server/SettingQueryTest.scala +++ b/main/src/test/scala/sbt/internal/server/SettingQueryTest.scala @@ -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) 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! +} From 452e97e41dc3e283314e6493c5f34d4bcc46427d Mon Sep 17 00:00:00 2001 From: wpopielarski Date: Mon, 27 Nov 2017 15:04:37 +0100 Subject: [PATCH 2/2] Adds backticks to class/trait/object name. Adapts tests to changed specs2 dependency. Tiny fixes. Removes Scala IDE compiler clues. --- main/src/main/scala/sbt/Defaults.scala | 7 --- .../sbt/internal/server/Definition.scala | 42 +++++++++++++--- .../sbt/internal/server/DefinitionTest.scala | 48 ++++++++----------- .../internal/server/SettingQueryTest.scala | 4 +- 4 files changed, 56 insertions(+), 45 deletions(-) diff --git a/main/src/main/scala/sbt/Defaults.scala b/main/src/main/scala/sbt/Defaults.scala index 06e6291df..d40467dff 100755 --- a/main/src/main/scala/sbt/Defaults.scala +++ b/main/src/main/scala/sbt/Defaults.scala @@ -132,7 +132,6 @@ 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 @@ -645,7 +644,6 @@ 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( @@ -1035,7 +1033,6 @@ object Defaults extends BuildCommon { art.value)).asFile } - import Configurations._ def artifactSetting: Initialize[Artifact] = Def.setting { val a = artifact.value @@ -1394,7 +1391,6 @@ 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) @@ -1679,7 +1675,6 @@ 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 @@ -1713,7 +1708,6 @@ 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( @@ -3207,7 +3201,6 @@ 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 diff --git a/main/src/main/scala/sbt/internal/server/Definition.scala b/main/src/main/scala/sbt/internal/server/Definition.scala index 52dc7e397..4459861db 100644 --- a/main/src/main/scala/sbt/internal/server/Definition.scala +++ b/main/src/main/scala/sbt/internal/server/Definition.scala @@ -19,7 +19,7 @@ import scala.util.matching.Regex.MatchIterator import java.nio.file.{ Files, Paths } import sbt.StandardMain -object Definition { +private[sbt] object Definition { import java.net.URI import Keys._ import sbt.internal.inc.Analysis @@ -55,8 +55,33 @@ object Definition { } } - def identifier(line: String, point: Int): Option[String] = { - val whiteSpaceReg = "\\s+".r + 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) @@ -94,7 +119,10 @@ object Definition { 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 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) || @@ -114,9 +142,9 @@ object Definition { 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) + 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)) diff --git a/main/src/test/scala/sbt/internal/server/DefinitionTest.scala b/main/src/test/scala/sbt/internal/server/DefinitionTest.scala index 6095c8ce7..534d77083 100644 --- a/main/src/test/scala/sbt/internal/server/DefinitionTest.scala +++ b/main/src/test/scala/sbt/internal/server/DefinitionTest.scala @@ -127,61 +127,51 @@ class DefinitionTest extends org.specs2.mutable.Specification { } } "definition" should { + import scalacache.caffeine._ + import scalacache.modes.sync._ "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)) + Definition.updateCache(cache)(cacheFile, useBinary) + + val actual = 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 + case s => s.asInstanceOf[Set[((String, Boolean), Option[Analysis])]] + }.get should contain("Test.scala" -> true -> None) } "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)) + Definition.updateCache(cache)(cacheFile, falseUseBinary) + Definition.updateCache(cache)(cacheFile, useBinary) + + val actual = 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 + case s => s.asInstanceOf[Set[((String, Boolean), Option[Analysis])]] + }.get should contain("Test.scala" -> true -> None) } "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)) + Definition.updateCache(cache)(otherCacheFile, otherUseBinary) + Definition.updateCache(cache)(cacheFile, useBinary) + + val actual = 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 + case s => s.asInstanceOf[Set[((String, Boolean), Option[Analysis])]] + }.get should contain("Test.scala" -> true -> None, "OtherTest.scala" -> false -> None) } } } diff --git a/main/src/test/scala/sbt/internal/server/SettingQueryTest.scala b/main/src/test/scala/sbt/internal/server/SettingQueryTest.scala index bcb2e0d6d..7acf7955f 100644 --- a/main/src/test/scala/sbt/internal/server/SettingQueryTest.scala +++ b/main/src/test/scala/sbt/internal/server/SettingQueryTest.scala @@ -161,11 +161,11 @@ object SettingQueryTest extends org.specs2.mutable.Specification { val settings: Seq[Setting[_]] = finalTransforms( buildConfigurations(loadedBuild, getRootProject(units), config.injectSettings)) - val delegates = defaultDelegates(loadedBuild) + val delegates: Scope => Seq[Scope] = defaultDelegates(loadedBuild) val scopeLocal: ScopeLocal = EvaluateTask.injectStreams val display: Show[ScopedKey[_]] = Project showLoadingKey loadedBuild - val data = Def.make(settings)(delegates, scopeLocal, display) + val data: Settings[Scope] = Def.make(settings)(delegates, scopeLocal, display) val extra: KeyIndex => BuildUtil[_] = index => BuildUtil(baseUri, units, index, data) val index: StructureIndex = structureIndex(data, settings, extra, units)