diff --git a/.gitignore b/.gitignore index 16bb334..68179a7 100644 --- a/.gitignore +++ b/.gitignore @@ -5,4 +5,5 @@ target/ .settings/ node_modules/ out/ -.DS_Store \ No newline at end of file +.DS_Store +.ensime_cache \ No newline at end of file diff --git a/build.sbt b/build.sbt index 8c6930f..43e446a 100644 --- a/build.sbt +++ b/build.sbt @@ -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" ) @@ -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) + } + ) diff --git a/ensime-server/src/main/resources/logback.groovy b/ensime-server/src/main/resources/logback.groovy new file mode 100644 index 0000000..7476c53 --- /dev/null +++ b/ensime-server/src/main/resources/logback.groovy @@ -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"]) diff --git a/ensime-server/src/main/scala/org/github/dragos/vscode/EnsimeLanguageServer.scala b/ensime-server/src/main/scala/org/github/dragos/vscode/EnsimeLanguageServer.scala new file mode 100644 index 0000000..c55a585 --- /dev/null +++ b/ensime-server/src/main/scala/org/github/dragos/vscode/EnsimeLanguageServer.scala @@ -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") + } +} diff --git a/ensime-server/src/main/scala/org/github/dragos/vscode/EnsimeProjectServer.scala b/ensime-server/src/main/scala/org/github/dragos/vscode/EnsimeProjectServer.scala new file mode 100644 index 0000000..f77cf5c --- /dev/null +++ b/ensime-server/src/main/scala/org/github/dragos/vscode/EnsimeProjectServer.scala @@ -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}") + } +} diff --git a/ensime-server/src/main/scala/org/github/dragos/vscode/Main.scala b/ensime-server/src/main/scala/org/github/dragos/vscode/Main.scala new file mode 100644 index 0000000..459afcb --- /dev/null +++ b/ensime-server/src/main/scala/org/github/dragos/vscode/Main.scala @@ -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() + } +} diff --git a/languageserver/bin/logback.groovy b/languageserver/bin/logback.groovy new file mode 100644 index 0000000..cc7dcf9 --- /dev/null +++ b/languageserver/bin/logback.groovy @@ -0,0 +1,9 @@ +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/LanguageServer.scala b/languageserver/src/main/scala/langserver/core/LanguageServer.scala index 9454c66..aa5a904 100644 --- a/languageserver/src/main/scala/langserver/core/LanguageServer.scala +++ b/languageserver/src/main/scala/langserver/core/LanguageServer.scala @@ -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") @@ -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. * @@ -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") @@ -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) } } diff --git a/languageserver/src/main/scala/langserver/messages/Commands.scala b/languageserver/src/main/scala/langserver/messages/Commands.scala index 93a6789..4d841fd 100644 --- a/languageserver/src/main/scala/langserver/messages/Commands.scala +++ b/languageserver/src/main/scala/langserver/messages/Commands.scala @@ -49,69 +49,94 @@ case class ServerCapabilities( /** * Defines how text documents are synced. */ - textDocumentSync: Int = TextDocumentSyncKind.Full /* + textDocumentSync: Int = TextDocumentSyncKind.Full, /** * The server provides hover support. */ - hoverProvider: Boolean, + hoverProvider: Boolean = false, /** * The server provides completion support. */ - completionProvider: CompletionOptions, + completionProvider: Option[CompletionOptions], /** * The server provides signature help support. */ - signatureHelpProvider: SignatureHelpOptions, + signatureHelpProvider: Option[SignatureHelpOptions] = None, /** * The server provides goto definition support. */ - definitionProvider: Boolean, + definitionProvider: Boolean = false, /** * The server provides find references support. */ - referencesProvider: Boolean, + referencesProvider: Boolean = false, /** * The server provides document highlight support. */ - documentHighlightProvider: Boolean, + documentHighlightProvider: Boolean = false, /** * The server provides document symbol support. */ - documentSymbolProvider: Boolean, + documentSymbolProvider: Boolean = false, /** * The server provides workspace symbol support. */ - workspaceSymbolProvider: Boolean, + workspaceSymbolProvider: Boolean = false, /** * The server provides code actions. */ - codeActionProvider: Boolean, + codeActionProvider: Boolean = false, /** * The server provides code lens. */ - codeLensProvider: CodeLensOptions, + codeLensProvider: Option[CodeLensOptions] = None, /** * The server provides document formatting. */ - documentFormattingProvider: Boolean, + documentFormattingProvider: Boolean = false, /** * The server provides document range formatting. */ - documentRangeFormattingProvider: Boolean, + documentRangeFormattingProvider: Boolean = false, /** * The server provides document formatting on typing. */ - documentOnTypeFormattingProvider: DocumentOnTypeFormattingOptions, + documentOnTypeFormattingProvider: Option[DocumentOnTypeFormattingOptions] = None, /** * The server provides rename support. */ - renameProvider: Boolean - */ ) + renameProvider: Boolean = false +) object ServerCapabilities { implicit val format = 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 CompletionList(isIncomplete: Boolean, items: Seq[CompletionItem]) extends ResultResponse +object CompletionList { + implicit val format = Json.format[CompletionList] +} + case class InitializeResult(capabilities: ServerCapabilities) extends ResultResponse case class Shutdown() extends ServerCommand @@ -204,5 +229,6 @@ object Notification extends NotificationCompanion[Notification] { object ResultResponse extends ResponseCompanion[ResultResponse] { override val ResponseFormats = Message.MethodFormats( - "initialize" -> Json.format[InitializeResult]) + "initialize" -> Json.format[InitializeResult], + "textDocument/completion" -> Json.format[CompletionList]) } diff --git a/languageserver/src/main/scala/langserver/types/types.scala b/languageserver/src/main/scala/langserver/types/types.scala index 753e708..c83ff2a 100644 --- a/languageserver/src/main/scala/langserver/types/types.scala +++ b/languageserver/src/main/scala/langserver/types/types.scala @@ -77,7 +77,7 @@ case class TextDocumentItem( object TextDocumentItem { implicit val format = Json.format[TextDocumentItem] } - + object CompletionItemKind { final val Text = 1 final val Method = 2 @@ -108,11 +108,13 @@ case class CompletionItem( filterText: Option[String], insertText: Option[String], textEdit: Option[String], - data: Option[Any]) // An data entry field that is preserved on a completion item between + data: Option[String]) // An data entry field that is preserved on a completion item between // a [CompletionRequest](#CompletionRequest) and a [CompletionResolveRequest] -// (#CompletionResolveRequest) +// (#CompletionResolveRequest) -case class CompletionList(isIncomplete: Boolean, items: Seq[CompletionItem]) +object CompletionItem { + implicit def format = Json.format[CompletionItem] +} case class MarkedString(language: String, value: String) { def this(value: String) { @@ -311,22 +313,22 @@ case class TextDocumentChangeEvent(document: TextDocument) * the new text is considered to be the full content of the document. */ case class TextDocumentContentChangeEvent( - /** - * The range of the document that changed. - */ - range: Option[Range], - - /** - * The length of the range that got replaced. - */ - rangeLength: Option[Int], - - /** - * The new text of the document. - */ - text: String + /** + * The range of the document that changed. + */ + range: Option[Range], + + /** + * The length of the range that got replaced. + */ + rangeLength: Option[Int], + + /** + * The new text of the document. + */ + text: String ) object TextDocumentContentChangeEvent { implicit val format = Json.format[TextDocumentContentChangeEvent] -} \ No newline at end of file +} diff --git a/project/plugins.sbt b/project/plugins.sbt index 39c1bb8..4dc3c19 100644 --- a/project/plugins.sbt +++ b/project/plugins.sbt @@ -1 +1,2 @@ addSbtPlugin("com.eed3si9n" % "sbt-assembly" % "0.14.3") +addSbtPlugin("org.ensime" % "sbt-ensime" % "1.0.0") \ No newline at end of file diff --git a/scala/src/extension.ts b/scala/src/extension.ts index ab1f357..27a964d 100644 --- a/scala/src/extension.ts +++ b/scala/src/extension.ts @@ -11,10 +11,14 @@ import { LanguageClient, LanguageClientOptions, SettingMonitor, ServerOptions, T export function activate(context: ExtensionContext) { + + let toolsJar = process.env.JAVA_HOME + "/lib/tools.jar" + console.info("Adding to classpath " + toolsJar); + // The server is implemented in Scala - let assemblyPath = path.join(context.extensionPath, "../languageserver/target/scala-2.11/languageserver-assembly-0.1.0.jar") + let assemblyPath = path.join(context.extensionPath, "../ensime-server/target/scala-2.11/ensimeServer-assembly-0.1.0.jar") - let javaArgs = [ "-jar", assemblyPath ]; + let javaArgs = [ "-cp", toolsJar + ":" + assemblyPath, "org.github.dragos.vscode.Main" ]; // The debug options for the server let debugOptions = { execArgv: ["--nolazy", "--debug=6004"] };