What's the big IDEA? Writing IntelliJ plugins for Kotlin

What's the big IDEA? Writing IntelliJ plugins for Kotlin

Given with Egor Andreevich

With Kotlin having full interop with Java, mixed codebases have become common and effective - but have made writing developer tools more challenging. How do you support multiple languages with a single tool? How do you convert existing plugins from Java to Kotlin and is there a way to avoid having to?

This talk will cover UAST (Universal Abstract Syntax Tree), an API for working with languages generically. With UAST you can write a single tool that will work for both Java and Kotlin - no special casing needed. We will talk about how to setup a plugin to use UAST and walk through a sample that works on mixed codebases.

The talk will also dive into the types of problems you can solve by writing an IntelliJ plugin, as well as other applications for UAST outside of IntelliJ IDEA.

Video to follow

5fe8f40633300a70ea088a594cb33031?s=128

Alec Strong

October 05, 2018
Tweet

Transcript

  1. What’s the big IDEA? @strongolopolis @egorand | Writing IntelliJ plugins

    for Kotlin
  2. Life without IDEs

  3. Writing Code • Autocompletion • Error highlighting • Autoimports •

    Refactoring tools
  4. • Switching to a different window • Memorizing the commands

    • Error reporting Running Code
  5. Debugging? Profiling?

  6. None
  7. Plugins

  8. None
  9. Gradle

  10. Gradle

  11. Gradle

  12. Plugins • Developer experience • Simplify how you interact with

    code • Problems you can’t solve with code • Demystifying confusing parts • e.g. concurrency
  13. Plugins that we use (and love)

  14. SQLDelight

  15. Android

  16. Plugins that we want

  17. Wire

  18. Dagger (for Kotlin)

  19. Writing one

  20. import javax.inject.Inject; class SomeClass { @Inject String injectedString; private void

    someMethod() { System.out.println(injectedString); } }
  21. import javax.inject.Inject; class SomeClass { @Inject String injectedString; private void

    someMethod() { System.out.println(injectedString); } } import javax.inject.Inject; class SomeClass { @Inject String injectedString; private void someMethod() { System.out.println(injectedString); } } Imports
  22. import javax.inject.Inject; class SomeClass { @Inject String injectedString; private void

    someMethod() { System.out.println(injectedString); } } import javax.inject.Inject; class SomeClass { @Inject String injectedString; private void someMethod() { System.out.println(injectedString); } } Imports Classes
  23. import javax.inject.Inject; class SomeClass { @Inject String injectedString; private void

    someMethod() { System.out.println(injectedString); } } import javax.inject.Inject; class SomeClass { @Inject String injectedString; private void someMethod() { System.out.println(injectedString); } } Imports Classes • Fields
  24. import javax.inject.Inject; class SomeClass { @Inject String injectedString; private void

    someMethod() { System.out.println(injectedString); } } import javax.inject.Inject; class SomeClass { @Inject String injectedString; private void someMethod() { System.out.println(injectedString); } } Imports Classes • Fields
 • Annotations
  25. import javax.inject.Inject; class SomeClass { @Inject String injectedString; private void

    someMethod() { System.out.println(injectedString); } } import javax.inject.Inject; class SomeClass { @Inject String injectedString; private void someMethod() { System.out.println(injectedString); } } Imports Classes • Fields
 • Annotations Types
  26. import javax.inject.Inject; class SomeClass { @Inject String injectedString; private void

    someMethod() { System.out.println(injectedString); } } import javax.inject.Inject; class SomeClass { @Inject String injectedString; private void someMethod() { System.out.println(injectedString); } } Imports Classes • Fields
 • Annotations Types Expressions
  27. import javax.inject.Inject; class SomeClass { @Inject String injectedString; private void

    someMethod() { System.out.println(injectedString); } } import javax.inject.Inject; class SomeClass { @Inject String injectedString; private void someMethod() { System.out.printlnd(); } } Imports Classes • Fields
 • Annotations Types Expressions Identifiers
  28. import javax.inject.Inject; class SomeClass { @Inject String injectedString; private void

    someMethod() { System.out.println(injectedString); } } import javax.inject.Inject; class SomeClass { @Inject String injectedString; private void someMethod() { System.out.println(injectedString); } } Imports Classes • Fields
 • Annotations Types Expressions Identifiers Tokens
  29. import javax.inject.Inject; class SomeClass {A @Inject String injectedString; private void

    someMethod() {A System.out.println(injectedString); }A }A Imports Classes • Fields
 • Annotations Types Expressions Identifiers Tokens Psi Psi Psi Psi Psi Psi Psi Psi
  30. import javax.inject.Inject; class SomeClass {A @Inject String injectedString; private void

    someMethod() {A System.out.println(injectedString); }A }A PsiImports PsiClasses PsiFields
 PsiAnnotations PsiTypes PsiExpressions PsiIdentifiers PsiTokens
  31. import javax.inject.Inject; class SomeClass {A @Inject String injectedString; private void

    someMethod() {A System.out.println(injectedString); }A }A PsiJavaToken
  32. PsiJavaToken : PsiElement import javax.inject.Inject; class SomeClass {A @Inject String

    injectedString; private void someMethod() {A System.out.println(injectedString); }A }A
  33. import javax.inject.Inject; class SomeClass {A @Inject String injectedString; private void

    someMethod() {A System.out.println(injectedString); }A }A PsiWhitespace : PsiElement
  34. import javax.inject.Inject; class SomeClass {A @Inject String injectedString; private void

    someMethod() {A System.out.println(injectedString); }A }A PsiType
  35. PsiElementType : PsiElement import javax.inject.Inject; class SomeClass {A @Inject String

    injectedString; private void someMethod() {A System.out.println(injectedString); }A }A import javax.inject.Inject; class SomeClass { @Inject String injectedString; private void someMethod() { System.out.println(injectedString); } }
  36. import javax.inject.Inject; class SomeClass {A @Inject String injectedString; private void

    someMethod() {A System.out.println(injectedString); }A }A import javax.inject.Inject; class SomeClass { @Inject String injectedString; private void someMethod() { System.out.println(injectedString); } } val typeElement: PsiTypeElement val type: PsiType = typeElement.type
  37. import javax.inject.Inject; class SomeClass {A @Inject String injectedString; private void

    someMethod() {A System.out.println(injectedString); }A }A import javax.inject.Inject; class SomeClass { @Inject String injectedString; private void someMethod() { System.out.println(injectedString); } } val typeElement: PsiTypeElement val type: PsiJavaCodeReferenceElement? = typeElement.innermostComponentReferenceElement
  38. PsiJavaCodeReferenceElement : PsiElement, PsiReference import javax.inject.Inject; class SomeClass {A @Inject

    String injectedString; private void someMethod() {A System.out.println(injectedString); }A }A String
  39. public interface PsiReference { /** * Returns the underlying (referencing)

    element of the reference. * * @return the underlying element of the reference. */ @NotNull PsiElement getElement(); }
  40. import javax.inject.Inject; class SomeClass { @Inject String injectedString; private void

    someMethod() { System.out.println(injectedString); } } import javax.inject.Inject; class SomeClass { @Inject String injectedString; private void someMethod() { System.out.println(injectedString); } } PsiJavaCodeReferenceElement : PsiElement, PsiReference
  41. import javax.inject.Inject; class SomeClass { @Inject String injectedString; private void

    someMethod() { System.out.println(injectedString); }A }A
  42. import javax.inject.Inject; class SomeClass { @Inject String injectedString; private void

    someMethod() { System.out.println(injectedString); } } PsiFile : PsiElement import javax.inject.Inject; class SomeClass { @Inject String injectedString; private void someMethod() { System.out.println(injectedString); }A }A
  43. import javax.inject.Inject; class SomeClass { @Inject String injectedString; private void

    someMethod() { System.out.println(injectedString); } } import javax.inject.Inject; class SomeClass { @Inject String injectedString; private void someMethod() { System.out.println(injectedString); }A }A PsiViewer
  44. import javax.inject.Inject; class SomeClass { @Inject String injectedString; private void

    someMethod() { System.out.println(injectedString); } } PsiJavaFile : PsiElement import javax.inject.Inject; class SomeClass { @Inject String injectedString; private void someMethod() { System.out.println(injectedString); }A }A
  45. import javax.inject.Inject; class SomeClass { @Inject String injectedString; private void

    someMethod() { System.out.println(injectedString); } } import javax.inject.Inject; class SomeClass {A @Inject String injectedString; private void someMethod() {A System.out.println(injectedString); }A }A val element: PsiElement val file: PsiFile = element.containingFile
  46. import javax.inject.Inject; class SomeClass {A @Inject String injectedString; private void

    someMethod() {A System.out.println(injectedString); }A }A val file: PsiJavaFile
  47. import javax.inject.Inject; class SomeClass {A @Inject String injectedString; private void

    someMethod() {A System.out.println(injectedString); }A }A import javax.inject.Inject; class SomeClass {A @Inject String injectedString; private void someMethod() {A System.out.println(injectedString); }A }A val file: PsiJavaFile val classes: Array<PsiClass> = file.classes
  48. import javax.inject.Inject; class SomeClass {A @Inject String injectedString; private void

    someMethod() {A System.out.println(injectedString); }A }A import javax.inject.Inject; class SomeClass {A @Inject String injectedString; private void someMethod() {A System.out.println(injectedString); }A }A val file: PsiJavaFile val classes: Array<PsiClass> = file.classes val fields: List<PsiField> = classes.flatMap { it.fields.asList() }
  49. import javax.inject.Inject; class SomeClass {A @Inject String injectedString; private void

    someMethod() {A System.out.println(injectedString); }A }A import javax.inject.Inject; class SomeClass {A @Inject String injectedString; private void someMethod() {A System.out.println(injectedString); }A }A val file: PsiJavaFile val classes: Array<PsiClass> = file.classes val fields: List<PsiField> = classes.flatMap { it.fields.asList() } val injectedFields = fields .filter { it.hasAnnotation(“javax.inject.Inject") }
  50. import javax.inject.Inject; class SomeClass {A @Inject String injectedString; private void

    someMethod() {A System.out.println(injectedString); }A }A val file: PsiJavaFile val classes = Array<PsiClass> = file.classes val fields: List<PsiField> = classes.flatMap { it.fields.asList() } val injectedFields = fields .filter { it.hasAnnotation(“javax.inject.Inject") } val file: PsiJavaFile
  51. Application

  52. Project

  53. Module

  54. VirtualFile

  55. Document

  56. Editor

  57. Editor Document val editor: Editor val document: Document = editor.document

  58. Document val document: Document val virtualFile: VirtualFile = FileDocumentManager.getInstance().getFile(document) VirtualFile

  59. VirtualFile val virtualFile: VirtualFile val document: Document = FileDocumentManager.getInstance().getDocument(virtualFile) Document

  60. VirtualFile val virtualFile: VirtualFile val cachedDocument: Document = FileDocumentManager.getInstance().getCachedDocument(virtualFile) Document

  61. java.io.File val file: File val virtualFile: VirtualFile = LocalFileSystem.getInstance().findFileByIoFile(file) VirtualFile

  62. VirtualFile val virtualFile: VirtualFile val psiFile: PsiFile = PsiManager.getInstance(project).findFile(virtualFile) PsiFile

  63. val virtualFile: VirtualFile val psiFile: PsiFile = PsiManager.getInstance(project).findFile(virtualFile) VirtualFile val

    virtualFile: VirtualFile val psiFile: PsiFile = PsiManager.getInstance(project).findFile(virtualFile) PsiFile
  64. plugin.xml

  65. <idea-plugin version="2"> <id>com.squareup.ideaplugin.dagger</id> <name>Dagger Plugin</name> <version>2.0.0</version> <vendor email="support@squareup.com" url="http:// square.github.io">Square,

    Inc.</vendor> <depends>org.jetbrains.kotlin</depends> <project-components...> <extensions defaultExtensionNs="com.intellij"...> </idea-plugin>
  66. <idea-plugin version="2"> <id>com.squareup.ideaplugin.dagger</id> <name>Dagger Plugin</name> <version>2.0.0</version> <vendor email="support@squareup.com" url="http:// square.github.io">Square,

    Inc.</vendor> <depends>org.jetbrains.kotlin</depends> <project-components...> <extensions defaultExtensionNs="com.intellij"...> </idea-plugin> <idea-plugin version="2"> <id>com.squareup.ideaplugin.dagger</id> <name>Dagger Plugin</name> <version>2.0.0</version> <vendor email="support@squareup.com" url="http:// square.github.io">Square, Inc.</vendor> <depends>org.jetbrains.kotlin</depends> <project-components...> <extensions defaultExtensionNs="com.intellij"...> </idea-plugin>
  67. <idea-plugin version="2"> <id>com.squareup.ideaplugin.dagger</id> <name>Dagger Plugin</name> <version>2.0.0</version> <vendor email="support@squareup.com" url="http:// square.github.io">Square,

    Inc.</vendor> <depends>org.jetbrains.kotlin</depends> <project-components...> <extensions defaultExtensionNs="com.intellij"...> </idea-plugin> <project-components...>
  68. <idea-plugin version="2"> <id>com.squareup.ideaplugin.dagger</id> <name>Dagger Plugin</name> <version>2.0.0</version> <vendor email="support@squareup.com" url="http:// square.github.io">Square,

    Inc.</vendor> <depends>org.jetbrains.kotlin</depends> <extensions defaultExtensionNs="com.intellij"...> </idea-plugin> <idea-plugin version=“2”> <id...> <project-components> <component> <implementation-class> com.squareup.idea.dagger.DaggerProjectComponent </implementation-class> </component> </project-components> <extensions defaultExtensionNs="com.intellij"...> </idea-plugin> <project-components> <component> <implementation-class> com.squareup.idea.dagger.DaggerProjectComponent </implementation-class> </component> </project-components>
  69. class DaggerProjectComponent : ProjectComponent {A override fun projectOpened() {A println("Hello

    World!") }A override fun projectClosed() {A println("Goodbye!") }A }A
  70. class DaggerProjectComponent( private val project: Project ) : ProjectComponent {A

    override fun projectOpened() {A println("Hello World!") }A override fun projectClosed() {A println("Goodbye!") }A }A
  71. <idea-plugin version=“2”> <id...> <project-components> <component> <implementation-class> com.squareup.idea.dagger.DaggerProjectComponent </implementation-class> </component> </project-components>

    <extensions defaultExtensionNs="com.intellij"...> </idea-plugin> <extensions defaultExtensionNs="com.intellij"...> </idea-plugin>
  72. <idea-plugin version=“2”> <id...> <project-components...> <extensions defaultExtensionNs="com.intellij"> <codeInsight.lineMarkerProvider language="JAVA" implementationClass= “com.squareup.idea.dagger.InjectionLineMarkerProvider"

    /> </extensions> </idea-plugin> <idea-plugin version=“2”> <id...> <project-components> <component> <implementation-class> com.squareup.idea.dagger.DaggerProjectComponent </implementation-class> </component> </project-components> </idea-plugin> <extensions defaultExtensionNs="com.intellij"> <codeInsight.lineMarkerProvider language="JAVA" implementationClass= “com.squareup.idea.dagger.InjectionLineMarkerProvider" /> </extensions>
  73. class InjectionLineMarkerProvider : LineMarkerProvider { override fun getLineMarkerInfo( element: PsiElement

    ): LineMarkerInfo<*>? { ... } }
  74. PSI in mixed codebases

  75. import javax.inject.Inject; class SomeClass { @Inject String injectedString; private void

    someMethod() { System.out.println(injectedString); } }
  76. import javax.inject.Inject class SomeClass { @Inject var injectedString: String? =

    null private fun someMethod() { println(injectedString) } }
  77. None
  78. public class InjectionLineMarkerProvider implements LineMarkerProvider { public LineMarkerInfo getLineMarkerInfo(PsiElement element)

    { … } else if (element instanceof PsiField) { // Field injection. PsiField fieldElement = (PsiField) element; PsiTypeElement typeElement = fieldElement.getTypeElement(); … } return null; }
  79. None
  80. public class InjectionLineMarkerProvider implements LineMarkerProvider { public LineMarkerInfo getLineMarkerInfo(PsiElement element)

    { … } else if (element instanceof PsiField) { // Field injection. PsiField fieldElement = (PsiField) element; PsiTypeElement typeElement = fieldElement.getTypeElement(); … } return null; }
  81. None
  82. None
  83. public class InjectionLineMarkerProvider implements LineMarkerProvider { public LineMarkerInfo getLineMarkerInfo(PsiElement element)

    { … } else if (element instanceof PsiField) { … } else if (element instanceof KtProperty) { … } return null; }
  84. public class InjectionLineMarkerProvider implements LineMarkerProvider { public LineMarkerInfo getLineMarkerInfo(PsiElement element)

    { … } else if (element instanceof PsiField) { // Field injection. PsiField fieldElement = (PsiField) element; PsiTypeElement typeElement = fieldElement.getTypeElement(); … } else if (element instanceof KtProperty) { ??? } return null; }
  85. UAST

  86. Abstract Unified Syntax Tree

  87. PsiElement PsiFile PsiClass PsiField PsiMethod UElement UFile UClass UField UMethod

  88. val psiElement: PsiElement = ... val uElement: UElement = psiElement.toUElement()

    val uField: UField = psiElement.toUElementOfType<UField>()
  89. class InjectionLineMarkerProvider : LineMarkerProvider { fun getLineMarkerInfo(element: PsiElement): LineMarkerInfo<*>? {

    val uElement = element.toUElement() if (uElement is UField) { // Field injection. val typeElement = uElement.typeElement … } return null }
  90. None
  91. val psiType = uElement.type

  92. “The PsiElement obtained this way is ambiguous. If you need

    "physical" PsiElement please use UElementKt.getSourcePsiElement(), if you need PsiElement that "emulates" behaviour of Java-elements (PsiClass, PsiMethod, etc.) then please use UElementKt.getAsJavaPsiElement().”
  93. interface UElement interface PsiElement interface PsiField interface UField

  94. <extensions defaultExtensionNs="com.intellij"> <codeInsight.lineMarkerProvider language="JAVA" implementationClass=“com.squareup.idea.dagger.ILMP" /> </extensions>

  95. <extensions defaultExtensionNs="com.intellij"> <codeInsight.lineMarkerProvider language="JAVA" implementationClass=“com.squareup.idea.dagger.ILMP" /> <codeInsight.lineMarkerProvider language="kotlin" implementationClass=“com.squareup.idea.dagger.ILMP" />

    </extensions>
  96. <extensions defaultExtensionNs="com.intellij"> <codeInsight.lineMarkerProvider language="JAVA" implementationClass=“com.squareup.idea.dagger.ILMP" /> <codeInsight.lineMarkerProvider language="kotlin" implementationClass=“com.squareup.idea.dagger.ILMP" />

    </extensions> <extensions defaultExtensionNs="com.intellij"> <codeInsight.lineMarkerProvider language="JAVA" implementationClass=“com.squareup.idea.dagger.ILMP" /> <codeInsight.lineMarkerProvider language="kotlin" implementationClass=“com.squareup.idea.dagger.ILMP" /> </extensions>
  97. <idea-plugin> <depends>org.jetbrains.kotlin</depends> <extensions defaultExtensionNs="com.intellij"> <codeInsight.lineMarkerProvider language="JAVA" implementationClass=“com.squareup.idea.dagger.ILMP" /> <codeInsight.lineMarkerProvider language="kotlin"

    implementationClass=“com.squareup.idea.dagger.ILMP" /> </extensions> </idea-plugin> <idea-plugin> <depends>org.jetbrains.kotlin</depends> <extensions defaultExtensionNs="com.intellij"> <codeInsight.lineMarkerProvider language="JAVA" implementationClass=“com.squareup.idea.dagger.ILMP" /> <codeInsight.lineMarkerProvider language="kotlin" implementationClass=“com.squareup.idea.dagger.ILMP" /> </extensions> </idea-plugin>
  98. apply plugin: 'org.jetbrains.intellij'

  99. apply plugin: 'org.jetbrains.intellij' intellij {A version “IC-2018.1" pluginName = 'SQLDelight'

    }A
  100. apply plugin: 'org.jetbrains.intellij' intellij {A version “IC-2018.1" pluginName = 'SQLDelight'

    plugins = [A “org.jetbrains.kotlin:1.2.70-release-IJ2018.1-1” ]A }A
  101. apply plugin: 'org.jetbrains.intellij' intellij {A version “IC-2018.1" pluginName = 'SQLDelight'

    plugins = [A “org.jetbrains.kotlin:1.2.70-release-IJ2018.1-1” ]A }A // Automatically adds tasks.create("runIde")
  102. apply plugin: 'org.jetbrains.intellij' intellij { version “IC-2018.1" pluginName = 'SQLDelight'

    plugins = [ “org.jetbrains.kotlin:1.2.70-release-IJ2018.1-1” ] } // Automatically adds tasks.create("runIde") tasks.create("buildPlugin")
  103. PSI UAST vs

  104. PSI UAST • JVM language analysis • Language-agnostic types •

    Language-agnostic features • e.g. querying annotations • APIs that accept it • Language metadata • Text range
  105. UAST use cases

  106. • Language-agnostic Intellij plugins • Build tools • Lint •

    IDEA-based annotation processing • Headless IDEA environment UAST use cases
  107. None
  108. None
  109. @strongolopolis @egorand | Thanks!