From 8a5a43d670acf0d0c83327a5ca092c1934013a68 Mon Sep 17 00:00:00 2001 From: Simon Ogorodnik Date: Thu, 15 Mar 2018 15:34:14 +0300 Subject: [PATCH] Add IDEA action to show raw FIR of currently opened file This action works correctly with fir.enabled=true in Gradle properties --- build.gradle.kts | 10 + compiler/fir/tree/build.gradle.kts | 9 +- idea/build.gradle.kts | 1 + idea/build.gradle.kts.173 | 1 + idea/build.gradle.kts.as31 | 1 + idea/build.gradle.kts.as32 | 1 + idea/build.gradle.kts.as33 | 1 + idea/build.gradle.kts.as34 | 1 + idea/fir-view/build.gradle.kts | 33 +++ idea/fir-view/src/FirExplorerToolWindow.kt | 307 +++++++++++++++++++++ idea/fir-view/src/ShowFirAction.kt | 45 +++ idea/src/META-INF/plugin-common.xml | 3 + prepare/idea-plugin/build.gradle.kts | 11 + settings.gradle | 1 + 14 files changed, 422 insertions(+), 3 deletions(-) create mode 100644 idea/fir-view/build.gradle.kts create mode 100644 idea/fir-view/src/FirExplorerToolWindow.kt create mode 100644 idea/fir-view/src/ShowFirAction.kt diff --git a/build.gradle.kts b/build.gradle.kts index 66e2a3675cb..e0e83daf0ed 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -183,6 +183,16 @@ extra["compilerModules"] = arrayOf( ":compiler:resolution", ":compiler:serialization", ":compiler:psi", + *if (project.findProperty("fir.enabled") == "true") { + arrayOf( + ":compiler:fir:cones", + ":compiler:fir:resolve", + ":compiler:fir:tree", + ":compiler:fir:psi2fir" + ) + } else { + emptyArray() + }, ":compiler:frontend", ":compiler:frontend.java", ":compiler:frontend.script", diff --git a/compiler/fir/tree/build.gradle.kts b/compiler/fir/tree/build.gradle.kts index 688481f92e8..5d47c2236b9 100644 --- a/compiler/fir/tree/build.gradle.kts +++ b/compiler/fir/tree/build.gradle.kts @@ -10,12 +10,9 @@ plugins { jvmTarget = "1.6" -val generatorClasspath by configurations.creating - dependencies { compile(project(":core:descriptors")) compile(project(":compiler:fir:cones")) - generatorClasspath(project("visitors-generator")) // Necessary only to store bound PsiElement inside FirElement compileOnly(intellijCoreDep()) { includeJars("intellij-core", "annotations") } } @@ -28,6 +25,12 @@ sourceSets { "test" {} } +val generatorClasspath by configurations.creating + +dependencies { + generatorClasspath(project("visitors-generator")) +} + val generateVisitors by tasks.creating(JavaExec::class) { val generationRoot = "$projectDir/src/org/jetbrains/kotlin/fir/" val output = "$projectDir/visitors" diff --git a/idea/build.gradle.kts b/idea/build.gradle.kts index 886879ab36d..d1ab3a115a4 100644 --- a/idea/build.gradle.kts +++ b/idea/build.gradle.kts @@ -33,6 +33,7 @@ dependencies { compile(project(":eval4j")) compile(project(":j2k")) compile(project(":idea:formatter")) + compile(project(":idea:fir-view")) compile(project(":idea:idea-core")) compile(project(":idea:ide-common")) compile(project(":idea:idea-jps-common")) diff --git a/idea/build.gradle.kts.173 b/idea/build.gradle.kts.173 index 02240c5c283..1f2d57b4235 100644 --- a/idea/build.gradle.kts.173 +++ b/idea/build.gradle.kts.173 @@ -33,6 +33,7 @@ dependencies { compile(project(":eval4j")) compile(project(":j2k")) compile(project(":idea:formatter")) + compile(project(":idea:fir-view")) compile(project(":idea:idea-core")) compile(project(":idea:ide-common")) compile(project(":idea:idea-jps-common")) diff --git a/idea/build.gradle.kts.as31 b/idea/build.gradle.kts.as31 index 31c4c93492d..0aaed569a1a 100644 --- a/idea/build.gradle.kts.as31 +++ b/idea/build.gradle.kts.as31 @@ -33,6 +33,7 @@ dependencies { compile(project(":eval4j")) compile(project(":j2k")) compile(project(":idea:formatter")) + compile(project(":idea:fir-view")) compile(project(":idea:idea-core")) compile(project(":idea:ide-common")) compile(project(":idea:idea-jps-common")) diff --git a/idea/build.gradle.kts.as32 b/idea/build.gradle.kts.as32 index 98d0461970a..2845f66c050 100644 --- a/idea/build.gradle.kts.as32 +++ b/idea/build.gradle.kts.as32 @@ -33,6 +33,7 @@ dependencies { compile(project(":eval4j")) compile(project(":j2k")) compile(project(":idea:formatter")) + compile(project(":idea:fir-view")) compile(project(":idea:idea-core")) compile(project(":idea:ide-common")) compile(project(":idea:idea-jps-common")) diff --git a/idea/build.gradle.kts.as33 b/idea/build.gradle.kts.as33 index 406a5f3ecdf..21aadc0c32a 100644 --- a/idea/build.gradle.kts.as33 +++ b/idea/build.gradle.kts.as33 @@ -33,6 +33,7 @@ dependencies { compile(project(":eval4j")) compile(project(":j2k")) compile(project(":idea:formatter")) + compile(project(":idea:fir-view")) compile(project(":idea:idea-core")) compile(project(":idea:ide-common")) compile(project(":idea:idea-jps-common")) diff --git a/idea/build.gradle.kts.as34 b/idea/build.gradle.kts.as34 index 406a5f3ecdf..21aadc0c32a 100644 --- a/idea/build.gradle.kts.as34 +++ b/idea/build.gradle.kts.as34 @@ -33,6 +33,7 @@ dependencies { compile(project(":eval4j")) compile(project(":j2k")) compile(project(":idea:formatter")) + compile(project(":idea:fir-view")) compile(project(":idea:idea-core")) compile(project(":idea:ide-common")) compile(project(":idea:idea-jps-common")) diff --git a/idea/fir-view/build.gradle.kts b/idea/fir-view/build.gradle.kts new file mode 100644 index 00000000000..3e26bbc4418 --- /dev/null +++ b/idea/fir-view/build.gradle.kts @@ -0,0 +1,33 @@ +/* + * Copyright 2000-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license + * that can be found in the license/LICENSE.txt file. + */ + + +apply { plugin("kotlin") } +apply { plugin("jps-compatible") } + +dependencies { + val compile by configurations + val compileOnly by configurations + + + compile(projectDist(":kotlin-stdlib")) + compileOnly(project(":kotlin-reflect-api")) + compile(project(":core:descriptors")) + compile(project(":compiler:fir:tree")) + compile(project(":compiler:fir:psi2fir")) + + compile(project(":idea:idea-core")) + + compileOnly(intellijCoreDep()) { includeJars("intellij-core") } + compileOnly(intellijDep()) { includeJars("util", "openapi", "idea", "asm-all", "jdom", "annotations", "trove4j", "guava", rootProject = rootProject) } + compileOnly(intellijPluginDep("gradle")) //{ includeJars("gradle-tooling-api", "gradle", rootProject = rootProject) } +} + +sourceSets { + "main" { + projectDefault() + } + "test" {} +} diff --git a/idea/fir-view/src/FirExplorerToolWindow.kt b/idea/fir-view/src/FirExplorerToolWindow.kt new file mode 100644 index 00000000000..63067f249d7 --- /dev/null +++ b/idea/fir-view/src/FirExplorerToolWindow.kt @@ -0,0 +1,307 @@ +/* + * Copyright 2000-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license + * that can be found in the license/LICENSE.txt file. + */ + +package org.jetbrains.kotlin.idea.actions.internal + +import com.intellij.icons.AllIcons +import com.intellij.ide.projectView.PresentationData +import com.intellij.ide.util.treeView.IndexComparator +import com.intellij.openapi.Disposable +import com.intellij.openapi.application.runInEdt +import com.intellij.openapi.editor.Editor +import com.intellij.openapi.editor.markup.* +import com.intellij.openapi.fileEditor.* +import com.intellij.openapi.progress.EmptyProgressIndicator +import com.intellij.openapi.progress.ProgressIndicator +import com.intellij.openapi.progress.ProgressManager +import com.intellij.openapi.progress.Task +import com.intellij.openapi.project.Project +import com.intellij.openapi.wm.ToolWindow +import com.intellij.psi.PsiDocumentManager +import com.intellij.ui.JBColor +import com.intellij.ui.SimpleTextAttributes +import com.intellij.ui.treeStructure.SimpleNode +import com.intellij.ui.treeStructure.SimpleTreeBuilder +import com.intellij.ui.treeStructure.SimpleTreeStructure +import com.intellij.ui.treeStructure.Tree +import org.jetbrains.kotlin.fir.FirElement +import org.jetbrains.kotlin.fir.FirSession +import org.jetbrains.kotlin.fir.FirSessionBase +import org.jetbrains.kotlin.fir.builder.RawFirBuilder +import org.jetbrains.kotlin.idea.util.application.runReadAction +import org.jetbrains.kotlin.psi.KtFile +import org.jetbrains.kotlin.psi.psiUtil.endOffset +import org.jetbrains.kotlin.psi.psiUtil.startOffset +import java.awt.BorderLayout +import javax.swing.JPanel +import javax.swing.tree.DefaultMutableTreeNode +import javax.swing.tree.DefaultTreeModel +import kotlin.math.min +import kotlin.reflect.* +import kotlin.reflect.full.* +import kotlin.reflect.jvm.isAccessible + +class FirExplorerToolWindow(private val project: Project, private val toolWindow: ToolWindow) : JPanel(BorderLayout()), Disposable { + + + private val tree = Tree() + private val treeStructure = FirExplorerTreeStructure() + private val builder = SimpleTreeBuilder(tree, DefaultTreeModel(DefaultMutableTreeNode()), treeStructure, IndexComparator.INSTANCE) + + private val level = HighlighterLayer.SELECTION - 100 + private val highlightingAttributes = TextAttributes().apply { + effectType = EffectType.ROUNDED_BOX + effectColor = JBColor.RED + } + + + private val rangeHighlightMarkers = mutableListOf() + + private var currentEditor: Editor? = FileEditorManager.getInstance(project).selectedTextEditor + + fun clearHighlighting(editor: Editor) { + rangeHighlightMarkers.forEach { editor.markupModel.removeHighlighter(it) } + rangeHighlightMarkers.clear() + } + + fun postponeTreeRebuild(editor: Editor, init: Boolean) { + ProgressManager.getInstance().runProcessWithProgressAsynchronously( + object : Task.Backgroundable(project, "") { + override fun run(indicator: ProgressIndicator) { + val psiDocumentManager = PsiDocumentManager.getInstance(project) + val file = runReadAction { psiDocumentManager.getPsiFile(editor.document) as? KtFile } + if (file != null) { + val firFile = runReadAction { RawFirBuilder(object : FirSessionBase() {}).buildFirFile(file) } + runInEdt { + treeStructure.root = FirExplorerTreeNode("root = ", firFile, null) + builder.updateFromRoot(!init) + } + } + } + }, + EmptyProgressIndicator() + ) + + } + + init { + + + project.messageBus.connect(this).subscribe( + FileEditorManagerListener.FILE_EDITOR_MANAGER, + object : FileEditorManagerListener { + override fun selectionChanged(event: FileEditorManagerEvent) { + runInEdt { + val oldEditor = (event.oldEditor as? TextEditor) + val textEditor = (event.newEditor as? TextEditor) + if (oldEditor != null && oldEditor.editor == currentEditor) { + clearHighlighting(currentEditor!!) + currentEditor = null + } + + if (textEditor != null) { + currentEditor = textEditor.editor + postponeTreeRebuild(currentEditor!!, false) + } + } + } + } + ) + + tree.addTreeSelectionListener { event -> + val currentEditor = FileEditorManager.getInstance(project).selectedTextEditor + if (currentEditor != null) { + runReadAction { + clearHighlighting(currentEditor) + event.paths.filter { event.isAddedPath(it) }.mapNotNull { + val treeNode = it.lastPathComponent + if (treeNode is DefaultMutableTreeNode) { + val userObject = treeNode.userObject + if (userObject is FirExplorerTreeNode) { + + val data = userObject.data + val psi = (data as? FirElement)?.psi + if (data is FirElement && psi != null) { + if (FileDocumentManager.getInstance().getFile(currentEditor.document) == psi.containingFile.virtualFile) { + + rangeHighlightMarkers += currentEditor.markupModel.addRangeHighlighter( + psi.startOffset, + min(currentEditor.document.textLength, psi.endOffset), + level, + highlightingAttributes, + HighlighterTargetArea.EXACT_RANGE + ) + } + } + } + } + } + } + + } + } + + + + + + + add(tree) + builder.initRoot() + postponeTreeRebuild(currentEditor!!, true) + } + + abstract class AbstractFirExplorerTreeNode(val propertyName: String, parent: SimpleNode?) : SimpleNode(parent) { + + init { + templatePresentation.addText(propertyName, SimpleTextAttributes.REGULAR_ATTRIBUTES) + } + + } + + class FirExplorerTreeNode(propertyName: String, val data: Any?, parent: SimpleNode?) : + AbstractFirExplorerTreeNode(propertyName, parent) { + + init { + templatePresentation.setIcon(AllIcons.Nodes.Property) + if (data != null) { + templatePresentation.addText(data::class.java.simpleName, SimpleTextAttributes.REGULAR_ATTRIBUTES) + templatePresentation.addText("(toString = $data)", SimpleTextAttributes.REGULAR_ATTRIBUTES) + } else { + templatePresentation.addText("null", SimpleTextAttributes.GRAY_ATTRIBUTES) + } + } + + + private fun KClass<*>.getMemberProperties(): Collection> { + return try { + memberProperties + } catch (e: Throwable) { + try { + declaredMemberProperties + } catch (e: Throwable) { + emptyList() + } + } + } + + override fun getChildren(): Array { + if (data == null) { + return SimpleNode.NO_CHILDREN + } else { + val classOfData = data::class + + val members = classOfData.getMemberProperties() + + return members.filter { + it.visibility == null || it.visibility!! <= KVisibility.PROTECTED + }.filter { + it.instanceParameter != null + }.map { + it.getter.isAccessible = true + val value = it.getter.call(data) + val notNullValueType = it.returnType.withNullability(false) + + val namePrefix = "${it.name}: ${it.returnType.renderSimple()} = " + + when { + notNullValueType.isSubtypeOf(firElementType) -> { + FirExplorerTreeNode(namePrefix, value as FirElement?, this) + } + notNullValueType.isSubtypeOf(listElementType) -> { + FirExplorerTreeListNode(namePrefix, value as List<*>?, this) + } + else -> { + FirExplorerTreeNode(namePrefix, value, this) + } + } + }.toTypedArray() + } + } + + companion object { + val firElementKlass = FirElement::class + val firElementType = firElementKlass.starProjectedType + val listElementType = List::class.starProjectedType + } + } + + + class FirExplorerTreeListNode(propertyName: String, val data: List?, parent: SimpleNode?) : + AbstractFirExplorerTreeNode(propertyName, parent) { + + init { + if (data != null) { + templatePresentation.addText(data::class.simpleName, SimpleTextAttributes.REGULAR_ATTRIBUTES) + templatePresentation.addText("(size = ${data.size})", SimpleTextAttributes.REGULAR_ATTRIBUTES) + } else { + templatePresentation.addText("null", SimpleTextAttributes.REGULAR_ATTRIBUTES) + } + } + + override fun update(presentation: PresentationData) { + super.update(presentation) + presentation.setIcon(AllIcons.General.Recursive) + } + + override fun getChildren(): Array { + if (data == null) return SimpleNode.NO_CHILDREN + + + return data.mapIndexed { index, any -> + val propName = "[$index] = " + if (any is List<*>) { + FirExplorerTreeListNode(propName, any, this) + } else { + FirExplorerTreeNode(propName, any, this) + } + }.toTypedArray() + + } + + } + + + inner class FirExplorerTreeStructure : SimpleTreeStructure() { + var root: SimpleNode = FirExplorerTreeListNode("root = ", null, null) + + override fun getRootElement(): Any { + return root + } + } + + override fun dispose() { + + } + +} + +private fun KType.renderSimple(): String { + fun KVariance?.render(): String { + return when (this) { + KVariance.IN -> "in " + KVariance.OUT -> "out " + else -> "" + } + } + + return buildString { + val classifier = classifier + when (classifier) { + is KClass<*> -> append(classifier.simpleName) + is KTypeParameter -> append(classifier.name) + } + if (arguments.isNotEmpty()) { + append("<") + arguments.joinTo(this) { + it.variance.render() + it.type?.renderSimple() + } + append(">") + } + if (isMarkedNullable) { + append("?") + } + } +} \ No newline at end of file diff --git a/idea/fir-view/src/ShowFirAction.kt b/idea/fir-view/src/ShowFirAction.kt new file mode 100644 index 00000000000..6ba86958770 --- /dev/null +++ b/idea/fir-view/src/ShowFirAction.kt @@ -0,0 +1,45 @@ +/* + * Copyright 2000-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license + * that can be found in the license/LICENSE.txt file. + */ + +package org.jetbrains.kotlin.idea.actions.internal + +import com.intellij.openapi.actionSystem.AnAction +import com.intellij.openapi.actionSystem.AnActionEvent +import com.intellij.openapi.actionSystem.CommonDataKeys +import com.intellij.openapi.application.ApplicationManager +import com.intellij.openapi.wm.ToolWindowAnchor +import com.intellij.openapi.wm.ToolWindowManager +import com.intellij.ui.content.ContentFactory +import org.jetbrains.kotlin.idea.KotlinFileType +import org.jetbrains.kotlin.idea.KotlinIcons + +class ShowFirAction : AnAction() { + + val TOOLWINDOW_ID = "FIR Explorer" + + + override fun actionPerformed(e: AnActionEvent) { + val project = e.project ?: return + val toolWindowManager = ToolWindowManager.getInstance(project) + + var toolWindow = toolWindowManager.getToolWindow(TOOLWINDOW_ID) + if (toolWindow == null) { + toolWindow = toolWindowManager.registerToolWindow(TOOLWINDOW_ID, false, ToolWindowAnchor.RIGHT) + toolWindow.icon = KotlinIcons.SMALL_LOGO_13 + + val contentManager = toolWindow.contentManager + val contentFactory = ContentFactory.SERVICE.getInstance() + contentManager.addContent(contentFactory.createContent(FirExplorerToolWindow(project, toolWindow), "", false)) + } + toolWindow.activate(null) + } + + override fun update(e: AnActionEvent) { + val file = e.getData(CommonDataKeys.PSI_FILE) + e.presentation.isVisible = ApplicationManager.getApplication().isInternal + e.presentation.isEnabled = e.project != null && file?.fileType == KotlinFileType.INSTANCE + } + +} \ No newline at end of file diff --git a/idea/src/META-INF/plugin-common.xml b/idea/src/META-INF/plugin-common.xml index 95e814bf9ab..5d850d4baec 100644 --- a/idea/src/META-INF/plugin-common.xml +++ b/idea/src/META-INF/plugin-common.xml @@ -131,6 +131,9 @@ + + diff --git a/prepare/idea-plugin/build.gradle.kts b/prepare/idea-plugin/build.gradle.kts index 37aa724f048..85cb428443a 100644 --- a/prepare/idea-plugin/build.gradle.kts +++ b/prepare/idea-plugin/build.gradle.kts @@ -29,12 +29,23 @@ val projectsToShadow by extra(listOf( ":eval4j", ":idea:formatter", ":compiler:psi", + *if (project.findProperty("fir.enabled") == "true") { + arrayOf( + ":compiler:fir:cones", + ":compiler:fir:resolve", + ":compiler:fir:tree", + ":compiler:fir:psi2fir" + ) + } else { + emptyArray() + }, ":compiler:frontend", ":compiler:frontend.java", ":compiler:frontend.script", ":idea:ide-common", ":idea", ":idea:idea-native", + ":idea:fir-view", ":idea:idea-core", ":idea:idea-gradle", ":idea:idea-gradle-native", diff --git a/settings.gradle b/settings.gradle index 9c062ec9fa8..d2140a0a324 100644 --- a/settings.gradle +++ b/settings.gradle @@ -74,6 +74,7 @@ include ":kotlin-build-common", ":custom-dependencies:protobuf-relocated", ":custom-dependencies:protobuf-lite", ":custom-dependencies:android-sdk", + ":idea:fir-view", ":idea:idea-jvm", ":idea:idea-maven", ":idea:idea-gradle",