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

Commit

Permalink
Added ensime-based implementation of LangServer
Browse files Browse the repository at this point in the history
Managed to start ensime and get some events flowing.
  • Loading branch information
dragos committed Sep 18, 2016
1 parent d558b18 commit 2149326
Show file tree
Hide file tree
Showing 12 changed files with 299 additions and 47 deletions.
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,5 @@ target/
.settings/
node_modules/
out/
.DS_Store
.DS_Store
.ensime_cache
20 changes: 19 additions & 1 deletion build.sbt
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
name := "vscode-scala"


scalaVersion in ThisBuild := "2.11.8"

lazy val commonSettings = Seq(
organization := "com.github.dragos",
version := "0.1.0",
scalaVersion := "2.11.8",
resolvers += "dhpcs at bintray" at "https://dl.bintray.com/dhpcs/maven"
)

Expand All @@ -19,3 +21,19 @@ lazy val languageserver = project.
"org.codehaus.groovy" % "groovy" % "2.4.0"
)
)

lazy val ensimeServer = project.
in(file("ensime-server")).
dependsOn(languageserver).
settings(commonSettings:_*).
settings(
libraryDependencies ++= Seq(
"org.ensime" %% "core" % "1.0.0"
),
assemblyMergeStrategy in assembly := {
case PathList("org", "apache", "commons", "vfs2", xs @ _*) => MergeStrategy.first // assumes our classpath is setup correctly
case PathList("scala", "reflect", "io", xs @ _*) => MergeStrategy.first // assumes our classpath is setup correctly
case PathList("logback.groovy", xs @ _*) => MergeStrategy.first // assumes our classpath is setup correctly
case other => MergeStrategy.defaultMergeStrategy(other)
}
)
12 changes: 12 additions & 0 deletions ensime-server/src/main/resources/logback.groovy
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
appender("FILE", FileAppender) {
file = "ensime-langserver.log"
append = false
encoder(PatternLayoutEncoder) {
pattern = "%level %logger - %msg%n"
}
}

root(DEBUG, ["FILE"])
logger("slick", ERROR, ["FILE"])
logger("langserver.core", ERROR, ["FILE"])
logger("scala.tools.nsc", ERROR, ["FILE"])
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
package org.github.dragos.vscode

import java.io.File
import java.io.InputStream
import java.io.OutputStream

import org.ensime.api._
import org.ensime.config.EnsimeConfigProtocol

import com.google.common.base.Charsets
import com.google.common.io.Files

import akka.actor.ActorSystem
import langserver.core.LanguageServer
import langserver.messages._
import langserver.types.TextDocumentIdentifier
import langserver.types.TextDocumentContentChangeEvent
import langserver.types.VersionedTextDocumentIdentifier
import langserver.types.TextDocumentItem
import akka.actor.ActorRef
import akka.actor.Props
import org.ensime.api.TypecheckFileReq
import java.net.URI
import scalariform.formatter.preferences.FormattingPreferences

