Skip to content
This repository has been archived by the owner on Jun 8, 2023. It is now read-only.

Commit

Permalink
Error reporting working for the first time:
Browse files Browse the repository at this point in the history
- errors are not properly cleaned
- ensime reports duplicate errors sometimes
  • Loading branch information
dragos committed Sep 19, 2016
1 parent 2149326 commit 699c9c6
Show file tree
Hide file tree
Showing 10 changed files with 117 additions and 68 deletions.
8 changes: 6 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,12 @@
# Scala language server for VS Code

There is nothing to see yet, but this will be an experiment for building a Language Server for Scala, in Scala.
![demo](code.gif "Demo")


This is an experiment for building a Language Server for Scala, in Scala.

- language server: A Scala-based implementation of the [language server protocol](https://github.com/Microsoft/language-server-protocol/blob/master/protocol.md)
- client: A Scala.js-based Scala extension (language client)
- scala: A Typescript-based Scala extension (language client). Ideally it will be ported to Scala.js
- ensime-server: An implementation of the Language Server based on Ensime

The language server may be backed up by [ensime](http://ensime.github.io/) or directly by the presentation compiler. Ideally, the language server ca be used as a basis for implementing support for any language, not just Scala.
Binary file added code.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions ensime-server/src/main/resources/logback.groovy
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,4 @@ root(DEBUG, ["FILE"])
logger("slick", ERROR, ["FILE"])
logger("langserver.core", ERROR, ["FILE"])
logger("scala.tools.nsc", ERROR, ["FILE"])
logger("com.zaxxer.hikari", ERROR, ["FILE"])
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import akka.actor.Props
import org.ensime.api.TypecheckFileReq
import java.net.URI
import scalariform.formatter.preferences.FormattingPreferences
import org.ensime.core.ShutdownRequest

class EnsimeLanguageServer(in: InputStream, out: OutputStream) extends LanguageServer(in, out) {
private val system = ActorSystem("ENSIME")
Expand Down Expand Up @@ -51,13 +52,13 @@ class EnsimeLanguageServer(in: InputStream, out: OutputStream) extends LanguageS
EnsimeConfigProtocol.parse(Files.toString(ensimeFile, Charsets.UTF_8))
} catch {
case e: Throwable =>
showMessage(MessageType.Error, s"There was a problem parsing $ensimeFile ${e.getMessage}")
connection.showMessage(MessageType.Error, s"There was a problem parsing $ensimeFile ${e.getMessage}")
noConfig
}
showMessage(MessageType.Info, s"Using configuration: $ensimeFile")
//showMessage(MessageType.Info, s"Using configuration: $ensimeFile")
logger.info(s"Using configuration: $config")

ensimeProject = system.actorOf(Props(classOf[EnsimeProjectServer], config))
ensimeProject = system.actorOf(Props(classOf[EnsimeProjectServer], connection, config))

// we don't give a damn about them, but Ensime expects it
ensimeProject ! ConnectionInfoReq
Expand Down Expand Up @@ -87,10 +88,14 @@ class EnsimeLanguageServer(in: InputStream, out: OutputStream) extends LanguageS

override def onSaveTextDocument(td: TextDocumentIdentifier) = {
logger.debug(s"saveTextDocuemnt $td")
showMessage(MessageType.Info, s"Saved text document ${td.uri}")
}

override def onCloseTextDocument(td: TextDocumentIdentifier) = {
logger.debug(s"closeTextDocuemnt $td")
}

override def shutdown() {
logger.info("Shutdown request")
ensimeProject ! ShutdownRequest("Requested by client")
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,12 @@ import com.typesafe.scalalogging.LazyLogging

import akka.actor.Actor
import akka.util.Timeout
import langserver.core.Connection
import langserver.types._
import java.net.URI
import langserver.messages.MessageType

class EnsimeProjectServer(implicit val config: EnsimeConfig) extends Actor with LazyLogging {
class EnsimeProjectServer(connection: Connection, implicit val config: EnsimeConfig) extends Actor with LazyLogging {
implicit val timeout: Timeout = Timeout(10 seconds)

val broadcaster = context.actorOf(Broadcaster(), "broadcaster")
Expand All @@ -35,6 +39,7 @@ class EnsimeProjectServer(implicit val config: EnsimeConfig) extends Actor with

case AnalyzerReadyEvent =>
logger.info("Analyzer is ready!")
connection.showMessage(MessageType.Info, "Ensime is ready")

case FullTypeCheckCompleteEvent =>
logger.info("Full typecheck complete event")
Expand All @@ -44,6 +49,26 @@ class EnsimeProjectServer(implicit val config: EnsimeConfig) extends Actor with
}

private def publishDiagnostics(): Unit = {
logger.debug(s"Scala notes: ${compilerDiagnostics}")
logger.debug(s"Scala notes: ${compilerDiagnostics.mkString("\n")}")

for ((file, notes) <- compilerDiagnostics.groupBy(_.file))
connection.publishDiagnostics(s"file://$file", notes.map(toDiagnostic))
}

private def toDiagnostic(note: Note): Diagnostic = {
val start: Int = note.beg
val end: Int = note.end
val length = end - start

val severity = note.severity match {
case NoteError => DiagnosticSeverity.Error
case NoteWarn => DiagnosticSeverity.Warning
case NoteInfo => DiagnosticSeverity.Information
}

// Scalac reports 1-based line and columns, while Code expects 0-based
val range = Range(Position(note.line - 1, note.col - 1), Position(note.line - 1, note.col - 1 + length))

Diagnostic(range, Some(severity), code = None, source = Some("Scala"), message = note.msg)
}
}
20 changes: 8 additions & 12 deletions languageserver/src/main/scala/langserver/core/Connection.scala
Original file line number Diff line number Diff line change
Expand Up @@ -9,37 +9,33 @@ trait Connection {
/**
* A message request, that will also show an action and require a response
* from the client.
*
*
* @param tpe One of MessageType values
* @param message The message to display in the client
* @param actions A short title like 'Retry', 'Open Log' etc.
*/
def showMessage(tpe: Int, message: String, actions: String*): Unit

/**
* 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

/**
* A notification sent to the client to log a message.
*
*
* @param tpe One of MessageType values
* @param message The message to display in the client
*/
def logMessage(tpe: Int, message: String): Unit

/**
* A notification sent to the client to signal results of validation runs.
*/
def publishDiagnostics(uri: String, diagnostics: Seq[Diagnostic]): Unit

def onMethodCall(methodName: String, params: JsValue): Unit

def sendResponse(id: Long, result: JsValue, error: Option[JsonRpcResponseError]): Unit

def sendNotification(methodName: String, params: Option[JsValue]): Unit

def sendNotification(params: Notification): Unit
}
67 changes: 50 additions & 17 deletions languageserver/src/main/scala/langserver/core/ConnectionImpl.scala
Original file line number Diff line number Diff line change
@@ -1,29 +1,31 @@
package langserver.core

import java.io.OutputStream
import java.io.InputStream
import com.dhpcs.jsonrpc._
import langserver.messages.ServerCommand
import scala.util.Try
import play.api.libs.json._
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.collection.mutable.ListBuffer
import langserver.messages.Notification
import scala.util.Try

import com.dhpcs.jsonrpc._
import com.typesafe.scalalogging.LazyLogging
import langserver.messages.Response
import langserver.messages.ResultResponse
import scala.concurrent.Future
import scala.concurrent.ExecutionContext
import java.util.concurrent.Executors

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
*/
class ConnectionImpl(inStream: InputStream, outStream: OutputStream)(val commandHandler: ServerCommand => ResultResponse) extends LazyLogging {
class ConnectionImpl(inStream: InputStream, outStream: OutputStream)(val commandHandler: ServerCommand => ResultResponse)
extends Connection with LazyLogging {
private val msgReader = new MessageReader(inStream)
private val msgWriter = new MessageWriter(outStream)

Expand All @@ -34,15 +36,46 @@ class ConnectionImpl(inStream: InputStream, outStream: OutputStream)(val command

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

def sendNotification(params: Notification): Unit = {
override 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
*/
override 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
*/
override def logMessage(tpe: Int, message: String): Unit = {
sendNotification(LogMessageParams(tpe, message))
}

/**
* Publish compilation errors for the given file.
*/
override def publishDiagnostics(uri: String, diagnostics: Seq[Diagnostic]): Unit = {
sendNotification(PublishDiagnostics(uri, diagnostics))
}

def start() {
while (true) {
val jsonString = msgReader.nextPayload()
Expand Down Expand Up @@ -118,7 +151,7 @@ class ConnectionImpl(inStream: InputStream, outStream: OutputStream)(val command
command => request.id -> Right(command)))
}

private def handleCommand(id: Either[String,BigDecimal], command: ServerCommand) = {
private def handleCommand(id: Either[String, BigDecimal], command: ServerCommand) = {
logger.info(s"Received commmand: $id -- $command")
Future(commandHandler(command)).map { result =>
msgWriter.write(ResultResponse.write(Right(result), Some(id)))
Expand Down
36 changes: 7 additions & 29 deletions languageserver/src/main/scala/langserver/core/LanguageServer.scala
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,9 @@ class LanguageServer(inStream: InputStream, outStream: OutputStream) extends Laz
InitializeResult(initialize(pid, rootPath, capabilities))
case TextDocumentPositionParams(textDocument, position) =>
completionRequest(textDocument, position)
case Shutdown() =>
shutdown()
ShutdownResult(0) // the value is a dummy, because Play Json needs to serialize something
case c =>
logger.error(s"Unknown command $c")
sys.error("Unknown command")
Expand All @@ -36,34 +39,6 @@ class LanguageServer(inStream: InputStream, outStream: OutputStream) extends Laz
connection.start()
}

/**
* 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 = {
connection.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: Int, message: String): Unit = {
connection.sendNotification(LogMessageParams(tpe, message))
}

/**
* Publish compilation errors for the given file.
*/
def publishDiagnostics(uri: String, diagnostics: Seq[Diagnostic]): Unit = {
connection.sendNotification(PublishDiagnostics(uri, diagnostics))
}

def onOpenTextDocument(td: TextDocumentItem) = {
logger.debug(s"openTextDocuemnt $td")
}
Expand All @@ -74,7 +49,7 @@ class LanguageServer(inStream: InputStream, outStream: OutputStream) extends Laz

def onSaveTextDocument(td: TextDocumentIdentifier) = {
logger.debug(s"saveTextDocuemnt $td")
showMessage(MessageType.Info, s"Saved text document ${td.uri}")
connection.showMessage(MessageType.Info, s"Saved text document ${td.uri}")
}

def onCloseTextDocument(td: TextDocumentIdentifier) = {
Expand All @@ -94,4 +69,7 @@ class LanguageServer(inStream: InputStream, outStream: OutputStream) extends Laz
CompletionList(isIncomplete = false, Nil)
}

def shutdown(): Unit = {

}
}
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,8 @@ object Shutdown {
Writes(s => Json.obj()))
}

case class ShutdownResult(dummy: Int) extends ResultResponse

case class ShowMessageRequestParams(
/**
* The message type. @see MessageType
Expand Down Expand Up @@ -220,6 +222,7 @@ object Notification extends NotificationCompanion[Notification] {
override val NotificationFormats = Message.MethodFormats(
"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],
Expand All @@ -230,5 +233,6 @@ object Notification extends NotificationCompanion[Notification] {
object ResultResponse extends ResponseCompanion[ResultResponse] {
override val ResponseFormats = Message.MethodFormats(
"initialize" -> Json.format[InitializeResult],
"textDocument/completion" -> Json.format[CompletionList])
"textDocument/completion" -> Json.format[CompletionList],
"shutdown" -> Json.format[ShutdownResult])
}
5 changes: 4 additions & 1 deletion languageserver/src/main/scala/langserver/types/types.scala
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,10 @@ case class Diagnostic(
code: Option[String], // a code for this diagnostic
source: Option[String], // the source of this diagnostic (like 'typescript' or 'scala')
message: String // the diagnostic message
)
)
object Diagnostic {
implicit val format = Json.format[Diagnostic]
}

/**
* A reference to a command.
Expand Down

0 comments on commit 699c9c6

Please sign in to comment.