[sbt-server] LSP completions support

This commit is contained in:
andrea 2018-10-02 15:51:20 +01:00
parent a23479dfb1
commit 34e0fc159c
32 changed files with 724 additions and 32 deletions

View File

@ -93,6 +93,7 @@ val mimaSettings = Def settings (
exclude[FinalClassProblem]("sbt.internal.*"),
exclude[FinalMethodProblem]("sbt.internal.*"),
exclude[IncompatibleResultTypeProblem]("sbt.internal.*"),
exclude[ReversedMissingMethodProblem]("sbt.internal.*")
),
)

View File

@ -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)

View File

@ -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
}

View File

@ -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))
}
}

View File

@ -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)
}
/**

View File

@ -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)

View File

@ -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 {
}

View File

@ -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))
}

View File

@ -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)
}

View File

@ -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)
}

View File

@ -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))
}

View File

@ -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 {

View File

@ -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 {
}

View File

@ -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)
}

View File

@ -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.")
}
}
}

View File

@ -0,0 +1,29 @@
/**
* This code is generated using [[http://www.scala-sbt.org/contraband/ sbt-contraband]].
*/
// DO NOT EDIT MANUALLY
package sbt.internal.langserver.codec
import _root_.sjsonnew.{ Unbuilder, Builder, JsonFormat, deserializationError }
trait 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()
}
}
}

View File

@ -0,0 +1,27 @@
/**
* This code is generated using [[http://www.scala-sbt.org/contraband/ sbt-contraband]].
*/
// DO NOT EDIT MANUALLY
package sbt.internal.langserver.codec
import _root_.sjsonnew.{ Unbuilder, Builder, JsonFormat, deserializationError }
trait 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()
}
}
}

View File

@ -0,0 +1,29 @@
/**
* This code is generated using [[http://www.scala-sbt.org/contraband/ sbt-contraband]].
*/
// DO NOT EDIT MANUALLY
package sbt.internal.langserver.codec
import _root_.sjsonnew.{ Unbuilder, Builder, JsonFormat, deserializationError }
trait 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()
}
}
}

View File

@ -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()
}
}
}

View File

@ -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

View File

@ -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")
}

View File

@ -0,0 +1,29 @@
/**
* This code is generated using [[http://www.scala-sbt.org/contraband/ sbt-contraband]].
*/
// DO NOT EDIT MANUALLY
package sbt.internal.langserver.codec
import _root_.sjsonnew.{ Unbuilder, Builder, JsonFormat, deserializationError }
trait 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()
}
}
}

View File

@ -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)
}

View File

@ -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)
}

View File

@ -0,0 +1,27 @@
/**
* This code is generated using [[http://www.scala-sbt.org/contraband/ sbt-contraband]].
*/
// DO NOT EDIT MANUALLY
package sbt.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()
}
}
}

View File

@ -0,0 +1,27 @@
/**
* This code is generated using [[http://www.scala-sbt.org/contraband/ sbt-contraband]].
*/
// DO NOT EDIT MANUALLY
package sbt.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()
}
}
}

View File

@ -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

View File

@ -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.

View File

@ -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

View File

@ -0,0 +1,6 @@
val hello = taskKey[Unit]("Say hello")
hello := {}
libraryDependencies += "org.scalatest" %% "scalatest" % "3.0.5"

View File

@ -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)
}
}

View File

@ -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] = {