class EnsimeLanguageServer(in: InputStream, out: OutputStream) extends LanguageServer(in, out) {
private val system = ActorSystem("ENSIME")

private var ensimeProject: ActorRef = _

override def initialize(pid: Long, rootPath: String, capabilities: ClientCapabilities): ServerCapabilities = {
logger.info(s"Initialized with $pid, $rootPath, $capabilities")
val rootFile = new File(rootPath)
val cacheDir = new File(rootFile, ".ensime-vscode-cache")
cacheDir.mkdir()
val noConfig = EnsimeConfig(
rootFile,
cacheDir,
javaHome = new File(scala.util.Properties.javaHome),
name = "scala",
scalaVersion = "2.11.8",
compilerArgs = Nil,
referenceSourceRoots = Nil,
subprojects = Nil,
formattingPrefs = FormattingPreferences(),
sourceMode = false,
javaLibs = Nil)

val ensimeFile = new File(s"$rootPath/.ensime")
val config: EnsimeConfig = try {
EnsimeConfigProtocol.parse(Files.toString(ensimeFile, Charsets.UTF_8))
} catch {
case e: Throwable =>
showMessage(MessageType.Error, s"There was a problem parsing $ensimeFile ${e.getMessage}")
noConfig
}
showMessage(MessageType.Info, s"Using configuration: $ensimeFile")
logger.info(s"Using configuration: $config")

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

// we don't give a damn about them, but Ensime expects it
ensimeProject ! ConnectionInfoReq
ServerCapabilities(completionProvider = Some(CompletionOptions(false, Seq("."))))
}

override def onOpenTextDocument(td: TextDocumentItem) = {
logger.debug(s"openTextDocuemnt $td")

val f = new File(new URI(td.uri))
ensimeProject ! TypecheckFileReq(SourceFileInfo(f, Some(td.text)))
}

override def onChangeTextDocument(td: VersionedTextDocumentIdentifier, changes: Seq[TextDocumentContentChangeEvent]) = {
logger.debug(s"changeTextDocuemnt $td")

val f = new File(new URI(td.uri))

// we assume full text sync
assert(changes.size == 1)
val change = changes.head
assert(change.range.isEmpty)
assert(change.rangeLength.isEmpty)

ensimeProject ! TypecheckFileReq(SourceFileInfo(f, Some(change.text)))
}

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")
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
package org.github.dragos.vscode

import scala.collection.mutable.ListBuffer
import scala.concurrent.duration._

import org.ensime.api._
import org.ensime.api.EnsimeConfig
import org.ensime.core.Broadcaster
import org.ensime.core.Project

import com.typesafe.scalalogging.LazyLogging

import akka.actor.Actor
import akka.util.Timeout

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

val broadcaster = context.actorOf(Broadcaster(), "broadcaster")
val project = context.actorOf(Project(broadcaster), "project")

override def preStart() {
broadcaster ! Broadcaster.Register
}

val compilerDiagnostics: ListBuffer[Note] = ListBuffer.empty

override def receive = {
case ClearAllScalaNotesEvent =>
compilerDiagnostics.clear()

case NewScalaNotesEvent(isFull, notes) =>
compilerDiagnostics ++= notes
publishDiagnostics()

case AnalyzerReadyEvent =>
logger.info("Analyzer is ready!")

case FullTypeCheckCompleteEvent =>
logger.info("Full typecheck complete event")

case message =>
project forward message
}

private def publishDiagnostics(): Unit = {
logger.debug(s"Scala notes: ${compilerDiagnostics}")
}
}
14 changes: 14 additions & 0 deletions ensime-server/src/main/scala/org/github/dragos/vscode/Main.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package org.github.dragos.vscode

import com.typesafe.scalalogging.LazyLogging
import scala.util.Properties

object Main extends LazyLogging {
def main(args: Array[String]): Unit = {
logger.info(s"Starting server in ${System.getenv("PWD")}")
logger.info(s"Classpath: ${Properties.javaClassPath}")

val server = new EnsimeLanguageServer(System.in, System.out)
server.start()
}
}
9 changes: 9 additions & 0 deletions languageserver/bin/logback.groovy
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
appender("FILE", FileAppender) {
file = "scala-langserver.log"
append = false
encoder(PatternLayoutEncoder) {
pattern = "%level %logger - %msg%n"
}
}

root(DEBUG, ["FILE"])
34 changes: 27 additions & 7 deletions languageserver/src/main/scala/langserver/core/LanguageServer.scala
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,15 @@ import langserver.messages._
import langserver.types._
import play.api.libs.json.JsObject

/**
* A language server implementation. Users should subclass this class and implement specific behavior.
*/
class LanguageServer(inStream: InputStream, outStream: OutputStream) extends LazyLogging {
val connection = (new ConnectionImpl(inStream, outStream)) {
case InitializeParams(pid, rootPath, capabilities) =>
InitializeResult(initialize(pid, rootPath, capabilities))
case TextDocumentPositionParams(textDocument, position) =>
completionRequest(textDocument, position)
case c =>
logger.error(s"Unknown command $c")
sys.error("Unknown command")
Expand All @@ -31,12 +36,6 @@ class LanguageServer(inStream: InputStream, outStream: OutputStream) extends Laz
connection.start()
}

// def initialize(params: InitializeParams): Unit

// def shutdown(): Unit
//
// def onDidChangeConfiguration(params: Any): Unit

/**
* A notification sent to the client to show a message.
*
Expand All @@ -47,6 +46,23 @@ class LanguageServer(inStream: InputStream, outStream: OutputStream) extends Laz
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 @@ -71,7 +87,11 @@ class LanguageServer(inStream: InputStream, outStream: OutputStream) extends Laz

def initialize(pid: Long, rootPath: String, capabilities: ClientCapabilities): ServerCapabilities = {
logger.info(s"Initialized with $pid, $rootPath, $capabilities")
ServerCapabilities()
ServerCapabilities(completionProvider = Some(CompletionOptions(false, Seq("."))))
}

def completionRequest(textDocument: TextDocumentIdentifier, position: Position): ResultResponse = {
CompletionList(isIncomplete = false, Nil)
}

}
Loading

0 comments on commit 2149326

Please sign in to comment.