PSI-LSIF Indexer is an IntelliJ IDEA plugin used to generate LSIF dump.
The Language Server Index Format (LSIF) is a standard format for language servers or other programming tools to emit their knowledge about a code workspace. This persisted information can later be used to answer LSP requests for the same workspace without running a language server
The Program Structure Interface (PSI) is a layer in the IntelliJ Platform that is responsible for parsing files and creating the syntactic and semantic code model that powers so many of the platform’s features.
The reasons of building an LSIF indexer on top of IntelliJ platform:
- PSI capability: IntelliJ IDEs (and language plugins) already have PSI implementations for most languages, so the psi-lsif plugin could be used across different IDEs.
- Client-side generation: reuse the available indexes already generated from IDE, thus reduce the cost of generating LSIF from analysing the source to just converting PSI information to LSIF.
Future work
- Extensibility of LSIF:
- Cross-file & cross-repo implementation
- There are numerous code inspections exist for Java and other languages, these code analysis capability could be also extended to the LSIF implementation
- Server-side generation:
- Integrate the psi-lsif indexer into headless IntelliJ runtime
Protocol & ADT
constants
enum class ElementTypes {
@SerializedName("vertex") VERTEX,
@SerializedName("edge") EDGE
}
object Label {
object Vertex {
const val document = "document"
const val metaData = "metaData"
const val project = "project"
const val hoverResult = "hoverResult"
const val definitionResult = "definitionResult"
const val referenceResult = "referenceResult"
const val resultSet = "resultSet"
const val range = "range"
}
object Edge {
const val hover = "textDocument/hover"
const val definition = "textDocument/definition"
const val references = "textDocument/references"
const val next = "next"
const val item = "item"
const val contains = "contains"
}
object Property {
const val definitions = "definitions"
const val references = "references"
}
}
typealias Uri = String
Edge definitions
abstract class Edge(
val type: ElementTypes = ElementTypes.EDGE
) {
abstract val id: Number
abstract val label: String
abstract val outV: Number
}
data class Edge11( ..., val inV: Number) : Edge()
data class Edge1N( ...,
val inVs: List<Number>,
val property: String? = null,
val document: Number? = null
) : Edge()
Vertex definitions
abstract class Vertex(
val type: ElementTypes = ElementTypes.VERTEX
) {
abstract val id: Number
abstract val label: String
}
data class DocumentVertex(
override val id: Number,
val languageId: String,
val uri: Uri,
val contents: String,
override val label: String = Label.Vertex.document
): Vertex()
data class BaseVertex( ... ): Vertex()
data class MetaDataVertex( ... ): Vertex()
data class ProjectVertex( ... ): Vertex()
data class RangeVertex( ... ): Vertex()
data class HoverResultVertex( ... ): Vertex()
Emitter
helper class with static method for dumping lsif file
object Emitter {
private val gson = Gson()
private val edges = mutableListOf<Edge1N>()
private var writer: BufferedWriter? = null
fun start(lsifPath: String, id: AtomicInteger, action: () -> Unit) {
ApplicationManager.getApplication().executeOnPooledThread {
// try catch block
writer = FileOutputStream(File(lsifPath)).bufferedWriter(Charsets.UTF_8)
action()
unionEdge1NsAndCommit(id)
writer?.close()
}
}
fun emitVertex(vertex: Vertex) {
writer?.append("${gson.toJson(vertex)}\n")
}
fun emitEdge(edge: Edge) {
if (edge is Edge1N) {
edges.add(edge)
} else {
writer?.append("${gson.toJson(edge)}\n")
}
}
private fun unionEdge1NsAndCommit(id: AtomicInteger) {
edges.groupBy { it.label }.forEach { (_, list) ->
list.groupBy { it.outV }.forEach { (k, v) ->
val item = v.first()
val edge = Edge1N(id.getAndIncrement(), item.label, k, v.flatMap { it.inVs }.distinct(), item.property, item.document)
writer?.append("${gson.toJson(edge)}\n")
}
}
}
}
Indexer
finally the projectIndexer & documentIndexer
class DocumentIndexer (
private val id: AtomicInteger,
private val psiFile: PsiFile,
private val document: Document
) {
private fun buildHoverText(psiElement: PsiElement) {
val (name, doc) = when (psiElement) {
is PsiClass -> listOf(
PsiFormatUtil.formatClass(
psiElement,
SHOW_NAME or SHOW_FQ_NAME or SHOW_ANONYMOUS_CLASS_VERBOSE)
, psiElement.docComment?.text.orEmpty()
)
is PsiMethod -> listOf(
PsiFormatUtil.formatMethod(
psiElement,
PsiSubstitutor.EMPTY,
SHOW_NAME or SHOW_PARAMETERS or SHOW_TYPE or
SHOW_MODIFIERS or SHOW_THROWS,
SHOW_NAME or SHOW_PARAMETERS or SHOW_TYPE
), psiElement.docComment?.text.orEmpty()
)
is PsiVariable -> listOf("", psiElement.text.orEmpty())
else -> listOf("", "")
}
return "```${JAVA_MKD_PREFIX}\n" + (if (doc.isBlank()) "" else "${doc.padJavaDocIndent()}\n") + "${name}\n```";
}
private fun emitDefinition(psiElement: PsiElement) {
// ...
}
}