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