All Downloads are FREE. Search and download functionalities are using the official Maven repository.

langserver.core.Connection.scala Maven / Gradle / Ivy

The newest version!
package langserver.core

import java.io.InputStream
import java.io.OutputStream
import java.util.concurrent.Executors

import scala.collection.mutable.ListBuffer
import scala.concurrent.ExecutionContext
import scala.concurrent.Future
import scala.util.Failure
import scala.util.Success
import scala.util.Try

import com.dhpcs.jsonrpc._
import com.typesafe.scalalogging.LazyLogging

import langserver.messages._
import langserver.types._
import play.api.libs.json._

/**
 * A connection that reads and writes Language Server Protocol messages.
 *
 * @note Commands are executed asynchronously via a thread pool
 * @note Notifications are executed synchronously on the calling thread
 * @note The command handler returns Any because sometimes response objects can't be part
 *       of a sealed hierarchy. For instance, goto definition returns a {{{Seq[Location]}}}
 *       and that can't subclass anything other than Any
 */
class Connection(inStream: InputStream, outStream: OutputStream)(val commandHandler: (String, ServerCommand) => Any)
    extends LazyLogging {
  private val msgReader = new MessageReader(inStream)
  private val msgWriter = new MessageWriter(outStream)

  // 4 threads should be enough for everyone
  implicit private val commandExecutionContext = ExecutionContext.fromExecutorService(Executors.newFixedThreadPool(4))

  val notificationHandlers: ListBuffer[Notification => Unit] = ListBuffer.empty

  def notifySubscribers(n: Notification): Unit = {
    notificationHandlers.foreach(f =>
      Try(f(n)).recover { case e => logger.error("failed notification handler", e) })
  }

  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: Int, message: String): Unit = {
    sendNotification(ShowMessageParams(tpe, message))
  }

  def showMessage(tpe: Int, message: String, actions: String*): Unit = {
    ???
  }

  /**
   * 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: Int, 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 =>
                Notification.read(notification).fold {
                  logger.error(s"No notification type exists with method=${notification.method}")
                }(_.fold({ errors => logger.error(s"Invalid Notification: $errors") },
                  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 =>
                logger.info(s"Received response: $response")

              case m =>
                logger.error(s"Received unknown message: $m")
            }
          }
      }
    } while (!streamClosed)
  }

  private def readJsonRpcMessage(jsonString: String): Either[JsonRpcResponseError, JsonRpcMessage] = {
    logger.debug(s"Received $jsonString")
    Try(Json.parse(jsonString)) match {
      case Failure(exception) =>
        Left(JsonRpcResponseError.parseError(exception))

      case Success(json) =>
        Json.fromJson[JsonRpcMessage](json).fold({ errors =>
          Left(JsonRpcResponseError.invalidRequest(errors))
        }, Right(_))
    }
  }

  private def readCommand(jsonString: String): (Option[Either[String, BigDecimal]], Either[JsonRpcResponseError, ServerCommand]) =
    Try(Json.parse(jsonString)) match {
      case Failure(exception) =>
        None -> Left(JsonRpcResponseError.parseError(exception))

      case Success(json) =>
        Json.fromJson[JsonRpcRequestMessage](json).fold(
          errors => None -> Left(JsonRpcResponseError.invalidRequest(errors)),

          jsonRpcRequestMessage =>
            ServerCommand.read(jsonRpcRequestMessage)
              .fold[(Option[Either[String, BigDecimal]], Either[JsonRpcResponseError, ServerCommand])](
                jsonRpcRequestMessage.id -> Left(JsonRpcResponseError.methodNotFound(jsonRpcRequestMessage.method)))(commandJsResult => commandJsResult.fold(
                  errors => jsonRpcRequestMessage.id -> Left(JsonRpcResponseError.invalidParams(errors)),
                  command => jsonRpcRequestMessage.id -> Right(command))))

    }

  private def unpackRequest(request: JsonRpcRequestMessage): (Option[Either[String, BigDecimal]], Either[JsonRpcResponseError, ServerCommand]) = {
    ServerCommand.read(request)
      .fold[(Option[Either[String, BigDecimal]], Either[JsonRpcResponseError, ServerCommand])](

        request.id -> Left(
          JsonRpcResponseError.methodNotFound(request.method)))(commandJsResult => commandJsResult.fold(
          errors => request.id -> Left(JsonRpcResponseError.invalidParams(errors)),
          command => request.id -> Right(command)))
  }

  private def handleCommand(method: String, id: Either[String, BigDecimal], command: ServerCommand) = {
    Future(commandHandler(method, command)).map { result =>
      msgWriter.write(ResultResponse.write(Right(result), Some(id)))
    }
  }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy