From 27848d3468d589f31f8d312b60c23ddf6076a5bc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=93lafur=20P=C3=A1ll=20Geirsson?= Date: Sun, 24 Dec 2017 00:28:46 +0000 Subject: [PATCH 01/18] Experiment with different service --- .../main/scala/langserver/core/Service.scala | 78 +++++++++++++++++++ 1 file changed, 78 insertions(+) create mode 100644 languageserver/src/main/scala/langserver/core/Service.scala diff --git a/languageserver/src/main/scala/langserver/core/Service.scala b/languageserver/src/main/scala/langserver/core/Service.scala new file mode 100644 index 00000000000..9d3a07bd898 --- /dev/null +++ b/languageserver/src/main/scala/langserver/core/Service.scala @@ -0,0 +1,78 @@ +package langserver.core + +import com.dhpcs.jsonrpc.JsonRpcMessage +import com.dhpcs.jsonrpc.JsonRpcMessage.Params +import com.dhpcs.jsonrpc.JsonRpcMessage.ParamsOps._ +import com.dhpcs.jsonrpc.JsonRpcRequestMessage +import com.dhpcs.jsonrpc.JsonRpcResponseErrorMessage +import com.dhpcs.jsonrpc.JsonRpcResponseMessage +import com.dhpcs.jsonrpc.JsonRpcResponseSuccessMessage +import monix.eval.Task +import play.api.libs.json.JsNull +import play.api.libs.json.Json +import play.api.libs.json.Reads +import play.api.libs.json.Writes +import play.api.libs.json._ + +trait Service[A, B] { self => + def method: String + def handleRequest(request: A): Task[B] +} + +class JsonRpcService( + notifications: List[NotificationService], + requests: List[RequestService] +) { + +} +trait NotificationService extends Service[JsonRpcRequestMessage, Unit] +trait RequestService + extends Service[JsonRpcRequestMessage, JsonRpcResponseMessage] + +object Service { + def notification[A]( + methodName: String + )(f: A => Task[Unit]): Service[A, Unit] = + request[A, Unit](methodName)(f) + def request[A, B](methodName: String)(f: A => Task[B]): Service[A, B] = + new Service[A, B] { + override def method: String = methodName + override def handleRequest(request: A): Task[B] = f(request) + } + + private implicit val ParamsReads: Writes[Params] = + params => params.unlift.fold(JsNull: JsValue)(Json.toJson(_)) + + def toRequestService[A, B]( + service: Service[A, B] + )(implicit reads: Reads[A], writes: Writes[B]): RequestService = + new RequestService { + override def method: String = service.method + override def handleRequest( + request: JsonRpcRequestMessage + ): Task[JsonRpcResponseMessage] = + Json.toJson(request.params).validate[A] match { + case err: JsError => + Task.now( + JsonRpcResponseErrorMessage.invalidRequest(err, request.id) + ) + case JsSuccess(value, _) => + service.handleRequest(value).map { response => + JsonRpcResponseSuccessMessage( + Json.toJson(response), + request.id + ) + } + } + } + def toNotificationService[A]( + service: Service[A, Unit] + )(implicit ev: Reads[A]): NotificationService = new NotificationService { + override def method: String = service.method + override def handleRequest(request: JsonRpcRequestMessage): Task[Unit] = + Json + .toJson(request.params) + .asOpt[A] + .fold(Task.unit)(service.handleRequest) + } +} From 804b6e8ab9e5063b249ed206ce58a4f0f2b99e9d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=93lafur=20P=C3=A1ll=20Geirsson?= Date: Sun, 24 Dec 2017 23:56:51 +0000 Subject: [PATCH 02/18] Reimplement json rpc data structures --- .../main/scala/langserver/core/Service.scala | 172 +++++++++++------- 1 file changed, 109 insertions(+), 63 deletions(-) diff --git a/languageserver/src/main/scala/langserver/core/Service.scala b/languageserver/src/main/scala/langserver/core/Service.scala index 9d3a07bd898..2472f386db1 100644 --- a/languageserver/src/main/scala/langserver/core/Service.scala +++ b/languageserver/src/main/scala/langserver/core/Service.scala @@ -1,78 +1,124 @@ package langserver.core -import com.dhpcs.jsonrpc.JsonRpcMessage -import com.dhpcs.jsonrpc.JsonRpcMessage.Params -import com.dhpcs.jsonrpc.JsonRpcMessage.ParamsOps._ -import com.dhpcs.jsonrpc.JsonRpcRequestMessage -import com.dhpcs.jsonrpc.JsonRpcResponseErrorMessage -import com.dhpcs.jsonrpc.JsonRpcResponseMessage -import com.dhpcs.jsonrpc.JsonRpcResponseSuccessMessage +import com.typesafe.scalalogging.LazyLogging +import enumeratum.values.IntEnum +import enumeratum.values.IntEnumEntry +import enumeratum.values.IntPlayJsonValueEnum import monix.eval.Task import play.api.libs.json.JsNull -import play.api.libs.json.Json import play.api.libs.json.Reads import play.api.libs.json.Writes import play.api.libs.json._ -trait Service[A, B] { self => - def method: String - def handleRequest(request: A): Task[B] +sealed trait RPC +case class Request(method: String, params: Option[JsValue], id: RequestId) + extends RPC +case class Notification(method: String, params: JsValue) extends RPC + +sealed trait Response extends RPC +case class SuccessResponse(result: JsValue, id: RequestId) extends Response +case class ErrorResponse(error: ErrorObject, id: RequestId) extends Response + +sealed trait RequestId +object RequestId { + implicit val format: Format[RequestId] = Format[RequestId]( + Reads { + case num: JsNumber => JsSuccess(Number(num)) + case JsNull => JsSuccess(Null) + case value: JsString => JsSuccess(String(value)) + case els => JsError(s"Expected number, string or null. Obtained $els") + }, + Writes { + case Number(value) => value + case String(value) => value + case Null => JsNull + } + ) + case class Number(value: JsNumber) extends RequestId + case class String(value: JsString) extends RequestId + case object Null extends RequestId +} + +case class ErrorObject(code: ErrorCode, message: String, data: Option[JsValue]) +sealed abstract class ErrorCode(val value: Int) extends IntEnumEntry +case object ErrorCode + extends IntEnum[ErrorCode] + with IntPlayJsonValueEnum[ErrorCode] { + + /** + * Invalid JSON was received by the server. + * + * An error occurred on the server while parsing the JSON text. + */ + case object ParseError extends ErrorCode(-32700) + + /** The JSON sent is not a valid Request object. */ + case object InvalidRequest extends ErrorCode(-32600) + + /** The method does not exist / is not available. */ + case object MethodNotFound extends ErrorCode(-32601) + + /** Invalid method parameter(s). */ + case object InvalidParams extends ErrorCode(-32602) + + /** Internal JSON-RPC error. */ + case object InternalError extends ErrorCode(-32603) + + /** Reserved for implementation-defined server-errors. */ + case object ServerError extends ErrorCode(-32000) + + val values: collection.immutable.IndexedSeq[ErrorCode] = findValues } -class JsonRpcService( - notifications: List[NotificationService], - requests: List[RequestService] -) { - +trait Service + extends RequestHandler[Request, Response] + with NotificationHandler[Notification] + +case class MethodRequestHandler( + method: String, + handler: RequestHandler[Request, Response] +) +case class MethodNotificationHandler( + method: String, + handler: NotificationHandler[Notification] +) +trait RequestHandler[-A, +B] { + def handleRequest(request: A): Task[B] +} +trait NotificationHandler[-A] { + def handleNotification(notification: A): Task[Unit] } -trait NotificationService extends Service[JsonRpcRequestMessage, Unit] -trait RequestService - extends Service[JsonRpcRequestMessage, JsonRpcResponseMessage] - -object Service { - def notification[A]( - methodName: String - )(f: A => Task[Unit]): Service[A, Unit] = - request[A, Unit](methodName)(f) - def request[A, B](methodName: String)(f: A => Task[B]): Service[A, B] = - new Service[A, B] { - override def method: String = methodName - override def handleRequest(request: A): Task[B] = f(request) + +class CompositeService( + notifications: List[MethodNotificationHandler], + requests: List[MethodRequestHandler] +) extends Service + with LazyLogging { + private val ns = notifications.iterator.map(n => n.method -> n).toMap + private val rs = requests.iterator.map(n => n.method -> n).toMap + override def handleNotification(notification: Notification): Task[Unit] = + ns.get(notification.method) match { + case Some(service) => service.handler.handleNotification(notification) + case None => + logger.warn(s"Method not found '${notification.method}'") + Task.unit // No way to report error on notifications } - private implicit val ParamsReads: Writes[Params] = - params => params.unlift.fold(JsNull: JsValue)(Json.toJson(_)) - - def toRequestService[A, B]( - service: Service[A, B] - )(implicit reads: Reads[A], writes: Writes[B]): RequestService = - new RequestService { - override def method: String = service.method - override def handleRequest( - request: JsonRpcRequestMessage - ): Task[JsonRpcResponseMessage] = - Json.toJson(request.params).validate[A] match { - case err: JsError => - Task.now( - JsonRpcResponseErrorMessage.invalidRequest(err, request.id) - ) - case JsSuccess(value, _) => - service.handleRequest(value).map { response => - JsonRpcResponseSuccessMessage( - Json.toJson(response), - request.id - ) - } - } + override def handleRequest(request: Request): Task[Response] = + rs.get(request.method) match { + case None => + Task.now( + ErrorResponse( + ErrorObject( + ErrorCode.MethodNotFound, + s"Method '${request.method}' not found, expected one of ${rs.keys.mkString(", ")}", + None + ), + request.id + ) + ) + case Some(service) => + service.handler.handleRequest(request) } - def toNotificationService[A]( - service: Service[A, Unit] - )(implicit ev: Reads[A]): NotificationService = new NotificationService { - override def method: String = service.method - override def handleRequest(request: JsonRpcRequestMessage): Task[Unit] = - Json - .toJson(request.params) - .asOpt[A] - .fold(Task.unit)(service.handleRequest) - } + } From 96667fe6e4d922910f7e938ad10a3f204c9a40f1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=93lafur=20P=C3=A1ll=20Geirsson?= Date: Mon, 25 Dec 2017 00:14:34 +0000 Subject: [PATCH 03/18] Implement Observable[LSPMessage] --- .../src/main/scala/langserver/core/Main.scala | 17 --- .../core => scala/meta/lsp}/Service.scala | 69 +++++++----- .../langserver/core/MessageReaderSuite.scala | 5 +- .../scala/scala/meta/lsp/LSPMessage.scala | 22 ++++ .../scala/meta/lsp/LSPMessageParser.scala | 102 ++++++++++++++++++ .../src/test/scala/tests/JsonRpcSuite.scala | 62 +++++++++++ .../src/test/scala/tests/MegaSuite.scala | 5 + 7 files changed, 236 insertions(+), 46 deletions(-) delete mode 100644 languageserver/src/main/scala/langserver/core/Main.scala rename languageserver/src/main/scala/{langserver/core => scala/meta/lsp}/Service.scala (65%) create mode 100644 metaserver/src/main/scala/scala/meta/lsp/LSPMessage.scala create mode 100644 metaserver/src/main/scala/scala/meta/lsp/LSPMessageParser.scala create mode 100644 metaserver/src/test/scala/tests/JsonRpcSuite.scala diff --git a/languageserver/src/main/scala/langserver/core/Main.scala b/languageserver/src/main/scala/langserver/core/Main.scala deleted file mode 100644 index 9514db1148d..00000000000 --- a/languageserver/src/main/scala/langserver/core/Main.scala +++ /dev/null @@ -1,17 +0,0 @@ -package langserver.core - -import com.typesafe.scalalogging.LazyLogging -import scala.util.Try -import monix.execution.Scheduler.Implicits.global - -object Main extends LazyLogging { - def main(args: Array[String]): Unit = { - logger.info(s"Starting server in ${System.getenv("PWD")}") - - val server = Try { - val s = new LanguageServer(System.in, System.out) - s.start() - } - server.recover{case e => {logger.error(e.getMessage); e.printStackTrace} } - } -} diff --git a/languageserver/src/main/scala/langserver/core/Service.scala b/languageserver/src/main/scala/scala/meta/lsp/Service.scala similarity index 65% rename from languageserver/src/main/scala/langserver/core/Service.scala rename to languageserver/src/main/scala/scala/meta/lsp/Service.scala index 2472f386db1..a532e6806d8 100644 --- a/languageserver/src/main/scala/langserver/core/Service.scala +++ b/languageserver/src/main/scala/scala/meta/lsp/Service.scala @@ -1,10 +1,12 @@ -package langserver.core +package scala.meta.lsp +import scala.util.control.NonFatal import com.typesafe.scalalogging.LazyLogging import enumeratum.values.IntEnum import enumeratum.values.IntEnumEntry import enumeratum.values.IntPlayJsonValueEnum import monix.eval.Task +import monix.reactive.Observable import play.api.libs.json.JsNull import play.api.libs.json.Reads import play.api.libs.json.Writes @@ -12,7 +14,10 @@ import play.api.libs.json._ sealed trait RPC case class Request(method: String, params: Option[JsValue], id: RequestId) - extends RPC + extends RPC { + def toError(code: ErrorCode, message: String): ErrorResponse = + ErrorResponse(ErrorObject(code, message, None), id) +} case class Notification(method: String, params: JsValue) extends RPC sealed trait Response extends RPC @@ -70,28 +75,47 @@ case object ErrorCode val values: collection.immutable.IndexedSeq[ErrorCode] = findValues } -trait Service - extends RequestHandler[Request, Response] - with NotificationHandler[Notification] - -case class MethodRequestHandler( +case class MethodRequestService( method: String, - handler: RequestHandler[Request, Response] + handler: JsonRequestService ) -case class MethodNotificationHandler( +case class MethodNotificationService( method: String, - handler: NotificationHandler[Notification] + handler: JsonNotificationService ) -trait RequestHandler[-A, +B] { - def handleRequest(request: A): Task[B] +trait JsonRequestService { + def handleRequest(request: Request): Task[Response] } -trait NotificationHandler[-A] { - def handleNotification(notification: A): Task[Unit] +abstract class RequestService[A: Reads, B: Writes] extends JsonRequestService { + override def handleRequest(request: Request): Task[Response] = + request.params.getOrElse(JsNull).validate[A] match { + case err: JsError => + Task.eval(request.toError(ErrorCode.InvalidParams, err.toString)) + case JsSuccess(value, _) => + handle(value) + .map[Response] { + case Right(response) => + SuccessResponse(Json.toJson(response), request.id) + case Left(err) => + err + } + .onErrorRecover { + case NonFatal(e) => + request.toError(ErrorCode.InternalError, e.getMessage) + } + } + def handle(a: A): Task[Either[ErrorResponse, B]] } +trait JsonNotificationService { + def handleNotification(notification: Notification): Task[Unit] + Observable.fromInputStream(???) +} + +trait Service extends JsonRequestService with JsonNotificationService class CompositeService( - notifications: List[MethodNotificationHandler], - requests: List[MethodRequestHandler] + notifications: List[MethodNotificationService], + requests: List[MethodRequestService] ) extends Service with LazyLogging { private val ns = notifications.iterator.map(n => n.method -> n).toMap @@ -107,18 +131,13 @@ class CompositeService( override def handleRequest(request: Request): Task[Response] = rs.get(request.method) match { case None => - Task.now( - ErrorResponse( - ErrorObject( - ErrorCode.MethodNotFound, - s"Method '${request.method}' not found, expected one of ${rs.keys.mkString(", ")}", - None - ), - request.id + Task.eval( + request.toError( + ErrorCode.MethodNotFound, + s"Method '${request.method}' not found, expected one of ${rs.keys.mkString(", ")}" ) ) case Some(service) => service.handler.handleRequest(request) } - } diff --git a/languageserver/src/test/scala/langserver/core/MessageReaderSuite.scala b/languageserver/src/test/scala/langserver/core/MessageReaderSuite.scala index 6ed4f1b2b0e..42a07e0e278 100644 --- a/languageserver/src/test/scala/langserver/core/MessageReaderSuite.scala +++ b/languageserver/src/test/scala/langserver/core/MessageReaderSuite.scala @@ -25,6 +25,7 @@ class MessageReaderSuite extends FunSuite inStream.close() } + test("full headers are supported") { val msgReader = new MessageReader(inStream) @@ -82,10 +83,6 @@ Content-Type: application/vscode-jsonrpc; charset=utf8 test("chunked payload arrives") { val msgReader = new MessageReader(inStream) - write("""Content-Length: 43 - -{"jsonrpc":"2.0",""") - write(""""id":1,"method":"example"}""") val payload = msgReader.nextPayload() assert(payload.value === """{"jsonrpc":"2.0","id":1,"method":"example"}""") diff --git a/metaserver/src/main/scala/scala/meta/lsp/LSPMessage.scala b/metaserver/src/main/scala/scala/meta/lsp/LSPMessage.scala new file mode 100644 index 00000000000..6eb8b5daf49 --- /dev/null +++ b/metaserver/src/main/scala/scala/meta/lsp/LSPMessage.scala @@ -0,0 +1,22 @@ +package scala.meta.lsp + +import java.io.InputStream +import monix.reactive.Observable + +case class LSPMessage(header: Map[String, String], content: String) { + override def toString: String = { + val sb = new java.lang.StringBuilder() + header.foreach { + case (key, value) => + sb.append(key).append(": ").append(value).append("\n") + } + sb.append("\n") + .append(content) + sb.toString + } +} + +object LSPMessage { + def fromInputStream(in: InputStream): Observable[LSPMessage] = + Observable.fromInputStream(in).liftByOperator(new LSPMessageParser) +} diff --git a/metaserver/src/main/scala/scala/meta/lsp/LSPMessageParser.scala b/metaserver/src/main/scala/scala/meta/lsp/LSPMessageParser.scala new file mode 100644 index 00000000000..36f2b4cd58f --- /dev/null +++ b/metaserver/src/main/scala/scala/meta/lsp/LSPMessageParser.scala @@ -0,0 +1,102 @@ +package scala.meta.lsp + +import java.nio.charset.StandardCharsets +import scala.collection.mutable.ArrayBuffer +import scala.concurrent.Future +import com.typesafe.scalalogging.LazyLogging +import monix.execution.Ack +import monix.execution.Scheduler +import monix.reactive.observables.ObservableLike.Operator +import monix.reactive.observers.Subscriber + +final class LSPMessageParser + extends Operator[Array[Byte], LSPMessage] + with LazyLogging { + override def apply(out: Subscriber[LSPMessage]): Subscriber[Array[Byte]] = { + new Subscriber[Array[Byte]] { + import Ack._ + private[this] val data = ArrayBuffer.empty[Byte] + private[this] var contentLength = -1 + private[this] var header = Map.empty[String, String] + private def atDelimiter(idx: Int): Boolean = { + (data.size >= idx + 4 + && data(idx) == '\r' + && data(idx + 1) == '\n' + && data(idx + 2) == '\r' + && data(idx + 3) == '\n') + } + private val EmptyPair = "" -> "" + private[this] def readHeaders: Future[Ack] = { + if (data.size < 4) Continue + else { + var i = 0 + while (i + 4 < data.size && !atDelimiter(i)) { + i += 1 + } + if (!atDelimiter(i)) Continue + else { + val bytes = new Array[Byte](i) + data.copyToArray(bytes) + data.remove(0, i + 4) + val headers = new String(bytes, StandardCharsets.US_ASCII) + val pairs: Map[String, String] = headers + .split("\r\n") + .iterator + .filterNot(_.trim.isEmpty) + .map { line => + line.split(":") match { + case Array(key, value) => key.trim -> value.trim + case _ => + logger.error(s"Malformed input: $line") + EmptyPair + } + } + .toMap + pairs.get("Content-Length") match { + case Some(n) => + try { + contentLength = n.toInt + header = pairs + readContent + } catch { + case _: NumberFormatException => + logger.error( + s"Expected Content-Length to be a number, obtained $n" + ) + Continue + } + case _ => + logger.error(s"Missing Content-Length key in headers $pairs") + Continue + } + } + } + } + private[this] def readContent: Future[Ack] = { + if (contentLength > data.size) Continue + else { + val contentBytes = new Array[Byte](contentLength) + data.copyToArray(contentBytes) + data.remove(0, contentLength) + contentLength = -1 + val content = new String(contentBytes, StandardCharsets.UTF_8) + out.onNext(LSPMessage(header, content)).flatMap { + case Continue => readHeaders + case els => els + } + } + } + override implicit val scheduler: Scheduler = out.scheduler + override def onError(ex: Throwable): Unit = out.onError(ex) + override def onComplete(): Unit = { + data.clear() + out.onComplete() + } + override def onNext(elem: Array[Byte]): Future[Ack] = { + data ++= elem + if (contentLength < 0) readHeaders + else readContent + } + } + } +} diff --git a/metaserver/src/test/scala/tests/JsonRpcSuite.scala b/metaserver/src/test/scala/tests/JsonRpcSuite.scala new file mode 100644 index 00000000000..71dbc9c0d68 --- /dev/null +++ b/metaserver/src/test/scala/tests/JsonRpcSuite.scala @@ -0,0 +1,62 @@ +package tests + +import java.io.PipedInputStream +import java.io.PipedOutputStream +import java.io.PrintWriter +import scala.meta.lsp.LSPMessage +import monix.execution.CancelableFuture +import monix.execution.ExecutionModel.AlwaysAsyncExecution +import monix.execution.schedulers.TestScheduler + +object JsonRpcSuite extends MegaSuite { + var out: PrintWriter = _ + var messages = List.empty[LSPMessage] + implicit var s: TestScheduler = _ + var f: CancelableFuture[Unit] = CancelableFuture.unit + override def utestBeforeEach(path: Seq[String]): Unit = { + s = TestScheduler(AlwaysAsyncExecution) + val in = new PipedInputStream + out = new PrintWriter(new PipedOutputStream(in)) + messages = Nil + f = LSPMessage.fromInputStream(in).foreach { msg => + messages = msg :: messages + } + } + + override def utestAfterEach(path: Seq[String]): Unit = { + f.cancel() + } + + def write(msg: String): Unit = { + out.print(msg.replaceAll("\n", "\r\n")) + out.flush() + s.tickOne() + } + + val header = """Content-Length: 43 + +""" + val content = """{"jsonrpc":"2.0","id":1,"method":"example"}""" + + val message: String = header + content + val lspMessage = LSPMessage(Map("Content-Length" -> "43"), content) + + test("header and content together") { + write(message) + assertEquals(messages, List(lspMessage)) + } + + test("combined") { + write(message * 2) + s.tickOne() + assertEquals(messages, List(lspMessage, lspMessage)) + } + + test("header and content separately") { + write(header) + assert(messages.isEmpty) + write(content) + assertEquals(messages, List(lspMessage)) + } + +} diff --git a/metaserver/src/test/scala/tests/MegaSuite.scala b/metaserver/src/test/scala/tests/MegaSuite.scala index 17ff432ea90..5aa4d3ab5f7 100644 --- a/metaserver/src/test/scala/tests/MegaSuite.scala +++ b/metaserver/src/test/scala/tests/MegaSuite.scala @@ -24,6 +24,11 @@ class MegaSuite extends TestSuite { def afterAll(): Unit = () def intercept[T: ClassTag](exprs: Unit): T = macro Asserts.interceptProxy[T] def assert(exprs: Boolean*): Unit = macro Asserts.assertProxy + def assertEquals[T](a: T, b: T): Unit = { + if (a != b) { + fail(s"$a != $b") + } + } def assertNoDiff( obtained: String, expected: String, From bf515781ad25ea711712aee092ef64292cafa1f0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=93lafur=20P=C3=A1ll=20Geirsson?= Date: Tue, 26 Dec 2017 22:21:38 +0000 Subject: [PATCH 04/18] Implement loop --- .../main/scala/scala/meta/lsp/Service.scala | 89 ++++++++++++------ .../scala/scala/meta/lsp/LanguageServer.scala | 91 +++++++++++++++++++ 2 files changed, 153 insertions(+), 27 deletions(-) create mode 100644 metaserver/src/main/scala/scala/meta/lsp/LanguageServer.scala diff --git a/languageserver/src/main/scala/scala/meta/lsp/Service.scala b/languageserver/src/main/scala/scala/meta/lsp/Service.scala index a532e6806d8..5168933714c 100644 --- a/languageserver/src/main/scala/scala/meta/lsp/Service.scala +++ b/languageserver/src/main/scala/scala/meta/lsp/Service.scala @@ -12,17 +12,60 @@ import play.api.libs.json.Reads import play.api.libs.json.Writes import play.api.libs.json._ -sealed trait RPC +sealed trait Message +object Message { + private case class IndeterminateMessage( + method: String, + params: Option[JsValue], + id: Option[RequestId] + ) + implicit val reads: Reads[Message] = Json.reads[IndeterminateMessage].map { + case IndeterminateMessage(method, params, Some(id)) => + Request(method, params, id) + case IndeterminateMessage(method, params, None) => + Notification(method, params) + } +} case class Request(method: String, params: Option[JsValue], id: RequestId) - extends RPC { - def toError(code: ErrorCode, message: String): ErrorResponse = - ErrorResponse(ErrorObject(code, message, None), id) + extends Message { + def toError(code: ErrorCode, message: String): Response = + Response.error(ErrorObject(code, message, None), id) } -case class Notification(method: String, params: JsValue) extends RPC +case class Notification(method: String, params: Option[JsValue]) extends Message -sealed trait Response extends RPC -case class SuccessResponse(result: JsValue, id: RequestId) extends Response -case class ErrorResponse(error: ErrorObject, id: RequestId) extends Response +sealed trait Response { + def toTask: Task[Response] = Task.eval(this) +} +object Response { + case class Success(result: JsValue, id: RequestId) extends Response + object Success { + implicit val format: OFormat[Success] = Json.format[Success] + } + case class Error(error: ErrorObject, id: RequestId) extends Response + object Error { + implicit val format: OFormat[Error] = Json.format[Error] + } + case object Empty extends Response + def empty: Response = Empty + def success(result: JsValue, id: RequestId): Response = + Success(result, id) + def error(error: ErrorObject, id: RequestId): Response = + Error(error, id) + def internalError(message: String, id: RequestId): Response = + Error(ErrorObject(ErrorCode.InternalError, message, None), id) + def invalidRequest(message: String): Response = + Error( + ErrorObject(ErrorCode.InvalidParams, message, None), + RequestId.Null + ) + def cancelled(id: JsValue): Response = + Error( + ErrorObject(ErrorCode.RequestCancelled, "", None), + id.asOpt[RequestId].getOrElse(RequestId.Null) + ) + def parseError(message: String): Response = + Error(ErrorObject(ErrorCode.ParseError, message, None), RequestId.Null) +} sealed trait RequestId object RequestId { @@ -45,33 +88,20 @@ object RequestId { } case class ErrorObject(code: ErrorCode, message: String, data: Option[JsValue]) +object ErrorObject { + implicit val format: OFormat[ErrorObject] = Json.format[ErrorObject] +} sealed abstract class ErrorCode(val value: Int) extends IntEnumEntry case object ErrorCode extends IntEnum[ErrorCode] with IntPlayJsonValueEnum[ErrorCode] { - - /** - * Invalid JSON was received by the server. - * - * An error occurred on the server while parsing the JSON text. - */ case object ParseError extends ErrorCode(-32700) - - /** The JSON sent is not a valid Request object. */ case object InvalidRequest extends ErrorCode(-32600) - - /** The method does not exist / is not available. */ case object MethodNotFound extends ErrorCode(-32601) - - /** Invalid method parameter(s). */ case object InvalidParams extends ErrorCode(-32602) - - /** Internal JSON-RPC error. */ case object InternalError extends ErrorCode(-32603) - - /** Reserved for implementation-defined server-errors. */ case object ServerError extends ErrorCode(-32000) - + case object RequestCancelled extends ErrorCode(-32800) val values: collection.immutable.IndexedSeq[ErrorCode] = findValues } @@ -95,7 +125,7 @@ abstract class RequestService[A: Reads, B: Writes] extends JsonRequestService { handle(value) .map[Response] { case Right(response) => - SuccessResponse(Json.toJson(response), request.id) + Response.success(Json.toJson(response), request.id) case Left(err) => err } @@ -104,7 +134,7 @@ abstract class RequestService[A: Reads, B: Writes] extends JsonRequestService { request.toError(ErrorCode.InternalError, e.getMessage) } } - def handle(a: A): Task[Either[ErrorResponse, B]] + def handle(a: A): Task[Either[Response, B]] } trait JsonNotificationService { def handleNotification(notification: Notification): Task[Unit] @@ -141,3 +171,8 @@ class CompositeService( service.handler.handleRequest(request) } } + +case class CancelParams(id: JsValue) +object CancelParams { + implicit val format: OFormat[CancelParams] = Json.format[CancelParams] +} diff --git a/metaserver/src/main/scala/scala/meta/lsp/LanguageServer.scala b/metaserver/src/main/scala/scala/meta/lsp/LanguageServer.scala new file mode 100644 index 00000000000..bb4f0ee3fa1 --- /dev/null +++ b/metaserver/src/main/scala/scala/meta/lsp/LanguageServer.scala @@ -0,0 +1,91 @@ +package scala.meta.lsp + +import java.io.OutputStream +import scala.collection.concurrent.TrieMap +import scala.util.control.NonFatal +import com.fasterxml.jackson.core.JsonParseException +import com.typesafe.scalalogging.LazyLogging +import langserver.core.MessageWriter +import monix.eval.Task +import monix.execution.Cancelable +import monix.execution.Scheduler +import monix.reactive.Observable +import play.api.libs.json.JsError +import play.api.libs.json.JsSuccess +import play.api.libs.json.JsValue +import play.api.libs.json.Json + +final class LanguageServer( + in: Observable[LSPMessage], + out: OutputStream, + service: Service, + requestScheduler: Scheduler +) extends LazyLogging { + private val writer = new MessageWriter(out) + private val activeClientRequests: TrieMap[JsValue, Cancelable] = TrieMap.empty + + def handleValidMessage(message: Message): Task[Response] = + message match { + case request: Request => + val runningRequest = service + .handleRequest(request) + .onErrorRecover { + case NonFatal(e) => + logger.error(s"Unhandled error handling request $request", e) + Response.internalError(e.getMessage, request.id) + } + .runAsync(requestScheduler) + activeClientRequests.put(Json.toJson(request.id), runningRequest) + Task.fromFuture(runningRequest) + case notification: Notification => + notification match { + case Notification("$/cancelNotification", Some(id)) => + activeClientRequests.get(id) match { + case None => + Response.empty.toTask + case Some(request) => + Task.eval { + request.cancel() + activeClientRequests.remove(id) + Response.cancelled(id) + } + } + case _ => + service.handleNotification(notification).map(_ => Response.empty) + } + } + + def handleMessage(message: LSPMessage): Task[Response] = + LanguageServer.parseMessage(message) match { + case Left(parseError) => Task.now(parseError) + case Right(json) => + json.validate[Message] match { + case err: JsError => Task.now(Response.invalidRequest(err.toString)) + case JsSuccess(msg, _) => handleValidMessage(msg) + } + } + + def start: Task[Unit] = in.foreachL { msg => + handleMessage(msg) + .map { + case Response.Empty => () + case x: Response.Success => writer.write(x) + case x: Response.Error => writer.write(x) + } + .onErrorRecover { + case NonFatal(e) => + logger.error("Unhandled error", e) + } + .runAsync(requestScheduler) + } +} + +object LanguageServer { + def parseMessage(message: LSPMessage): Either[Response, JsValue] = + try { + Right(Json.parse(message.content)) + } catch { + case e: JsonParseException => + Left(Response.parseError(e.getMessage)) + } +} From 9296fc0674d396b4012ae832cd9fccacc81639d0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=93lafur=20P=C3=A1ll=20Geirsson?= Date: Wed, 27 Dec 2017 00:28:23 +0000 Subject: [PATCH 05/18] Refactor code for cleaner organization --- .../scala/scala/meta/lsp/CancelParams.scala | 11 ++ .../lsp/CompositeNotificationService.scala | 19 ++ .../meta/lsp/CompositeRequestService.scala | 22 +++ .../main/scala/scala/meta/lsp/ErrorCode.scala | 20 ++ .../scala/scala/meta/lsp/ErrorObject.scala | 11 ++ .../meta/lsp/JsonNotificationService.scala | 32 ++++ .../scala/meta/lsp/JsonRequestService.scala | 38 ++++ .../main/scala/scala/meta/lsp/Message.scala | 33 ++++ .../main/scala/scala/meta/lsp/RequestId.scala | 24 +++ .../main/scala/scala/meta/lsp/Response.scala | 37 ++++ .../main/scala/scala/meta/lsp/Service.scala | 178 ------------------ .../scala/scala/meta/lsp/LanguageServer.scala | 11 +- 12 files changed, 254 insertions(+), 182 deletions(-) create mode 100644 languageserver/src/main/scala/scala/meta/lsp/CancelParams.scala create mode 100644 languageserver/src/main/scala/scala/meta/lsp/CompositeNotificationService.scala create mode 100644 languageserver/src/main/scala/scala/meta/lsp/CompositeRequestService.scala create mode 100644 languageserver/src/main/scala/scala/meta/lsp/ErrorCode.scala create mode 100644 languageserver/src/main/scala/scala/meta/lsp/ErrorObject.scala create mode 100644 languageserver/src/main/scala/scala/meta/lsp/JsonNotificationService.scala create mode 100644 languageserver/src/main/scala/scala/meta/lsp/JsonRequestService.scala create mode 100644 languageserver/src/main/scala/scala/meta/lsp/Message.scala create mode 100644 languageserver/src/main/scala/scala/meta/lsp/RequestId.scala create mode 100644 languageserver/src/main/scala/scala/meta/lsp/Response.scala delete mode 100644 languageserver/src/main/scala/scala/meta/lsp/Service.scala diff --git a/languageserver/src/main/scala/scala/meta/lsp/CancelParams.scala b/languageserver/src/main/scala/scala/meta/lsp/CancelParams.scala new file mode 100644 index 00000000000..fed0f1b82f8 --- /dev/null +++ b/languageserver/src/main/scala/scala/meta/lsp/CancelParams.scala @@ -0,0 +1,11 @@ +package scala.meta.lsp + +import play.api.libs.json.JsValue +import play.api.libs.json.Json +import play.api.libs.json.OFormat + +case class CancelParams(id: JsValue) + +object CancelParams { + implicit val format: OFormat[CancelParams] = Json.format[CancelParams] +} \ No newline at end of file diff --git a/languageserver/src/main/scala/scala/meta/lsp/CompositeNotificationService.scala b/languageserver/src/main/scala/scala/meta/lsp/CompositeNotificationService.scala new file mode 100644 index 00000000000..6d5df65ba20 --- /dev/null +++ b/languageserver/src/main/scala/scala/meta/lsp/CompositeNotificationService.scala @@ -0,0 +1,19 @@ +package scala.meta.lsp + +import com.typesafe.scalalogging.LazyLogging +import monix.eval.Task + +class CompositeNotificationService( + notifications: List[NamedNotificationService] +) extends JsonNotificationService + with LazyLogging { + private val map = notifications.iterator.map(n => n.method -> n).toMap + + override def handleNotification(notification: Notification): Task[Unit] = + map.get(notification.method) match { + case Some(service) => service.handleNotification(notification) + case None => + logger.warn(s"Method not found '${notification.method}'") + Task.unit // No way to report error on notifications + } +} diff --git a/languageserver/src/main/scala/scala/meta/lsp/CompositeRequestService.scala b/languageserver/src/main/scala/scala/meta/lsp/CompositeRequestService.scala new file mode 100644 index 00000000000..1f2e04b6ab6 --- /dev/null +++ b/languageserver/src/main/scala/scala/meta/lsp/CompositeRequestService.scala @@ -0,0 +1,22 @@ +package scala.meta.lsp + +import com.typesafe.scalalogging.LazyLogging +import monix.eval.Task + +class CompositeRequestService(requests: List[NamedRequestService]) + extends JsonRequestService + with LazyLogging { + private val map = requests.iterator.map(n => n.method -> n).toMap + override def handleRequest(request: Request): Task[Response] = + map.get(request.method) match { + case None => + Task.now( + Response.methodNotFound( + s"Method '${request.method}' not found, expected one of ${map.keys.mkString(", ")}", + request.id + ) + ) + case Some(service) => + service.handleRequest(request) + } +} diff --git a/languageserver/src/main/scala/scala/meta/lsp/ErrorCode.scala b/languageserver/src/main/scala/scala/meta/lsp/ErrorCode.scala new file mode 100644 index 00000000000..663d8949f7d --- /dev/null +++ b/languageserver/src/main/scala/scala/meta/lsp/ErrorCode.scala @@ -0,0 +1,20 @@ +package scala.meta.lsp + +import scala.collection.immutable.IndexedSeq +import enumeratum.values.IntEnum +import enumeratum.values.IntEnumEntry +import enumeratum.values.IntPlayJsonValueEnum + +sealed abstract class ErrorCode(val value: Int) extends IntEnumEntry +case object ErrorCode + extends IntEnum[ErrorCode] + with IntPlayJsonValueEnum[ErrorCode] { + case object ParseError extends ErrorCode(-32700) + case object InvalidRequest extends ErrorCode(-32600) + case object MethodNotFound extends ErrorCode(-32601) + case object InvalidParams extends ErrorCode(-32602) + case object InternalError extends ErrorCode(-32603) + case object ServerError extends ErrorCode(-32000) + case object RequestCancelled extends ErrorCode(-32800) + val values: IndexedSeq[ErrorCode] = findValues +} diff --git a/languageserver/src/main/scala/scala/meta/lsp/ErrorObject.scala b/languageserver/src/main/scala/scala/meta/lsp/ErrorObject.scala new file mode 100644 index 00000000000..a6e08999e72 --- /dev/null +++ b/languageserver/src/main/scala/scala/meta/lsp/ErrorObject.scala @@ -0,0 +1,11 @@ +package scala.meta.lsp + +import play.api.libs.json.JsValue +import play.api.libs.json.Json +import play.api.libs.json.OFormat + +case class ErrorObject(code: ErrorCode, message: String, data: Option[JsValue]) + +object ErrorObject { + implicit val format: OFormat[ErrorObject] = Json.format[ErrorObject] +} diff --git a/languageserver/src/main/scala/scala/meta/lsp/JsonNotificationService.scala b/languageserver/src/main/scala/scala/meta/lsp/JsonNotificationService.scala new file mode 100644 index 00000000000..f0139b9b672 --- /dev/null +++ b/languageserver/src/main/scala/scala/meta/lsp/JsonNotificationService.scala @@ -0,0 +1,32 @@ +package scala.meta.lsp + +import com.typesafe.scalalogging.LazyLogging +import monix.eval.Task +import play.api.libs.json.JsError +import play.api.libs.json.JsNull +import play.api.libs.json.JsSuccess +import play.api.libs.json.Reads + +trait JsonNotificationService { + def handleNotification(notification: Notification): Task[Unit] +} +trait NamedNotificationService extends JsonNotificationService { + def method: String +} +abstract class NotificationService[A: Reads](val method: String) + extends NamedNotificationService + with LazyLogging { + def handle(request: A): Task[Unit] + override def handleNotification(notification: Notification): Task[Unit] = + notification.params.getOrElse(JsNull).validate[A] match { + case err: JsError => + Task.eval { + logger.error( + s"Failed to parse notification $notification. Errors: $err" + ) + } + case JsSuccess(value, _) => + handle(value) + } + +} diff --git a/languageserver/src/main/scala/scala/meta/lsp/JsonRequestService.scala b/languageserver/src/main/scala/scala/meta/lsp/JsonRequestService.scala new file mode 100644 index 00000000000..577335aaf6c --- /dev/null +++ b/languageserver/src/main/scala/scala/meta/lsp/JsonRequestService.scala @@ -0,0 +1,38 @@ +package scala.meta.lsp + +import monix.eval.Task +import monix.execution.misc.NonFatal +import play.api.libs.json.JsError +import play.api.libs.json.JsNull +import play.api.libs.json.JsSuccess +import play.api.libs.json.Json +import play.api.libs.json.Reads +import play.api.libs.json.Writes + +trait JsonRequestService { + def handleRequest(request: Request): Task[Response] +} +trait NamedRequestService extends JsonRequestService { + def method: String +} +abstract class RequestService[A: Reads, B: Writes](val method: String) + extends NamedRequestService { + def handle(request: A): Task[Either[Response.Error, B]] + override def handleRequest(request: Request): Task[Response] = + request.params.getOrElse(JsNull).validate[A] match { + case err: JsError => + Task.eval(request.toError(ErrorCode.InvalidParams, err.toString)) + case JsSuccess(value, _) => + handle(value) + .map[Response] { + case Right(response) => + Response.success(Json.toJson(response), request.id) + case Left(err) => + err + } + .onErrorRecover { + case NonFatal(e) => + request.toError(ErrorCode.InternalError, e.getMessage) + } + } +} diff --git a/languageserver/src/main/scala/scala/meta/lsp/Message.scala b/languageserver/src/main/scala/scala/meta/lsp/Message.scala new file mode 100644 index 00000000000..48ec146d0f7 --- /dev/null +++ b/languageserver/src/main/scala/scala/meta/lsp/Message.scala @@ -0,0 +1,33 @@ +package scala.meta.lsp + +import play.api.libs.json._ + +sealed trait Message +object Message { + private case class IndeterminateMessage( + method: String, + params: Option[JsValue], + id: Option[RequestId] + ) + implicit val reads: Reads[Message] = Reads { + case json @ JsObject(map) => + if (map.contains("id")) json.validate[Request] + else json.validate[Notification] + case els => + JsError(s"Expected object, obtained $els") + } +} + +case class Request(method: String, params: Option[JsValue], id: RequestId) + extends Message { + def toError(code: ErrorCode, message: String): Response = + Response.error(ErrorObject(code, message, None), id) +} +object Request { + implicit val format: OFormat[Request] = Json.format[Request] +} + +case class Notification(method: String, params: Option[JsValue]) extends Message +object Notification { + implicit val format: OFormat[Notification] = Json.format[Notification] +} diff --git a/languageserver/src/main/scala/scala/meta/lsp/RequestId.scala b/languageserver/src/main/scala/scala/meta/lsp/RequestId.scala new file mode 100644 index 00000000000..5a214a4cd34 --- /dev/null +++ b/languageserver/src/main/scala/scala/meta/lsp/RequestId.scala @@ -0,0 +1,24 @@ +package scala.meta.lsp + +import play.api.libs.json._ + +sealed trait RequestId + +object RequestId { + implicit val format: Format[RequestId] = Format[RequestId]( + Reads { + case num: JsNumber => JsSuccess(Number(num)) + case JsNull => JsSuccess(Null) + case value: JsString => JsSuccess(String(value)) + case els => JsError(s"Expected number, string or null. Obtained $els") + }, + Writes { + case Number(value) => value + case String(value) => value + case Null => JsNull + } + ) + case class Number(value: JsNumber) extends RequestId + case class String(value: JsString) extends RequestId + case object Null extends RequestId +} \ No newline at end of file diff --git a/languageserver/src/main/scala/scala/meta/lsp/Response.scala b/languageserver/src/main/scala/scala/meta/lsp/Response.scala new file mode 100644 index 00000000000..673036d806f --- /dev/null +++ b/languageserver/src/main/scala/scala/meta/lsp/Response.scala @@ -0,0 +1,37 @@ +package scala.meta.lsp + +import play.api.libs.json._ + +sealed trait Response +object Response { + case class Success(result: JsValue, id: RequestId) extends Response + object Success { + implicit val format: OFormat[Success] = Json.format[Success] + } + case class Error(error: ErrorObject, id: RequestId) extends Response + object Error { + implicit val format: OFormat[Error] = Json.format[Error] + } + case object Empty extends Response + def empty: Response = Empty + def success(result: JsValue, id: RequestId): Response = + Success(result, id) + def error(error: ErrorObject, id: RequestId): Response.Error = + Error(error, id) + def internalError(message: String, id: RequestId): Response.Error = + Error(ErrorObject(ErrorCode.InternalError, message, None), id) + def invalidRequest(message: String): Response.Error = + Error( + ErrorObject(ErrorCode.InvalidParams, message, None), + RequestId.Null + ) + def cancelled(id: JsValue): Response.Error = + Error( + ErrorObject(ErrorCode.RequestCancelled, "", None), + id.asOpt[RequestId].getOrElse(RequestId.Null) + ) + def parseError(message: String): Response.Error = + Error(ErrorObject(ErrorCode.ParseError, message, None), RequestId.Null) + def methodNotFound(message: String, id: RequestId): Response.Error = + Error(ErrorObject(ErrorCode.MethodNotFound, message, None), id) +} diff --git a/languageserver/src/main/scala/scala/meta/lsp/Service.scala b/languageserver/src/main/scala/scala/meta/lsp/Service.scala deleted file mode 100644 index 5168933714c..00000000000 --- a/languageserver/src/main/scala/scala/meta/lsp/Service.scala +++ /dev/null @@ -1,178 +0,0 @@ -package scala.meta.lsp - -import scala.util.control.NonFatal -import com.typesafe.scalalogging.LazyLogging -import enumeratum.values.IntEnum -import enumeratum.values.IntEnumEntry -import enumeratum.values.IntPlayJsonValueEnum -import monix.eval.Task -import monix.reactive.Observable -import play.api.libs.json.JsNull -import play.api.libs.json.Reads -import play.api.libs.json.Writes -import play.api.libs.json._ - -sealed trait Message -object Message { - private case class IndeterminateMessage( - method: String, - params: Option[JsValue], - id: Option[RequestId] - ) - implicit val reads: Reads[Message] = Json.reads[IndeterminateMessage].map { - case IndeterminateMessage(method, params, Some(id)) => - Request(method, params, id) - case IndeterminateMessage(method, params, None) => - Notification(method, params) - } -} -case class Request(method: String, params: Option[JsValue], id: RequestId) - extends Message { - def toError(code: ErrorCode, message: String): Response = - Response.error(ErrorObject(code, message, None), id) -} -case class Notification(method: String, params: Option[JsValue]) extends Message - -sealed trait Response { - def toTask: Task[Response] = Task.eval(this) -} -object Response { - case class Success(result: JsValue, id: RequestId) extends Response - object Success { - implicit val format: OFormat[Success] = Json.format[Success] - } - case class Error(error: ErrorObject, id: RequestId) extends Response - object Error { - implicit val format: OFormat[Error] = Json.format[Error] - } - case object Empty extends Response - def empty: Response = Empty - def success(result: JsValue, id: RequestId): Response = - Success(result, id) - def error(error: ErrorObject, id: RequestId): Response = - Error(error, id) - def internalError(message: String, id: RequestId): Response = - Error(ErrorObject(ErrorCode.InternalError, message, None), id) - def invalidRequest(message: String): Response = - Error( - ErrorObject(ErrorCode.InvalidParams, message, None), - RequestId.Null - ) - def cancelled(id: JsValue): Response = - Error( - ErrorObject(ErrorCode.RequestCancelled, "", None), - id.asOpt[RequestId].getOrElse(RequestId.Null) - ) - def parseError(message: String): Response = - Error(ErrorObject(ErrorCode.ParseError, message, None), RequestId.Null) -} - -sealed trait RequestId -object RequestId { - implicit val format: Format[RequestId] = Format[RequestId]( - Reads { - case num: JsNumber => JsSuccess(Number(num)) - case JsNull => JsSuccess(Null) - case value: JsString => JsSuccess(String(value)) - case els => JsError(s"Expected number, string or null. Obtained $els") - }, - Writes { - case Number(value) => value - case String(value) => value - case Null => JsNull - } - ) - case class Number(value: JsNumber) extends RequestId - case class String(value: JsString) extends RequestId - case object Null extends RequestId -} - -case class ErrorObject(code: ErrorCode, message: String, data: Option[JsValue]) -object ErrorObject { - implicit val format: OFormat[ErrorObject] = Json.format[ErrorObject] -} -sealed abstract class ErrorCode(val value: Int) extends IntEnumEntry -case object ErrorCode - extends IntEnum[ErrorCode] - with IntPlayJsonValueEnum[ErrorCode] { - case object ParseError extends ErrorCode(-32700) - case object InvalidRequest extends ErrorCode(-32600) - case object MethodNotFound extends ErrorCode(-32601) - case object InvalidParams extends ErrorCode(-32602) - case object InternalError extends ErrorCode(-32603) - case object ServerError extends ErrorCode(-32000) - case object RequestCancelled extends ErrorCode(-32800) - val values: collection.immutable.IndexedSeq[ErrorCode] = findValues -} - -case class MethodRequestService( - method: String, - handler: JsonRequestService -) -case class MethodNotificationService( - method: String, - handler: JsonNotificationService -) -trait JsonRequestService { - def handleRequest(request: Request): Task[Response] -} -abstract class RequestService[A: Reads, B: Writes] extends JsonRequestService { - override def handleRequest(request: Request): Task[Response] = - request.params.getOrElse(JsNull).validate[A] match { - case err: JsError => - Task.eval(request.toError(ErrorCode.InvalidParams, err.toString)) - case JsSuccess(value, _) => - handle(value) - .map[Response] { - case Right(response) => - Response.success(Json.toJson(response), request.id) - case Left(err) => - err - } - .onErrorRecover { - case NonFatal(e) => - request.toError(ErrorCode.InternalError, e.getMessage) - } - } - def handle(a: A): Task[Either[Response, B]] -} -trait JsonNotificationService { - def handleNotification(notification: Notification): Task[Unit] - Observable.fromInputStream(???) -} - -trait Service extends JsonRequestService with JsonNotificationService - -class CompositeService( - notifications: List[MethodNotificationService], - requests: List[MethodRequestService] -) extends Service - with LazyLogging { - private val ns = notifications.iterator.map(n => n.method -> n).toMap - private val rs = requests.iterator.map(n => n.method -> n).toMap - override def handleNotification(notification: Notification): Task[Unit] = - ns.get(notification.method) match { - case Some(service) => service.handler.handleNotification(notification) - case None => - logger.warn(s"Method not found '${notification.method}'") - Task.unit // No way to report error on notifications - } - - override def handleRequest(request: Request): Task[Response] = - rs.get(request.method) match { - case None => - Task.eval( - request.toError( - ErrorCode.MethodNotFound, - s"Method '${request.method}' not found, expected one of ${rs.keys.mkString(", ")}" - ) - ) - case Some(service) => - service.handler.handleRequest(request) - } -} - -case class CancelParams(id: JsValue) -object CancelParams { - implicit val format: OFormat[CancelParams] = Json.format[CancelParams] -} diff --git a/metaserver/src/main/scala/scala/meta/lsp/LanguageServer.scala b/metaserver/src/main/scala/scala/meta/lsp/LanguageServer.scala index bb4f0ee3fa1..2b3904aabcf 100644 --- a/metaserver/src/main/scala/scala/meta/lsp/LanguageServer.scala +++ b/metaserver/src/main/scala/scala/meta/lsp/LanguageServer.scala @@ -18,7 +18,8 @@ import play.api.libs.json.Json final class LanguageServer( in: Observable[LSPMessage], out: OutputStream, - service: Service, + notifications: JsonNotificationService, + requests: JsonRequestService, requestScheduler: Scheduler ) extends LazyLogging { private val writer = new MessageWriter(out) @@ -27,7 +28,7 @@ final class LanguageServer( def handleValidMessage(message: Message): Task[Response] = message match { case request: Request => - val runningRequest = service + val runningRequest = requests .handleRequest(request) .onErrorRecover { case NonFatal(e) => @@ -42,7 +43,7 @@ final class LanguageServer( case Notification("$/cancelNotification", Some(id)) => activeClientRequests.get(id) match { case None => - Response.empty.toTask + Task.now(Response.empty) case Some(request) => Task.eval { request.cancel() @@ -51,7 +52,9 @@ final class LanguageServer( } } case _ => - service.handleNotification(notification).map(_ => Response.empty) + notifications + .handleNotification(notification) + .map(_ => Response.empty) } } From 14691047d647666c54ddf85bab52ffa96c6ae493 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=93lafur=20P=C3=A1ll=20Geirsson?= Date: Wed, 27 Dec 2017 00:32:44 +0000 Subject: [PATCH 06/18] Introduce scala.meta.languageserver.protocol --- .../scala/langserver/messages/Commands.scala | 3 +++ .../protocol/BaseProtocol.scala} | 10 +++++----- .../languageserver/protocol}/CancelParams.scala | 2 +- .../protocol}/CompositeNotificationService.scala | 2 +- .../protocol}/CompositeRequestService.scala | 2 +- .../languageserver/protocol}/ErrorCode.scala | 2 +- .../languageserver/protocol}/ErrorObject.scala | 2 +- .../protocol}/JsonNotificationService.scala | 11 +++++++++-- .../protocol}/JsonRequestService.scala | 2 +- .../protocol}/LanguageServer.scala | 16 +++++++++++----- .../meta/languageserver/protocol}/Message.scala | 2 +- .../protocol/ProtocolParser.scala} | 10 +++++----- .../languageserver/protocol}/RequestId.scala | 3 +-- .../meta/languageserver/protocol}/Response.scala | 2 +- .../src/test/scala/tests/JsonRpcSuite.scala | 8 ++++---- vscode-extension/src/extension.ts | 2 +- 16 files changed, 47 insertions(+), 32 deletions(-) rename metaserver/src/main/scala/scala/meta/{lsp/LSPMessage.scala => languageserver/protocol/BaseProtocol.scala} (54%) rename {languageserver/src/main/scala/scala/meta/lsp => metaserver/src/main/scala/scala/meta/languageserver/protocol}/CancelParams.scala (84%) rename {languageserver/src/main/scala/scala/meta/lsp => metaserver/src/main/scala/scala/meta/languageserver/protocol}/CompositeNotificationService.scala (93%) rename {languageserver/src/main/scala/scala/meta/lsp => metaserver/src/main/scala/scala/meta/languageserver/protocol}/CompositeRequestService.scala (93%) rename {languageserver/src/main/scala/scala/meta/lsp => metaserver/src/main/scala/scala/meta/languageserver/protocol}/ErrorCode.scala (94%) rename {languageserver/src/main/scala/scala/meta/lsp => metaserver/src/main/scala/scala/meta/languageserver/protocol}/ErrorObject.scala (86%) rename {languageserver/src/main/scala/scala/meta/lsp => metaserver/src/main/scala/scala/meta/languageserver/protocol}/JsonNotificationService.scala (77%) rename {languageserver/src/main/scala/scala/meta/lsp => metaserver/src/main/scala/scala/meta/languageserver/protocol}/JsonRequestService.scala (96%) rename metaserver/src/main/scala/scala/meta/{lsp => languageserver/protocol}/LanguageServer.scala (86%) rename {languageserver/src/main/scala/scala/meta/lsp => metaserver/src/main/scala/scala/meta/languageserver/protocol}/Message.scala (95%) rename metaserver/src/main/scala/scala/meta/{lsp/LSPMessageParser.scala => languageserver/protocol/ProtocolParser.scala} (92%) rename {languageserver/src/main/scala/scala/meta/lsp => metaserver/src/main/scala/scala/meta/languageserver/protocol}/RequestId.scala (93%) rename {languageserver/src/main/scala/scala/meta/lsp => metaserver/src/main/scala/scala/meta/languageserver/protocol}/Response.scala (96%) diff --git a/languageserver/src/main/scala/langserver/messages/Commands.scala b/languageserver/src/main/scala/langserver/messages/Commands.scala index c9a84922aa0..8cf7723b952 100644 --- a/languageserver/src/main/scala/langserver/messages/Commands.scala +++ b/languageserver/src/main/scala/langserver/messages/Commands.scala @@ -148,6 +148,9 @@ object CompletionList { } case class InitializeResult(capabilities: ServerCapabilities) extends ResultResponse +object InitializeResult { + implicit val format: OFormat[InitializeResult] = Json.format[InitializeResult] +} case class Shutdown() extends ServerCommand diff --git a/metaserver/src/main/scala/scala/meta/lsp/LSPMessage.scala b/metaserver/src/main/scala/scala/meta/languageserver/protocol/BaseProtocol.scala similarity index 54% rename from metaserver/src/main/scala/scala/meta/lsp/LSPMessage.scala rename to metaserver/src/main/scala/scala/meta/languageserver/protocol/BaseProtocol.scala index 6eb8b5daf49..999772ab7d7 100644 --- a/metaserver/src/main/scala/scala/meta/lsp/LSPMessage.scala +++ b/metaserver/src/main/scala/scala/meta/languageserver/protocol/BaseProtocol.scala @@ -1,9 +1,9 @@ -package scala.meta.lsp +package scala.meta.languageserver.protocol import java.io.InputStream import monix.reactive.Observable -case class LSPMessage(header: Map[String, String], content: String) { +case class BaseProtocol(header: Map[String, String], content: String) { override def toString: String = { val sb = new java.lang.StringBuilder() header.foreach { @@ -16,7 +16,7 @@ case class LSPMessage(header: Map[String, String], content: String) { } } -object LSPMessage { - def fromInputStream(in: InputStream): Observable[LSPMessage] = - Observable.fromInputStream(in).liftByOperator(new LSPMessageParser) +object BaseProtocol { + def fromInputStream(in: InputStream): Observable[BaseProtocol] = + Observable.fromInputStream(in).liftByOperator(new ProtocolParser) } diff --git a/languageserver/src/main/scala/scala/meta/lsp/CancelParams.scala b/metaserver/src/main/scala/scala/meta/languageserver/protocol/CancelParams.scala similarity index 84% rename from languageserver/src/main/scala/scala/meta/lsp/CancelParams.scala rename to metaserver/src/main/scala/scala/meta/languageserver/protocol/CancelParams.scala index fed0f1b82f8..33f1a5d7fed 100644 --- a/languageserver/src/main/scala/scala/meta/lsp/CancelParams.scala +++ b/metaserver/src/main/scala/scala/meta/languageserver/protocol/CancelParams.scala @@ -1,4 +1,4 @@ -package scala.meta.lsp +package scala.meta.languageserver.protocol import play.api.libs.json.JsValue import play.api.libs.json.Json diff --git a/languageserver/src/main/scala/scala/meta/lsp/CompositeNotificationService.scala b/metaserver/src/main/scala/scala/meta/languageserver/protocol/CompositeNotificationService.scala similarity index 93% rename from languageserver/src/main/scala/scala/meta/lsp/CompositeNotificationService.scala rename to metaserver/src/main/scala/scala/meta/languageserver/protocol/CompositeNotificationService.scala index 6d5df65ba20..9102167f7d3 100644 --- a/languageserver/src/main/scala/scala/meta/lsp/CompositeNotificationService.scala +++ b/metaserver/src/main/scala/scala/meta/languageserver/protocol/CompositeNotificationService.scala @@ -1,4 +1,4 @@ -package scala.meta.lsp +package scala.meta.languageserver.protocol import com.typesafe.scalalogging.LazyLogging import monix.eval.Task diff --git a/languageserver/src/main/scala/scala/meta/lsp/CompositeRequestService.scala b/metaserver/src/main/scala/scala/meta/languageserver/protocol/CompositeRequestService.scala similarity index 93% rename from languageserver/src/main/scala/scala/meta/lsp/CompositeRequestService.scala rename to metaserver/src/main/scala/scala/meta/languageserver/protocol/CompositeRequestService.scala index 1f2e04b6ab6..721ac650fe8 100644 --- a/languageserver/src/main/scala/scala/meta/lsp/CompositeRequestService.scala +++ b/metaserver/src/main/scala/scala/meta/languageserver/protocol/CompositeRequestService.scala @@ -1,4 +1,4 @@ -package scala.meta.lsp +package scala.meta.languageserver.protocol import com.typesafe.scalalogging.LazyLogging import monix.eval.Task diff --git a/languageserver/src/main/scala/scala/meta/lsp/ErrorCode.scala b/metaserver/src/main/scala/scala/meta/languageserver/protocol/ErrorCode.scala similarity index 94% rename from languageserver/src/main/scala/scala/meta/lsp/ErrorCode.scala rename to metaserver/src/main/scala/scala/meta/languageserver/protocol/ErrorCode.scala index 663d8949f7d..22a1955eeff 100644 --- a/languageserver/src/main/scala/scala/meta/lsp/ErrorCode.scala +++ b/metaserver/src/main/scala/scala/meta/languageserver/protocol/ErrorCode.scala @@ -1,4 +1,4 @@ -package scala.meta.lsp +package scala.meta.languageserver.protocol import scala.collection.immutable.IndexedSeq import enumeratum.values.IntEnum diff --git a/languageserver/src/main/scala/scala/meta/lsp/ErrorObject.scala b/metaserver/src/main/scala/scala/meta/languageserver/protocol/ErrorObject.scala similarity index 86% rename from languageserver/src/main/scala/scala/meta/lsp/ErrorObject.scala rename to metaserver/src/main/scala/scala/meta/languageserver/protocol/ErrorObject.scala index a6e08999e72..a5e1fc6193c 100644 --- a/languageserver/src/main/scala/scala/meta/lsp/ErrorObject.scala +++ b/metaserver/src/main/scala/scala/meta/languageserver/protocol/ErrorObject.scala @@ -1,4 +1,4 @@ -package scala.meta.lsp +package scala.meta.languageserver.protocol import play.api.libs.json.JsValue import play.api.libs.json.Json diff --git a/languageserver/src/main/scala/scala/meta/lsp/JsonNotificationService.scala b/metaserver/src/main/scala/scala/meta/languageserver/protocol/JsonNotificationService.scala similarity index 77% rename from languageserver/src/main/scala/scala/meta/lsp/JsonNotificationService.scala rename to metaserver/src/main/scala/scala/meta/languageserver/protocol/JsonNotificationService.scala index f0139b9b672..12957ef353b 100644 --- a/languageserver/src/main/scala/scala/meta/lsp/JsonNotificationService.scala +++ b/metaserver/src/main/scala/scala/meta/languageserver/protocol/JsonNotificationService.scala @@ -1,4 +1,4 @@ -package scala.meta.lsp +package scala.meta.languageserver.protocol import com.typesafe.scalalogging.LazyLogging import monix.eval.Task @@ -28,5 +28,12 @@ abstract class NotificationService[A: Reads](val method: String) case JsSuccess(value, _) => handle(value) } - +} +object NotificationService { + def method[A: Reads]( + name: String + )(f: A => Task[Unit]): NotificationService[A] = + new NotificationService[A](name) { + override def handle(request: A): Task[Unit] = f(request) + } } diff --git a/languageserver/src/main/scala/scala/meta/lsp/JsonRequestService.scala b/metaserver/src/main/scala/scala/meta/languageserver/protocol/JsonRequestService.scala similarity index 96% rename from languageserver/src/main/scala/scala/meta/lsp/JsonRequestService.scala rename to metaserver/src/main/scala/scala/meta/languageserver/protocol/JsonRequestService.scala index 577335aaf6c..81ac00a8bab 100644 --- a/languageserver/src/main/scala/scala/meta/lsp/JsonRequestService.scala +++ b/metaserver/src/main/scala/scala/meta/languageserver/protocol/JsonRequestService.scala @@ -1,4 +1,4 @@ -package scala.meta.lsp +package scala.meta.languageserver.protocol import monix.eval.Task import monix.execution.misc.NonFatal diff --git a/metaserver/src/main/scala/scala/meta/lsp/LanguageServer.scala b/metaserver/src/main/scala/scala/meta/languageserver/protocol/LanguageServer.scala similarity index 86% rename from metaserver/src/main/scala/scala/meta/lsp/LanguageServer.scala rename to metaserver/src/main/scala/scala/meta/languageserver/protocol/LanguageServer.scala index 2b3904aabcf..21d0e57268c 100644 --- a/metaserver/src/main/scala/scala/meta/lsp/LanguageServer.scala +++ b/metaserver/src/main/scala/scala/meta/languageserver/protocol/LanguageServer.scala @@ -1,6 +1,7 @@ -package scala.meta.lsp +package scala.meta.languageserver.protocol import java.io.OutputStream +import java.util.concurrent.Executors import scala.collection.concurrent.TrieMap import scala.util.control.NonFatal import com.fasterxml.jackson.core.JsonParseException @@ -16,7 +17,7 @@ import play.api.libs.json.JsValue import play.api.libs.json.Json final class LanguageServer( - in: Observable[LSPMessage], + in: Observable[BaseProtocol], out: OutputStream, notifications: JsonNotificationService, requests: JsonRequestService, @@ -58,7 +59,7 @@ final class LanguageServer( } } - def handleMessage(message: LSPMessage): Task[Response] = + def handleMessage(message: BaseProtocol): Task[Response] = LanguageServer.parseMessage(message) match { case Left(parseError) => Task.now(parseError) case Right(json) => @@ -68,7 +69,7 @@ final class LanguageServer( } } - def start: Task[Unit] = in.foreachL { msg => + def startTask: Task[Unit] = in.foreachL { msg => handleMessage(msg) .map { case Response.Empty => () @@ -81,10 +82,15 @@ final class LanguageServer( } .runAsync(requestScheduler) } + + def listen(): Unit = { + logger.info("Start listening....") + startTask.runAsync(Scheduler(Executors.newFixedThreadPool(1))) + } } object LanguageServer { - def parseMessage(message: LSPMessage): Either[Response, JsValue] = + def parseMessage(message: BaseProtocol): Either[Response, JsValue] = try { Right(Json.parse(message.content)) } catch { diff --git a/languageserver/src/main/scala/scala/meta/lsp/Message.scala b/metaserver/src/main/scala/scala/meta/languageserver/protocol/Message.scala similarity index 95% rename from languageserver/src/main/scala/scala/meta/lsp/Message.scala rename to metaserver/src/main/scala/scala/meta/languageserver/protocol/Message.scala index 48ec146d0f7..08d8257f3bc 100644 --- a/languageserver/src/main/scala/scala/meta/lsp/Message.scala +++ b/metaserver/src/main/scala/scala/meta/languageserver/protocol/Message.scala @@ -1,4 +1,4 @@ -package scala.meta.lsp +package scala.meta.languageserver.protocol import play.api.libs.json._ diff --git a/metaserver/src/main/scala/scala/meta/lsp/LSPMessageParser.scala b/metaserver/src/main/scala/scala/meta/languageserver/protocol/ProtocolParser.scala similarity index 92% rename from metaserver/src/main/scala/scala/meta/lsp/LSPMessageParser.scala rename to metaserver/src/main/scala/scala/meta/languageserver/protocol/ProtocolParser.scala index 36f2b4cd58f..a619e076a7a 100644 --- a/metaserver/src/main/scala/scala/meta/lsp/LSPMessageParser.scala +++ b/metaserver/src/main/scala/scala/meta/languageserver/protocol/ProtocolParser.scala @@ -1,4 +1,4 @@ -package scala.meta.lsp +package scala.meta.languageserver.protocol import java.nio.charset.StandardCharsets import scala.collection.mutable.ArrayBuffer @@ -9,10 +9,10 @@ import monix.execution.Scheduler import monix.reactive.observables.ObservableLike.Operator import monix.reactive.observers.Subscriber -final class LSPMessageParser - extends Operator[Array[Byte], LSPMessage] +final class ProtocolParser + extends Operator[Array[Byte], BaseProtocol] with LazyLogging { - override def apply(out: Subscriber[LSPMessage]): Subscriber[Array[Byte]] = { + override def apply(out: Subscriber[BaseProtocol]): Subscriber[Array[Byte]] = { new Subscriber[Array[Byte]] { import Ack._ private[this] val data = ArrayBuffer.empty[Byte] @@ -80,7 +80,7 @@ final class LSPMessageParser data.remove(0, contentLength) contentLength = -1 val content = new String(contentBytes, StandardCharsets.UTF_8) - out.onNext(LSPMessage(header, content)).flatMap { + out.onNext(BaseProtocol(header, content)).flatMap { case Continue => readHeaders case els => els } diff --git a/languageserver/src/main/scala/scala/meta/lsp/RequestId.scala b/metaserver/src/main/scala/scala/meta/languageserver/protocol/RequestId.scala similarity index 93% rename from languageserver/src/main/scala/scala/meta/lsp/RequestId.scala rename to metaserver/src/main/scala/scala/meta/languageserver/protocol/RequestId.scala index 5a214a4cd34..8fd49326fdd 100644 --- a/languageserver/src/main/scala/scala/meta/lsp/RequestId.scala +++ b/metaserver/src/main/scala/scala/meta/languageserver/protocol/RequestId.scala @@ -1,9 +1,8 @@ -package scala.meta.lsp +package scala.meta.languageserver.protocol import play.api.libs.json._ sealed trait RequestId - object RequestId { implicit val format: Format[RequestId] = Format[RequestId]( Reads { diff --git a/languageserver/src/main/scala/scala/meta/lsp/Response.scala b/metaserver/src/main/scala/scala/meta/languageserver/protocol/Response.scala similarity index 96% rename from languageserver/src/main/scala/scala/meta/lsp/Response.scala rename to metaserver/src/main/scala/scala/meta/languageserver/protocol/Response.scala index 673036d806f..06611e3a802 100644 --- a/languageserver/src/main/scala/scala/meta/lsp/Response.scala +++ b/metaserver/src/main/scala/scala/meta/languageserver/protocol/Response.scala @@ -1,4 +1,4 @@ -package scala.meta.lsp +package scala.meta.languageserver.protocol import play.api.libs.json._ diff --git a/metaserver/src/test/scala/tests/JsonRpcSuite.scala b/metaserver/src/test/scala/tests/JsonRpcSuite.scala index 71dbc9c0d68..13d0d4ead6a 100644 --- a/metaserver/src/test/scala/tests/JsonRpcSuite.scala +++ b/metaserver/src/test/scala/tests/JsonRpcSuite.scala @@ -3,14 +3,14 @@ package tests import java.io.PipedInputStream import java.io.PipedOutputStream import java.io.PrintWriter -import scala.meta.lsp.LSPMessage +import scala.meta.languageserver.protocol.BaseProtocol import monix.execution.CancelableFuture import monix.execution.ExecutionModel.AlwaysAsyncExecution import monix.execution.schedulers.TestScheduler object JsonRpcSuite extends MegaSuite { var out: PrintWriter = _ - var messages = List.empty[LSPMessage] + var messages = List.empty[BaseProtocol] implicit var s: TestScheduler = _ var f: CancelableFuture[Unit] = CancelableFuture.unit override def utestBeforeEach(path: Seq[String]): Unit = { @@ -18,7 +18,7 @@ object JsonRpcSuite extends MegaSuite { val in = new PipedInputStream out = new PrintWriter(new PipedOutputStream(in)) messages = Nil - f = LSPMessage.fromInputStream(in).foreach { msg => + f = BaseProtocol.fromInputStream(in).foreach { msg => messages = msg :: messages } } @@ -39,7 +39,7 @@ object JsonRpcSuite extends MegaSuite { val content = """{"jsonrpc":"2.0","id":1,"method":"example"}""" val message: String = header + content - val lspMessage = LSPMessage(Map("Content-Length" -> "43"), content) + val lspMessage = BaseProtocol(Map("Content-Length" -> "43"), content) test("header and content together") { write(message) diff --git a/vscode-extension/src/extension.ts b/vscode-extension/src/extension.ts index 6126113f819..d2d15d2a398 100644 --- a/vscode-extension/src/extension.ts +++ b/vscode-extension/src/extension.ts @@ -39,7 +39,7 @@ export async function activate(context: ExtensionContext) { toolsJar, 'org.scalameta:metaserver_2.12:0.1-SNAPSHOT', '-M', - 'scala.meta.languageserver.Main' + 'scala.meta.languageserver.protocol.SimpleMain' ]; const javaArgs = [ From 44c0acb4911ba4357246917fc55cb1abed0c6fee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=93lafur=20P=C3=A1ll=20Geirsson?= Date: Wed, 27 Dec 2017 01:11:02 +0000 Subject: [PATCH 07/18] Implement LSP with new scheme --- .../scala/langserver/messages/Commands.scala | 3 + .../languageserver/InitializeService.scala | 50 ++++++++++++++ .../protocol/BaseProtocol.scala | 22 ------ .../protocol/BaseProtocolMessage.scala | 27 ++++++++ ....scala => BaseProtocolMessageParser.scala} | 36 ++++++---- .../protocol/JsonRequestService.scala | 8 +++ .../protocol/LanguageServer.scala | 41 ++++++----- .../languageserver/protocol/SimpleMain.scala | 69 +++++++++++++++++++ .../src/test/scala/tests/JsonRpcSuite.scala | 10 +-- .../src/main/scala/example/User.scala | 1 + 10 files changed, 208 insertions(+), 59 deletions(-) create mode 100644 metaserver/src/main/scala/scala/meta/languageserver/InitializeService.scala delete mode 100644 metaserver/src/main/scala/scala/meta/languageserver/protocol/BaseProtocol.scala create mode 100644 metaserver/src/main/scala/scala/meta/languageserver/protocol/BaseProtocolMessage.scala rename metaserver/src/main/scala/scala/meta/languageserver/protocol/{ProtocolParser.scala => BaseProtocolMessageParser.scala} (69%) create mode 100644 metaserver/src/main/scala/scala/meta/languageserver/protocol/SimpleMain.scala diff --git a/languageserver/src/main/scala/langserver/messages/Commands.scala b/languageserver/src/main/scala/langserver/messages/Commands.scala index 8cf7723b952..2badc66079c 100644 --- a/languageserver/src/main/scala/langserver/messages/Commands.scala +++ b/languageserver/src/main/scala/langserver/messages/Commands.scala @@ -35,6 +35,9 @@ case class InitializeParams( * The capabilities provided by the client (editor) */ capabilities: ClientCapabilities) extends ServerCommand +object InitializeParams { + implicit val format: OFormat[InitializeParams] = Json.format[InitializeParams] +} case class InitializeError(retry: Boolean) diff --git a/metaserver/src/main/scala/scala/meta/languageserver/InitializeService.scala b/metaserver/src/main/scala/scala/meta/languageserver/InitializeService.scala new file mode 100644 index 00000000000..449d01f3923 --- /dev/null +++ b/metaserver/src/main/scala/scala/meta/languageserver/InitializeService.scala @@ -0,0 +1,50 @@ +package scala.meta.languageserver + +import scala.meta.languageserver.protocol.RequestService +import com.typesafe.scalalogging.LazyLogging +import langserver.messages.CompletionOptions +import langserver.messages.ExecuteCommandOptions +import langserver.messages.InitializeParams +import langserver.messages.InitializeResult +import langserver.messages.ServerCapabilities +import langserver.messages.SignatureHelpOptions +import monix.eval.Task +import monix.execution.Scheduler +import org.langmeta.io.AbsolutePath + +class InitializeService( + effects: List[Effects], + cwd: AbsolutePath +)(implicit s: Scheduler) + extends RequestService[InitializeParams, InitializeResult]("initialize") + with LazyLogging { + override def handle(request: InitializeParams) = Task { + logger.info(s"Initialized with $cwd, $request") +// cancelEffects = effects.map(_.subscribe()) +// loadAllRelevantFilesInThisWorkspace() + val capabilities = ServerCapabilities( + completionProvider = Some( + CompletionOptions( + resolveProvider = false, + triggerCharacters = "." :: Nil + ) + ), + signatureHelpProvider = Some( + SignatureHelpOptions( + triggerCharacters = "(" :: Nil + ) + ), + definitionProvider = true, + referencesProvider = true, + documentHighlightProvider = true, + documentSymbolProvider = true, + documentFormattingProvider = true, + hoverProvider = true, + executeCommandProvider = + ExecuteCommandOptions(WorkspaceCommand.values.map(_.entryName)), + workspaceSymbolProvider = true, + renameProvider = true + ) + Right(InitializeResult(capabilities)) + } +} diff --git a/metaserver/src/main/scala/scala/meta/languageserver/protocol/BaseProtocol.scala b/metaserver/src/main/scala/scala/meta/languageserver/protocol/BaseProtocol.scala deleted file mode 100644 index 999772ab7d7..00000000000 --- a/metaserver/src/main/scala/scala/meta/languageserver/protocol/BaseProtocol.scala +++ /dev/null @@ -1,22 +0,0 @@ -package scala.meta.languageserver.protocol - -import java.io.InputStream -import monix.reactive.Observable - -case class BaseProtocol(header: Map[String, String], content: String) { - override def toString: String = { - val sb = new java.lang.StringBuilder() - header.foreach { - case (key, value) => - sb.append(key).append(": ").append(value).append("\n") - } - sb.append("\n") - .append(content) - sb.toString - } -} - -object BaseProtocol { - def fromInputStream(in: InputStream): Observable[BaseProtocol] = - Observable.fromInputStream(in).liftByOperator(new ProtocolParser) -} diff --git a/metaserver/src/main/scala/scala/meta/languageserver/protocol/BaseProtocolMessage.scala b/metaserver/src/main/scala/scala/meta/languageserver/protocol/BaseProtocolMessage.scala new file mode 100644 index 00000000000..50198f96bbd --- /dev/null +++ b/metaserver/src/main/scala/scala/meta/languageserver/protocol/BaseProtocolMessage.scala @@ -0,0 +1,27 @@ +package scala.meta.languageserver.protocol + +import java.io.InputStream +import java.util.concurrent.Executors +import monix.execution.Scheduler +import monix.reactive.Observable + +case class BaseProtocolMessage(header: Map[String, String], content: String) { + override def toString: String = { + val sb = new java.lang.StringBuilder() + header.foreach { + case (key, value) => + sb.append(key).append(": ").append(value).append("\n") + } + sb.append("\n") + .append(content) + sb.toString + } +} + +object BaseProtocolMessage { + def fromInputStream(in: InputStream): Observable[BaseProtocolMessage] = + Observable + .fromInputStream(in) + .executeOn(Scheduler(Executors.newFixedThreadPool(1))) + .liftByOperator(new BaseProtocolMessageParser) +} diff --git a/metaserver/src/main/scala/scala/meta/languageserver/protocol/ProtocolParser.scala b/metaserver/src/main/scala/scala/meta/languageserver/protocol/BaseProtocolMessageParser.scala similarity index 69% rename from metaserver/src/main/scala/scala/meta/languageserver/protocol/ProtocolParser.scala rename to metaserver/src/main/scala/scala/meta/languageserver/protocol/BaseProtocolMessageParser.scala index a619e076a7a..7f90823f31a 100644 --- a/metaserver/src/main/scala/scala/meta/languageserver/protocol/ProtocolParser.scala +++ b/metaserver/src/main/scala/scala/meta/languageserver/protocol/BaseProtocolMessageParser.scala @@ -9,24 +9,30 @@ import monix.execution.Scheduler import monix.reactive.observables.ObservableLike.Operator import monix.reactive.observers.Subscriber -final class ProtocolParser - extends Operator[Array[Byte], BaseProtocol] +final class BaseProtocolMessageParser + extends Operator[Array[Byte], BaseProtocolMessage] with LazyLogging { - override def apply(out: Subscriber[BaseProtocol]): Subscriber[Array[Byte]] = { + override def apply( + out: Subscriber[BaseProtocolMessage] + ): Subscriber[Array[Byte]] = { new Subscriber[Array[Byte]] { import Ack._ + // NOTE(olafur): We should first benchmark before going into any + // optimization, but my intuition tells me ArrayBuffer[Byte] with many .remove + // and ++= is wasteful and can probably be replaced with some ByteBuffer + // or mutable.Queue[Array[Byte]] to get better performance. private[this] val data = ArrayBuffer.empty[Byte] private[this] var contentLength = -1 private[this] var header = Map.empty[String, String] - private def atDelimiter(idx: Int): Boolean = { + private[this] def atDelimiter(idx: Int): Boolean = { (data.size >= idx + 4 && data(idx) == '\r' && data(idx + 1) == '\n' && data(idx + 2) == '\r' && data(idx + 3) == '\n') } - private val EmptyPair = "" -> "" - private[this] def readHeaders: Future[Ack] = { + private[this] val EmptyPair = "" -> "" + private[this] def readHeaders(): Future[Ack] = { if (data.size < 4) Continue else { var i = 0 @@ -39,6 +45,10 @@ final class ProtocolParser data.copyToArray(bytes) data.remove(0, i + 4) val headers = new String(bytes, StandardCharsets.US_ASCII) + // NOTE(olafur) all LSP messages from vscode seem to start with + // Content-Length: N and include no other headers. + // However, the spec says there can be other headers so we parse them + // here anyways, even if we don't use any other values than Content-Length. val pairs: Map[String, String] = headers .split("\r\n") .iterator @@ -57,7 +67,7 @@ final class ProtocolParser try { contentLength = n.toInt header = pairs - readContent + readContent() } catch { case _: NumberFormatException => logger.error( @@ -72,7 +82,7 @@ final class ProtocolParser } } } - private[this] def readContent: Future[Ack] = { + private[this] def readContent(): Future[Ack] = { if (contentLength > data.size) Continue else { val contentBytes = new Array[Byte](contentLength) @@ -80,9 +90,9 @@ final class ProtocolParser data.remove(0, contentLength) contentLength = -1 val content = new String(contentBytes, StandardCharsets.UTF_8) - out.onNext(BaseProtocol(header, content)).flatMap { - case Continue => readHeaders - case els => els + out.onNext(BaseProtocolMessage(header, content)).flatMap { + case Continue => readHeaders() + case Stop => Stop } } } @@ -94,8 +104,8 @@ final class ProtocolParser } override def onNext(elem: Array[Byte]): Future[Ack] = { data ++= elem - if (contentLength < 0) readHeaders - else readContent + if (contentLength < 0) readHeaders() + else readContent() } } } diff --git a/metaserver/src/main/scala/scala/meta/languageserver/protocol/JsonRequestService.scala b/metaserver/src/main/scala/scala/meta/languageserver/protocol/JsonRequestService.scala index 81ac00a8bab..f82140d3525 100644 --- a/metaserver/src/main/scala/scala/meta/languageserver/protocol/JsonRequestService.scala +++ b/metaserver/src/main/scala/scala/meta/languageserver/protocol/JsonRequestService.scala @@ -36,3 +36,11 @@ abstract class RequestService[A: Reads, B: Writes](val method: String) } } } +object RequestService { + def method[A: Reads, B: Writes]( + name: String + )(f: A => Task[Either[Response.Error, B]]): RequestService[A, B] = + new RequestService[A, B](name) { + def handle(request: A): Task[Either[Response.Error, B]] = f(request) + } +} diff --git a/metaserver/src/main/scala/scala/meta/languageserver/protocol/LanguageServer.scala b/metaserver/src/main/scala/scala/meta/languageserver/protocol/LanguageServer.scala index 21d0e57268c..d199850ff96 100644 --- a/metaserver/src/main/scala/scala/meta/languageserver/protocol/LanguageServer.scala +++ b/metaserver/src/main/scala/scala/meta/languageserver/protocol/LanguageServer.scala @@ -1,8 +1,9 @@ package scala.meta.languageserver.protocol import java.io.OutputStream -import java.util.concurrent.Executors import scala.collection.concurrent.TrieMap +import scala.concurrent.Await +import scala.concurrent.duration.Duration import scala.util.control.NonFatal import com.fasterxml.jackson.core.JsonParseException import com.typesafe.scalalogging.LazyLogging @@ -17,7 +18,7 @@ import play.api.libs.json.JsValue import play.api.libs.json.Json final class LanguageServer( - in: Observable[BaseProtocol], + in: Observable[BaseProtocolMessage], out: OutputStream, notifications: JsonNotificationService, requests: JsonRequestService, @@ -59,7 +60,7 @@ final class LanguageServer( } } - def handleMessage(message: BaseProtocol): Task[Response] = + def handleMessage(message: BaseProtocolMessage): Task[Response] = LanguageServer.parseMessage(message) match { case Left(parseError) => Task.now(parseError) case Right(json) => @@ -69,28 +70,30 @@ final class LanguageServer( } } - def startTask: Task[Unit] = in.foreachL { msg => - handleMessage(msg) - .map { - case Response.Empty => () - case x: Response.Success => writer.write(x) - case x: Response.Error => writer.write(x) - } - .onErrorRecover { - case NonFatal(e) => - logger.error("Unhandled error", e) - } - .runAsync(requestScheduler) - } + def startTask: Task[Unit] = + in.foreachL { msg => + handleMessage(msg) + .map { + case Response.Empty => () + case x: Response.Success => writer.write(x) + case x: Response.Error => writer.write(x) + } + .onErrorRecover { + case NonFatal(e) => + logger.error("Unhandled error", e) + } + .runAsync(requestScheduler) + } def listen(): Unit = { - logger.info("Start listening....") - startTask.runAsync(Scheduler(Executors.newFixedThreadPool(1))) + val f = startTask.runAsync(requestScheduler) + logger.info("Listening....") + Await.result(f, Duration.Inf) } } object LanguageServer { - def parseMessage(message: BaseProtocol): Either[Response, JsValue] = + def parseMessage(message: BaseProtocolMessage): Either[Response, JsValue] = try { Right(Json.parse(message.content)) } catch { diff --git a/metaserver/src/main/scala/scala/meta/languageserver/protocol/SimpleMain.scala b/metaserver/src/main/scala/scala/meta/languageserver/protocol/SimpleMain.scala new file mode 100644 index 00000000000..3f5f3f037c2 --- /dev/null +++ b/metaserver/src/main/scala/scala/meta/languageserver/protocol/SimpleMain.scala @@ -0,0 +1,69 @@ +package scala.meta.languageserver.protocol + +import java.io.FileOutputStream +import java.io.PrintStream +import java.nio.file.Files +import java.util.concurrent.Executors +import scala.util.Properties +import scala.util.control.NonFatal +import com.typesafe.scalalogging.LazyLogging +import langserver.messages.InitializeResult +import langserver.messages.ServerCapabilities +import monix.eval.Task +import monix.execution.Scheduler +import monix.execution.schedulers.SchedulerService +import org.langmeta.internal.io.PathIO +import play.api.libs.json.Json + +object SimpleMain extends LazyLogging { + def main(args: Array[String]): Unit = { + val cwd = PathIO.workingDirectory + val configDir = cwd.resolve(".metaserver").toNIO + val logFile = configDir.resolve("metaserver.log").toFile + Files.createDirectories(configDir) + val out = new PrintStream(new FileOutputStream(logFile)) + val err = new PrintStream(new FileOutputStream(logFile)) + val stdin = System.in + val stdout = System.out + val stderr = System.err + val s: SchedulerService = + Scheduler(Executors.newFixedThreadPool(4)) + try { + // route System.out somewhere else. Any output not from the server (e.g. logging) + // messes up with the client, since stdout is used for the language server protocol + System.setOut(out) + System.setErr(err) + logger.info(s"Starting server in $cwd") + logger.info(s"Classpath: ${Properties.javaClassPath}") + val server = new LanguageServer( + BaseProtocolMessage.fromInputStream(stdin), + stdout, + (notification: Notification) => + Task.eval(logger.info(s"Notificaiton $notification")), + (request: Request) => + Task.eval { + if (request.method == "initialize") + Response.success( + Json.toJson(InitializeResult(ServerCapabilities())), + request.id + ) + else { + logger.info(request.toString) + Response.internalError("???", request.id) + } + }, + s + ) + server.listen() + logger.warn("Stopped listening :(") + } catch { + case NonFatal(e) => + logger.error("Uncaught top-level error", e) + } finally { + System.setOut(stdout) + System.setErr(stderr) + } + System.exit(0) + } + +} diff --git a/metaserver/src/test/scala/tests/JsonRpcSuite.scala b/metaserver/src/test/scala/tests/JsonRpcSuite.scala index 13d0d4ead6a..27279475b7f 100644 --- a/metaserver/src/test/scala/tests/JsonRpcSuite.scala +++ b/metaserver/src/test/scala/tests/JsonRpcSuite.scala @@ -3,14 +3,14 @@ package tests import java.io.PipedInputStream import java.io.PipedOutputStream import java.io.PrintWriter -import scala.meta.languageserver.protocol.BaseProtocol +import scala.meta.languageserver.protocol.BaseProtocolMessage import monix.execution.CancelableFuture import monix.execution.ExecutionModel.AlwaysAsyncExecution import monix.execution.schedulers.TestScheduler -object JsonRpcSuite extends MegaSuite { +class JsonRpcSuite extends MegaSuite { var out: PrintWriter = _ - var messages = List.empty[BaseProtocol] + var messages = List.empty[BaseProtocolMessage] implicit var s: TestScheduler = _ var f: CancelableFuture[Unit] = CancelableFuture.unit override def utestBeforeEach(path: Seq[String]): Unit = { @@ -18,7 +18,7 @@ object JsonRpcSuite extends MegaSuite { val in = new PipedInputStream out = new PrintWriter(new PipedOutputStream(in)) messages = Nil - f = BaseProtocol.fromInputStream(in).foreach { msg => + f = BaseProtocolMessage.fromInputStream(in).foreach { msg => messages = msg :: messages } } @@ -39,7 +39,7 @@ object JsonRpcSuite extends MegaSuite { val content = """{"jsonrpc":"2.0","id":1,"method":"example"}""" val message: String = header + content - val lspMessage = BaseProtocol(Map("Content-Length" -> "43"), content) + val lspMessage = BaseProtocolMessage(Map("Content-Length" -> "43"), content) test("header and content together") { write(message) diff --git a/test-workspace/src/main/scala/example/User.scala b/test-workspace/src/main/scala/example/User.scala index afa25528e5f..15305dc5ece 100644 --- a/test-workspace/src/main/scala/example/User.scala +++ b/test-workspace/src/main/scala/example/User.scala @@ -7,6 +7,7 @@ object a { val y = List(1, x).length def z = { val localSymbol = "222" // can be renamed + localSymbol.length } } From ac32a84def69d543d8e24ea2f66c97954f3c8165 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=93lafur=20P=C3=A1ll=20Geirsson?= Date: Wed, 3 Jan 2018 12:41:25 +0100 Subject: [PATCH 08/18] Start implementing lsp messages, looking good --- .../scala/langserver/messages/Commands.scala | 15 +++ .../protocol/JsonNotificationService.scala | 7 +- .../protocol/JsonRequestService.scala | 6 +- .../protocol/LanguageServer.scala | 12 +++ .../languageserver/protocol/Response.scala | 6 +- .../languageserver/protocol/Services.scala | 95 +++++++++++++++++++ .../languageserver/protocol/SimpleMain.scala | 94 +++++++++++++++--- .../src/main/scala/example/User.scala | 3 +- .../src/test/scala/example/UserTest.scala | 2 +- 9 files changed, 215 insertions(+), 25 deletions(-) create mode 100644 metaserver/src/main/scala/scala/meta/languageserver/protocol/Services.scala diff --git a/languageserver/src/main/scala/langserver/messages/Commands.scala b/languageserver/src/main/scala/langserver/messages/Commands.scala index 2badc66079c..17b2de33469 100644 --- a/languageserver/src/main/scala/langserver/messages/Commands.scala +++ b/languageserver/src/main/scala/langserver/messages/Commands.scala @@ -191,6 +191,9 @@ case class TextDocumentPositionParams( textDocument: TextDocumentIdentifier, position: Position ) +object TextDocumentPositionParams { + implicit val format = Json.format[TextDocumentPositionParams] +} case class ReferenceParams( textDocument: TextDocumentIdentifier, position: Position, @@ -295,12 +298,24 @@ object PublishDiagnostics { case class Exit() extends Notification case class DidOpenTextDocumentParams(textDocument: TextDocumentItem) extends Notification +object DidOpenTextDocumentParams { + implicit val format = Json.format[DidOpenTextDocumentParams] +} case class DidChangeTextDocumentParams( textDocument: VersionedTextDocumentIdentifier, contentChanges: Seq[TextDocumentContentChangeEvent]) extends Notification +object DidChangeTextDocumentParams { + implicit val format: OFormat[DidChangeTextDocumentParams] = Json.format[DidChangeTextDocumentParams] +} case class DidCloseTextDocumentParams(textDocument: TextDocumentIdentifier) extends Notification +object DidCloseTextDocumentParams { + implicit val format = Json.format[DidCloseTextDocumentParams] +} case class DidSaveTextDocumentParams(textDocument: TextDocumentIdentifier) extends Notification +object DidSaveTextDocumentParams { + implicit val format = Json.format[DidSaveTextDocumentParams] +} case class DidChangeWatchedFilesParams(changes: Seq[FileEvent]) extends Notification case class DidChangeConfigurationParams(settings: JsValue) extends Notification diff --git a/metaserver/src/main/scala/scala/meta/languageserver/protocol/JsonNotificationService.scala b/metaserver/src/main/scala/scala/meta/languageserver/protocol/JsonNotificationService.scala index 12957ef353b..201a6ad7c71 100644 --- a/metaserver/src/main/scala/scala/meta/languageserver/protocol/JsonNotificationService.scala +++ b/metaserver/src/main/scala/scala/meta/languageserver/protocol/JsonNotificationService.scala @@ -29,10 +29,11 @@ abstract class NotificationService[A: Reads](val method: String) handle(value) } } + object NotificationService { - def method[A: Reads]( - name: String - )(f: A => Task[Unit]): NotificationService[A] = + def notification[A: Reads](name: String)( + f: A => Task[Unit] + ): NamedNotificationService = new NotificationService[A](name) { override def handle(request: A): Task[Unit] = f(request) } diff --git a/metaserver/src/main/scala/scala/meta/languageserver/protocol/JsonRequestService.scala b/metaserver/src/main/scala/scala/meta/languageserver/protocol/JsonRequestService.scala index f82140d3525..81882da4718 100644 --- a/metaserver/src/main/scala/scala/meta/languageserver/protocol/JsonRequestService.scala +++ b/metaserver/src/main/scala/scala/meta/languageserver/protocol/JsonRequestService.scala @@ -37,9 +37,9 @@ abstract class RequestService[A: Reads, B: Writes](val method: String) } } object RequestService { - def method[A: Reads, B: Writes]( - name: String - )(f: A => Task[Either[Response.Error, B]]): RequestService[A, B] = + def request[A: Reads, B: Writes](name: String)( + f: A => Task[Either[Response.Error, B]] + ): NamedRequestService = new RequestService[A, B](name) { def handle(request: A): Task[Either[Response.Error, B]] = f(request) } diff --git a/metaserver/src/main/scala/scala/meta/languageserver/protocol/LanguageServer.scala b/metaserver/src/main/scala/scala/meta/languageserver/protocol/LanguageServer.scala index d199850ff96..dc38dccf802 100644 --- a/metaserver/src/main/scala/scala/meta/languageserver/protocol/LanguageServer.scala +++ b/metaserver/src/main/scala/scala/meta/languageserver/protocol/LanguageServer.scala @@ -24,6 +24,18 @@ final class LanguageServer( requests: JsonRequestService, requestScheduler: Scheduler ) extends LazyLogging { + def this( + in: Observable[BaseProtocolMessage], + out: OutputStream, + services: Services, + requestScheduler: Scheduler + ) = this( + in, + out, + new CompositeNotificationService(services.notifications), + new CompositeRequestService(services.requests), + requestScheduler + ) private val writer = new MessageWriter(out) private val activeClientRequests: TrieMap[JsValue, Cancelable] = TrieMap.empty diff --git a/metaserver/src/main/scala/scala/meta/languageserver/protocol/Response.scala b/metaserver/src/main/scala/scala/meta/languageserver/protocol/Response.scala index 06611e3a802..d37cc1af18e 100644 --- a/metaserver/src/main/scala/scala/meta/languageserver/protocol/Response.scala +++ b/metaserver/src/main/scala/scala/meta/languageserver/protocol/Response.scala @@ -14,15 +14,19 @@ object Response { } case object Empty extends Response def empty: Response = Empty + def ok(result: JsValue, id: RequestId): Response = + success(result, id) def success(result: JsValue, id: RequestId): Response = Success(result, id) def error(error: ErrorObject, id: RequestId): Response.Error = Error(error, id) def internalError(message: String, id: RequestId): Response.Error = Error(ErrorObject(ErrorCode.InternalError, message, None), id) + def invalidParams(message: String, id: RequestId): Response.Error = + Error(ErrorObject(ErrorCode.InvalidParams, message, None), id) def invalidRequest(message: String): Response.Error = Error( - ErrorObject(ErrorCode.InvalidParams, message, None), + ErrorObject(ErrorCode.InvalidRequest, message, None), RequestId.Null ) def cancelled(id: JsValue): Response.Error = diff --git a/metaserver/src/main/scala/scala/meta/languageserver/protocol/Services.scala b/metaserver/src/main/scala/scala/meta/languageserver/protocol/Services.scala new file mode 100644 index 00000000000..8d95c316466 --- /dev/null +++ b/metaserver/src/main/scala/scala/meta/languageserver/protocol/Services.scala @@ -0,0 +1,95 @@ +package scala.meta.languageserver.protocol + +import scala.meta.languageserver.PlayJsonEnrichments._ +import com.typesafe.scalalogging.LazyLogging +import langserver.messages.DidChangeTextDocumentParams +import langserver.messages.InitializeParams +import langserver.messages.InitializeResult +import monix.eval.Task +import play.api.libs.json._ + +trait Service[A, B] { + def handle(request: A): Task[B] +} +trait MethodName { + def methodName: String +} + +trait JsonRpcService extends Service[Message, Response] +trait NamedJsonRpcService extends JsonRpcService with MethodName + +object Service extends LazyLogging { + + def notification[A: Reads]( + method: String + )(f: Service[A, Unit]): NamedJsonRpcService = + new NamedJsonRpcService { + private def fail(msg: String): Task[Response] = Task { + logger.error(msg) + Response.empty + } + override def handle(message: Message): Task[Response] = message match { + case Notification(`method`, params) => + params.getOrElse(JsNull).validate[A] match { + case err: JsError => + fail(s"Failed to parse notification $message. Errors: $err") + case JsSuccess(value, _) => + f.handle(value).map(_ => Response.empty) + } + case Notification(invalidMethod, _) => + fail(s"Expected method '$method', obtained '$invalidMethod'") + case request: Request => + fail( + s"Expected notification with no ID, obtained request with id $request" + ) + } + + override def methodName: String = method + } + + def request[A: Reads, B: Writes](method: String)( + f: Service[A, B] + ): NamedJsonRpcService = new NamedJsonRpcService { + override def handle(message: Message): Task[Response] = message match { + case Request(`method`, params, id) => + params.getOrElse(JsNull).validate[A] match { + case err: JsError => + Task(Response.invalidParams(err.show, id)) + case JsSuccess(value, _) => + f.handle(value).map { response => + Response.ok(Json.toJson(response), id) + } + } + case Request(invalidMethod, _, id) => + Task(Response.methodNotFound(invalidMethod, id)) + case _ => + Task(Response.invalidRequest(s"Expected request, obtained $message")) + } + override def methodName: String = method + } + +} + +object Services { + val init: Services = Services(scala.Nil, scala.Nil) +} + +case class Services( + requests: List[NamedRequestService], + notifications: List[NamedNotificationService] +) extends Router { + override def requestAsync[A: Reads, B: Writes](method: String)( + f: A => Task[Either[Response.Error, B]] + ): Services = + Services( + RequestService.request[A, B](method)(f) :: requests, + notifications + ) + override def notificationAsync[A: Reads](method: String)( + f: A => Task[Unit] + ): Services = + Services( + requests, + NotificationService.notification[A](method)(f) :: notifications + ) +} diff --git a/metaserver/src/main/scala/scala/meta/languageserver/protocol/SimpleMain.scala b/metaserver/src/main/scala/scala/meta/languageserver/protocol/SimpleMain.scala index 3f5f3f037c2..29b2d6c8bc2 100644 --- a/metaserver/src/main/scala/scala/meta/languageserver/protocol/SimpleMain.scala +++ b/metaserver/src/main/scala/scala/meta/languageserver/protocol/SimpleMain.scala @@ -7,15 +7,42 @@ import java.util.concurrent.Executors import scala.util.Properties import scala.util.control.NonFatal import com.typesafe.scalalogging.LazyLogging +import langserver.messages.CompletionList +import langserver.messages.CompletionOptions +import langserver.messages.DidChangeTextDocumentParams +import langserver.messages.DidCloseTextDocumentParams +import langserver.messages.DidOpenTextDocumentParams +import langserver.messages.DidSaveTextDocumentParams +import langserver.messages.InitializeParams import langserver.messages.InitializeResult import langserver.messages.ServerCapabilities +import langserver.messages.TextDocumentPositionParams +import langserver.types.CompletionItem +import langserver.types.TextDocumentSyncKind import monix.eval.Task import monix.execution.Scheduler import monix.execution.schedulers.SchedulerService import org.langmeta.internal.io.PathIO -import play.api.libs.json.Json +import play.api.libs.json.JsNull +import play.api.libs.json.JsValue +import play.api.libs.json.Reads +import play.api.libs.json.Writes + +trait Router { + def requestAsync[A: Reads, B: Writes](method: String)( + f: A => Task[Either[Response.Error, B]] + ): Router + def request[A: Reads, B: Writes](method: String)( + f: A => Either[Response.Error, B] + ): Router = + requestAsync[A, B](method)(request => Task(f(request))) + def notificationAsync[A: Reads](method: String)(f: A => Task[Unit]): Router + def notification[A: Reads](method: String)(f: A => Unit): Router = + notificationAsync[A](method)(request => Task(f(request))) +} object SimpleMain extends LazyLogging { + def main(args: Array[String]): Unit = { val cwd = PathIO.workingDirectory val configDir = cwd.resolve(".metaserver").toNIO @@ -35,23 +62,60 @@ object SimpleMain extends LazyLogging { System.setErr(err) logger.info(s"Starting server in $cwd") logger.info(s"Classpath: ${Properties.javaClassPath}") + val services = Services.init + .request[InitializeParams, InitializeResult]("initialize") { params => + pprint.log(params) + Right( + InitializeResult( + ServerCapabilities( + completionProvider = + Some(CompletionOptions(resolveProvider = true, "." :: Nil)) + ) + ) + ) + } + .request[JsValue, JsValue]("shutdown") { _ => + pprint.log("shutdown") + Right(JsNull) + } + .notification[JsValue]("exit") { _ => + pprint.log("exit") + sys.exit(0) + } + .request[TextDocumentPositionParams, List[CompletionItem]]( + "textDocument/completion" + ) { params => + pprint.log(params) + Right(Nil) + } + .notification[DidCloseTextDocumentParams]( + "textDocument/didClose" + ) { params => + pprint.log(params) + () + } + .notification[DidOpenTextDocumentParams]( + "textDocument/didOpen" + ) { params => + pprint.log(params) + () + } + .notification[DidChangeTextDocumentParams]( + "textDocument/didChange" + ) { params => + pprint.log(params) + () + } + .notification[DidSaveTextDocumentParams]( + "textDocument/didSave" + ) { params => + pprint.log(params) + () + } val server = new LanguageServer( BaseProtocolMessage.fromInputStream(stdin), stdout, - (notification: Notification) => - Task.eval(logger.info(s"Notificaiton $notification")), - (request: Request) => - Task.eval { - if (request.method == "initialize") - Response.success( - Json.toJson(InitializeResult(ServerCapabilities())), - request.id - ) - else { - logger.info(request.toString) - Response.internalError("???", request.id) - } - }, + services.asInstanceOf[Services], s ) server.listen() diff --git a/test-workspace/src/main/scala/example/User.scala b/test-workspace/src/main/scala/example/User.scala index 15305dc5ece..b19bee53863 100644 --- a/test-workspace/src/main/scala/example/User.scala +++ b/test-workspace/src/main/scala/example/User.scala @@ -7,7 +7,6 @@ object a { val y = List(1, x).length def z = { val localSymbol = "222" // can be renamed - localSymbol.length } -} +} \ No newline at end of file diff --git a/test-workspace/src/test/scala/example/UserTest.scala b/test-workspace/src/test/scala/example/UserTest.scala index cd1de19ee22..91ca57e7f89 100644 --- a/test-workspace/src/test/scala/example/UserTest.scala +++ b/test-workspace/src/test/scala/example/UserTest.scala @@ -8,5 +8,5 @@ class UserTest { .map(x => x.+(user.age)) scala.runtime.CharRef.create('a') val str = user.name + a.a.x - val left: Either[String, Int] = Left("") + val left: Either[String, Int] = Left("") } From 101f302d29fddb63c7fea2af0496238130a8dd57 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=93lafur=20P=C3=A1ll=20Geirsson?= Date: Wed, 3 Jan 2018 18:54:15 +0100 Subject: [PATCH 09/18] Before the great refactoring --- build.sbt | 112 +++---- .../scala/meta/languageserver/Linter.scala | 10 +- .../ScalametaLanguageServer.scala | 312 +++++++++--------- .../protocol/LanguageServer.scala | 88 ++--- .../languageserver/protocol/Message.scala | 4 +- .../languageserver/protocol/Services.scala | 99 +++--- .../languageserver/protocol/SimpleMain.scala | 88 +---- .../providers/CodeActionProvider.scala | 10 +- .../providers/DefinitionProvider.scala | 14 +- .../providers/DocumentHighlightProvider.scala | 9 +- .../providers/DocumentSymbolProvider.scala | 7 +- .../providers/SquiggliesProvider.scala | 5 +- 12 files changed, 343 insertions(+), 415 deletions(-) diff --git a/build.sbt b/build.sbt index 0e1ed5c1c66..b0c37bcf249 100644 --- a/build.sbt +++ b/build.sbt @@ -1,62 +1,62 @@ inThisBuild( - semanticdbSettings ++ - List( - version ~= { old => - if (sys.env.contains("CI")) old - else "0.1-SNAPSHOT" // to avoid manually updating extension.js - }, - scalaVersion := V.scala212, - scalacOptions ++= List( - "-deprecation", - "-Xlint" - ), - scalafixEnabled := false, - organization := "org.scalameta", - licenses := Seq( - "Apache-2.0" -> url("http://www.apache.org/licenses/LICENSE-2.0") +// semanticdbSettings ++ + List( + version ~= { old => + if (sys.env.contains("CI")) old + else "0.1-SNAPSHOT" // to avoid manually updating extension.js + }, + scalaVersion := V.scala212, + scalacOptions ++= List( + "-deprecation", +// "-Xlint" + ), + scalafixEnabled := false, + organization := "org.scalameta", + licenses := Seq( + "Apache-2.0" -> url("http://www.apache.org/licenses/LICENSE-2.0") + ), + homepage := Some(url("https://github.com/scalameta/language-server")), + developers := List( + Developer( + "laughedelic", + "Alexey Alekhin", + "laughedelic@gmail.com", + url("https://github.com/laughedelic") ), - homepage := Some(url("https://github.com/scalameta/language-server")), - developers := List( - Developer( - "laughedelic", - "Alexey Alekhin", - "laughedelic@gmail.com", - url("https://github.com/laughedelic") - ), - Developer( - "gabro", - "Gabriele Petronella", - "gabriele@buildo.io", - url("https://github.com/gabro") - ), - Developer( - "olafurpg", - "Ólafur Páll Geirsson", - "olafurpg@gmail.com", - url("https://geirsson.com") - ), - Developer( - "ShaneDelmore", - "Shane Delmore", - "sdelmore@twitter.com", - url("http://delmore.io") - ) + Developer( + "gabro", + "Gabriele Petronella", + "gabriele@buildo.io", + url("https://github.com/gabro") ), - scmInfo in ThisBuild := Some( - ScmInfo( - url("https://github.com/scalameta/language-server"), - s"scm:git:git@github.com:scalameta/language-server.git" - ) + Developer( + "olafurpg", + "Ólafur Páll Geirsson", + "olafurpg@gmail.com", + url("https://geirsson.com") ), - releaseEarlyWith := BintrayPublisher, - releaseEarlyEnableSyncToMaven := false, - publishMavenStyle := true, - bintrayOrganization := Some("scalameta"), - bintrayReleaseOnPublish := dynverGitDescribeOutput.value.isVersionStable, - // faster publishLocal: - publishArtifact in packageDoc := sys.env.contains("CI"), - publishArtifact in packageSrc := sys.env.contains("CI") - ) + Developer( + "ShaneDelmore", + "Shane Delmore", + "sdelmore@twitter.com", + url("http://delmore.io") + ) + ), + scmInfo in ThisBuild := Some( + ScmInfo( + url("https://github.com/scalameta/language-server"), + s"scm:git:git@github.com:scalameta/language-server.git" + ) + ), + releaseEarlyWith := BintrayPublisher, + releaseEarlyEnableSyncToMaven := false, + publishMavenStyle := true, + bintrayOrganization := Some("scalameta"), + bintrayReleaseOnPublish := dynverGitDescribeOutput.value.isVersionStable, + // faster publishLocal: + publishArtifact in packageDoc := sys.env.contains("CI"), + publishArtifact in packageSrc := sys.env.contains("CI") + ) ) lazy val V = new { @@ -126,7 +126,7 @@ lazy val metaserver = project "org.scalameta" %% "semanticdb-scalac" % V.scalameta cross CrossVersion.full, "com.beachape" %% "enumeratum" % V.enumeratum, "com.lihaoyi" %% "utest" % "0.6.0" % Test, - "org.scalameta" %% "testkit" % V.scalameta % Test, + "org.scalameta" %% "testkit" % V.scalameta % Test ) ) .dependsOn( diff --git a/metaserver/src/main/scala/scala/meta/languageserver/Linter.scala b/metaserver/src/main/scala/scala/meta/languageserver/Linter.scala index 23937d4eebf..35f2bd48018 100644 --- a/metaserver/src/main/scala/scala/meta/languageserver/Linter.scala +++ b/metaserver/src/main/scala/scala/meta/languageserver/Linter.scala @@ -18,10 +18,7 @@ import com.typesafe.scalalogging.LazyLogging import langserver.types.Diagnostic import org.langmeta.io.AbsolutePath -class Linter( - cwd: AbsolutePath, - out: OutputStream, -) extends LazyLogging { +class Linter(cwd: AbsolutePath) extends LazyLogging { // Simple method to run syntactic scalafix rules on a string. def onSyntacticInput( @@ -92,9 +89,6 @@ class Linter( private def configFile: Option[m.Input] = ScalafixConfig.auto(cwd) private def lazySemanticdbIndex(index: SemanticdbIndex): LazySemanticdbIndex = - new LazySemanticdbIndex( - _ => Some(index), - ScalafixReporter.default.copy(outStream = new PrintStream(out)) - ) + new LazySemanticdbIndex(_ => Some(index), ScalafixReporter.default) } diff --git a/metaserver/src/main/scala/scala/meta/languageserver/ScalametaLanguageServer.scala b/metaserver/src/main/scala/scala/meta/languageserver/ScalametaLanguageServer.scala index 4580121e16a..ab85cdefae2 100644 --- a/metaserver/src/main/scala/scala/meta/languageserver/ScalametaLanguageServer.scala +++ b/metaserver/src/main/scala/scala/meta/languageserver/ScalametaLanguageServer.scala @@ -1,9 +1,6 @@ package scala.meta.languageserver import java.io.IOException -import java.io.InputStream -import java.io.OutputStream -import java.io.PrintStream import java.nio.file.FileVisitResult import java.nio.file.Files import java.nio.file.Path @@ -11,47 +8,19 @@ import java.nio.file.SimpleFileVisitor import java.nio.file.attribute.BasicFileAttributes import java.util.concurrent.Executors import scala.concurrent.duration.FiniteDuration +import scala.meta.languageserver.PlayJsonEnrichments._ import scala.meta.languageserver.compiler.CompilerConfig import scala.meta.languageserver.compiler.Cursor import scala.meta.languageserver.compiler.ScalacProvider +import scala.meta.languageserver.protocol.Response +import scala.meta.languageserver.protocol.Services import scala.meta.languageserver.providers._ import scala.meta.languageserver.refactoring.OrganizeImports import scala.meta.languageserver.search.SymbolIndex -import scala.meta.languageserver.PlayJsonEnrichments._ import com.typesafe.scalalogging.LazyLogging import io.github.soc.directories.ProjectDirectories import langserver.core.Connection -import langserver.core.LanguageServer -import langserver.messages.CodeActionRequest -import langserver.messages.CodeActionResult -import langserver.messages.CompletionList -import langserver.messages.CompletionOptions -import langserver.messages.DefinitionResult -import langserver.messages.DocumentFormattingResult -import langserver.messages.DocumentHighlightResult -import langserver.messages.DocumentSymbolParams -import langserver.messages.DocumentSymbolResult -import langserver.messages.ExecuteCommandOptions -import langserver.messages.Hover -import langserver.messages.InitializeParams -import langserver.messages.InitializeResult -import langserver.messages.ReferencesResult -import langserver.messages.RenameResult -import langserver.messages.ServerCapabilities -import langserver.messages.ShutdownResult -import langserver.messages.SignatureHelpOptions -import langserver.messages.SignatureHelpResult -import langserver.messages.TextDocumentCompletionRequest -import langserver.messages.TextDocumentDefinitionRequest -import langserver.messages.TextDocumentDocumentHighlightRequest -import langserver.messages.TextDocumentFormattingRequest -import langserver.messages.TextDocumentHoverRequest -import langserver.messages.TextDocumentReferencesRequest -import langserver.messages.TextDocumentRenameRequest -import langserver.messages.TextDocumentSignatureHelpRequest -import langserver.messages.WorkspaceExecuteCommandRequest -import langserver.messages.WorkspaceSymbolRequest -import langserver.messages.WorkspaceSymbolResult +import langserver.messages._ import langserver.types._ import monix.eval.Task import monix.execution.Cancelable @@ -69,23 +38,24 @@ import org.langmeta.io.AbsolutePath import org.langmeta.languageserver.InputEnrichments._ import org.langmeta.semanticdb import play.api.libs.json.JsError +import play.api.libs.json.JsNull import play.api.libs.json.JsSuccess import play.api.libs.json.JsValue class ScalametaLanguageServer( - cwd: AbsolutePath, - lspIn: InputStream, - lspOut: OutputStream, - stdout: PrintStream + cwd: AbsolutePath )(implicit s: Scheduler) - extends LanguageServer(lspIn, lspOut) { + extends LazyLogging { + val connection: Connection = null private val tempSourcesDir: AbsolutePath = cwd.resolve("target").resolve("sources") // Always run the presentation compiler on the same thread private val presentationCompilerScheduler: SchedulerService = Scheduler(Executors.newFixedThreadPool(1)) - def withPC[A](f: => A): Task[A] = - Task(f).executeOn(presentationCompilerScheduler) + def onPresentationCompilerThread[A]( + f: => A + ): Task[Either[Response.Error, A]] = + Task(Right(f)).executeOn(presentationCompilerScheduler) val (fileSystemSemanticdbSubscriber, fileSystemSemanticdbsPublisher) = ScalametaLanguageServer.fileSystemSemanticdbStream(cwd) val (compilerConfigSubscriber, compilerConfigPublisher) = @@ -106,7 +76,7 @@ class ScalametaLanguageServer( val documentFormattingProvider = new DocumentFormattingProvider(configurationPublisher, cwd, connection) val squiggliesProvider = - new SquiggliesProvider(configurationPublisher, cwd, stdout) + new SquiggliesProvider(configurationPublisher, cwd) val scalacProvider = new ScalacProvider val interactiveSemanticdbs: Observable[semanticdb.Database] = sourceChangePublisher @@ -158,46 +128,149 @@ class ScalametaLanguageServer( onChangedFile(path)(_ => ()) } } - - override def initialize( - request: InitializeParams - ): Task[InitializeResult] = Task { - logger.info(s"Initialized with $cwd, $request") - cancelEffects = effects.map(_.subscribe()) - loadAllRelevantFilesInThisWorkspace() - val capabilities = ServerCapabilities( - completionProvider = Some( - CompletionOptions( - resolveProvider = false, - triggerCharacters = "." :: Nil - ) - ), - signatureHelpProvider = Some( - SignatureHelpOptions( - triggerCharacters = "(" :: Nil - ) - ), - definitionProvider = true, - referencesProvider = true, - documentHighlightProvider = true, - documentSymbolProvider = true, - documentFormattingProvider = true, - hoverProvider = true, - executeCommandProvider = - ExecuteCommandOptions(WorkspaceCommand.values.map(_.entryName)), - workspaceSymbolProvider = true, - renameProvider = true, - codeActionProvider = true - ) - InitializeResult(capabilities) - } - - override def shutdown(): Task[ShutdownResult] = Task { - logger.info("Shutting down...") - cancelEffects.foreach(_.cancel()) - connection.cancelAllActiveRequests() - ShutdownResult() - } + val services: Services = Services.empty + .request[InitializeParams, InitializeResult]("initialize") { params => + pprint.log(params) + logger.info(s"Initialized with $cwd, $request") + cancelEffects = effects.map(_.subscribe()) + loadAllRelevantFilesInThisWorkspace() + val capabilities = ServerCapabilities( + completionProvider = Some( + CompletionOptions( + resolveProvider = false, + triggerCharacters = "." :: Nil + ) + ), + signatureHelpProvider = Some( + SignatureHelpOptions( + triggerCharacters = "(" :: Nil + ) + ), + definitionProvider = true, + referencesProvider = true, + documentHighlightProvider = true, + documentSymbolProvider = true, + documentFormattingProvider = true, + hoverProvider = true, + executeCommandProvider = + ExecuteCommandOptions(WorkspaceCommand.values.map(_.entryName)), + workspaceSymbolProvider = true, + renameProvider = true, + codeActionProvider = true + ) + InitializeResult(capabilities) + } + .request[JsValue, JsValue]("shutdown") { _ => + logger.info("Shutting down...") + cancelEffects.foreach(_.cancel()) + connection.cancelAllActiveRequests() + JsNull + } + .notification[JsValue]("exit") { _ => + pprint.log("exit") + sys.exit(0) + } + .requestAsync[TextDocumentPositionParams, CompletionList]( + "textDocument/completion" + ) { params => + onPresentationCompilerThread { + logger.info("completion") + scalacProvider.getCompiler(params.textDocument) match { + case Some(g) => + CompletionProvider.completions( + g, + toPoint(params.textDocument, params.position) + ) + case None => CompletionProvider.empty + } + } + } + .request[TextDocumentPositionParams, List[Location]]( + "textDocument/definition" + ) { params => + DefinitionProvider.definition( + symbolIndex, + Uri(params.textDocument.uri), + params.position, + tempSourcesDir + ) + } + .request[CodeActionParams, List[Command]]( + "textDocument/codeActions" + ) { params => + CodeActionProvider.codeActions(params) + } + .notification[DidCloseTextDocumentParams]( + "textDocument/didClose" + ) { params => + pprint.log(params) + () + } + .notification[DidOpenTextDocumentParams]( + "textDocument/didOpen" + ) { params => + pprint.log(params) + () + } + .notification[DidChangeTextDocumentParams]( + "textDocument/didChange" + ) { params => + pprint.log(params) + () + } + .notification[DidSaveTextDocumentParams]( + "textDocument/didSave" + ) { params => + pprint.log(params) + () + } + .notification[DidChangeConfigurationParams]( + "workspace/didChangeConfiguration" + ) { params => + (params.settings \ "scalameta").validate[Configuration] match { + case err: JsError => + connection.showMessage(MessageType.Error, err.show) + case JsSuccess(conf, _) => + logger.info(s"Configuration updated $conf") + configurationSubscriber.onNext(conf) + } + } + .notification[DidChangeWatchedFilesParams]( + "workspace/didChangeWatchedFiles" + ) { params => + params.changes.foreach { + case FileEvent( + Uri(path), + FileChangeType.Created | FileChangeType.Changed + ) => + onChangedFile(path.toAbsolutePath) { _ => + logger.warn(s"Unknown file extension for path $path") + } + + case event => + logger.warn(s"Unhandled file event: $event") + () + } + () + } + .request[TextDocumentPositionParams, List[DocumentHighlight]]( + "textDocument/documentHighlight" + ) { params => + DocumentHighlightProvider.highlight( + symbolIndex, + Uri(params.textDocument.uri), + params.position + ) + } + .request[DocumentSymbolParams, List[SymbolInformation]]( + "textDocument/documentSymbol" + ) { params => + val uri = Uri(params.textDocument.uri) + buffers.source(uri) match { + case Some(source) => DocumentSymbolProvider.documentSymbols(uri, source) + case None => DocumentSymbolProvider.empty + } + } private def onChangedFile( path: AbsolutePath @@ -211,80 +284,9 @@ class ScalametaLanguageServer( } } - override def onChangeWatchedFiles(changes: Seq[FileEvent]): Unit = - changes.foreach { - case FileEvent( - Uri(path), - FileChangeType.Created | FileChangeType.Changed - ) => - onChangedFile(path.toAbsolutePath) { _ => - logger.warn(s"Unknown file extension for path $path") - } - - case event => - logger.warn(s"Unhandled file event: $event") - () - } - - override def onChangeConfiguration(settings: JsValue): Unit = { - (settings \ "scalameta").validate[Configuration] match { - case err: JsError => - connection.showMessage(MessageType.Error, err.show) - case JsSuccess(conf, _) => - logger.info(s"Configuration updated $conf") - configurationSubscriber.onNext(conf) - } - } - - override def completion( - request: TextDocumentCompletionRequest - ): Task[CompletionList] = withPC { - logger.info("completion") - scalacProvider.getCompiler(request.params.textDocument) match { - case Some(g) => - CompletionProvider.completions( - g, - toPoint(request.params.textDocument, request.params.position) - ) - case None => CompletionProvider.empty - } - } - - override def codeAction(request: CodeActionRequest): Task[CodeActionResult] = - Task { - CodeActionProvider.codeActions(request) - } - - override def definition( - request: TextDocumentDefinitionRequest - ): Task[DefinitionResult] = Task { - DefinitionProvider.definition( - symbolIndex, - Uri(request.params.textDocument.uri), - request.params.position, - tempSourcesDir - ) - } - - override def documentHighlight( - request: TextDocumentDocumentHighlightRequest - ): Task[DocumentHighlightResult] = Task { - DocumentHighlightProvider.highlight( - symbolIndex, - Uri(request.params.textDocument.uri), - request.params.position - ) - } - override def documentSymbol( request: DocumentSymbolParams - ): Task[DocumentSymbolResult] = Task { - val uri = Uri(request.textDocument.uri) - buffers.source(uri) match { - case Some(source) => DocumentSymbolProvider.documentSymbols(uri, source) - case None => DocumentSymbolProvider.empty - } - } + ): Task[DocumentSymbolResult] = Task {} override def formatting( request: TextDocumentFormattingRequest diff --git a/metaserver/src/main/scala/scala/meta/languageserver/protocol/LanguageServer.scala b/metaserver/src/main/scala/scala/meta/languageserver/protocol/LanguageServer.scala index dc38dccf802..19e034188f4 100644 --- a/metaserver/src/main/scala/scala/meta/languageserver/protocol/LanguageServer.scala +++ b/metaserver/src/main/scala/scala/meta/languageserver/protocol/LanguageServer.scala @@ -20,57 +20,61 @@ import play.api.libs.json.Json final class LanguageServer( in: Observable[BaseProtocolMessage], out: OutputStream, - notifications: JsonNotificationService, - requests: JsonRequestService, + services: Services, requestScheduler: Scheduler ) extends LazyLogging { - def this( - in: Observable[BaseProtocolMessage], - out: OutputStream, - services: Services, - requestScheduler: Scheduler - ) = this( - in, - out, - new CompositeNotificationService(services.notifications), - new CompositeRequestService(services.requests), - requestScheduler - ) private val writer = new MessageWriter(out) private val activeClientRequests: TrieMap[JsValue, Cancelable] = TrieMap.empty + private val cancelNotification = + Service.notification[JsValue]("$/cancelNotification") { id => + activeClientRequests.get(id) match { + case None => + Task { + logger.warn(s"Can't cancel request $id, no active request found.") + Response.empty + } + case Some(request) => + Task { + request.cancel() + activeClientRequests.remove(id) + Response.cancelled(id) + } + } + } + private val handlersByMethodName: Map[String, NamedJsonRpcService] = + services.addService(cancelNotification).byMethodName - def handleValidMessage(message: Message): Task[Response] = - message match { - case request: Request => - val runningRequest = requests - .handleRequest(request) - .onErrorRecover { + def handleValidMessage(message: Message): Task[Response] = message match { + case Notification(method, _) => + handlersByMethodName.get(method) match { + case None => + Task { + // Can't respond to invalid notifications + logger.error(s"Unknown method '$method'") + Response.empty + } + case Some(handler) => + handler.handle(message).onErrorRecover { + case NonFatal(e) => + logger.error(s"Error handling notification $message", e) + Response.empty + } + } + case request @ Request(method, _, id) => + handlersByMethodName.get(method) match { + case None => Task(Response.methodNotFound(method, id)) + case Some(handler) => + val response = handler.handle(request).onErrorRecover { case NonFatal(e) => logger.error(s"Unhandled error handling request $request", e) Response.internalError(e.getMessage, request.id) } - .runAsync(requestScheduler) - activeClientRequests.put(Json.toJson(request.id), runningRequest) - Task.fromFuture(runningRequest) - case notification: Notification => - notification match { - case Notification("$/cancelNotification", Some(id)) => - activeClientRequests.get(id) match { - case None => - Task.now(Response.empty) - case Some(request) => - Task.eval { - request.cancel() - activeClientRequests.remove(id) - Response.cancelled(id) - } - } - case _ => - notifications - .handleNotification(notification) - .map(_ => Response.empty) - } - } + val runningResponse = response.runAsync(requestScheduler) + activeClientRequests.put(Json.toJson(request.id), runningResponse) + Task.fromFuture(runningResponse) + } + + } def handleMessage(message: BaseProtocolMessage): Task[Response] = LanguageServer.parseMessage(message) match { diff --git a/metaserver/src/main/scala/scala/meta/languageserver/protocol/Message.scala b/metaserver/src/main/scala/scala/meta/languageserver/protocol/Message.scala index 08d8257f3bc..bea4dbbb3b2 100644 --- a/metaserver/src/main/scala/scala/meta/languageserver/protocol/Message.scala +++ b/metaserver/src/main/scala/scala/meta/languageserver/protocol/Message.scala @@ -2,7 +2,9 @@ package scala.meta.languageserver.protocol import play.api.libs.json._ -sealed trait Message +sealed trait Message { + def method: String +} object Message { private case class IndeterminateMessage( method: String, diff --git a/metaserver/src/main/scala/scala/meta/languageserver/protocol/Services.scala b/metaserver/src/main/scala/scala/meta/languageserver/protocol/Services.scala index 8d95c316466..cd3535ff461 100644 --- a/metaserver/src/main/scala/scala/meta/languageserver/protocol/Services.scala +++ b/metaserver/src/main/scala/scala/meta/languageserver/protocol/Services.scala @@ -2,9 +2,6 @@ package scala.meta.languageserver.protocol import scala.meta.languageserver.PlayJsonEnrichments._ import com.typesafe.scalalogging.LazyLogging -import langserver.messages.DidChangeTextDocumentParams -import langserver.messages.InitializeParams -import langserver.messages.InitializeResult import monix.eval.Task import play.api.libs.json._ @@ -20,10 +17,33 @@ trait NamedJsonRpcService extends JsonRpcService with MethodName object Service extends LazyLogging { - def notification[A: Reads]( - method: String - )(f: Service[A, Unit]): NamedJsonRpcService = + def request[A: Reads, B: Writes](method: String)( + f: Service[A, Either[Response.Error, B]] + ): NamedJsonRpcService = new NamedJsonRpcService { + override def methodName: String = method + override def handle(message: Message): Task[Response] = message match { + case Request(`method`, params, id) => + params.getOrElse(JsNull).validate[A] match { + case err: JsError => + Task(Response.invalidParams(err.show, id)) + case JsSuccess(value, _) => + f.handle(value).map { + case Right(response) => Response.ok(Json.toJson(response), id) + case Left(err) => err + } + } + case Request(invalidMethod, _, id) => + Task(Response.methodNotFound(invalidMethod, id)) + case _ => + Task(Response.invalidRequest(s"Expected request, obtained $message")) + } + } + + def notification[A: Reads](method: String)( + f: Service[A, Unit] + ): NamedJsonRpcService = new NamedJsonRpcService { + override def methodName: String = method private def fail(msg: String): Task[Response] = Task { logger.error(msg) Response.empty @@ -43,53 +63,38 @@ object Service extends LazyLogging { s"Expected notification with no ID, obtained request with id $request" ) } - - override def methodName: String = method } - def request[A: Reads, B: Writes](method: String)( - f: Service[A, B] - ): NamedJsonRpcService = new NamedJsonRpcService { - override def handle(message: Message): Task[Response] = message match { - case Request(`method`, params, id) => - params.getOrElse(JsNull).validate[A] match { - case err: JsError => - Task(Response.invalidParams(err.show, id)) - case JsSuccess(value, _) => - f.handle(value).map { response => - Response.ok(Json.toJson(response), id) - } - } - case Request(invalidMethod, _, id) => - Task(Response.methodNotFound(invalidMethod, id)) - case _ => - Task(Response.invalidRequest(s"Expected request, obtained $message")) - } - override def methodName: String = method - } - } object Services { - val init: Services = Services(scala.Nil, scala.Nil) + val empty: Services = new Services(Nil) } -case class Services( - requests: List[NamedRequestService], - notifications: List[NamedNotificationService] -) extends Router { - override def requestAsync[A: Reads, B: Writes](method: String)( - f: A => Task[Either[Response.Error, B]] - ): Services = - Services( - RequestService.request[A, B](method)(f) :: requests, - notifications - ) - override def notificationAsync[A: Reads](method: String)( - f: A => Task[Unit] - ): Services = - Services( - requests, - NotificationService.notification[A](method)(f) :: notifications +class Services private (val services: List[NamedJsonRpcService]) { + def request[A: Reads, B: Writes](method: String)( + f: A => B + ): Services = requestAsync[A, B](method)(request => Task(Right(f(request)))) + def requestAsync[A: Reads, B: Writes](method: String)( + f: Service[A, Either[Response.Error, B]] + ): Services = addService(Service.request[A, B](method)(f)) + def notification[A: Reads](method: String)( + f: A => Unit + ): Services = notificationAsync[A](method)(request => Task(f(request))) + def notificationAsync[A: Reads](method: String)( + f: Service[A, Unit] + ): Services = addService(Service.notification[A](method)(f)) + + def +(other: Services): Services = + other.services.foldLeft(this)(_ addService _) + def byMethodName: Map[String, NamedJsonRpcService] = + services.iterator.map(s => s.methodName -> s).toMap + def addService(service: NamedJsonRpcService): Services = { + val duplicate = services.find(_.methodName == service.methodName) + require( + duplicate.isEmpty, + s"Duplicate service handler for method ${duplicate.get.methodName}" ) + new Services(service :: services) + } } diff --git a/metaserver/src/main/scala/scala/meta/languageserver/protocol/SimpleMain.scala b/metaserver/src/main/scala/scala/meta/languageserver/protocol/SimpleMain.scala index 29b2d6c8bc2..6141515097a 100644 --- a/metaserver/src/main/scala/scala/meta/languageserver/protocol/SimpleMain.scala +++ b/metaserver/src/main/scala/scala/meta/languageserver/protocol/SimpleMain.scala @@ -4,42 +4,13 @@ import java.io.FileOutputStream import java.io.PrintStream import java.nio.file.Files import java.util.concurrent.Executors +import scala.meta.languageserver.ScalametaLanguageServer import scala.util.Properties import scala.util.control.NonFatal import com.typesafe.scalalogging.LazyLogging -import langserver.messages.CompletionList -import langserver.messages.CompletionOptions -import langserver.messages.DidChangeTextDocumentParams -import langserver.messages.DidCloseTextDocumentParams -import langserver.messages.DidOpenTextDocumentParams -import langserver.messages.DidSaveTextDocumentParams -import langserver.messages.InitializeParams -import langserver.messages.InitializeResult -import langserver.messages.ServerCapabilities -import langserver.messages.TextDocumentPositionParams -import langserver.types.CompletionItem -import langserver.types.TextDocumentSyncKind -import monix.eval.Task import monix.execution.Scheduler import monix.execution.schedulers.SchedulerService import org.langmeta.internal.io.PathIO -import play.api.libs.json.JsNull -import play.api.libs.json.JsValue -import play.api.libs.json.Reads -import play.api.libs.json.Writes - -trait Router { - def requestAsync[A: Reads, B: Writes](method: String)( - f: A => Task[Either[Response.Error, B]] - ): Router - def request[A: Reads, B: Writes](method: String)( - f: A => Either[Response.Error, B] - ): Router = - requestAsync[A, B](method)(request => Task(f(request))) - def notificationAsync[A: Reads](method: String)(f: A => Task[Unit]): Router - def notification[A: Reads](method: String)(f: A => Unit): Router = - notificationAsync[A](method)(request => Task(f(request))) -} object SimpleMain extends LazyLogging { @@ -62,63 +33,14 @@ object SimpleMain extends LazyLogging { System.setErr(err) logger.info(s"Starting server in $cwd") logger.info(s"Classpath: ${Properties.javaClassPath}") - val services = Services.init - .request[InitializeParams, InitializeResult]("initialize") { params => - pprint.log(params) - Right( - InitializeResult( - ServerCapabilities( - completionProvider = - Some(CompletionOptions(resolveProvider = true, "." :: Nil)) - ) - ) - ) - } - .request[JsValue, JsValue]("shutdown") { _ => - pprint.log("shutdown") - Right(JsNull) - } - .notification[JsValue]("exit") { _ => - pprint.log("exit") - sys.exit(0) - } - .request[TextDocumentPositionParams, List[CompletionItem]]( - "textDocument/completion" - ) { params => - pprint.log(params) - Right(Nil) - } - .notification[DidCloseTextDocumentParams]( - "textDocument/didClose" - ) { params => - pprint.log(params) - () - } - .notification[DidOpenTextDocumentParams]( - "textDocument/didOpen" - ) { params => - pprint.log(params) - () - } - .notification[DidChangeTextDocumentParams]( - "textDocument/didChange" - ) { params => - pprint.log(params) - () - } - .notification[DidSaveTextDocumentParams]( - "textDocument/didSave" - ) { params => - pprint.log(params) - () - } - val server = new LanguageServer( + val metaserver = new ScalametaLanguageServer(cwd)(s) + val langserver = new LanguageServer( BaseProtocolMessage.fromInputStream(stdin), stdout, - services.asInstanceOf[Services], + metaserver.services, s ) - server.listen() + langserver.listen() logger.warn("Stopped listening :(") } catch { case NonFatal(e) => diff --git a/metaserver/src/main/scala/scala/meta/languageserver/providers/CodeActionProvider.scala b/metaserver/src/main/scala/scala/meta/languageserver/providers/CodeActionProvider.scala index c8ab10151f3..03dae023149 100644 --- a/metaserver/src/main/scala/scala/meta/languageserver/providers/CodeActionProvider.scala +++ b/metaserver/src/main/scala/scala/meta/languageserver/providers/CodeActionProvider.scala @@ -2,22 +2,20 @@ package scala.meta.languageserver.providers import scala.meta.languageserver.WorkspaceCommand.ScalafixUnusedImports import com.typesafe.scalalogging.LazyLogging -import langserver.messages.CodeActionRequest -import langserver.messages.CodeActionResult +import langserver.messages.CodeActionParams import langserver.types.Command import langserver.types.Diagnostic import play.api.libs.json.Json object CodeActionProvider extends LazyLogging { - def codeActions(request: CodeActionRequest): CodeActionResult = { - val removeUnusedImports = request.params.context.diagnostics.collectFirst { + def codeActions(params: CodeActionParams): List[Command] = { + params.context.diagnostics.collectFirst { case Diagnostic(_, _, _, Some("scalac"), "Unused import") => Command( "Remove unused imports", ScalafixUnusedImports.entryName, - Json.toJson(request.params.textDocument) :: Nil + Json.toJson(params.textDocument) :: Nil ) }.toList - CodeActionResult(removeUnusedImports) } } diff --git a/metaserver/src/main/scala/scala/meta/languageserver/providers/DefinitionProvider.scala b/metaserver/src/main/scala/scala/meta/languageserver/providers/DefinitionProvider.scala index 8a97d409f7b..12e0f0a4460 100644 --- a/metaserver/src/main/scala/scala/meta/languageserver/providers/DefinitionProvider.scala +++ b/metaserver/src/main/scala/scala/meta/languageserver/providers/DefinitionProvider.scala @@ -1,12 +1,12 @@ package scala.meta.languageserver.providers -import com.typesafe.scalalogging.LazyLogging -import org.langmeta.io.AbsolutePath -import langserver.{types => l} -import langserver.messages.DefinitionResult -import scala.meta.languageserver.search.SymbolIndex import scala.meta.languageserver.ScalametaEnrichments._ import scala.meta.languageserver.Uri +import scala.meta.languageserver.search.SymbolIndex +import com.typesafe.scalalogging.LazyLogging +import langserver.types.Location +import langserver.{types => l} +import org.langmeta.io.AbsolutePath object DefinitionProvider extends LazyLogging { @@ -15,13 +15,13 @@ object DefinitionProvider extends LazyLogging { uri: Uri, position: l.Position, tempSourcesDir: AbsolutePath - ): DefinitionResult = { + ): List[Location] = { val locations = for { data <- symbolIndex.findDefinition(uri, position.line, position.character) pos <- data.definition _ = logger.info(s"Found definition ${pos.pretty} ${data.symbol}") } yield pos.toLocation.toNonJar(tempSourcesDir) - DefinitionResult(locations.toList) + locations.toList } } diff --git a/metaserver/src/main/scala/scala/meta/languageserver/providers/DocumentHighlightProvider.scala b/metaserver/src/main/scala/scala/meta/languageserver/providers/DocumentHighlightProvider.scala index ebf237d0cbf..2632237c249 100644 --- a/metaserver/src/main/scala/scala/meta/languageserver/providers/DocumentHighlightProvider.scala +++ b/metaserver/src/main/scala/scala/meta/languageserver/providers/DocumentHighlightProvider.scala @@ -6,6 +6,8 @@ import langserver.messages.DocumentHighlightResult import scala.meta.languageserver.Uri import scala.meta.languageserver.search.SymbolIndex import scala.meta.languageserver.ScalametaEnrichments._ +import langserver.types.DocumentHighlight +import langserver.types.Location object DocumentHighlightProvider extends LazyLogging { @@ -13,17 +15,16 @@ object DocumentHighlightProvider extends LazyLogging { symbolIndex: SymbolIndex, uri: Uri, position: l.Position - ): DocumentHighlightResult = { + ): List[DocumentHighlight] = { logger.info(s"Document highlight in $uri") - val locations = for { + for { data <- symbolIndex.findReferences(uri, position.line, position.character) _ = logger.info(s"Highlighting symbol `${data.name}: ${data.signature}`") pos <- data.referencePositions(withDefinition = true) if pos.uri == uri.value _ = logger.debug(s"Found highlight at [${pos.range.get.pretty}]") - } yield pos.toLocation + } yield DocumentHighlight(pos.range.get.toRange) // TODO(alexey) add DocumentHighlightKind: Text (default), Read, Write - DocumentHighlightResult(locations) } } diff --git a/metaserver/src/main/scala/scala/meta/languageserver/providers/DocumentSymbolProvider.scala b/metaserver/src/main/scala/scala/meta/languageserver/providers/DocumentSymbolProvider.scala index bde98781429..d11ccdf9f51 100644 --- a/metaserver/src/main/scala/scala/meta/languageserver/providers/DocumentSymbolProvider.scala +++ b/metaserver/src/main/scala/scala/meta/languageserver/providers/DocumentSymbolProvider.scala @@ -5,6 +5,7 @@ import scala.meta.languageserver.ScalametaEnrichments._ import scala.meta.languageserver.Uri import com.typesafe.scalalogging.LazyLogging import langserver.messages.DocumentSymbolResult +import langserver.types.SymbolInformation import langserver.{types => l} object DocumentSymbolProvider extends LazyLogging { @@ -55,10 +56,10 @@ object DocumentSymbolProvider extends LazyLogging { } } - def empty = DocumentSymbolResult(Nil) + def empty: List[SymbolInformation] = Nil def documentSymbols( uri: Uri, source: Source - ): DocumentSymbolResult = - DocumentSymbolResult(new SymbolTraverser(uri).apply(source)) + ): List[SymbolInformation] = + new SymbolTraverser(uri).apply(source) } diff --git a/metaserver/src/main/scala/scala/meta/languageserver/providers/SquiggliesProvider.scala b/metaserver/src/main/scala/scala/meta/languageserver/providers/SquiggliesProvider.scala index 8193166f5eb..797ad9ca6da 100644 --- a/metaserver/src/main/scala/scala/meta/languageserver/providers/SquiggliesProvider.scala +++ b/metaserver/src/main/scala/scala/meta/languageserver/providers/SquiggliesProvider.scala @@ -15,14 +15,13 @@ import org.langmeta.AbsolutePath class SquiggliesProvider( configuration: Observable[Configuration], - cwd: AbsolutePath, - stdout: OutputStream + cwd: AbsolutePath )(implicit s: Scheduler) extends LazyLogging { private val isEnabled: () => Boolean = configuration.map(_.scalafix.enabled).toFunction0() - lazy val linter = new Linter(cwd, stdout) + lazy val linter = new Linter(cwd) def squigglies(doc: m.Document): Task[Seq[PublishDiagnostics]] = squigglies(m.Database(doc :: Nil)) From 0e259ecb5d438d14ba48baad97485826b6f21308 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=93lafur=20P=C3=A1ll=20Geirsson?= Date: Wed, 3 Jan 2018 19:35:48 +0100 Subject: [PATCH 10/18] Tests pass! --- build.sbt | 4 +- .../scala/langserver/core/Connection.scala | 2 +- .../langserver/core/LanguageServer.scala | 2 +- .../scala/langserver/core/MessageWriter.scala | 2 +- .../scala/langserver/core/Notifications.scala | 24 +- .../scala/langserver/messages/Commands.scala | 33 +- .../main/scala/langserver/types/types.scala | 9 +- .../languageserver/InitializeService.scala | 50 --- .../scala/meta/languageserver/Main.scala | 15 +- ...porter.scala => ScalacErrorReporter.scala} | 5 +- .../ScalametaLanguageServer.scala | 321 +++++++++--------- .../CompositeNotificationService.scala | 19 -- .../protocol/CompositeRequestService.scala | 22 -- .../protocol/JsonNotificationService.scala | 40 --- .../protocol/JsonRequestService.scala | 46 --- .../protocol/LanguageClient.scala | 104 ++++++ .../protocol/LanguageServer.scala | 23 +- .../languageserver/protocol/Message.scala | 59 +++- .../languageserver/protocol/RequestId.scala | 2 + .../languageserver/protocol/Response.scala | 41 --- .../languageserver/protocol/Services.scala | 6 +- .../languageserver/protocol/SimpleMain.scala | 55 --- .../DocumentFormattingProvider.scala | 13 +- .../providers/DocumentHighlightProvider.scala | 2 +- .../providers/ReferencesProvider.scala | 6 +- .../providers/RenameProvider.scala | 17 +- .../providers/SignatureHelpProvider.scala | 8 +- .../tests/compiler/SignatureHelpTest.scala | 4 +- .../scala/tests/compiler/SquiggliesTest.scala | 5 +- .../scala/tests/search/SymbolIndexTest.scala | 28 +- vscode-extension/src/extension.ts | 106 +++--- 31 files changed, 504 insertions(+), 569 deletions(-) delete mode 100644 metaserver/src/main/scala/scala/meta/languageserver/InitializeService.scala rename metaserver/src/main/scala/scala/meta/languageserver/{ErrorReporter.scala => ScalacErrorReporter.scala} (92%) delete mode 100644 metaserver/src/main/scala/scala/meta/languageserver/protocol/CompositeNotificationService.scala delete mode 100644 metaserver/src/main/scala/scala/meta/languageserver/protocol/CompositeRequestService.scala delete mode 100644 metaserver/src/main/scala/scala/meta/languageserver/protocol/JsonNotificationService.scala delete mode 100644 metaserver/src/main/scala/scala/meta/languageserver/protocol/JsonRequestService.scala create mode 100644 metaserver/src/main/scala/scala/meta/languageserver/protocol/LanguageClient.scala delete mode 100644 metaserver/src/main/scala/scala/meta/languageserver/protocol/Response.scala delete mode 100644 metaserver/src/main/scala/scala/meta/languageserver/protocol/SimpleMain.scala diff --git a/build.sbt b/build.sbt index b0c37bcf249..670d264df1c 100644 --- a/build.sbt +++ b/build.sbt @@ -1,5 +1,5 @@ inThisBuild( -// semanticdbSettings ++ + semanticdbSettings ++ List( version ~= { old => if (sys.env.contains("CI")) old @@ -8,7 +8,7 @@ inThisBuild( scalaVersion := V.scala212, scalacOptions ++= List( "-deprecation", -// "-Xlint" + "-Xlint" ), scalafixEnabled := false, organization := "org.scalameta", diff --git a/languageserver/src/main/scala/langserver/core/Connection.scala b/languageserver/src/main/scala/langserver/core/Connection.scala index 18e1d944c22..8987b61d2c6 100644 --- a/languageserver/src/main/scala/langserver/core/Connection.scala +++ b/languageserver/src/main/scala/langserver/core/Connection.scala @@ -27,7 +27,7 @@ import monix.execution.Scheduler * @param s thread pool to execute commands and notifications. */ abstract class Connection(inStream: InputStream, outStream: OutputStream)(implicit s: Scheduler) - extends LazyLogging with Notifications { + extends LazyLogging { private val msgReader = new MessageReader(inStream) private val msgWriter = new MessageWriter(outStream) private val activeRequestsById: JMap[Int, CancelableFuture[Unit]] = diff --git a/languageserver/src/main/scala/langserver/core/LanguageServer.scala b/languageserver/src/main/scala/langserver/core/LanguageServer.scala index 49d5d0f7ead..7de7fa4a982 100644 --- a/languageserver/src/main/scala/langserver/core/LanguageServer.scala +++ b/languageserver/src/main/scala/langserver/core/LanguageServer.scala @@ -75,7 +75,7 @@ class LanguageServer(inStream: InputStream, outStream: OutputStream)(implicit s: def hover(request: TextDocumentHoverRequest): Task[Hover] = Task.now(Hover(Nil, None)) def references(request: TextDocumentReferencesRequest): Task[ReferencesResult] = Task.now(ReferencesResult(Nil)) def rename(request: TextDocumentRenameRequest): Task[RenameResult] = Task.now(RenameResult(WorkspaceEdit(Map.empty))) - def signatureHelp(request: TextDocumentSignatureHelpRequest): Task[SignatureHelpResult] = Task.now(SignatureHelpResult(Nil, None, None)) + def signatureHelp(request: TextDocumentSignatureHelpRequest): Task[SignatureHelp] = Task.now(SignatureHelp(Nil, None, None)) // workspace def executeCommand(request: WorkspaceExecuteCommandRequest): Task[Unit] = Task.now(()) diff --git a/languageserver/src/main/scala/langserver/core/MessageWriter.scala b/languageserver/src/main/scala/langserver/core/MessageWriter.scala index eca6e4fe524..23f65df549c 100644 --- a/languageserver/src/main/scala/langserver/core/MessageWriter.scala +++ b/languageserver/src/main/scala/langserver/core/MessageWriter.scala @@ -28,7 +28,7 @@ class MessageWriter(out: OutputStream) extends LazyLogging { * Write a message to the output stream. This method can be called from multiple threads, * but it may block waiting for other threads to finish writing. */ - def write[T](msg: T, h: Map[String, String] = Map.empty)(implicit o: Format[T]): Unit = lock.synchronized { + def write[T](msg: T, h: Map[String, String] = Map.empty)(implicit o: Writes[T]): Unit = lock.synchronized { require(h.get(ContentLen).isEmpty) val str = Json.stringify(o.writes(msg)) diff --git a/languageserver/src/main/scala/langserver/core/Notifications.scala b/languageserver/src/main/scala/langserver/core/Notifications.scala index e94b947f476..e809f3c3237 100644 --- a/languageserver/src/main/scala/langserver/core/Notifications.scala +++ b/languageserver/src/main/scala/langserver/core/Notifications.scala @@ -1,11 +1,31 @@ package langserver.core +import langserver.messages.PublishDiagnostics +import langserver.messages.ShowMessageParams +import langserver.types.Diagnostic import langserver.types.MessageType /** Stub interface for Connection.showMessage */ trait Notifications { - def showMessage(tpe: MessageType, message: String): Unit + def publishDiagnostics(params: PublishDiagnostics): Unit + final def publishDiagnostics( + uri: String, + diagnostics: Seq[Diagnostic] + ): Unit = publishDiagnostics(PublishDiagnostics(uri, diagnostics)) + + def showMessage(params: ShowMessageParams): Unit + final def showMessage( + tpe: MessageType, + message: String + ): Unit = showMessage(ShowMessageParams(tpe, message)) } object Notifications { - val empty: Notifications = (_, _) => () + val empty: Notifications = new Notifications { + override def showMessage( + params: ShowMessageParams + ): Unit = () + override def publishDiagnostics( + publishDiagnostics: PublishDiagnostics + ): Unit = () + } } diff --git a/languageserver/src/main/scala/langserver/messages/Commands.scala b/languageserver/src/main/scala/langserver/messages/Commands.scala index 17b2de33469..d7184827acf 100644 --- a/languageserver/src/main/scala/langserver/messages/Commands.scala +++ b/languageserver/src/main/scala/langserver/messages/Commands.scala @@ -199,6 +199,9 @@ case class ReferenceParams( position: Position, context: ReferenceContext ) +object ReferenceParams { + implicit val format = Json.format[ReferenceParams] +} case class RenameParams( textDocument: TextDocumentIdentifier, @@ -223,6 +226,9 @@ object CodeActionRequest { implicit val format: OFormat[CodeActionRequest] = Json.format[CodeActionRequest] } case class DocumentSymbolParams(textDocument: TextDocumentIdentifier) extends ServerCommand +object DocumentSymbolParams { + implicit val format = Json.format[DocumentSymbolParams] +} case class TextDocumentRenameRequest(params: RenameParams) extends ServerCommand object TextDocumentRenameRequest { implicit val format: OFormat[TextDocumentRenameRequest] = Json.format[TextDocumentRenameRequest] @@ -234,8 +240,12 @@ case class TextDocumentReferencesRequest(params: ReferenceParams) extends Server case class TextDocumentDocumentHighlightRequest(params: TextDocumentPositionParams) extends ServerCommand case class TextDocumentHoverRequest(params: TextDocumentPositionParams) extends ServerCommand case class TextDocumentFormattingRequest(params: DocumentFormattingParams) extends ServerCommand -case class WorkspaceExecuteCommandRequest(params: WorkspaceExecuteCommandParams) extends ServerCommand +case class WorkspaceExecuteCommandRequest(params: ExecuteCommandParams) extends ServerCommand case class WorkspaceSymbolRequest(params: WorkspaceSymbolParams) extends ServerCommand +case class ApplyWorkspaceEditResponse(applied: Boolean) +object ApplyWorkspaceEditResponse { + implicit val format = Json.format[ApplyWorkspaceEditResponse] +} case class ApplyWorkspaceEditParams(label: Option[String], edit: WorkspaceEdit) object ApplyWorkspaceEditParams { implicit val format: OFormat[ApplyWorkspaceEditParams] = Json.format[ApplyWorkspaceEditParams] @@ -287,6 +297,9 @@ object ClientCommand extends CommandCompanion[ClientCommand] { // From server to client case class ShowMessageParams(`type`: MessageType, message: String) extends Notification +object ShowMessageParams { + implicit val format = Json.format[ShowMessageParams] +} case class LogMessageParams(`type`: MessageType, message: String) extends Notification case class PublishDiagnostics(uri: String, diagnostics: Seq[Diagnostic]) extends Notification object PublishDiagnostics { @@ -317,7 +330,13 @@ object DidSaveTextDocumentParams { implicit val format = Json.format[DidSaveTextDocumentParams] } case class DidChangeWatchedFilesParams(changes: Seq[FileEvent]) extends Notification +object DidChangeWatchedFilesParams { + implicit val format = Json.format[DidChangeWatchedFilesParams] +} case class DidChangeConfigurationParams(settings: JsValue) extends Notification +object DidChangeConfigurationParams { + implicit val format = Json.format[DidChangeConfigurationParams] +} case class Initialized() extends Notification object Initialized { @@ -366,11 +385,11 @@ case class DefinitionResult(params: Seq[Location]) extends ResultResponse case class ReferencesResult(params: Seq[Location]) extends ResultResponse case class DocumentHighlightResult(params: Seq[Location]) extends ResultResponse case class DocumentFormattingResult(params: Seq[TextEdit]) extends ResultResponse -case class SignatureHelpResult(signatures: Seq[SignatureInformation], - activeSignature: Option[Int], - activeParameter: Option[Int]) extends ResultResponse -object SignatureHelpResult { - implicit val format: OFormat[SignatureHelpResult] = Json.format[SignatureHelpResult] +case class SignatureHelp(signatures: Seq[SignatureInformation], + activeSignature: Option[Int], + activeParameter: Option[Int]) extends ResultResponse +object SignatureHelp { + implicit val format: OFormat[SignatureHelp] = Json.format[SignatureHelp] } case object ExecuteCommandResult extends ResultResponse case class WorkspaceSymbolResult(params: Seq[SymbolInformation]) extends ResultResponse @@ -386,7 +405,7 @@ object ResultResponse extends ResponseCompanion[Any] { "textDocument/completion" -> Json.format[CompletionList], "textDocument/codeAction" -> valueFormat(CodeActionResult.apply)(_.params), "textDocument/rename" -> valueFormat(RenameResult.apply)(_.params), - "textDocument/signatureHelp" -> Json.format[SignatureHelpResult], + "textDocument/signatureHelp" -> Json.format[SignatureHelp], "textDocument/definition" -> valueFormat(DefinitionResult)(_.params), "textDocument/references" -> valueFormat(ReferencesResult)(_.params), "textDocument/documentHighlight" -> valueFormat(DocumentHighlightResult)(_.params), diff --git a/languageserver/src/main/scala/langserver/types/types.scala b/languageserver/src/main/scala/langserver/types/types.scala index b9b365b931c..43b42dc041c 100644 --- a/languageserver/src/main/scala/langserver/types/types.scala +++ b/languageserver/src/main/scala/langserver/types/types.scala @@ -168,6 +168,9 @@ case class DocumentHighlight( /** The highlight kind, default is [text](#DocumentHighlightKind.Text). */ kind: DocumentHighlightKind = DocumentHighlightKind.Text) +object DocumentHighlight { + implicit val format = Json.format[DocumentHighlight] +} case class SymbolInformation( name: String, @@ -329,9 +332,9 @@ object DocumentFormattingParams { implicit val format: OFormat[DocumentFormattingParams] = Json.format[DocumentFormattingParams] } -case class WorkspaceExecuteCommandParams(command: String, arguments: Option[Seq[JsValue]]) -object WorkspaceExecuteCommandParams { - implicit val format: OFormat[WorkspaceExecuteCommandParams] = Json.format[WorkspaceExecuteCommandParams] +case class ExecuteCommandParams(command: String, arguments: Option[Seq[JsValue]]) +object ExecuteCommandParams { + implicit val format: OFormat[ExecuteCommandParams] = Json.format[ExecuteCommandParams] } diff --git a/metaserver/src/main/scala/scala/meta/languageserver/InitializeService.scala b/metaserver/src/main/scala/scala/meta/languageserver/InitializeService.scala deleted file mode 100644 index 449d01f3923..00000000000 --- a/metaserver/src/main/scala/scala/meta/languageserver/InitializeService.scala +++ /dev/null @@ -1,50 +0,0 @@ -package scala.meta.languageserver - -import scala.meta.languageserver.protocol.RequestService -import com.typesafe.scalalogging.LazyLogging -import langserver.messages.CompletionOptions -import langserver.messages.ExecuteCommandOptions -import langserver.messages.InitializeParams -import langserver.messages.InitializeResult -import langserver.messages.ServerCapabilities -import langserver.messages.SignatureHelpOptions -import monix.eval.Task -import monix.execution.Scheduler -import org.langmeta.io.AbsolutePath - -class InitializeService( - effects: List[Effects], - cwd: AbsolutePath -)(implicit s: Scheduler) - extends RequestService[InitializeParams, InitializeResult]("initialize") - with LazyLogging { - override def handle(request: InitializeParams) = Task { - logger.info(s"Initialized with $cwd, $request") -// cancelEffects = effects.map(_.subscribe()) -// loadAllRelevantFilesInThisWorkspace() - val capabilities = ServerCapabilities( - completionProvider = Some( - CompletionOptions( - resolveProvider = false, - triggerCharacters = "." :: Nil - ) - ), - signatureHelpProvider = Some( - SignatureHelpOptions( - triggerCharacters = "(" :: Nil - ) - ), - definitionProvider = true, - referencesProvider = true, - documentHighlightProvider = true, - documentSymbolProvider = true, - documentFormattingProvider = true, - hoverProvider = true, - executeCommandProvider = - ExecuteCommandOptions(WorkspaceCommand.values.map(_.entryName)), - workspaceSymbolProvider = true, - renameProvider = true - ) - Right(InitializeResult(capabilities)) - } -} diff --git a/metaserver/src/main/scala/scala/meta/languageserver/Main.scala b/metaserver/src/main/scala/scala/meta/languageserver/Main.scala index d270cf783b7..5481ba27e4d 100644 --- a/metaserver/src/main/scala/scala/meta/languageserver/Main.scala +++ b/metaserver/src/main/scala/scala/meta/languageserver/Main.scala @@ -4,6 +4,9 @@ import java.io.FileOutputStream import java.io.PrintStream import java.nio.file.Files import java.util.concurrent.Executors +import scala.meta.languageserver.protocol.BaseProtocolMessage +import scala.meta.languageserver.protocol.LanguageClient +import scala.meta.languageserver.protocol.LanguageServer import scala.util.Properties import scala.util.control.NonFatal import com.typesafe.scalalogging.LazyLogging @@ -29,11 +32,17 @@ object Main extends LazyLogging { // messes up with the client, since stdout is used for the language server protocol System.setOut(out) System.setErr(err) + val client = new LanguageClient(stdout) logger.info(s"Starting server in $cwd") logger.info(s"Classpath: ${Properties.javaClassPath}") - val server = new ScalametaLanguageServer(cwd, stdin, stdout, out) - LSPLogger.connection = Some(server.connection) - server.start() + val metaserver = new ScalametaLanguageServer(cwd, client)(s) + val langserver = new LanguageServer( + BaseProtocolMessage.fromInputStream(stdin), + client, + metaserver.services, + s + ) + langserver.listen() } catch { case NonFatal(e) => logger.error("Uncaught top-level exception", e) diff --git a/metaserver/src/main/scala/scala/meta/languageserver/ErrorReporter.scala b/metaserver/src/main/scala/scala/meta/languageserver/ScalacErrorReporter.scala similarity index 92% rename from metaserver/src/main/scala/scala/meta/languageserver/ErrorReporter.scala rename to metaserver/src/main/scala/scala/meta/languageserver/ScalacErrorReporter.scala index a9e99214b21..eb0619df3ef 100644 --- a/metaserver/src/main/scala/scala/meta/languageserver/ErrorReporter.scala +++ b/metaserver/src/main/scala/scala/meta/languageserver/ScalacErrorReporter.scala @@ -6,18 +6,19 @@ import scala.meta.semanticdb import scalafix.internal.util.EagerInMemorySemanticdbIndex import scalafix.util.SemanticdbIndex import langserver.core.Connection +import langserver.core.Notifications import langserver.messages.PublishDiagnostics import langserver.{types => l} class ScalacErrorReporter( - connection: Connection, + connection: Notifications, ) { def reportErrors( mdb: m.Database ): Effects.PublishScalacDiagnostics = { val messages = analyzeIndex(mdb) - messages.foreach(connection.sendNotification) + messages.foreach(connection.publishDiagnostics) Effects.PublishScalacDiagnostics } private def analyzeIndex(mdb: m.Database): Seq[PublishDiagnostics] = diff --git a/metaserver/src/main/scala/scala/meta/languageserver/ScalametaLanguageServer.scala b/metaserver/src/main/scala/scala/meta/languageserver/ScalametaLanguageServer.scala index ab85cdefae2..a5ad7611564 100644 --- a/metaserver/src/main/scala/scala/meta/languageserver/ScalametaLanguageServer.scala +++ b/metaserver/src/main/scala/scala/meta/languageserver/ScalametaLanguageServer.scala @@ -12,6 +12,7 @@ import scala.meta.languageserver.PlayJsonEnrichments._ import scala.meta.languageserver.compiler.CompilerConfig import scala.meta.languageserver.compiler.Cursor import scala.meta.languageserver.compiler.ScalacProvider +import scala.meta.languageserver.protocol.LanguageClient import scala.meta.languageserver.protocol.Response import scala.meta.languageserver.protocol.Services import scala.meta.languageserver.providers._ @@ -43,10 +44,10 @@ import play.api.libs.json.JsSuccess import play.api.libs.json.JsValue class ScalametaLanguageServer( - cwd: AbsolutePath + cwd: AbsolutePath, + client: LanguageClient )(implicit s: Scheduler) extends LazyLogging { - val connection: Connection = null private val tempSourcesDir: AbsolutePath = cwd.resolve("target").resolve("sources") // Always run the presentation compiler on the same thread @@ -66,15 +67,14 @@ class ScalametaLanguageServer( OverflowStrategy.DropOld(2) ) val (configurationSubscriber, configurationPublisher) = - ScalametaLanguageServer.configurationStream(connection) + ScalametaLanguageServer.configurationStream val buffers: Buffers = Buffers() val symbolIndex: SymbolIndex = - SymbolIndex(cwd, connection, buffers, configurationPublisher) - val scalacErrorReporter: ScalacErrorReporter = new ScalacErrorReporter( - connection - ) + SymbolIndex(cwd, client, buffers, configurationPublisher) + val scalacErrorReporter: ScalacErrorReporter = + new ScalacErrorReporter(client) val documentFormattingProvider = - new DocumentFormattingProvider(configurationPublisher, cwd, connection) + new DocumentFormattingProvider(configurationPublisher, cwd, client) val squiggliesProvider = new SquiggliesProvider(configurationPublisher, cwd) val scalacProvider = new ScalacProvider @@ -108,7 +108,7 @@ class ScalametaLanguageServer( val publishDiagnostics: Observable[Effects.PublishSquigglies] = metaSemanticdbs.mapTask { db => squiggliesProvider.squigglies(db).map { diagnostics => - diagnostics.foreach(connection.sendNotification) + diagnostics.foreach(client.publishDiagnostics) Effects.PublishSquigglies } } @@ -123,47 +123,52 @@ class ScalametaLanguageServer( publishDiagnostics, ) - private def loadAllRelevantFilesInThisWorkspace(): Unit = { - Workspace.initialize(cwd) { path => - onChangedFile(path)(_ => ()) - } + // TODO(olafur): make it easier to invoke fluid services from tests + def initialize( + params: InitializeParams + ): Task[Either[Response.Error, InitializeResult]] = { + logger.info(s"Initialized with $cwd, $params") + cancelEffects = effects.map(_.subscribe()) + loadAllRelevantFilesInThisWorkspace() + val capabilities = ServerCapabilities( + completionProvider = Some( + CompletionOptions( + resolveProvider = false, + triggerCharacters = "." :: Nil + ) + ), + signatureHelpProvider = Some( + SignatureHelpOptions( + triggerCharacters = "(" :: Nil + ) + ), + definitionProvider = true, + referencesProvider = true, + documentHighlightProvider = true, + documentSymbolProvider = true, + documentFormattingProvider = true, + hoverProvider = true, + executeCommandProvider = + ExecuteCommandOptions(WorkspaceCommand.values.map(_.entryName)), + workspaceSymbolProvider = true, + renameProvider = true, + codeActionProvider = true + ) + Task(Right(InitializeResult(capabilities))) } + + // TODO(olafur): make it easier to invoke fluid services from tests + def shutdown(): Unit = { + logger.info("Shutting down...") + cancelEffects.foreach(_.cancel()) + } + val services: Services = Services.empty - .request[InitializeParams, InitializeResult]("initialize") { params => - pprint.log(params) - logger.info(s"Initialized with $cwd, $request") - cancelEffects = effects.map(_.subscribe()) - loadAllRelevantFilesInThisWorkspace() - val capabilities = ServerCapabilities( - completionProvider = Some( - CompletionOptions( - resolveProvider = false, - triggerCharacters = "." :: Nil - ) - ), - signatureHelpProvider = Some( - SignatureHelpOptions( - triggerCharacters = "(" :: Nil - ) - ), - definitionProvider = true, - referencesProvider = true, - documentHighlightProvider = true, - documentSymbolProvider = true, - documentFormattingProvider = true, - hoverProvider = true, - executeCommandProvider = - ExecuteCommandOptions(WorkspaceCommand.values.map(_.entryName)), - workspaceSymbolProvider = true, - renameProvider = true, - codeActionProvider = true - ) - InitializeResult(capabilities) + .requestAsync[InitializeParams, InitializeResult]("initialize") { params => + initialize(params) } .request[JsValue, JsValue]("shutdown") { _ => - logger.info("Shutting down...") - cancelEffects.foreach(_.cancel()) - connection.cancelAllActiveRequests() + shutdown() JsNull } .notification[JsValue]("exit") { _ => @@ -179,7 +184,7 @@ class ScalametaLanguageServer( case Some(g) => CompletionProvider.completions( g, - toPoint(params.textDocument, params.position) + toCursor(params.textDocument, params.position) ) case None => CompletionProvider.empty } @@ -196,26 +201,33 @@ class ScalametaLanguageServer( ) } .request[CodeActionParams, List[Command]]( - "textDocument/codeActions" + "textDocument/codeAction" ) { params => CodeActionProvider.codeActions(params) } .notification[DidCloseTextDocumentParams]( "textDocument/didClose" ) { params => - pprint.log(params) + buffers.closed(Uri(params.textDocument)) () } .notification[DidOpenTextDocumentParams]( "textDocument/didOpen" ) { params => - pprint.log(params) + val input = + Input.VirtualFile(params.textDocument.uri, params.textDocument.text) + buffers.changed(input) + sourceChangeSubscriber.onNext(input) () } .notification[DidChangeTextDocumentParams]( "textDocument/didChange" ) { params => - pprint.log(params) + val changes = params.contentChanges + require(changes.length == 1, s"Expected one change, got $changes") + val input = Input.VirtualFile(params.textDocument.uri, changes.head.text) + buffers.changed(input) + sourceChangeSubscriber.onNext(input) () } .notification[DidSaveTextDocumentParams]( @@ -229,7 +241,7 @@ class ScalametaLanguageServer( ) { params => (params.settings \ "scalameta").validate[Configuration] match { case err: JsError => - connection.showMessage(MessageType.Error, err.show) + client.showMessage(MessageType.Error, err.show) case JsSuccess(conf, _) => logger.info(s"Configuration updated $conf") configurationSubscriber.onNext(conf) @@ -271,122 +283,86 @@ class ScalametaLanguageServer( case None => DocumentSymbolProvider.empty } } - - private def onChangedFile( - path: AbsolutePath - )(fallback: AbsolutePath => Unit): Unit = { - val name = PathIO.extension(path.toNIO) - logger.info(s"File $path changed, extension=$name") - name match { - case "semanticdb" => fileSystemSemanticdbSubscriber.onNext(path) - case "compilerconfig" => compilerConfigSubscriber.onNext(path) - case _ => fallback(path) + .requestAsync[DocumentFormattingParams, List[TextEdit]]( + "textDocument/formatting" + ) { params => + val uri = Uri(params.textDocument) + documentFormattingProvider.format(uri.toInput(buffers)) } - } - - override def documentSymbol( - request: DocumentSymbolParams - ): Task[DocumentSymbolResult] = Task {} - - override def formatting( - request: TextDocumentFormattingRequest - ): Task[DocumentFormattingResult] = { - val uri = Uri(request.params.textDocument) - documentFormattingProvider.format(uri.toInput(buffers)) - } - - override def hover( - request: TextDocumentHoverRequest - ): Task[Hover] = Task { - HoverProvider.hover( - symbolIndex, - Uri(request.params.textDocument), - request.params.position.line, - request.params.position.character - ) - } - - override def references( - request: TextDocumentReferencesRequest - ): Task[ReferencesResult] = Task { - ReferencesProvider.references( - symbolIndex, - Uri(request.params.textDocument.uri), - request.params.position, - request.params.context - ) - } - - override def rename(request: TextDocumentRenameRequest): Task[RenameResult] = - Task { - RenameProvider.rename(request, symbolIndex, connection) + .request[TextDocumentPositionParams, Hover]( + "textDocument/hover" + ) { params => + HoverProvider.hover( + symbolIndex, + Uri(params.textDocument), + params.position.line, + params.position.character + ) } - - override def signatureHelp( - request: TextDocumentSignatureHelpRequest - ): Task[SignatureHelpResult] = Task { - scalacProvider.getCompiler(request.params.textDocument) match { - case Some(g) => - SignatureHelpProvider.signatureHelp( - g, - toPoint(request.params.textDocument, request.params.position) - ) - case None => SignatureHelpProvider.empty + .request[ReferenceParams, List[Location]]( + "textDocument/references" + ) { params => + ReferencesProvider.references( + symbolIndex, + Uri(params.textDocument.uri), + params.position, + params.context + ) } - } - - override def onOpenTextDocument(td: TextDocumentItem): Unit = { - val input = Input.VirtualFile(td.uri, td.text) - buffers.changed(input) - sourceChangeSubscriber.onNext(input) - } - - override def executeCommand( - request: WorkspaceExecuteCommandRequest - ): Task[Unit] = Task { - import WorkspaceCommand._ - WorkspaceCommand - .withNameOption(request.params.command) - .fold(logger.error(s"Unknown command ${request.params.command}")) { - case ClearIndexCache => - logger.info("Clearing the index cache") - ScalametaLanguageServer.clearCacheDirectory() - symbolIndex.clearIndex() - scalacProvider.allCompilerConfigs.foreach( - config => symbolIndex.indexDependencyClasspath(config.sourceJars) + .request[RenameParams, WorkspaceEdit]( + "textDocument/rename" + ) { params => + RenameProvider.rename(params, symbolIndex, client) + } + .request[TextDocumentPositionParams, SignatureHelp]( + "textDocument/signatureHelp" + ) { params => + scalacProvider.getCompiler(params.textDocument) match { + case Some(g) => + SignatureHelpProvider.signatureHelp( + g, + toCursor(params.textDocument, params.position) ) - case ResetPresentationCompiler => - logger.info("Resetting all compiler instances") - scalacProvider.resetCompilers() - case ScalafixUnusedImports => - logger.info("Removing unused imports") - val result = - OrganizeImports.removeUnused(request.params.arguments, symbolIndex) - connection.workspaceApplyEdit(result) + case None => SignatureHelpProvider.empty } - } - - override def workspaceSymbol( - request: WorkspaceSymbolRequest - ): Task[WorkspaceSymbolResult] = Task { - logger.info(s"Workspace $request") - WorkspaceSymbolResult(symbolIndex.workspaceSymbols(request.params.query)) - } - - override def onChangeTextDocument( - td: VersionedTextDocumentIdentifier, - changes: Seq[TextDocumentContentChangeEvent] - ): Unit = { - require(changes.length == 1, s"Expected one change, got $changes") - val input = Input.VirtualFile(td.uri, changes.head.text) - buffers.changed(input) - sourceChangeSubscriber.onNext(input) - } - - override def onCloseTextDocument(td: TextDocumentIdentifier): Unit = - buffers.closed(Uri(td)) + } + .request[ExecuteCommandParams, JsValue]( + "workspace/executeCommand" + ) { params => + logger.info(s"executeCommand $params") + import WorkspaceCommand._ + WorkspaceCommand + .withNameOption(params.command) + .fold(logger.error(s"Unknown command ${params.command}")) { + case ClearIndexCache => + logger.info("Clearing the index cache") + ScalametaLanguageServer.clearCacheDirectory() + symbolIndex.clearIndex() + scalacProvider.allCompilerConfigs.foreach( + config => symbolIndex.indexDependencyClasspath(config.sourceJars) + ) + case ResetPresentationCompiler => + logger.info("Resetting all compiler instances") + scalacProvider.resetCompilers() + case ScalafixUnusedImports => + logger.info("Removing unused imports") + val result = + OrganizeImports.removeUnused( + params.arguments, + symbolIndex + ) + // TODO(olafur) make method return async + client.workspaceApplyEdit(result).runAsync + } + JsNull + } + .request[WorkspaceSymbolParams, List[SymbolInformation]]( + "workspace/symbol" + ) { params => + symbolIndex.workspaceSymbols(params.query) + } - private def toPoint( + private def toCursor( td: TextDocumentIdentifier, pos: Position ): Cursor = { @@ -396,6 +372,23 @@ class ScalametaLanguageServer( Cursor(Uri(td.uri), contents, offset) } + private def loadAllRelevantFilesInThisWorkspace(): Unit = { + Workspace.initialize(cwd) { path => + onChangedFile(path)(_ => ()) + } + } + + private def onChangedFile( + path: AbsolutePath + )(fallback: AbsolutePath => Unit): Unit = { + val name = PathIO.extension(path.toNIO) + logger.info(s"File $path changed, extension=$name") + name match { + case "semanticdb" => fileSystemSemanticdbSubscriber.onNext(path) + case "compilerconfig" => compilerConfigSubscriber.onNext(path) + case _ => fallback(path) + } + } } object ScalametaLanguageServer extends LazyLogging { @@ -446,7 +439,7 @@ object ScalametaLanguageServer extends LazyLogging { subscriber -> semanticdbPublisher } - def configurationStream(connection: Connection)( + def configurationStream( implicit scheduler: Scheduler ): (Observer.Sync[Configuration], Observable[Configuration]) = { val (subscriber, publisher) = @@ -457,7 +450,7 @@ object ScalametaLanguageServer extends LazyLogging { def multicast[A]( strategy: MulticastStrategy[A] = MulticastStrategy.publish - )(implicit s: Scheduler) = { + )(implicit s: Scheduler): (Observer.Sync[A], Observable[A]) = { val (sub, pub) = Observable.multicast[A](strategy) (sub, pub.doOnError(onError)) } diff --git a/metaserver/src/main/scala/scala/meta/languageserver/protocol/CompositeNotificationService.scala b/metaserver/src/main/scala/scala/meta/languageserver/protocol/CompositeNotificationService.scala deleted file mode 100644 index 9102167f7d3..00000000000 --- a/metaserver/src/main/scala/scala/meta/languageserver/protocol/CompositeNotificationService.scala +++ /dev/null @@ -1,19 +0,0 @@ -package scala.meta.languageserver.protocol - -import com.typesafe.scalalogging.LazyLogging -import monix.eval.Task - -class CompositeNotificationService( - notifications: List[NamedNotificationService] -) extends JsonNotificationService - with LazyLogging { - private val map = notifications.iterator.map(n => n.method -> n).toMap - - override def handleNotification(notification: Notification): Task[Unit] = - map.get(notification.method) match { - case Some(service) => service.handleNotification(notification) - case None => - logger.warn(s"Method not found '${notification.method}'") - Task.unit // No way to report error on notifications - } -} diff --git a/metaserver/src/main/scala/scala/meta/languageserver/protocol/CompositeRequestService.scala b/metaserver/src/main/scala/scala/meta/languageserver/protocol/CompositeRequestService.scala deleted file mode 100644 index 721ac650fe8..00000000000 --- a/metaserver/src/main/scala/scala/meta/languageserver/protocol/CompositeRequestService.scala +++ /dev/null @@ -1,22 +0,0 @@ -package scala.meta.languageserver.protocol - -import com.typesafe.scalalogging.LazyLogging -import monix.eval.Task - -class CompositeRequestService(requests: List[NamedRequestService]) - extends JsonRequestService - with LazyLogging { - private val map = requests.iterator.map(n => n.method -> n).toMap - override def handleRequest(request: Request): Task[Response] = - map.get(request.method) match { - case None => - Task.now( - Response.methodNotFound( - s"Method '${request.method}' not found, expected one of ${map.keys.mkString(", ")}", - request.id - ) - ) - case Some(service) => - service.handleRequest(request) - } -} diff --git a/metaserver/src/main/scala/scala/meta/languageserver/protocol/JsonNotificationService.scala b/metaserver/src/main/scala/scala/meta/languageserver/protocol/JsonNotificationService.scala deleted file mode 100644 index 201a6ad7c71..00000000000 --- a/metaserver/src/main/scala/scala/meta/languageserver/protocol/JsonNotificationService.scala +++ /dev/null @@ -1,40 +0,0 @@ -package scala.meta.languageserver.protocol - -import com.typesafe.scalalogging.LazyLogging -import monix.eval.Task -import play.api.libs.json.JsError -import play.api.libs.json.JsNull -import play.api.libs.json.JsSuccess -import play.api.libs.json.Reads - -trait JsonNotificationService { - def handleNotification(notification: Notification): Task[Unit] -} -trait NamedNotificationService extends JsonNotificationService { - def method: String -} -abstract class NotificationService[A: Reads](val method: String) - extends NamedNotificationService - with LazyLogging { - def handle(request: A): Task[Unit] - override def handleNotification(notification: Notification): Task[Unit] = - notification.params.getOrElse(JsNull).validate[A] match { - case err: JsError => - Task.eval { - logger.error( - s"Failed to parse notification $notification. Errors: $err" - ) - } - case JsSuccess(value, _) => - handle(value) - } -} - -object NotificationService { - def notification[A: Reads](name: String)( - f: A => Task[Unit] - ): NamedNotificationService = - new NotificationService[A](name) { - override def handle(request: A): Task[Unit] = f(request) - } -} diff --git a/metaserver/src/main/scala/scala/meta/languageserver/protocol/JsonRequestService.scala b/metaserver/src/main/scala/scala/meta/languageserver/protocol/JsonRequestService.scala deleted file mode 100644 index 81882da4718..00000000000 --- a/metaserver/src/main/scala/scala/meta/languageserver/protocol/JsonRequestService.scala +++ /dev/null @@ -1,46 +0,0 @@ -package scala.meta.languageserver.protocol - -import monix.eval.Task -import monix.execution.misc.NonFatal -import play.api.libs.json.JsError -import play.api.libs.json.JsNull -import play.api.libs.json.JsSuccess -import play.api.libs.json.Json -import play.api.libs.json.Reads -import play.api.libs.json.Writes - -trait JsonRequestService { - def handleRequest(request: Request): Task[Response] -} -trait NamedRequestService extends JsonRequestService { - def method: String -} -abstract class RequestService[A: Reads, B: Writes](val method: String) - extends NamedRequestService { - def handle(request: A): Task[Either[Response.Error, B]] - override def handleRequest(request: Request): Task[Response] = - request.params.getOrElse(JsNull).validate[A] match { - case err: JsError => - Task.eval(request.toError(ErrorCode.InvalidParams, err.toString)) - case JsSuccess(value, _) => - handle(value) - .map[Response] { - case Right(response) => - Response.success(Json.toJson(response), request.id) - case Left(err) => - err - } - .onErrorRecover { - case NonFatal(e) => - request.toError(ErrorCode.InternalError, e.getMessage) - } - } -} -object RequestService { - def request[A: Reads, B: Writes](name: String)( - f: A => Task[Either[Response.Error, B]] - ): NamedRequestService = - new RequestService[A, B](name) { - def handle(request: A): Task[Either[Response.Error, B]] = f(request) - } -} diff --git a/metaserver/src/main/scala/scala/meta/languageserver/protocol/LanguageClient.scala b/metaserver/src/main/scala/scala/meta/languageserver/protocol/LanguageClient.scala new file mode 100644 index 00000000000..bcc8218bf9a --- /dev/null +++ b/metaserver/src/main/scala/scala/meta/languageserver/protocol/LanguageClient.scala @@ -0,0 +1,104 @@ +package scala.meta.languageserver.protocol + +import scala.collection.concurrent.TrieMap +import scala.concurrent.duration.Duration +import scala.tools.nsc.interpreter.OutputStream +import com.typesafe.scalalogging.LazyLogging +import langserver.core.MessageWriter +import langserver.core.Notifications +import langserver.messages.ApplyWorkspaceEditParams +import langserver.messages.ApplyWorkspaceEditResponse +import langserver.messages.PublishDiagnostics +import langserver.messages.ShowMessageParams +import monix.eval.Callback +import monix.eval.Task +import monix.execution.Cancelable +import monix.execution.atomic.Atomic +import monix.execution.atomic.AtomicInt +import play.api.libs.json.JsError +import play.api.libs.json.JsSuccess +import play.api.libs.json.JsValue +import play.api.libs.json.Json +import play.api.libs.json.Reads +import play.api.libs.json.Writes + +class LanguageClient(out: OutputStream) extends LazyLogging with Notifications { + private val writer = new MessageWriter(out) + private val counter: AtomicInt = Atomic(1) + private val activeServerRequests = + TrieMap.empty[RequestId, Callback[Response]] + def notify[A: Writes](method: String, notification: A): Unit = + writer.write(Notification(method, Some(Json.toJson(notification)))) + def serverRespond(response: Response): Unit = response match { + case Response.Empty => () + case x: Response.Success => writer.write(x) + case x: Response.Error => writer.write(x) + } + def clientRespond(response: Response): Unit = + for { + id <- response match { + case Response.Empty => None + case Response.Success(_, requestId) => Some(requestId) + case Response.Error(_, requestId) => Some(requestId) + } + callback <- activeServerRequests.get(id).orElse { + logger.error(s"Response to unknown request: $response") + None + } + } { + activeServerRequests.remove(id) + callback.onSuccess(response) + } + + def request[A: Writes, B: Reads](method: String, request: A): Task[B] = { + val response = Task.create[Response] { (out, cb) => + val nextId = RequestId(counter.incrementAndGet()) + val scheduled = out.scheduleOnce(Duration(0, "s")) { + val json = Request(method, Some(Json.toJson(request)), nextId) + activeServerRequests.put(nextId, cb) + writer.write(json) + } + Cancelable { () => + scheduled.cancel() + this.notify("$/cancelRequest", CancelParams(nextId.value)) + } + } + def fail(json: JsValue): Nothing = + throw new IllegalArgumentException(Json.prettyPrint(json)) + response.map { + case Response.Empty => + throw new IllegalArgumentException( + s"Got empty response for request $method -> $request" + ) + case Response.Error(result, _) => + fail(Json.toJson(result)) + case Response.Success(result, _) => + result.validate[B] match { + case JsSuccess(value, _) => + value + case err: JsError => + fail(JsError.toJson(err)) + } + } + } + + override def showMessage(params: ShowMessageParams): Unit = { + notify("window/showMessage", params) + } + + override def publishDiagnostics( + publishDiagnostics: PublishDiagnostics + ): Unit = { + notify("textDocument/publishDiagnostics", publishDiagnostics) + } + def workspaceApplyEdit( + params: ApplyWorkspaceEditParams + ): Task[ApplyWorkspaceEditResponse] = + request[ApplyWorkspaceEditParams, ApplyWorkspaceEditResponse]( + "workspace/applyEdit", + params + ) + +} + + diff --git a/metaserver/src/main/scala/scala/meta/languageserver/protocol/LanguageServer.scala b/metaserver/src/main/scala/scala/meta/languageserver/protocol/LanguageServer.scala index 19e034188f4..1998a5a4c7c 100644 --- a/metaserver/src/main/scala/scala/meta/languageserver/protocol/LanguageServer.scala +++ b/metaserver/src/main/scala/scala/meta/languageserver/protocol/LanguageServer.scala @@ -19,14 +19,13 @@ import play.api.libs.json.Json final class LanguageServer( in: Observable[BaseProtocolMessage], - out: OutputStream, + client: LanguageClient, services: Services, requestScheduler: Scheduler ) extends LazyLogging { - private val writer = new MessageWriter(out) private val activeClientRequests: TrieMap[JsValue, Cancelable] = TrieMap.empty private val cancelNotification = - Service.notification[JsValue]("$/cancelNotification") { id => + Service.notification[JsValue]("$/cancelRequest") { id => activeClientRequests.get(id) match { case None => Task { @@ -35,6 +34,7 @@ final class LanguageServer( } case Some(request) => Task { + logger.info(s"Cancelling request $id") request.cancel() activeClientRequests.remove(id) Response.cancelled(id) @@ -45,6 +45,11 @@ final class LanguageServer( services.addService(cancelNotification).byMethodName def handleValidMessage(message: Message): Task[Response] = message match { + case response: Response => + Task { + client.clientRespond(response) + Response.empty + } case Notification(method, _) => handlersByMethodName.get(method) match { case None => @@ -62,7 +67,11 @@ final class LanguageServer( } case request @ Request(method, _, id) => handlersByMethodName.get(method) match { - case None => Task(Response.methodNotFound(method, id)) + case None => + Task { + logger.info(s"Method not found '$method'") + Response.methodNotFound(method, id) + } case Some(handler) => val response = handler.handle(request).onErrorRecover { case NonFatal(e) => @@ -89,11 +98,7 @@ final class LanguageServer( def startTask: Task[Unit] = in.foreachL { msg => handleMessage(msg) - .map { - case Response.Empty => () - case x: Response.Success => writer.write(x) - case x: Response.Error => writer.write(x) - } + .map(client.serverRespond) .onErrorRecover { case NonFatal(e) => logger.error("Unhandled error", e) diff --git a/metaserver/src/main/scala/scala/meta/languageserver/protocol/Message.scala b/metaserver/src/main/scala/scala/meta/languageserver/protocol/Message.scala index bea4dbbb3b2..3a998826464 100644 --- a/metaserver/src/main/scala/scala/meta/languageserver/protocol/Message.scala +++ b/metaserver/src/main/scala/scala/meta/languageserver/protocol/Message.scala @@ -2,19 +2,15 @@ package scala.meta.languageserver.protocol import play.api.libs.json._ -sealed trait Message { - def method: String -} +sealed trait Message object Message { - private case class IndeterminateMessage( - method: String, - params: Option[JsValue], - id: Option[RequestId] - ) implicit val reads: Reads[Message] = Reads { case json @ JsObject(map) => - if (map.contains("id")) json.validate[Request] - else json.validate[Notification] + if (map.contains("id")) { + if (map.contains("error")) json.validate[Response.Error] + else if (map.contains("result")) json.validate[Response.Success] + else json.validate[Request] + } else json.validate[Notification] case els => JsError(s"Expected object, obtained $els") } @@ -33,3 +29,46 @@ case class Notification(method: String, params: Option[JsValue]) extends Message object Notification { implicit val format: OFormat[Notification] = Json.format[Notification] } + +sealed trait Response extends Message { + def isSuccess: Boolean = this.isInstanceOf[Response.Success] +} +object Response { + case class Success(result: JsValue, id: RequestId) extends Response + object Success { + implicit val format: OFormat[Success] = Json.format[Success] + } + case class Error(error: ErrorObject, id: RequestId) extends Response + object Error { + implicit val format: OFormat[Error] = Json.format[Error] + } + case object Empty extends Response + def empty: Response = Empty + def ok(result: JsValue, id: RequestId): Response = + success(result, id) + def success(result: JsValue, id: RequestId): Response = + Success(result, id) + def error(error: ErrorObject, id: RequestId): Response.Error = + Error(error, id) + def internalError(message: String, id: RequestId): Response.Error = + Error(ErrorObject(ErrorCode.InternalError, message, None), id) + def invalidParams( + message: String, + id: RequestId = RequestId.Null + ): Response.Error = + Error(ErrorObject(ErrorCode.InvalidParams, message, None), id) + def invalidRequest(message: String): Response.Error = + Error( + ErrorObject(ErrorCode.InvalidRequest, message, None), + RequestId.Null + ) + def cancelled(id: JsValue): Response.Error = + Error( + ErrorObject(ErrorCode.RequestCancelled, "", None), + id.asOpt[RequestId].getOrElse(RequestId.Null) + ) + def parseError(message: String): Response.Error = + Error(ErrorObject(ErrorCode.ParseError, message, None), RequestId.Null) + def methodNotFound(message: String, id: RequestId): Response.Error = + Error(ErrorObject(ErrorCode.MethodNotFound, message, None), id) +} diff --git a/metaserver/src/main/scala/scala/meta/languageserver/protocol/RequestId.scala b/metaserver/src/main/scala/scala/meta/languageserver/protocol/RequestId.scala index 8fd49326fdd..3a8043e1d9a 100644 --- a/metaserver/src/main/scala/scala/meta/languageserver/protocol/RequestId.scala +++ b/metaserver/src/main/scala/scala/meta/languageserver/protocol/RequestId.scala @@ -4,6 +4,8 @@ import play.api.libs.json._ sealed trait RequestId object RequestId { + def apply(n: Int): RequestId.Number = + RequestId.Number(JsNumber(BigDecimal(n))) implicit val format: Format[RequestId] = Format[RequestId]( Reads { case num: JsNumber => JsSuccess(Number(num)) diff --git a/metaserver/src/main/scala/scala/meta/languageserver/protocol/Response.scala b/metaserver/src/main/scala/scala/meta/languageserver/protocol/Response.scala deleted file mode 100644 index d37cc1af18e..00000000000 --- a/metaserver/src/main/scala/scala/meta/languageserver/protocol/Response.scala +++ /dev/null @@ -1,41 +0,0 @@ -package scala.meta.languageserver.protocol - -import play.api.libs.json._ - -sealed trait Response -object Response { - case class Success(result: JsValue, id: RequestId) extends Response - object Success { - implicit val format: OFormat[Success] = Json.format[Success] - } - case class Error(error: ErrorObject, id: RequestId) extends Response - object Error { - implicit val format: OFormat[Error] = Json.format[Error] - } - case object Empty extends Response - def empty: Response = Empty - def ok(result: JsValue, id: RequestId): Response = - success(result, id) - def success(result: JsValue, id: RequestId): Response = - Success(result, id) - def error(error: ErrorObject, id: RequestId): Response.Error = - Error(error, id) - def internalError(message: String, id: RequestId): Response.Error = - Error(ErrorObject(ErrorCode.InternalError, message, None), id) - def invalidParams(message: String, id: RequestId): Response.Error = - Error(ErrorObject(ErrorCode.InvalidParams, message, None), id) - def invalidRequest(message: String): Response.Error = - Error( - ErrorObject(ErrorCode.InvalidRequest, message, None), - RequestId.Null - ) - def cancelled(id: JsValue): Response.Error = - Error( - ErrorObject(ErrorCode.RequestCancelled, "", None), - id.asOpt[RequestId].getOrElse(RequestId.Null) - ) - def parseError(message: String): Response.Error = - Error(ErrorObject(ErrorCode.ParseError, message, None), RequestId.Null) - def methodNotFound(message: String, id: RequestId): Response.Error = - Error(ErrorObject(ErrorCode.MethodNotFound, message, None), id) -} diff --git a/metaserver/src/main/scala/scala/meta/languageserver/protocol/Services.scala b/metaserver/src/main/scala/scala/meta/languageserver/protocol/Services.scala index cd3535ff461..40323c434da 100644 --- a/metaserver/src/main/scala/scala/meta/languageserver/protocol/Services.scala +++ b/metaserver/src/main/scala/scala/meta/languageserver/protocol/Services.scala @@ -29,7 +29,7 @@ object Service extends LazyLogging { case JsSuccess(value, _) => f.handle(value).map { case Right(response) => Response.ok(Json.toJson(response), id) - case Left(err) => err + case Left(err) => err.copy(id = id) } } case Request(invalidMethod, _, id) => @@ -58,9 +58,9 @@ object Service extends LazyLogging { } case Notification(invalidMethod, _) => fail(s"Expected method '$method', obtained '$invalidMethod'") - case request: Request => + case _ => fail( - s"Expected notification with no ID, obtained request with id $request" + s"Expected notification, obtained $message" ) } } diff --git a/metaserver/src/main/scala/scala/meta/languageserver/protocol/SimpleMain.scala b/metaserver/src/main/scala/scala/meta/languageserver/protocol/SimpleMain.scala deleted file mode 100644 index 6141515097a..00000000000 --- a/metaserver/src/main/scala/scala/meta/languageserver/protocol/SimpleMain.scala +++ /dev/null @@ -1,55 +0,0 @@ -package scala.meta.languageserver.protocol - -import java.io.FileOutputStream -import java.io.PrintStream -import java.nio.file.Files -import java.util.concurrent.Executors -import scala.meta.languageserver.ScalametaLanguageServer -import scala.util.Properties -import scala.util.control.NonFatal -import com.typesafe.scalalogging.LazyLogging -import monix.execution.Scheduler -import monix.execution.schedulers.SchedulerService -import org.langmeta.internal.io.PathIO - -object SimpleMain extends LazyLogging { - - def main(args: Array[String]): Unit = { - val cwd = PathIO.workingDirectory - val configDir = cwd.resolve(".metaserver").toNIO - val logFile = configDir.resolve("metaserver.log").toFile - Files.createDirectories(configDir) - val out = new PrintStream(new FileOutputStream(logFile)) - val err = new PrintStream(new FileOutputStream(logFile)) - val stdin = System.in - val stdout = System.out - val stderr = System.err - val s: SchedulerService = - Scheduler(Executors.newFixedThreadPool(4)) - try { - // route System.out somewhere else. Any output not from the server (e.g. logging) - // messes up with the client, since stdout is used for the language server protocol - System.setOut(out) - System.setErr(err) - logger.info(s"Starting server in $cwd") - logger.info(s"Classpath: ${Properties.javaClassPath}") - val metaserver = new ScalametaLanguageServer(cwd)(s) - val langserver = new LanguageServer( - BaseProtocolMessage.fromInputStream(stdin), - stdout, - metaserver.services, - s - ) - langserver.listen() - logger.warn("Stopped listening :(") - } catch { - case NonFatal(e) => - logger.error("Uncaught top-level error", e) - } finally { - System.setOut(stdout) - System.setErr(stderr) - } - System.exit(0) - } - -} diff --git a/metaserver/src/main/scala/scala/meta/languageserver/providers/DocumentFormattingProvider.scala b/metaserver/src/main/scala/scala/meta/languageserver/providers/DocumentFormattingProvider.scala index f580f9e12e3..6c8d88ee5a7 100644 --- a/metaserver/src/main/scala/scala/meta/languageserver/providers/DocumentFormattingProvider.scala +++ b/metaserver/src/main/scala/scala/meta/languageserver/providers/DocumentFormattingProvider.scala @@ -5,6 +5,8 @@ import scala.meta.languageserver.Configuration import scala.meta.languageserver.Configuration.Scalafmt import scala.meta.languageserver.Formatter import scala.meta.languageserver.MonixEnrichments._ +import scala.meta.languageserver.protocol.RequestId +import scala.meta.languageserver.protocol.Response import scala.util.control.NonFatal import com.typesafe.scalalogging.LazyLogging import langserver.core.Notifications @@ -66,7 +68,7 @@ class DocumentFormattingProvider( def format( input: Input.VirtualFile - ): Task[DocumentFormattingResult] = Task.eval { + ): Task[Either[Response.Error, List[TextEdit]]] = Task { val formatResult = for { scalafmt <- formatter() scalafmtConf <- config() @@ -79,14 +81,11 @@ class DocumentFormattingProvider( } } formatResult match { - case Left(err) => - // TODO(olafur) return invalid params when we refactor to lsp4s. - // We should not have to return a bogus empty result here. - notifications.showMessage(MessageType.Error, err) - DocumentFormattingResult(Nil) + case Left(errorMessage) => + Left(Response.invalidParams(errorMessage)) case Right(formatted) => val edits = List(TextEdit(fullDocumentRange, formatted)) - DocumentFormattingResult(edits) + Right(edits) } } diff --git a/metaserver/src/main/scala/scala/meta/languageserver/providers/DocumentHighlightProvider.scala b/metaserver/src/main/scala/scala/meta/languageserver/providers/DocumentHighlightProvider.scala index 2632237c249..c2e0d8b4b4e 100644 --- a/metaserver/src/main/scala/scala/meta/languageserver/providers/DocumentHighlightProvider.scala +++ b/metaserver/src/main/scala/scala/meta/languageserver/providers/DocumentHighlightProvider.scala @@ -23,8 +23,8 @@ object DocumentHighlightProvider extends LazyLogging { pos <- data.referencePositions(withDefinition = true) if pos.uri == uri.value _ = logger.debug(s"Found highlight at [${pos.range.get.pretty}]") + // TODO(alexey) add DocumentHighlightKind: Text (default), Read, Write } yield DocumentHighlight(pos.range.get.toRange) - // TODO(alexey) add DocumentHighlightKind: Text (default), Read, Write } } diff --git a/metaserver/src/main/scala/scala/meta/languageserver/providers/ReferencesProvider.scala b/metaserver/src/main/scala/scala/meta/languageserver/providers/ReferencesProvider.scala index 9d17e674dbd..d87b3b93611 100644 --- a/metaserver/src/main/scala/scala/meta/languageserver/providers/ReferencesProvider.scala +++ b/metaserver/src/main/scala/scala/meta/languageserver/providers/ReferencesProvider.scala @@ -6,6 +6,7 @@ import langserver.messages.ReferencesResult import scala.meta.languageserver.search.SymbolIndex import scala.meta.languageserver.ScalametaEnrichments._ import scala.meta.languageserver.Uri +import langserver.types.Location object ReferencesProvider extends LazyLogging { @@ -14,13 +15,12 @@ object ReferencesProvider extends LazyLogging { uri: Uri, position: l.Position, context: l.ReferenceContext - ): ReferencesResult = { - val locations = for { + ): List[Location] = { + for { data <- symbolIndex.findReferences(uri, position.line, position.character) pos <- data.referencePositions(context.includeDeclaration) _ = logger.info(s"Found reference ${pos.pretty} ${data.symbol}") } yield pos.toLocation - ReferencesResult(locations) } } diff --git a/metaserver/src/main/scala/scala/meta/languageserver/providers/RenameProvider.scala b/metaserver/src/main/scala/scala/meta/languageserver/providers/RenameProvider.scala index 37acac7f748..cf9da6ec3d8 100644 --- a/metaserver/src/main/scala/scala/meta/languageserver/providers/RenameProvider.scala +++ b/metaserver/src/main/scala/scala/meta/languageserver/providers/RenameProvider.scala @@ -7,18 +7,17 @@ import scala.meta.languageserver.refactoring.Backtick import scala.meta.languageserver.search.SymbolIndex import com.typesafe.scalalogging.LazyLogging import langserver.core.Notifications -import langserver.messages.RenameResult -import langserver.messages.TextDocumentRenameRequest +import langserver.messages.RenameParams import langserver.types.MessageType import langserver.types.TextEdit import langserver.types.WorkspaceEdit object RenameProvider extends LazyLogging { def rename( - request: TextDocumentRenameRequest, + params: RenameParams, symbolIndex: SymbolIndex, notifications: Notifications - ): RenameResult = Backtick.backtickWrap(request.params.newName) match { + ): WorkspaceEdit = Backtick.backtickWrap(params.newName) match { case Left(err) => // LSP specifies that a ResponseError should be returned in this case // but it seems when we do that at least vscode doesn't display the error @@ -26,14 +25,14 @@ object RenameProvider extends LazyLogging { // I prefer to use showMessage to explain what went wrong and perform // no text edit. notifications.showMessage(MessageType.Warning, err) - RenameResult(WorkspaceEdit(Map.empty)) + WorkspaceEdit(Map.empty) case Right(newName) => - val uri = Uri(request.params.textDocument.uri) + val uri = Uri(params.textDocument.uri) val edits = for { reference <- symbolIndex.findReferences( uri, - request.params.position.line, - request.params.position.character + params.position.line, + params.position.character ) symbol = Symbol(reference.symbol) if { @@ -55,6 +54,6 @@ object RenameProvider extends LazyLogging { // NOTE(olafur) uri.value is hardcoded here because local symbols // cannot be references across multiple files. When we add support for // renaming global symbols then this needs to change. - RenameResult(WorkspaceEdit(Map(uri.value -> edits))) + WorkspaceEdit(Map(uri.value -> edits)) } } diff --git a/metaserver/src/main/scala/scala/meta/languageserver/providers/SignatureHelpProvider.scala b/metaserver/src/main/scala/scala/meta/languageserver/providers/SignatureHelpProvider.scala index ba25ed55f1b..3f50d3e18af 100644 --- a/metaserver/src/main/scala/scala/meta/languageserver/providers/SignatureHelpProvider.scala +++ b/metaserver/src/main/scala/scala/meta/languageserver/providers/SignatureHelpProvider.scala @@ -7,13 +7,13 @@ import scala.meta.languageserver.compiler.CompilerEnrichments._ import scala.reflect.internal.util.Position import scala.tools.nsc.interactive.Global import com.typesafe.scalalogging.LazyLogging -import langserver.messages.SignatureHelpResult +import langserver.messages.SignatureHelp import langserver.types.ParameterInformation import langserver.types.SignatureInformation object SignatureHelpProvider extends LazyLogging { - def empty: SignatureHelpResult = SignatureHelpResult(Nil, None, None) - def signatureHelp(compiler: Global, cursor: Cursor): SignatureHelpResult = { + def empty: SignatureHelp = SignatureHelp(Nil, None, None) + def signatureHelp(compiler: Global, cursor: Cursor): SignatureHelp = { val unit = ScalacProvider.addCompilationUnit( global = compiler, code = cursor.contents, @@ -82,7 +82,7 @@ object SignatureHelpProvider extends LazyLogging { val activeArgument: Int = if (isVararg) signatureInformations.map(_.parameters.length - 1).max else callSite.activeArgument - SignatureHelpResult( + SignatureHelp( signatures = signatureInformations, // TODO(olafur) populate activeSignature and activeParameter fields, see // https://github.com/scalameta/language-server/issues/52 diff --git a/metaserver/src/test/scala/tests/compiler/SignatureHelpTest.scala b/metaserver/src/test/scala/tests/compiler/SignatureHelpTest.scala index a96a0d694f3..942d5e86dc6 100644 --- a/metaserver/src/test/scala/tests/compiler/SignatureHelpTest.scala +++ b/metaserver/src/test/scala/tests/compiler/SignatureHelpTest.scala @@ -1,7 +1,7 @@ package tests.compiler import scala.meta.languageserver.providers.SignatureHelpProvider -import langserver.messages.SignatureHelpResult +import langserver.messages.SignatureHelp import play.api.libs.json.Json object SignatureHelpTest extends CompilerSuite { @@ -9,7 +9,7 @@ object SignatureHelpTest extends CompilerSuite { def check( filename: String, code: String, - fn: SignatureHelpResult => Unit + fn: SignatureHelp => Unit ): Unit = { targeted( filename, diff --git a/metaserver/src/test/scala/tests/compiler/SquiggliesTest.scala b/metaserver/src/test/scala/tests/compiler/SquiggliesTest.scala index 278470221e5..554477f0fa8 100644 --- a/metaserver/src/test/scala/tests/compiler/SquiggliesTest.scala +++ b/metaserver/src/test/scala/tests/compiler/SquiggliesTest.scala @@ -21,15 +21,14 @@ object SquiggliesTest extends CompilerSuite { val logFile = tmp.resolve("metaserver.log").toFile val out = new PrintStream(new FileOutputStream(logFile)) val config = Observable.now(Configuration()) - val squiggliesProvider = - new SquiggliesProvider(config, AbsolutePath(tmp), out) + val squiggliesProvider = new SquiggliesProvider(config, AbsolutePath(tmp)) Files.write( tmp.resolve(".scalafix.conf"), """ |rules = [ NoInfer ] """.stripMargin.getBytes() ) - val linter: Linter = new Linter(AbsolutePath(tmp), System.out) + val linter: Linter = new Linter(AbsolutePath(tmp)) def check(name: String, original: String, expected: String): Unit = { test(name) { val input = Input.VirtualFile(name, original) diff --git a/metaserver/src/test/scala/tests/search/SymbolIndexTest.scala b/metaserver/src/test/scala/tests/search/SymbolIndexTest.scala index 1e48ac254da..3bdf80ff96e 100644 --- a/metaserver/src/test/scala/tests/search/SymbolIndexTest.scala +++ b/metaserver/src/test/scala/tests/search/SymbolIndexTest.scala @@ -1,19 +1,20 @@ package tests.search -import java.io.PipedInputStream import java.io.PipedOutputStream import java.nio.file.Files import java.nio.file.Paths import scala.meta.languageserver.ScalametaEnrichments._ -import scala.meta.languageserver.internal.BuildInfo import scala.meta.languageserver.ScalametaLanguageServer import scala.meta.languageserver.Uri +import scala.meta.languageserver.internal.BuildInfo +import scala.meta.languageserver.protocol.LanguageClient import scala.meta.languageserver.search.InMemorySymbolIndex import scala.meta.languageserver.search.InverseSymbolIndexer import scala.meta.languageserver.search.SymbolIndex import scala.{meta => m} -import langserver.{types => l} import langserver.messages.ClientCapabilities +import langserver.messages.InitializeParams +import langserver.{types => l} import monix.execution.schedulers.TestScheduler import org.langmeta.io.AbsolutePath import org.langmeta.io.Classpath @@ -51,15 +52,15 @@ object SymbolIndexTest extends MegaSuite { path.UserTest.toString() ) val s = TestScheduler() - val client = new PipedOutputStream() - val stdin = new PipedInputStream(client) val stdout = new PipedOutputStream() // TODO(olafur) run this as part of utest.runner.Framework.setup() - val server = - new ScalametaLanguageServer(cwd, stdin, stdout, System.out)(s) - server.initialize(0L, cwd.toString(), ClientCapabilities()).runAsync(s) + val client = new LanguageClient(stdout) + val metaserver = new ScalametaLanguageServer(cwd, client)(s) + metaserver + .initialize(InitializeParams(0L, cwd.toString(), ClientCapabilities())) + .runAsync(s) while (s.tickOne()) () // Trigger indexing - val index: SymbolIndex = server.symbolIndex + val index: SymbolIndex = metaserver.symbolIndex val reminderMsg = "Did you run scalametaEnableCompletions from sbt?" override val tests = Tests { @@ -206,7 +207,7 @@ object SymbolIndexTest extends MegaSuite { expected: String* )(implicit path: utest.framework.TestPath): Unit = { while (s.tickOne()) () - val result = server.symbolIndex.workspaceSymbols(path.value.last) + val result = metaserver.symbolIndex.workspaceSymbols(path.value.last) val obtained = result.toIterator.map(_.name).mkString("\n") assertNoDiff(obtained, expected.mkString("\n")) } @@ -254,9 +255,9 @@ object SymbolIndexTest extends MegaSuite { } "edit-distance" - { - val user = path.UserTestUri.toInput(server.buffers) + val user = path.UserTestUri.toInput(metaserver.buffers) val newUser = user.copy(value = "// leading comment\n" + user.value) - server.buffers.changed(newUser) + metaserver.buffers.changed(newUser) assertSymbolDefinition(path.UserReferenceLine + 1, 17)( "_root_.a.User.", "_root_.a.User#" @@ -266,8 +267,7 @@ object SymbolIndexTest extends MegaSuite { override def utestAfterAll(): Unit = { println("Shutting down server...") - server.shutdown() + metaserver.shutdown() while (s.tickOne()) () - stdin.close() } } diff --git a/vscode-extension/src/extension.ts b/vscode-extension/src/extension.ts index d2d15d2a398..2a8b294031f 100644 --- a/vscode-extension/src/extension.ts +++ b/vscode-extension/src/extension.ts @@ -1,7 +1,7 @@ -'use strict'; +"use strict"; -import * as path from 'path'; -import { workspace, ExtensionContext, window, commands } from 'vscode'; +import * as path from "path"; +import { workspace, ExtensionContext, window, commands } from "vscode"; import { LanguageClient, LanguageClientOptions, @@ -9,9 +9,9 @@ import { TransportKind, RevealOutputChannelOn, ExecuteCommandRequest -} from 'vscode-languageclient'; -import { Requirements } from './requirements'; -import { exec } from 'child_process'; +} from "vscode-languageclient"; +import { Requirements } from "./requirements"; +import { exec } from "child_process"; export async function activate(context: ExtensionContext) { const req = new Requirements(); @@ -19,47 +19,47 @@ export async function activate(context: ExtensionContext) { window.showErrorMessage(pathNotFound); }); - const toolsJar = javaHome + '/lib/tools.jar'; + const toolsJar = javaHome + "/lib/tools.jar"; // The debug options for the server const debugOptions = [ - '-Xdebug', - '-Xrunjdwp:transport=dt_socket,server=y,suspend=n,address=8000,quiet=y' + "-Xdebug", + "-Xrunjdwp:transport=dt_socket,server=y,suspend=n,address=8000,quiet=y" ]; - const coursierPath = path.join(context.extensionPath, './coursier'); + const coursierPath = path.join(context.extensionPath, "./coursier"); const coursierArgs = [ - 'launch', - '-r', - 'https://dl.bintray.com/dhpcs/maven', - '-r', - 'sonatype:releases', - '-J', + "launch", + "-r", + "https://dl.bintray.com/dhpcs/maven", + "-r", + "sonatype:releases", + "-J", toolsJar, - 'org.scalameta:metaserver_2.12:0.1-SNAPSHOT', - '-M', - 'scala.meta.languageserver.protocol.SimpleMain' + "org.scalameta:metaserver_2.12:0.1-SNAPSHOT", + "-M", + "scala.meta.languageserver.Main" ]; const javaArgs = [ `-XX:+UseG1GC`, `-XX:+UseStringDeduplication`, - '-jar', + "-jar", coursierPath ].concat(coursierArgs); const serverOptions: ServerOptions = { - run: { command: 'java', args: javaArgs }, - debug: { command: 'java', args: debugOptions.concat(javaArgs) } + run: { command: "java", args: javaArgs }, + debug: { command: "java", args: debugOptions.concat(javaArgs) } }; const clientOptions: LanguageClientOptions = { - documentSelector: ['scala'], + documentSelector: ["scala"], synchronize: { fileEvents: [ - workspace.createFileSystemWatcher('**/*.semanticdb'), - workspace.createFileSystemWatcher('**/*.compilerconfig') + workspace.createFileSystemWatcher("**/*.semanticdb"), + workspace.createFileSystemWatcher("**/*.compilerconfig") ], configurationSection: "scalameta" }, @@ -67,34 +67,50 @@ export async function activate(context: ExtensionContext) { }; const client = new LanguageClient( - 'scalameta', - 'Scalameta', + "scalameta", + "Scalameta", serverOptions, clientOptions ); - const restartServerCommand = commands.registerCommand("scalameta.restartServer", async () => { - const serverPid = client['_serverProcess'].pid; - await exec(`kill ${serverPid}`); - const showLogsAction = "Show server logs"; - const selectedAction = await window.showInformationMessage( - "Scalameta Language Server killed, it should restart in a few seconds", - showLogsAction - ); + const restartServerCommand = commands.registerCommand( + "scalameta.restartServer", + async () => { + const serverPid = client["_serverProcess"].pid; + await exec(`kill ${serverPid}`); + const showLogsAction = "Show server logs"; + const selectedAction = await window.showInformationMessage( + "Scalameta Language Server killed, it should restart in a few seconds", + showLogsAction + ); - if (selectedAction === showLogsAction) { - client.outputChannel.show(true); + if (selectedAction === showLogsAction) { + client.outputChannel.show(true); + } } - }); + ); client.onReady().then(() => { - const clearIndexCacheCommand = commands.registerCommand("scalameta.clearIndexCache", async () => { - return client.sendRequest(ExecuteCommandRequest.type, { command: "clearIndexCache" }); - }); - const resetPresentationCompiler = commands.registerCommand("scalameta.resetPresentationCompiler", async () => { - return client.sendRequest(ExecuteCommandRequest.type, { command: "resetPresentationCompiler" }); - }); - context.subscriptions.push(clearIndexCacheCommand, resetPresentationCompiler); + const clearIndexCacheCommand = commands.registerCommand( + "scalameta.clearIndexCache", + async () => { + return client.sendRequest(ExecuteCommandRequest.type, { + command: "clearIndexCache" + }); + } + ); + const resetPresentationCompiler = commands.registerCommand( + "scalameta.resetPresentationCompiler", + async () => { + return client.sendRequest(ExecuteCommandRequest.type, { + command: "resetPresentationCompiler" + }); + } + ); + context.subscriptions.push( + clearIndexCacheCommand, + resetPresentationCompiler + ); }); context.subscriptions.push(client.start(), restartServerCommand); From 0085b08cc3cc7b1f7e293b7000ad08c0c39cdfe0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=93lafur=20P=C3=A1ll=20Geirsson?= Date: Wed, 3 Jan 2018 22:49:05 +0100 Subject: [PATCH 11/18] Clean up languageserver project --- build.sbt | 108 +++++----- languageserver/bin/logback.groovy | 9 - .../scala/langserver/core/Connection.scala | 188 ----------------- .../langserver/core/LanguageServer.scala | 106 ---------- .../scala/langserver/core/MessageReader.scala | 158 -------------- .../scala/langserver/core/MessageWriter.scala | 5 +- .../scala/langserver/core/Notifications.scala | 16 +- .../scala/langserver/core/TextDocument.scala | 87 -------- .../langserver/core/TextDocumentManager.scala | 51 ----- .../scala/langserver/messages/Commands.scala | 198 +++++------------- .../main/scala/langserver/types/package.scala | 5 - .../main/scala/langserver/types/types.scala | 2 +- .../scala/langserver/utils/JsonRpcUtils.scala | 47 ----- .../langserver/core/MessageReaderSuite.scala | 95 --------- .../langserver/core/MessageWriterSuite.scala | 45 ---- .../langserver/core/TextDocumentSuite.scala | 62 ------ .../messages/CommandProtocolTest.scala | 23 -- .../scala/meta/languageserver/LSPLogger.scala | 10 +- .../scala/meta/languageserver/Linter.scala | 2 - .../languageserver/ScalacErrorReporter.scala | 1 - .../ScalametaLanguageServer.scala | 73 ++++--- .../protocol/LanguageClient.scala | 39 ++-- .../protocol/LanguageServer.scala | 2 - .../languageserver/protocol/Message.scala | 3 + .../DocumentFormattingProvider.scala | 3 - .../providers/DocumentHighlightProvider.scala | 2 - .../providers/DocumentSymbolProvider.scala | 1 - .../providers/ReferencesProvider.scala | 1 - .../providers/SquiggliesProvider.scala | 1 - .../refactoring/OrganizeImports.scala | 22 +- 30 files changed, 220 insertions(+), 1145 deletions(-) delete mode 100644 languageserver/bin/logback.groovy delete mode 100644 languageserver/src/main/scala/langserver/core/Connection.scala delete mode 100644 languageserver/src/main/scala/langserver/core/LanguageServer.scala delete mode 100644 languageserver/src/main/scala/langserver/core/MessageReader.scala delete mode 100644 languageserver/src/main/scala/langserver/core/TextDocument.scala delete mode 100644 languageserver/src/main/scala/langserver/core/TextDocumentManager.scala delete mode 100644 languageserver/src/main/scala/langserver/types/package.scala delete mode 100644 languageserver/src/main/scala/langserver/utils/JsonRpcUtils.scala delete mode 100644 languageserver/src/test/scala/langserver/core/MessageReaderSuite.scala delete mode 100644 languageserver/src/test/scala/langserver/core/MessageWriterSuite.scala delete mode 100644 languageserver/src/test/scala/langserver/core/TextDocumentSuite.scala delete mode 100644 languageserver/src/test/scala/langserver/messages/CommandProtocolTest.scala diff --git a/build.sbt b/build.sbt index 670d264df1c..4964d94bfef 100644 --- a/build.sbt +++ b/build.sbt @@ -1,62 +1,62 @@ inThisBuild( semanticdbSettings ++ - List( - version ~= { old => - if (sys.env.contains("CI")) old - else "0.1-SNAPSHOT" // to avoid manually updating extension.js - }, - scalaVersion := V.scala212, - scalacOptions ++= List( - "-deprecation", - "-Xlint" - ), - scalafixEnabled := false, - organization := "org.scalameta", - licenses := Seq( - "Apache-2.0" -> url("http://www.apache.org/licenses/LICENSE-2.0") - ), - homepage := Some(url("https://github.com/scalameta/language-server")), - developers := List( - Developer( - "laughedelic", - "Alexey Alekhin", - "laughedelic@gmail.com", - url("https://github.com/laughedelic") + List( + version ~= { old => + if (sys.env.contains("CI")) old + else "0.1-SNAPSHOT" // to avoid manually updating extension.js + }, + scalaVersion := V.scala212, + scalacOptions ++= List( + "-deprecation", + "-Xlint" ), - Developer( - "gabro", - "Gabriele Petronella", - "gabriele@buildo.io", - url("https://github.com/gabro") + scalafixEnabled := false, + organization := "org.scalameta", + licenses := Seq( + "Apache-2.0" -> url("http://www.apache.org/licenses/LICENSE-2.0") ), - Developer( - "olafurpg", - "Ólafur Páll Geirsson", - "olafurpg@gmail.com", - url("https://geirsson.com") + homepage := Some(url("https://github.com/scalameta/language-server")), + developers := List( + Developer( + "laughedelic", + "Alexey Alekhin", + "laughedelic@gmail.com", + url("https://github.com/laughedelic") + ), + Developer( + "gabro", + "Gabriele Petronella", + "gabriele@buildo.io", + url("https://github.com/gabro") + ), + Developer( + "olafurpg", + "Ólafur Páll Geirsson", + "olafurpg@gmail.com", + url("https://geirsson.com") + ), + Developer( + "ShaneDelmore", + "Shane Delmore", + "sdelmore@twitter.com", + url("http://delmore.io") + ) ), - Developer( - "ShaneDelmore", - "Shane Delmore", - "sdelmore@twitter.com", - url("http://delmore.io") - ) - ), - scmInfo in ThisBuild := Some( - ScmInfo( - url("https://github.com/scalameta/language-server"), - s"scm:git:git@github.com:scalameta/language-server.git" - ) - ), - releaseEarlyWith := BintrayPublisher, - releaseEarlyEnableSyncToMaven := false, - publishMavenStyle := true, - bintrayOrganization := Some("scalameta"), - bintrayReleaseOnPublish := dynverGitDescribeOutput.value.isVersionStable, - // faster publishLocal: - publishArtifact in packageDoc := sys.env.contains("CI"), - publishArtifact in packageSrc := sys.env.contains("CI") - ) + scmInfo in ThisBuild := Some( + ScmInfo( + url("https://github.com/scalameta/language-server"), + s"scm:git:git@github.com:scalameta/language-server.git" + ) + ), + releaseEarlyWith := BintrayPublisher, + releaseEarlyEnableSyncToMaven := false, + publishMavenStyle := true, + bintrayOrganization := Some("scalameta"), + bintrayReleaseOnPublish := dynverGitDescribeOutput.value.isVersionStable, + // faster publishLocal: + publishArtifact in packageDoc := sys.env.contains("CI"), + publishArtifact in packageSrc := sys.env.contains("CI") + ) ) lazy val V = new { diff --git a/languageserver/bin/logback.groovy b/languageserver/bin/logback.groovy deleted file mode 100644 index cc7dcf95c75..00000000000 --- a/languageserver/bin/logback.groovy +++ /dev/null @@ -1,9 +0,0 @@ -appender("FILE", FileAppender) { - file = "scala-langserver.log" - append = false - encoder(PatternLayoutEncoder) { - pattern = "%level %logger - %msg%n" - } -} - -root(DEBUG, ["FILE"]) \ No newline at end of file diff --git a/languageserver/src/main/scala/langserver/core/Connection.scala b/languageserver/src/main/scala/langserver/core/Connection.scala deleted file mode 100644 index 8987b61d2c6..00000000000 --- a/languageserver/src/main/scala/langserver/core/Connection.scala +++ /dev/null @@ -1,188 +0,0 @@ -package langserver.core - -import java.util.{Map => JMap} -import java.io.InputStream -import java.io.OutputStream -import java.util.concurrent.ConcurrentHashMap -import java.util.concurrent.atomic.AtomicInteger -import scala.collection.mutable.ListBuffer -import scala.concurrent.Future -import scala.util.Failure -import scala.util.Success -import scala.util.Try -import scala.util.control.NonFatal -import com.dhpcs.jsonrpc._ -import com.typesafe.scalalogging.LazyLogging -import langserver.messages._ -import langserver.types._ -import play.api.libs.json._ -import com.dhpcs.jsonrpc.JsonRpcMessage._ -import monix.eval.Task -import monix.execution.CancelableFuture -import monix.execution.Scheduler - -/** - * A connection that reads and writes Language Server Protocol messages. - * - * @param s thread pool to execute commands and notifications. - */ -abstract class Connection(inStream: InputStream, outStream: OutputStream)(implicit s: Scheduler) - extends LazyLogging { - private val msgReader = new MessageReader(inStream) - private val msgWriter = new MessageWriter(outStream) - private val activeRequestsById: JMap[Int, CancelableFuture[Unit]] = - new ConcurrentHashMap() - - def commandHandler(method: String, command: ServerCommand): Task[ResultResponse] - - val notificationHandlers: ListBuffer[Notification => Unit] = ListBuffer.empty - - def notifySubscribers(n: Notification): Unit = n match { - case CancelRequest(id) => cancelRequest(id) - case _ => - Task.sequence { - notificationHandlers.map(f => Task(f(n))) - }.onErrorRecover[Any] { - case NonFatal(e) => - logger.error("Failed notification handler", e) - }.runAsync - } - - def cancelAllActiveRequests(): Unit = { - activeRequestsById.values().forEach(_.cancel()) - } - private def cancelRequest(id: Int): Unit = { - Option(activeRequestsById.get(id)).foreach { future => - logger.info(s"Cancelling request $id") - future.cancel() - activeRequestsById.remove(id) - } - } - - private val i = new AtomicInteger() - // send request for workspace/applyEdit ignoring response - def workspaceApplyEdit(params: ApplyWorkspaceEditParams): Unit = { - val json = JsonRpcRequestMessage("workspace/applyEdit", Json.toJsObject(params), NumericCorrelationId(i.getAndIncrement())) - msgWriter.write(json) - } - - def sendNotification(params: Notification): Unit = { - val json = Notification.write(params) - msgWriter.write(json) - } - - /** - * A notification sent to the client to show a message. - * - * @param tpe One of MessageType values - * @param message The message to display in the client - */ - def showMessage(tpe: MessageType, message: String): Unit = { - sendNotification(ShowMessageParams(tpe, message)) - } - - /** - * The log message notification is sent from the server to the client to ask - * the client to log a particular message. - * - * @param tpe One of MessageType values - * @param message The message to display in the client - */ - def logMessage(tpe: MessageType, message: String): Unit = { - sendNotification(LogMessageParams(tpe, message)) - } - - /** - * Publish compilation errors for the given file. - */ - def publishDiagnostics(uri: String, diagnostics: Seq[Diagnostic]): Unit = { - sendNotification(PublishDiagnostics(uri, diagnostics)) - } - - def start() { - var streamClosed = false - do { - msgReader.nextPayload() match { - case None => streamClosed = true - - case Some(jsonString) => - readJsonRpcMessage(jsonString) match { - case Left(e) => - msgWriter.write(e) - - case Right(message) => message match { - case notification: JsonRpcNotificationMessage => - Option(Notification.read(notification)).fold( - logger.error(s"No notification type exists with method=${notification.method}") - )(_.fold( - errors => logger.error(s"Invalid Notification: $errors - Message: $message"), - notifySubscribers)) - - case request: JsonRpcRequestMessage => - unpackRequest(request) match { - case (_, Left(e)) => msgWriter.write(e) - case (None, Right(c)) => // this is disallowed by the language server specification - logger.error(s"Received request without 'id'. $c") - case (Some(id), Right(command)) => handleCommand(request.method, id, command) - } - - case response: JsonRpcResponseMessage => - // TODO(olafur) complete request, for example workspace/applyEdit - logger.debug(s"Received response: $response") - - case m => - logger.error(s"Received unknown message: $m") - } - case m => logger.error(s"Received unknown message: $m") - } - } - } while (!streamClosed) - } - - private def readJsonRpcMessage(jsonString: String): Either[JsonRpcResponseErrorMessage, JsonRpcMessage] = { - Try(Json.parse(jsonString)) match { - case Failure(exception) => - Left(JsonRpcResponseErrorMessage.parseError(exception,NoCorrelationId)) - - case Success(json) => - Json.fromJson[JsonRpcMessage](json).fold({ errors => - Left(JsonRpcResponseErrorMessage.invalidRequest(JsError(errors),NoCorrelationId)) - }, Right(_)) - } - } - - private def unpackRequest(request: JsonRpcRequestMessage): (Option[CorrelationId], Either[JsonRpcResponseErrorMessage, ServerCommand]) = { - Option(ServerCommand.read(request)) - .fold[(Option[CorrelationId], Either[JsonRpcResponseErrorMessage, ServerCommand])]( - Some(request.id) -> Left(JsonRpcResponseErrorMessage.methodNotFound(request.method,request.id )))( - commandJsResult => commandJsResult.fold(errors => - Some(request.id) -> Left(JsonRpcResponseErrorMessage.invalidParams(JsError(errors),request.id )), - command => Some(request.id) -> Right(command))) - - } - - private def handleCommand(method: String, id: CorrelationId, command: ServerCommand): Future[Unit] = { - val future = commandHandler(method, command).map { result => - val rJson = ResultResponse.write(result, id) - msgWriter.write(rJson) - }.onErrorRecover[Unit] { - case NonFatal(e) => - e match { - case InvalidParamsResponseError(message) => - msgWriter.write(JsonRpcResponseErrorMessage.invalidRequest(JsError(message), id)) - case _ => - logger.error(e.getMessage, e) - msgWriter.write(JsonRpcResponseErrorMessage.internalError(Some(JsString(e.getMessage)), id)) - } - }.runAsync - id match { - case NumericCorrelationId(value) => - activeRequestsById.put(value.toIntExact, future) - case _ => - } - future.onComplete { _ => - activeRequestsById.remove(id) - } - future - } -} diff --git a/languageserver/src/main/scala/langserver/core/LanguageServer.scala b/languageserver/src/main/scala/langserver/core/LanguageServer.scala deleted file mode 100644 index 7de7fa4a982..00000000000 --- a/languageserver/src/main/scala/langserver/core/LanguageServer.scala +++ /dev/null @@ -1,106 +0,0 @@ -package langserver.core - -import java.io.InputStream -import java.io.OutputStream -import com.typesafe.scalalogging.LazyLogging -import langserver.messages._ -import langserver.types._ -import monix.eval.Task -import monix.execution.Scheduler -import play.api.libs.json.JsValue - -/** - * A language server implementation. Users should subclass this class and implement specific behavior. - */ -class LanguageServer(inStream: InputStream, outStream: OutputStream)(implicit s: Scheduler) extends LazyLogging { - val connection: Connection = new Connection(inStream, outStream) { - override def commandHandler(method: String, command: ServerCommand): Task[ResultResponse] = (method, command) match { - case ("initialize", request: InitializeParams) => initialize(request) - case ("textDocument/completion", request: TextDocumentCompletionRequest) => completion(request) - case ("textDocument/codeAction", request: CodeActionRequest) => codeAction(request) - case ("textDocument/definition", request: TextDocumentDefinitionRequest) => definition(request) - case ("textDocument/documentHighlight", request: TextDocumentDocumentHighlightRequest) => documentHighlight(request) - case ("textDocument/documentSymbol", request: DocumentSymbolParams) => documentSymbol(request) - case ("textDocument/formatting", request: TextDocumentFormattingRequest) => formatting(request) - case ("textDocument/hover", request: TextDocumentHoverRequest) => hover(request) - case ("textDocument/references", request: TextDocumentReferencesRequest) => references(request) - case ("textDocument/rename", request: TextDocumentRenameRequest) => rename(request) - case ("textDocument/signatureHelp", request: TextDocumentSignatureHelpRequest) => signatureHelp(request) - case ("workspace/executeCommand", request: WorkspaceExecuteCommandRequest) => executeCommand(request).map(_ => ExecuteCommandResult) - case ("workspace/symbol", request: WorkspaceSymbolRequest) => workspaceSymbol(request) - case ("shutdown", _: Shutdown) => shutdown() - case c => Task.raiseError(new IllegalArgumentException(s"Unknown command $c")) - } - } - - connection.notificationHandlers += { - case Initialized() => logger.info("Client has initialized") - case Exit() => onExit() - case DidOpenTextDocumentParams(td) => onOpenTextDocument(td) - case DidChangeTextDocumentParams(td, changes) => onChangeTextDocument(td, changes) - case DidSaveTextDocumentParams(td) => onSaveTextDocument(td) - case DidCloseTextDocumentParams(td) => onCloseTextDocument(td) - case DidChangeWatchedFilesParams(changes) => onChangeWatchedFiles(changes) - case DidChangeConfigurationParams(settings) => onChangeConfiguration(settings) - case e => logger.error(s"Unknown notification $e") - } - - - // lifecycle - def start(): Unit = { - connection.start() - } - def onExit(): Unit = { - logger.debug("exit") - // TODO: should exit with success code 0 if the shutdown request has been received before; otherwise with error code 1 - sys.exit(0) - } - - def initialize(processId: Long, rootPath: String, capabilities: ClientCapabilities): Task[InitializeResult] = - initialize(InitializeParams(processId, rootPath, capabilities)) - def initialize(request: InitializeParams): Task[InitializeResult] = Task.now { - logger.debug(s"initialize with $request") - InitializeResult(ServerCapabilities()) - } - def shutdown(): Task[ShutdownResult] = - Task.now(ShutdownResult()) - - // textDocument - def completion(request: TextDocumentCompletionRequest): Task[CompletionList] = Task.now(CompletionList(isIncomplete = false, Nil)) - def codeAction(request: CodeActionRequest): Task[CodeActionResult] = Task.now(CodeActionResult(Nil)) - def definition(request: TextDocumentDefinitionRequest): Task[DefinitionResult] = Task.now(DefinitionResult(Nil)) - def documentHighlight(request: TextDocumentDocumentHighlightRequest): Task[DocumentHighlightResult] = Task.now(DocumentHighlightResult(Nil)) - def documentSymbol(request: DocumentSymbolParams): Task[DocumentSymbolResult] = Task.now(DocumentSymbolResult(Nil)) - def formatting(request: TextDocumentFormattingRequest): Task[DocumentFormattingResult] = Task.now(DocumentFormattingResult(Nil)) - def hover(request: TextDocumentHoverRequest): Task[Hover] = Task.now(Hover(Nil, None)) - def references(request: TextDocumentReferencesRequest): Task[ReferencesResult] = Task.now(ReferencesResult(Nil)) - def rename(request: TextDocumentRenameRequest): Task[RenameResult] = Task.now(RenameResult(WorkspaceEdit(Map.empty))) - def signatureHelp(request: TextDocumentSignatureHelpRequest): Task[SignatureHelp] = Task.now(SignatureHelp(Nil, None, None)) - - // workspace - def executeCommand(request: WorkspaceExecuteCommandRequest): Task[Unit] = Task.now(()) - def workspaceSymbol(request: WorkspaceSymbolRequest): Task[WorkspaceSymbolResult] = Task.now(WorkspaceSymbolResult(Nil)) - - def onOpenTextDocument(td: TextDocumentItem): Unit = { - logger.debug(s"openTextDocument $td") - } - - def onChangeTextDocument(td: VersionedTextDocumentIdentifier, changes: Seq[TextDocumentContentChangeEvent]): Unit = { - logger.debug(s"changeTextDocument $td") - } - - def onSaveTextDocument(td: TextDocumentIdentifier): Unit = { - logger.debug(s"saveTextDocument $td") - } - - def onCloseTextDocument(td: TextDocumentIdentifier): Unit = { - logger.debug(s"closeTextDocument $td") - } - - def onChangeWatchedFiles(changes: Seq[FileEvent]): Unit = - logger.debug(s"changeWatchedFiles $changes") - - def onChangeConfiguration(settings: JsValue): Unit = - logger.debug(s"changeConfiguration $settings") - -} diff --git a/languageserver/src/main/scala/langserver/core/MessageReader.scala b/languageserver/src/main/scala/langserver/core/MessageReader.scala deleted file mode 100644 index 300a58b50d4..00000000000 --- a/languageserver/src/main/scala/langserver/core/MessageReader.scala +++ /dev/null @@ -1,158 +0,0 @@ -package langserver.core - -import java.io.InputStream -import java.nio.charset.Charset - -import scala.collection.mutable.ArrayBuffer -import com.typesafe.scalalogging.LazyLogging - -/** - * A Language Server message Reader. It expects the following format: - * - *
'\r\n' - * - * Header := FieldName ':' FieldValue '\r\n' - * - * Currently there are two defined header fields: - * - 'Content-Length' in bytes (required) - * - 'Content-Type' (string), defaults to 'application/vscode-jsonrpc; charset=utf8' - * - * @note The header part is defined to be ASCII encoded, while the content part is UTF8. - */ -class MessageReader(in: InputStream) extends LazyLogging { - val BufferSize = 8192 - - private val buffer = new Array[Byte](BufferSize) - @volatile - private var data = ArrayBuffer.empty[Byte] - @volatile - private var streamClosed = false - - private val lock = new Object - - private class PumpInput extends Thread("Input Reader") { - override def run() { - var nRead = 0 - do { - nRead = in.read(buffer) - if (nRead > 0) lock.synchronized { - data ++= buffer.slice(0, nRead) - lock.notify() - } - } while (nRead > 0) - logger.info("End of stream, terminating thread") - lock.synchronized { - streamClosed = true - lock.notify() // some threads might be still waiting for input - } - } - } - - (new PumpInput).start() - - /** - * Return headers, if any are available. It returns only full headers, after the - * \r\n\r\n mark has been seen. - * - * @return A map of headers. If the map is empty it could be that the input stream - * was closed, or there were no headers before the delimiter. You can disambiguate - * by checking {{{this.streamClosed}}} - */ - private[core] final def getHeaders(): Map[String, String] = lock.synchronized { - val EmptyPair = "" -> "" - val EmptyMap = Map.empty[String, String] - def atDelimiter(idx: Int): Boolean = { - (data.size >= idx + 4 - && data(idx) == '\r' - && data(idx + 1) == '\n' - && data(idx + 2) == '\r' - && data(idx + 3) == '\n') - } - - while (data.size < 4 && !streamClosed) lock.wait() - - if (streamClosed) return EmptyMap - - var i = 0 - while (i + 4 < data.size && !atDelimiter(i)) { - i += 1 - } - - if (atDelimiter(i)) { - val headers = new String(data.slice(0, i).toArray, MessageReader.AsciiCharset) - - val pairs = headers.split("\r\n").filter(_.trim.length() > 0) map { line => - line.split(":") match { - case Array(key, value) => key.trim -> value.trim - case _ => - logger.error(s"Malformed input: $line") - EmptyPair - } - } - - // drop headers - data = data.drop(i + 4) - - // if there was a malformed header we keep trying to re-sync and read again - if (pairs.exists(_ == EmptyPair)) { - logger.error(s"There was an empty pair in $pairs, trying to read another header.") - getHeaders - } else pairs.toMap - } else if (streamClosed) { - EmptyMap - } else { - lock.wait() - getHeaders() - } - } - - /** - * Return `len` bytes of content as a string encoded in UTF8. - * - * @note If the stream was closed this method returns the empty string. - */ - private [core] def getContent(len: Int): String = lock.synchronized { - while (data.size < len && !streamClosed) lock.wait() - - if (streamClosed) "" - else { - assert(data.size >= len) - val content = data.take(len).toArray - data = data.drop(len) - new String(content, MessageReader.Utf8Charset) - } - } - - /** - * Return the next JSON RPC content payload. Blocks until enough data has been received. - */ - def nextPayload(): Option[String] = if (streamClosed) None else { - // blocks until headers are available - val headers = getHeaders() - - if (headers.isEmpty && streamClosed) - None - else { - val length = headers.get("Content-Length") match { - case Some(len) => try len.toInt catch { case e: NumberFormatException => -1 } - case _ => -1 - } - - if (length > 0) { - val content = getContent(length) - if (content.isEmpty() && streamClosed) None else { - logger.debug(s"<-- $content") - Some(content) - } - } else { - logger.error("Input must have Content-Length header with a numeric value.") - nextPayload() - } - } - } -} - -object MessageReader { - val AsciiCharset = Charset.forName("ASCII") - val Utf8Charset = Charset.forName("UTF-8") -} diff --git a/languageserver/src/main/scala/langserver/core/MessageWriter.scala b/languageserver/src/main/scala/langserver/core/MessageWriter.scala index 23f65df549c..bd4e6f58c08 100644 --- a/languageserver/src/main/scala/langserver/core/MessageWriter.scala +++ b/languageserver/src/main/scala/langserver/core/MessageWriter.scala @@ -1,6 +1,7 @@ package langserver.core import java.io.OutputStream +import java.nio.charset.StandardCharsets import play.api.libs.json._ import com.typesafe.scalalogging.LazyLogging @@ -32,14 +33,14 @@ class MessageWriter(out: OutputStream) extends LazyLogging { require(h.get(ContentLen).isEmpty) val str = Json.stringify(o.writes(msg)) - val contentBytes = str.getBytes(MessageReader.Utf8Charset) + val contentBytes = str.getBytes(StandardCharsets.UTF_8) val headers = (h + (ContentLen -> contentBytes.length)) .map { case (k, v) => s"$k: $v" } .mkString("", "\r\n", "\r\n\r\n") logger.debug(s" --> $str") - val headerBytes = headers.getBytes(MessageReader.AsciiCharset) + val headerBytes = headers.getBytes(StandardCharsets.US_ASCII) out.write(headerBytes) out.write(contentBytes) diff --git a/languageserver/src/main/scala/langserver/core/Notifications.scala b/languageserver/src/main/scala/langserver/core/Notifications.scala index e809f3c3237..9f6995d846e 100644 --- a/languageserver/src/main/scala/langserver/core/Notifications.scala +++ b/languageserver/src/main/scala/langserver/core/Notifications.scala @@ -1,5 +1,6 @@ package langserver.core +import langserver.messages.LogMessageParams import langserver.messages.PublishDiagnostics import langserver.messages.ShowMessageParams import langserver.types.Diagnostic @@ -13,6 +14,12 @@ trait Notifications { diagnostics: Seq[Diagnostic] ): Unit = publishDiagnostics(PublishDiagnostics(uri, diagnostics)) + def logMessage(params: LogMessageParams): Unit + final def logMessage( + `type`: MessageType, + message: String + ): Unit = logMessage(LogMessageParams(`type`, message)) + def showMessage(params: ShowMessageParams): Unit final def showMessage( tpe: MessageType, @@ -21,11 +28,8 @@ trait Notifications { } object Notifications { val empty: Notifications = new Notifications { - override def showMessage( - params: ShowMessageParams - ): Unit = () - override def publishDiagnostics( - publishDiagnostics: PublishDiagnostics - ): Unit = () + override def showMessage(params: ShowMessageParams): Unit = () + override def publishDiagnostics(params: PublishDiagnostics): Unit = () + override def logMessage(params: LogMessageParams): Unit = () } } diff --git a/languageserver/src/main/scala/langserver/core/TextDocument.scala b/languageserver/src/main/scala/langserver/core/TextDocument.scala deleted file mode 100644 index 766c2e18b9f..00000000000 --- a/languageserver/src/main/scala/langserver/core/TextDocument.scala +++ /dev/null @@ -1,87 +0,0 @@ -package langserver.core - -import langserver.types.TextDocumentContentChangeEvent -import langserver.types.Position -import java.io.File -import java.net.URI - -case class TextDocument(uri: String, contents: Array[Char]) { - def applyChanges(changes: Seq[TextDocumentContentChangeEvent]): TextDocument = { - // we assume full text sync - assert(changes.size == 1) - val change = changes.head - assert(change.range.isEmpty) - assert(change.rangeLength.isEmpty) - - copy(contents = change.text.toArray) - } - - private def peek(idx: Int): Int = - if (idx < contents.size) contents(idx) else -1 - - def toFile: File = - new File(URI.create(uri)) - - /** - * Return the corresponding position in this text document as 0-based line and column. - */ - def offsetToPosition(offset: Int): Position = { - if (offset >= contents.size) - throw new IndexOutOfBoundsException(s"$uri: asked position at offset $offset, but contents is only ${contents.size} characters long.") - - var i, line, col = 0 - - while (i < offset) { - contents(i) match { - case '\r' => - line += 1 - col = 0 - if (peek(i + 1) == '\n') i += 1 - - case '\n' => - line += 1 - col = 0 - - case _ => - col += 1 - } - i += 1 - } - - Position(line, col) - } - - /** - * Return the offset in the current document, for a given 0-based line/col position. - */ - def positionToOffset(pos: Position): Int = { - val Position(line, col) = pos - - var i, l = 0 - while (i < contents.size && l < line) { - contents(i) match { - case '\r' => - l += 1 - if (peek(i + 1) == '\n') i += 1 - - case '\n' => - l += 1 - - case _ => - } - i += 1 - } - - if (l < line) - throw new IllegalArgumentException(s"$uri: Can't find position $pos in contents of only $l lines long.") - if (i + col < contents.size) - i + col - else - throw new IllegalArgumentException(s"$uri: Invalid column. Position $pos in line '${contents.slice(i, contents.size).mkString}'") - } - - def lineToOffset(lineNr: Int): Int = { - positionToOffset(Position(lineNr, 0)) - } - -} diff --git a/languageserver/src/main/scala/langserver/core/TextDocumentManager.scala b/languageserver/src/main/scala/langserver/core/TextDocumentManager.scala deleted file mode 100644 index e18f9d98e47..00000000000 --- a/languageserver/src/main/scala/langserver/core/TextDocumentManager.scala +++ /dev/null @@ -1,51 +0,0 @@ -package langserver.core - -import langserver.messages._ -import langserver.types._ -import java.util.concurrent.ConcurrentMap -import java.util.concurrent.ConcurrentHashMap -import com.typesafe.scalalogging.LazyLogging - -import scala.collection.JavaConverters._ - - -/** - * A class to manage text documents coming over the wire from a Language Server client. - * - * The manager keeps an up to date version of each document that is currently open by the client. - */ -class TextDocumentManager(connection: Connection) extends LazyLogging { - connection.notificationHandlers += { - case DidOpenTextDocumentParams(td) => onOpenTextDocument(td) - case DidChangeTextDocumentParams(td, changes) => onChangeTextDocument(td, changes) - case DidCloseTextDocumentParams(td) => onCloseTextDocument(td) - case e => () - } - - private val docs: ConcurrentMap[String, TextDocument] = new ConcurrentHashMap - - def documentForUri(uri: String): Option[TextDocument] = - Option(docs.get(uri)) - - def allOpenDocuments: Seq[TextDocument] = docs.values.asScala.toSeq - - def onOpenTextDocument(td: TextDocumentItem) = { - docs.put(td.uri, new TextDocument(td.uri, td.text.toCharArray)) - } - - def onChangeTextDocument(td: VersionedTextDocumentIdentifier, changes: Seq[TextDocumentContentChangeEvent]) = { - docs.get(td.uri) match { - case null => - logger.error(s"Document ${td.uri} not found in this manager. Adding now") - // we assume full text sync - docs.put(td.uri, new TextDocument(td.uri, changes.head.text.toCharArray)) - case doc => - docs.put(td.uri, doc.applyChanges(changes)) - } - } - - def onCloseTextDocument(td: TextDocumentIdentifier) = { - docs.remove(td.uri) - } - -} diff --git a/languageserver/src/main/scala/langserver/messages/Commands.scala b/languageserver/src/main/scala/langserver/messages/Commands.scala index d7184827acf..1aa17127b28 100644 --- a/languageserver/src/main/scala/langserver/messages/Commands.scala +++ b/languageserver/src/main/scala/langserver/messages/Commands.scala @@ -1,19 +1,9 @@ package langserver.messages -import com.dhpcs.jsonrpc._ -import play.api.libs.json._ import langserver.types._ -import langserver.utils.JsonRpcUtils import play.api.libs.json.OFormat +import play.api.libs.json._ -sealed trait Message -sealed trait ServerCommand extends Message -sealed trait ClientCommand extends Message - -sealed trait Response extends Message -sealed trait ResultResponse extends Response - -sealed trait Notification extends Message /** * Parameters and types used in the `initialize` message. @@ -34,7 +24,7 @@ case class InitializeParams( /** * The capabilities provided by the client (editor) */ - capabilities: ClientCapabilities) extends ServerCommand + capabilities: ClientCapabilities) object InitializeParams { implicit val format: OFormat[InitializeParams] = Json.format[InitializeParams] } @@ -145,19 +135,19 @@ object ExecuteCommandOptions { implicit val format: Format[ExecuteCommandOptions] = Json.format[ExecuteCommandOptions] } -case class CompletionList(isIncomplete: Boolean, items: Seq[CompletionItem]) extends ResultResponse +case class CompletionList(isIncomplete: Boolean, items: Seq[CompletionItem]) object CompletionList { implicit val format: OFormat[CompletionList] = Json.format[CompletionList] } -case class InitializeResult(capabilities: ServerCapabilities) extends ResultResponse +case class InitializeResult(capabilities: ServerCapabilities) object InitializeResult { implicit val format: OFormat[InitializeResult] = Json.format[InitializeResult] } -case class Shutdown() extends ServerCommand +case class Shutdown() -case class ShutdownResult() extends ResultResponse +case class ShutdownResult() object ShutdownResult { implicit val format: Format[ShutdownResult] = OFormat( Reads(jsValue => JsSuccess(ShutdownResult())), @@ -177,7 +167,7 @@ case class ShowMessageRequestParams( `type`: MessageType, message: String, actions: Seq[MessageActionItem] -) extends ClientCommand +) /** * A short title like 'Retry', 'Open Log' etc. @@ -192,7 +182,7 @@ case class TextDocumentPositionParams( position: Position ) object TextDocumentPositionParams { - implicit val format = Json.format[TextDocumentPositionParams] + implicit val format: OFormat[TextDocumentPositionParams] = Json.format[TextDocumentPositionParams] } case class ReferenceParams( textDocument: TextDocumentIdentifier, @@ -200,7 +190,7 @@ case class ReferenceParams( context: ReferenceContext ) object ReferenceParams { - implicit val format = Json.format[ReferenceParams] + implicit val format: OFormat[ReferenceParams] = Json.format[ReferenceParams] } case class RenameParams( @@ -221,124 +211,92 @@ object CodeActionParams { implicit val format: OFormat[CodeActionParams] = Json.format[CodeActionParams] } -case class CodeActionRequest(params: CodeActionParams) extends ServerCommand +case class CodeActionRequest(params: CodeActionParams) object CodeActionRequest { implicit val format: OFormat[CodeActionRequest] = Json.format[CodeActionRequest] } -case class DocumentSymbolParams(textDocument: TextDocumentIdentifier) extends ServerCommand +case class DocumentSymbolParams(textDocument: TextDocumentIdentifier) object DocumentSymbolParams { - implicit val format = Json.format[DocumentSymbolParams] + implicit val format: OFormat[DocumentSymbolParams] = Json.format[DocumentSymbolParams] } -case class TextDocumentRenameRequest(params: RenameParams) extends ServerCommand +case class TextDocumentRenameRequest(params: RenameParams) object TextDocumentRenameRequest { implicit val format: OFormat[TextDocumentRenameRequest] = Json.format[TextDocumentRenameRequest] } -case class TextDocumentSignatureHelpRequest(params: TextDocumentPositionParams) extends ServerCommand -case class TextDocumentCompletionRequest(params: TextDocumentPositionParams) extends ServerCommand -case class TextDocumentDefinitionRequest(params: TextDocumentPositionParams) extends ServerCommand -case class TextDocumentReferencesRequest(params: ReferenceParams) extends ServerCommand -case class TextDocumentDocumentHighlightRequest(params: TextDocumentPositionParams) extends ServerCommand -case class TextDocumentHoverRequest(params: TextDocumentPositionParams) extends ServerCommand -case class TextDocumentFormattingRequest(params: DocumentFormattingParams) extends ServerCommand -case class WorkspaceExecuteCommandRequest(params: ExecuteCommandParams) extends ServerCommand -case class WorkspaceSymbolRequest(params: WorkspaceSymbolParams) extends ServerCommand +case class TextDocumentSignatureHelpRequest(params: TextDocumentPositionParams) +case class TextDocumentCompletionRequest(params: TextDocumentPositionParams) +case class TextDocumentDefinitionRequest(params: TextDocumentPositionParams) +case class TextDocumentReferencesRequest(params: ReferenceParams) +case class TextDocumentDocumentHighlightRequest(params: TextDocumentPositionParams) +case class TextDocumentHoverRequest(params: TextDocumentPositionParams) +case class TextDocumentFormattingRequest(params: DocumentFormattingParams) +case class WorkspaceExecuteCommandRequest(params: ExecuteCommandParams) +case class WorkspaceSymbolRequest(params: WorkspaceSymbolParams) case class ApplyWorkspaceEditResponse(applied: Boolean) object ApplyWorkspaceEditResponse { - implicit val format = Json.format[ApplyWorkspaceEditResponse] + implicit val format: OFormat[ApplyWorkspaceEditResponse] = Json.format[ApplyWorkspaceEditResponse] } case class ApplyWorkspaceEditParams(label: Option[String], edit: WorkspaceEdit) object ApplyWorkspaceEditParams { implicit val format: OFormat[ApplyWorkspaceEditParams] = Json.format[ApplyWorkspaceEditParams] } -case class Hover(contents: Seq[MarkedString], range: Option[Range]) extends ResultResponse +case class Hover(contents: Seq[MarkedString], range: Option[Range]) object Hover { implicit val format: OFormat[Hover] = Json.format[Hover] } -object ServerCommand extends CommandCompanion[ServerCommand] { - import JsonRpcUtils._ - - implicit val positionParamsFormat: OFormat[TextDocumentPositionParams] = Json.format[TextDocumentPositionParams] - implicit val referenceParamsFormat: OFormat[ReferenceParams] = Json.format[ReferenceParams] - - override val CommandFormats = Message.MessageFormats( - "initialize" -> Json.format[InitializeParams], - "textDocument/completion" -> valueFormat(TextDocumentCompletionRequest)(_.params), - "textDocument/codeAction" -> valueFormat(CodeActionRequest.apply)(_.params), - "textDocument/rename" -> valueFormat(TextDocumentRenameRequest.apply)(_.params), - "textDocument/signatureHelp" -> valueFormat(TextDocumentSignatureHelpRequest)(_.params), - "textDocument/definition" -> valueFormat(TextDocumentDefinitionRequest)(_.params), - "textDocument/references" -> valueFormat(TextDocumentReferencesRequest)(_.params), - "textDocument/documentHighlight" -> valueFormat(TextDocumentDocumentHighlightRequest)(_.params), - "textDocument/hover" -> valueFormat(TextDocumentHoverRequest)(_.params), - "textDocument/documentSymbol" -> Json.format[DocumentSymbolParams], - "textDocument/formatting" -> valueFormat(TextDocumentFormattingRequest)(_.params), - "workspace/executeCommand" -> valueFormat(WorkspaceExecuteCommandRequest)(_.params), - "workspace/symbol" -> valueFormat(WorkspaceSymbolRequest)(_.params), - ) - - // NOTE: this is a workaround to read `shutdown` request which doesn't have parameters (scala-json-rpc requires parameters for all requests) - override def read(jsonRpcRequestMessage: JsonRpcRequestMessage): JsResult[_ <: ServerCommand] = { - jsonRpcRequestMessage.method match { - case "shutdown" => JsSuccess(Shutdown()) - case _ => super.read(jsonRpcRequestMessage) - } - } -} - -object ClientCommand extends CommandCompanion[ClientCommand] { - override val CommandFormats = Message.MessageFormats( - "window/showMessageRequest" -> Json.format[ShowMessageRequestParams]) -} ///////////////////////////// Notifications /////////////////////////////// // From server to client -case class ShowMessageParams(`type`: MessageType, message: String) extends Notification +case class ShowMessageParams(`type`: MessageType, message: String) object ShowMessageParams { - implicit val format = Json.format[ShowMessageParams] + implicit val format: OFormat[ShowMessageParams] = Json.format[ShowMessageParams] } -case class LogMessageParams(`type`: MessageType, message: String) extends Notification -case class PublishDiagnostics(uri: String, diagnostics: Seq[Diagnostic]) extends Notification +case class LogMessageParams(`type`: MessageType, message: String) +object LogMessageParams { + implicit val format: OFormat[LogMessageParams] = Json.format[LogMessageParams] +} +case class PublishDiagnostics(uri: String, diagnostics: Seq[Diagnostic]) object PublishDiagnostics { implicit val format: OFormat[PublishDiagnostics] = Json.format[PublishDiagnostics] } // from client to server -case class Exit() extends Notification +case class Exit() -case class DidOpenTextDocumentParams(textDocument: TextDocumentItem) extends Notification +case class DidOpenTextDocumentParams(textDocument: TextDocumentItem) object DidOpenTextDocumentParams { - implicit val format = Json.format[DidOpenTextDocumentParams] + implicit val format: OFormat[DidOpenTextDocumentParams] = Json.format[DidOpenTextDocumentParams] } case class DidChangeTextDocumentParams( textDocument: VersionedTextDocumentIdentifier, - contentChanges: Seq[TextDocumentContentChangeEvent]) extends Notification + contentChanges: Seq[TextDocumentContentChangeEvent]) object DidChangeTextDocumentParams { implicit val format: OFormat[DidChangeTextDocumentParams] = Json.format[DidChangeTextDocumentParams] } -case class DidCloseTextDocumentParams(textDocument: TextDocumentIdentifier) extends Notification +case class DidCloseTextDocumentParams(textDocument: TextDocumentIdentifier) object DidCloseTextDocumentParams { - implicit val format = Json.format[DidCloseTextDocumentParams] + implicit val format: OFormat[DidCloseTextDocumentParams] = Json.format[DidCloseTextDocumentParams] } -case class DidSaveTextDocumentParams(textDocument: TextDocumentIdentifier) extends Notification +case class DidSaveTextDocumentParams(textDocument: TextDocumentIdentifier) object DidSaveTextDocumentParams { - implicit val format = Json.format[DidSaveTextDocumentParams] + implicit val format: OFormat[DidSaveTextDocumentParams] = Json.format[DidSaveTextDocumentParams] } -case class DidChangeWatchedFilesParams(changes: Seq[FileEvent]) extends Notification +case class DidChangeWatchedFilesParams(changes: Seq[FileEvent]) object DidChangeWatchedFilesParams { - implicit val format = Json.format[DidChangeWatchedFilesParams] + implicit val format: OFormat[DidChangeWatchedFilesParams] = Json.format[DidChangeWatchedFilesParams] } -case class DidChangeConfigurationParams(settings: JsValue) extends Notification +case class DidChangeConfigurationParams(settings: JsValue) object DidChangeConfigurationParams { - implicit val format = Json.format[DidChangeConfigurationParams] + implicit val format: OFormat[DidChangeConfigurationParams] = Json.format[DidChangeConfigurationParams] } -case class Initialized() extends Notification +case class Initialized() object Initialized { implicit val format: Format[Initialized] = OFormat( Reads(jsValue => JsSuccess(Initialized())), @@ -346,78 +304,34 @@ object Initialized { } -case class CancelRequest(id: Int) extends Notification - -object Notification extends NotificationCompanion[Notification] { - override val NotificationFormats = Message.MessageFormats( - "window/showMessage" -> Json.format[ShowMessageParams], - "window/logMessage" -> Json.format[LogMessageParams], - "textDocument/publishDiagnostics" -> Json.format[PublishDiagnostics], - "textDocument/didOpen" -> Json.format[DidOpenTextDocumentParams], - "textDocument/didChange" -> Json.format[DidChangeTextDocumentParams], - "textDocument/didClose" -> Json.format[DidCloseTextDocumentParams], - "textDocument/didSave" -> Json.format[DidSaveTextDocumentParams], - "workspace/didChangeWatchedFiles" -> Json.format[DidChangeWatchedFilesParams], - "workspace/didChangeConfiguration" -> Json.format[DidChangeConfigurationParams], - "initialized" -> Initialized.format, - "$/cancelRequest" -> Json.format[CancelRequest] - ) +case class CancelRequest(id: Int) - // NOTE: this is a workaround to read `exit` notification which doesn't have parameters (scala-json-rpc requires parameters for all notifications) - override def read(jsonRpcNotificationMessage: JsonRpcNotificationMessage): JsResult[_ <: Notification] = { - jsonRpcNotificationMessage.method match { - case "exit" => JsSuccess(Exit()) - case _ => super.read(jsonRpcNotificationMessage) - } - } -} - -case class RenameResult(params: WorkspaceEdit) extends ResultResponse +case class RenameResult(params: WorkspaceEdit) object RenameResult { implicit val format: OFormat[RenameResult] = Json.format[RenameResult] } -case class CodeActionResult(params: Seq[Command]) extends ResultResponse +case class CodeActionResult(params: Seq[Command]) object CodeActionResult { implicit val format: OFormat[CodeActionResult] = Json.format[CodeActionResult] } -case class DocumentSymbolResult(params: Seq[SymbolInformation]) extends ResultResponse -case class DefinitionResult(params: Seq[Location]) extends ResultResponse -case class ReferencesResult(params: Seq[Location]) extends ResultResponse -case class DocumentHighlightResult(params: Seq[Location]) extends ResultResponse -case class DocumentFormattingResult(params: Seq[TextEdit]) extends ResultResponse +case class DocumentSymbolResult(params: Seq[SymbolInformation]) +case class DefinitionResult(params: Seq[Location]) +case class ReferencesResult(params: Seq[Location]) +case class DocumentHighlightResult(params: Seq[Location]) +case class DocumentFormattingResult(params: Seq[TextEdit]) case class SignatureHelp(signatures: Seq[SignatureInformation], activeSignature: Option[Int], - activeParameter: Option[Int]) extends ResultResponse + activeParameter: Option[Int]) object SignatureHelp { implicit val format: OFormat[SignatureHelp] = Json.format[SignatureHelp] } -case object ExecuteCommandResult extends ResultResponse -case class WorkspaceSymbolResult(params: Seq[SymbolInformation]) extends ResultResponse +case object ExecuteCommandResult +case class WorkspaceSymbolResult(params: Seq[SymbolInformation]) object WorkspaceSymbolResult { implicit val format: OFormat[WorkspaceSymbolResult] = Json.format[WorkspaceSymbolResult] } -object ResultResponse extends ResponseCompanion[Any] { - import JsonRpcUtils._ - - override val ResponseFormats = Message.MessageFormats( - "initialize" -> Json.format[InitializeResult], - "textDocument/completion" -> Json.format[CompletionList], - "textDocument/codeAction" -> valueFormat(CodeActionResult.apply)(_.params), - "textDocument/rename" -> valueFormat(RenameResult.apply)(_.params), - "textDocument/signatureHelp" -> Json.format[SignatureHelp], - "textDocument/definition" -> valueFormat(DefinitionResult)(_.params), - "textDocument/references" -> valueFormat(ReferencesResult)(_.params), - "textDocument/documentHighlight" -> valueFormat(DocumentHighlightResult)(_.params), - "textDocument/hover" -> Json.format[Hover], - "textDocument/documentSymbol" -> valueFormat(DocumentSymbolResult)(_.params), - "textDocument/formatting" -> valueFormat(DocumentFormattingResult)(_.params), - "workspace/executeCommand" -> emptyFormat[ExecuteCommandResult.type], - "workspace/symbol" -> valueFormat(WorkspaceSymbolResult.apply)(_.params), - "shutdown" -> ShutdownResult.format - ) -} // Errors -case class InvalidParamsResponseError(message: String) extends Exception(message) with Response +case class InvalidParamsResponseError(message: String) extends Exception(message) diff --git a/languageserver/src/main/scala/langserver/types/package.scala b/languageserver/src/main/scala/langserver/types/package.scala deleted file mode 100644 index 45d27e69f23..00000000000 --- a/languageserver/src/main/scala/langserver/types/package.scala +++ /dev/null @@ -1,5 +0,0 @@ -package langserver - -package object types { - type Definition = Location -} \ No newline at end of file diff --git a/languageserver/src/main/scala/langserver/types/types.scala b/languageserver/src/main/scala/langserver/types/types.scala index 43b42dc041c..cfc5b9c0218 100644 --- a/languageserver/src/main/scala/langserver/types/types.scala +++ b/languageserver/src/main/scala/langserver/types/types.scala @@ -169,7 +169,7 @@ case class DocumentHighlight( /** The highlight kind, default is [text](#DocumentHighlightKind.Text). */ kind: DocumentHighlightKind = DocumentHighlightKind.Text) object DocumentHighlight { - implicit val format = Json.format[DocumentHighlight] + implicit val format: OFormat[DocumentHighlight] = Json.format[DocumentHighlight] } case class SymbolInformation( diff --git a/languageserver/src/main/scala/langserver/utils/JsonRpcUtils.scala b/languageserver/src/main/scala/langserver/utils/JsonRpcUtils.scala deleted file mode 100644 index 07c12faf5af..00000000000 --- a/languageserver/src/main/scala/langserver/utils/JsonRpcUtils.scala +++ /dev/null @@ -1,47 +0,0 @@ -package langserver.utils - -import play.api.libs.json._ - -/** - * Utility for messages that use the same parameter type in different requests. - * - * The JsonRpc library expects non-overlapping types in a command server object, but - * the language server re-uses such parameters in different requests. - * - * @see https://github.com/dhpcs/play-json-rpc/issues/2 - */ -object JsonRpcUtils { - - /** - * Create a format that reads and writes as a different type. Most useful for - * providing different types in the JsonRpc library, even though the serialized form - * is the same. - * - * Since the library expects non-overlapping types in the MessageFormat structure, you - * can work around that using this little helper function - * - * {{{ - * case class TextDocumentCompletionCommand(positionParams: TextDocumentPositionParams) extends ServerCommand - * case class TextDocumentDefinitionCommand(positionParams: TextDocumentPositionParams) extends ServerCommand - * - * object ServerCommand extends CommandCompanion[ServerCommand] { - * override val CommandFormats = { - * implicit val positionParamsFormat = Json.format[TextDocumentPositionParams] - * Message.MessageFormats( - * .. - * "textDocument/completion" -> valueFormat(TextDocumentCompletionCommand)(_.positionParams), - * "textDocument/definition" -> valueFormat(TextDocumentDefinitionCommand)(_.positionParams), - * ) - * }}} - */ - def valueFormat[A, B: Format](apply: B => A)(unapply: A => B): Format[A] = new Format[A] { - override def reads(json: JsValue) = Reads.of[B].reads(json).map(apply(_)) - override def writes(o: A) = Writes.of[B].writes(unapply(o)) - } - - def emptyFormat[A]: Format[A] = new Format[A] { - override def reads(json: JsValue) = ??? - override def writes(u: A) = JsObject.empty - } - -} diff --git a/languageserver/src/test/scala/langserver/core/MessageReaderSuite.scala b/languageserver/src/test/scala/langserver/core/MessageReaderSuite.scala deleted file mode 100644 index 42a07e0e278..00000000000 --- a/languageserver/src/test/scala/langserver/core/MessageReaderSuite.scala +++ /dev/null @@ -1,95 +0,0 @@ -package langserver.core - -import java.io.PipedInputStream -import java.io.PipedOutputStream - -import org.scalatest.FunSuite -import java.io.PrintWriter -import org.scalatest.OptionValues -import org.scalatest.BeforeAndAfter - -class MessageReaderSuite extends FunSuite - with BeforeAndAfter - with OptionValues { - - var inStream: PipedInputStream = _ - var out: PrintWriter = _ - - before { - inStream = new PipedInputStream - out = new PrintWriter(new PipedOutputStream(inStream)) - } - - after { - out.close() - inStream.close() - } - - - test("full headers are supported") { - val msgReader = new MessageReader(inStream) - - write(""" -Content-Length: 80 -Content-Type: application/vscode-jsonrpc; charset=utf8 - -""") - - val headers = msgReader.getHeaders() - - assert(headers("Content-Length") == "80") - assert(headers("Content-Type") == "application/vscode-jsonrpc; charset=utf8") - } - - test("partial headers are correctly concatenated") { - val msgReader = new MessageReader(inStream) - - write("""Content-Length: 80 -Content""") - write("""-Type: application/vscode-jsonrpc; charset=utf8 - -""") - val headers = msgReader.getHeaders() - - assert(headers("Content-Length") == "80") - assert(headers("Content-Type") == "application/vscode-jsonrpc; charset=utf8") - } - - test("multi-chunk header") { - val msgReader = new MessageReader(inStream) - - write("""Content-""") - write("""Length: 80""") - Thread.sleep(100) - write(""" -Content-Type: application/vscode-jsonrpc; charset=utf8 - -""") - val headers = msgReader.getHeaders() - - assert(headers("Content-Length") == "80") - assert(headers("Content-Type") == "application/vscode-jsonrpc; charset=utf8") - } - - test("payload arrives") { - val msgReader = new MessageReader(inStream) - write("""Content-Length: 43 - -{"jsonrpc":"2.0","id":1,"method":"example"}""") - - val payload = msgReader.nextPayload() - assert(payload.value === """{"jsonrpc":"2.0","id":1,"method":"example"}""") - } - - test("chunked payload arrives") { - val msgReader = new MessageReader(inStream) - - val payload = msgReader.nextPayload() - assert(payload.value === """{"jsonrpc":"2.0","id":1,"method":"example"}""") - } - - private def write(msg: String): Unit = { - out.print(msg.replaceAll("\n", "\r\n")) - out.flush() - } -} diff --git a/languageserver/src/test/scala/langserver/core/MessageWriterSuite.scala b/languageserver/src/test/scala/langserver/core/MessageWriterSuite.scala deleted file mode 100644 index 7570bcf0e1b..00000000000 --- a/languageserver/src/test/scala/langserver/core/MessageWriterSuite.scala +++ /dev/null @@ -1,45 +0,0 @@ -package langserver.core - -import java.io.PipedInputStream -import java.io.PipedOutputStream - -import org.scalatest.BeforeAndAfter -import org.scalatest.FunSuite -import org.scalatest.OptionValues - -import langserver.messages.ShowMessageParams -import langserver.types.MessageType -import play.api.libs.json.Json - -class MessageWriterSuite extends FunSuite - with BeforeAndAfter - with OptionValues { - - var inStream: PipedInputStream = _ - var outStream: PipedOutputStream = _ - - before { - inStream = new PipedInputStream - outStream = new PipedOutputStream(inStream) - } - - after { - outStream.close() - inStream.close() - } - - test("simple message can loop back") { - implicit val f = Json.format[ShowMessageParams] - val msgWriter = new MessageWriter(outStream) - val msgReader = new MessageReader(inStream) - - val obj = ShowMessageParams(MessageType.Error, "test") - msgWriter.write(obj) - - val payload = msgReader.nextPayload() - val r = f.reads(Json.parse(payload.get)) - - assert(r.isSuccess) - assert(r.asOpt.value == obj) - } -} diff --git a/languageserver/src/test/scala/langserver/core/TextDocumentSuite.scala b/languageserver/src/test/scala/langserver/core/TextDocumentSuite.scala deleted file mode 100644 index 29503cb7865..00000000000 --- a/languageserver/src/test/scala/langserver/core/TextDocumentSuite.scala +++ /dev/null @@ -1,62 +0,0 @@ -package langserver.core - -import org.scalatest.FunSuite -import langserver.types.Position - -class TextDocumentSuite extends FunSuite { - test("correct one line doc") { - val text = """one line document""" - - val td = new TextDocument("file:///dummy.txt", text.toArray) - - assert(td.offsetToPosition(0) == Position(0, 0)) - assert(td.offsetToPosition(3) == Position(0, 3)) - assert(td.offsetToPosition(16) == Position(0, 16)) - - assert(td.positionToOffset(Position(0, 0)) == 0) - assert(td.positionToOffset(Position(0, 3)) == 3) - assert(td.positionToOffset(Position(0, 16)) == 16) - } - - test("correct two lines doc") { - val text = """line1 -line2 -""" - - val td = new TextDocument("file:///dummy.txt", text.toArray) - - assert(td.offsetToPosition(0) == Position(0, 0)) - assert(td.offsetToPosition(4) == Position(0, 4)) - assert(td.offsetToPosition(5) == Position(0, 5)) // exactly the new line character - assert(td.offsetToPosition(6) == Position(1, 0)) - assert(td.offsetToPosition(7) == Position(1, 1)) - assert(td.offsetToPosition(10) == Position(1, 4)) - assert(td.offsetToPosition(11) == Position(1, 5)) - - assert(td.positionToOffset(Position(0, 0)) == 0) - assert(td.positionToOffset(Position(0, 4)) == 4) - assert(td.positionToOffset(Position(1, 0)) == 6) - assert(td.positionToOffset(Position(1, 4)) == 10) - assert(td.positionToOffset(Position(1, 5)) == 11) - } - - test("several lines CR/LF") { - val text = "line1\r\nline2\r\n" - - val td = new TextDocument("file:///dummy.txt", text.toArray) - - assert(td.offsetToPosition(0) == Position(0, 0)) - assert(td.offsetToPosition(4) == Position(0, 4)) - assert(td.offsetToPosition(5) == Position(0, 5)) // exactly the new line character - assert(td.offsetToPosition(7) == Position(1, 0)) - assert(td.offsetToPosition(8) == Position(1, 1)) - assert(td.offsetToPosition(11) == Position(1, 4)) - assert(td.offsetToPosition(12) == Position(1, 5)) - - assert(td.positionToOffset(Position(0, 0)) == 0) - assert(td.positionToOffset(Position(0, 4)) == 4) - assert(td.positionToOffset(Position(1, 0)) == 7) - assert(td.positionToOffset(Position(1, 4)) == 11) - assert(td.positionToOffset(Position(1, 5)) == 12) - } -} diff --git a/languageserver/src/test/scala/langserver/messages/CommandProtocolTest.scala b/languageserver/src/test/scala/langserver/messages/CommandProtocolTest.scala deleted file mode 100644 index b4d323262cb..00000000000 --- a/languageserver/src/test/scala/langserver/messages/CommandProtocolTest.scala +++ /dev/null @@ -1,23 +0,0 @@ -package langserver.messages - - - -import org.scalatest.FunSuite - -class CommandProtocolSuite extends FunSuite { - test("ServerCommand instantiastes") { - ServerCommand // the constructor may throw - } - - test("ResultResponse instantiastes") { - ResultResponse // the constructor may throw - } - - test("ClientCommand instantiates") { - ClientCommand // the constructor may throw - } - - test("Notification instantiates") { - Notification // the constructor may throw - } -} \ No newline at end of file diff --git a/metaserver/src/main/scala/scala/meta/languageserver/LSPLogger.scala b/metaserver/src/main/scala/scala/meta/languageserver/LSPLogger.scala index 6db83048f56..830e507ce5e 100644 --- a/metaserver/src/main/scala/scala/meta/languageserver/LSPLogger.scala +++ b/metaserver/src/main/scala/scala/meta/languageserver/LSPLogger.scala @@ -1,12 +1,12 @@ package scala.meta.languageserver -import ch.qos.logback.classic.spi.ILoggingEvent +import java.nio.charset.StandardCharsets.UTF_8 +import scala.beans.BeanProperty import ch.qos.logback.classic.encoder.PatternLayoutEncoder +import ch.qos.logback.classic.spi.ILoggingEvent import ch.qos.logback.core.AppenderBase -import langserver.core.Connection +import langserver.core.Notifications import langserver.types.MessageType -import java.nio.charset.StandardCharsets.UTF_8 -import scala.beans.BeanProperty class LSPLogger(@BeanProperty var encoder: PatternLayoutEncoder) extends AppenderBase[ILoggingEvent] { @@ -21,5 +21,5 @@ class LSPLogger(@BeanProperty var encoder: PatternLayoutEncoder) } object LSPLogger { - var connection: Option[Connection] = None + var connection: Option[Notifications] = None } diff --git a/metaserver/src/main/scala/scala/meta/languageserver/Linter.scala b/metaserver/src/main/scala/scala/meta/languageserver/Linter.scala index 35f2bd48018..d405f15d67e 100644 --- a/metaserver/src/main/scala/scala/meta/languageserver/Linter.scala +++ b/metaserver/src/main/scala/scala/meta/languageserver/Linter.scala @@ -1,9 +1,7 @@ package scala.meta.languageserver -import java.io.PrintStream import scala.meta.internal.tokenizers.PlatformTokenizerCache import scala.meta.parsers.Parsed -import scala.tools.nsc.interpreter.OutputStream import scala.{meta => m} import scalafix.internal.config.LazySemanticdbIndex import scalafix.internal.config.ScalafixConfig diff --git a/metaserver/src/main/scala/scala/meta/languageserver/ScalacErrorReporter.scala b/metaserver/src/main/scala/scala/meta/languageserver/ScalacErrorReporter.scala index eb0619df3ef..5b613a6f110 100644 --- a/metaserver/src/main/scala/scala/meta/languageserver/ScalacErrorReporter.scala +++ b/metaserver/src/main/scala/scala/meta/languageserver/ScalacErrorReporter.scala @@ -5,7 +5,6 @@ import scala.{meta => m} import scala.meta.semanticdb import scalafix.internal.util.EagerInMemorySemanticdbIndex import scalafix.util.SemanticdbIndex -import langserver.core.Connection import langserver.core.Notifications import langserver.messages.PublishDiagnostics import langserver.{types => l} diff --git a/metaserver/src/main/scala/scala/meta/languageserver/ScalametaLanguageServer.scala b/metaserver/src/main/scala/scala/meta/languageserver/ScalametaLanguageServer.scala index a5ad7611564..7bbfb6a69c7 100644 --- a/metaserver/src/main/scala/scala/meta/languageserver/ScalametaLanguageServer.scala +++ b/metaserver/src/main/scala/scala/meta/languageserver/ScalametaLanguageServer.scala @@ -20,7 +20,6 @@ import scala.meta.languageserver.refactoring.OrganizeImports import scala.meta.languageserver.search.SymbolIndex import com.typesafe.scalalogging.LazyLogging import io.github.soc.directories.ProjectDirectories -import langserver.core.Connection import langserver.messages._ import langserver.types._ import monix.eval.Task @@ -128,6 +127,7 @@ class ScalametaLanguageServer( params: InitializeParams ): Task[Either[Response.Error, InitializeResult]] = { logger.info(s"Initialized with $cwd, $params") + LSPLogger.connection = Some(client) cancelEffects = effects.map(_.subscribe()) loadAllRelevantFilesInThisWorkspace() val capabilities = ServerCapabilities( @@ -326,35 +326,58 @@ class ScalametaLanguageServer( case None => SignatureHelpProvider.empty } } - .request[ExecuteCommandParams, JsValue]( + .requestAsync[ExecuteCommandParams, JsValue]( "workspace/executeCommand" ) { params => logger.info(s"executeCommand $params") import WorkspaceCommand._ - WorkspaceCommand - .withNameOption(params.command) - .fold(logger.error(s"Unknown command ${params.command}")) { - case ClearIndexCache => - logger.info("Clearing the index cache") - ScalametaLanguageServer.clearCacheDirectory() - symbolIndex.clearIndex() - scalacProvider.allCompilerConfigs.foreach( - config => symbolIndex.indexDependencyClasspath(config.sourceJars) + val ok = Task(Right(JsNull)) + WorkspaceCommand.withNameOption(params.command) match { + case None => + val msg = s"Unknown command ${params.command}" + logger.error(msg) + Task(Left(Response.invalidParams(msg))) + case Some(ClearIndexCache) => + logger.info("Clearing the index cache") + ScalametaLanguageServer.clearCacheDirectory() + symbolIndex.clearIndex() + scalacProvider.allCompilerConfigs.foreach( + config => symbolIndex.indexDependencyClasspath(config.sourceJars) + ) + ok + case Some(ResetPresentationCompiler) => + logger.info("Resetting all compiler instances") + scalacProvider.resetCompilers() + ok + case Some(ScalafixUnusedImports) => + logger.info("Removing unused imports") + val response = for { + result <- OrganizeImports.removeUnused( + params.arguments, + symbolIndex ) - case ResetPresentationCompiler => - logger.info("Resetting all compiler instances") - scalacProvider.resetCompilers() - case ScalafixUnusedImports => - logger.info("Removing unused imports") - val result = - OrganizeImports.removeUnused( - params.arguments, - symbolIndex - ) - // TODO(olafur) make method return async - client.workspaceApplyEdit(result).runAsync - } - JsNull + applied <- result match { + // TODO(olafur): Monad Transformers? (no please!) + case Left(err) => Task(Left(err)) + case Right(command) => client.workspaceApplyEdit(command) + } + } yield { + applied match { + case Left(err) => + logger.warn(s"Failed to apply command $err") + Right(JsNull) + case Right(edit) => + if (edit.applied) { + logger.info(s"Successfully applied command $params") + } else { + logger.warn(s"Failed to apply edit for command $params") + } + case _ => + } + applied + } + response.map(_ => Right(JsNull)) + } } .request[WorkspaceSymbolParams, List[SymbolInformation]]( "workspace/symbol" diff --git a/metaserver/src/main/scala/scala/meta/languageserver/protocol/LanguageClient.scala b/metaserver/src/main/scala/scala/meta/languageserver/protocol/LanguageClient.scala index bcc8218bf9a..81f123fe131 100644 --- a/metaserver/src/main/scala/scala/meta/languageserver/protocol/LanguageClient.scala +++ b/metaserver/src/main/scala/scala/meta/languageserver/protocol/LanguageClient.scala @@ -8,6 +8,7 @@ import langserver.core.MessageWriter import langserver.core.Notifications import langserver.messages.ApplyWorkspaceEditParams import langserver.messages.ApplyWorkspaceEditResponse +import langserver.messages.LogMessageParams import langserver.messages.PublishDiagnostics import langserver.messages.ShowMessageParams import monix.eval.Callback @@ -17,7 +18,6 @@ import monix.execution.atomic.Atomic import monix.execution.atomic.AtomicInt import play.api.libs.json.JsError import play.api.libs.json.JsSuccess -import play.api.libs.json.JsValue import play.api.libs.json.Json import play.api.libs.json.Reads import play.api.libs.json.Writes @@ -50,9 +50,12 @@ class LanguageClient(out: OutputStream) extends LazyLogging with Notifications { callback.onSuccess(response) } - def request[A: Writes, B: Reads](method: String, request: A): Task[B] = { + def request[A: Writes, B: Reads]( + method: String, + request: A + ): Task[Either[Response.Error, B]] = { + val nextId = RequestId(counter.incrementAndGet()) val response = Task.create[Response] { (out, cb) => - val nextId = RequestId(counter.incrementAndGet()) val scheduled = out.scheduleOnce(Duration(0, "s")) { val json = Request(method, Some(Json.toJson(request)), nextId) activeServerRequests.put(nextId, cb) @@ -63,21 +66,27 @@ class LanguageClient(out: OutputStream) extends LazyLogging with Notifications { this.notify("$/cancelRequest", CancelParams(nextId.value)) } } - def fail(json: JsValue): Nothing = - throw new IllegalArgumentException(Json.prettyPrint(json)) response.map { case Response.Empty => - throw new IllegalArgumentException( - s"Got empty response for request $method -> $request" + Left( + Response.invalidParams( + s"Got empty response for request $request", + nextId + ) ) - case Response.Error(result, _) => - fail(Json.toJson(result)) + case err: Response.Error => + Left(err) case Response.Success(result, _) => result.validate[B] match { case JsSuccess(value, _) => - value + Right(value) case err: JsError => - fail(JsError.toJson(err)) + Left( + Response.invalidParams( + Json.prettyPrint(JsError.toJson(err)), + nextId + ) + ) } } } @@ -86,6 +95,10 @@ class LanguageClient(out: OutputStream) extends LazyLogging with Notifications { notify("window/showMessage", params) } + override def logMessage(params: LogMessageParams): Unit = { + notify("window/logMessage", params) + } + override def publishDiagnostics( publishDiagnostics: PublishDiagnostics ): Unit = { @@ -93,12 +106,10 @@ class LanguageClient(out: OutputStream) extends LazyLogging with Notifications { } def workspaceApplyEdit( params: ApplyWorkspaceEditParams - ): Task[ApplyWorkspaceEditResponse] = + ): Task[Either[Response.Error, ApplyWorkspaceEditResponse]] = request[ApplyWorkspaceEditParams, ApplyWorkspaceEditResponse]( "workspace/applyEdit", params ) } - - diff --git a/metaserver/src/main/scala/scala/meta/languageserver/protocol/LanguageServer.scala b/metaserver/src/main/scala/scala/meta/languageserver/protocol/LanguageServer.scala index 1998a5a4c7c..2c8ae15454c 100644 --- a/metaserver/src/main/scala/scala/meta/languageserver/protocol/LanguageServer.scala +++ b/metaserver/src/main/scala/scala/meta/languageserver/protocol/LanguageServer.scala @@ -1,13 +1,11 @@ package scala.meta.languageserver.protocol -import java.io.OutputStream import scala.collection.concurrent.TrieMap import scala.concurrent.Await import scala.concurrent.duration.Duration import scala.util.control.NonFatal import com.fasterxml.jackson.core.JsonParseException import com.typesafe.scalalogging.LazyLogging -import langserver.core.MessageWriter import monix.eval.Task import monix.execution.Cancelable import monix.execution.Scheduler diff --git a/metaserver/src/main/scala/scala/meta/languageserver/protocol/Message.scala b/metaserver/src/main/scala/scala/meta/languageserver/protocol/Message.scala index 3a998826464..4e639e1a4c8 100644 --- a/metaserver/src/main/scala/scala/meta/languageserver/protocol/Message.scala +++ b/metaserver/src/main/scala/scala/meta/languageserver/protocol/Message.scala @@ -1,5 +1,6 @@ package scala.meta.languageserver.protocol +import monix.eval.Task import play.api.libs.json._ sealed trait Message @@ -46,6 +47,8 @@ object Response { def empty: Response = Empty def ok(result: JsValue, id: RequestId): Response = success(result, id) + def okAsync[T](value: T): Task[Either[Response.Error, T]] = + Task(Right(value)) def success(result: JsValue, id: RequestId): Response = Success(result, id) def error(error: ErrorObject, id: RequestId): Response.Error = diff --git a/metaserver/src/main/scala/scala/meta/languageserver/providers/DocumentFormattingProvider.scala b/metaserver/src/main/scala/scala/meta/languageserver/providers/DocumentFormattingProvider.scala index 6c8d88ee5a7..e997a47767a 100644 --- a/metaserver/src/main/scala/scala/meta/languageserver/providers/DocumentFormattingProvider.scala +++ b/metaserver/src/main/scala/scala/meta/languageserver/providers/DocumentFormattingProvider.scala @@ -5,13 +5,10 @@ import scala.meta.languageserver.Configuration import scala.meta.languageserver.Configuration.Scalafmt import scala.meta.languageserver.Formatter import scala.meta.languageserver.MonixEnrichments._ -import scala.meta.languageserver.protocol.RequestId import scala.meta.languageserver.protocol.Response import scala.util.control.NonFatal import com.typesafe.scalalogging.LazyLogging import langserver.core.Notifications -import langserver.messages.DocumentFormattingResult -import langserver.types.MessageType import langserver.types.Position import langserver.types.Range import langserver.types.TextEdit diff --git a/metaserver/src/main/scala/scala/meta/languageserver/providers/DocumentHighlightProvider.scala b/metaserver/src/main/scala/scala/meta/languageserver/providers/DocumentHighlightProvider.scala index c2e0d8b4b4e..b88689fbfd6 100644 --- a/metaserver/src/main/scala/scala/meta/languageserver/providers/DocumentHighlightProvider.scala +++ b/metaserver/src/main/scala/scala/meta/languageserver/providers/DocumentHighlightProvider.scala @@ -2,12 +2,10 @@ package scala.meta.languageserver.providers import com.typesafe.scalalogging.LazyLogging import langserver.{types => l} -import langserver.messages.DocumentHighlightResult import scala.meta.languageserver.Uri import scala.meta.languageserver.search.SymbolIndex import scala.meta.languageserver.ScalametaEnrichments._ import langserver.types.DocumentHighlight -import langserver.types.Location object DocumentHighlightProvider extends LazyLogging { diff --git a/metaserver/src/main/scala/scala/meta/languageserver/providers/DocumentSymbolProvider.scala b/metaserver/src/main/scala/scala/meta/languageserver/providers/DocumentSymbolProvider.scala index d11ccdf9f51..86b39bed294 100644 --- a/metaserver/src/main/scala/scala/meta/languageserver/providers/DocumentSymbolProvider.scala +++ b/metaserver/src/main/scala/scala/meta/languageserver/providers/DocumentSymbolProvider.scala @@ -4,7 +4,6 @@ import scala.meta._ import scala.meta.languageserver.ScalametaEnrichments._ import scala.meta.languageserver.Uri import com.typesafe.scalalogging.LazyLogging -import langserver.messages.DocumentSymbolResult import langserver.types.SymbolInformation import langserver.{types => l} diff --git a/metaserver/src/main/scala/scala/meta/languageserver/providers/ReferencesProvider.scala b/metaserver/src/main/scala/scala/meta/languageserver/providers/ReferencesProvider.scala index d87b3b93611..d3fea8545d8 100644 --- a/metaserver/src/main/scala/scala/meta/languageserver/providers/ReferencesProvider.scala +++ b/metaserver/src/main/scala/scala/meta/languageserver/providers/ReferencesProvider.scala @@ -2,7 +2,6 @@ package scala.meta.languageserver.providers import com.typesafe.scalalogging.LazyLogging import langserver.{types => l} -import langserver.messages.ReferencesResult import scala.meta.languageserver.search.SymbolIndex import scala.meta.languageserver.ScalametaEnrichments._ import scala.meta.languageserver.Uri diff --git a/metaserver/src/main/scala/scala/meta/languageserver/providers/SquiggliesProvider.scala b/metaserver/src/main/scala/scala/meta/languageserver/providers/SquiggliesProvider.scala index 797ad9ca6da..396a68c572e 100644 --- a/metaserver/src/main/scala/scala/meta/languageserver/providers/SquiggliesProvider.scala +++ b/metaserver/src/main/scala/scala/meta/languageserver/providers/SquiggliesProvider.scala @@ -7,7 +7,6 @@ import scala.{meta => m} import langserver.messages.PublishDiagnostics import scala.meta.languageserver.ScalametaEnrichments._ import scala.meta.languageserver.MonixEnrichments._ -import scala.tools.nsc.interpreter.OutputStream import monix.eval.Task import monix.execution.Scheduler import monix.reactive.Observable diff --git a/metaserver/src/main/scala/scala/meta/languageserver/refactoring/OrganizeImports.scala b/metaserver/src/main/scala/scala/meta/languageserver/refactoring/OrganizeImports.scala index 5d117758694..dde4c02b530 100644 --- a/metaserver/src/main/scala/scala/meta/languageserver/refactoring/OrganizeImports.scala +++ b/metaserver/src/main/scala/scala/meta/languageserver/refactoring/OrganizeImports.scala @@ -6,15 +6,16 @@ import scala.meta.languageserver.Uri import scala.meta.languageserver.search.SymbolIndex import scalafix.internal.rule.RemoveUnusedImports import scala.meta.languageserver.ScalametaEnrichments._ +import scala.meta.languageserver.protocol.Response import scalafix.languageserver.ScalafixEnrichments._ import scalafix.languageserver.ScalafixPatchEnrichments._ import scalafix.rule.RuleCtx import scalafix.util.SemanticdbIndex import com.typesafe.scalalogging.LazyLogging import langserver.messages.ApplyWorkspaceEditParams -import langserver.messages.InvalidParamsResponseError import langserver.types.TextDocumentIdentifier import langserver.types.WorkspaceEdit +import monix.eval.Task import play.api.libs.json.JsValue import play.api.libs.json.Json @@ -25,17 +26,24 @@ object OrganizeImports extends LazyLogging { def removeUnused( arguments: Option[Seq[JsValue]], index: SymbolIndex - ): ApplyWorkspaceEditParams = { + ): Task[Either[Response.Error, ApplyWorkspaceEditParams]] = { val result = for { as <- arguments argument <- as.headOption textDocument <- Json.fromJson[TextDocumentIdentifier](argument).asOpt } yield removeUnused(Uri(textDocument), index) - result.getOrElse( - throw InvalidParamsResponseError( - s"Unable to parse TextDocumentIdentifier from $arguments" - ) - ) + Task { + result match { + case Some(x) => + Right(x) + case None => + Left( + Response.invalidParams( + s"Unable to parse TextDocumentIdentifier from $arguments" + ) + ) + } + } } def removeUnused(uri: Uri, index: SymbolIndex): ApplyWorkspaceEditParams = { From 78ac11650b35dc9543113f9e40e893d598a00751 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=93lafur=20P=C3=A1ll=20Geirsson?= Date: Wed, 3 Jan 2018 23:57:03 +0100 Subject: [PATCH 12/18] Clean before PR - fix $/cancelParams - remove scala-json-rpc - remove dead code --- build.sbt | 3 - .../main/scala/langserver/types/types.scala | 6 ++ .../ScalametaLanguageServer.scala | 9 +-- .../protocol/BaseProtocolMessage.scala | 8 ++- .../protocol/BaseProtocolMessageParser.scala | 13 ++-- .../protocol/CancelParams.scala | 11 ---- .../protocol/LanguageClient.scala | 2 +- .../protocol/LanguageServer.scala | 4 +- .../languageserver/protocol/RequestId.scala | 2 +- .../languageserver/protocol/Services.scala | 26 +++++--- .../src/test/scala/tests/JsonRpcSuite.scala | 62 ------------------- .../src/main/scala/example/User.scala | 2 +- .../src/test/scala/example/UserTest.scala | 2 +- vscode-extension/src/extension.ts | 2 - 14 files changed, 45 insertions(+), 107 deletions(-) delete mode 100644 metaserver/src/main/scala/scala/meta/languageserver/protocol/CancelParams.scala delete mode 100644 metaserver/src/test/scala/tests/JsonRpcSuite.scala diff --git a/build.sbt b/build.sbt index 4964d94bfef..3775edea5c7 100644 --- a/build.sbt +++ b/build.sbt @@ -84,9 +84,7 @@ lazy val semanticdbSettings = List( lazy val languageserver = project .settings( - resolvers += Resolver.bintrayRepo("dhpcs", "maven"), libraryDependencies ++= Seq( - "com.dhpcs" %% "scala-json-rpc" % "2.0.1", "com.beachape" %% "enumeratum" % V.enumeratum, "com.beachape" %% "enumeratum-play-json" % "1.5.12-2.6.0-M7", "com.typesafe.scala-logging" %% "scala-logging" % "3.7.2", @@ -105,7 +103,6 @@ lazy val metaserver = project flatPackage = true // Don't append filename to package ) -> sourceManaged.in(Compile).value./("protobuf") ), - resolvers += Resolver.bintrayRepo("dhpcs", "maven"), testFrameworks := new TestFramework("utest.runner.Framework") :: Nil, fork in Test := true, // required for jni interrop with leveldb. buildInfoKeys := Seq[BuildInfoKey]( diff --git a/languageserver/src/main/scala/langserver/types/types.scala b/languageserver/src/main/scala/langserver/types/types.scala index cfc5b9c0218..2caf60cc0de 100644 --- a/languageserver/src/main/scala/langserver/types/types.scala +++ b/languageserver/src/main/scala/langserver/types/types.scala @@ -351,3 +351,9 @@ case class FileEvent( object FileEvent { implicit val format: OFormat[FileEvent] = Json.format[FileEvent] } + +case class CancelParams(id: JsValue) + +object CancelParams { + implicit val format: OFormat[CancelParams] = Json.format[CancelParams] +} diff --git a/metaserver/src/main/scala/scala/meta/languageserver/ScalametaLanguageServer.scala b/metaserver/src/main/scala/scala/meta/languageserver/ScalametaLanguageServer.scala index 7bbfb6a69c7..ad4e0112929 100644 --- a/metaserver/src/main/scala/scala/meta/languageserver/ScalametaLanguageServer.scala +++ b/metaserver/src/main/scala/scala/meta/languageserver/ScalametaLanguageServer.scala @@ -52,9 +52,7 @@ class ScalametaLanguageServer( // Always run the presentation compiler on the same thread private val presentationCompilerScheduler: SchedulerService = Scheduler(Executors.newFixedThreadPool(1)) - def onPresentationCompilerThread[A]( - f: => A - ): Task[Either[Response.Error, A]] = + def withPC[A](f: => A): Task[Either[Response.Error, A]] = Task(Right(f)).executeOn(presentationCompilerScheduler) val (fileSystemSemanticdbSubscriber, fileSystemSemanticdbsPublisher) = ScalametaLanguageServer.fileSystemSemanticdbStream(cwd) @@ -172,13 +170,13 @@ class ScalametaLanguageServer( JsNull } .notification[JsValue]("exit") { _ => - pprint.log("exit") + logger.info("exit(0)") sys.exit(0) } .requestAsync[TextDocumentPositionParams, CompletionList]( "textDocument/completion" ) { params => - onPresentationCompilerThread { + withPC { logger.info("completion") scalacProvider.getCompiler(params.textDocument) match { case Some(g) => @@ -233,7 +231,6 @@ class ScalametaLanguageServer( .notification[DidSaveTextDocumentParams]( "textDocument/didSave" ) { params => - pprint.log(params) () } .notification[DidChangeConfigurationParams]( diff --git a/metaserver/src/main/scala/scala/meta/languageserver/protocol/BaseProtocolMessage.scala b/metaserver/src/main/scala/scala/meta/languageserver/protocol/BaseProtocolMessage.scala index 50198f96bbd..35a824b31cc 100644 --- a/metaserver/src/main/scala/scala/meta/languageserver/protocol/BaseProtocolMessage.scala +++ b/metaserver/src/main/scala/scala/meta/languageserver/protocol/BaseProtocolMessage.scala @@ -2,6 +2,7 @@ package scala.meta.languageserver.protocol import java.io.InputStream import java.util.concurrent.Executors +import monix.execution.ExecutionModel import monix.execution.Scheduler import monix.reactive.Observable @@ -22,6 +23,11 @@ object BaseProtocolMessage { def fromInputStream(in: InputStream): Observable[BaseProtocolMessage] = Observable .fromInputStream(in) - .executeOn(Scheduler(Executors.newFixedThreadPool(1))) + .executeOn( + Scheduler( + Executors.newFixedThreadPool(1), + ExecutionModel.AlwaysAsyncExecution + ) + ) .liftByOperator(new BaseProtocolMessageParser) } diff --git a/metaserver/src/main/scala/scala/meta/languageserver/protocol/BaseProtocolMessageParser.scala b/metaserver/src/main/scala/scala/meta/languageserver/protocol/BaseProtocolMessageParser.scala index 7f90823f31a..fbef2cb1102 100644 --- a/metaserver/src/main/scala/scala/meta/languageserver/protocol/BaseProtocolMessageParser.scala +++ b/metaserver/src/main/scala/scala/meta/languageserver/protocol/BaseProtocolMessageParser.scala @@ -19,17 +19,16 @@ final class BaseProtocolMessageParser import Ack._ // NOTE(olafur): We should first benchmark before going into any // optimization, but my intuition tells me ArrayBuffer[Byte] with many .remove - // and ++= is wasteful and can probably be replaced with some ByteBuffer - // or mutable.Queue[Array[Byte]] to get better performance. + // and ++= is wasteful and can probably be replaced with scodec-bits BitVector private[this] val data = ArrayBuffer.empty[Byte] private[this] var contentLength = -1 private[this] var header = Map.empty[String, String] private[this] def atDelimiter(idx: Int): Boolean = { - (data.size >= idx + 4 - && data(idx) == '\r' - && data(idx + 1) == '\n' - && data(idx + 2) == '\r' - && data(idx + 3) == '\n') + data.size >= idx + 4 && + data(idx) == '\r' && + data(idx + 1) == '\n' && + data(idx + 2) == '\r' && + data(idx + 3) == '\n' } private[this] val EmptyPair = "" -> "" private[this] def readHeaders(): Future[Ack] = { diff --git a/metaserver/src/main/scala/scala/meta/languageserver/protocol/CancelParams.scala b/metaserver/src/main/scala/scala/meta/languageserver/protocol/CancelParams.scala deleted file mode 100644 index 33f1a5d7fed..00000000000 --- a/metaserver/src/main/scala/scala/meta/languageserver/protocol/CancelParams.scala +++ /dev/null @@ -1,11 +0,0 @@ -package scala.meta.languageserver.protocol - -import play.api.libs.json.JsValue -import play.api.libs.json.Json -import play.api.libs.json.OFormat - -case class CancelParams(id: JsValue) - -object CancelParams { - implicit val format: OFormat[CancelParams] = Json.format[CancelParams] -} \ No newline at end of file diff --git a/metaserver/src/main/scala/scala/meta/languageserver/protocol/LanguageClient.scala b/metaserver/src/main/scala/scala/meta/languageserver/protocol/LanguageClient.scala index 81f123fe131..a1e13bdab3e 100644 --- a/metaserver/src/main/scala/scala/meta/languageserver/protocol/LanguageClient.scala +++ b/metaserver/src/main/scala/scala/meta/languageserver/protocol/LanguageClient.scala @@ -11,6 +11,7 @@ import langserver.messages.ApplyWorkspaceEditResponse import langserver.messages.LogMessageParams import langserver.messages.PublishDiagnostics import langserver.messages.ShowMessageParams +import langserver.types.CancelParams import monix.eval.Callback import monix.eval.Task import monix.execution.Cancelable @@ -111,5 +112,4 @@ class LanguageClient(out: OutputStream) extends LazyLogging with Notifications { "workspace/applyEdit", params ) - } diff --git a/metaserver/src/main/scala/scala/meta/languageserver/protocol/LanguageServer.scala b/metaserver/src/main/scala/scala/meta/languageserver/protocol/LanguageServer.scala index 2c8ae15454c..5f446d5aa4f 100644 --- a/metaserver/src/main/scala/scala/meta/languageserver/protocol/LanguageServer.scala +++ b/metaserver/src/main/scala/scala/meta/languageserver/protocol/LanguageServer.scala @@ -6,6 +6,7 @@ import scala.concurrent.duration.Duration import scala.util.control.NonFatal import com.fasterxml.jackson.core.JsonParseException import com.typesafe.scalalogging.LazyLogging +import langserver.types.CancelParams import monix.eval.Task import monix.execution.Cancelable import monix.execution.Scheduler @@ -23,7 +24,8 @@ final class LanguageServer( ) extends LazyLogging { private val activeClientRequests: TrieMap[JsValue, Cancelable] = TrieMap.empty private val cancelNotification = - Service.notification[JsValue]("$/cancelRequest") { id => + Service.notification[CancelParams]("$/cancelRequest") { params => + val id = params.id activeClientRequests.get(id) match { case None => Task { diff --git a/metaserver/src/main/scala/scala/meta/languageserver/protocol/RequestId.scala b/metaserver/src/main/scala/scala/meta/languageserver/protocol/RequestId.scala index 3a8043e1d9a..fa5fd43e87d 100644 --- a/metaserver/src/main/scala/scala/meta/languageserver/protocol/RequestId.scala +++ b/metaserver/src/main/scala/scala/meta/languageserver/protocol/RequestId.scala @@ -22,4 +22,4 @@ object RequestId { case class Number(value: JsNumber) extends RequestId case class String(value: JsString) extends RequestId case object Null extends RequestId -} \ No newline at end of file +} diff --git a/metaserver/src/main/scala/scala/meta/languageserver/protocol/Services.scala b/metaserver/src/main/scala/scala/meta/languageserver/protocol/Services.scala index 40323c434da..9803b378ae4 100644 --- a/metaserver/src/main/scala/scala/meta/languageserver/protocol/Services.scala +++ b/metaserver/src/main/scala/scala/meta/languageserver/protocol/Services.scala @@ -29,6 +29,9 @@ object Service extends LazyLogging { case JsSuccess(value, _) => f.handle(value).map { case Right(response) => Response.ok(Json.toJson(response), id) + // Service[A, ...] doesn't have access to the request ID so + // by convention it's OK to set the ID to null by default + // and we fill it in here instead. case Left(err) => err.copy(id = id) } } @@ -59,12 +62,9 @@ object Service extends LazyLogging { case Notification(invalidMethod, _) => fail(s"Expected method '$method', obtained '$invalidMethod'") case _ => - fail( - s"Expected notification, obtained $message" - ) + fail(s"Expected notification, obtained $message") } } - } object Services { @@ -72,21 +72,27 @@ object Services { } class Services private (val services: List[NamedJsonRpcService]) { + def request[A: Reads, B: Writes](method: String)( f: A => B - ): Services = requestAsync[A, B](method)(request => Task(Right(f(request)))) + ): Services = + requestAsync[A, B](method)(request => Task(Right(f(request)))) + def requestAsync[A: Reads, B: Writes](method: String)( f: Service[A, Either[Response.Error, B]] - ): Services = addService(Service.request[A, B](method)(f)) + ): Services = + addService(Service.request[A, B](method)(f)) + def notification[A: Reads](method: String)( f: A => Unit - ): Services = notificationAsync[A](method)(request => Task(f(request))) + ): Services = + notificationAsync[A](method)(request => Task(f(request))) + def notificationAsync[A: Reads](method: String)( f: Service[A, Unit] - ): Services = addService(Service.notification[A](method)(f)) + ): Services = + addService(Service.notification[A](method)(f)) - def +(other: Services): Services = - other.services.foldLeft(this)(_ addService _) def byMethodName: Map[String, NamedJsonRpcService] = services.iterator.map(s => s.methodName -> s).toMap def addService(service: NamedJsonRpcService): Services = { diff --git a/metaserver/src/test/scala/tests/JsonRpcSuite.scala b/metaserver/src/test/scala/tests/JsonRpcSuite.scala deleted file mode 100644 index 27279475b7f..00000000000 --- a/metaserver/src/test/scala/tests/JsonRpcSuite.scala +++ /dev/null @@ -1,62 +0,0 @@ -package tests - -import java.io.PipedInputStream -import java.io.PipedOutputStream -import java.io.PrintWriter -import scala.meta.languageserver.protocol.BaseProtocolMessage -import monix.execution.CancelableFuture -import monix.execution.ExecutionModel.AlwaysAsyncExecution -import monix.execution.schedulers.TestScheduler - -class JsonRpcSuite extends MegaSuite { - var out: PrintWriter = _ - var messages = List.empty[BaseProtocolMessage] - implicit var s: TestScheduler = _ - var f: CancelableFuture[Unit] = CancelableFuture.unit - override def utestBeforeEach(path: Seq[String]): Unit = { - s = TestScheduler(AlwaysAsyncExecution) - val in = new PipedInputStream - out = new PrintWriter(new PipedOutputStream(in)) - messages = Nil - f = BaseProtocolMessage.fromInputStream(in).foreach { msg => - messages = msg :: messages - } - } - - override def utestAfterEach(path: Seq[String]): Unit = { - f.cancel() - } - - def write(msg: String): Unit = { - out.print(msg.replaceAll("\n", "\r\n")) - out.flush() - s.tickOne() - } - - val header = """Content-Length: 43 - -""" - val content = """{"jsonrpc":"2.0","id":1,"method":"example"}""" - - val message: String = header + content - val lspMessage = BaseProtocolMessage(Map("Content-Length" -> "43"), content) - - test("header and content together") { - write(message) - assertEquals(messages, List(lspMessage)) - } - - test("combined") { - write(message * 2) - s.tickOne() - assertEquals(messages, List(lspMessage, lspMessage)) - } - - test("header and content separately") { - write(header) - assert(messages.isEmpty) - write(content) - assertEquals(messages, List(lspMessage)) - } - -} diff --git a/test-workspace/src/main/scala/example/User.scala b/test-workspace/src/main/scala/example/User.scala index b19bee53863..afa25528e5f 100644 --- a/test-workspace/src/main/scala/example/User.scala +++ b/test-workspace/src/main/scala/example/User.scala @@ -9,4 +9,4 @@ object a { val localSymbol = "222" // can be renamed localSymbol.length } -} \ No newline at end of file +} diff --git a/test-workspace/src/test/scala/example/UserTest.scala b/test-workspace/src/test/scala/example/UserTest.scala index 91ca57e7f89..cd1de19ee22 100644 --- a/test-workspace/src/test/scala/example/UserTest.scala +++ b/test-workspace/src/test/scala/example/UserTest.scala @@ -8,5 +8,5 @@ class UserTest { .map(x => x.+(user.age)) scala.runtime.CharRef.create('a') val str = user.name + a.a.x - val left: Either[String, Int] = Left("") + val left: Either[String, Int] = Left("") } diff --git a/vscode-extension/src/extension.ts b/vscode-extension/src/extension.ts index 2a8b294031f..32c6c95669d 100644 --- a/vscode-extension/src/extension.ts +++ b/vscode-extension/src/extension.ts @@ -32,8 +32,6 @@ export async function activate(context: ExtensionContext) { const coursierArgs = [ "launch", "-r", - "https://dl.bintray.com/dhpcs/maven", - "-r", "sonatype:releases", "-J", toolsJar, From d9b38bc5e8b0b24e3a1c28a2aba27d42695ee825 Mon Sep 17 00:00:00 2001 From: Gabriele Petronella Date: Thu, 4 Jan 2018 16:53:50 +0100 Subject: [PATCH 13/18] Replace play-json with circe --- build.sbt | 10 +- .../scala/langserver/core/MessageWriter.scala | 6 +- .../scala/langserver/messages/Commands.scala | 217 ++++-------------- .../main/scala/langserver/types/enums.scala | 14 +- .../main/scala/langserver/types/types.scala | 172 +++++--------- .../meta/languageserver/Configuration.scala | 75 ++---- .../languageserver/PlayJsonEnrichments.scala | 15 -- .../ScalametaLanguageServer.scala | 34 ++- .../languageserver/protocol/ErrorCode.scala | 4 +- .../languageserver/protocol/ErrorObject.scala | 11 +- .../protocol/LanguageClient.scala | 30 +-- .../protocol/LanguageServer.scala | 32 +-- .../languageserver/protocol/Message.scala | 52 ++--- .../languageserver/protocol/RequestId.scala | 34 +-- .../languageserver/protocol/Services.scala | 34 +-- .../providers/CodeActionProvider.scala | 4 +- .../refactoring/OrganizeImports.scala | 9 +- .../tests/compiler/CompletionsTest.scala | 26 ++- .../tests/compiler/SignatureHelpTest.scala | 195 +++++++++++----- .../scala/tests/hover/BaseHoverTest.scala | 15 +- 20 files changed, 403 insertions(+), 586 deletions(-) delete mode 100644 metaserver/src/main/scala/scala/meta/languageserver/PlayJsonEnrichments.scala diff --git a/build.sbt b/build.sbt index 3775edea5c7..f2e6236e194 100644 --- a/build.sbt +++ b/build.sbt @@ -55,7 +55,8 @@ inThisBuild( bintrayReleaseOnPublish := dynverGitDescribeOutput.value.isVersionStable, // faster publishLocal: publishArtifact in packageDoc := sys.env.contains("CI"), - publishArtifact in packageSrc := sys.env.contains("CI") + publishArtifact in packageSrc := sys.env.contains("CI"), + addCompilerPlugin("org.scalamacros" % "paradise" % "2.1.0" cross CrossVersion.full) ) ) @@ -64,6 +65,7 @@ lazy val V = new { val scalameta = "2.1.5" val scalafix = "0.5.7" val enumeratum = "1.5.12" + val circe = "0.9.0" } lazy val noPublish = List( @@ -85,8 +87,12 @@ lazy val semanticdbSettings = List( lazy val languageserver = project .settings( libraryDependencies ++= Seq( + "io.circe" %% "circe-core" % V.circe, + "io.circe" %% "circe-generic" % V.circe, + "io.circe" %% "circe-generic-extras" % V.circe, + "io.circe" %% "circe-parser" % V.circe, "com.beachape" %% "enumeratum" % V.enumeratum, - "com.beachape" %% "enumeratum-play-json" % "1.5.12-2.6.0-M7", + "com.beachape" %% "enumeratum-circe" % "1.5.15", "com.typesafe.scala-logging" %% "scala-logging" % "3.7.2", "io.monix" %% "monix" % "2.3.0", "org.slf4j" % "slf4j-api" % "1.7.25", diff --git a/languageserver/src/main/scala/langserver/core/MessageWriter.scala b/languageserver/src/main/scala/langserver/core/MessageWriter.scala index bd4e6f58c08..4905b0c7af8 100644 --- a/languageserver/src/main/scala/langserver/core/MessageWriter.scala +++ b/languageserver/src/main/scala/langserver/core/MessageWriter.scala @@ -2,8 +2,8 @@ package langserver.core import java.io.OutputStream import java.nio.charset.StandardCharsets -import play.api.libs.json._ import com.typesafe.scalalogging.LazyLogging +import io.circe.Encoder /** * A class to write Json RPC messages on an output stream, following the Language Server Protocol. @@ -29,10 +29,10 @@ class MessageWriter(out: OutputStream) extends LazyLogging { * Write a message to the output stream. This method can be called from multiple threads, * but it may block waiting for other threads to finish writing. */ - def write[T](msg: T, h: Map[String, String] = Map.empty)(implicit o: Writes[T]): Unit = lock.synchronized { + def write[T](msg: T, h: Map[String, String] = Map.empty)(implicit encode: Encoder[T]): Unit = lock.synchronized { require(h.get(ContentLen).isEmpty) - val str = Json.stringify(o.writes(msg)) + val str = encode(msg).noSpaces val contentBytes = str.getBytes(StandardCharsets.UTF_8) val headers = (h + (ContentLen -> contentBytes.length)) .map { case (k, v) => s"$k: $v" } diff --git a/languageserver/src/main/scala/langserver/messages/Commands.scala b/languageserver/src/main/scala/langserver/messages/Commands.scala index 1aa17127b28..3659b2137bb 100644 --- a/languageserver/src/main/scala/langserver/messages/Commands.scala +++ b/languageserver/src/main/scala/langserver/messages/Commands.scala @@ -1,14 +1,13 @@ package langserver.messages import langserver.types._ -import play.api.libs.json.OFormat -import play.api.libs.json._ - +import io.circe.Json +import io.circe.generic.JsonCodec /** * Parameters and types used in the `initialize` message. */ -case class InitializeParams( +@JsonCodec case class InitializeParams( /** * The process Id of the parent process that started * the server. @@ -24,22 +23,14 @@ case class InitializeParams( /** * The capabilities provided by the client (editor) */ - capabilities: ClientCapabilities) -object InitializeParams { - implicit val format: OFormat[InitializeParams] = Json.format[InitializeParams] -} + capabilities: ClientCapabilities +) case class InitializeError(retry: Boolean) -case class ClientCapabilities() - -object ClientCapabilities { - implicit val format: Format[ClientCapabilities] = Format( - Reads(jsValue => JsSuccess(ClientCapabilities())), - Writes(c => Json.obj())) -} +@JsonCodec case class ClientCapabilities() -case class ServerCapabilities( +@JsonCodec case class ServerCapabilities( /** * Defines how text documents are synced. */ @@ -106,53 +97,15 @@ case class ServerCapabilities( executeCommandProvider: ExecuteCommandOptions = ExecuteCommandOptions(Nil) ) -object ServerCapabilities { - implicit val format: OFormat[ServerCapabilities] = Json.format[ServerCapabilities] -} - -case class CompletionOptions(resolveProvider: Boolean, triggerCharacters: Seq[String]) -object CompletionOptions { - implicit val format: Format[CompletionOptions] = Json.format[CompletionOptions] -} - -case class SignatureHelpOptions(triggerCharacters: Seq[String]) -object SignatureHelpOptions { - implicit val format: Format[SignatureHelpOptions] = Json.format[SignatureHelpOptions] -} - -case class CodeLensOptions(resolveProvider: Boolean = false) -object CodeLensOptions { - implicit val format: Format[CodeLensOptions] = Json.format[CodeLensOptions] -} - -case class DocumentOnTypeFormattingOptions(firstTriggerCharacter: String, moreTriggerCharacters: Seq[String]) -object DocumentOnTypeFormattingOptions { - implicit val format: Format[DocumentOnTypeFormattingOptions] = Json.format[DocumentOnTypeFormattingOptions] -} - -case class ExecuteCommandOptions(commands: Seq[String]) -object ExecuteCommandOptions { - implicit val format: Format[ExecuteCommandOptions] = Json.format[ExecuteCommandOptions] -} - -case class CompletionList(isIncomplete: Boolean, items: Seq[CompletionItem]) -object CompletionList { - implicit val format: OFormat[CompletionList] = Json.format[CompletionList] -} - -case class InitializeResult(capabilities: ServerCapabilities) -object InitializeResult { - implicit val format: OFormat[InitializeResult] = Json.format[InitializeResult] -} - -case class Shutdown() - -case class ShutdownResult() -object ShutdownResult { - implicit val format: Format[ShutdownResult] = OFormat( - Reads(jsValue => JsSuccess(ShutdownResult())), - OWrites[ShutdownResult](s => Json.obj())) -} +@JsonCodec case class CompletionOptions(resolveProvider: Boolean, triggerCharacters: Seq[String]) +@JsonCodec case class SignatureHelpOptions(triggerCharacters: Seq[String]) +@JsonCodec case class CodeLensOptions(resolveProvider: Boolean = false) +@JsonCodec case class DocumentOnTypeFormattingOptions(firstTriggerCharacter: String, moreTriggerCharacters: Seq[String]) +@JsonCodec case class ExecuteCommandOptions(commands: Seq[String]) +@JsonCodec case class CompletionList(isIncomplete: Boolean, items: Seq[CompletionItem]) +@JsonCodec case class InitializeResult(capabilities: ServerCapabilities) +@JsonCodec case class Shutdown() +@JsonCodec case class ShutdownResult() /** * The show message request is sent from a server to a client to ask the client to display a @@ -163,7 +116,7 @@ object ShutdownResult { * @param message The actual message * @param actions The message action items to present. */ -case class ShowMessageRequestParams( +@JsonCodec case class ShowMessageRequestParams( `type`: MessageType, message: String, actions: Seq[MessageActionItem] @@ -172,57 +125,37 @@ case class ShowMessageRequestParams( /** * A short title like 'Retry', 'Open Log' etc. */ -case class MessageActionItem(title: String) -object MessageActionItem { - implicit val format: OFormat[MessageActionItem] = Json.format[MessageActionItem] -} +@JsonCodec case class MessageActionItem(title: String) -case class TextDocumentPositionParams( +@JsonCodec case class TextDocumentPositionParams( textDocument: TextDocumentIdentifier, position: Position ) -object TextDocumentPositionParams { - implicit val format: OFormat[TextDocumentPositionParams] = Json.format[TextDocumentPositionParams] -} -case class ReferenceParams( + +@JsonCodec case class ReferenceParams( textDocument: TextDocumentIdentifier, position: Position, context: ReferenceContext ) -object ReferenceParams { - implicit val format: OFormat[ReferenceParams] = Json.format[ReferenceParams] -} -case class RenameParams( +@JsonCodec case class RenameParams( textDocument: TextDocumentIdentifier, position: Position, newName: String ) -object RenameParams { - implicit val format: OFormat[RenameParams] = Json.format[RenameParams] -} -case class CodeActionParams( +@JsonCodec case class CodeActionParams( textDocument: TextDocumentIdentifier, range: Range, context: CodeActionContext ) -object CodeActionParams { - implicit val format: OFormat[CodeActionParams] = Json.format[CodeActionParams] -} -case class CodeActionRequest(params: CodeActionParams) -object CodeActionRequest { - implicit val format: OFormat[CodeActionRequest] = Json.format[CodeActionRequest] -} -case class DocumentSymbolParams(textDocument: TextDocumentIdentifier) -object DocumentSymbolParams { - implicit val format: OFormat[DocumentSymbolParams] = Json.format[DocumentSymbolParams] -} -case class TextDocumentRenameRequest(params: RenameParams) -object TextDocumentRenameRequest { - implicit val format: OFormat[TextDocumentRenameRequest] = Json.format[TextDocumentRenameRequest] -} +@JsonCodec case class CodeActionRequest(params: CodeActionParams) + +@JsonCodec case class DocumentSymbolParams(textDocument: TextDocumentIdentifier) + +@JsonCodec case class TextDocumentRenameRequest(params: RenameParams) + case class TextDocumentSignatureHelpRequest(params: TextDocumentPositionParams) case class TextDocumentCompletionRequest(params: TextDocumentPositionParams) case class TextDocumentDefinitionRequest(params: TextDocumentPositionParams) @@ -232,105 +165,49 @@ case class TextDocumentHoverRequest(params: TextDocumentPositionParams) case class TextDocumentFormattingRequest(params: DocumentFormattingParams) case class WorkspaceExecuteCommandRequest(params: ExecuteCommandParams) case class WorkspaceSymbolRequest(params: WorkspaceSymbolParams) -case class ApplyWorkspaceEditResponse(applied: Boolean) -object ApplyWorkspaceEditResponse { - implicit val format: OFormat[ApplyWorkspaceEditResponse] = Json.format[ApplyWorkspaceEditResponse] -} -case class ApplyWorkspaceEditParams(label: Option[String], edit: WorkspaceEdit) -object ApplyWorkspaceEditParams { - implicit val format: OFormat[ApplyWorkspaceEditParams] = Json.format[ApplyWorkspaceEditParams] -} +@JsonCodec case class ApplyWorkspaceEditResponse(applied: Boolean) +@JsonCodec case class ApplyWorkspaceEditParams(label: Option[String], edit: WorkspaceEdit) -case class Hover(contents: Seq[MarkedString], range: Option[Range]) -object Hover { - implicit val format: OFormat[Hover] = Json.format[Hover] -} +@JsonCodec case class Hover(contents: Seq[MarkedString], range: Option[Range]) ///////////////////////////// Notifications /////////////////////////////// // From server to client -case class ShowMessageParams(`type`: MessageType, message: String) -object ShowMessageParams { - implicit val format: OFormat[ShowMessageParams] = Json.format[ShowMessageParams] -} -case class LogMessageParams(`type`: MessageType, message: String) -object LogMessageParams { - implicit val format: OFormat[LogMessageParams] = Json.format[LogMessageParams] -} -case class PublishDiagnostics(uri: String, diagnostics: Seq[Diagnostic]) -object PublishDiagnostics { - implicit val format: OFormat[PublishDiagnostics] = Json.format[PublishDiagnostics] -} +@JsonCodec case class ShowMessageParams(`type`: MessageType, message: String) +@JsonCodec case class LogMessageParams(`type`: MessageType, message: String) +@JsonCodec case class PublishDiagnostics(uri: String, diagnostics: Seq[Diagnostic]) // from client to server case class Exit() -case class DidOpenTextDocumentParams(textDocument: TextDocumentItem) -object DidOpenTextDocumentParams { - implicit val format: OFormat[DidOpenTextDocumentParams] = Json.format[DidOpenTextDocumentParams] -} -case class DidChangeTextDocumentParams( +@JsonCodec case class DidOpenTextDocumentParams(textDocument: TextDocumentItem) +@JsonCodec case class DidChangeTextDocumentParams( textDocument: VersionedTextDocumentIdentifier, contentChanges: Seq[TextDocumentContentChangeEvent]) -object DidChangeTextDocumentParams { - implicit val format: OFormat[DidChangeTextDocumentParams] = Json.format[DidChangeTextDocumentParams] -} - -case class DidCloseTextDocumentParams(textDocument: TextDocumentIdentifier) -object DidCloseTextDocumentParams { - implicit val format: OFormat[DidCloseTextDocumentParams] = Json.format[DidCloseTextDocumentParams] -} -case class DidSaveTextDocumentParams(textDocument: TextDocumentIdentifier) -object DidSaveTextDocumentParams { - implicit val format: OFormat[DidSaveTextDocumentParams] = Json.format[DidSaveTextDocumentParams] -} -case class DidChangeWatchedFilesParams(changes: Seq[FileEvent]) -object DidChangeWatchedFilesParams { - implicit val format: OFormat[DidChangeWatchedFilesParams] = Json.format[DidChangeWatchedFilesParams] -} -case class DidChangeConfigurationParams(settings: JsValue) -object DidChangeConfigurationParams { - implicit val format: OFormat[DidChangeConfigurationParams] = Json.format[DidChangeConfigurationParams] -} +@JsonCodec case class DidCloseTextDocumentParams(textDocument: TextDocumentIdentifier) +@JsonCodec case class DidSaveTextDocumentParams(textDocument: TextDocumentIdentifier) +@JsonCodec case class DidChangeWatchedFilesParams(changes: Seq[FileEvent]) +@JsonCodec case class DidChangeConfigurationParams(settings: Json) -case class Initialized() -object Initialized { - implicit val format: Format[Initialized] = OFormat( - Reads(jsValue => JsSuccess(Initialized())), - OWrites[Initialized](s => Json.obj())) -} +@JsonCodec case class Initialized() +@JsonCodec case class CancelRequest(id: Int) -case class CancelRequest(id: Int) - -case class RenameResult(params: WorkspaceEdit) -object RenameResult { - implicit val format: OFormat[RenameResult] = Json.format[RenameResult] -} -case class CodeActionResult(params: Seq[Command]) -object CodeActionResult { - implicit val format: OFormat[CodeActionResult] = Json.format[CodeActionResult] -} +@JsonCodec case class RenameResult(params: WorkspaceEdit) +@JsonCodec case class CodeActionResult(params: Seq[Command]) case class DocumentSymbolResult(params: Seq[SymbolInformation]) case class DefinitionResult(params: Seq[Location]) case class ReferencesResult(params: Seq[Location]) case class DocumentHighlightResult(params: Seq[Location]) case class DocumentFormattingResult(params: Seq[TextEdit]) -case class SignatureHelp(signatures: Seq[SignatureInformation], +@JsonCodec case class SignatureHelp(signatures: Seq[SignatureInformation], activeSignature: Option[Int], activeParameter: Option[Int]) -object SignatureHelp { - implicit val format: OFormat[SignatureHelp] = Json.format[SignatureHelp] -} case object ExecuteCommandResult -case class WorkspaceSymbolResult(params: Seq[SymbolInformation]) -object WorkspaceSymbolResult { - implicit val format: OFormat[WorkspaceSymbolResult] = Json.format[WorkspaceSymbolResult] -} - +@JsonCodec case class WorkspaceSymbolResult(params: Seq[SymbolInformation]) // Errors case class InvalidParamsResponseError(message: String) extends Exception(message) diff --git a/languageserver/src/main/scala/langserver/types/enums.scala b/languageserver/src/main/scala/langserver/types/enums.scala index d882a602750..04b0a2c5e22 100644 --- a/languageserver/src/main/scala/langserver/types/enums.scala +++ b/languageserver/src/main/scala/langserver/types/enums.scala @@ -4,7 +4,7 @@ import enumeratum.values._ sealed abstract class DiagnosticSeverity(val value: Int) extends IntEnumEntry -case object DiagnosticSeverity extends IntEnum[DiagnosticSeverity] with IntPlayJsonValueEnum[DiagnosticSeverity] { +case object DiagnosticSeverity extends IntEnum[DiagnosticSeverity] with IntCirceEnum[DiagnosticSeverity] { case object Error extends DiagnosticSeverity(1) case object Warning extends DiagnosticSeverity(2) @@ -16,7 +16,7 @@ case object DiagnosticSeverity extends IntEnum[DiagnosticSeverity] with IntPlayJ sealed abstract class CompletionItemKind(val value: Int) extends IntEnumEntry -case object CompletionItemKind extends IntEnum[CompletionItemKind] with IntPlayJsonValueEnum[CompletionItemKind] { +case object CompletionItemKind extends IntEnum[CompletionItemKind] with IntCirceEnum[CompletionItemKind] { case object Text extends CompletionItemKind(1) case object Method extends CompletionItemKind(2) @@ -42,7 +42,7 @@ case object CompletionItemKind extends IntEnum[CompletionItemKind] with IntPlayJ sealed abstract class DocumentHighlightKind(val value: Int) extends IntEnumEntry -case object DocumentHighlightKind extends IntEnum[DocumentHighlightKind] with IntPlayJsonValueEnum[DocumentHighlightKind] { +case object DocumentHighlightKind extends IntEnum[DocumentHighlightKind] with IntCirceEnum[DocumentHighlightKind] { /** A textual occurrence */ case object Text extends DocumentHighlightKind(1) /** Read-access of a symbol, like reading a variable */ @@ -55,7 +55,7 @@ case object DocumentHighlightKind extends IntEnum[DocumentHighlightKind] with In sealed abstract class SymbolKind(val value: Int) extends IntEnumEntry -case object SymbolKind extends IntEnum[SymbolKind] with IntPlayJsonValueEnum[SymbolKind] { +case object SymbolKind extends IntEnum[SymbolKind] with IntCirceEnum[SymbolKind] { case object File extends SymbolKind(1) case object Module extends SymbolKind(2) @@ -81,7 +81,7 @@ case object SymbolKind extends IntEnum[SymbolKind] with IntPlayJsonValueEnum[Sym sealed abstract class MessageType(val value: Int) extends IntEnumEntry -case object MessageType extends IntEnum[MessageType] with IntPlayJsonValueEnum[MessageType] { +case object MessageType extends IntEnum[MessageType] with IntCirceEnum[MessageType] { /** An error message. */ case object Error extends MessageType(1) @@ -97,7 +97,7 @@ case object MessageType extends IntEnum[MessageType] with IntPlayJsonValueEnum[M sealed abstract class TextDocumentSyncKind(val value: Int) extends IntEnumEntry -case object TextDocumentSyncKind extends IntEnum[TextDocumentSyncKind] with IntPlayJsonValueEnum[TextDocumentSyncKind] { +case object TextDocumentSyncKind extends IntEnum[TextDocumentSyncKind] with IntCirceEnum[TextDocumentSyncKind] { /** Documents should not be synced at all */ case object None extends TextDocumentSyncKind(0) @@ -113,7 +113,7 @@ case object TextDocumentSyncKind extends IntEnum[TextDocumentSyncKind] with IntP sealed abstract class FileChangeType(val value: Int) extends IntEnumEntry -case object FileChangeType extends IntEnum[FileChangeType] with IntPlayJsonValueEnum[FileChangeType] { +case object FileChangeType extends IntEnum[FileChangeType] with IntCirceEnum[FileChangeType] { case object Created extends FileChangeType(1) case object Changed extends FileChangeType(2) diff --git a/languageserver/src/main/scala/langserver/types/types.scala b/languageserver/src/main/scala/langserver/types/types.scala index 2caf60cc0de..7ab9b0f3c4d 100644 --- a/languageserver/src/main/scala/langserver/types/types.scala +++ b/languageserver/src/main/scala/langserver/types/types.scala @@ -1,26 +1,26 @@ package langserver.types -import play.api.libs.json._ -import play.api.libs.json.OFormat +import io.circe.Json +import io.circe.Decoder +import io.circe.Encoder +import io.circe.generic.JsonCodec +import cats.syntax.either._ /** * Position in a text document expressed as zero-based line and character offset. */ -case class Position(line: Int, character: Int) -object Position { implicit val format: OFormat[Position] = Json.format[Position] } +@JsonCodec case class Position(line: Int, character: Int) /** * A range in a text document. */ -case class Range(start: Position, end: Position) -object Range { implicit val format: OFormat[Range] = Json.format[Range] } +@JsonCodec case class Range(start: Position, end: Position) /** * Represents a location inside a resource, such as a line * inside a text file. */ -case class Location(uri: String, range: Range) -object Location { implicit val format: OFormat[Location] = Json.format[Location] } +@JsonCodec case class Location(uri: String, range: Range) /** * Represents a diagnostic, such as a compiler error or warning. Diagnostic objects are only valid @@ -32,16 +32,13 @@ object Location { implicit val format: OFormat[Location] = Json.format[Location] * @param source the source of this diagnostic (like 'typescript' or 'scala') * @param message the diagnostic message */ -case class Diagnostic( +@JsonCodec case class Diagnostic( range: Range, severity: Option[DiagnosticSeverity], code: Option[String], source: Option[String], message: String ) -object Diagnostic { - implicit val format: OFormat[Diagnostic] = Json.format[Diagnostic] -} /** * A reference to a command. @@ -50,39 +47,27 @@ object Diagnostic { * @param command The identifier of the actual command handler * @param arguments The arugments this command may be invoked with */ -case class Command(title: String, command: String, arguments: Seq[JsValue]) -object Command { - implicit val format: OFormat[Command] = Json.format[Command] -} +@JsonCodec case class Command(title: String, command: String, arguments: Seq[Json]) -case class TextEdit(range: Range, newText: String) - -object TextEdit { - implicit val formatter: OFormat[TextEdit] = Json.format[TextEdit] -} +@JsonCodec case class TextEdit(range: Range, newText: String) /** * A workspace edit represents changes to many resources managed * in the workspace. */ -case class WorkspaceEdit( +@JsonCodec case class WorkspaceEdit( changes: Map[String, Seq[TextEdit]] // uri -> changes ) -object WorkspaceEdit { - implicit val format: OFormat[WorkspaceEdit] = Json.format[WorkspaceEdit] -} -case class TextDocumentIdentifier(uri: String) -object TextDocumentIdentifier { implicit val format: OFormat[TextDocumentIdentifier] = Json.format[TextDocumentIdentifier] } +@JsonCodec case class TextDocumentIdentifier(uri: String) -case class VersionedTextDocumentIdentifier(uri: String, version: Long) -object VersionedTextDocumentIdentifier { implicit val format: OFormat[VersionedTextDocumentIdentifier] = Json.format[VersionedTextDocumentIdentifier] } +@JsonCodec case class VersionedTextDocumentIdentifier(uri: String, version: Long) /** * An item to transfer a text document from the client to the * server. */ -case class TextDocumentItem( +@JsonCodec case class TextDocumentItem( uri: String, languageId: String, /** @@ -90,13 +75,11 @@ case class TextDocumentItem( * change, including undo/redo). */ version: Long, - text: String) + text: String +) -object TextDocumentItem { - implicit val format: OFormat[TextDocumentItem] = Json.format[TextDocumentItem] -} -case class CompletionItem( +@JsonCodec case class CompletionItem( label: String, kind: Option[CompletionItemKind] = None, detail: Option[String] = None, @@ -105,94 +88,70 @@ case class CompletionItem( filterText: Option[String] = None, insertText: Option[String] = None, textEdit: Option[String] = None, - data: Option[String] = None) // An data entry field that is preserved on a completion item between -// a [CompletionRequest](#CompletionRequest) and a [CompletionResolveRequest] -// (#CompletionResolveRequest) - -object CompletionItem { - implicit def format: OFormat[CompletionItem] = Json.format[CompletionItem] -} - -trait MarkedString - -case class RawMarkedString(language: String, value: String) extends MarkedString { - def this(value: String) { - this("text", value) - } -} - -case class MarkdownString(contents: String) extends MarkedString + /** An data entry field that is preserved on a completion item between + * a [CompletionRequest](#CompletionRequest) and a [CompletionResolveRequest] + * (#CompletionResolveRequest) + */ + data: Option[String] = None +) +sealed trait MarkedString object MarkedString { - implicit val reads: Reads[MarkedString] = - Json.reads[RawMarkedString].map(x => x: MarkedString).orElse(Json.reads[MarkdownString].map(x => x: MarkedString)) - - implicit val writes: Writes[MarkedString] = Writes[MarkedString] { - case raw: RawMarkedString => Json.writes[RawMarkedString].writes(raw) - case md: MarkdownString => Json.writes[MarkdownString].writes(md) + implicit val encoder: Encoder[MarkedString] = { + case m: RawMarkedString => Encoder[RawMarkedString].apply(m) + case m: MarkdownString => Encoder[MarkdownString].apply(m) + } + implicit val decoder: Decoder[MarkedString] = Decoder.decodeJsonObject.emap { obj => + val json = Json.fromJsonObject(obj) + val result = + if (obj.contains("value")) json.as[RawMarkedString] + else json.as[MarkdownString] + result.leftMap(_.toString) } } +@JsonCodec case class RawMarkedString(language: String, value: String) extends MarkedString +@JsonCodec case class MarkdownString(contents: String) extends MarkedString -case class ParameterInformation(label: String, documentation: Option[String]) -object ParameterInformation { - implicit val format: OFormat[ParameterInformation] = Json.format[ParameterInformation] -} - -case class SignatureInformation(label: String, documentation: Option[String], parameters: Seq[ParameterInformation]) -object SignatureInformation { - implicit val format: OFormat[SignatureInformation] = Json.format[SignatureInformation] -} +@JsonCodec case class ParameterInformation(label: String, documentation: Option[String]) +@JsonCodec case class SignatureInformation(label: String, documentation: Option[String], parameters: Seq[ParameterInformation]) /** * Value-object that contains additional information when * requesting references. */ -case class ReferenceContext( +@JsonCodec case class ReferenceContext( /** Include the declaration of the current symbol. */ includeDeclaration: Boolean ) -object ReferenceContext { - implicit var format: OFormat[ReferenceContext] = Json.format[ReferenceContext] -} /** * A document highlight is a range inside a text document which deserves * special attention. Usually a document highlight is visualized by changing * the background color of its range. */ -case class DocumentHighlight( +@JsonCodec case class DocumentHighlight( /** The range this highlight applies to. */ range: Range, /** The highlight kind, default is [text](#DocumentHighlightKind.Text). */ - kind: DocumentHighlightKind = DocumentHighlightKind.Text) -object DocumentHighlight { - implicit val format: OFormat[DocumentHighlight] = Json.format[DocumentHighlight] -} + kind: DocumentHighlightKind = DocumentHighlightKind.Text +) -case class SymbolInformation( +@JsonCodec case class SymbolInformation( name: String, kind: SymbolKind, location: Location, - containerName: Option[String]) -object SymbolInformation { - implicit val format: OFormat[SymbolInformation] = Json.format[SymbolInformation] -} + containerName: Option[String] +) /** * The parameters of a [WorkspaceSymbolRequest](#WorkspaceSymbolRequest). */ -case class WorkspaceSymbolParams(query: String) -object WorkspaceSymbolParams { - implicit val format: OFormat[WorkspaceSymbolParams] = Json.format[WorkspaceSymbolParams] -} +@JsonCodec case class WorkspaceSymbolParams(query: String) -case class CodeActionContext(diagnostics: Seq[Diagnostic]) -object CodeActionContext { - implicit val format: OFormat[CodeActionContext] = Json.format[CodeActionContext] -} +@JsonCodec case class CodeActionContext(diagnostics: Seq[Diagnostic]) /** * A code lens represents a [command](#Command) that should be shown along with @@ -222,7 +181,7 @@ case class CodeLens( /** * Value-object describing what options formatting should use. */ -case class FormattingOptions( +@JsonCodec case class FormattingOptions( /** * Size of a tab in spaces. */ @@ -239,10 +198,6 @@ case class FormattingOptions( // params: Map[String, Any] // [key: string]: boolean | number | string; ) -object FormattingOptions { - implicit val formatter: OFormat[FormattingOptions] = Json.format[FormattingOptions] -} - trait TextDocument { /** * The associated URI for this document. Most documents have the __file__-scheme, indicating that they @@ -295,7 +250,7 @@ case class TextDocumentChangeEvent(document: TextDocument) * An event describing a change to a text document. If range and rangeLength are omitted * the new text is considered to be the full content of the document. */ -case class TextDocumentContentChangeEvent( +@JsonCodec case class TextDocumentContentChangeEvent( /** * The range of the document that changed. */ @@ -312,11 +267,7 @@ case class TextDocumentContentChangeEvent( text: String ) -object TextDocumentContentChangeEvent { - implicit val format: OFormat[TextDocumentContentChangeEvent] = Json.format[TextDocumentContentChangeEvent] -} - -case class DocumentFormattingParams( +@JsonCodec case class DocumentFormattingParams( /** * The document to format. */ @@ -328,15 +279,7 @@ case class DocumentFormattingParams( options: FormattingOptions ) -object DocumentFormattingParams { - implicit val format: OFormat[DocumentFormattingParams] = Json.format[DocumentFormattingParams] -} - -case class ExecuteCommandParams(command: String, arguments: Option[Seq[JsValue]]) -object ExecuteCommandParams { - implicit val format: OFormat[ExecuteCommandParams] = Json.format[ExecuteCommandParams] -} - +@JsonCodec case class ExecuteCommandParams(command: String, arguments: Option[Seq[Json]]) /** * An event describing a file change. @@ -344,16 +287,9 @@ object ExecuteCommandParams { * @param uri The file's URI * @param `type` The change type */ -case class FileEvent( +@JsonCodec case class FileEvent( uri: String, `type`: FileChangeType ) -object FileEvent { - implicit val format: OFormat[FileEvent] = Json.format[FileEvent] -} -case class CancelParams(id: JsValue) - -object CancelParams { - implicit val format: OFormat[CancelParams] = Json.format[CancelParams] -} +@JsonCodec case class CancelParams(id: Json) diff --git a/metaserver/src/main/scala/scala/meta/languageserver/Configuration.scala b/metaserver/src/main/scala/scala/meta/languageserver/Configuration.scala index 4cb7643186c..cf459317d57 100644 --- a/metaserver/src/main/scala/scala/meta/languageserver/Configuration.scala +++ b/metaserver/src/main/scala/scala/meta/languageserver/Configuration.scala @@ -1,16 +1,16 @@ package scala.meta.languageserver -import play.api.libs.json._ -import play.api.libs.functional.syntax._ - import org.langmeta.AbsolutePath import org.langmeta.RelativePath -import java.nio.file.Paths +import scala.util.Try +import io.circe.Encoder +import io.circe.Decoder +import io.circe.generic.extras.{ConfiguredJsonCodec => JsonCodec} +import io.circe.generic.extras.{Configuration => CirceConfiguration} import Configuration._ -import play.api.libs.json.OFormat -case class Configuration( +@JsonCodec case class Configuration( scalac: Scalac = Scalac(), scalafmt: Scalafmt = Scalafmt(), scalafix: Scalafix = Scalafix(), @@ -20,61 +20,34 @@ case class Configuration( ) object Configuration { + implicit val circeConfiguration: CirceConfiguration = CirceConfiguration.default.withDefaults - case class Scalac(enabled: Boolean = false) - case class Hover(enabled: Boolean = false) - case class Rename(enabled: Boolean = false) + @JsonCodec case class Scalac(enabled: Boolean = false) + @JsonCodec case class Hover(enabled: Boolean = false) + @JsonCodec case class Rename(enabled: Boolean = false) - case class Scalafmt( + @JsonCodec case class Scalafmt( enabled: Boolean = true, version: String = "1.3.0", confPath: Option[RelativePath] = None ) - case class Scalafix( + object Scalafmt { + lazy val defaultConfPath = RelativePath(".scalafmt.conf") + } + @JsonCodec case class Scalafix( enabled: Boolean = true, confPath: RelativePath = RelativePath(".scalafix.conf") ) // TODO(olafur): re-enable indexJDK after https://github.com/scalameta/language-server/issues/43 is fixed - case class Search(indexJDK: Boolean = false, indexClasspath: Boolean = true) + @JsonCodec case class Search(indexJDK: Boolean = false, indexClasspath: Boolean = true) - implicit val absolutePathReads: Reads[AbsolutePath] = - Reads.StringReads - .filter(s => Paths.get(s).isAbsolute) - .map(AbsolutePath(_)) - implicit val absolutePathWrites: Writes[AbsolutePath] = - Writes.StringWrites.contramap(_.toString) - implicit val relativePathReads: Reads[RelativePath] = - Reads.StringReads.map(RelativePath(_)) - implicit val relativePathWrites: Writes[RelativePath] = - Writes.StringWrites.contramap(_.toString) + implicit val absolutePathReads: Decoder[AbsolutePath] = + Decoder.decodeString.emapTry(s => Try(AbsolutePath(s))) + implicit val absolutePathWrites: Encoder[AbsolutePath] = + Encoder.encodeString.contramap(_.toString) + implicit val relativePathReads: Decoder[RelativePath] = + Decoder.decodeString.emapTry(s => Try(RelativePath(s))) + implicit val relativePathWrites: Encoder[RelativePath] = + Encoder.encodeString.contramap(_.toString) - // TODO(gabro): Json.format[A] is tedious to write. - // We should use an annotation macro to cut the boilerplate - object Scalac { - implicit val format: OFormat[Scalac] = - Json.using[Json.WithDefaultValues].format[Scalac] - } - object Hover { - implicit val format: OFormat[Hover] = - Json.using[Json.WithDefaultValues].format[Hover] - } - object Rename { - implicit val format: OFormat[Rename] = - Json.using[Json.WithDefaultValues].format[Rename] - } - object Scalafmt { - lazy val defaultConfPath = RelativePath(".scalafmt.conf") - implicit val format: OFormat[Scalafmt] = - Json.using[Json.WithDefaultValues].format[Scalafmt] - } - object Scalafix { - implicit val format: OFormat[Scalafix] = - Json.using[Json.WithDefaultValues].format[Scalafix] - } - object Search { - implicit val format: OFormat[Search] = - Json.using[Json.WithDefaultValues].format[Search] - } - implicit val format: OFormat[Configuration] = - Json.using[Json.WithDefaultValues].format[Configuration] } diff --git a/metaserver/src/main/scala/scala/meta/languageserver/PlayJsonEnrichments.scala b/metaserver/src/main/scala/scala/meta/languageserver/PlayJsonEnrichments.scala deleted file mode 100644 index 6fafd6c47c5..00000000000 --- a/metaserver/src/main/scala/scala/meta/languageserver/PlayJsonEnrichments.scala +++ /dev/null @@ -1,15 +0,0 @@ -package scala.meta.languageserver - -import play.api.libs.json.JsError - -object PlayJsonEnrichments { - implicit class XtensionPlayJsonError(val jsError: JsError) extends AnyVal { - def show: String = - jsError.errors.iterator - .map { - case (path, err) => - s"$path: ${err.mkString(", ")}" - } - .mkString("; ") - } -} diff --git a/metaserver/src/main/scala/scala/meta/languageserver/ScalametaLanguageServer.scala b/metaserver/src/main/scala/scala/meta/languageserver/ScalametaLanguageServer.scala index ad4e0112929..28336321fbe 100644 --- a/metaserver/src/main/scala/scala/meta/languageserver/ScalametaLanguageServer.scala +++ b/metaserver/src/main/scala/scala/meta/languageserver/ScalametaLanguageServer.scala @@ -8,7 +8,6 @@ import java.nio.file.SimpleFileVisitor import java.nio.file.attribute.BasicFileAttributes import java.util.concurrent.Executors import scala.concurrent.duration.FiniteDuration -import scala.meta.languageserver.PlayJsonEnrichments._ import scala.meta.languageserver.compiler.CompilerConfig import scala.meta.languageserver.compiler.Cursor import scala.meta.languageserver.compiler.ScalacProvider @@ -37,10 +36,7 @@ import org.langmeta.internal.semanticdb.schema import org.langmeta.io.AbsolutePath import org.langmeta.languageserver.InputEnrichments._ import org.langmeta.semanticdb -import play.api.libs.json.JsError -import play.api.libs.json.JsNull -import play.api.libs.json.JsSuccess -import play.api.libs.json.JsValue +import io.circe.Json class ScalametaLanguageServer( cwd: AbsolutePath, @@ -165,11 +161,11 @@ class ScalametaLanguageServer( .requestAsync[InitializeParams, InitializeResult]("initialize") { params => initialize(params) } - .request[JsValue, JsValue]("shutdown") { _ => + .request[Json, Json]("shutdown") { _ => shutdown() - JsNull + Json.Null } - .notification[JsValue]("exit") { _ => + .notification[Json]("exit") { _ => logger.info("exit(0)") sys.exit(0) } @@ -236,13 +232,13 @@ class ScalametaLanguageServer( .notification[DidChangeConfigurationParams]( "workspace/didChangeConfiguration" ) { params => - (params.settings \ "scalameta").validate[Configuration] match { - case err: JsError => - client.showMessage(MessageType.Error, err.show) - case JsSuccess(conf, _) => - logger.info(s"Configuration updated $conf") - configurationSubscriber.onNext(conf) - } + params.settings.hcursor.downField("scalameta").as[Configuration]// match { + // case Left(err) => + // client.showMessage(MessageType.Error, err.show) + // case Right(conf) => + // logger.info(s"Configuration updated $conf") + // configurationSubscriber.onNext(conf) + // } } .notification[DidChangeWatchedFilesParams]( "workspace/didChangeWatchedFiles" @@ -323,12 +319,12 @@ class ScalametaLanguageServer( case None => SignatureHelpProvider.empty } } - .requestAsync[ExecuteCommandParams, JsValue]( + .requestAsync[ExecuteCommandParams, Json]( "workspace/executeCommand" ) { params => logger.info(s"executeCommand $params") import WorkspaceCommand._ - val ok = Task(Right(JsNull)) + val ok = Task(Right(Json.Null)) WorkspaceCommand.withNameOption(params.command) match { case None => val msg = s"Unknown command ${params.command}" @@ -362,7 +358,7 @@ class ScalametaLanguageServer( applied match { case Left(err) => logger.warn(s"Failed to apply command $err") - Right(JsNull) + Right(Json.Null) case Right(edit) => if (edit.applied) { logger.info(s"Successfully applied command $params") @@ -373,7 +369,7 @@ class ScalametaLanguageServer( } applied } - response.map(_ => Right(JsNull)) + response.map(_ => Right(Json.Null)) } } .request[WorkspaceSymbolParams, List[SymbolInformation]]( diff --git a/metaserver/src/main/scala/scala/meta/languageserver/protocol/ErrorCode.scala b/metaserver/src/main/scala/scala/meta/languageserver/protocol/ErrorCode.scala index 22a1955eeff..895c2450fb3 100644 --- a/metaserver/src/main/scala/scala/meta/languageserver/protocol/ErrorCode.scala +++ b/metaserver/src/main/scala/scala/meta/languageserver/protocol/ErrorCode.scala @@ -3,12 +3,12 @@ package scala.meta.languageserver.protocol import scala.collection.immutable.IndexedSeq import enumeratum.values.IntEnum import enumeratum.values.IntEnumEntry -import enumeratum.values.IntPlayJsonValueEnum +import enumeratum.values.IntCirceEnum sealed abstract class ErrorCode(val value: Int) extends IntEnumEntry case object ErrorCode extends IntEnum[ErrorCode] - with IntPlayJsonValueEnum[ErrorCode] { + with IntCirceEnum[ErrorCode] { case object ParseError extends ErrorCode(-32700) case object InvalidRequest extends ErrorCode(-32600) case object MethodNotFound extends ErrorCode(-32601) diff --git a/metaserver/src/main/scala/scala/meta/languageserver/protocol/ErrorObject.scala b/metaserver/src/main/scala/scala/meta/languageserver/protocol/ErrorObject.scala index a5e1fc6193c..1d1b6a69e92 100644 --- a/metaserver/src/main/scala/scala/meta/languageserver/protocol/ErrorObject.scala +++ b/metaserver/src/main/scala/scala/meta/languageserver/protocol/ErrorObject.scala @@ -1,11 +1,6 @@ package scala.meta.languageserver.protocol -import play.api.libs.json.JsValue -import play.api.libs.json.Json -import play.api.libs.json.OFormat +import io.circe.Json +import io.circe.generic.JsonCodec -case class ErrorObject(code: ErrorCode, message: String, data: Option[JsValue]) - -object ErrorObject { - implicit val format: OFormat[ErrorObject] = Json.format[ErrorObject] -} +@JsonCodec case class ErrorObject(code: ErrorCode, message: String, data: Option[Json]) diff --git a/metaserver/src/main/scala/scala/meta/languageserver/protocol/LanguageClient.scala b/metaserver/src/main/scala/scala/meta/languageserver/protocol/LanguageClient.scala index a1e13bdab3e..d713f707229 100644 --- a/metaserver/src/main/scala/scala/meta/languageserver/protocol/LanguageClient.scala +++ b/metaserver/src/main/scala/scala/meta/languageserver/protocol/LanguageClient.scala @@ -17,19 +17,17 @@ import monix.eval.Task import monix.execution.Cancelable import monix.execution.atomic.Atomic import monix.execution.atomic.AtomicInt -import play.api.libs.json.JsError -import play.api.libs.json.JsSuccess -import play.api.libs.json.Json -import play.api.libs.json.Reads -import play.api.libs.json.Writes +import io.circe.Encoder +import io.circe.Decoder +import cats.syntax.either._ class LanguageClient(out: OutputStream) extends LazyLogging with Notifications { private val writer = new MessageWriter(out) private val counter: AtomicInt = Atomic(1) private val activeServerRequests = TrieMap.empty[RequestId, Callback[Response]] - def notify[A: Writes](method: String, notification: A): Unit = - writer.write(Notification(method, Some(Json.toJson(notification)))) + def notify[A](method: String, notification: A)(implicit encode: Encoder[A]): Unit = + writer.write(Notification(method, Some(encode(notification)))) def serverRespond(response: Response): Unit = response match { case Response.Empty => () case x: Response.Success => writer.write(x) @@ -51,14 +49,14 @@ class LanguageClient(out: OutputStream) extends LazyLogging with Notifications { callback.onSuccess(response) } - def request[A: Writes, B: Reads]( + def request[A, B]( method: String, request: A - ): Task[Either[Response.Error, B]] = { + )(implicit encode: Encoder[A], decode: Decoder[B]): Task[Either[Response.Error, B]] = { val nextId = RequestId(counter.incrementAndGet()) val response = Task.create[Response] { (out, cb) => val scheduled = out.scheduleOnce(Duration(0, "s")) { - val json = Request(method, Some(Json.toJson(request)), nextId) + val json = Request(method, Some(encode(request)), nextId) activeServerRequests.put(nextId, cb) writer.write(json) } @@ -78,16 +76,8 @@ class LanguageClient(out: OutputStream) extends LazyLogging with Notifications { case err: Response.Error => Left(err) case Response.Success(result, _) => - result.validate[B] match { - case JsSuccess(value, _) => - Right(value) - case err: JsError => - Left( - Response.invalidParams( - Json.prettyPrint(JsError.toJson(err)), - nextId - ) - ) + result.as[B].leftMap { err => + Response.invalidParams(err.toString, nextId) } } } diff --git a/metaserver/src/main/scala/scala/meta/languageserver/protocol/LanguageServer.scala b/metaserver/src/main/scala/scala/meta/languageserver/protocol/LanguageServer.scala index 5f446d5aa4f..96875da8251 100644 --- a/metaserver/src/main/scala/scala/meta/languageserver/protocol/LanguageServer.scala +++ b/metaserver/src/main/scala/scala/meta/languageserver/protocol/LanguageServer.scala @@ -4,17 +4,15 @@ import scala.collection.concurrent.TrieMap import scala.concurrent.Await import scala.concurrent.duration.Duration import scala.util.control.NonFatal -import com.fasterxml.jackson.core.JsonParseException import com.typesafe.scalalogging.LazyLogging import langserver.types.CancelParams import monix.eval.Task import monix.execution.Cancelable import monix.execution.Scheduler import monix.reactive.Observable -import play.api.libs.json.JsError -import play.api.libs.json.JsSuccess -import play.api.libs.json.JsValue -import play.api.libs.json.Json +import io.circe.Json +import io.circe.parser.parse +import io.circe.syntax._ final class LanguageServer( in: Observable[BaseProtocolMessage], @@ -22,7 +20,7 @@ final class LanguageServer( services: Services, requestScheduler: Scheduler ) extends LazyLogging { - private val activeClientRequests: TrieMap[JsValue, Cancelable] = TrieMap.empty + private val activeClientRequests: TrieMap[Json, Cancelable] = TrieMap.empty private val cancelNotification = Service.notification[CancelParams]("$/cancelRequest") { params => val id = params.id @@ -79,19 +77,19 @@ final class LanguageServer( Response.internalError(e.getMessage, request.id) } val runningResponse = response.runAsync(requestScheduler) - activeClientRequests.put(Json.toJson(request.id), runningResponse) + activeClientRequests.put(request.id.asJson, runningResponse) Task.fromFuture(runningResponse) } } def handleMessage(message: BaseProtocolMessage): Task[Response] = - LanguageServer.parseMessage(message) match { - case Left(parseError) => Task.now(parseError) + parse(message.content) match { + case Left(err) => Task.now(Response.parseError(err.toString)) case Right(json) => - json.validate[Message] match { - case err: JsError => Task.now(Response.invalidRequest(err.toString)) - case JsSuccess(msg, _) => handleValidMessage(msg) + json.as[Message] match { + case Left(err) => Task.now(Response.invalidRequest(err.toString)) + case Right(msg) => handleValidMessage(msg) } } @@ -112,13 +110,3 @@ final class LanguageServer( Await.result(f, Duration.Inf) } } - -object LanguageServer { - def parseMessage(message: BaseProtocolMessage): Either[Response, JsValue] = - try { - Right(Json.parse(message.content)) - } catch { - case e: JsonParseException => - Left(Response.parseError(e.getMessage)) - } -} diff --git a/metaserver/src/main/scala/scala/meta/languageserver/protocol/Message.scala b/metaserver/src/main/scala/scala/meta/languageserver/protocol/Message.scala index 4e639e1a4c8..f81d3e8e9c5 100644 --- a/metaserver/src/main/scala/scala/meta/languageserver/protocol/Message.scala +++ b/metaserver/src/main/scala/scala/meta/languageserver/protocol/Message.scala @@ -1,55 +1,45 @@ package scala.meta.languageserver.protocol import monix.eval.Task -import play.api.libs.json._ +import io.circe.Json +import io.circe.Decoder +import io.circe.generic.JsonCodec +import cats.syntax.either._ sealed trait Message object Message { - implicit val reads: Reads[Message] = Reads { - case json @ JsObject(map) => - if (map.contains("id")) { - if (map.contains("error")) json.validate[Response.Error] - else if (map.contains("result")) json.validate[Response.Success] - else json.validate[Request] - } else json.validate[Notification] - case els => - JsError(s"Expected object, obtained $els") + implicit val decoder: Decoder[Message] = Decoder.decodeJsonObject.emap { obj => + val json = Json.fromJsonObject(obj) + val result = if (obj.contains("id")) + if (obj.contains("error")) json.as[Response.Error] + else if (obj.contains("result")) json.as[Response.Success] + else json.as[Request] + else json.as[Notification] + result.leftMap(_.toString) } } -case class Request(method: String, params: Option[JsValue], id: RequestId) +@JsonCodec case class Request(method: String, params: Option[Json], id: RequestId) extends Message { def toError(code: ErrorCode, message: String): Response = Response.error(ErrorObject(code, message, None), id) } -object Request { - implicit val format: OFormat[Request] = Json.format[Request] -} -case class Notification(method: String, params: Option[JsValue]) extends Message -object Notification { - implicit val format: OFormat[Notification] = Json.format[Notification] -} +@JsonCodec case class Notification(method: String, params: Option[Json]) extends Message -sealed trait Response extends Message { +@JsonCodec sealed trait Response extends Message { def isSuccess: Boolean = this.isInstanceOf[Response.Success] } object Response { - case class Success(result: JsValue, id: RequestId) extends Response - object Success { - implicit val format: OFormat[Success] = Json.format[Success] - } - case class Error(error: ErrorObject, id: RequestId) extends Response - object Error { - implicit val format: OFormat[Error] = Json.format[Error] - } + @JsonCodec case class Success(result: Json, id: RequestId) extends Response + @JsonCodec case class Error(error: ErrorObject, id: RequestId) extends Response case object Empty extends Response def empty: Response = Empty - def ok(result: JsValue, id: RequestId): Response = + def ok(result: Json, id: RequestId): Response = success(result, id) def okAsync[T](value: T): Task[Either[Response.Error, T]] = Task(Right(value)) - def success(result: JsValue, id: RequestId): Response = + def success(result: Json, id: RequestId): Response = Success(result, id) def error(error: ErrorObject, id: RequestId): Response.Error = Error(error, id) @@ -65,10 +55,10 @@ object Response { ErrorObject(ErrorCode.InvalidRequest, message, None), RequestId.Null ) - def cancelled(id: JsValue): Response.Error = + def cancelled(id: Json): Response.Error = Error( ErrorObject(ErrorCode.RequestCancelled, "", None), - id.asOpt[RequestId].getOrElse(RequestId.Null) + id.as[RequestId].getOrElse(RequestId.Null) ) def parseError(message: String): Response.Error = Error(ErrorObject(ErrorCode.ParseError, message, None), RequestId.Null) diff --git a/metaserver/src/main/scala/scala/meta/languageserver/protocol/RequestId.scala b/metaserver/src/main/scala/scala/meta/languageserver/protocol/RequestId.scala index fa5fd43e87d..e6125339225 100644 --- a/metaserver/src/main/scala/scala/meta/languageserver/protocol/RequestId.scala +++ b/metaserver/src/main/scala/scala/meta/languageserver/protocol/RequestId.scala @@ -1,25 +1,25 @@ package scala.meta.languageserver.protocol -import play.api.libs.json._ +import io.circe.Json +import io.circe.Decoder +import io.circe.Encoder sealed trait RequestId object RequestId { def apply(n: Int): RequestId.Number = - RequestId.Number(JsNumber(BigDecimal(n))) - implicit val format: Format[RequestId] = Format[RequestId]( - Reads { - case num: JsNumber => JsSuccess(Number(num)) - case JsNull => JsSuccess(Null) - case value: JsString => JsSuccess(String(value)) - case els => JsError(s"Expected number, string or null. Obtained $els") - }, - Writes { - case Number(value) => value - case String(value) => value - case Null => JsNull - } - ) - case class Number(value: JsNumber) extends RequestId - case class String(value: JsString) extends RequestId + RequestId.Number(Json.fromBigDecimal(BigDecimal(n))) + implicit val decoder: Decoder[RequestId] = Decoder.decodeJson.map { + case s if s.isString => RequestId.String(s) + case n if n.isNumber => RequestId.Number(n) + case n if n.isNull => RequestId.Null + } + implicit val encoder: Encoder[RequestId] = Encoder.encodeJson.contramap { + case RequestId.Number(v) => v + case RequestId.String(v) => v + case RequestId.Null => Json.Null + } + // implicit val encoder: Decoder = + case class Number(value: Json) extends RequestId + case class String(value: Json) extends RequestId case object Null extends RequestId } diff --git a/metaserver/src/main/scala/scala/meta/languageserver/protocol/Services.scala b/metaserver/src/main/scala/scala/meta/languageserver/protocol/Services.scala index 9803b378ae4..eef010d67ce 100644 --- a/metaserver/src/main/scala/scala/meta/languageserver/protocol/Services.scala +++ b/metaserver/src/main/scala/scala/meta/languageserver/protocol/Services.scala @@ -1,9 +1,11 @@ package scala.meta.languageserver.protocol -import scala.meta.languageserver.PlayJsonEnrichments._ import com.typesafe.scalalogging.LazyLogging import monix.eval.Task -import play.api.libs.json._ +import io.circe.Decoder +import io.circe.Encoder +import io.circe.Json +import io.circe.syntax._ trait Service[A, B] { def handle(request: A): Task[B] @@ -17,18 +19,18 @@ trait NamedJsonRpcService extends JsonRpcService with MethodName object Service extends LazyLogging { - def request[A: Reads, B: Writes](method: String)( + def request[A: Decoder, B: Encoder](method: String)( f: Service[A, Either[Response.Error, B]] ): NamedJsonRpcService = new NamedJsonRpcService { override def methodName: String = method override def handle(message: Message): Task[Response] = message match { case Request(`method`, params, id) => - params.getOrElse(JsNull).validate[A] match { - case err: JsError => - Task(Response.invalidParams(err.show, id)) - case JsSuccess(value, _) => + params.getOrElse(Json.Null).as[A] match { + case Left(err) => + Task(Response.invalidParams(err.toString, id)) + case Right(value) => f.handle(value).map { - case Right(response) => Response.ok(Json.toJson(response), id) + case Right(response) => Response.ok(response.asJson, id) // Service[A, ...] doesn't have access to the request ID so // by convention it's OK to set the ID to null by default // and we fill it in here instead. @@ -42,7 +44,7 @@ object Service extends LazyLogging { } } - def notification[A: Reads](method: String)( + def notification[A: Decoder](method: String)( f: Service[A, Unit] ): NamedJsonRpcService = new NamedJsonRpcService { @@ -53,10 +55,10 @@ object Service extends LazyLogging { } override def handle(message: Message): Task[Response] = message match { case Notification(`method`, params) => - params.getOrElse(JsNull).validate[A] match { - case err: JsError => + params.getOrElse(Json.Null).as[A] match { + case Left(err) => fail(s"Failed to parse notification $message. Errors: $err") - case JsSuccess(value, _) => + case Right(value) => f.handle(value).map(_ => Response.empty) } case Notification(invalidMethod, _) => @@ -73,22 +75,22 @@ object Services { class Services private (val services: List[NamedJsonRpcService]) { - def request[A: Reads, B: Writes](method: String)( + def request[A: Decoder, B: Encoder](method: String)( f: A => B ): Services = requestAsync[A, B](method)(request => Task(Right(f(request)))) - def requestAsync[A: Reads, B: Writes](method: String)( + def requestAsync[A: Decoder, B: Encoder](method: String)( f: Service[A, Either[Response.Error, B]] ): Services = addService(Service.request[A, B](method)(f)) - def notification[A: Reads](method: String)( + def notification[A: Decoder](method: String)( f: A => Unit ): Services = notificationAsync[A](method)(request => Task(f(request))) - def notificationAsync[A: Reads](method: String)( + def notificationAsync[A: Decoder](method: String)( f: Service[A, Unit] ): Services = addService(Service.notification[A](method)(f)) diff --git a/metaserver/src/main/scala/scala/meta/languageserver/providers/CodeActionProvider.scala b/metaserver/src/main/scala/scala/meta/languageserver/providers/CodeActionProvider.scala index 03dae023149..16ce68040b5 100644 --- a/metaserver/src/main/scala/scala/meta/languageserver/providers/CodeActionProvider.scala +++ b/metaserver/src/main/scala/scala/meta/languageserver/providers/CodeActionProvider.scala @@ -5,7 +5,7 @@ import com.typesafe.scalalogging.LazyLogging import langserver.messages.CodeActionParams import langserver.types.Command import langserver.types.Diagnostic -import play.api.libs.json.Json +import io.circe.syntax._ object CodeActionProvider extends LazyLogging { def codeActions(params: CodeActionParams): List[Command] = { @@ -14,7 +14,7 @@ object CodeActionProvider extends LazyLogging { Command( "Remove unused imports", ScalafixUnusedImports.entryName, - Json.toJson(params.textDocument) :: Nil + params.textDocument.asJson :: Nil ) }.toList } diff --git a/metaserver/src/main/scala/scala/meta/languageserver/refactoring/OrganizeImports.scala b/metaserver/src/main/scala/scala/meta/languageserver/refactoring/OrganizeImports.scala index dde4c02b530..04d677762fa 100644 --- a/metaserver/src/main/scala/scala/meta/languageserver/refactoring/OrganizeImports.scala +++ b/metaserver/src/main/scala/scala/meta/languageserver/refactoring/OrganizeImports.scala @@ -1,6 +1,6 @@ package scala.meta.languageserver.refactoring -import scala.meta._ +import scala.meta.Document import scala.meta.languageserver.Parser import scala.meta.languageserver.Uri import scala.meta.languageserver.search.SymbolIndex @@ -16,21 +16,20 @@ import langserver.messages.ApplyWorkspaceEditParams import langserver.types.TextDocumentIdentifier import langserver.types.WorkspaceEdit import monix.eval.Task -import play.api.libs.json.JsValue -import play.api.libs.json.Json +import io.circe.Json object OrganizeImports extends LazyLogging { val empty = ApplyWorkspaceEditParams(None, WorkspaceEdit(Map.empty)) def removeUnused( - arguments: Option[Seq[JsValue]], + arguments: Option[Seq[Json]], index: SymbolIndex ): Task[Either[Response.Error, ApplyWorkspaceEditParams]] = { val result = for { as <- arguments argument <- as.headOption - textDocument <- Json.fromJson[TextDocumentIdentifier](argument).asOpt + textDocument <- argument.as[TextDocumentIdentifier].toOption } yield removeUnused(Uri(textDocument), index) Task { result match { diff --git a/metaserver/src/test/scala/tests/compiler/CompletionsTest.scala b/metaserver/src/test/scala/tests/compiler/CompletionsTest.scala index aa8644bda54..0ac99e09837 100644 --- a/metaserver/src/test/scala/tests/compiler/CompletionsTest.scala +++ b/metaserver/src/test/scala/tests/compiler/CompletionsTest.scala @@ -3,7 +3,7 @@ package tests.compiler import scala.meta.languageserver.providers.CompletionProvider import langserver.messages.CompletionList import langserver.types.CompletionItemKind -import play.api.libs.json.Json +import io.circe.syntax._ object CompletionsTest extends CompilerSuite { @@ -29,7 +29,7 @@ object CompletionsTest extends CompilerSuite { check( filename, code, { completions => - val obtained = Json.prettyPrint(Json.toJson(completions)) + val obtained = completions.asJson.spaces2 assertNoDiff(obtained, expected) } ) @@ -48,12 +48,19 @@ object CompletionsTest extends CompilerSuite { s""" |{ | "isIncomplete" : false, - | "items" : [ { - | "label" : "$label", - | "kind" : ${kind.value}, - | "detail" : "$detail", - | "sortText" : "00000" - | } ] + | "items" : [ + | { + | "label" : "$label", + | "kind" : ${kind.value}, + | "detail" : "$detail", + | "documentation" : null, + | "sortText" : "00000", + | "filterText" : null, + | "insertText" : null, + | "textEdit" : null, + | "data" : null + | } + | ] |} """.stripMargin ) @@ -67,7 +74,8 @@ object CompletionsTest extends CompilerSuite { """ |{ | "isIncomplete" : false, - | "items" : [ ] + | "items" : [ + | ] |} """.stripMargin ) diff --git a/metaserver/src/test/scala/tests/compiler/SignatureHelpTest.scala b/metaserver/src/test/scala/tests/compiler/SignatureHelpTest.scala index 942d5e86dc6..829ff00f091 100644 --- a/metaserver/src/test/scala/tests/compiler/SignatureHelpTest.scala +++ b/metaserver/src/test/scala/tests/compiler/SignatureHelpTest.scala @@ -2,7 +2,7 @@ package tests.compiler import scala.meta.languageserver.providers.SignatureHelpProvider import langserver.messages.SignatureHelp -import play.api.libs.json.Json +import io.circe.syntax._ object SignatureHelpTest extends CompilerSuite { @@ -26,7 +26,7 @@ object SignatureHelpTest extends CompilerSuite { expected: String ): Unit = { check(filename, code, { result => - val obtained = Json.prettyPrint(Json.toJson(result)) + val obtained = result.asJson.spaces2 assertNoDiff(obtained, expected) }) } @@ -40,19 +40,33 @@ object SignatureHelpTest extends CompilerSuite { """.stripMargin, """ |{ - | "signatures" : [ { - | "label" : "assert(assertion: Boolean, message: => Any)Unit", - | "parameters" : [ { - | "label" : "assertion: Boolean" - | }, { - | "label" : "message: => Any" - | } ] - | }, { - | "label" : "assert(assertion: Boolean)Unit", - | "parameters" : [ { - | "label" : "assertion: Boolean" - | } ] - | } ], + | "signatures" : [ + | { + | "label" : "assert(assertion: Boolean, message: => Any)Unit", + | "documentation" : null, + | "parameters" : [ + | { + | "label" : "assertion: Boolean", + | "documentation" : null + | }, + | { + | "label" : "message: => Any", + | "documentation" : null + | } + | ] + | }, + | { + | "label" : "assert(assertion: Boolean)Unit", + | "documentation" : null, + | "parameters" : [ + | { + | "label" : "assertion: Boolean", + | "documentation" : null + | } + | ] + | } + | ], + | "activeSignature" : null, | "activeParameter" : 0 |}""".stripMargin ) @@ -66,14 +80,23 @@ object SignatureHelpTest extends CompilerSuite { """.stripMargin, """ |{ - | "signatures" : [ { - | "label" : "assert(assertion: Boolean, message: => Any)Unit", - | "parameters" : [ { - | "label" : "assertion: Boolean" - | }, { - | "label" : "message: => Any" - | } ] - | } ], + | "signatures" : [ + | { + | "label" : "assert(assertion: Boolean, message: => Any)Unit", + | "documentation" : null, + | "parameters" : [ + | { + | "label" : "assertion: Boolean", + | "documentation" : null + | }, + | { + | "label" : "message: => Any", + | "documentation" : null + | } + | ] + | } + | ], + | "activeSignature" : null, | "activeParameter" : 1 |} """.stripMargin @@ -117,14 +140,23 @@ object SignatureHelpTest extends CompilerSuite { """.stripMargin, """ |{ - | "signatures" : [ { - | "label" : "apply(name: String, age: Int)User", - | "parameters" : [ { - | "label" : "name: String" - | }, { - | "label" : "age: Int" - | } ] - | } ], + | "signatures" : [ + | { + | "label" : "apply(name: String, age: Int)User", + | "documentation" : null, + | "parameters" : [ + | { + | "label" : "name: String", + | "documentation" : null + | }, + | { + | "label" : "age: Int", + | "documentation" : null + | } + | ] + | } + | ], + | "activeSignature" : null, | "activeParameter" : 1 |} """.stripMargin @@ -139,12 +171,19 @@ object SignatureHelpTest extends CompilerSuite { """.stripMargin, """ |{ - | "signatures" : [ { - | "label" : "apply[A](xs: A*)List[A]", - | "parameters" : [ { - | "label" : "xs: A*" - | } ] - | } ], + | "signatures" : [ + | { + | "label" : "apply[A](xs: A*)List[A]", + | "documentation" : null, + | "parameters" : [ + | { + | "label" : "xs: A*", + | "documentation" : null + | } + | ] + | } + | ], + | "activeSignature" : null, | "activeParameter" : 0 |} """.stripMargin @@ -159,12 +198,19 @@ object SignatureHelpTest extends CompilerSuite { """.stripMargin, """ |{ - | "signatures" : [ { - | "label" : "apply[A](xs: A*)List[A]", - | "parameters" : [ { - | "label" : "xs: A*" - | } ] - | } ], + | "signatures" : [ + | { + | "label" : "apply[A](xs: A*)List[A]", + | "documentation" : null, + | "parameters" : [ + | { + | "label" : "xs: A*", + | "documentation" : null + | } + | ] + | } + | ], + | "activeSignature" : null, | "activeParameter" : 0 |} """.stripMargin @@ -181,7 +227,9 @@ object SignatureHelpTest extends CompilerSuite { """.stripMargin, """ |{ - | "signatures" : [ ], + | "signatures" : [ + | ], + | "activeSignature" : null, | "activeParameter" : 0 |} """.stripMargin @@ -199,19 +247,33 @@ object SignatureHelpTest extends CompilerSuite { """.stripMargin, """ |{ - | "signatures" : [ { - | "label" : "(name: String)User", - | "parameters" : [ { - | "label" : "name: String" - | } ] - | }, { - | "label" : "(name: String, age: Int)User", - | "parameters" : [ { - | "label" : "name: String" - | }, { - | "label" : "age: Int" - | } ] - | } ], + | "signatures" : [ + | { + | "label" : "(name: String)User", + | "documentation" : null, + | "parameters" : [ + | { + | "label" : "name: String", + | "documentation" : null + | } + | ] + | }, + | { + | "label" : "(name: String, age: Int)User", + | "documentation" : null, + | "parameters" : [ + | { + | "label" : "name: String", + | "documentation" : null + | }, + | { + | "label" : "age: Int", + | "documentation" : null + | } + | ] + | } + | ], + | "activeSignature" : null, | "activeParameter" : 0 |} """.stripMargin @@ -236,12 +298,19 @@ object SignatureHelpTest extends CompilerSuite { """.stripMargin, """ |{ - | "signatures" : [ { - | "label" : "apply[A](xs: A*)List[A]", - | "parameters" : [ { - | "label" : "xs: A*" - | } ] - | } ], + | "signatures" : [ + | { + | "label" : "apply[A](xs: A*)List[A]", + | "documentation" : null, + | "parameters" : [ + | { + | "label" : "xs: A*", + | "documentation" : null + | } + | ] + | } + | ], + | "activeSignature" : null, | "activeParameter" : 0 |} """.stripMargin diff --git a/metaserver/src/test/scala/tests/hover/BaseHoverTest.scala b/metaserver/src/test/scala/tests/hover/BaseHoverTest.scala index 13e88b3a1c0..1a75ca91bd4 100644 --- a/metaserver/src/test/scala/tests/hover/BaseHoverTest.scala +++ b/metaserver/src/test/scala/tests/hover/BaseHoverTest.scala @@ -4,8 +4,8 @@ import scala.meta.languageserver.Uri import scala.meta.languageserver.providers.HoverProvider import scala.{meta => m} import langserver.messages.Hover -import play.api.libs.json.Json import tests.search.BaseIndexTest +import io.circe.syntax._ abstract class BaseHoverTest extends BaseIndexTest { @@ -37,13 +37,16 @@ abstract class BaseHoverTest extends BaseIndexTest { check( filename, code, { result => - val obtained = Json.prettyPrint(Json.toJson(result)) + val obtained = result.asJson.spaces2 val expected = s"""{ - | "contents" : [ { - | "language" : "scala", - | "value" : "$expectedValue" - | } ] + | "contents" : [ + | { + | "language" : "scala", + | "value" : "$expectedValue" + | } + | ], + | "range" : null |}""".stripMargin assertNoDiff(obtained, expected) } From b80928f7b3b7b4209777442b5abf3ba95fe284e8 Mon Sep 17 00:00:00 2001 From: Gabriele Petronella Date: Thu, 4 Jan 2018 17:19:00 +0100 Subject: [PATCH 14/18] Re-add commented out code --- .../languageserver/ScalametaLanguageServer.scala | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/metaserver/src/main/scala/scala/meta/languageserver/ScalametaLanguageServer.scala b/metaserver/src/main/scala/scala/meta/languageserver/ScalametaLanguageServer.scala index 28336321fbe..e34a60f3293 100644 --- a/metaserver/src/main/scala/scala/meta/languageserver/ScalametaLanguageServer.scala +++ b/metaserver/src/main/scala/scala/meta/languageserver/ScalametaLanguageServer.scala @@ -232,13 +232,13 @@ class ScalametaLanguageServer( .notification[DidChangeConfigurationParams]( "workspace/didChangeConfiguration" ) { params => - params.settings.hcursor.downField("scalameta").as[Configuration]// match { - // case Left(err) => - // client.showMessage(MessageType.Error, err.show) - // case Right(conf) => - // logger.info(s"Configuration updated $conf") - // configurationSubscriber.onNext(conf) - // } + params.settings.hcursor.downField("scalameta").as[Configuration] match { + case Left(err) => + client.showMessage(MessageType.Error, err.toString) + case Right(conf) => + logger.info(s"Configuration updated $conf") + configurationSubscriber.onNext(conf) + } } .notification[DidChangeWatchedFilesParams]( "workspace/didChangeWatchedFiles" From 58e78e0a099df7196bbbb4ff88b647f2771b7e81 Mon Sep 17 00:00:00 2001 From: Gabriele Petronella Date: Thu, 4 Jan 2018 18:32:44 +0100 Subject: [PATCH 15/18] Drop null values in tests --- .../src/test/scala/tests/MegaSuite.scala | 10 ++++ .../tests/compiler/CompletionsTest.scala | 10 +--- .../tests/compiler/SignatureHelpTest.scala | 59 +++++-------------- .../scala/tests/hover/BaseHoverTest.scala | 6 +- 4 files changed, 28 insertions(+), 57 deletions(-) diff --git a/metaserver/src/test/scala/tests/MegaSuite.scala b/metaserver/src/test/scala/tests/MegaSuite.scala index 5aa4d3ab5f7..ebb5736512e 100644 --- a/metaserver/src/test/scala/tests/MegaSuite.scala +++ b/metaserver/src/test/scala/tests/MegaSuite.scala @@ -11,6 +11,9 @@ import utest.framework.TestCallTree import utest.framework.Tree import utest.ufansi.Str +import io.circe.Json +import io.circe.Printer + /** * Test suite that supports * @@ -20,6 +23,7 @@ import utest.ufansi.Str * - FunSuite-style test("name") { => fun } */ class MegaSuite extends TestSuite { + private val jsonPrinter: Printer = Printer.spaces2.copy(dropNullValues = true) def beforeAll(): Unit = () def afterAll(): Unit = () def intercept[T: ClassTag](exprs: Unit): T = macro Asserts.interceptProxy[T] @@ -36,6 +40,12 @@ class MegaSuite extends TestSuite { ): Unit = { DiffAsserts.assertNoDiff(obtained, expected, title) } + def assertNoDiff( + obtained: Json, + expected: String + ): Unit = { + assertNoDiff(obtained.pretty(jsonPrinter), expected) + } override def utestAfterAll(): Unit = afterAll() override def utestFormatter: Formatter = new Formatter { diff --git a/metaserver/src/test/scala/tests/compiler/CompletionsTest.scala b/metaserver/src/test/scala/tests/compiler/CompletionsTest.scala index 0ac99e09837..2984f42e836 100644 --- a/metaserver/src/test/scala/tests/compiler/CompletionsTest.scala +++ b/metaserver/src/test/scala/tests/compiler/CompletionsTest.scala @@ -29,8 +29,7 @@ object CompletionsTest extends CompilerSuite { check( filename, code, { completions => - val obtained = completions.asJson.spaces2 - assertNoDiff(obtained, expected) + assertNoDiff(completions.asJson, expected) } ) } @@ -53,12 +52,7 @@ object CompletionsTest extends CompilerSuite { | "label" : "$label", | "kind" : ${kind.value}, | "detail" : "$detail", - | "documentation" : null, - | "sortText" : "00000", - | "filterText" : null, - | "insertText" : null, - | "textEdit" : null, - | "data" : null + | "sortText" : "00000" | } | ] |} diff --git a/metaserver/src/test/scala/tests/compiler/SignatureHelpTest.scala b/metaserver/src/test/scala/tests/compiler/SignatureHelpTest.scala index 829ff00f091..31a734f1ae9 100644 --- a/metaserver/src/test/scala/tests/compiler/SignatureHelpTest.scala +++ b/metaserver/src/test/scala/tests/compiler/SignatureHelpTest.scala @@ -26,8 +26,7 @@ object SignatureHelpTest extends CompilerSuite { expected: String ): Unit = { check(filename, code, { result => - val obtained = result.asJson.spaces2 - assertNoDiff(obtained, expected) + assertNoDiff(result.asJson, expected) }) } @@ -43,30 +42,24 @@ object SignatureHelpTest extends CompilerSuite { | "signatures" : [ | { | "label" : "assert(assertion: Boolean, message: => Any)Unit", - | "documentation" : null, | "parameters" : [ | { - | "label" : "assertion: Boolean", - | "documentation" : null + | "label" : "assertion: Boolean" | }, | { - | "label" : "message: => Any", - | "documentation" : null + | "label" : "message: => Any" | } | ] | }, | { | "label" : "assert(assertion: Boolean)Unit", - | "documentation" : null, | "parameters" : [ | { - | "label" : "assertion: Boolean", - | "documentation" : null + | "label" : "assertion: Boolean" | } | ] | } | ], - | "activeSignature" : null, | "activeParameter" : 0 |}""".stripMargin ) @@ -83,20 +76,16 @@ object SignatureHelpTest extends CompilerSuite { | "signatures" : [ | { | "label" : "assert(assertion: Boolean, message: => Any)Unit", - | "documentation" : null, | "parameters" : [ | { - | "label" : "assertion: Boolean", - | "documentation" : null + | "label" : "assertion: Boolean" | }, | { - | "label" : "message: => Any", - | "documentation" : null + | "label" : "message: => Any" | } | ] | } | ], - | "activeSignature" : null, | "activeParameter" : 1 |} """.stripMargin @@ -143,20 +132,16 @@ object SignatureHelpTest extends CompilerSuite { | "signatures" : [ | { | "label" : "apply(name: String, age: Int)User", - | "documentation" : null, | "parameters" : [ | { - | "label" : "name: String", - | "documentation" : null + | "label" : "name: String" | }, | { - | "label" : "age: Int", - | "documentation" : null + | "label" : "age: Int" | } | ] | } | ], - | "activeSignature" : null, | "activeParameter" : 1 |} """.stripMargin @@ -174,16 +159,13 @@ object SignatureHelpTest extends CompilerSuite { | "signatures" : [ | { | "label" : "apply[A](xs: A*)List[A]", - | "documentation" : null, | "parameters" : [ | { - | "label" : "xs: A*", - | "documentation" : null + | "label" : "xs: A*" | } | ] | } | ], - | "activeSignature" : null, | "activeParameter" : 0 |} """.stripMargin @@ -201,16 +183,13 @@ object SignatureHelpTest extends CompilerSuite { | "signatures" : [ | { | "label" : "apply[A](xs: A*)List[A]", - | "documentation" : null, | "parameters" : [ | { - | "label" : "xs: A*", - | "documentation" : null + | "label" : "xs: A*" | } | ] | } | ], - | "activeSignature" : null, | "activeParameter" : 0 |} """.stripMargin @@ -229,7 +208,6 @@ object SignatureHelpTest extends CompilerSuite { |{ | "signatures" : [ | ], - | "activeSignature" : null, | "activeParameter" : 0 |} """.stripMargin @@ -250,30 +228,24 @@ object SignatureHelpTest extends CompilerSuite { | "signatures" : [ | { | "label" : "(name: String)User", - | "documentation" : null, | "parameters" : [ | { - | "label" : "name: String", - | "documentation" : null + | "label" : "name: String" | } | ] | }, | { | "label" : "(name: String, age: Int)User", - | "documentation" : null, | "parameters" : [ | { - | "label" : "name: String", - | "documentation" : null + | "label" : "name: String" | }, | { - | "label" : "age: Int", - | "documentation" : null + | "label" : "age: Int" | } | ] | } | ], - | "activeSignature" : null, | "activeParameter" : 0 |} """.stripMargin @@ -301,16 +273,13 @@ object SignatureHelpTest extends CompilerSuite { | "signatures" : [ | { | "label" : "apply[A](xs: A*)List[A]", - | "documentation" : null, | "parameters" : [ | { - | "label" : "xs: A*", - | "documentation" : null + | "label" : "xs: A*" | } | ] | } | ], - | "activeSignature" : null, | "activeParameter" : 0 |} """.stripMargin diff --git a/metaserver/src/test/scala/tests/hover/BaseHoverTest.scala b/metaserver/src/test/scala/tests/hover/BaseHoverTest.scala index 1a75ca91bd4..1480a18b497 100644 --- a/metaserver/src/test/scala/tests/hover/BaseHoverTest.scala +++ b/metaserver/src/test/scala/tests/hover/BaseHoverTest.scala @@ -37,7 +37,6 @@ abstract class BaseHoverTest extends BaseIndexTest { check( filename, code, { result => - val obtained = result.asJson.spaces2 val expected = s"""{ | "contents" : [ @@ -45,10 +44,9 @@ abstract class BaseHoverTest extends BaseIndexTest { | "language" : "scala", | "value" : "$expectedValue" | } - | ], - | "range" : null + | ] |}""".stripMargin - assertNoDiff(obtained, expected) + assertNoDiff(result.asJson, expected) } ) } From f7ccaf191f19036dfd8104bff2a1eea77d04cea5 Mon Sep 17 00:00:00 2001 From: Gabriele Petronella Date: Thu, 4 Jan 2018 18:46:39 +0100 Subject: [PATCH 16/18] Remove a bit of boilerplate with context bounds and circe.syntax --- .../main/scala/langserver/core/MessageWriter.scala | 5 +++-- .../meta/languageserver/protocol/LanguageClient.scala | 11 ++++++----- 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/languageserver/src/main/scala/langserver/core/MessageWriter.scala b/languageserver/src/main/scala/langserver/core/MessageWriter.scala index 4905b0c7af8..d22feaa1680 100644 --- a/languageserver/src/main/scala/langserver/core/MessageWriter.scala +++ b/languageserver/src/main/scala/langserver/core/MessageWriter.scala @@ -4,6 +4,7 @@ import java.io.OutputStream import java.nio.charset.StandardCharsets import com.typesafe.scalalogging.LazyLogging import io.circe.Encoder +import io.circe.syntax._ /** * A class to write Json RPC messages on an output stream, following the Language Server Protocol. @@ -29,10 +30,10 @@ class MessageWriter(out: OutputStream) extends LazyLogging { * Write a message to the output stream. This method can be called from multiple threads, * but it may block waiting for other threads to finish writing. */ - def write[T](msg: T, h: Map[String, String] = Map.empty)(implicit encode: Encoder[T]): Unit = lock.synchronized { + def write[T: Encoder](msg: T, h: Map[String, String] = Map.empty): Unit = lock.synchronized { require(h.get(ContentLen).isEmpty) - val str = encode(msg).noSpaces + val str = msg.asJson.noSpaces val contentBytes = str.getBytes(StandardCharsets.UTF_8) val headers = (h + (ContentLen -> contentBytes.length)) .map { case (k, v) => s"$k: $v" } diff --git a/metaserver/src/main/scala/scala/meta/languageserver/protocol/LanguageClient.scala b/metaserver/src/main/scala/scala/meta/languageserver/protocol/LanguageClient.scala index d713f707229..bdb0f717285 100644 --- a/metaserver/src/main/scala/scala/meta/languageserver/protocol/LanguageClient.scala +++ b/metaserver/src/main/scala/scala/meta/languageserver/protocol/LanguageClient.scala @@ -19,6 +19,7 @@ import monix.execution.atomic.Atomic import monix.execution.atomic.AtomicInt import io.circe.Encoder import io.circe.Decoder +import io.circe.syntax._ import cats.syntax.either._ class LanguageClient(out: OutputStream) extends LazyLogging with Notifications { @@ -26,8 +27,8 @@ class LanguageClient(out: OutputStream) extends LazyLogging with Notifications { private val counter: AtomicInt = Atomic(1) private val activeServerRequests = TrieMap.empty[RequestId, Callback[Response]] - def notify[A](method: String, notification: A)(implicit encode: Encoder[A]): Unit = - writer.write(Notification(method, Some(encode(notification)))) + def notify[A: Encoder](method: String, notification: A): Unit = + writer.write(Notification(method, Some(notification.asJson))) def serverRespond(response: Response): Unit = response match { case Response.Empty => () case x: Response.Success => writer.write(x) @@ -49,14 +50,14 @@ class LanguageClient(out: OutputStream) extends LazyLogging with Notifications { callback.onSuccess(response) } - def request[A, B]( + def request[A: Encoder, B: Decoder]( method: String, request: A - )(implicit encode: Encoder[A], decode: Decoder[B]): Task[Either[Response.Error, B]] = { + ): Task[Either[Response.Error, B]] = { val nextId = RequestId(counter.incrementAndGet()) val response = Task.create[Response] { (out, cb) => val scheduled = out.scheduleOnce(Duration(0, "s")) { - val json = Request(method, Some(encode(request)), nextId) + val json = Request(method, Some(request.asJson), nextId) activeServerRequests.put(nextId, cb) writer.write(json) } From c10c55a8d86e7d86ae5eba955eeff146e098dc3b Mon Sep 17 00:00:00 2001 From: Gabriele Petronella Date: Thu, 4 Jan 2018 18:47:01 +0100 Subject: [PATCH 17/18] Format --- build.sbt | 4 ++- .../meta/languageserver/Configuration.scala | 8 +++-- .../languageserver/protocol/ErrorCode.scala | 4 +-- .../languageserver/protocol/ErrorObject.scala | 6 +++- .../languageserver/protocol/Message.scala | 31 ++++++++++++------- 5 files changed, 34 insertions(+), 19 deletions(-) diff --git a/build.sbt b/build.sbt index f2e6236e194..9517ded1138 100644 --- a/build.sbt +++ b/build.sbt @@ -56,7 +56,9 @@ inThisBuild( // faster publishLocal: publishArtifact in packageDoc := sys.env.contains("CI"), publishArtifact in packageSrc := sys.env.contains("CI"), - addCompilerPlugin("org.scalamacros" % "paradise" % "2.1.0" cross CrossVersion.full) + addCompilerPlugin( + "org.scalamacros" % "paradise" % "2.1.0" cross CrossVersion.full + ) ) ) diff --git a/metaserver/src/main/scala/scala/meta/languageserver/Configuration.scala b/metaserver/src/main/scala/scala/meta/languageserver/Configuration.scala index cf459317d57..90198137c6f 100644 --- a/metaserver/src/main/scala/scala/meta/languageserver/Configuration.scala +++ b/metaserver/src/main/scala/scala/meta/languageserver/Configuration.scala @@ -20,7 +20,8 @@ import Configuration._ ) object Configuration { - implicit val circeConfiguration: CirceConfiguration = CirceConfiguration.default.withDefaults + implicit val circeConfiguration: CirceConfiguration = + CirceConfiguration.default.withDefaults @JsonCodec case class Scalac(enabled: Boolean = false) @JsonCodec case class Hover(enabled: Boolean = false) @@ -39,7 +40,10 @@ object Configuration { confPath: RelativePath = RelativePath(".scalafix.conf") ) // TODO(olafur): re-enable indexJDK after https://github.com/scalameta/language-server/issues/43 is fixed - @JsonCodec case class Search(indexJDK: Boolean = false, indexClasspath: Boolean = true) + @JsonCodec case class Search( + indexJDK: Boolean = false, + indexClasspath: Boolean = true + ) implicit val absolutePathReads: Decoder[AbsolutePath] = Decoder.decodeString.emapTry(s => Try(AbsolutePath(s))) diff --git a/metaserver/src/main/scala/scala/meta/languageserver/protocol/ErrorCode.scala b/metaserver/src/main/scala/scala/meta/languageserver/protocol/ErrorCode.scala index 895c2450fb3..2f10f38a6a9 100644 --- a/metaserver/src/main/scala/scala/meta/languageserver/protocol/ErrorCode.scala +++ b/metaserver/src/main/scala/scala/meta/languageserver/protocol/ErrorCode.scala @@ -6,9 +6,7 @@ import enumeratum.values.IntEnumEntry import enumeratum.values.IntCirceEnum sealed abstract class ErrorCode(val value: Int) extends IntEnumEntry -case object ErrorCode - extends IntEnum[ErrorCode] - with IntCirceEnum[ErrorCode] { +case object ErrorCode extends IntEnum[ErrorCode] with IntCirceEnum[ErrorCode] { case object ParseError extends ErrorCode(-32700) case object InvalidRequest extends ErrorCode(-32600) case object MethodNotFound extends ErrorCode(-32601) diff --git a/metaserver/src/main/scala/scala/meta/languageserver/protocol/ErrorObject.scala b/metaserver/src/main/scala/scala/meta/languageserver/protocol/ErrorObject.scala index 1d1b6a69e92..9ff2b4b90a1 100644 --- a/metaserver/src/main/scala/scala/meta/languageserver/protocol/ErrorObject.scala +++ b/metaserver/src/main/scala/scala/meta/languageserver/protocol/ErrorObject.scala @@ -3,4 +3,8 @@ package scala.meta.languageserver.protocol import io.circe.Json import io.circe.generic.JsonCodec -@JsonCodec case class ErrorObject(code: ErrorCode, message: String, data: Option[Json]) +@JsonCodec case class ErrorObject( + code: ErrorCode, + message: String, + data: Option[Json] +) diff --git a/metaserver/src/main/scala/scala/meta/languageserver/protocol/Message.scala b/metaserver/src/main/scala/scala/meta/languageserver/protocol/Message.scala index f81d3e8e9c5..3281ff9b2d4 100644 --- a/metaserver/src/main/scala/scala/meta/languageserver/protocol/Message.scala +++ b/metaserver/src/main/scala/scala/meta/languageserver/protocol/Message.scala @@ -8,31 +8,38 @@ import cats.syntax.either._ sealed trait Message object Message { - implicit val decoder: Decoder[Message] = Decoder.decodeJsonObject.emap { obj => - val json = Json.fromJsonObject(obj) - val result = if (obj.contains("id")) - if (obj.contains("error")) json.as[Response.Error] - else if (obj.contains("result")) json.as[Response.Success] - else json.as[Request] - else json.as[Notification] - result.leftMap(_.toString) + implicit val decoder: Decoder[Message] = Decoder.decodeJsonObject.emap { + obj => + val json = Json.fromJsonObject(obj) + val result = + if (obj.contains("id")) + if (obj.contains("error")) json.as[Response.Error] + else if (obj.contains("result")) json.as[Response.Success] + else json.as[Request] + else json.as[Notification] + result.leftMap(_.toString) } } -@JsonCodec case class Request(method: String, params: Option[Json], id: RequestId) - extends Message { +@JsonCodec case class Request( + method: String, + params: Option[Json], + id: RequestId +) extends Message { def toError(code: ErrorCode, message: String): Response = Response.error(ErrorObject(code, message, None), id) } -@JsonCodec case class Notification(method: String, params: Option[Json]) extends Message +@JsonCodec case class Notification(method: String, params: Option[Json]) + extends Message @JsonCodec sealed trait Response extends Message { def isSuccess: Boolean = this.isInstanceOf[Response.Success] } object Response { @JsonCodec case class Success(result: Json, id: RequestId) extends Response - @JsonCodec case class Error(error: ErrorObject, id: RequestId) extends Response + @JsonCodec case class Error(error: ErrorObject, id: RequestId) + extends Response case object Empty extends Response def empty: Response = Empty def ok(result: Json, id: RequestId): Response = From 9924498af0dea2182e425f6b31e51a7db3c97ff7 Mon Sep 17 00:00:00 2001 From: Gabriele Petronella Date: Thu, 4 Jan 2018 18:48:49 +0100 Subject: [PATCH 18/18] Add explicit cats dependency We've started using cats, but so far it was a sub-dependency of circe and monix. --- build.sbt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/build.sbt b/build.sbt index 9517ded1138..d5f47bfed4c 100644 --- a/build.sbt +++ b/build.sbt @@ -68,6 +68,7 @@ lazy val V = new { val scalafix = "0.5.7" val enumeratum = "1.5.12" val circe = "0.9.0" + val cats = "1.0.1" } lazy val noPublish = List( @@ -93,6 +94,7 @@ lazy val languageserver = project "io.circe" %% "circe-generic" % V.circe, "io.circe" %% "circe-generic-extras" % V.circe, "io.circe" %% "circe-parser" % V.circe, + "org.typelevel" %% "cats-core" % V.cats, "com.beachape" %% "enumeratum" % V.enumeratum, "com.beachape" %% "enumeratum-circe" % "1.5.15", "com.typesafe.scala-logging" %% "scala-logging" % "3.7.2",