Skip to content

Commit

Permalink
TECH: extend intelllij plugin with refactor options (#29)
Browse files Browse the repository at this point in the history
Co-authored-by: Jerre van Veluw <[email protected]>
  • Loading branch information
wilmveel and jerrevanveluw authored Apr 20, 2023
1 parent 12b2905 commit fc6e521
Show file tree
Hide file tree
Showing 14 changed files with 331 additions and 80 deletions.
2 changes: 1 addition & 1 deletion buildSrc/src/main/java/Versions.kt
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import Versions.coroutines
object Versions {
const val arrow = "1.1.4"
const val coroutines = "1.6.4"
const val intellij = "1.13.0"
const val intellij = "1.13.3"
}

object Libraries {
Expand Down
31 changes: 21 additions & 10 deletions lsp/intellij-plugin/src/main/kotlin/ChooseByNameContributor.kt
Original file line number Diff line number Diff line change
Expand Up @@ -2,20 +2,31 @@ package community.flock.wirespec.lsp.intellij_plugin

import com.intellij.navigation.NavigationItem
import com.intellij.openapi.project.Project
import com.intellij.psi.PsiElement
import com.intellij.psi.PsiManager
import com.intellij.psi.search.FileTypeIndex
import com.intellij.psi.search.GlobalSearchScope
import com.intellij.psi.util.PsiTreeUtil
import com.intellij.navigation.ChooseByNameContributor as IntellijChooseByNameContributor


class ChooseByNameContributor : IntellijChooseByNameContributor {
override fun getNames(project: Project?, includeNonProjectItems: Boolean) =
arrayOf<String>()

override fun getItemsByName(
name: String?,
pattern: String?,
project: Project?,
includeNonProjectItems: Boolean
) =
arrayOf<NavigationItem>()
private lateinit var map: Map<String, PsiElement>

override fun getNames(project: Project, includeNonProjectItems: Boolean) = FileTypeIndex
.getFiles(FileType.INSTANCE, GlobalSearchScope.allScope(project))
.map(PsiManager.getInstance(project)::findFile)
.flatMap { file ->
PsiTreeUtil.getChildrenOfType(file, TypeDefElement::class.java).orEmpty()
.mapNotNull { PsiTreeUtil.findChildOfType(it, CustomTypeElementDef::class.java) }
.map { it.node }
.map { it.chars.toString() to it.psi }
}
.toMap()
.also { map = it }
.keys.toTypedArray()

}
override fun getItemsByName(name: String, pattern: String, project: Project, includeNonProjectItems: Boolean) =
listOfNotNull(map[name] as NavigationItem).toTypedArray()
}
28 changes: 28 additions & 0 deletions lsp/intellij-plugin/src/main/kotlin/FindUsagesProvider.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package community.flock.wirespec.lsp.intellij_plugin

import com.intellij.lang.cacheBuilder.DefaultWordsScanner
import com.intellij.lang.cacheBuilder.WordsScanner
import com.intellij.psi.PsiElement
import com.intellij.psi.tree.TokenSet
import com.intellij.lang.findUsages.FindUsagesProvider as IntellijFindUsagesProvider


class FindUsagesProvider : IntellijFindUsagesProvider {

override fun getWordsScanner(): WordsScanner = DefaultWordsScanner(
Lexer(),
TokenSets.CUSTOM_TYPE,
TokenSet.EMPTY,
TokenSet.EMPTY
)

override fun canFindUsagesFor(psiElement: PsiElement) = psiElement is CustomTypeElement

override fun getHelpId(psiElement: PsiElement) = null

override fun getType(element: PsiElement) = if (element is CustomTypeElement) "custom Type" else ""

override fun getDescriptiveName(element: PsiElement) = if (element is CustomTypeElement) element.name ?: "" else ""

override fun getNodeText(element: PsiElement, useFullName: Boolean) = getDescriptiveName(element)
}
61 changes: 35 additions & 26 deletions lsp/intellij-plugin/src/main/kotlin/Lexer.kt
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,21 @@ package community.flock.wirespec.lsp.intellij_plugin
import community.flock.wirespec.compiler.core.Wirespec
import community.flock.wirespec.compiler.core.tokenize.Token
import community.flock.wirespec.compiler.core.tokenize.tokenize
import community.flock.wirespec.compiler.core.tokenize.types.Brackets
import community.flock.wirespec.compiler.core.tokenize.types.Colon
import community.flock.wirespec.compiler.core.tokenize.types.Comma
import community.flock.wirespec.compiler.core.tokenize.types.CustomType
import community.flock.wirespec.compiler.core.tokenize.types.CustomValue
import community.flock.wirespec.compiler.core.tokenize.types.EndOfProgram
import community.flock.wirespec.compiler.core.tokenize.types.Keyword
import community.flock.wirespec.compiler.core.tokenize.types.Invalid
import community.flock.wirespec.compiler.core.tokenize.types.LeftCurly
import community.flock.wirespec.compiler.core.tokenize.types.QuestionMark
import community.flock.wirespec.compiler.core.tokenize.types.RightCurly
import community.flock.wirespec.compiler.core.tokenize.types.WsType
import community.flock.wirespec.compiler.core.tokenize.types.WhiteSpace
import community.flock.wirespec.compiler.core.tokenize.types.WsBoolean
import community.flock.wirespec.compiler.core.tokenize.types.WsInteger
import community.flock.wirespec.compiler.core.tokenize.types.WsString
import community.flock.wirespec.compiler.core.tokenize.types.WsTypeDef
import com.intellij.lexer.Lexer as IntellijLexer
import com.intellij.lexer.LexerPosition as IntellijLexerPosition

Expand All @@ -24,8 +32,7 @@ class Lexer : IntellijLexer() {
this.index = 0
this.tokens = emptyList()
if (buffer.isNotEmpty()) {
tokens = Wirespec.tokenize(buffer.toString())
.filterNot { token -> token.type is EndOfProgram }
tokens = Wirespec.tokenize(buffer.toString()).filterNot { it.type is EndOfProgram }
}
}

Expand All @@ -34,52 +41,54 @@ class Lexer : IntellijLexer() {
override fun getState() = 0

override fun getTokenType() =
if (index == tokens.size) {
null
} else {
when (tokens[index].type) {
is WsType -> Types.TYPE
is Keyword -> Types.KEYWORD
is RightCurly -> Types.BRACKETS
is LeftCurly -> Types.BRACKETS
if (index == tokens.size) null
else {
val token = tokens[index]
when (token.type) {
is WsTypeDef -> Types.TYPE_DEF
is WhiteSpace -> Types.WHITE_SPACE
is Brackets -> Types.BRACKETS
is Colon -> Types.COLON
is Comma -> Types.COMMA
else -> Types.VALUE
is CustomValue -> Types.CUSTOM_VALUE
is CustomType -> Types.CUSTOM_TYPE
is WsBoolean -> Types.BOOLEAN
is WsInteger -> Types.INTEGER
is WsString -> Types.STRING
is LeftCurly -> Types.LEFT_CURLY
is QuestionMark -> Types.QUESTION_MARK
is RightCurly -> Types.RIGHT_CURLY
is EndOfProgram -> Types.END_OF_PROGRAM
is Invalid -> Types.INVALID
}
}

override fun getTokenStart() = tokens[index]
.coordinates
.let { it.idxAndLength.idx - it.idxAndLength.length }

.getStartPos()

override fun getTokenEnd() = tokens[index]
.coordinates
.idxAndLength
.idx


override fun advance() {
index++
}

override fun getCurrentPosition(): IntellijLexerPosition = tokens[index]
.coordinates
.let {
val pos = it.idxAndLength.idx - it.idxAndLength.length
LexerPosition(pos, state)
}
.run { LexerPosition(getStartPos(), state) }

override fun restore(position: IntellijLexerPosition) {}

override fun getBufferEnd() = buffer.toString().length

internal class LexerPosition(private val myOffset: Int, private val myState: Int) : IntellijLexerPosition {
override fun getOffset(): Int {
return myOffset
}
override fun getOffset() = myOffset

override fun getState(): Int {
return 1
}
override fun getState() = myState
}
}

fun Token.Coordinates.getStartPos() = idxAndLength.idx - idxAndLength.length
130 changes: 118 additions & 12 deletions lsp/intellij-plugin/src/main/kotlin/Parser.kt
Original file line number Diff line number Diff line change
@@ -1,53 +1,159 @@
package community.flock.wirespec.lsp.intellij_plugin

import com.intellij.extapi.psi.ASTWrapperPsiElement
import com.intellij.lang.ASTNode
import com.intellij.lang.PsiBuilder
import com.intellij.lang.PsiParser
import com.intellij.navigation.ItemPresentation
import com.intellij.openapi.project.Project
import com.intellij.psi.FileViewProvider
import com.intellij.psi.PsiElement
import com.intellij.psi.PsiFileFactory
import com.intellij.psi.PsiNameIdentifierOwner
import com.intellij.psi.PsiNamedElement
import com.intellij.psi.PsiReference
import com.intellij.psi.tree.IElementType
import com.intellij.psi.tree.IFileElementType
import com.intellij.psi.tree.TokenSet
import com.intellij.psi.util.PsiTreeUtil
import community.flock.wirespec.lsp.intellij_plugin.Types.Companion.CUSTOM_TYPE
import community.flock.wirespec.lsp.intellij_plugin.Types.Companion.LEFT_CURLY
import community.flock.wirespec.lsp.intellij_plugin.Types.Companion.RIGHT_CURLY
import com.intellij.lang.ParserDefinition as IntellijParserDefinition
import com.intellij.psi.tree.TokenSet as IntellijTokenSet


class Parser : PsiParser {
override fun parse(root: IElementType, builder: PsiBuilder): ASTNode {

val rootMarker = builder.mark()
class TypeDef : IElementType("TYPE_DEF", Language.INSTANCE)
class CustomTypeDef : IElementType("CUSTOM_TYPE_DEF", Language.INSTANCE)
class CustomTypeRef : IElementType("CUSTOM_TYPE_REF", Language.INSTANCE)
class Body : IElementType("BODY", Language.INSTANCE)

while (!builder.eof()) {
val token = builder.tokenType
if (token != null) {
builder.mark().done(token)

override fun parse(root: IElementType, builder: PsiBuilder): ASTNode {
val rootMarker = builder.mark()
var typeMarker: PsiBuilder.Marker? = null
var bodyMarker: PsiBuilder.Marker? = null

fun parseNode() {
when (builder.tokenType) {
CUSTOM_TYPE -> {
builder.mark()
.also { builder.advanceLexer() }
.done(CustomTypeDef())
parseNode()
}

LEFT_CURLY -> {
bodyMarker = builder.mark()
builder.advanceLexer()
parseNode()
}

RIGHT_CURLY -> {
builder.advanceLexer()
bodyMarker?.done(Body()).also { bodyMarker = null }
typeMarker?.done(TypeDef()).also { typeMarker = null }
}

else -> {
builder.advanceLexer()
parseNode()
}
}
}

fun parseDef() {
typeMarker = builder.mark()
builder.advanceLexer()
if (!builder.eof() && builder.tokenType != Types.TYPE_DEF) {
parseNode()
}
}

while (!builder.eof()) {
when (builder.tokenType) {
Types.TYPE_DEF -> parseDef()
else -> builder.advanceLexer()
}
}
rootMarker.done(root)

return builder.treeBuilt
}

}


class ParserDefinition : IntellijParserDefinition {
override fun createLexer(project: Project) = Lexer()

override fun getCommentTokens() = TokenSet.COMMENTS
override fun getCommentTokens() = TokenSet.create()

override fun getStringLiteralElements() = IntellijTokenSet.EMPTY
override fun getStringLiteralElements(): TokenSet = IntellijTokenSet.EMPTY

override fun createParser(project: Project) = Parser()

override fun getFileNodeType() = FILE

override fun createFile(viewProvider: FileViewProvider) = File(viewProvider)

override fun createElement(node: ASTNode): PsiElement = Element(node)
override fun createElement(node: ASTNode): PsiElement = when (node.elementType) {
is Parser.TypeDef -> TypeDefElement(node)
is Parser.CustomTypeDef -> CustomTypeElementDef(node)
is Parser.CustomTypeRef -> CustomTypeElementRef(node)
is Parser.Body -> BodyElement(node)
else -> error("Cannot create type")
}

companion object {
val FILE = IFileElementType(Language.INSTANCE)
}
}

}

class TypeDefElement(ast: ASTNode) : ASTWrapperPsiElement(ast)
class BodyElement(ast: ASTNode) : ASTWrapperPsiElement(ast)

fun createDefNode(project: Project, name: String) = PsiFileFactory
.getInstance(project)
.createFileFromText("dummy.ws", FileType.INSTANCE, "type $name {}")
.firstChild
.let { PsiTreeUtil.findChildOfType(it, CustomTypeElementDef::class.java) }
?.node
?: error("Cannot create new node")

fun createRefNode(project: Project, name: String) = PsiFileFactory
.getInstance(project)
.createFileFromText("dummy.ws", FileType.INSTANCE, "type X { y: $name }")
.firstChild
.let { PsiTreeUtil.findChildOfType(it, CustomTypeElementRef::class.java) }
?.node
?: error("Cannot create new node")

abstract class CustomTypeElement(ast: ASTNode) : ASTWrapperPsiElement(ast), PsiNamedElement {
override fun getName(): String? = text

override fun getPresentation(): ItemPresentation = Utils.getPresentation(this)
}

class CustomTypeElementDef(private val ast: ASTNode) : CustomTypeElement(ast), PsiNameIdentifierOwner {

override fun setName(name: String): PsiElement {
parent.node.replaceChild(node, createDefNode(project, name))
return this
}

override fun getNameIdentifier(): PsiElement = ast.firstChildNode.psi
}

class CustomTypeElementRef(private val ast: ASTNode) : CustomTypeElement(ast), PsiNameIdentifierOwner {

override fun setName(name: String): PsiElement {
this.parent.node.replaceChild(this.node, createRefNode(project, name))
return this
}

override fun getNameIdentifier(): PsiElement = ast.firstChildNode.psi

override fun getReference(): PsiReference = Reference(this)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package community.flock.wirespec.lsp.intellij_plugin

import com.intellij.lang.refactoring.RefactoringSupportProvider
import com.intellij.psi.PsiElement

class RefactoringSupportProvider : RefactoringSupportProvider() {
override fun isMemberInplaceRenameAvailable(elementToRename: PsiElement, context: PsiElement?) =
elementToRename is CustomTypeElement
}
Loading

0 comments on commit fc6e521

Please sign in to comment.