intellij-lsp/intellij-lsp-plugin

View on GitHub
src/main/scala/com/github/gtache/lsp/PluginMain.scala

Summary

Maintainability
A
3 hrs
Test Coverage
F
0%
/**
 *     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.
 */
package com.github.gtache.lsp

import java.util.concurrent.{TimeUnit, TimeoutException}

import com.github.gtache.lsp.client.languageserver.ServerStatus
import com.github.gtache.lsp.client.languageserver.serverdefinition.LanguageServerDefinition
import com.github.gtache.lsp.client.languageserver.wrapper.{LanguageServerWrapper, LanguageServerWrapperImpl}
import com.github.gtache.lsp.contributors.LSPNavigationItem
import com.github.gtache.lsp.editor.listeners.{EditorListener, FileDocumentManagerListenerImpl, VFSListener}
import com.github.gtache.lsp.requests.{Timeout, Timeouts}
import com.github.gtache.lsp.settings.LSPState
import com.github.gtache.lsp.utils.{ApplicationUtils, FileUtils, GUIUtils}
import com.intellij.AppTopics
import com.intellij.navigation.NavigationItem
import com.intellij.openapi.application.ApplicationManager
import com.intellij.openapi.components.ApplicationComponent
import com.intellij.openapi.diagnostic.Logger
import com.intellij.openapi.editor.{Editor, EditorFactory}
import com.intellij.openapi.fileEditor.{FileDocumentManager, FileEditorManager, TextEditor}
import com.intellij.openapi.project.{Project, ProjectManager}
import com.intellij.openapi.util.Disposer
import com.intellij.openapi.vfs.{VirtualFile, VirtualFileManager}
import org.eclipse.lsp4j._

import scala.collection.immutable.HashMap
import scala.collection.mutable

/**
  * The main class of the plugin
  */
object PluginMain {

  private val LOG: Logger = Logger.getInstance(classOf[PluginMain])
  private val extToLanguageWrapper: mutable.Map[(String, String), LanguageServerWrapper] = mutable.HashMap()
  private val projectToLanguageWrappers: mutable.Map[String, mutable.Set[LanguageServerWrapper]] = mutable.HashMap()
  private var extToServerDefinition: Map[String, LanguageServerDefinition] = HashMap()
  private var loadedExtensions: Boolean = false

  /**
    * @return All instantiated ServerWrappers
    */
  def getAllServerWrappers: Set[LanguageServerWrapper] = {
    projectToLanguageWrappers.values.flatten.toSet
  }

  /**
    * @param ext An extension
    * @return True if there is a LanguageServer supporting this extension, false otherwise
    */
  def isExtensionSupported(ext: String): Boolean = {
    extToServerDefinition.contains(ext)
  }

  /**
    * Sets the extensions->languageServer mapping
    *
    * @param newExt a Java Map
    */
  def setExtToServerDefinition(newExt: java.util.Map[String, _ <: LanguageServerDefinition]): Unit = {
    import scala.collection.JavaConverters._
    setExtToServerDefinition(newExt.asScala)
  }

  /**
    * Sets the extensions->languageServer mapping
    *
    * @param newExt a Scala map
    */
  def setExtToServerDefinition(newExt: collection.Map[String, _ <: LanguageServerDefinition]): Unit = {
    val nullDef = newExt.filter(d => d._2 == null)
    val oldServerDef = extToServerDefinition
    extToServerDefinition = newExt.toMap.filter(d => d._2 != null)
    flattenExt()
    nullDef.foreach(ext => LOG.error("Definition for " + ext + " is null"))
    ApplicationUtils.pool(() => {
      val added = newExt.keys.filter(e => !oldServerDef.contains(e)).toSet
      val removed = oldServerDef.keys.filter(e => !newExt.contains(e)).toSet
      extToLanguageWrapper.keys.filter(k => removed.contains(k._1)).foreach(k => {
        val wrapper = extToLanguageWrapper(k)
        wrapper.stop()
        wrapper.removeWidget()
        extToLanguageWrapper.remove(k)
      })
      val openedEditors: Iterable[Editor] = ApplicationUtils.computableReadAction(() => ProjectManager.getInstance().getOpenProjects.flatMap(proj => FileEditorManager.getInstance(proj).getAllEditors()).collect { case t: TextEditor => t.getEditor })
      val files = openedEditors.map(e => FileDocumentManager.getInstance().getFile(e.getDocument))
      files.zip(openedEditors).foreach(f => if (added.contains(f._1.getExtension)) editorOpened(f._2))
    })
  }

  private def flattenExt(): Unit = {
    extToServerDefinition = extToServerDefinition.map(p => {
      val ext = p._1
      val sDef = p._2
      val split = ext.split(LanguageServerDefinition.SPLIT_CHAR)
      split.map(s => (s, sDef))
    }).flatten.toMap
  }

