diff --git a/build.sbt b/build.sbt index 0e1ed5c1c66..f55036047a2 100644 --- a/build.sbt +++ b/build.sbt @@ -8,7 +8,9 @@ inThisBuild( scalaVersion := V.scala212, scalacOptions ++= List( "-deprecation", - "-Xlint" + // -Xlint is unusable because of + // https://github.com/scala/bug/issues/10448 + "-Ywarn-unused:imports" ), scalafixEnabled := false, organization := "org.scalameta", @@ -55,7 +57,10 @@ 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 +69,8 @@ lazy val V = new { val scalameta = "2.1.5" val scalafix = "0.5.7" val enumeratum = "1.5.12" + val circe = "0.9.0" + val cats = "1.0.1" } lazy val noPublish = List( @@ -84,11 +91,14 @@ lazy val semanticdbSettings = List( lazy val languageserver = project .settings( - resolvers += Resolver.bintrayRepo("dhpcs", "maven"), libraryDependencies ++= Seq( - "com.dhpcs" %% "scala-json-rpc" % "2.0.1", + "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, + "org.typelevel" %% "cats-core" % V.cats, "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", @@ -105,7 +115,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]( @@ -126,7 +135,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/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 18e1d944c22..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 with Notifications { - 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 49d5d0f7ead..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[SignatureHelpResult] = Task.now(SignatureHelpResult(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/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/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 eca6e4fe524..d22feaa1680 100644 --- a/languageserver/src/main/scala/langserver/core/MessageWriter.scala +++ b/languageserver/src/main/scala/langserver/core/MessageWriter.scala @@ -1,8 +1,10 @@ package langserver.core import java.io.OutputStream -import play.api.libs.json._ +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. @@ -28,18 +30,18 @@ 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: Encoder](msg: T, h: Map[String, String] = Map.empty): Unit = lock.synchronized { require(h.get(ContentLen).isEmpty) - val str = Json.stringify(o.writes(msg)) - val contentBytes = str.getBytes(MessageReader.Utf8Charset) + 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" } .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 e94b947f476..eeeb92cc7a9 100644 --- a/languageserver/src/main/scala/langserver/core/Notifications.scala +++ b/languageserver/src/main/scala/langserver/core/Notifications.scala @@ -1,11 +1,35 @@ package langserver.core +import langserver.messages.LogMessageParams +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 logMessage(params: LogMessageParams): Unit + final def logMessage( + tpe: MessageType, + message: String + ): Unit = logMessage(LogMessageParams(tpe, message)) + + 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(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 c9a84922aa0..3659b2137bb 100644 --- a/languageserver/src/main/scala/langserver/messages/Commands.scala +++ b/languageserver/src/main/scala/langserver/messages/Commands.scala @@ -1,24 +1,13 @@ 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 - -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 +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. @@ -34,19 +23,14 @@ case class InitializeParams( /** * The capabilities provided by the client (editor) */ - capabilities: ClientCapabilities) extends ServerCommand + capabilities: ClientCapabilities +) case class InitializeError(retry: Boolean) -case class ClientCapabilities() +@JsonCodec case class ClientCapabilities() -object ClientCapabilities { - implicit val format: Format[ClientCapabilities] = Format( - Reads(jsValue => JsSuccess(ClientCapabilities())), - Writes(c => Json.obj())) -} - -case class ServerCapabilities( +@JsonCodec case class ServerCapabilities( /** * Defines how text documents are synced. */ @@ -113,50 +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]) extends ResultResponse -object CompletionList { - implicit val format: OFormat[CompletionList] = Json.format[CompletionList] -} - -case class InitializeResult(capabilities: ServerCapabilities) extends ResultResponse - -case class Shutdown() extends ServerCommand - -case class ShutdownResult() extends ResultResponse -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 @@ -167,217 +116,99 @@ 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] -) extends ClientCommand +) /** * 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 ) -case class ReferenceParams( + +@JsonCodec case class ReferenceParams( textDocument: TextDocumentIdentifier, position: Position, context: ReferenceContext ) -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) extends ServerCommand -object CodeActionRequest { - implicit val format: OFormat[CodeActionRequest] = Json.format[CodeActionRequest] -} -case class DocumentSymbolParams(textDocument: TextDocumentIdentifier) extends ServerCommand -case class TextDocumentRenameRequest(params: RenameParams) extends ServerCommand -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: WorkspaceExecuteCommandParams) extends ServerCommand -case class WorkspaceSymbolRequest(params: WorkspaceSymbolParams) extends ServerCommand -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 -object Hover { - implicit val format: OFormat[Hover] = Json.format[Hover] -} +@JsonCodec case class CodeActionRequest(params: CodeActionParams) -object ServerCommand extends CommandCompanion[ServerCommand] { - import JsonRpcUtils._ +@JsonCodec case class DocumentSymbolParams(textDocument: TextDocumentIdentifier) - implicit val positionParamsFormat: OFormat[TextDocumentPositionParams] = Json.format[TextDocumentPositionParams] - implicit val referenceParamsFormat: OFormat[ReferenceParams] = Json.format[ReferenceParams] +@JsonCodec case class TextDocumentRenameRequest(params: RenameParams) - 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), - ) +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) +@JsonCodec case class ApplyWorkspaceEditResponse(applied: Boolean) +@JsonCodec case class ApplyWorkspaceEditParams(label: Option[String], edit: WorkspaceEdit) - // 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) - } - } -} +@JsonCodec case class Hover(contents: Seq[MarkedString], range: Option[Range]) -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 LogMessageParams(`type`: MessageType, message: String) extends Notification -case class PublishDiagnostics(uri: String, diagnostics: Seq[Diagnostic]) extends Notification -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() extends Notification +case class Exit() -case class DidOpenTextDocumentParams(textDocument: TextDocumentItem) extends Notification -case class DidChangeTextDocumentParams( +@JsonCodec case class DidOpenTextDocumentParams(textDocument: TextDocumentItem) +@JsonCodec case class DidChangeTextDocumentParams( textDocument: VersionedTextDocumentIdentifier, - contentChanges: Seq[TextDocumentContentChangeEvent]) extends Notification - -case class DidCloseTextDocumentParams(textDocument: TextDocumentIdentifier) extends Notification -case class DidSaveTextDocumentParams(textDocument: TextDocumentIdentifier) extends Notification -case class DidChangeWatchedFilesParams(changes: Seq[FileEvent]) extends Notification -case class DidChangeConfigurationParams(settings: JsValue) extends Notification - -case class Initialized() extends Notification -object Initialized { - implicit val format: Format[Initialized] = OFormat( - Reads(jsValue => JsSuccess(Initialized())), - OWrites[Initialized](s => Json.obj())) -} - - -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] - ) - - // 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 -object RenameResult { - implicit val format: OFormat[RenameResult] = Json.format[RenameResult] -} -case class CodeActionResult(params: Seq[Command]) extends ResultResponse -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 SignatureHelpResult(signatures: Seq[SignatureInformation], - activeSignature: Option[Int], - activeParameter: Option[Int]) extends ResultResponse -object SignatureHelpResult { - implicit val format: OFormat[SignatureHelpResult] = Json.format[SignatureHelpResult] -} -case object ExecuteCommandResult extends ResultResponse -case class WorkspaceSymbolResult(params: Seq[SymbolInformation]) extends ResultResponse -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[SignatureHelpResult], - "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 - ) -} + contentChanges: Seq[TextDocumentContentChangeEvent]) +@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) + +@JsonCodec case class Initialized() + +@JsonCodec case class CancelRequest(id: Int) + +@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]) +@JsonCodec case class SignatureHelp(signatures: Seq[SignatureInformation], + activeSignature: Option[Int], + activeParameter: Option[Int]) +case object ExecuteCommandResult +@JsonCodec case class WorkspaceSymbolResult(params: Seq[SymbolInformation]) // 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/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/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 b9b365b931c..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,91 +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) + 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 @@ -219,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. */ @@ -236,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 @@ -292,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. */ @@ -309,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. */ @@ -325,15 +279,7 @@ case class DocumentFormattingParams( options: FormattingOptions ) -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] -} - +@JsonCodec case class ExecuteCommandParams(command: String, arguments: Option[Seq[Json]]) /** * An event describing a file change. @@ -341,10 +287,9 @@ object WorkspaceExecuteCommandParams { * @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] -} + +@JsonCodec case class CancelParams(id: Json) 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 6ed4f1b2b0e..00000000000 --- a/languageserver/src/test/scala/langserver/core/MessageReaderSuite.scala +++ /dev/null @@ -1,98 +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) - 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"}""") - } - - 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/Configuration.scala b/metaserver/src/main/scala/scala/meta/languageserver/Configuration.scala index 4cb7643186c..90198137c6f 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,38 @@ 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/LSPLogger.scala b/metaserver/src/main/scala/scala/meta/languageserver/LSPLogger.scala index 6db83048f56..eba0455c768 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] { @@ -16,10 +16,10 @@ class LSPLogger(@BeanProperty var encoder: PatternLayoutEncoder) val message = if (encoder != null) new String(encoder.encode(event), UTF_8) else event.getFormattedMessage - connection.foreach(_.logMessage(MessageType.Log, message)) + notifications.foreach(_.logMessage(MessageType.Log, message)) } } object LSPLogger { - var connection: Option[Connection] = None + var notifications: 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 23937d4eebf..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 @@ -18,10 +16,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 +87,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/Main.scala b/metaserver/src/main/scala/scala/meta/languageserver/Main.scala index d270cf783b7..5f5ae4bfe8e 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 @@ -31,9 +34,15 @@ object Main extends LazyLogging { System.setErr(err) 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 client = new LanguageClient(stdout) + val services = new ScalametaServices(cwd, client)(s) + val languageServer = new LanguageServer( + BaseProtocolMessage.fromInputStream(stdin), + client, + services.services, + s + ) + languageServer.listen() } catch { case NonFatal(e) => logger.error("Uncaught top-level exception", e) 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/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..5b613a6f110 100644 --- a/metaserver/src/main/scala/scala/meta/languageserver/ErrorReporter.scala +++ b/metaserver/src/main/scala/scala/meta/languageserver/ScalacErrorReporter.scala @@ -5,19 +5,19 @@ 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} 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 deleted file mode 100644 index 4580121e16a..00000000000 --- a/metaserver/src/main/scala/scala/meta/languageserver/ScalametaLanguageServer.scala +++ /dev/null @@ -1,466 +0,0 @@ -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 -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.compiler.CompilerConfig -import scala.meta.languageserver.compiler.Cursor -import scala.meta.languageserver.compiler.ScalacProvider -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.types._ -import monix.eval.Task -import monix.execution.Cancelable -import monix.execution.Scheduler -import monix.execution.schedulers.SchedulerService -import monix.reactive.MulticastStrategy -import monix.reactive.Observable -import monix.reactive.Observer -import monix.reactive.OverflowStrategy -import org.langmeta.inputs.Input -import org.langmeta.internal.io.PathIO -import org.langmeta.internal.semanticdb.XtensionDatabase -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.JsSuccess -import play.api.libs.json.JsValue - -class ScalametaLanguageServer( - cwd: AbsolutePath, - lspIn: InputStream, - lspOut: OutputStream, - stdout: PrintStream -)(implicit s: Scheduler) - extends LanguageServer(lspIn, lspOut) { - 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) - val (fileSystemSemanticdbSubscriber, fileSystemSemanticdbsPublisher) = - ScalametaLanguageServer.fileSystemSemanticdbStream(cwd) - val (compilerConfigSubscriber, compilerConfigPublisher) = - ScalametaLanguageServer.compilerConfigStream(cwd) - val (sourceChangeSubscriber, sourceChangePublisher) = - Observable.multicast[Input.VirtualFile]( - MulticastStrategy.Publish, - OverflowStrategy.DropOld(2) - ) - val (configurationSubscriber, configurationPublisher) = - ScalametaLanguageServer.configurationStream(connection) - val buffers: Buffers = Buffers() - val symbolIndex: SymbolIndex = - SymbolIndex(cwd, connection, buffers, configurationPublisher) - val scalacErrorReporter: ScalacErrorReporter = new ScalacErrorReporter( - connection - ) - val documentFormattingProvider = - new DocumentFormattingProvider(configurationPublisher, cwd, connection) - val squiggliesProvider = - new SquiggliesProvider(configurationPublisher, cwd, stdout) - val scalacProvider = new ScalacProvider - val interactiveSemanticdbs: Observable[semanticdb.Database] = - sourceChangePublisher - .debounce(FiniteDuration(1, "s")) - .flatMap { input => - Observable - .fromIterable(Semanticdbs.toSemanticdb(input, scalacProvider)) - .executeOn(presentationCompilerScheduler) - } - val interactiveSchemaSemanticdbs: Observable[schema.Database] = - interactiveSemanticdbs.flatMap(db => Observable(db.toSchema(cwd))) - val metaSemanticdbs: Observable[semanticdb.Database] = - Observable.merge( - fileSystemSemanticdbsPublisher.map(_.toDb(sourcepath = None)), - interactiveSemanticdbs - ) - - // Effects - val indexedSemanticdbs: Observable[Effects.IndexSemanticdb] = - Observable - .merge(fileSystemSemanticdbsPublisher, interactiveSchemaSemanticdbs) - .map(symbolIndex.indexDatabase) - val indexedDependencyClasspath: Observable[Effects.IndexSourcesClasspath] = - compilerConfigPublisher.mapTask( - c => symbolIndex.indexDependencyClasspath(c.sourceJars) - ) - val installedCompilers: Observable[Effects.InstallPresentationCompiler] = - compilerConfigPublisher.map(scalacProvider.loadNewCompilerGlobals) - val publishDiagnostics: Observable[Effects.PublishSquigglies] = - metaSemanticdbs.mapTask { db => - squiggliesProvider.squigglies(db).map { diagnostics => - diagnostics.foreach(connection.sendNotification) - Effects.PublishSquigglies - } - } - val scalacErrors: Observable[Effects.PublishScalacDiagnostics] = - metaSemanticdbs.map(scalacErrorReporter.reportErrors) - private var cancelEffects = List.empty[Cancelable] - val effects: List[Observable[Effects]] = List( - configurationPublisher.map(_ => Effects.UpdateBuffers), - indexedDependencyClasspath, - indexedSemanticdbs, - installedCompilers, - publishDiagnostics, - ) - - private def loadAllRelevantFilesInThisWorkspace(): Unit = { - Workspace.initialize(cwd) { path => - 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() - } - - 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) - } - } - - 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 - } - } - - 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) - } - - 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 - } - } - - 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) - ) - 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) - } - } - - 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)) - - private def toPoint( - td: TextDocumentIdentifier, - pos: Position - ): Cursor = { - val contents = buffers.read(td) - val input = Input.VirtualFile(td.uri, contents) - val offset = input.toOffset(pos) - Cursor(Uri(td.uri), contents, offset) - } - -} - -object ScalametaLanguageServer extends LazyLogging { - lazy val cacheDirectory: AbsolutePath = { - val path = AbsolutePath( - ProjectDirectories.fromProjectName("metaserver").projectCacheDir - ) - Files.createDirectories(path.toNIO) - path - } - - def clearCacheDirectory(): Unit = - Files.walkFileTree( - cacheDirectory.toNIO, - new SimpleFileVisitor[Path] { - override def visitFile( - file: Path, - attr: BasicFileAttributes - ): FileVisitResult = { - Files.delete(file) - FileVisitResult.CONTINUE - } - override def postVisitDirectory( - dir: Path, - exc: IOException - ): FileVisitResult = { - Files.delete(dir) - FileVisitResult.CONTINUE - } - } - ) - - def compilerConfigStream(cwd: AbsolutePath)( - implicit scheduler: Scheduler - ): (Observer.Sync[AbsolutePath], Observable[CompilerConfig]) = { - val (subscriber, publisher) = multicast[AbsolutePath]() - val compilerConfigPublished = publisher - .map(path => CompilerConfig.fromPath(path)) - subscriber -> compilerConfigPublished - } - - def fileSystemSemanticdbStream(cwd: AbsolutePath)( - implicit scheduler: Scheduler - ): (Observer.Sync[AbsolutePath], Observable[schema.Database]) = { - val (subscriber, publisher) = multicast[AbsolutePath]() - val semanticdbPublisher = publisher - .map(path => Semanticdbs.loadFromFile(semanticdbPath = path, cwd)) - subscriber -> semanticdbPublisher - } - - def configurationStream(connection: Connection)( - implicit scheduler: Scheduler - ): (Observer.Sync[Configuration], Observable[Configuration]) = { - val (subscriber, publisher) = - multicast[Configuration](MulticastStrategy.behavior(Configuration())) - val configurationPublisher = publisher - subscriber -> configurationPublisher - } - - def multicast[A]( - strategy: MulticastStrategy[A] = MulticastStrategy.publish - )(implicit s: Scheduler) = { - val (sub, pub) = Observable.multicast[A](strategy) - (sub, pub.doOnError(onError)) - } - - private def onError(e: Throwable): Unit = { - logger.error(e.getMessage, e) - } -} diff --git a/metaserver/src/main/scala/scala/meta/languageserver/ScalametaServices.scala b/metaserver/src/main/scala/scala/meta/languageserver/ScalametaServices.scala new file mode 100644 index 00000000000..31ae7f5e3c8 --- /dev/null +++ b/metaserver/src/main/scala/scala/meta/languageserver/ScalametaServices.scala @@ -0,0 +1,496 @@ +package scala.meta.languageserver + +import java.io.IOException +import java.nio.file.FileVisitResult +import java.nio.file.Files +import java.nio.file.Path +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.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._ +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.messages._ +import langserver.types._ +import monix.eval.Task +import monix.execution.Cancelable +import monix.execution.Scheduler +import monix.execution.schedulers.SchedulerService +import monix.reactive.MulticastStrategy +import monix.reactive.Observable +import monix.reactive.Observer +import monix.reactive.OverflowStrategy +import org.langmeta.inputs.Input +import org.langmeta.internal.io.PathIO +import org.langmeta.internal.semanticdb.XtensionDatabase +import org.langmeta.internal.semanticdb.schema +import org.langmeta.io.AbsolutePath +import org.langmeta.languageserver.InputEnrichments._ +import org.langmeta.semanticdb +import io.circe.Json +import monix.execution.atomic.Atomic + +class ScalametaServices( + cwd: AbsolutePath, + client: LanguageClient +)(implicit s: Scheduler) + extends LazyLogging { + 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[Either[Response.Error, A]] = + Task(Right(f)).executeOn(presentationCompilerScheduler) + val (fileSystemSemanticdbSubscriber, fileSystemSemanticdbsPublisher) = + ScalametaServices.fileSystemSemanticdbStream(cwd) + val (compilerConfigSubscriber, compilerConfigPublisher) = + ScalametaServices.compilerConfigStream(cwd) + val (sourceChangeSubscriber, sourceChangePublisher) = + Observable.multicast[Input.VirtualFile]( + MulticastStrategy.Publish, + OverflowStrategy.DropOld(2) + ) + val (configurationSubscriber, configurationPublisher) = + ScalametaServices.configurationStream + val buffers: Buffers = Buffers() + val symbolIndex: SymbolIndex = + SymbolIndex(cwd, client, buffers, configurationPublisher) + val scalacErrorReporter: ScalacErrorReporter = + new ScalacErrorReporter(client) + val documentFormattingProvider = + new DocumentFormattingProvider(configurationPublisher, cwd, client) + val squiggliesProvider = + new SquiggliesProvider(configurationPublisher, cwd) + val scalacProvider = new ScalacProvider + val interactiveSemanticdbs: Observable[semanticdb.Database] = + sourceChangePublisher + .debounce(FiniteDuration(1, "s")) + .flatMap { input => + Observable + .fromIterable(Semanticdbs.toSemanticdb(input, scalacProvider)) + .executeOn(presentationCompilerScheduler) + } + val interactiveSchemaSemanticdbs: Observable[schema.Database] = + interactiveSemanticdbs.flatMap(db => Observable(db.toSchema(cwd))) + val metaSemanticdbs: Observable[semanticdb.Database] = + Observable.merge( + fileSystemSemanticdbsPublisher.map(_.toDb(sourcepath = None)), + interactiveSemanticdbs + ) + + // Effects + val indexedSemanticdbs: Observable[Effects.IndexSemanticdb] = + Observable + .merge(fileSystemSemanticdbsPublisher, interactiveSchemaSemanticdbs) + .map(symbolIndex.indexDatabase) + val indexedDependencyClasspath: Observable[Effects.IndexSourcesClasspath] = + compilerConfigPublisher.mapTask( + c => symbolIndex.indexDependencyClasspath(c.sourceJars) + ) + val installedCompilers: Observable[Effects.InstallPresentationCompiler] = + compilerConfigPublisher.map(scalacProvider.loadNewCompilerGlobals) + val publishDiagnostics: Observable[Effects.PublishSquigglies] = + metaSemanticdbs.mapTask { db => + squiggliesProvider.squigglies(db).map { diagnostics => + diagnostics.foreach(client.publishDiagnostics) + Effects.PublishSquigglies + } + } + val scalacErrors: Observable[Effects.PublishScalacDiagnostics] = + metaSemanticdbs.map(scalacErrorReporter.reportErrors) + private var cancelEffects = List.empty[Cancelable] + val effects: List[Observable[Effects]] = List( + configurationPublisher.map(_ => Effects.UpdateBuffers), + indexedDependencyClasspath, + indexedSemanticdbs, + installedCompilers, + publishDiagnostics, + ) + + // 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") + LSPLogger.notifications = Some(client) + 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()) + } + + private val shutdownReceived = Atomic(false) + val services: Services = Services.empty + .requestAsync[InitializeParams, InitializeResult]("initialize") { params => + initialize(params) + } + .request[Json, Json]("shutdown") { _ => + shutdown() + shutdownReceived.set(true) + Json.Null + } + .notification[Json]("exit") { _ => + // The server should exit with success code 0 if the shutdown request has + // been received before; otherwise with error code 1 + // -- https://microsoft.github.io/language-server-protocol/specification#exit + val code = if (shutdownReceived.get) 0 else 1 + logger.info(s"exit($code)") + sys.exit(code) + } + .requestAsync[TextDocumentPositionParams, CompletionList]( + "textDocument/completion" + ) { params => + withPC { + logger.info("completion") + scalacProvider.getCompiler(params.textDocument) match { + case Some(g) => + CompletionProvider.completions( + g, + toCursor(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/codeAction" + ) { params => + CodeActionProvider.codeActions(params) + } + .notification[DidCloseTextDocumentParams]( + "textDocument/didClose" + ) { params => + buffers.closed(Uri(params.textDocument)) + () + } + .notification[DidOpenTextDocumentParams]( + "textDocument/didOpen" + ) { params => + val input = + Input.VirtualFile(params.textDocument.uri, params.textDocument.text) + buffers.changed(input) + sourceChangeSubscriber.onNext(input) + () + } + .notification[DidChangeTextDocumentParams]( + "textDocument/didChange" + ) { 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]( + "textDocument/didSave" + ) { params => + () + } + .notification[DidChangeConfigurationParams]( + "workspace/didChangeConfiguration" + ) { params => + 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" + ) { 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 + } + } + .requestAsync[DocumentFormattingParams, List[TextEdit]]( + "textDocument/formatting" + ) { params => + val uri = Uri(params.textDocument) + documentFormattingProvider.format(uri.toInput(buffers)) + } + .request[TextDocumentPositionParams, Hover]( + "textDocument/hover" + ) { params => + HoverProvider.hover( + symbolIndex, + Uri(params.textDocument), + params.position.line, + params.position.character + ) + } + .request[ReferenceParams, List[Location]]( + "textDocument/references" + ) { params => + ReferencesProvider.references( + symbolIndex, + Uri(params.textDocument.uri), + params.position, + params.context + ) + } + .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 None => SignatureHelpProvider.empty + } + } + .requestAsync[ExecuteCommandParams, Json]( + "workspace/executeCommand" + ) { params => + logger.info(s"executeCommand $params") + WorkspaceCommand.withNameOption(params.command) match { + case None => + Task { + val msg = s"Unknown command ${params.command}" + logger.error(msg) + Left(Response.invalidParams(msg)) + } + case Some(command) => + executeCommand(command, params) + } + } + .request[WorkspaceSymbolParams, List[SymbolInformation]]( + "workspace/symbol" + ) { params => + symbolIndex.workspaceSymbols(params.query) + } + + import WorkspaceCommand._ + val ok = Right(Json.Null) + private def executeCommand( + command: WorkspaceCommand, + params: ExecuteCommandParams + ): Task[Either[Response.Error, Json]] = command match { + case ClearIndexCache => + Task { + logger.info("Clearing the index cache") + ScalametaServices.clearCacheDirectory() + symbolIndex.clearIndex() + scalacProvider.allCompilerConfigs.foreach( + config => symbolIndex.indexDependencyClasspath(config.sourceJars) + ) + Right(Json.Null) + } + case ResetPresentationCompiler => + Task { + logger.info("Resetting all compiler instances") + scalacProvider.resetCompilers() + Right(Json.Null) + } + case ScalafixUnusedImports => + logger.info("Removing unused imports") + val response = for { + result <- Task( + OrganizeImports.removeUnused(params.arguments, symbolIndex) + ) + applied <- result match { + case Left(err) => Task.now(Left(err)) + case Right(workspaceEdit) => client.workspaceApplyEdit(workspaceEdit) + } + } yield { + applied match { + case Left(err) => + logger.warn(s"Failed to apply command $err") + Right(Json.Null) + 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.right.map(_ => Json.Null) + } + response + } + + private def toCursor( + td: TextDocumentIdentifier, + pos: Position + ): Cursor = { + val contents = buffers.read(td) + val input = Input.VirtualFile(td.uri, contents) + val offset = input.toOffset(pos) + 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 ScalametaServices extends LazyLogging { + lazy val cacheDirectory: AbsolutePath = { + val path = AbsolutePath( + ProjectDirectories.fromProjectName("metaserver").projectCacheDir + ) + Files.createDirectories(path.toNIO) + path + } + + def clearCacheDirectory(): Unit = + Files.walkFileTree( + cacheDirectory.toNIO, + new SimpleFileVisitor[Path] { + override def visitFile( + file: Path, + attr: BasicFileAttributes + ): FileVisitResult = { + Files.delete(file) + FileVisitResult.CONTINUE + } + override def postVisitDirectory( + dir: Path, + exc: IOException + ): FileVisitResult = { + Files.delete(dir) + FileVisitResult.CONTINUE + } + } + ) + + def compilerConfigStream(cwd: AbsolutePath)( + implicit scheduler: Scheduler + ): (Observer.Sync[AbsolutePath], Observable[CompilerConfig]) = { + val (subscriber, publisher) = multicast[AbsolutePath]() + val compilerConfigPublished = publisher + .map(path => CompilerConfig.fromPath(path)) + subscriber -> compilerConfigPublished + } + + def fileSystemSemanticdbStream(cwd: AbsolutePath)( + implicit scheduler: Scheduler + ): (Observer.Sync[AbsolutePath], Observable[schema.Database]) = { + val (subscriber, publisher) = multicast[AbsolutePath]() + val semanticdbPublisher = publisher + .map(path => Semanticdbs.loadFromFile(semanticdbPath = path, cwd)) + subscriber -> semanticdbPublisher + } + + def configurationStream( + implicit scheduler: Scheduler + ): (Observer.Sync[Configuration], Observable[Configuration]) = { + val (subscriber, publisher) = + multicast[Configuration](MulticastStrategy.behavior(Configuration())) + val configurationPublisher = publisher + subscriber -> configurationPublisher + } + + def multicast[A]( + strategy: MulticastStrategy[A] = MulticastStrategy.publish + )(implicit s: Scheduler): (Observer.Sync[A], Observable[A]) = { + val (sub, pub) = Observable.multicast[A](strategy) + (sub, pub.doOnError(onError)) + } + + private def onError(e: Throwable): Unit = { + logger.error(e.getMessage, e) + } +} 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..d55bc683e0f --- /dev/null +++ b/metaserver/src/main/scala/scala/meta/languageserver/protocol/BaseProtocolMessage.scala @@ -0,0 +1,33 @@ +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 + +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("\r\n") + } + sb.append("\r\n") + .append(content) + sb.toString + } +} + +object BaseProtocolMessage { + def fromInputStream(in: InputStream): Observable[BaseProtocolMessage] = + Observable + .fromInputStream(in) + .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 new file mode 100644 index 00000000000..fbef2cb1102 --- /dev/null +++ b/metaserver/src/main/scala/scala/meta/languageserver/protocol/BaseProtocolMessageParser.scala @@ -0,0 +1,111 @@ +package scala.meta.languageserver.protocol + +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 BaseProtocolMessageParser + extends Operator[Array[Byte], BaseProtocolMessage] + with LazyLogging { + 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 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' + } + private[this] 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) + // 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 + .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(BaseProtocolMessage(header, content)).flatMap { + case Continue => readHeaders() + case Stop => Stop + } + } + } + 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/main/scala/scala/meta/languageserver/protocol/ErrorCode.scala b/metaserver/src/main/scala/scala/meta/languageserver/protocol/ErrorCode.scala new file mode 100644 index 00000000000..2f10f38a6a9 --- /dev/null +++ b/metaserver/src/main/scala/scala/meta/languageserver/protocol/ErrorCode.scala @@ -0,0 +1,18 @@ +package scala.meta.languageserver.protocol + +import scala.collection.immutable.IndexedSeq +import enumeratum.values.IntEnum +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 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/metaserver/src/main/scala/scala/meta/languageserver/protocol/ErrorObject.scala b/metaserver/src/main/scala/scala/meta/languageserver/protocol/ErrorObject.scala new file mode 100644 index 00000000000..9ff2b4b90a1 --- /dev/null +++ b/metaserver/src/main/scala/scala/meta/languageserver/protocol/ErrorObject.scala @@ -0,0 +1,10 @@ +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] +) 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..bdb0f717285 --- /dev/null +++ b/metaserver/src/main/scala/scala/meta/languageserver/protocol/LanguageClient.scala @@ -0,0 +1,106 @@ +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.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 +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 { + private val writer = new MessageWriter(out) + private val counter: AtomicInt = Atomic(1) + private val activeServerRequests = + TrieMap.empty[RequestId, Callback[Response]] + 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) + 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: Encoder, B: Decoder]( + method: String, + request: A + ): 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(request.asJson), nextId) + activeServerRequests.put(nextId, cb) + writer.write(json) + } + Cancelable { () => + scheduled.cancel() + this.notify("$/cancelRequest", CancelParams(nextId.value)) + } + } + response.map { + case Response.Empty => + Left( + Response.invalidParams( + s"Got empty response for request $request", + nextId + ) + ) + case err: Response.Error => + Left(err) + case Response.Success(result, _) => + result.as[B].leftMap { err => + Response.invalidParams(err.toString, nextId) + } + } + } + + override def showMessage(params: ShowMessageParams): Unit = { + notify("window/showMessage", params) + } + + override def logMessage(params: LogMessageParams): Unit = { + notify("window/logMessage", params) + } + + override def publishDiagnostics( + publishDiagnostics: PublishDiagnostics + ): Unit = { + notify("textDocument/publishDiagnostics", publishDiagnostics) + } + def workspaceApplyEdit( + params: ApplyWorkspaceEditParams + ): 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 new file mode 100644 index 00000000000..96875da8251 --- /dev/null +++ b/metaserver/src/main/scala/scala/meta/languageserver/protocol/LanguageServer.scala @@ -0,0 +1,112 @@ +package scala.meta.languageserver.protocol + +import scala.collection.concurrent.TrieMap +import scala.concurrent.Await +import scala.concurrent.duration.Duration +import scala.util.control.NonFatal +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 io.circe.Json +import io.circe.parser.parse +import io.circe.syntax._ + +final class LanguageServer( + in: Observable[BaseProtocolMessage], + client: LanguageClient, + services: Services, + requestScheduler: Scheduler +) extends LazyLogging { + private val activeClientRequests: TrieMap[Json, Cancelable] = TrieMap.empty + private val cancelNotification = + Service.notification[CancelParams]("$/cancelRequest") { params => + val id = params.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 { + logger.info(s"Cancelling request $id") + 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 response: Response => + Task { + client.clientRespond(response) + Response.empty + } + 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 { + logger.info(s"Method not found '$method'") + 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) + } + val runningResponse = response.runAsync(requestScheduler) + activeClientRequests.put(request.id.asJson, runningResponse) + Task.fromFuture(runningResponse) + } + + } + + def handleMessage(message: BaseProtocolMessage): Task[Response] = + parse(message.content) match { + case Left(err) => Task.now(Response.parseError(err.toString)) + case Right(json) => + json.as[Message] match { + case Left(err) => Task.now(Response.invalidRequest(err.toString)) + case Right(msg) => handleValidMessage(msg) + } + } + + def startTask: Task[Unit] = + in.foreachL { msg => + handleMessage(msg) + .map(client.serverRespond) + .onErrorRecover { + case NonFatal(e) => + logger.error("Unhandled error", e) + } + .runAsync(requestScheduler) + } + + def listen(): Unit = { + val f = startTask.runAsync(requestScheduler) + logger.info("Listening....") + Await.result(f, Duration.Inf) + } +} diff --git a/metaserver/src/main/scala/scala/meta/languageserver/protocol/Message.scala b/metaserver/src/main/scala/scala/meta/languageserver/protocol/Message.scala new file mode 100644 index 00000000000..8e5f0f30c5a --- /dev/null +++ b/metaserver/src/main/scala/scala/meta/languageserver/protocol/Message.scala @@ -0,0 +1,75 @@ +package scala.meta.languageserver.protocol + +import monix.eval.Task +import io.circe.Json +import io.circe.Decoder +import io.circe.generic.JsonCodec +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) + } +} + +@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 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 + case object Empty extends Response + def empty: Response = Empty + 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: Json, id: RequestId): Response = + Success(result, id) + def error(error: ErrorObject, id: RequestId): Response.Error = + Error(error, id) + def internalError(message: String): Response.Error = + internalError(message, RequestId.Null) + def internalError(message: String, id: RequestId): Response.Error = + Error(ErrorObject(ErrorCode.InternalError, message, None), id) + def invalidParams(message: String): Response.Error = + invalidParams(message, RequestId.Null) + 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: Json): Response.Error = + Error( + ErrorObject(ErrorCode.RequestCancelled, "", None), + id.as[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 new file mode 100644 index 00000000000..e6125339225 --- /dev/null +++ b/metaserver/src/main/scala/scala/meta/languageserver/protocol/RequestId.scala @@ -0,0 +1,25 @@ +package scala.meta.languageserver.protocol + +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(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 new file mode 100644 index 00000000000..eef010d67ce --- /dev/null +++ b/metaserver/src/main/scala/scala/meta/languageserver/protocol/Services.scala @@ -0,0 +1,108 @@ +package scala.meta.languageserver.protocol + +import com.typesafe.scalalogging.LazyLogging +import monix.eval.Task +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] +} +trait MethodName { + def methodName: String +} + +trait JsonRpcService extends Service[Message, Response] +trait NamedJsonRpcService extends JsonRpcService with MethodName + +object Service extends LazyLogging { + + 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(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(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. + case Left(err) => err.copy(id = id) + } + } + case Request(invalidMethod, _, id) => + Task(Response.methodNotFound(invalidMethod, id)) + case _ => + Task(Response.invalidRequest(s"Expected request, obtained $message")) + } + } + + def notification[A: Decoder](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 + } + override def handle(message: Message): Task[Response] = message match { + case Notification(`method`, params) => + params.getOrElse(Json.Null).as[A] match { + case Left(err) => + fail(s"Failed to parse notification $message. Errors: $err") + case Right(value) => + f.handle(value).map(_ => Response.empty) + } + case Notification(invalidMethod, _) => + fail(s"Expected method '$method', obtained '$invalidMethod'") + case _ => + fail(s"Expected notification, obtained $message") + } + } +} + +object Services { + val empty: Services = new Services(Nil) +} + +class Services private (val services: List[NamedJsonRpcService]) { + + def request[A: Decoder, B: Encoder](method: String)( + f: A => B + ): Services = + requestAsync[A, B](method)(request => Task(Right(f(request)))) + + 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: Decoder](method: String)( + f: A => Unit + ): Services = + notificationAsync[A](method)(request => Task(f(request))) + + def notificationAsync[A: Decoder](method: String)( + f: Service[A, Unit] + ): Services = + addService(Service.notification[A](method)(f)) + + 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/providers/CodeActionProvider.scala b/metaserver/src/main/scala/scala/meta/languageserver/providers/CodeActionProvider.scala index c8ab10151f3..16ce68040b5 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 +import io.circe.syntax._ 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 + params.textDocument.asJson :: 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/DocumentFormattingProvider.scala b/metaserver/src/main/scala/scala/meta/languageserver/providers/DocumentFormattingProvider.scala index f580f9e12e3..06791d023bf 100644 --- a/metaserver/src/main/scala/scala/meta/languageserver/providers/DocumentFormattingProvider.scala +++ b/metaserver/src/main/scala/scala/meta/languageserver/providers/DocumentFormattingProvider.scala @@ -5,10 +5,12 @@ 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.Response import scala.util.control.NonFatal import com.typesafe.scalalogging.LazyLogging +import cats.syntax.bifunctor._ +import cats.instances.either._ import langserver.core.Notifications -import langserver.messages.DocumentFormattingResult import langserver.types.MessageType import langserver.types.Position import langserver.types.Range @@ -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() @@ -78,16 +80,16 @@ class DocumentFormattingProvider( scalafmt.format(input.value, input.path, path) } } - 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 Right(formatted) => - val edits = List(TextEdit(fullDocumentRange, formatted)) - DocumentFormattingResult(edits) - } + formatResult.bimap( + message => { + // We show a message here to be sure the message is + // reported in the UI. invalidParams responses don't + // get reported in vscode at least. + notifications.showMessage(MessageType.Error, message) + Response.invalidParams(message) + }, + formatted => List(TextEdit(fullDocumentRange, formatted)) + ) } } 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..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,10 +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 object DocumentHighlightProvider extends LazyLogging { @@ -13,17 +13,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 - // TODO(alexey) add DocumentHighlightKind: Text (default), Read, Write - DocumentHighlightResult(locations) + // TODO(alexey) add DocumentHighlightKind: Text (default), Read, Write + } yield DocumentHighlight(pos.range.get.toRange) } } 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..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,7 @@ 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} object DocumentSymbolProvider extends LazyLogging { @@ -55,10 +55,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/ReferencesProvider.scala b/metaserver/src/main/scala/scala/meta/languageserver/providers/ReferencesProvider.scala index 9d17e674dbd..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,10 +2,10 @@ 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 +import langserver.types.Location object ReferencesProvider extends LazyLogging { @@ -14,13 +14,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/main/scala/scala/meta/languageserver/providers/SquiggliesProvider.scala b/metaserver/src/main/scala/scala/meta/languageserver/providers/SquiggliesProvider.scala index 8193166f5eb..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 @@ -15,14 +14,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)) 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..eb54baa4e22 100644 --- a/metaserver/src/main/scala/scala/meta/languageserver/refactoring/OrganizeImports.scala +++ b/metaserver/src/main/scala/scala/meta/languageserver/refactoring/OrganizeImports.scala @@ -1,38 +1,39 @@ package scala.meta.languageserver.refactoring -import scala.meta._ +import cats.syntax.either._ +import scala.meta.Document import scala.meta.languageserver.Parser 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 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 - ): ApplyWorkspaceEditParams = { + ): 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) - result.getOrElse( - throw InvalidParamsResponseError( + Either.fromOption( + result, + Response.invalidParams( s"Unable to parse TextDocumentIdentifier from $arguments" ) ) diff --git a/metaserver/src/main/scala/scala/meta/languageserver/search/InMemorySymbolIndex.scala b/metaserver/src/main/scala/scala/meta/languageserver/search/InMemorySymbolIndex.scala index d2cc229f9a1..03da477e969 100644 --- a/metaserver/src/main/scala/scala/meta/languageserver/search/InMemorySymbolIndex.scala +++ b/metaserver/src/main/scala/scala/meta/languageserver/search/InMemorySymbolIndex.scala @@ -5,7 +5,7 @@ import scala.meta.languageserver.Buffers import scala.meta.languageserver.Effects import scala.meta.languageserver.Configuration import scala.meta.languageserver.ScalametaEnrichments._ -import scala.meta.languageserver.ScalametaLanguageServer.cacheDirectory +import scala.meta.languageserver.ScalametaServices.cacheDirectory import scala.meta.languageserver.Uri import scala.meta.languageserver.compiler.CompilerConfig import scala.meta.languageserver.index.SymbolData diff --git a/metaserver/src/test/scala/tests/MegaSuite.scala b/metaserver/src/test/scala/tests/MegaSuite.scala index 17ff432ea90..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,10 +23,16 @@ 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] 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, @@ -31,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 aa8644bda54..2984f42e836 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,8 +29,7 @@ object CompletionsTest extends CompilerSuite { check( filename, code, { completions => - val obtained = Json.prettyPrint(Json.toJson(completions)) - assertNoDiff(obtained, expected) + assertNoDiff(completions.asJson, expected) } ) } @@ -48,12 +47,14 @@ 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", + | "sortText" : "00000" + | } + | ] |} """.stripMargin ) @@ -67,7 +68,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 a96a0d694f3..31a734f1ae9 100644 --- a/metaserver/src/test/scala/tests/compiler/SignatureHelpTest.scala +++ b/metaserver/src/test/scala/tests/compiler/SignatureHelpTest.scala @@ -1,15 +1,15 @@ package tests.compiler import scala.meta.languageserver.providers.SignatureHelpProvider -import langserver.messages.SignatureHelpResult -import play.api.libs.json.Json +import langserver.messages.SignatureHelp +import io.circe.syntax._ object SignatureHelpTest extends CompilerSuite { def check( filename: String, code: String, - fn: SignatureHelpResult => Unit + fn: SignatureHelp => Unit ): Unit = { targeted( filename, @@ -26,8 +26,7 @@ object SignatureHelpTest extends CompilerSuite { expected: String ): Unit = { check(filename, code, { result => - val obtained = Json.prettyPrint(Json.toJson(result)) - assertNoDiff(obtained, expected) + assertNoDiff(result.asJson, expected) }) } @@ -40,19 +39,27 @@ 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", + | "parameters" : [ + | { + | "label" : "assertion: Boolean" + | }, + | { + | "label" : "message: => Any" + | } + | ] + | }, + | { + | "label" : "assert(assertion: Boolean)Unit", + | "parameters" : [ + | { + | "label" : "assertion: Boolean" + | } + | ] + | } + | ], | "activeParameter" : 0 |}""".stripMargin ) @@ -66,14 +73,19 @@ 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", + | "parameters" : [ + | { + | "label" : "assertion: Boolean" + | }, + | { + | "label" : "message: => Any" + | } + | ] + | } + | ], | "activeParameter" : 1 |} """.stripMargin @@ -117,14 +129,19 @@ 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", + | "parameters" : [ + | { + | "label" : "name: String" + | }, + | { + | "label" : "age: Int" + | } + | ] + | } + | ], | "activeParameter" : 1 |} """.stripMargin @@ -139,12 +156,16 @@ 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]", + | "parameters" : [ + | { + | "label" : "xs: A*" + | } + | ] + | } + | ], | "activeParameter" : 0 |} """.stripMargin @@ -159,12 +180,16 @@ 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]", + | "parameters" : [ + | { + | "label" : "xs: A*" + | } + | ] + | } + | ], | "activeParameter" : 0 |} """.stripMargin @@ -181,7 +206,8 @@ object SignatureHelpTest extends CompilerSuite { """.stripMargin, """ |{ - | "signatures" : [ ], + | "signatures" : [ + | ], | "activeParameter" : 0 |} """.stripMargin @@ -199,19 +225,27 @@ 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", + | "parameters" : [ + | { + | "label" : "name: String" + | } + | ] + | }, + | { + | "label" : "(name: String, age: Int)User", + | "parameters" : [ + | { + | "label" : "name: String" + | }, + | { + | "label" : "age: Int" + | } + | ] + | } + | ], | "activeParameter" : 0 |} """.stripMargin @@ -236,12 +270,16 @@ 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]", + | "parameters" : [ + | { + | "label" : "xs: A*" + | } + | ] + | } + | ], | "activeParameter" : 0 |} """.stripMargin 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/hover/BaseHoverTest.scala b/metaserver/src/test/scala/tests/hover/BaseHoverTest.scala index 13e88b3a1c0..1480a18b497 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,15 +37,16 @@ abstract class BaseHoverTest extends BaseIndexTest { check( filename, code, { result => - val obtained = Json.prettyPrint(Json.toJson(result)) val expected = s"""{ - | "contents" : [ { - | "language" : "scala", - | "value" : "$expectedValue" - | } ] + | "contents" : [ + | { + | "language" : "scala", + | "value" : "$expectedValue" + | } + | ] |}""".stripMargin - assertNoDiff(obtained, expected) + assertNoDiff(result.asJson, expected) } ) } diff --git a/metaserver/src/test/scala/tests/search/SymbolIndexTest.scala b/metaserver/src/test/scala/tests/search/SymbolIndexTest.scala index 1e48ac254da..32404b25f11 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.ScalametaServices 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 ScalametaServices(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 6126113f819..32c6c95669d 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,45 @@ 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", + "sonatype:releases", + "-J", toolsJar, - 'org.scalameta:metaserver_2.12:0.1-SNAPSHOT', - '-M', - 'scala.meta.languageserver.Main' + "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 +65,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);