src/main/scala/com/github/gtache/lsp/client/languageserver/wrapper/LanguageServerWrapperImpl.scala
/**
* Copyright 2017-2018 Guillaume Tâche
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/* Adapted from lsp4e */
package com.github.gtache.lsp.client.languageserver.wrapper
import java.io.IOException
import java.net.URI
import java.util.concurrent._
import com.github.gtache.lsp.PluginMain
import com.github.gtache.lsp.client.languageserver.requestmanager.{RequestManager, SimpleRequestManager}
import com.github.gtache.lsp.client.languageserver.serverdefinition.LanguageServerDefinition
import com.github.gtache.lsp.client.languageserver.{LSPServerStatusWidget, ServerOptions, ServerStatus}
import com.github.gtache.lsp.client.{DynamicRegistrationMethods, LanguageClientImpl}
import com.github.gtache.lsp.editor.EditorEventManager
import com.github.gtache.lsp.editor.listeners.{DocumentListenerImpl, EditorMouseListenerImpl, EditorMouseMotionListenerImpl, SelectionListenerImpl}
import com.github.gtache.lsp.requests.{Timeout, Timeouts}
import com.github.gtache.lsp.utils.{ApplicationUtils, FileUtils, LSPException}
import com.intellij.openapi.diagnostic.Logger
import com.intellij.openapi.editor.Editor
import com.intellij.openapi.fileEditor.{FileEditorManager, TextEditor}
import com.intellij.openapi.project.Project
import com.intellij.openapi.ui.Messages
import org.eclipse.lsp4j._
import org.eclipse.lsp4j.jsonrpc.ResponseErrorException
import org.eclipse.lsp4j.jsonrpc.messages.{Either, Message, ResponseErrorCode, ResponseMessage}
import org.eclipse.lsp4j.launch.LSPLauncher
import org.eclipse.lsp4j.services.LanguageServer
import org.jetbrains.annotations.Nullable
import scala.collection.concurrent.TrieMap
import scala.collection.mutable
object LanguageServerWrapperImpl {
private val uriToLanguageServerWrapper: mutable.Map[String, LanguageServerWrapper] = TrieMap()
private val editorToLanguageServerWrapper: mutable.Map[Editor, LanguageServerWrapper] = TrieMap()
/**
* @param uri A file uri
* @return The wrapper for the given uri, or None
*/
def forUri(uri: String): Option[LanguageServerWrapper] = {
uriToLanguageServerWrapper.get(uri)
}
/**
* @param editor An editor
* @return The wrapper for the given editor, or None
*/
def forEditor(editor: Editor): Option[LanguageServerWrapper] = {
editorToLanguageServerWrapper.get(editor)
}
}
/**
* The implementation of a LanguageServerWrapper (specific to a serverDefinition and a project)
*
* @param serverDefinition The serverDefinition
* @param project The project
*/
class LanguageServerWrapperImpl(val serverDefinition: LanguageServerDefinition, val project: Project) extends LanguageServerWrapper {
import LanguageServerWrapperImpl._
import ServerStatus._
private val rootPath = project.getBasePath
private val connectedEditors: mutable.Map[String, EditorEventManager] = mutable.HashMap()
private val LOG: Logger = Logger.getInstance(classOf[LanguageServerWrapperImpl])
private val statusWidget: LSPServerStatusWidget = LSPServerStatusWidget.createWidgetFor(this)
private val registrations: mutable.Map[String, DynamicRegistrationMethods] = mutable.HashMap()
private var crashCount = 0
@volatile private var alreadyShownTimeout = false
@volatile private var alreadyShownCrash = false
@volatile private var status: ServerStatus = ServerStatus.STOPPED
private var languageServer: LanguageServer = _
private var client: LanguageClientImpl = _
private var requestManager: RequestManager = _
private var initializeResult: InitializeResult = _
private var launcherFuture: Future[_] = _
private var initializeFuture: CompletableFuture[InitializeResult] = _
private var capabilitiesAlreadyRequested = false
private var initializeStartTime = 0L
override def getServerDefinition: LanguageServerDefinition = serverDefinition
/**
* @return if the server supports willSaveWaitUntil
*/
def isWillSaveWaitUntil: Boolean = {
val capabilities = getServerCapabilities.getTextDocumentSync
if (capabilities.isLeft) {
false
} else {
capabilities.getRight.getWillSaveWaitUntil
}
}
/**
* Warning: this is a long running operation
*
* @return the languageServer capabilities, or null if initialization job didn't complete
*/
@Nullable def getServerCapabilities: ServerCapabilities = {
if (this.initializeResult != null) this.initializeResult.getCapabilities else {
try {
start()
if (this.initializeFuture != null) this.initializeFuture.get(if (capabilitiesAlreadyRequested) 0 else Timeout.INIT_TIMEOUT, TimeUnit.MILLISECONDS)
notifySuccess(Timeouts.INIT)
} catch {
case e: TimeoutException =>
notifyFailure(Timeouts.INIT)
val msg = "LanguageServer for definition\n " + serverDefinition + "\nnot initialized after " + Timeout.INIT_TIMEOUT / 1000 + "s\nCheck settings"
LOG.warn(msg, e)
ApplicationUtils.invokeLater(() => if (!alreadyShownTimeout) {
Messages.showErrorDialog(msg, "LSP error")
alreadyShownTimeout = true
})
stop()
case e@(_: IOException | _: InterruptedException | _: ExecutionException) =>
LOG.warn(e)
stop()
}
this.capabilitiesAlreadyRequested = true
if (initializeResult != null) this.initializeResult.getCapabilities
else null
}
}
override def notifyResult(timeout: Timeouts, success: Boolean): Unit = {
statusWidget.notifyResult(timeout, success)
}
/**
* Returns the EditorEventManager for a given uri
*
* @param uri the URI as a string
* @return the EditorEventManager (or null)
*/
def getEditorManagerFor(uri: String): EditorEventManager = {
connectedEditors.get(uri).orNull
}
/**
* @return The request manager for this wrapper
*/
def getRequestManager: RequestManager = {
requestManager
}
/**
* @return whether the underlying connection to language languageServer is still active
*/
def isActive: Boolean = this.launcherFuture != null && !this.launcherFuture.isDone && !this.launcherFuture.isCancelled && !alreadyShownTimeout && !alreadyShownCrash
/**
* Connects an editor to the languageServer
*
* @param editor the editor
*/
@throws[IOException]
def connect(editor: Editor): Unit = {
val uri = FileUtils.editorToURIString(editor)
if (!this.connectedEditors.contains(uri)) {
start()
if (this.initializeFuture != null && editor != null) {
val capabilities = getServerCapabilities
if (capabilities != null) {
initializeFuture.thenRun(() => {
if (!this.connectedEditors.contains(uri)) {
val syncOptions: Either[TextDocumentSyncKind, TextDocumentSyncOptions] = if (capabilities == null) null else capabilities.getTextDocumentSync
var syncKind: TextDocumentSyncKind = null
if (syncOptions != null) {
if (syncOptions.isRight) syncKind = syncOptions.getRight.getChange
else if (syncOptions.isLeft) syncKind = syncOptions.getLeft
val mouseListener = new EditorMouseListenerImpl
val mouseMotionListener = new EditorMouseMotionListenerImpl
val documentListener = new DocumentListenerImpl
val selectionListener = new SelectionListenerImpl
val serverOptions = ServerOptions(syncKind, capabilities.getCompletionProvider, capabilities.getSignatureHelpProvider, capabilities.getCodeLensProvider, capabilities.getDocumentOnTypeFormattingProvider, capabilities.getDocumentLinkProvider, capabilities.getExecuteCommandProvider)
val manager = new EditorEventManager(editor, mouseListener, mouseMotionListener, documentListener, selectionListener, requestManager, serverOptions, this)
mouseListener.setManager(manager)
mouseMotionListener.setManager(manager)
documentListener.setManager(manager)
selectionListener.setManager(manager)
manager.registerListeners()
this.connectedEditors.put(uri, manager)
editorToLanguageServerWrapper.put(editor, this)
uriToLanguageServerWrapper.put(uri, this)
manager.documentOpened()
LOG.info("Created a manager for " + uri)
}
}
})
} else {
LOG.warn("Capabilities are null for " + serverDefinition)
}
} else {
LOG.warn(if (editor == null) "editor is null for " + serverDefinition else "initializeFuture is null for " + serverDefinition)
}
}
}
/**
* Disconnects an editor from the LanguageServer
*
* @param uri The uri of the editor
*/
def disconnect(uri: String): Unit = {
this.connectedEditors.remove(uri).foreach({ e =>
uriToLanguageServerWrapper.remove(uri)
editorToLanguageServerWrapper.remove(e.editor)
e.removeListeners()
e.documentClosed()
})
if (this.connectedEditors.isEmpty) stop()
}
/**
* Checks if the wrapper is already connected to the document at the given path
*/
def isConnectedTo(location: String): Boolean = connectedEditors.contains(location)
/**
* @return the LanguageServer
*/
@Nullable def getServer: LanguageServer = {
start()
if (initializeFuture != null && !this.initializeFuture.isDone) this.initializeFuture.join
this.languageServer
}
/**
* Starts the LanguageServer
*/
@throws[IOException]
def start(): Unit = {
if (status == STOPPED && !alreadyShownCrash && !alreadyShownTimeout) {
setStatus(STARTING)
try {
val (inputStream, outputStream) = serverDefinition.start(rootPath)
client = serverDefinition.createLanguageClient
val initParams = new InitializeParams
initParams.setRootUri(FileUtils.pathToUri(rootPath))
val launcher = LSPLauncher.createClientLauncher(client, inputStream, outputStream)
this.languageServer = launcher.getRemoteProxy
client.connect(languageServer, this)
this.launcherFuture = launcher.startListening
//TODO update capabilities when implemented
val workspaceClientCapabilities = new WorkspaceClientCapabilities
workspaceClientCapabilities.setApplyEdit(true)
//workspaceClientCapabilities.setDidChangeConfiguration(new DidChangeConfigurationCapabilities)
workspaceClientCapabilities.setDidChangeWatchedFiles(new DidChangeWatchedFilesCapabilities)
workspaceClientCapabilities.setExecuteCommand(new ExecuteCommandCapabilities)
workspaceClientCapabilities.setWorkspaceEdit(new WorkspaceEditCapabilities(true))
workspaceClientCapabilities.setSymbol(new SymbolCapabilities)
workspaceClientCapabilities.setWorkspaceFolders(false)
workspaceClientCapabilities.setConfiguration(false)
val textDocumentClientCapabilities = new TextDocumentClientCapabilities
textDocumentClientCapabilities.setCodeAction(new CodeActionCapabilities)
//textDocumentClientCapabilities.setCodeLens(new CodeLensCapabilities)
textDocumentClientCapabilities.setCompletion(new CompletionCapabilities(new CompletionItemCapabilities(false)))
textDocumentClientCapabilities.setDefinition(new DefinitionCapabilities)
textDocumentClientCapabilities.setDocumentHighlight(new DocumentHighlightCapabilities)
//textDocumentClientCapabilities.setDocumentLink(new DocumentLinkCapabilities)
//textDocumentClientCapabilities.setDocumentSymbol(new DocumentSymbolCapabilities)
textDocumentClientCapabilities.setFormatting(new FormattingCapabilities)
textDocumentClientCapabilities.setHover(new HoverCapabilities)
//textDocumentClientCapabilities.setImplementation(new ImplementationCapabilities())
textDocumentClientCapabilities.setOnTypeFormatting(new OnTypeFormattingCapabilities)
textDocumentClientCapabilities.setRangeFormatting(new RangeFormattingCapabilities)
textDocumentClientCapabilities.setReferences(new ReferencesCapabilities)
textDocumentClientCapabilities.setRename(new RenameCapabilities)
textDocumentClientCapabilities.setSignatureHelp(new SignatureHelpCapabilities)
textDocumentClientCapabilities.setSynchronization(new SynchronizationCapabilities(true, true, true))
//textDocumentClientCapabilities.setTypeDefinition(new TypeDefinitionCapabilities())
initParams.setCapabilities(new ClientCapabilities(workspaceClientCapabilities, textDocumentClientCapabilities, null))
initParams.setInitializationOptions(this.serverDefinition.getInitializationOptions(URI.create(initParams.getRootUri)))
initializeFuture = languageServer.initialize(initParams).thenApply((res: InitializeResult) => {
initializeResult = res
LOG.info("Got initializeResult for " + serverDefinition + " ; " + rootPath)
requestManager = new SimpleRequestManager(this, languageServer, client, res.getCapabilities)
setStatus(STARTED)
res
})
initializeStartTime = System.currentTimeMillis
} catch {
case e@(_: LSPException | _: IOException) =>
LOG.warn(e)
ApplicationUtils.invokeLater(() => Messages.showErrorDialog("Can't start server, please check settings\n" + e.getMessage, "LSP Error"))
removeDefinition()
}
}
}
/**
* @return The language ID that this wrapper is dealing with if defined in the content type mapping for the language languageServer
*/
@Nullable def getLanguageId(contentTypes: Array[String]): String = {
if (contentTypes.exists(serverDefinition.getMappedExtensions.contains(_))) serverDefinition.id else null
}
def logMessage(message: Message): Unit = {
message match {
case responseMessage: ResponseMessage if responseMessage.getError != null && (responseMessage.getId eq Integer.toString(ResponseErrorCode.RequestCancelled.getValue)) =>
LOG.error(new ResponseErrorException(responseMessage.getError))
case _ =>
}
}
def stop(): Unit = {
if (this.initializeFuture != null) {
this.initializeFuture.cancel(true)
this.initializeFuture = null
}
this.initializeResult = null
this.capabilitiesAlreadyRequested = false
if (this.languageServer != null) try {
val shutdown: CompletableFuture[AnyRef] = this.languageServer.shutdown
shutdown.get(Timeout.SHUTDOWN_TIMEOUT, TimeUnit.MILLISECONDS)
notifySuccess(Timeouts.SHUTDOWN)
} catch {
case _: Exception =>
notifyFailure(Timeouts.SHUTDOWN)
// most likely closed externally
}
if (this.launcherFuture != null) {
this.launcherFuture.cancel(true)
this.launcherFuture = null
}
if (this.serverDefinition != null) this.serverDefinition.stop(rootPath)
connectedEditors.foreach(e => disconnect(e._1))
this.languageServer = null
setStatus(STOPPED)
}
override def registerCapability(params: RegistrationParams): CompletableFuture[Void] = {
CompletableFuture.runAsync(() => {
import scala.collection.JavaConverters._
params.getRegistrations.asScala.foreach(r => {
val id = r.getId
val method = DynamicRegistrationMethods.forName(r.getMethod)
val options = r.getRegisterOptions
registrations.put(id, method)
})
})
}
override def unregisterCapability(params: UnregistrationParams): CompletableFuture[Void] = {
CompletableFuture.runAsync(() => {
import scala.collection.JavaConverters._
params.getUnregisterations.asScala.foreach(r => {
val id = r.getId
val method = DynamicRegistrationMethods.forName(r.getMethod)
if (registrations.contains(id)) {
registrations.remove(id)
} else {
val invert = registrations.map(mapping => (mapping._2, mapping._1))
if (invert.contains(method)) {
registrations.remove(invert(method))
}
}
})
})
}
override def getProject: Project = project
override def getStatus: ServerStatus = status
private def setStatus(status: ServerStatus): Unit = {
this.status = status
statusWidget.setStatus(status)
}
override def crashed(e: Exception): Unit = {
crashCount += 1
if (crashCount < 2) {
val editors = connectedEditors.clone().toMap.keys
stop()
editors.foreach(uri => connect(uri))
} else {
removeDefinition()
if (!alreadyShownCrash) ApplicationUtils.invokeLater(() => if (!alreadyShownCrash) {
Messages.showErrorDialog("LanguageServer for definition " + serverDefinition + ", project " + project + " keeps crashing due to \n" + e.getMessage + "\nCheck settings.", "LSP Error")
alreadyShownCrash = true
})
}
}
override def getConnectedFiles: Iterable[String] = {
connectedEditors.keys.map(s => new URI(FileUtils.sanitizeURI(s)).toString)
}
override def removeWidget(): Unit = {
statusWidget.dispose()
}
private def removeDefinition(): Unit = {
stop()
removeWidget()
PluginMain.setExtToServerDefinition(PluginMain.getExtToServerDefinition -- serverDefinition.ext.split(LanguageServerDefinition.SPLIT_CHAR)) //Remove so that the user isn't disturbed anymore
}
private def connect(uri: String): Unit = {
val editors = FileEditorManager.getInstance(project).getAllEditors(FileUtils.URIToVFS(uri))
.collect { case t: TextEditor => t.getEditor }
if (editors.nonEmpty) {
connect(editors.head)
}
}
}