From d9e8ae18b2b3de0da3dd8fe5db669614d64af174 Mon Sep 17 00:00:00 2001 From: Ethan Atkins Date: Wed, 10 Oct 2018 16:45:22 -0700 Subject: [PATCH 1/6] Fix default FileTreeViewConfig It was possible that on startup, when this function was first invoked, that the default boot commands are present. This was a problem because the global file repository is instantiated using the value of this task. When we start a continuous build, this task gets run again to evaluate again. When sbt is started without an implicit task list, then the task is implicitly shell as indicated by the command "iflast shell". We can use this to determine whether or not to use the global file system cache or not. --- main/src/main/scala/sbt/internal/FileManagement.scala | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/main/src/main/scala/sbt/internal/FileManagement.scala b/main/src/main/scala/sbt/internal/FileManagement.scala index 15a17461a..9d4cf4fb1 100644 --- a/main/src/main/scala/sbt/internal/FileManagement.scala +++ b/main/src/main/scala/sbt/internal/FileManagement.scala @@ -23,9 +23,11 @@ private[sbt] object FileManagement { // If the session is interactive or if the commands include a continuous build, then use // the default configuration. Otherwise, use the sbt1_2_compat config, which does not cache // anything, which makes it less likely to cause issues with CI. - val interactive = remaining.contains("shell") && !remaining.contains("setUpScripted") + val interactive = remaining.contains("shell") || remaining.lastOption.contains("iflast shell") + val scripted = remaining.contains("setUpScripted") + val continuous = remaining.exists(_.startsWith(ContinuousExecutePrefix)) - if (interactive || continuous) { + if (!scripted && (interactive || continuous)) { FileTreeViewConfig .default(watchAntiEntropy.value, pollInterval.value, pollingDirectories.value) } else FileTreeViewConfig.sbt1_2_compat(pollInterval.value, watchAntiEntropy.value) From a1580bafbf7e952f32ab437b66a26b8ab2ec75f6 Mon Sep 17 00:00:00 2001 From: Ethan Atkins Date: Wed, 10 Oct 2018 18:27:22 -0700 Subject: [PATCH 2/6] Improve error message Previously, the invalid commands would be wrapped in 'Left($CMD)'. --- main-command/src/main/scala/sbt/Watched.scala | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/main-command/src/main/scala/sbt/Watched.scala b/main-command/src/main/scala/sbt/Watched.scala index 0e9bc670e..57267bb2e 100644 --- a/main-command/src/main/scala/sbt/Watched.scala +++ b/main-command/src/main/scala/sbt/Watched.scala @@ -286,9 +286,8 @@ object Watched { val terminationAction = watch(in, task, config) config.onWatchTerminated(terminationAction, command, state) } else { - config.logger.error( - s"Terminating watch due to invalid command(s): ${invalid.mkString("'", "', '", "'")}" - ) + val commands = invalid.flatMap(_.left.toOption).mkString("'", "', '", "'") + config.logger.error(s"Terminating watch due to invalid command(s): $commands") state.fail } } From 2cfbfcc842733a5541ea99f56ca2eb7968d8b6c9 Mon Sep 17 00:00:00 2001 From: Ethan Atkins Date: Wed, 10 Oct 2018 18:28:59 -0700 Subject: [PATCH 3/6] Disable re-run feature on windows For whatever reason, I couldn't get jline to work on windows, so I'm disabling the re-run with 'r' feature. This can almost surely be fixed, but the way I was invoking jline was blocking the continuous build from exiting when the user pressed enter. --- main-command/src/main/scala/sbt/Watched.scala | 38 +++++++++++-------- 1 file changed, 23 insertions(+), 15 deletions(-) diff --git a/main-command/src/main/scala/sbt/Watched.scala b/main-command/src/main/scala/sbt/Watched.scala index 57267bb2e..d697bb56c 100644 --- a/main-command/src/main/scala/sbt/Watched.scala +++ b/main-command/src/main/scala/sbt/Watched.scala @@ -109,27 +109,34 @@ object Watched { type WatchSource = Source def terminateWatch(key: Int): Boolean = Watched.isEnter(key) - private def withCharBufferedStdIn[R](f: InputStream => R): R = JLine.usingTerminal { terminal => - val in = terminal.wrapInIfNeeded(System.in) - try { - while (in.available > 0) in.read() + private[this] val isWin = Properties.isWin + private def drain(is: InputStream): Unit = while (is.available > 0) is.read() + private def withCharBufferedStdIn[R](f: InputStream => R): R = + if (!isWin) JLine.usingTerminal { terminal => terminal.init() - f(in) - } finally { - while (in.available > 0) in.read() - terminal.reset() - } - } + val in = terminal.wrapInIfNeeded(System.in) + try { + drain(in) + f(in) + } finally { + drain(in) + terminal.reset() + } + } else + try { + drain(System.in) + f(System.in) + } finally drain(System.in) private[sbt] final val handleInput: InputStream => Action = in => { @tailrec def scanInput(): Action = { if (in.available > 0) { in.read() match { - case key if isEnter(key) => CancelWatch - case key if isR(key) => Trigger - case key if key >= 0 => scanInput() - case _ => Ignore + case key if isEnter(key) => CancelWatch + case key if isR(key) && !isWin => Trigger + case key if key >= 0 => scanInput() + case _ => Ignore } } else { Ignore @@ -137,8 +144,9 @@ object Watched { } scanInput() } + private[this] val reRun = if (isWin) "" else " or 'r' to re-run the command" private def waitMessage(project: String): String = - s"Waiting for source changes$project... (press enter to interrupt or 'r' to re-run the command)" + s"Waiting for source changes$project... (press enter to interrupt$reRun)" val defaultStartWatch: Int => Option[String] = count => Some(s"$count. ${waitMessage("")}") @deprecated("Use defaultStartWatch in conjunction with the watchStartMessage key", "1.3.0") val defaultWatchingMessage: WatchState => String = ws => defaultStartWatch(ws.count).get From f579b89577cbf72a1f5e52a1de7d4add22697639 Mon Sep 17 00:00:00 2001 From: Ethan Atkins Date: Wed, 10 Oct 2018 18:48:42 -0700 Subject: [PATCH 4/6] Fix windows reload loop On windows* it was possible to get into a loop where the build would continually restart because for some reason the build.sbt file would get touched during test (I did not see this behavior on osx). Thankfully, the repository keeps track of the file hash and when we detect that the build file has been updated, we check the file hash to see if it actually changed. Note that had this bug shipped, it would have been fixable by overriding the watchOnEvent task in user builds. The loop would occur if I ran ~filesJVM/test in https://github.com/swoval/swoval. It would not occur if I ran test:compile, so the fact that the build file is being touched seems to be related to the test run itself. --- main-command/src/main/scala/sbt/Watched.scala | 14 +++++++++++++- main/src/main/scala/sbt/Defaults.scala | 10 ++-------- 2 files changed, 15 insertions(+), 9 deletions(-) diff --git a/main-command/src/main/scala/sbt/Watched.scala b/main-command/src/main/scala/sbt/Watched.scala index d697bb56c..e6b851d14 100644 --- a/main-command/src/main/scala/sbt/Watched.scala +++ b/main-command/src/main/scala/sbt/Watched.scala @@ -23,7 +23,7 @@ import sbt.internal.io.{ EventMonitor, Source, WatchState } import sbt.internal.util.Types.const import sbt.internal.util.complete.DefaultParsers import sbt.internal.util.{ AttributeKey, JLine } -import sbt.io.FileEventMonitor.Event +import sbt.io.FileEventMonitor.{ Creation, Deletion, Event, Update } import sbt.io._ import sbt.util.{ Level, Logger } import xsbti.compile.analysis.Stamp @@ -144,6 +144,18 @@ object Watched { } scanInput() } + private[sbt] def onEvent( + sources: Seq[WatchSource], + projectSources: Seq[WatchSource] + ): Event[StampedFile] => Watched.Action = + event => + if (sources.exists(_.accept(event.entry.typedPath.getPath))) Watched.Trigger + else if (projectSources.exists(_.accept(event.entry.typedPath.getPath))) event match { + case Update(prev, cur, _) if prev.value.map(_.stamp) != cur.value.map(_.stamp) => Reload + case _: Creation[_] | _: Deletion[_] => Reload + case _ => Ignore + } else Ignore + private[this] val reRun = if (isWin) "" else " or 'r' to re-run the command" private def waitMessage(project: String): String = s"Waiting for source changes$project... (press enter to interrupt$reRun)" diff --git a/main/src/main/scala/sbt/Defaults.scala b/main/src/main/scala/sbt/Defaults.scala index 3c54cadf2..f4d329710 100755 --- a/main/src/main/scala/sbt/Defaults.scala +++ b/main/src/main/scala/sbt/Defaults.scala @@ -621,14 +621,8 @@ object Defaults extends BuildCommon { consoleProject := consoleProjectTask.value, watchTransitiveSources := watchTransitiveSourcesTask.value, watchProjectTransitiveSources := watchTransitiveSourcesTaskImpl(watchProjectSources).value, - watchOnEvent := { - val sources = watchTransitiveSources.value - val projectSources = watchProjectTransitiveSources.value - e => - if (sources.exists(_.accept(e.entry.typedPath.getPath))) Watched.Trigger - else if (projectSources.exists(_.accept(e.entry.typedPath.getPath))) Watched.Reload - else Watched.Ignore - }, + watchOnEvent := Watched + .onEvent(watchTransitiveSources.value, watchProjectTransitiveSources.value), watchHandleInput := Watched.handleInput, watchPreWatch := { (_, _) => Watched.Ignore From a23479dfb104ca1bb0abb3e8c8494767dfa4d248 Mon Sep 17 00:00:00 2001 From: andrea Date: Tue, 9 Oct 2018 10:20:01 +0100 Subject: [PATCH 5/6] fixing ServerSpec flaky tests --- sbt/src/test/scala/testpkg/ServerSpec.scala | 183 ++++++++++++-------- 1 file changed, 109 insertions(+), 74 deletions(-) diff --git a/sbt/src/test/scala/testpkg/ServerSpec.scala b/sbt/src/test/scala/testpkg/ServerSpec.scala index e13532489..acfc2b323 100644 --- a/sbt/src/test/scala/testpkg/ServerSpec.scala +++ b/sbt/src/test/scala/testpkg/ServerSpec.scala @@ -11,6 +11,7 @@ import org.scalatest._ import scala.concurrent._ import scala.annotation.tailrec import sbt.protocol.ClientSocket +import scala.util.Try import TestServer.withTestServer import java.io.File import sbt.io.syntax._ @@ -19,101 +20,129 @@ import sbt.RunFromSourceMain import scala.concurrent.ExecutionContext import java.util.concurrent.ForkJoinPool -class ServerSpec extends AsyncFreeSpec with Matchers { +class ServerSpec extends fixture.AsyncFreeSpec with fixture.AsyncTestDataFixture with Matchers { "server" - { - "should start" in withTestServer("handshake") { p => - p.writeLine( - """{ "jsonrpc": "2.0", "id": "3", "method": "sbt/setting", "params": { "setting": "root/name" } }""" - ) - assert(p.waitForString(10) { s => - s contains """"id":"3"""" - }) + "should start" in { implicit td => + withTestServer("handshake") { p => + p.writeLine( + """{ "jsonrpc": "2.0", "id": "3", "method": "sbt/setting", "params": { "setting": "root/name" } }""" + ) + assert(p.waitForString(10) { s => + s contains """"id":"3"""" + }) + } } - "return number id when number id is sent" in withTestServer("handshake") { p => - p.writeLine( - """{ "jsonrpc": "2.0", "id": 3, "method": "sbt/setting", "params": { "setting": "root/name" } }""" - ) - assert(p.waitForString(10) { s => - s contains """"id":3""" - }) + "return number id when number id is sent" in { implicit td => + withTestServer("handshake") { p => + p.writeLine( + """{ "jsonrpc": "2.0", "id": 3, "method": "sbt/setting", "params": { "setting": "root/name" } }""" + ) + assert(p.waitForString(10) { s => + s contains """"id":3""" + }) + } } - "report task failures in case of exceptions" in withTestServer("events") { p => - p.writeLine( - """{ "jsonrpc": "2.0", "id": 11, "method": "sbt/exec", "params": { "commandLine": "hello" } }""" - ) - assert(p.waitForString(10) { s => - (s contains """"id":11""") && (s contains """"error":""") - }) + "report task failures in case of exceptions" in { implicit td => + withTestServer("events") { p => + p.writeLine( + """{ "jsonrpc": "2.0", "id": 11, "method": "sbt/exec", "params": { "commandLine": "hello" } }""" + ) + assert(p.waitForString(10) { s => + (s contains """"id":11""") && (s contains """"error":""") + }) + } } - "return error if cancelling non-matched task id" in withTestServer("events") { p => - p.writeLine( - """{ "jsonrpc": "2.0", "id":12, "method": "sbt/exec", "params": { "commandLine": "run" } }""" - ) - p.writeLine( - """{ "jsonrpc": "2.0", "id":13, "method": "sbt/cancelRequest", "params": { "id": "55" } }""" - ) + "return error if cancelling non-matched task id" in { implicit td => + withTestServer("events") { p => + p.writeLine( + """{ "jsonrpc": "2.0", "id":12, "method": "sbt/exec", "params": { "commandLine": "run" } }""" + ) + p.writeLine( + """{ "jsonrpc": "2.0", "id":13, "method": "sbt/cancelRequest", "params": { "id": "55" } }""" + ) - assert(p.waitForString(20) { s => - (s contains """"error":{"code":-32800""") - }) + assert(p.waitForString(20) { s => + (s contains """"error":{"code":-32800""") + }) + } } - "cancel on-going task with numeric id" in withTestServer("events") { p => - p.writeLine( - """{ "jsonrpc": "2.0", "id":12, "method": "sbt/exec", "params": { "commandLine": "run" } }""" - ) + "cancel on-going task with numeric id" in { implicit td => + withTestServer("events") { p => + p.writeLine( + """{ "jsonrpc": "2.0", "id":12, "method": "sbt/exec", "params": { "commandLine": "run" } }""" + ) - Thread.sleep(1000) - - p.writeLine( - """{ "jsonrpc": "2.0", "id":13, "method": "sbt/cancelRequest", "params": { "id": "12" } }""" - ) - - assert(p.waitForString(30) { s => - s contains """"result":{"status":"Task cancelled"""" - }) + assert(p.waitForString(60) { s => + p.writeLine( + """{ "jsonrpc": "2.0", "id":13, "method": "sbt/cancelRequest", "params": { "id": "12" } }""" + ) + s contains """"result":{"status":"Task cancelled"""" + }) + } } - "cancel on-going task with string id" in withTestServer("events") { p => - p.writeLine( - """{ "jsonrpc": "2.0", "id": "foo", "method": "sbt/exec", "params": { "commandLine": "run" } }""" - ) + "cancel on-going task with string id" in { implicit td => + withTestServer("events") { p => + p.writeLine( + """{ "jsonrpc": "2.0", "id": "foo", "method": "sbt/exec", "params": { "commandLine": "run" } }""" + ) - Thread.sleep(1000) - - p.writeLine( - """{ "jsonrpc": "2.0", "id": "bar", "method": "sbt/cancelRequest", "params": { "id": "foo" } }""" - ) - - assert(p.waitForString(30) { s => - s contains """"result":{"status":"Task cancelled"""" - }) + assert(p.waitForString(60) { s => + p.writeLine( + """{ "jsonrpc": "2.0", "id": "bar", "method": "sbt/cancelRequest", "params": { "id": "foo" } }""" + ) + s contains """"result":{"status":"Task cancelled"""" + }) + } } } } object TestServer { - // The test server instance will be executed in a Thread pool separated from the tests - implicit val ec = ExecutionContext.fromExecutor(new ForkJoinPool()) private val serverTestBase: File = new File(".").getAbsoluteFile / "sbt" / "src" / "server-test" - def withTestServer(testBuild: String)(f: TestServer => Future[Assertion]): Future[Assertion] = { + def withTestServer( + testBuild: String + )(f: TestServer => Future[Assertion])(implicit td: TestData): Future[Assertion] = { + println(s"Starting test: ${td.name}") IO.withTemporaryDirectory { temp => IO.copyDirectory(serverTestBase / testBuild, temp / testBuild) - withTestServer(temp / testBuild)(f) + withTestServer(testBuild, temp / testBuild)(f) } } - def withTestServer(baseDirectory: File)(f: TestServer => Future[Assertion]): Future[Assertion] = { - val testServer = TestServer(baseDirectory) - try { - f(testServer) - } finally { - try { testServer.bye() } finally {} + def withTestServer(testBuild: String, baseDirectory: File)( + f: TestServer => Future[Assertion] + )(implicit td: TestData): Future[Assertion] = { + // Each test server instance will be executed in a Thread pool separated from the tests + val testServer = TestServer(baseDirectory)( + ExecutionContext.fromExecutor(new ForkJoinPool()) + ) + // checking last log message after initialization + // if something goes wrong here the communication streams are corrupted, restarting + val init = + Try { + testServer.waitForString(30) { s => + s contains """"message":"Done"""" + } + }.toOption + + init match { + case Some(_) => + try { + f(testServer) + } finally { + try { testServer.bye() } finally {} + } + case _ => + try { testServer.bye() } finally {} + hostLog("Server started but not connected properly... restarting...") + withTestServer(testBuild)(f) } } @@ -216,13 +245,19 @@ case class TestServer(baseDirectory: File)(implicit ec: ExecutionContext) { @tailrec final def waitForString(num: Int)(f: String => Boolean): Boolean = { - if (num < 0) false - else - readFrame match { - case Some(x) if f(x) => true - case _ => - waitForString(num - 1)(f) + if (num < 0) { throw new Exception("Retries are over.") } else { + // readFrame should be called in another Thread in orrder to be able to time limit it's execution + val res = Future { readFrame }(ec) + + import scala.concurrent.duration._ + Try { + Await.result(res, 1.second) + }.toOption.flatten match { + // function f should be called in this Thread in order to be executed exactly once before eventually returning + case Some(str) if f(str) => true + case _ => waitForString(num - 1)(f) } + } } def readLine: Option[String] = { From 34e0fc159c19f2d0c784b881b50264916c977b11 Mon Sep 17 00:00:00 2001 From: andrea Date: Tue, 2 Oct 2018 15:51:20 +0100 Subject: [PATCH 6/6] [sbt-server] LSP completions support --- build.sbt | 1 + main-command/src/main/scala/sbt/State.scala | 22 ++++- .../sbt/internal/server/ServerHandler.scala | 3 +- main/src/main/scala/sbt/EvaluateTask.scala | 2 + .../server/LanguageServerProtocol.scala | 9 +- .../sbt/internal/server/NetworkChannel.scala | 44 +++++++++ .../sbt/internal/langserver/Command.scala | 28 ++++++ .../langserver/CompletionContext.scala | 40 ++++++++ .../internal/langserver/CompletionItem.scala | 32 +++++++ .../internal/langserver/CompletionList.scala | 36 +++++++ .../langserver/CompletionParams.scala | 50 ++++++++++ .../TextDocumentPositionParams.scala | 5 +- .../TextDocumentPositionParamsInterface.scala | 28 ++++++ .../sbt/internal/langserver/TextEdit.scala | 36 +++++++ .../langserver/codec/CommandFormats.scala | 17 ++++ .../codec/CompletionContextFormats.scala | 29 ++++++ .../codec/CompletionItemFormats.scala | 27 ++++++ .../codec/CompletionListFormats.scala | 29 ++++++ .../codec/CompletionParamsFormats.scala | 31 +++++++ .../langserver/codec/JsonProtocol.scala | 1 + ...cumentPositionParamsInterfaceFormats.scala | 11 +++ .../langserver/codec/TextEditFormats.scala | 29 ++++++ .../sbt/protocol/CompletionParams.scala | 32 +++++++ .../sbt/protocol/CompletionResponse.scala | 32 +++++++ .../codec/CompletionParamsFormats.scala | 27 ++++++ .../codec/CompletionResponseFormats.scala | 27 ++++++ .../sbt/protocol/codec/JsonProtocol.scala | 2 + protocol/src/main/contraband/lsp.contra | 10 +- protocol/src/main/contraband/server.contra | 7 ++ sbt/src/server-test/completions/build.sbt | 6 ++ .../src/test/scala/ExampleSpec.scala | 10 ++ sbt/src/test/scala/testpkg/ServerSpec.scala | 93 ++++++++++++++----- 32 files changed, 724 insertions(+), 32 deletions(-) create mode 100644 protocol/src/main/contraband-scala/sbt/internal/langserver/Command.scala create mode 100644 protocol/src/main/contraband-scala/sbt/internal/langserver/CompletionContext.scala create mode 100644 protocol/src/main/contraband-scala/sbt/internal/langserver/CompletionItem.scala create mode 100644 protocol/src/main/contraband-scala/sbt/internal/langserver/CompletionList.scala create mode 100644 protocol/src/main/contraband-scala/sbt/internal/langserver/CompletionParams.scala create mode 100644 protocol/src/main/contraband-scala/sbt/internal/langserver/TextDocumentPositionParamsInterface.scala create mode 100644 protocol/src/main/contraband-scala/sbt/internal/langserver/TextEdit.scala create mode 100644 protocol/src/main/contraband-scala/sbt/internal/langserver/codec/CommandFormats.scala create mode 100644 protocol/src/main/contraband-scala/sbt/internal/langserver/codec/CompletionContextFormats.scala create mode 100644 protocol/src/main/contraband-scala/sbt/internal/langserver/codec/CompletionItemFormats.scala create mode 100644 protocol/src/main/contraband-scala/sbt/internal/langserver/codec/CompletionListFormats.scala create mode 100644 protocol/src/main/contraband-scala/sbt/internal/langserver/codec/CompletionParamsFormats.scala create mode 100644 protocol/src/main/contraband-scala/sbt/internal/langserver/codec/TextDocumentPositionParamsInterfaceFormats.scala create mode 100644 protocol/src/main/contraband-scala/sbt/internal/langserver/codec/TextEditFormats.scala create mode 100644 protocol/src/main/contraband-scala/sbt/protocol/CompletionParams.scala create mode 100644 protocol/src/main/contraband-scala/sbt/protocol/CompletionResponse.scala create mode 100644 protocol/src/main/contraband-scala/sbt/protocol/codec/CompletionParamsFormats.scala create mode 100644 protocol/src/main/contraband-scala/sbt/protocol/codec/CompletionResponseFormats.scala create mode 100644 sbt/src/server-test/completions/build.sbt create mode 100644 sbt/src/server-test/completions/src/test/scala/ExampleSpec.scala diff --git a/build.sbt b/build.sbt index 5d8694d49..91dd01c9a 100644 --- a/build.sbt +++ b/build.sbt @@ -93,6 +93,7 @@ val mimaSettings = Def settings ( exclude[FinalClassProblem]("sbt.internal.*"), exclude[FinalMethodProblem]("sbt.internal.*"), exclude[IncompatibleResultTypeProblem]("sbt.internal.*"), + exclude[ReversedMissingMethodProblem]("sbt.internal.*") ), ) diff --git a/main-command/src/main/scala/sbt/State.scala b/main-command/src/main/scala/sbt/State.scala index d1e8aa0b8..0f38eaf57 100644 --- a/main-command/src/main/scala/sbt/State.scala +++ b/main-command/src/main/scala/sbt/State.scala @@ -18,7 +18,7 @@ import sbt.internal.util.{ ExitHooks, GlobalLogging } -import sbt.internal.util.complete.HistoryCommands +import sbt.internal.util.complete.{ HistoryCommands, Parser } import sbt.internal.inc.classpath.ClassLoaderCache /** @@ -54,6 +54,26 @@ final case class State( } } +/** + * Data structure extracted form the State Machine for safe observability purposes. + * + * @param currentExecId provide the execId extracted from the original State. + * @param combinedParser the parser extracted from the original State. + */ +private[sbt] final case class SafeState( + currentExecId: Option[String], + combinedParser: Parser[() => sbt.State] +) + +private[sbt] object SafeState { + def apply(s: State) = { + new SafeState( + currentExecId = s.currentCommand.map(_.execId).flatten, + combinedParser = s.combinedParser + ) + } +} + trait Identity { override final def hashCode = super.hashCode override final def equals(a: Any) = super.equals(a) diff --git a/main-command/src/main/scala/sbt/internal/server/ServerHandler.scala b/main-command/src/main/scala/sbt/internal/server/ServerHandler.scala index c6db2994b..befc7fabf 100644 --- a/main-command/src/main/scala/sbt/internal/server/ServerHandler.scala +++ b/main-command/src/main/scala/sbt/internal/server/ServerHandler.scala @@ -12,7 +12,7 @@ package server import sjsonnew.JsonFormat import sbt.internal.protocol._ import sbt.util.Logger -import sbt.protocol.{ SettingQuery => Q } +import sbt.protocol.{ SettingQuery => Q, CompletionParams => CP } /** * ServerHandler allows plugins to extend sbt server. @@ -70,4 +70,5 @@ trait ServerCallback { private[sbt] def authenticate(token: String): Boolean private[sbt] def setInitialized(value: Boolean): Unit private[sbt] def onSettingQuery(execId: Option[String], req: Q): Unit + private[sbt] def onCompletionRequest(execId: Option[String], cp: CP): Unit } diff --git a/main/src/main/scala/sbt/EvaluateTask.scala b/main/src/main/scala/sbt/EvaluateTask.scala index b1e2ff7f5..d0a630e1c 100644 --- a/main/src/main/scala/sbt/EvaluateTask.scala +++ b/main/src/main/scala/sbt/EvaluateTask.scala @@ -383,6 +383,7 @@ object EvaluateTask { (dummyRoots, roots) :: (Def.dummyStreamsManager, streams) :: (dummyState, state) :: dummies ) + val lastEvaluatedState: AtomicReference[SafeState] = new AtomicReference() val currentlyRunningEngine: AtomicReference[(State, RunningTaskEngine)] = new AtomicReference() /** @@ -452,6 +453,7 @@ object EvaluateTask { finally { strat.onTaskEngineFinish(cancelState) currentlyRunningEngine.set(null) + lastEvaluatedState.set(SafeState(state)) } } diff --git a/main/src/main/scala/sbt/internal/server/LanguageServerProtocol.scala b/main/src/main/scala/sbt/internal/server/LanguageServerProtocol.scala index ce035e158..80948027e 100644 --- a/main/src/main/scala/sbt/internal/server/LanguageServerProtocol.scala +++ b/main/src/main/scala/sbt/internal/server/LanguageServerProtocol.scala @@ -13,7 +13,7 @@ import sjsonnew.JsonFormat import sjsonnew.shaded.scalajson.ast.unsafe.JValue import sjsonnew.support.scalajson.unsafe.Converter import sbt.protocol.Serialization -import sbt.protocol.{ SettingQuery => Q, ExecStatusEvent } +import sbt.protocol.{ SettingQuery => Q, ExecStatusEvent, CompletionParams => CP } import sbt.internal.protocol._ import sbt.internal.protocol.codec._ import sbt.internal.langserver._ @@ -135,6 +135,10 @@ private[sbt] object LanguageServerProtocol { case NonFatal(e) => errorRespond("Cancel request failed") } + case r: JsonRpcRequestMessage if r.method == "sbt/completion" => + import sbt.protocol.codec.JsonProtocol._ + val param = Converter.fromJson[CP](json(r)).get + onCompletionRequest(Option(r.id), param) } }, { case n: JsonRpcNotificationMessage if n.method == "textDocument/didSave" => @@ -155,6 +159,7 @@ private[sbt] trait LanguageServerProtocol extends CommandChannel { self => protected def setInitialized(value: Boolean): Unit protected def log: Logger protected def onSettingQuery(execId: Option[String], req: Q): Unit + protected def onCompletionRequest(execId: Option[String], cp: CP): Unit protected lazy val callbackImpl: ServerCallback = new ServerCallback { def jsonRpcRespond[A: JsonFormat](event: A, execId: Option[String]): Unit = @@ -174,6 +179,8 @@ private[sbt] trait LanguageServerProtocol extends CommandChannel { self => private[sbt] def setInitialized(value: Boolean): Unit = self.setInitialized(value) private[sbt] def onSettingQuery(execId: Option[String], req: Q): Unit = self.onSettingQuery(execId, req) + private[sbt] def onCompletionRequest(execId: Option[String], cp: CP): Unit = + self.onCompletionRequest(execId, cp) } /** diff --git a/main/src/main/scala/sbt/internal/server/NetworkChannel.scala b/main/src/main/scala/sbt/internal/server/NetworkChannel.scala index d56f59e4e..2cd81c050 100644 --- a/main/src/main/scala/sbt/internal/server/NetworkChannel.scala +++ b/main/src/main/scala/sbt/internal/server/NetworkChannel.scala @@ -17,9 +17,11 @@ import scala.annotation.tailrec import sbt.protocol._ import sbt.internal.langserver.ErrorCodes import sbt.internal.util.{ ObjectEvent, StringEvent } +import sbt.internal.util.complete.Parser import sbt.internal.util.codec.JValueFormats import sbt.internal.protocol.{ JsonRpcRequestMessage, JsonRpcNotificationMessage } import sbt.util.Logger +import scala.util.control.NonFatal final class NetworkChannel( val name: String, @@ -364,6 +366,48 @@ final class NetworkChannel( } } + protected def onCompletionRequest(execId: Option[String], cp: CompletionParams) = { + if (initialized) { + try { + Option(EvaluateTask.lastEvaluatedState.get) match { + case Some(sstate) => + val completionItems = + Parser + .completions(sstate.combinedParser, cp.query, 9) + .get + .map(c => { + if (!c.isEmpty) Some(c.append.replaceAll("\n", " ")) + else None + }) + .flatten + .map(c => cp.query + c.toString) + import sbt.protocol.codec.JsonProtocol._ + jsonRpcRespond( + CompletionResponse( + items = completionItems.toVector + ), + execId + ) + case _ => + jsonRpcRespondError( + execId, + ErrorCodes.UnknownError, + "No available sbt state" + ) + } + } catch { + case NonFatal(e) => + jsonRpcRespondError( + execId, + ErrorCodes.UnknownError, + "Completions request failed" + ) + } + } else { + log.warn(s"ignoring completion request $cp before initialization") + } + } + def shutdown(): Unit = { log.info("Shutting down client connection") running.set(false) diff --git a/protocol/src/main/contraband-scala/sbt/internal/langserver/Command.scala b/protocol/src/main/contraband-scala/sbt/internal/langserver/Command.scala new file mode 100644 index 000000000..f3c320752 --- /dev/null +++ b/protocol/src/main/contraband-scala/sbt/internal/langserver/Command.scala @@ -0,0 +1,28 @@ +/** + * This code is generated using [[http://www.scala-sbt.org/contraband/ sbt-contraband]]. + */ + +// DO NOT EDIT MANUALLY +package sbt.internal.langserver +abstract class Command( + val title: Option[String], + val command: Option[String], + val arguments: Vector[String]) extends Serializable { + + + + + override def equals(o: Any): Boolean = o match { + case x: Command => (this.title == x.title) && (this.command == x.command) && (this.arguments == x.arguments) + case _ => false + } + override def hashCode: Int = { + 37 * (37 * (37 * (37 * (17 + "sbt.internal.langserver.Command".##) + title.##) + command.##) + arguments.##) + } + override def toString: String = { + "Command(" + title + ", " + command + ", " + arguments + ")" + } +} +object Command { + +} diff --git a/protocol/src/main/contraband-scala/sbt/internal/langserver/CompletionContext.scala b/protocol/src/main/contraband-scala/sbt/internal/langserver/CompletionContext.scala new file mode 100644 index 000000000..422069c97 --- /dev/null +++ b/protocol/src/main/contraband-scala/sbt/internal/langserver/CompletionContext.scala @@ -0,0 +1,40 @@ +/** + * This code is generated using [[http://www.scala-sbt.org/contraband/ sbt-contraband]]. + */ + +// DO NOT EDIT MANUALLY +package sbt.internal.langserver +final class CompletionContext private ( + val triggerKind: Int, + val triggerCharacter: Option[String]) extends Serializable { + + + + override def equals(o: Any): Boolean = o match { + case x: CompletionContext => (this.triggerKind == x.triggerKind) && (this.triggerCharacter == x.triggerCharacter) + case _ => false + } + override def hashCode: Int = { + 37 * (37 * (37 * (17 + "sbt.internal.langserver.CompletionContext".##) + triggerKind.##) + triggerCharacter.##) + } + override def toString: String = { + "CompletionContext(" + triggerKind + ", " + triggerCharacter + ")" + } + private[this] def copy(triggerKind: Int = triggerKind, triggerCharacter: Option[String] = triggerCharacter): CompletionContext = { + new CompletionContext(triggerKind, triggerCharacter) + } + def withTriggerKind(triggerKind: Int): CompletionContext = { + copy(triggerKind = triggerKind) + } + def withTriggerCharacter(triggerCharacter: Option[String]): CompletionContext = { + copy(triggerCharacter = triggerCharacter) + } + def withTriggerCharacter(triggerCharacter: String): CompletionContext = { + copy(triggerCharacter = Option(triggerCharacter)) + } +} +object CompletionContext { + + def apply(triggerKind: Int, triggerCharacter: Option[String]): CompletionContext = new CompletionContext(triggerKind, triggerCharacter) + def apply(triggerKind: Int, triggerCharacter: String): CompletionContext = new CompletionContext(triggerKind, Option(triggerCharacter)) +} diff --git a/protocol/src/main/contraband-scala/sbt/internal/langserver/CompletionItem.scala b/protocol/src/main/contraband-scala/sbt/internal/langserver/CompletionItem.scala new file mode 100644 index 000000000..f10d989fc --- /dev/null +++ b/protocol/src/main/contraband-scala/sbt/internal/langserver/CompletionItem.scala @@ -0,0 +1,32 @@ +/** + * This code is generated using [[http://www.scala-sbt.org/contraband/ sbt-contraband]]. + */ + +// DO NOT EDIT MANUALLY +package sbt.internal.langserver +final class CompletionItem private ( + val label: String) extends Serializable { + + + + override def equals(o: Any): Boolean = o match { + case x: CompletionItem => (this.label == x.label) + case _ => false + } + override def hashCode: Int = { + 37 * (37 * (17 + "sbt.internal.langserver.CompletionItem".##) + label.##) + } + override def toString: String = { + "CompletionItem(" + label + ")" + } + private[this] def copy(label: String = label): CompletionItem = { + new CompletionItem(label) + } + def withLabel(label: String): CompletionItem = { + copy(label = label) + } +} +object CompletionItem { + + def apply(label: String): CompletionItem = new CompletionItem(label) +} diff --git a/protocol/src/main/contraband-scala/sbt/internal/langserver/CompletionList.scala b/protocol/src/main/contraband-scala/sbt/internal/langserver/CompletionList.scala new file mode 100644 index 000000000..ffebf8bd8 --- /dev/null +++ b/protocol/src/main/contraband-scala/sbt/internal/langserver/CompletionList.scala @@ -0,0 +1,36 @@ +/** + * This code is generated using [[http://www.scala-sbt.org/contraband/ sbt-contraband]]. + */ + +// DO NOT EDIT MANUALLY +package sbt.internal.langserver +final class CompletionList private ( + val isIncomplete: Boolean, + val items: Vector[sbt.internal.langserver.CompletionItem]) extends Serializable { + + + + override def equals(o: Any): Boolean = o match { + case x: CompletionList => (this.isIncomplete == x.isIncomplete) && (this.items == x.items) + case _ => false + } + override def hashCode: Int = { + 37 * (37 * (37 * (17 + "sbt.internal.langserver.CompletionList".##) + isIncomplete.##) + items.##) + } + override def toString: String = { + "CompletionList(" + isIncomplete + ", " + items + ")" + } + private[this] def copy(isIncomplete: Boolean = isIncomplete, items: Vector[sbt.internal.langserver.CompletionItem] = items): CompletionList = { + new CompletionList(isIncomplete, items) + } + def withIsIncomplete(isIncomplete: Boolean): CompletionList = { + copy(isIncomplete = isIncomplete) + } + def withItems(items: Vector[sbt.internal.langserver.CompletionItem]): CompletionList = { + copy(items = items) + } +} +object CompletionList { + + def apply(isIncomplete: Boolean, items: Vector[sbt.internal.langserver.CompletionItem]): CompletionList = new CompletionList(isIncomplete, items) +} diff --git a/protocol/src/main/contraband-scala/sbt/internal/langserver/CompletionParams.scala b/protocol/src/main/contraband-scala/sbt/internal/langserver/CompletionParams.scala new file mode 100644 index 000000000..3859a64d3 --- /dev/null +++ b/protocol/src/main/contraband-scala/sbt/internal/langserver/CompletionParams.scala @@ -0,0 +1,50 @@ +/** + * This code is generated using [[http://www.scala-sbt.org/contraband/ sbt-contraband]]. + */ + +// DO NOT EDIT MANUALLY +package sbt.internal.langserver +/** + * Completion request interfaces + * @param textDocument The text document. + * @param position The position inside the text document. + * @param context completion context + */ +final class CompletionParams private ( + textDocument: sbt.internal.langserver.TextDocumentIdentifier, + position: sbt.internal.langserver.Position, + val context: Option[sbt.internal.langserver.CompletionContext]) extends sbt.internal.langserver.TextDocumentPositionParamsInterface(textDocument, position) with Serializable { + + + + override def equals(o: Any): Boolean = o match { + case x: CompletionParams => (this.textDocument == x.textDocument) && (this.position == x.position) && (this.context == x.context) + case _ => false + } + override def hashCode: Int = { + 37 * (37 * (37 * (37 * (17 + "sbt.internal.langserver.CompletionParams".##) + textDocument.##) + position.##) + context.##) + } + override def toString: String = { + "CompletionParams(" + textDocument + ", " + position + ", " + context + ")" + } + private[this] def copy(textDocument: sbt.internal.langserver.TextDocumentIdentifier = textDocument, position: sbt.internal.langserver.Position = position, context: Option[sbt.internal.langserver.CompletionContext] = context): CompletionParams = { + new CompletionParams(textDocument, position, context) + } + def withTextDocument(textDocument: sbt.internal.langserver.TextDocumentIdentifier): CompletionParams = { + copy(textDocument = textDocument) + } + def withPosition(position: sbt.internal.langserver.Position): CompletionParams = { + copy(position = position) + } + def withContext(context: Option[sbt.internal.langserver.CompletionContext]): CompletionParams = { + copy(context = context) + } + def withContext(context: sbt.internal.langserver.CompletionContext): CompletionParams = { + copy(context = Option(context)) + } +} +object CompletionParams { + + def apply(textDocument: sbt.internal.langserver.TextDocumentIdentifier, position: sbt.internal.langserver.Position, context: Option[sbt.internal.langserver.CompletionContext]): CompletionParams = new CompletionParams(textDocument, position, context) + def apply(textDocument: sbt.internal.langserver.TextDocumentIdentifier, position: sbt.internal.langserver.Position, context: sbt.internal.langserver.CompletionContext): CompletionParams = new CompletionParams(textDocument, position, Option(context)) +} diff --git a/protocol/src/main/contraband-scala/sbt/internal/langserver/TextDocumentPositionParams.scala b/protocol/src/main/contraband-scala/sbt/internal/langserver/TextDocumentPositionParams.scala index 7b8b5ac53..409485082 100644 --- a/protocol/src/main/contraband-scala/sbt/internal/langserver/TextDocumentPositionParams.scala +++ b/protocol/src/main/contraband-scala/sbt/internal/langserver/TextDocumentPositionParams.scala @@ -5,13 +5,12 @@ // DO NOT EDIT MANUALLY package sbt.internal.langserver /** - * Goto definition params model * @param textDocument The text document. * @param position The position inside the text document. */ final class TextDocumentPositionParams private ( - val textDocument: sbt.internal.langserver.TextDocumentIdentifier, - val position: sbt.internal.langserver.Position) extends Serializable { + textDocument: sbt.internal.langserver.TextDocumentIdentifier, + position: sbt.internal.langserver.Position) extends sbt.internal.langserver.TextDocumentPositionParamsInterface(textDocument, position) with Serializable { diff --git a/protocol/src/main/contraband-scala/sbt/internal/langserver/TextDocumentPositionParamsInterface.scala b/protocol/src/main/contraband-scala/sbt/internal/langserver/TextDocumentPositionParamsInterface.scala new file mode 100644 index 000000000..3ee063ecc --- /dev/null +++ b/protocol/src/main/contraband-scala/sbt/internal/langserver/TextDocumentPositionParamsInterface.scala @@ -0,0 +1,28 @@ +/** + * 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 */ +abstract class TextDocumentPositionParamsInterface( + val textDocument: sbt.internal.langserver.TextDocumentIdentifier, + val position: sbt.internal.langserver.Position) extends Serializable { + + + + + override def equals(o: Any): Boolean = o match { + case x: TextDocumentPositionParamsInterface => (this.textDocument == x.textDocument) && (this.position == x.position) + case _ => false + } + override def hashCode: Int = { + 37 * (37 * (37 * (17 + "sbt.internal.langserver.TextDocumentPositionParamsInterface".##) + textDocument.##) + position.##) + } + override def toString: String = { + "TextDocumentPositionParamsInterface(" + textDocument + ", " + position + ")" + } +} +object TextDocumentPositionParamsInterface { + +} diff --git a/protocol/src/main/contraband-scala/sbt/internal/langserver/TextEdit.scala b/protocol/src/main/contraband-scala/sbt/internal/langserver/TextEdit.scala new file mode 100644 index 000000000..627772312 --- /dev/null +++ b/protocol/src/main/contraband-scala/sbt/internal/langserver/TextEdit.scala @@ -0,0 +1,36 @@ +/** + * This code is generated using [[http://www.scala-sbt.org/contraband/ sbt-contraband]]. + */ + +// DO NOT EDIT MANUALLY +package sbt.internal.langserver +final class TextEdit private ( + val range: sbt.internal.langserver.Range, + val newText: String) extends Serializable { + + + + override def equals(o: Any): Boolean = o match { + case x: TextEdit => (this.range == x.range) && (this.newText == x.newText) + case _ => false + } + override def hashCode: Int = { + 37 * (37 * (37 * (17 + "sbt.internal.langserver.TextEdit".##) + range.##) + newText.##) + } + override def toString: String = { + "TextEdit(" + range + ", " + newText + ")" + } + private[this] def copy(range: sbt.internal.langserver.Range = range, newText: String = newText): TextEdit = { + new TextEdit(range, newText) + } + def withRange(range: sbt.internal.langserver.Range): TextEdit = { + copy(range = range) + } + def withNewText(newText: String): TextEdit = { + copy(newText = newText) + } +} +object TextEdit { + + def apply(range: sbt.internal.langserver.Range, newText: String): TextEdit = new TextEdit(range, newText) +} diff --git a/protocol/src/main/contraband-scala/sbt/internal/langserver/codec/CommandFormats.scala b/protocol/src/main/contraband-scala/sbt/internal/langserver/codec/CommandFormats.scala new file mode 100644 index 000000000..ac4efad4b --- /dev/null +++ b/protocol/src/main/contraband-scala/sbt/internal/langserver/codec/CommandFormats.scala @@ -0,0 +1,17 @@ +/** + * 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.{ deserializationError, serializationError, Builder, JsonFormat, Unbuilder } +trait CommandFormats { + implicit lazy val CommandFormat: JsonFormat[sbt.internal.langserver.Command] = new JsonFormat[sbt.internal.langserver.Command] { + override def read[J](jsOpt: Option[J], unbuilder: Unbuilder[J]): sbt.internal.langserver.Command = { + deserializationError("No known implementation of Command.") + } + override def write[J](obj: sbt.internal.langserver.Command, builder: Builder[J]): Unit = { + serializationError("No known implementation of Command.") + } + } +} diff --git a/protocol/src/main/contraband-scala/sbt/internal/langserver/codec/CompletionContextFormats.scala b/protocol/src/main/contraband-scala/sbt/internal/langserver/codec/CompletionContextFormats.scala new file mode 100644 index 000000000..ef59ec048 --- /dev/null +++ b/protocol/src/main/contraband-scala/sbt/internal/langserver/codec/CompletionContextFormats.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 CompletionContextFormats { self: sjsonnew.BasicJsonProtocol => +implicit lazy val CompletionContextFormat: JsonFormat[sbt.internal.langserver.CompletionContext] = new JsonFormat[sbt.internal.langserver.CompletionContext] { + override def read[J](jsOpt: Option[J], unbuilder: Unbuilder[J]): sbt.internal.langserver.CompletionContext = { + jsOpt match { + case Some(js) => + unbuilder.beginObject(js) + val triggerKind = unbuilder.readField[Int]("triggerKind") + val triggerCharacter = unbuilder.readField[Option[String]]("triggerCharacter") + unbuilder.endObject() + sbt.internal.langserver.CompletionContext(triggerKind, triggerCharacter) + case None => + deserializationError("Expected JsObject but found None") + } + } + override def write[J](obj: sbt.internal.langserver.CompletionContext, builder: Builder[J]): Unit = { + builder.beginObject() + builder.addField("triggerKind", obj.triggerKind) + builder.addField("triggerCharacter", obj.triggerCharacter) + builder.endObject() + } +} +} diff --git a/protocol/src/main/contraband-scala/sbt/internal/langserver/codec/CompletionItemFormats.scala b/protocol/src/main/contraband-scala/sbt/internal/langserver/codec/CompletionItemFormats.scala new file mode 100644 index 000000000..56c4aa129 --- /dev/null +++ b/protocol/src/main/contraband-scala/sbt/internal/langserver/codec/CompletionItemFormats.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 CompletionItemFormats { self: sjsonnew.BasicJsonProtocol => +implicit lazy val CompletionItemFormat: JsonFormat[sbt.internal.langserver.CompletionItem] = new JsonFormat[sbt.internal.langserver.CompletionItem] { + override def read[J](jsOpt: Option[J], unbuilder: Unbuilder[J]): sbt.internal.langserver.CompletionItem = { + jsOpt match { + case Some(js) => + unbuilder.beginObject(js) + val label = unbuilder.readField[String]("label") + unbuilder.endObject() + sbt.internal.langserver.CompletionItem(label) + case None => + deserializationError("Expected JsObject but found None") + } + } + override def write[J](obj: sbt.internal.langserver.CompletionItem, builder: Builder[J]): Unit = { + builder.beginObject() + builder.addField("label", obj.label) + builder.endObject() + } +} +} diff --git a/protocol/src/main/contraband-scala/sbt/internal/langserver/codec/CompletionListFormats.scala b/protocol/src/main/contraband-scala/sbt/internal/langserver/codec/CompletionListFormats.scala new file mode 100644 index 000000000..21397041d --- /dev/null +++ b/protocol/src/main/contraband-scala/sbt/internal/langserver/codec/CompletionListFormats.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 CompletionListFormats { self: sbt.internal.langserver.codec.CompletionItemFormats with sjsonnew.BasicJsonProtocol => +implicit lazy val CompletionListFormat: JsonFormat[sbt.internal.langserver.CompletionList] = new JsonFormat[sbt.internal.langserver.CompletionList] { + override def read[J](jsOpt: Option[J], unbuilder: Unbuilder[J]): sbt.internal.langserver.CompletionList = { + jsOpt match { + case Some(js) => + unbuilder.beginObject(js) + val isIncomplete = unbuilder.readField[Boolean]("isIncomplete") + val items = unbuilder.readField[Vector[sbt.internal.langserver.CompletionItem]]("items") + unbuilder.endObject() + sbt.internal.langserver.CompletionList(isIncomplete, items) + case None => + deserializationError("Expected JsObject but found None") + } + } + override def write[J](obj: sbt.internal.langserver.CompletionList, builder: Builder[J]): Unit = { + builder.beginObject() + builder.addField("isIncomplete", obj.isIncomplete) + builder.addField("items", obj.items) + builder.endObject() + } +} +} diff --git a/protocol/src/main/contraband-scala/sbt/internal/langserver/codec/CompletionParamsFormats.scala b/protocol/src/main/contraband-scala/sbt/internal/langserver/codec/CompletionParamsFormats.scala new file mode 100644 index 000000000..7599ee7ed --- /dev/null +++ b/protocol/src/main/contraband-scala/sbt/internal/langserver/codec/CompletionParamsFormats.scala @@ -0,0 +1,31 @@ +/** + * 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 CompletionParamsFormats { self: sbt.internal.langserver.codec.TextDocumentIdentifierFormats with sbt.internal.langserver.codec.PositionFormats with sbt.internal.langserver.codec.CompletionContextFormats with sjsonnew.BasicJsonProtocol => +implicit lazy val CompletionParamsFormat: JsonFormat[sbt.internal.langserver.CompletionParams] = new JsonFormat[sbt.internal.langserver.CompletionParams] { + override def read[J](jsOpt: Option[J], unbuilder: Unbuilder[J]): sbt.internal.langserver.CompletionParams = { + 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") + val context = unbuilder.readField[Option[sbt.internal.langserver.CompletionContext]]("context") + unbuilder.endObject() + sbt.internal.langserver.CompletionParams(textDocument, position, context) + case None => + deserializationError("Expected JsObject but found None") + } + } + override def write[J](obj: sbt.internal.langserver.CompletionParams, builder: Builder[J]): Unit = { + builder.beginObject() + builder.addField("textDocument", obj.textDocument) + builder.addField("position", obj.position) + builder.addField("context", obj.context) + builder.endObject() + } +} +} 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 512b67fab..1dfdb664d 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 @@ -22,4 +22,5 @@ trait JsonProtocol extends sjsonnew.BasicJsonProtocol with sbt.internal.langserver.codec.CancelRequestParamsFormats with sbt.internal.langserver.codec.TextDocumentIdentifierFormats with sbt.internal.langserver.codec.TextDocumentPositionParamsFormats + with sbt.internal.langserver.codec.TextDocumentPositionParamsInterfaceFormats object JsonProtocol extends JsonProtocol \ No newline at end of file diff --git a/protocol/src/main/contraband-scala/sbt/internal/langserver/codec/TextDocumentPositionParamsInterfaceFormats.scala b/protocol/src/main/contraband-scala/sbt/internal/langserver/codec/TextDocumentPositionParamsInterfaceFormats.scala new file mode 100644 index 000000000..f58a76159 --- /dev/null +++ b/protocol/src/main/contraband-scala/sbt/internal/langserver/codec/TextDocumentPositionParamsInterfaceFormats.scala @@ -0,0 +1,11 @@ +/** + * 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.JsonFormat +trait TextDocumentPositionParamsInterfaceFormats { self: sbt.internal.langserver.codec.TextDocumentIdentifierFormats with sbt.internal.langserver.codec.PositionFormats with sjsonnew.BasicJsonProtocol with sbt.internal.langserver.codec.TextDocumentPositionParamsFormats => +implicit lazy val TextDocumentPositionParamsInterfaceFormat: JsonFormat[sbt.internal.langserver.TextDocumentPositionParamsInterface] = flatUnionFormat1[sbt.internal.langserver.TextDocumentPositionParamsInterface, sbt.internal.langserver.TextDocumentPositionParams]("type") +} diff --git a/protocol/src/main/contraband-scala/sbt/internal/langserver/codec/TextEditFormats.scala b/protocol/src/main/contraband-scala/sbt/internal/langserver/codec/TextEditFormats.scala new file mode 100644 index 000000000..abf2f9c22 --- /dev/null +++ b/protocol/src/main/contraband-scala/sbt/internal/langserver/codec/TextEditFormats.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 TextEditFormats { self: sbt.internal.langserver.codec.RangeFormats with sjsonnew.BasicJsonProtocol => +implicit lazy val TextEditFormat: JsonFormat[sbt.internal.langserver.TextEdit] = new JsonFormat[sbt.internal.langserver.TextEdit] { + override def read[J](jsOpt: Option[J], unbuilder: Unbuilder[J]): sbt.internal.langserver.TextEdit = { + jsOpt match { + case Some(js) => + unbuilder.beginObject(js) + val range = unbuilder.readField[sbt.internal.langserver.Range]("range") + val newText = unbuilder.readField[String]("newText") + unbuilder.endObject() + sbt.internal.langserver.TextEdit(range, newText) + case None => + deserializationError("Expected JsObject but found None") + } + } + override def write[J](obj: sbt.internal.langserver.TextEdit, builder: Builder[J]): Unit = { + builder.beginObject() + builder.addField("range", obj.range) + builder.addField("newText", obj.newText) + builder.endObject() + } +} +} diff --git a/protocol/src/main/contraband-scala/sbt/protocol/CompletionParams.scala b/protocol/src/main/contraband-scala/sbt/protocol/CompletionParams.scala new file mode 100644 index 000000000..592284ca0 --- /dev/null +++ b/protocol/src/main/contraband-scala/sbt/protocol/CompletionParams.scala @@ -0,0 +1,32 @@ +/** + * This code is generated using [[http://www.scala-sbt.org/contraband/ sbt-contraband]]. + */ + +// DO NOT EDIT MANUALLY +package sbt.protocol +final class CompletionParams private ( + val query: String) extends Serializable { + + + + override def equals(o: Any): Boolean = o match { + case x: CompletionParams => (this.query == x.query) + case _ => false + } + override def hashCode: Int = { + 37 * (37 * (17 + "sbt.protocol.CompletionParams".##) + query.##) + } + override def toString: String = { + "CompletionParams(" + query + ")" + } + private[this] def copy(query: String = query): CompletionParams = { + new CompletionParams(query) + } + def withQuery(query: String): CompletionParams = { + copy(query = query) + } +} +object CompletionParams { + + def apply(query: String): CompletionParams = new CompletionParams(query) +} diff --git a/protocol/src/main/contraband-scala/sbt/protocol/CompletionResponse.scala b/protocol/src/main/contraband-scala/sbt/protocol/CompletionResponse.scala new file mode 100644 index 000000000..9b08441af --- /dev/null +++ b/protocol/src/main/contraband-scala/sbt/protocol/CompletionResponse.scala @@ -0,0 +1,32 @@ +/** + * This code is generated using [[http://www.scala-sbt.org/contraband/ sbt-contraband]]. + */ + +// DO NOT EDIT MANUALLY +package sbt.protocol +final class CompletionResponse private ( + val items: Vector[String]) extends Serializable { + + + + override def equals(o: Any): Boolean = o match { + case x: CompletionResponse => (this.items == x.items) + case _ => false + } + override def hashCode: Int = { + 37 * (37 * (17 + "sbt.protocol.CompletionResponse".##) + items.##) + } + override def toString: String = { + "CompletionResponse(" + items + ")" + } + private[this] def copy(items: Vector[String] = items): CompletionResponse = { + new CompletionResponse(items) + } + def withItems(items: Vector[String]): CompletionResponse = { + copy(items = items) + } +} +object CompletionResponse { + + def apply(items: Vector[String]): CompletionResponse = new CompletionResponse(items) +} diff --git a/protocol/src/main/contraband-scala/sbt/protocol/codec/CompletionParamsFormats.scala b/protocol/src/main/contraband-scala/sbt/protocol/codec/CompletionParamsFormats.scala new file mode 100644 index 000000000..01a762233 --- /dev/null +++ b/protocol/src/main/contraband-scala/sbt/protocol/codec/CompletionParamsFormats.scala @@ -0,0 +1,27 @@ +/** + * This code is generated using [[http://www.scala-sbt.org/contraband/ sbt-contraband]]. + */ + +// DO NOT EDIT MANUALLY +package sbt.protocol.codec +import _root_.sjsonnew.{ Unbuilder, Builder, JsonFormat, deserializationError } +trait CompletionParamsFormats { self: sjsonnew.BasicJsonProtocol => +implicit lazy val CompletionParamsFormat: JsonFormat[sbt.protocol.CompletionParams] = new JsonFormat[sbt.protocol.CompletionParams] { + override def read[J](jsOpt: Option[J], unbuilder: Unbuilder[J]): sbt.protocol.CompletionParams = { + jsOpt match { + case Some(js) => + unbuilder.beginObject(js) + val query = unbuilder.readField[String]("query") + unbuilder.endObject() + sbt.protocol.CompletionParams(query) + case None => + deserializationError("Expected JsObject but found None") + } + } + override def write[J](obj: sbt.protocol.CompletionParams, builder: Builder[J]): Unit = { + builder.beginObject() + builder.addField("query", obj.query) + builder.endObject() + } +} +} diff --git a/protocol/src/main/contraband-scala/sbt/protocol/codec/CompletionResponseFormats.scala b/protocol/src/main/contraband-scala/sbt/protocol/codec/CompletionResponseFormats.scala new file mode 100644 index 000000000..3e7377c18 --- /dev/null +++ b/protocol/src/main/contraband-scala/sbt/protocol/codec/CompletionResponseFormats.scala @@ -0,0 +1,27 @@ +/** + * This code is generated using [[http://www.scala-sbt.org/contraband/ sbt-contraband]]. + */ + +// DO NOT EDIT MANUALLY +package sbt.protocol.codec +import _root_.sjsonnew.{ Unbuilder, Builder, JsonFormat, deserializationError } +trait CompletionResponseFormats { self: sjsonnew.BasicJsonProtocol => +implicit lazy val CompletionResponseFormat: JsonFormat[sbt.protocol.CompletionResponse] = new JsonFormat[sbt.protocol.CompletionResponse] { + override def read[J](jsOpt: Option[J], unbuilder: Unbuilder[J]): sbt.protocol.CompletionResponse = { + jsOpt match { + case Some(js) => + unbuilder.beginObject(js) + val items = unbuilder.readField[Vector[String]]("items") + unbuilder.endObject() + sbt.protocol.CompletionResponse(items) + case None => + deserializationError("Expected JsObject but found None") + } + } + override def write[J](obj: sbt.protocol.CompletionResponse, builder: Builder[J]): Unit = { + builder.beginObject() + builder.addField("items", obj.items) + builder.endObject() + } +} +} diff --git a/protocol/src/main/contraband-scala/sbt/protocol/codec/JsonProtocol.scala b/protocol/src/main/contraband-scala/sbt/protocol/codec/JsonProtocol.scala index cc9d0fa90..6a29bda4c 100644 --- a/protocol/src/main/contraband-scala/sbt/protocol/codec/JsonProtocol.scala +++ b/protocol/src/main/contraband-scala/sbt/protocol/codec/JsonProtocol.scala @@ -9,6 +9,7 @@ trait JsonProtocol extends sjsonnew.BasicJsonProtocol with sbt.protocol.codec.ExecCommandFormats with sbt.protocol.codec.SettingQueryFormats with sbt.protocol.codec.CommandMessageFormats + with sbt.protocol.codec.CompletionParamsFormats with sbt.protocol.codec.ChannelAcceptedEventFormats with sbt.protocol.codec.LogEventFormats with sbt.protocol.codec.ExecStatusEventFormats @@ -17,5 +18,6 @@ trait JsonProtocol extends sjsonnew.BasicJsonProtocol with sbt.protocol.codec.SettingQueryFailureFormats with sbt.protocol.codec.EventMessageFormats with sbt.protocol.codec.SettingQueryResponseFormats + with sbt.protocol.codec.CompletionResponseFormats with sbt.protocol.codec.ExecutionEventFormats object JsonProtocol extends JsonProtocol \ No newline at end of file diff --git a/protocol/src/main/contraband/lsp.contra b/protocol/src/main/contraband/lsp.contra index 314c2a163..343157d38 100644 --- a/protocol/src/main/contraband/lsp.contra +++ b/protocol/src/main/contraband/lsp.contra @@ -137,7 +137,7 @@ type CancelRequestParams { } ## Goto definition params model -type TextDocumentPositionParams { +interface TextDocumentPositionParamsInterface { ## The text document. textDocument: sbt.internal.langserver.TextDocumentIdentifier! @@ -145,6 +145,14 @@ type TextDocumentPositionParams { position: sbt.internal.langserver.Position! } +type TextDocumentPositionParams implements TextDocumentPositionParamsInterface { + ## 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. diff --git a/protocol/src/main/contraband/server.contra b/protocol/src/main/contraband/server.contra index 63c7b816d..fc39411b0 100644 --- a/protocol/src/main/contraband/server.contra +++ b/protocol/src/main/contraband/server.contra @@ -22,6 +22,9 @@ type SettingQuery implements CommandMessage { setting: String! } +type CompletionParams { + query: String! +} ## Message for events. interface EventMessage { @@ -57,6 +60,10 @@ type SettingQueryFailure implements SettingQueryResponse { message: String! } +type CompletionResponse { + items: [String] +} + # enum Status { # Ready # Processing diff --git a/sbt/src/server-test/completions/build.sbt b/sbt/src/server-test/completions/build.sbt new file mode 100644 index 000000000..2ffc095d3 --- /dev/null +++ b/sbt/src/server-test/completions/build.sbt @@ -0,0 +1,6 @@ + +val hello = taskKey[Unit]("Say hello") + +hello := {} + +libraryDependencies += "org.scalatest" %% "scalatest" % "3.0.5" diff --git a/sbt/src/server-test/completions/src/test/scala/ExampleSpec.scala b/sbt/src/server-test/completions/src/test/scala/ExampleSpec.scala new file mode 100644 index 000000000..8719766a6 --- /dev/null +++ b/sbt/src/server-test/completions/src/test/scala/ExampleSpec.scala @@ -0,0 +1,10 @@ +package org.sbt + +import org.scalatest.FlatSpec + +class ExampleSpec extends FlatSpec { + "a test" should "do something" in { + assert(true == true) + assert(false == false) + } +} diff --git a/sbt/src/test/scala/testpkg/ServerSpec.scala b/sbt/src/test/scala/testpkg/ServerSpec.scala index acfc2b323..68ade60fb 100644 --- a/sbt/src/test/scala/testpkg/ServerSpec.scala +++ b/sbt/src/test/scala/testpkg/ServerSpec.scala @@ -9,7 +9,6 @@ package testpkg import org.scalatest._ import scala.concurrent._ -import scala.annotation.tailrec import sbt.protocol.ClientSocket import scala.util.Try import TestServer.withTestServer @@ -17,6 +16,7 @@ import java.io.File import sbt.io.syntax._ import sbt.io.IO import sbt.RunFromSourceMain +import scala.util.Try import scala.concurrent.ExecutionContext import java.util.concurrent.ForkJoinPool @@ -24,7 +24,7 @@ class ServerSpec extends fixture.AsyncFreeSpec with fixture.AsyncTestDataFixture "server" - { "should start" in { implicit td => withTestServer("handshake") { p => - p.writeLine( + p.sendJsonRpc( """{ "jsonrpc": "2.0", "id": "3", "method": "sbt/setting", "params": { "setting": "root/name" } }""" ) assert(p.waitForString(10) { s => @@ -35,7 +35,7 @@ class ServerSpec extends fixture.AsyncFreeSpec with fixture.AsyncTestDataFixture "return number id when number id is sent" in { implicit td => withTestServer("handshake") { p => - p.writeLine( + p.sendJsonRpc( """{ "jsonrpc": "2.0", "id": 3, "method": "sbt/setting", "params": { "setting": "root/name" } }""" ) assert(p.waitForString(10) { s => @@ -46,7 +46,7 @@ class ServerSpec extends fixture.AsyncFreeSpec with fixture.AsyncTestDataFixture "report task failures in case of exceptions" in { implicit td => withTestServer("events") { p => - p.writeLine( + p.sendJsonRpc( """{ "jsonrpc": "2.0", "id": 11, "method": "sbt/exec", "params": { "commandLine": "hello" } }""" ) assert(p.waitForString(10) { s => @@ -57,10 +57,10 @@ class ServerSpec extends fixture.AsyncFreeSpec with fixture.AsyncTestDataFixture "return error if cancelling non-matched task id" in { implicit td => withTestServer("events") { p => - p.writeLine( + p.sendJsonRpc( """{ "jsonrpc": "2.0", "id":12, "method": "sbt/exec", "params": { "commandLine": "run" } }""" ) - p.writeLine( + p.sendJsonRpc( """{ "jsonrpc": "2.0", "id":13, "method": "sbt/cancelRequest", "params": { "id": "55" } }""" ) @@ -72,12 +72,12 @@ class ServerSpec extends fixture.AsyncFreeSpec with fixture.AsyncTestDataFixture "cancel on-going task with numeric id" in { implicit td => withTestServer("events") { p => - p.writeLine( + p.sendJsonRpc( """{ "jsonrpc": "2.0", "id":12, "method": "sbt/exec", "params": { "commandLine": "run" } }""" ) assert(p.waitForString(60) { s => - p.writeLine( + p.sendJsonRpc( """{ "jsonrpc": "2.0", "id":13, "method": "sbt/cancelRequest", "params": { "id": "12" } }""" ) s contains """"result":{"status":"Task cancelled"""" @@ -87,18 +87,65 @@ class ServerSpec extends fixture.AsyncFreeSpec with fixture.AsyncTestDataFixture "cancel on-going task with string id" in { implicit td => withTestServer("events") { p => - p.writeLine( + p.sendJsonRpc( """{ "jsonrpc": "2.0", "id": "foo", "method": "sbt/exec", "params": { "commandLine": "run" } }""" ) assert(p.waitForString(60) { s => - p.writeLine( + p.sendJsonRpc( """{ "jsonrpc": "2.0", "id": "bar", "method": "sbt/cancelRequest", "params": { "id": "foo" } }""" ) s contains """"result":{"status":"Task cancelled"""" }) } } + + "return basic completions on request" in { implicit td => + withTestServer("completions") { p => + val completionStr = """{ "query": "" }""" + p.sendJsonRpc( + s"""{ "jsonrpc": "2.0", "id": 15, "method": "sbt/completion", "params": $completionStr }""" + ) + + assert(p.waitForString(10) { s => + s contains """"result":{"items":[""" + }) + } + } + + "return completion for custom tasks" in { implicit td => + withTestServer("completions") { p => + val completionStr = """{ "query": "hell" }""" + p.sendJsonRpc( + s"""{ "jsonrpc": "2.0", "id": 15, "method": "sbt/completion", "params": $completionStr }""" + ) + + assert(p.waitForString(10) { s => + s contains """"result":{"items":["hello"]}""" + }) + } + } + + "return completions for user classes" in { implicit td => + withTestServer("completions") { p => + p.sendJsonRpc( + """{ "jsonrpc": "2.0", "id":12, "method": "sbt/exec", "params": { "commandLine": "test" } }""" + ) + + p.waitForString(30) { s => + (s contains """"id":12,"result":{"status":"Done"""") && (s contains """"exitCode":0""") + } + + val completionStr = """{ "query": "testOnly org." }""" + p.sendJsonRpc( + s"""{ "jsonrpc": "2.0", "id": 15, "method": "sbt/completion", "params": $completionStr }""" + ) + + assert(p.waitForString(30) { s => + s contains """"result":{"items":["testOnly org.sbt.ExampleSpec"]}""" + }) + } + } } } @@ -154,7 +201,7 @@ object TestServer { case class TestServer(baseDirectory: File)(implicit ec: ExecutionContext) { import TestServer.hostLog - val readBuffer = new Array[Byte](4096) + val readBuffer = new Array[Byte](40960) var buffer: Vector[Byte] = Vector.empty var bytesRead = 0 private val delimiter: Byte = '\n'.toByte @@ -215,7 +262,7 @@ case class TestServer(baseDirectory: File)(implicit ec: ExecutionContext) { writeLine(message) } - def writeLine(s: String): Unit = { + private def writeLine(s: String): Unit = { def writeEndLine(): Unit = { val retByte: Byte = '\r'.toByte val delimiter: Byte = '\n'.toByte @@ -243,21 +290,17 @@ case class TestServer(baseDirectory: File)(implicit ec: ExecutionContext) { readContentLength(l) } - @tailrec final def waitForString(num: Int)(f: String => Boolean): Boolean = { - if (num < 0) { throw new Exception("Retries are over.") } else { - // readFrame should be called in another Thread in orrder to be able to time limit it's execution - val res = Future { readFrame }(ec) - - import scala.concurrent.duration._ - Try { - Await.result(res, 1.second) - }.toOption.flatten match { - // function f should be called in this Thread in order to be executed exactly once before eventually returning - case Some(str) if f(str) => true - case _ => waitForString(num - 1)(f) + val res = Future { + var done = false + while (!done) { + done = readFrame.fold(false)(f) } - } + true + }(ec) + + import scala.concurrent.duration._ + Await.result(res, num.seconds) } def readLine: Option[String] = {