  /**
    * Called when an editor is opened. Instantiates a LanguageServerWrapper if necessary, and adds the Editor to the Wrapper
    *
    * @param editor the editor
    */
  def editorOpened(editor: Editor): Unit = {
    if (!loadedExtensions) {
      val extensions = LanguageServerDefinition.getAllDefinitions.filter(s => !extToServerDefinition.contains(s.ext))
      LOG.info("Added serverDefinitions " + extensions + " from plugins")
      extToServerDefinition = extToServerDefinition ++ extensions.map(s => (s.ext, s))
      flattenExt()
      loadedExtensions = true
    }
    val file: VirtualFile = FileDocumentManager.getInstance.getFile(editor.getDocument)
    if (file != null) {
      ApplicationUtils.pool(() => {
        val ext: String = file.getExtension
        val project: Project = editor.getProject
        val rootPath: String = FileUtils.editorToProjectFolderPath(editor)
        val rootUri: String = FileUtils.pathToUri(rootPath)
        LOG.info("Opened " + file.getName)
        extToServerDefinition.get(ext).foreach(s => {
          extToLanguageWrapper.synchronized {
            var wrapper = extToLanguageWrapper.get((ext, rootUri)).orNull
            wrapper match {
              case null =>
                LOG.info("Instantiating wrapper for " + ext + " : " + rootUri)
                wrapper = new LanguageServerWrapperImpl(s, project)
                val exts = s.ext.split(LanguageServerDefinition.SPLIT_CHAR)
                exts.foreach(ext => extToLanguageWrapper.put((ext, rootUri), wrapper))
                projectToLanguageWrappers.get(rootUri) match {
                  case Some(set) =>
                    set.add(wrapper)
                  case None =>
                    projectToLanguageWrappers.put(rootUri, mutable.Set(wrapper))
                }
              case _: LanguageServerWrapperImpl =>
                LOG.info("Wrapper already existing for " + ext + " , " + rootUri)
            }
            LOG.info("Adding file " + file.getName)
            wrapper.connect(editor)
          }
        })
      })

    } else {
      LOG.warn("File for editor " + editor.getDocument.getText + " is null")
    }
  }

  /**
    * Returns the extensions->languageServer mapping
    *
    * @return the Scala map
    */
  def getExtToServerDefinition: Map[String, LanguageServerDefinition] = extToServerDefinition

  /**
    * Returns the extensions->languageServer mapping
    *
    * @return The Java map
    */
  def getExtToServerDefinitionJava: java.util.Map[String, LanguageServerDefinition] = {
    import scala.collection.JavaConverters._
    extToServerDefinition.asJava
  }

  /**
    * Called when an editor is closed. Notifies the LanguageServerWrapper if needed
    *
    * @param editor the editor.
    */
  def editorClosed(editor: Editor): Unit = {
    val file: VirtualFile = FileDocumentManager.getInstance.getFile(editor.getDocument)
    if (file != null) {
      val ext: String = file.getExtension
      extToServerDefinition.get(ext) match {
        case Some(_) =>
          val uri = FileUtils.editorToURIString(editor)
          ApplicationUtils.pool(() => {
            LanguageServerWrapperImpl.forUri(uri).foreach(l => {
              LOG.info("Disconnecting " + uri)
              l.disconnect(uri)
            })
          })
        case None =>
          LOG.info("Closing LSP-unsupported file with extension " + ext)
      }
    } else {
      LOG.warn("File for document " + editor.getDocument.getText + " is null")
    }
  }


  /**
    * Returns the corresponding workspaceSymbols given a name and a project
    *
    * @param name                   The name to search for
    * @param pattern                The pattern (unused)
    * @param project                The project in which to search
    * @param includeNonProjectItems Whether to search in libraries for example (unused)
    * @param onlyKind               Filter the results to only the kinds in the set (all by default)
    * @return An array of NavigationItem
    */
  def workspaceSymbols(name: String, pattern: String, project: Project, includeNonProjectItems: Boolean = false, onlyKind: Set[SymbolKind] = Set()): Array[NavigationItem] = {
    projectToLanguageWrappers.get(FileUtils.pathToUri(project.getBasePath)) match {
      case Some(set) =>
        val params: WorkspaceSymbolParams = new WorkspaceSymbolParams(name)
        val servDefToReq = set.collect {
          case w: LanguageServerWrapper if w.getStatus == ServerStatus.STARTED && w.getRequestManager != null =>
            (w, w.getRequestManager.symbol(params))
        }.toSet.filter(w => w._2 != null)
        if (!servDefToReq.contains(null)) {
          import scala.collection.JavaConverters._
          val servDefToSymb = servDefToReq.map(w => {
            try {
              val symbols = w._2.get(Timeout.SYMBOLS_TIMEOUT, TimeUnit.MILLISECONDS)
              w._1.notifyResult(Timeouts.SYMBOLS, success = true)
              (w._1, if (symbols != null) symbols.asScala
                .filter(s => if (onlyKind.isEmpty) true else onlyKind.contains(s.getKind)) else null)
            } catch {
              case e: TimeoutException =>
                LOG.warn(e)
                w._1.notifyResult(Timeouts.SYMBOLS, success = false)
                null
            }
          }
          ).filter(r => r._2 != null)
          servDefToSymb.flatMap(res => {
            val definition = res._1
            val symbols = res._2
            symbols.map(symb => {
              val start = symb.getLocation.getRange.getStart
              val uri = FileUtils.URIToVFS(symb.getLocation.getUri)
              val iconProvider = GUIUtils.getIconProviderFor(definition.getServerDefinition)
              LSPNavigationItem(symb.getName, symb.getContainerName, project, uri, start.getLine, start.getCharacter, iconProvider.getSymbolIcon(symb.getKind))
            })
          }).toArray.asInstanceOf[Array[NavigationItem]]

        } else Array.empty
      case None => LOG.info("No wrapper for project " + project.getBasePath)
        Array.empty
    }
  }
}

/**
  * The main class of the plugin
  */
class PluginMain extends ApplicationComponent {

  import PluginMain._

  override val getComponentName: String = "PluginMain"

  override def initComponent(): Unit = {
    LSPState.getInstance.getState //Need that to trigger loadState
    EditorFactory.getInstance.addEditorFactoryListener(new EditorListener, Disposer.newDisposable())
    VirtualFileManager.getInstance().addVirtualFileListener(VFSListener)
    ApplicationManager.getApplication.getMessageBus.connect().subscribe(AppTopics.FILE_DOCUMENT_SYNC, FileDocumentManagerListenerImpl)
    LOG.info("PluginMain init finished")
  }
}