├── .gitignore ├── .idea ├── .gitignore ├── .name ├── Kotlin-FirViewer.iml ├── codeStyles ├── compiler.xml ├── encodings.xml ├── inspectionProfiles │ └── Project_Default.xml ├── libraries-with-intellij-classes.xml ├── misc.xml ├── modules │ └── FirViewer.iml └── vcs.xml ├── CONTRIBUTING ├── LICENSE.txt ├── README.md ├── build.gradle ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── settings.gradle └── src └── main ├── kotlin └── io │ └── github │ └── tgeng │ └── firviewer │ ├── CfgRenderService.kt │ ├── CfgViewToolWindowFactory.kt │ ├── FirViewerToolWindowFactory.kt │ ├── KtViewerToolWindowFactory.kt │ ├── NotificationInspection.kt │ ├── ObjectTreeModel.kt │ ├── ObjectViewer.kt │ ├── SvgView.kt │ ├── TableObjectRenderer.kt │ ├── TableObjectViewer.kt │ ├── TreeObjectRenderer.kt │ ├── TreeUiState.kt │ └── uiUtils.kt └── resources └── META-INF └── plugin.xml /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by .ignore support plugin (hsz.mobi) 2 | ### JetBrains template 3 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider 4 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 5 | 6 | # User-specific stuff 7 | .idea/**/workspace.xml 8 | .idea/**/tasks.xml 9 | .idea/**/usage.statistics.xml 10 | .idea/**/dictionaries 11 | .idea/**/shelf 12 | 13 | # Generated files 14 | .idea/**/contentModel.xml 15 | 16 | # Sensitive or high-churn files 17 | .idea/**/dataSources/ 18 | .idea/**/dataSources.ids 19 | .idea/**/dataSources.local.xml 20 | .idea/**/sqlDataSources.xml 21 | .idea/**/dynamic.xml 22 | .idea/**/uiDesigner.xml 23 | .idea/**/dbnavigator.xml 24 | 25 | # Gradle 26 | .idea/**/gradle.xml 27 | .idea/**/libraries 28 | .idea/jarRepositories.xml 29 | 30 | # Gradle and Maven with auto-import 31 | # When using Gradle or Maven with auto-import, you should exclude module files, 32 | # since they will be recreated, and may cause churn. Uncomment if using 33 | # auto-import. 34 | # .idea/artifacts 35 | # .idea/compiler.xml 36 | # .idea/jarRepositories.xml 37 | # .idea/modules.xml 38 | # .idea/*.iml 39 | # .idea/modules 40 | # *.iml 41 | # *.ipr 42 | 43 | # CMake 44 | cmake-build-*/ 45 | 46 | # Mongo Explorer plugin 47 | .idea/**/mongoSettings.xml 48 | 49 | # File-based project format 50 | *.iws 51 | 52 | # IntelliJ 53 | out/ 54 | 55 | # mpeltonen/sbt-idea plugin 56 | .idea_modules/ 57 | 58 | # JIRA plugin 59 | atlassian-ide-plugin.xml 60 | 61 | # Cursive Clojure plugin 62 | .idea/replstate.xml 63 | 64 | # Crashlytics plugin (for Android Studio and IntelliJ) 65 | com_crashlytics_export_strings.xml 66 | crashlytics.properties 67 | crashlytics-build.properties 68 | fabric.properties 69 | 70 | # Editor-based Rest Client 71 | .idea/httpRequests 72 | 73 | # Android studio 3.1+ serialized cache file 74 | .idea/caches/build_file_checksums.ser 75 | 76 | ### Gradle template 77 | .gradle 78 | **/build/ 79 | !src/**/build/ 80 | 81 | # Ignore Gradle GUI config 82 | gradle-app.setting 83 | 84 | # Avoid ignoring Gradle wrapper jar file (.jar files are usually ignored) 85 | !gradle-wrapper.jar 86 | 87 | # Cache of project 88 | .gradletasknamecache 89 | 90 | # # Work around https://youtrack.jetbrains.com/issue/IDEA-116898 91 | # gradle/wrapper/gradle-wrapper.properties 92 | 93 | kotlin.fir.jar 94 | -------------------------------------------------------------------------------- /.idea/.gitignore: -------------------------------------------------------------------------------- 1 | # Default ignored files 2 | /shelf/ 3 | /workspace.xml 4 | # CodeStream ignored files 5 | /codestream.xml 6 | -------------------------------------------------------------------------------- /.idea/.name: -------------------------------------------------------------------------------- 1 | FirViewer -------------------------------------------------------------------------------- /.idea/Kotlin-FirViewer.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /.idea/codeStyles: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | -------------------------------------------------------------------------------- /.idea/compiler.xml: -------------------------------------------------------------------------------- 1 | 2 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /.idea/encodings.xml: -------------------------------------------------------------------------------- 1 | 2 | 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /.idea/inspectionProfiles/Project_Default.xml: -------------------------------------------------------------------------------- 1 | 16 | 17 | 18 | 19 | 101 | -------------------------------------------------------------------------------- /.idea/libraries-with-intellij-classes.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 64 | 65 | -------------------------------------------------------------------------------- /.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /.idea/modules/FirViewer.iml: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /CONTRIBUTING: -------------------------------------------------------------------------------- 1 | How to Contribute 2 | 3 | We'd love to accept your patches and contributions to this project. There are 4 | just a few small guidelines you need to follow. 5 | 6 | ## Contributor License Agreement 7 | 8 | Contributions to this project must be accompanied by a Contributor License 9 | Agreement (CLA). You (or your employer) retain the copyright to your 10 | contribution; this simply gives us permission to use and redistribute your 11 | contributions as part of the project. Head over to 12 | to see your current agreements on file or 13 | to sign a new one. 14 | 15 | You generally only need to submit a CLA once, so if you've already submitted one 16 | (even if it was for a different project), you probably don't need to do it 17 | again. 18 | 19 | ## Code Reviews 20 | 21 | All submissions, including submissions by project members, require review. We 22 | use GitHub pull requests for this purpose. Consult 23 | [GitHub Help](https://help.github.com/articles/about-pull-requests/) for more 24 | information on using pull requests. 25 | 26 | ## Community Guidelines 27 | 28 | This project follows 29 | [Google's Open Source Community Guidelines](https://opensource.google/conduct/). 30 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright [yyyy] [name of copyright owner] 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. 203 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Kotlin FIR Viewer 2 | 3 | A small tool to inspect Kotlin FIR structure. 4 | 5 | ![image](https://user-images.githubusercontent.com/29584386/106402741-d2c64c80-63df-11eb-9b7d-5f89dbe967e8.png) 6 | 7 | ## Installation 8 | 9 | Note: This plugin **only** works with Kotlin plugin in FIR mode. 10 | 11 | 1. Go to the [release](https://github.com/google/Kotlin-FirViewer/releases) page and download the most recent release. 12 | 2. Launch IntelliJ with FIR plugin. 13 | 3. Open plugins setting. Click the gear icon on top, select "Install plugin from disk...", and pick the downloaded zip file and click OK. 14 | 15 | ## How to use? 16 | 17 | The plugin provides three tool windows 18 | * FIR Viewer (View -> Tool Windows -> FIR Viewer): shows FIR structures for the current opened file 19 | * KT Viewer (View -> Tool Windows -> KT Viewer): shows the Kotlin PSI structures for the current opened file plus some information exposed by the new idea-frontend-api (those carries star icons) 20 | * CFG Viewer (View -> Tool Windows -> CFG Viewer): shows the FIR control flow graph. To use this, you must have `dot`(graphviz) available in your PATH. You can install it via `homebrew install graphviz` or `sudo apt-get install graphviz` 21 | 22 | ## Build Instruction 23 | 24 | This plugin depends on the Kotlin plugin in FIR mode. Since Kotlin FIR mode is currently not released yet, you will need a local build of the Kotlin IDE plugin. Also, since the master branch of Kotlin project is built with non-released version of Kotlin compiler, this plugin will need to be compiled with the same (or more recent) version of Kotlin compiler (see beloow for instructions). 25 | 26 | 1. Clone https://github.com/JetBrains/kotlin.git 27 | 28 | 2. Clone https://github.com/JetBrains/intellij-community.git and put it inside /intellij 29 | 30 | 3. Open intellij-community project in IntelliJ and add an artifact bulding module `kotlin.fir` and its dependency. The output should be in `/out/artifacts/kotlin_fir_jar` See the following screenshot 31 | ![image](https://user-images.githubusercontent.com/29584386/141865850-f31c4444-d024-4c2e-a500-8376cc072cbb.png) 32 | ![image](https://user-images.githubusercontent.com/29584386/141865919-fdadd4ef-2d68-4861-9475-cb4de0acee51.png) 33 | 34 | 4. Run `./gradlew install` in the Kotlin repo to install needed dependencies in the local maven repo. 35 | 36 | 5. Build the artifact created in step 3 and copy the artifact to the project root of `FirViewer`. Make sure the copied 37 | artifact is named `kotlin.fir.jar` (this should be the default name). 38 | 39 | 6. `cd` into FirViewer project and build FirViewer with `./gradlew buildPlugin`. Note that you may want to bump up the plugin version in `build.gradle` in order to create a new release. 40 | 41 | 7. The resulted plugin file is located at `build/distributions`. 42 | 8. If everything works fine. Please consider committing your change, pushing upstream, and publishing a new release so others can use it. 43 | -------------------------------------------------------------------------------- /build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id 'java' 3 | id 'org.jetbrains.intellij' version '1.5.3' 4 | id 'org.jetbrains.kotlin.jvm' version '1.7.255-SNAPSHOT' 5 | } 6 | 7 | group 'org.example' 8 | version '1.2.28' 9 | 10 | repositories { 11 | mavenCentral() 12 | mavenLocal() 13 | } 14 | 15 | dependencies { 16 | implementation "org.jetbrains.kotlin:kotlin-stdlib" 17 | implementation "org.jetbrains.kotlin:kotlin-reflect" 18 | implementation group: 'com.kitfox.svg', name: 'svg-salamander', version: '1.0' 19 | compileOnly files("kotlin.fir.jar") 20 | testImplementation 'org.junit.jupiter:junit-jupiter-api:5.6.0' 21 | testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine' 22 | } 23 | 24 | // See https://github.com/JetBrains/gradle-intellij-plugin/ 25 | intellij { 26 | pluginName = 'Kotlin FirViewer' 27 | version = '2021.2' 28 | updateSinceUntilBuild = false 29 | } 30 | 31 | test { 32 | useJUnitPlatform() 33 | } 34 | compileKotlin { 35 | kotlinOptions { 36 | jvmTarget = "11" 37 | freeCompilerArgs += "-Xopt-in=kotlin.RequiresOptIn" 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | kotlin.code.style=official -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/google/Kotlin-FirViewer/0bede8ef23dab8925dde49c9b316c7c02c6cdebb/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-7.4.2-bin.zip 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | # 4 | # Copyright 2015 the original author or authors. 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # https://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | 19 | ############################################################################## 20 | ## 21 | ## Gradle start up script for UN*X 22 | ## 23 | ############################################################################## 24 | 25 | # Attempt to set APP_HOME 26 | # Resolve links: $0 may be a link 27 | PRG="$0" 28 | # Need this for relative symlinks. 29 | while [ -h "$PRG" ] ; do 30 | ls=`ls -ld "$PRG"` 31 | link=`expr "$ls" : '.*-> \(.*\)$'` 32 | if expr "$link" : '/.*' > /dev/null; then 33 | PRG="$link" 34 | else 35 | PRG=`dirname "$PRG"`"/$link" 36 | fi 37 | done 38 | SAVED="`pwd`" 39 | cd "`dirname \"$PRG\"`/" >/dev/null 40 | APP_HOME="`pwd -P`" 41 | cd "$SAVED" >/dev/null 42 | 43 | APP_NAME="Gradle" 44 | APP_BASE_NAME=`basename "$0"` 45 | 46 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 47 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' 48 | 49 | # Use the maximum available, or set MAX_FD != -1 to use that value. 50 | MAX_FD="maximum" 51 | 52 | warn () { 53 | echo "$*" 54 | } 55 | 56 | die () { 57 | echo 58 | echo "$*" 59 | echo 60 | exit 1 61 | } 62 | 63 | # OS specific support (must be 'true' or 'false'). 64 | cygwin=false 65 | msys=false 66 | darwin=false 67 | nonstop=false 68 | case "`uname`" in 69 | CYGWIN* ) 70 | cygwin=true 71 | ;; 72 | Darwin* ) 73 | darwin=true 74 | ;; 75 | MINGW* ) 76 | msys=true 77 | ;; 78 | NONSTOP* ) 79 | nonstop=true 80 | ;; 81 | esac 82 | 83 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 84 | 85 | 86 | # Determine the Java command to use to start the JVM. 87 | if [ -n "$JAVA_HOME" ] ; then 88 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 89 | # IBM's JDK on AIX uses strange locations for the executables 90 | JAVACMD="$JAVA_HOME/jre/sh/java" 91 | else 92 | JAVACMD="$JAVA_HOME/bin/java" 93 | fi 94 | if [ ! -x "$JAVACMD" ] ; then 95 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 96 | 97 | Please set the JAVA_HOME variable in your environment to match the 98 | location of your Java installation." 99 | fi 100 | else 101 | JAVACMD="java" 102 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 103 | 104 | Please set the JAVA_HOME variable in your environment to match the 105 | location of your Java installation." 106 | fi 107 | 108 | # Increase the maximum file descriptors if we can. 109 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then 110 | MAX_FD_LIMIT=`ulimit -H -n` 111 | if [ $? -eq 0 ] ; then 112 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 113 | MAX_FD="$MAX_FD_LIMIT" 114 | fi 115 | ulimit -n $MAX_FD 116 | if [ $? -ne 0 ] ; then 117 | warn "Could not set maximum file descriptor limit: $MAX_FD" 118 | fi 119 | else 120 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 121 | fi 122 | fi 123 | 124 | # For Darwin, add options to specify how the application appears in the dock 125 | if $darwin; then 126 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 127 | fi 128 | 129 | # For Cygwin or MSYS, switch paths to Windows format before running java 130 | if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then 131 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 132 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 133 | 134 | JAVACMD=`cygpath --unix "$JAVACMD"` 135 | 136 | # We build the pattern for arguments to be converted via cygpath 137 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 138 | SEP="" 139 | for dir in $ROOTDIRSRAW ; do 140 | ROOTDIRS="$ROOTDIRS$SEP$dir" 141 | SEP="|" 142 | done 143 | OURCYGPATTERN="(^($ROOTDIRS))" 144 | # Add a user-defined pattern to the cygpath arguments 145 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 146 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 147 | fi 148 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 149 | i=0 150 | for arg in "$@" ; do 151 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 152 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 153 | 154 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 155 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 156 | else 157 | eval `echo args$i`="\"$arg\"" 158 | fi 159 | i=`expr $i + 1` 160 | done 161 | case $i in 162 | 0) set -- ;; 163 | 1) set -- "$args0" ;; 164 | 2) set -- "$args0" "$args1" ;; 165 | 3) set -- "$args0" "$args1" "$args2" ;; 166 | 4) set -- "$args0" "$args1" "$args2" "$args3" ;; 167 | 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 168 | 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 169 | 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 170 | 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 171 | 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 172 | esac 173 | fi 174 | 175 | # Escape application args 176 | save () { 177 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done 178 | echo " " 179 | } 180 | APP_ARGS=`save "$@"` 181 | 182 | # Collect all arguments for the java command, following the shell quoting and substitution rules 183 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" 184 | 185 | exec "$JAVACMD" "$@" 186 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | 17 | @if "%DEBUG%" == "" @echo off 18 | @rem ########################################################################## 19 | @rem 20 | @rem Gradle startup script for Windows 21 | @rem 22 | @rem ########################################################################## 23 | 24 | @rem Set local scope for the variables with windows NT shell 25 | if "%OS%"=="Windows_NT" setlocal 26 | 27 | set DIRNAME=%~dp0 28 | if "%DIRNAME%" == "" set DIRNAME=. 29 | set APP_BASE_NAME=%~n0 30 | set APP_HOME=%DIRNAME% 31 | 32 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 33 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 34 | 35 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 36 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 37 | 38 | @rem Find java.exe 39 | if defined JAVA_HOME goto findJavaFromJavaHome 40 | 41 | set JAVA_EXE=java.exe 42 | %JAVA_EXE% -version >NUL 2>&1 43 | if "%ERRORLEVEL%" == "0" goto execute 44 | 45 | echo. 46 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 47 | echo. 48 | echo Please set the JAVA_HOME variable in your environment to match the 49 | echo location of your Java installation. 50 | 51 | goto fail 52 | 53 | :findJavaFromJavaHome 54 | set JAVA_HOME=%JAVA_HOME:"=% 55 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 56 | 57 | if exist "%JAVA_EXE%" goto execute 58 | 59 | echo. 60 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 61 | echo. 62 | echo Please set the JAVA_HOME variable in your environment to match the 63 | echo location of your Java installation. 64 | 65 | goto fail 66 | 67 | :execute 68 | @rem Setup the command line 69 | 70 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 71 | 72 | 73 | @rem Execute Gradle 74 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* 75 | 76 | :end 77 | @rem End local scope for the variables with windows NT shell 78 | if "%ERRORLEVEL%"=="0" goto mainEnd 79 | 80 | :fail 81 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 82 | rem the _cmd.exe /c_ return code! 83 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 84 | exit /b 1 85 | 86 | :mainEnd 87 | if "%OS%"=="Windows_NT" endlocal 88 | 89 | :omega 90 | -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | pluginManagement { 2 | repositories { 3 | mavenLocal() 4 | gradlePluginPortal() 5 | } 6 | } 7 | rootProject.name = 'FirViewer' 8 | 9 | -------------------------------------------------------------------------------- /src/main/kotlin/io/github/tgeng/firviewer/CfgRenderService.kt: -------------------------------------------------------------------------------- 1 | package io.github.tgeng.firviewer 2 | 3 | import com.google.common.cache.CacheBuilder 4 | import com.intellij.openapi.application.ApplicationManager 5 | import com.intellij.openapi.components.Service 6 | import com.intellij.openapi.diagnostic.Logger.* 7 | import com.intellij.openapi.project.Project 8 | import com.intellij.openapi.vfs.VfsUtil 9 | import com.intellij.openapi.vfs.VirtualFile 10 | import com.intellij.psi.PsiManager 11 | import com.intellij.ui.JBColor 12 | import com.kitfox.svg.SVGDiagram 13 | import com.kitfox.svg.SVGUniverse 14 | import org.jetbrains.kotlin.fir.resolve.dfa.cfg.CFGNode 15 | import org.jetbrains.kotlin.fir.resolve.dfa.cfg.FirControlFlowGraphRenderVisitor 16 | import org.jetbrains.kotlin.fir.resolve.dfa.cfg.render 17 | import org.jetbrains.kotlin.analysis.low.level.api.fir.api.getOrBuildFirFile 18 | import org.jetbrains.kotlin.analysis.low.level.api.fir.api.getFirResolveSession 19 | import org.jetbrains.kotlin.psi.KtFile 20 | import java.awt.Color 21 | import java.io.File 22 | import java.util.* 23 | import java.util.concurrent.CompletableFuture 24 | import java.util.concurrent.Executors 25 | 26 | @Service 27 | class CfgRenderService(private val project: Project) { 28 | private val universe = SVGUniverse() 29 | private val logger = getInstance(CfgRenderService::class.java) 30 | private val cache = CacheBuilder.newBuilder().weakKeys().build() 31 | 32 | fun getSvg(key: GraphKey): CompletableFuture { 33 | val graph = getGraphString(key.virtualFile) 34 | return getGraphSvg(key, graph) 35 | } 36 | 37 | private fun getGraphString(vf: VirtualFile): String { 38 | val sb = StringBuilder() 39 | ApplicationManager.getApplication().runReadAction { 40 | val ktFile = PsiManager.getInstance(project).findFile(vf) as? KtFile ?: return@runReadAction 41 | val firFile = ktFile.getOrBuildFirFile(ktFile.getFirResolveSession()) 42 | firFile.accept(FirControlFlowGraphRenderVisitor(sb)) 43 | } 44 | return sb.toString() 45 | } 46 | 47 | private fun getGraphSvg(key: GraphKey, graph: String): CompletableFuture { 48 | if (!isDotAvailable()) return CompletableFuture.completedFuture(null) 49 | return CompletableFuture.supplyAsync({ 50 | val vf = key.virtualFile 51 | val storedHash = cache.get(vf) { 0 } 52 | if (storedHash != graph.hashCode()) { 53 | cache.put(vf, graph.hashCode()) 54 | vf.getScratchDir().deleteRecursively() 55 | val processes = mutableListOf>() 56 | for ((name, graph) in splitGraphs(graph)) { 57 | val graphKey = GraphKey(vf, name) 58 | graphKey.getDotFile().apply { 59 | parentFile.mkdirs() 60 | writeText(graph) 61 | processes += graphKey to ProcessBuilder( 62 | "dot", 63 | this.absolutePath, 64 | "-Tsvg", 65 | "-o", 66 | graphKey.getSvgFile().absolutePath 67 | ).start() 68 | } 69 | } 70 | processes.forEach { (key, process) -> 71 | process.waitFor() 72 | if (process.exitValue() == 0) { 73 | key.getSvgFile().writeText(key.getSvgFile().readText().replace("stroke=\"transparent\"", "")) 74 | } else { 75 | logger.error("Failed to convert ${key.getDotFile()} to PNG with dot.") 76 | key.getSvgFile().delete() 77 | } 78 | } 79 | } 80 | if (!key.getSvgFile().exists()) { 81 | return@supplyAsync null 82 | } 83 | key.getSvgFile().inputStream().use { it -> 84 | universe.getDiagram(universe.loadSVG(it, UUID.randomUUID().toString())) 85 | } 86 | }, executorService) 87 | } 88 | 89 | fun getGraphKey(virtualFile: VirtualFile) = GraphKey(virtualFile, "") 90 | 91 | fun getGraphKey(virtualFile: VirtualFile, node: CFGNode<*>) = 92 | GraphKey(virtualFile, node.render().replace("\"", "")) 93 | 94 | data class GraphKey(val virtualFile: VirtualFile, val name: String) 95 | 96 | private fun GraphKey.getDotFile(): File = virtualFile.getScratchDir().resolve(name + ".dot") 97 | private fun GraphKey.getSvgFile(): File = virtualFile.getScratchDir().resolve(name + ".svg") 98 | private fun VirtualFile.getScratchDir(): File = 99 | rendersRoot.resolve("cfg" + VfsUtil.virtualToIoFile(this).absolutePath) 100 | 101 | companion object { 102 | private val rendersRoot = File(System.getProperty("java.io.tmpdir")).resolve("firviewer") 103 | private val executorService = Executors.newSingleThreadExecutor() 104 | 105 | fun isDotAvailable(): Boolean { 106 | val process = ProcessBuilder("dot", "-V").start() 107 | process.waitFor() 108 | return process.exitValue() == 0 109 | } 110 | 111 | fun getInstance(project: Project): CfgRenderService = project.getService(CfgRenderService::class.java) 112 | } 113 | } 114 | 115 | private val labelNameRegex = Regex(""".*\[label="([^"]*)".*""") 116 | 117 | fun splitGraphs(graph: String): Map { 118 | val lines = graph.split(System.lineSeparator()) 119 | val firstSubGraphIndex = lines.indexOfFirst { it.startsWith(" subgraph cluster") } 120 | if (firstSubGraphIndex == -1) return emptyMap() // graph is not initialized yet 121 | val dark = JBColor.PanelBackground.brightness() < 0.5 122 | val header = if (dark) """ 123 | digraph atLeastOnce_kt { 124 | graph [nodesep=3 fontname="Arial" fontsize=24 bgcolor="${JBColor.PanelBackground.hex()}" color=white] 125 | node [shape=box margin="0.15,0.05" width=0 height=0 fontname="Arial" fontsize=24 color=white fontcolor=white] 126 | edge [penwidth=2 fontname="Arial" fontsize=24 len=0.5 color=white] 127 | """.trimIndent().split("\n") else 128 | """ 129 | digraph atLeastOnce_kt { 130 | graph [nodesep=3 fontname="Arial" fontsize=24 bgcolor="${JBColor.PanelBackground.hex()}"] 131 | node [shape=box margin="0.15,0.05" width=0 height=0 fontname="Arial" fontsize=24] 132 | edge [penwidth=2 fontname="Arial" fontsize=24 len=0.5] 133 | """.trimIndent().split("\n") 134 | 135 | 136 | val subGraphs = mutableListOf>() 137 | var currentSubGraphIndex = firstSubGraphIndex 138 | while (true) { 139 | var nextSubGraphIndex = -1 140 | for (i in currentSubGraphIndex + 1 until lines.size) { 141 | if (lines[i].startsWith(" subgraph cluster")) { 142 | nextSubGraphIndex = i 143 | break 144 | } 145 | } 146 | if (nextSubGraphIndex == -1) { 147 | // no more 148 | subGraphs.add(lines.subList(currentSubGraphIndex, lines.indexOfLast { it.startsWith("}") })) 149 | break 150 | } else { 151 | subGraphs.add(lines.subList(currentSubGraphIndex, nextSubGraphIndex)) 152 | } 153 | currentSubGraphIndex = nextSubGraphIndex 154 | } 155 | 156 | fun List.replaceGraphColors(): String = 157 | (header + this + listOf("}")).joinToString(System.lineSeparator()) { 158 | if (dark) { 159 | it.replace("=blue", "=\"#2abbd1\"") 160 | .replace("=gray", "=\"#7a7a7a\"") 161 | .replace("=green", "=\"#44ff3d\"") 162 | .replace("=yellow", "=\"#9c8700\"") 163 | .replace("=red", "=\"#d10808\"") 164 | } else { 165 | it.replace("=green", "=\"#3acf61\"") 166 | .replace("=gray", "=\"#bdbdbd\"") 167 | } 168 | } 169 | 170 | return (subGraphs.map { subGraph -> 171 | val key = subGraph.asSequence().mapNotNull { labelNameRegex.matchEntire(it) }.first().groupValues[1] 172 | val value = subGraph.replaceGraphColors() 173 | key to value 174 | } + ("" to subGraphs.flatten().replaceGraphColors())).toMap() 175 | } 176 | 177 | 178 | private fun Color.hex(): String = String.format("#%02x%02x%02x", red, green, blue); 179 | 180 | private fun Color.brightness(): Double = (0.2126 * red + 0.7152 * green + 0.0722 * blue) / 255 -------------------------------------------------------------------------------- /src/main/kotlin/io/github/tgeng/firviewer/CfgViewToolWindowFactory.kt: -------------------------------------------------------------------------------- 1 | package io.github.tgeng.firviewer 2 | 3 | import com.intellij.icons.AllIcons 4 | import com.intellij.openapi.actionSystem.AnAction 5 | import com.intellij.openapi.actionSystem.AnActionEvent 6 | import com.intellij.openapi.diagnostic.Logger 7 | import com.intellij.openapi.editor.event.CaretEvent 8 | import com.intellij.openapi.editor.event.CaretListener 9 | import com.intellij.openapi.fileEditor.FileDocumentManager 10 | import com.intellij.openapi.fileEditor.FileEditorManager 11 | import com.intellij.openapi.fileEditor.FileEditorManagerListener 12 | import com.intellij.openapi.project.DumbAware 13 | import com.intellij.openapi.project.DumbService 14 | import com.intellij.openapi.project.Project 15 | import com.intellij.openapi.vfs.VirtualFile 16 | import com.intellij.openapi.wm.ToolWindow 17 | import com.intellij.openapi.wm.ToolWindowFactory 18 | import com.intellij.psi.PsiManager 19 | import org.jetbrains.kotlin.fir.declarations.FirControlFlowGraphOwner 20 | import org.jetbrains.kotlin.fir.resolve.dfa.cfg.CFGNode 21 | import org.jetbrains.kotlin.fir.resolve.dfa.controlFlowGraph 22 | import org.jetbrains.kotlin.analysis.low.level.api.fir.api.LLFirResolveSession 23 | import org.jetbrains.kotlin.analysis.low.level.api.fir.api.getOrBuildFirSafe 24 | import org.jetbrains.kotlin.analysis.low.level.api.fir.api.getFirResolveSession 25 | import org.jetbrains.kotlin.psi.KtElement 26 | 27 | 28 | class CfgViewToolWindowFactory : ToolWindowFactory { 29 | 30 | val logger = Logger.getInstance(CfgViewToolWindowFactory::class.java) 31 | override fun createToolWindowContent(project: Project, toolWindow: ToolWindow) { 32 | toolWindow.title = "CfgViewer" 33 | toolWindow.setIcon(AllIcons.Toolwindows.ToolWindowAnt) 34 | val declarationView = SvgView() 35 | val fileView = SvgView() 36 | 37 | toolWindow.contentManager.addContent( 38 | toolWindow.contentManager.factory.createContent( 39 | declarationView, 40 | "Current Declaration", 41 | false 42 | ) 43 | ) 44 | toolWindow.contentManager.addContent( 45 | toolWindow.contentManager.factory.createContent( 46 | fileView, 47 | "Current File", 48 | false 49 | ) 50 | ) 51 | 52 | var currentGraphKey: CfgRenderService.GraphKey? = null 53 | fun refresh() { 54 | DumbService.getInstance(project).runWhenSmart { 55 | project.refreshFileView(toolWindow, fileView) 56 | (project.getCurrentGraphKey() ?: currentGraphKey)?.let { 57 | project.refreshDeclarationView(toolWindow, declarationView, it) 58 | } 59 | } 60 | } 61 | 62 | val caretListener = object : CaretListener { 63 | override fun caretPositionChanged(event: CaretEvent) { 64 | val key = project.getCurrentGraphKey() 65 | if (key != currentGraphKey && key != null) { 66 | currentGraphKey = key 67 | project.refreshDeclarationView(toolWindow, declarationView, key) 68 | } 69 | } 70 | } 71 | 72 | project.messageBus.connect() 73 | .subscribe(FileEditorManagerListener.FILE_EDITOR_MANAGER, object : FileEditorManagerListener { 74 | override fun fileOpened(source: FileEditorManager, file: VirtualFile) { 75 | refresh() 76 | source.selectedTextEditor?.let { editor -> 77 | editor.caretModel.addCaretListener(caretListener) 78 | } 79 | } 80 | }) 81 | 82 | project.messageBus.connect().subscribe(EVENT_TOPIC, Runnable { refresh() }) 83 | toolWindow.setTitleActions(listOf(object : 84 | AnAction("Reset", "Reset pan and zoom", AllIcons.General.ActualZoom), DumbAware { 85 | override fun actionPerformed(e: AnActionEvent) { 86 | when (toolWindow.contentManager.selectedContent?.toolwindowTitle) { 87 | "Current Declaration" -> declarationView.reset() 88 | "Current File" -> fileView.reset() 89 | } 90 | } 91 | }, 92 | object : AnAction(), DumbAware { 93 | override fun update(e: AnActionEvent) { 94 | e.presentation.icon = AllIcons.Actions.Refresh 95 | } 96 | 97 | override fun actionPerformed(e: AnActionEvent) = refresh() 98 | } 99 | )) 100 | 101 | refresh() 102 | val editorManager = FileEditorManager.getInstance(project) 103 | editorManager.selectedTextEditor?.let { editor -> 104 | editor.caretModel.addCaretListener(caretListener) 105 | } 106 | } 107 | 108 | private fun Project.getCurrentGraphKey(): CfgRenderService.GraphKey? { 109 | if (DumbService.isDumb(this)) return null 110 | val editor = FileEditorManager.getInstance(this).selectedTextEditor ?: return null 111 | val vf = FileDocumentManager.getInstance().getFile(editor.document) ?: return null 112 | val offset: Int = editor.caretModel.offset 113 | var element = PsiManager.getInstance(this).findFile(vf)?.findElementAt(offset) 114 | while (element != null && element !is KtElement) { 115 | element = element.parent 116 | } 117 | if (element == null) return null 118 | var ktElement = element as KtElement? 119 | val resolveState = ktElement!!.getFirResolveSession() 120 | var topLevelCfgEnterNode: CFGNode<*>? = null 121 | try { 122 | 123 | while (ktElement != null) { 124 | topLevelCfgEnterNode = ktElement.getTopLevelCfgEnterNode(resolveState) 125 | if (topLevelCfgEnterNode != null) break 126 | ktElement = ktElement.parentKtElement 127 | } 128 | } catch (e: Throwable) { 129 | logger.warn(e) 130 | return null 131 | } 132 | if (topLevelCfgEnterNode == null) return null 133 | return CfgRenderService.getInstance(this).getGraphKey(vf, topLevelCfgEnterNode) 134 | } 135 | 136 | private fun KtElement.getTopLevelCfgEnterNode(resolveState: LLFirResolveSession): CFGNode<*>? { 137 | val cfg = getOrBuildFirSafe(resolveState)?.controlFlowGraphReference?.controlFlowGraph 138 | ?: return null 139 | if (cfg.owner != null) return null // Not a top level CFG 140 | return cfg.enterNode 141 | } 142 | 143 | private val KtElement.parentKtElement: KtElement? 144 | get() { 145 | var current = parent 146 | while (current != null && current !is KtElement) { 147 | current = current.parent 148 | } 149 | return current as KtElement? 150 | } 151 | 152 | private fun Project.refreshDeclarationView( 153 | toolWindow: ToolWindow, 154 | view: SvgView, 155 | graphKey: CfgRenderService.GraphKey 156 | ) { 157 | if (!toolWindow.isVisible) return 158 | CfgRenderService.getInstance(this).getSvg(graphKey).thenAccept { 159 | if (it != null) view.setSvg(it) 160 | } 161 | } 162 | 163 | private fun Project.refreshFileView( 164 | toolWindow: ToolWindow, 165 | view: SvgView, 166 | ) { 167 | if (!toolWindow.isVisible) return 168 | val editor = FileEditorManager.getInstance(this).selectedTextEditor ?: return 169 | val vf = FileDocumentManager.getInstance().getFile(editor.document) ?: return 170 | val cfgRenderService = CfgRenderService.getInstance(this) 171 | cfgRenderService.getSvg(cfgRenderService.getGraphKey(vf)).thenAccept { 172 | if (it != null) view.setSvg(it) 173 | } 174 | } 175 | } -------------------------------------------------------------------------------- /src/main/kotlin/io/github/tgeng/firviewer/FirViewerToolWindowFactory.kt: -------------------------------------------------------------------------------- 1 | // Copyright 2021 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package io.github.tgeng.firviewer 16 | 17 | import com.google.common.cache.CacheBuilder 18 | import com.intellij.icons.AllIcons 19 | import com.intellij.openapi.actionSystem.AnAction 20 | import com.intellij.openapi.actionSystem.AnActionEvent 21 | import com.intellij.openapi.fileEditor.FileEditorManager 22 | import com.intellij.openapi.fileEditor.FileEditorManagerListener 23 | import com.intellij.openapi.project.DumbAware 24 | import com.intellij.openapi.project.Project 25 | import com.intellij.openapi.vfs.VirtualFile 26 | import com.intellij.openapi.wm.ToolWindow 27 | import com.intellij.openapi.wm.ToolWindowFactory 28 | import com.intellij.psi.PsiFile 29 | import com.intellij.psi.PsiManager 30 | import org.jetbrains.kotlin.fir.FirElement 31 | import org.jetbrains.kotlin.fir.FirPureAbstractElement 32 | import org.jetbrains.kotlin.fir.visitors.FirVisitorVoid 33 | import org.jetbrains.kotlin.analysis.low.level.api.fir.api.getOrBuildFirFile 34 | import org.jetbrains.kotlin.analysis.low.level.api.fir.api.getFirResolveSession 35 | import org.jetbrains.kotlin.psi.KtFile 36 | import java.awt.event.FocusAdapter 37 | import java.awt.event.FocusEvent 38 | 39 | class FirViewerToolWindowFactory : ToolWindowFactory, DumbAware { 40 | 41 | private val cache = CacheBuilder.newBuilder().weakKeys().build() 42 | 43 | override fun createToolWindowContent(project: Project, toolWindow: ToolWindow) { 44 | toolWindow.title = "FirViewer" 45 | toolWindow.setIcon(AllIcons.Toolwindows.ToolWindowHierarchy) 46 | refresh(project, toolWindow) 47 | 48 | toolWindow.setTitleActions(listOf(object : AnAction(), DumbAware { 49 | override fun update(e: AnActionEvent) { 50 | e.presentation.icon = AllIcons.Actions.Refresh 51 | } 52 | 53 | override fun actionPerformed(e: AnActionEvent) { 54 | refresh(project, toolWindow) 55 | } 56 | })) 57 | 58 | refresh(project, toolWindow) 59 | project.messageBus.connect() 60 | .subscribe(FileEditorManagerListener.FILE_EDITOR_MANAGER, object : FileEditorManagerListener { 61 | override fun fileOpened(source: FileEditorManager, file: VirtualFile) { 62 | refresh(project, toolWindow) 63 | } 64 | }) 65 | 66 | project.messageBus.connect().subscribe(EVENT_TOPIC, Runnable { refresh(project, toolWindow) }) 67 | } 68 | 69 | private fun refresh(project: Project, toolWindow: ToolWindow) { 70 | if (!toolWindow.isVisible) return 71 | val vf = FileEditorManager.getInstance(project).selectedFiles.firstOrNull() ?: return 72 | val ktFile = PsiManager.getInstance(project).findFile(vf) as? KtFile ?: return 73 | val treeUiState = cache.get(ktFile) { 74 | val treeModel = ObjectTreeModel( 75 | ktFile, 76 | FirPureAbstractElement::class, 77 | { it.getOrBuildFirFile(it.getFirResolveSession()) }) { consumer -> 78 | acceptChildren(object : FirVisitorVoid() { 79 | override fun visitElement(element: FirElement) { 80 | if (element is FirPureAbstractElement) consumer(element) 81 | } 82 | }) 83 | } 84 | 85 | treeModel.setupTreeUi(project).apply { 86 | pane.addFocusListener(object : FocusAdapter() { 87 | override fun focusGained(e: FocusEvent?) { 88 | refresh(project, toolWindow) 89 | } 90 | }) 91 | } 92 | } 93 | treeUiState.refreshTree() 94 | 95 | if (toolWindow.contentManager.contents.firstOrNull() != treeUiState.pane) { 96 | toolWindow.contentManager.removeAllContents(true) 97 | toolWindow.contentManager.addContent( 98 | toolWindow.contentManager.factory.createContent( 99 | treeUiState.pane, 100 | "Current File", 101 | true 102 | ) 103 | ) 104 | } 105 | } 106 | } 107 | 108 | -------------------------------------------------------------------------------- /src/main/kotlin/io/github/tgeng/firviewer/KtViewerToolWindowFactory.kt: -------------------------------------------------------------------------------- 1 | // Copyright 2021 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package io.github.tgeng.firviewer 16 | 17 | import com.google.common.cache.CacheBuilder 18 | import com.intellij.icons.AllIcons 19 | import com.intellij.openapi.actionSystem.AnAction 20 | import com.intellij.openapi.actionSystem.AnActionEvent 21 | import com.intellij.openapi.application.ApplicationManager 22 | import com.intellij.openapi.fileEditor.FileEditorManager 23 | import com.intellij.openapi.fileEditor.FileEditorManagerListener 24 | import com.intellij.openapi.project.DumbAware 25 | import com.intellij.openapi.project.Project 26 | import com.intellij.openapi.vfs.VirtualFile 27 | import com.intellij.openapi.wm.ToolWindow 28 | import com.intellij.openapi.wm.ToolWindowFactory 29 | import com.intellij.psi.* 30 | import org.jetbrains.kotlin.psi.KtFile 31 | import java.awt.event.FocusAdapter 32 | import java.awt.event.FocusEvent 33 | 34 | class KtViewerToolWindowFactory : ToolWindowFactory { 35 | 36 | private val cache = CacheBuilder.newBuilder().weakKeys().build() 37 | 38 | override fun createToolWindowContent(project: Project, toolWindow: ToolWindow) { 39 | toolWindow.title = "KtViewer" 40 | toolWindow.setIcon(AllIcons.Toolwindows.ToolWindowStructure) 41 | refresh(project, toolWindow) 42 | 43 | toolWindow.setTitleActions(listOf(object : AnAction(), DumbAware { 44 | override fun update(e: AnActionEvent) { 45 | e.presentation.icon = AllIcons.Actions.Refresh 46 | } 47 | 48 | override fun actionPerformed(e: AnActionEvent) { 49 | refresh(project, toolWindow) 50 | } 51 | })) 52 | 53 | refresh(project, toolWindow) 54 | project.messageBus.connect() 55 | .subscribe(FileEditorManagerListener.FILE_EDITOR_MANAGER, object : FileEditorManagerListener { 56 | override fun fileOpened(source: FileEditorManager, file: VirtualFile) { 57 | refresh(project, toolWindow) 58 | } 59 | }) 60 | 61 | project.messageBus.connect().subscribe(EVENT_TOPIC, Runnable { refresh(project, toolWindow) }) 62 | } 63 | 64 | private fun refresh(project: Project, toolWindow: ToolWindow) { 65 | if (!toolWindow.isVisible) return 66 | val vf = FileEditorManager.getInstance(project).selectedFiles.firstOrNull() ?: return 67 | val ktFile = PsiManager.getInstance(project).findFile(vf) as? KtFile ?: return 68 | val treeUiState = cache.get(ktFile) { 69 | val treeModel = ObjectTreeModel( 70 | ktFile, 71 | PsiElement::class, 72 | { it }) { consumer -> 73 | ApplicationManager.getApplication().runReadAction { 74 | this.takeIf { it.isValid }?.acceptChildren(object : PsiElementVisitor() { 75 | override fun visitElement(element: PsiElement) { 76 | if (element is PsiWhiteSpace) return 77 | consumer(element) 78 | } 79 | }) 80 | } 81 | } 82 | 83 | treeModel.setupTreeUi(project).apply { 84 | pane.addFocusListener(object : FocusAdapter() { 85 | override fun focusGained(e: FocusEvent?) { 86 | refresh(project, toolWindow) 87 | } 88 | }) 89 | } 90 | } 91 | treeUiState.refreshTree() 92 | 93 | if (toolWindow.contentManager.contents.firstOrNull() != treeUiState.pane) { 94 | toolWindow.contentManager.removeAllContents(true) 95 | toolWindow.contentManager.addContent( 96 | toolWindow.contentManager.factory.createContent( 97 | treeUiState.pane, 98 | "Current File", 99 | true 100 | ) 101 | ) 102 | } 103 | } 104 | } -------------------------------------------------------------------------------- /src/main/kotlin/io/github/tgeng/firviewer/NotificationInspection.kt: -------------------------------------------------------------------------------- 1 | // Copyright 2021 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package io.github.tgeng.firviewer 16 | 17 | import com.intellij.codeInspection.InspectionManager 18 | import com.intellij.codeInspection.LocalInspectionTool 19 | import com.intellij.codeInspection.ProblemDescriptor 20 | import com.intellij.openapi.application.ApplicationManager 21 | import com.intellij.psi.PsiFile 22 | import com.intellij.util.messages.Topic 23 | 24 | val EVENT_TOPIC = Topic.create("FIR_VIEWER_UPDATE_TOPIC", Runnable::class.java) 25 | 26 | class NotificationInspection : LocalInspectionTool() { 27 | 28 | override fun checkFile(file: PsiFile, manager: InspectionManager, isOnTheFly: Boolean): Array? { 29 | ApplicationManager.getApplication().invokeLater { 30 | file.project.messageBus.syncPublisher(EVENT_TOPIC).run() 31 | } 32 | return arrayOf() 33 | } 34 | } 35 | 36 | -------------------------------------------------------------------------------- /src/main/kotlin/io/github/tgeng/firviewer/ObjectTreeModel.kt: -------------------------------------------------------------------------------- 1 | // Copyright 2021 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package io.github.tgeng.firviewer 16 | 17 | import com.intellij.openapi.project.Project 18 | import com.intellij.ui.JBSplitter 19 | import com.intellij.ui.components.JBScrollPane 20 | import com.intellij.ui.tree.BaseTreeModel 21 | import com.intellij.ui.treeStructure.Tree 22 | import com.intellij.util.ui.tree.TreeUtil 23 | import org.jetbrains.kotlin.psi.KtElement 24 | import org.jetbrains.kotlin.psi.KtFile 25 | import java.awt.event.MouseEvent 26 | import java.awt.event.MouseListener 27 | import java.util.concurrent.atomic.AtomicInteger 28 | import javax.swing.BoxLayout 29 | import javax.swing.JPanel 30 | import javax.swing.tree.TreePath 31 | import kotlin.reflect.KClass 32 | 33 | class ObjectTreeModel( 34 | private val ktFile: KtFile, 35 | private val tClass: KClass, 36 | val ktFileToT: (KtFile) -> T, 37 | val acceptChildren: T.((T) -> Unit) -> Unit 38 | ) : BaseTreeModel>() { 39 | private var root: TreeNode? = null 40 | 41 | init { 42 | refresh() 43 | } 44 | 45 | override fun getRoot(): TreeNode { 46 | return root!! 47 | } 48 | 49 | override fun getChildren(parent: Any?): List> { 50 | val parent = parent as? TreeNode<*> ?: return emptyList() 51 | return parent.currentChildren as List> 52 | } 53 | 54 | fun refresh() { 55 | val firFile = ktFileToT(ktFile) 56 | if (firFile != root?.t) { 57 | val newFileNode = TreeNode("", firFile) 58 | root = newFileNode 59 | } 60 | root!!.refresh(listOf(root!!)) 61 | treeStructureChanged(null, null, null) 62 | } 63 | 64 | private fun TreeNode.refresh(path: List>) { 65 | val newChildren = mutableListOf>() 66 | val childNameAndValues = mutableListOf>() 67 | t.traverseObjectProperty { name, value, _ -> 68 | when { 69 | tClass.isInstance(value) -> childNameAndValues += (value to name) 70 | value is Collection<*> -> value.filter { tClass.isInstance(it) } 71 | .forEachIndexed { index, value -> childNameAndValues += value to "$name[$index]" } 72 | else -> { 73 | } 74 | } 75 | } 76 | val childMap = childNameAndValues.toMap() 77 | val fieldCounter = AtomicInteger() 78 | t.acceptChildren { element -> 79 | newChildren += TreeNode( 80 | childMap[element] ?: "", 81 | element 82 | ) 83 | } 84 | currentChildren = newChildren 85 | currentChildren.forEach { it.refresh(path + it) } 86 | } 87 | 88 | fun setupTreeUi(project: Project): TreeUiState { 89 | val tree = Tree(this) 90 | tree.cellRenderer = TreeObjectRenderer() 91 | val jbSplitter = JBSplitter(true).apply { 92 | firstComponent = JBScrollPane(tree).apply { 93 | horizontalScrollBar = null 94 | } 95 | } 96 | val tablePane = JPanel().apply { 97 | layout = BoxLayout(this, BoxLayout.Y_AXIS) 98 | } 99 | jbSplitter.secondComponent = JBScrollPane(tablePane) 100 | val state = TreeUiState( 101 | jbSplitter, 102 | tree, 103 | this, 104 | ObjectViewerUiState(tablePane) 105 | ) 106 | 107 | tree.addTreeSelectionListener { e -> 108 | if (e.newLeadSelectionPath != null) state.selectedTreePath = 109 | e.newLeadSelectionPath.getNamePath() 110 | val node = tree.lastSelectedPathComponent as? TreeNode<*> ?: return@addTreeSelectionListener 111 | tablePane.removeAll() 112 | state.objectViewerState.objectViewers.clear() 113 | val objectViewer = ObjectViewer.createObjectViewer( 114 | project, 115 | node.t, 116 | state.objectViewerState, 117 | 0, 118 | ktFile, 119 | node.t as? KtElement 120 | ) 121 | state.objectViewerState.objectViewers.add(objectViewer) 122 | tablePane.add(objectViewer.view) 123 | state.objectViewerState.selectedTablePath.firstOrNull()?.let { objectViewer.select(it) } 124 | highlightInEditor(node.t, project) 125 | tablePane.revalidate() 126 | tablePane.repaint() 127 | } 128 | tree.addMouseListener(object : MouseListener { 129 | override fun mouseClicked(e: MouseEvent) {} 130 | 131 | override fun mousePressed(e: MouseEvent?) {} 132 | 133 | override fun mouseReleased(e: MouseEvent?) { 134 | state.expandedTreePaths.clear() 135 | state.expandedTreePaths += TreeUtil.collectExpandedPaths(tree).map { path -> path.getNamePath() } 136 | } 137 | 138 | override fun mouseEntered(e: MouseEvent?) {} 139 | 140 | override fun mouseExited(e: MouseEvent?) {} 141 | 142 | }) 143 | return state 144 | } 145 | 146 | private fun TreePath.getNamePath(): List { 147 | val result = mutableListOf() 148 | for (i in 0 until pathCount) { 149 | result += (getPathComponent(i) as TreeNode<*>).name 150 | } 151 | return result 152 | } 153 | } 154 | 155 | data class TreeNode(val name: String = "", val t: T) { 156 | var currentChildren = mutableListOf>() 157 | 158 | } 159 | -------------------------------------------------------------------------------- /src/main/kotlin/io/github/tgeng/firviewer/ObjectViewer.kt: -------------------------------------------------------------------------------- 1 | // Copyright 2021 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package io.github.tgeng.firviewer 16 | 17 | import com.intellij.openapi.project.Project 18 | import org.jetbrains.kotlin.psi.KtElement 19 | import org.jetbrains.kotlin.psi.KtFile 20 | import javax.swing.JComponent 21 | import javax.swing.JPanel 22 | 23 | class ObjectViewerUiState( 24 | val tablePane: JPanel, 25 | ) { 26 | val objectViewers: MutableList = mutableListOf() 27 | val selectedTablePath: MutableList = mutableListOf() 28 | } 29 | 30 | abstract class ObjectViewer( 31 | val project: Project, 32 | protected val state: ObjectViewerUiState, 33 | protected val index: Int, 34 | private val ktFile: KtFile, 35 | private val elementToAnalyze: KtElement? 36 | ) { 37 | fun select(name: String): Boolean { 38 | val oldPath = if (index + 1 < state.selectedTablePath.size) { 39 | state.selectedTablePath.subList(index + 1, state.selectedTablePath.size).toList() 40 | } else { 41 | emptyList() 42 | } 43 | val nextObject = selectAndGetObject(name) ?: return false 44 | val nextViewer = 45 | createObjectViewer( 46 | project, 47 | nextObject, 48 | state, 49 | index + 1, 50 | ktFile, 51 | nextObject as? KtElement? ?: elementToAnalyze 52 | ) 53 | 54 | // Remove all tables below this one 55 | while (state.tablePane.components.size > index + 1) { 56 | state.tablePane.remove(state.tablePane.components.size - 1) 57 | state.objectViewers.removeLast() 58 | } 59 | while (state.selectedTablePath.size > index) { 60 | state.selectedTablePath.removeLast() 61 | } 62 | state.tablePane.add(nextViewer.view) 63 | state.objectViewers.add(nextViewer) 64 | state.selectedTablePath.add(name) 65 | state.tablePane.revalidate() 66 | state.tablePane.repaint() 67 | for (name in oldPath) { 68 | val objectViewer = state.objectViewers.last() 69 | if (!objectViewer.select(name)) break 70 | } 71 | return true 72 | } 73 | 74 | abstract val view: JComponent 75 | 76 | protected abstract fun selectAndGetObject(name: String): Any? 77 | 78 | companion object { 79 | fun createObjectViewer( 80 | project: Project, 81 | obj: Any, 82 | state: ObjectViewerUiState, 83 | index: Int, 84 | ktFile: KtFile, 85 | elementToAnalyze: KtElement? 86 | ): ObjectViewer = 87 | when (obj) { 88 | else -> TableObjectViewer(project, state, index, obj, ktFile, elementToAnalyze) 89 | } 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /src/main/kotlin/io/github/tgeng/firviewer/SvgView.kt: -------------------------------------------------------------------------------- 1 | package io.github.tgeng.firviewer 2 | 3 | import com.google.common.collect.ImmutableMap 4 | import com.intellij.openapi.application.ApplicationManager 5 | import com.intellij.openapi.util.SystemInfo 6 | import com.intellij.ui.JBColor 7 | import com.kitfox.svg.SVGDiagram 8 | import java.awt.Graphics 9 | import java.awt.Graphics2D 10 | import java.awt.RenderingHints 11 | import java.awt.RenderingHints.KEY_INTERPOLATION 12 | import java.awt.event.* 13 | import java.awt.geom.AffineTransform 14 | import java.awt.geom.Point2D 15 | import javax.swing.JPanel 16 | import kotlin.math.pow 17 | 18 | 19 | class SvgView : JPanel() { 20 | 21 | private var _svg: SVGDiagram? = null 22 | private val transform: AffineTransform = AffineTransform() 23 | 24 | init { 25 | var startX = 0 26 | var startY = 0 27 | val startTransform = AffineTransform() 28 | addMouseListener(object : MouseAdapter() { 29 | override fun mousePressed(e: MouseEvent) { 30 | startX = e.x 31 | startY = e.y 32 | startTransform.setTransform(transform) 33 | } 34 | }) 35 | addMouseMotionListener(object : MouseMotionAdapter() { 36 | override fun mouseDragged(e: MouseEvent) { 37 | transform.setTransform(startTransform) 38 | transform.translate( 39 | (e.x.toDouble() - startX) / transform.scaleX, 40 | (e.y.toDouble() - startY) / transform.scaleY 41 | ) 42 | repaint() 43 | } 44 | }) 45 | addMouseWheelListener { e -> 46 | val scale = 2.0.pow(-e.preciseWheelRotation * 0.2) 47 | val mouseP = Point2D.Double(e.x.toDouble(), e.y.toDouble()) 48 | transform.scaleAt(mouseP, scale) 49 | repaint() 50 | } 51 | val size = size 52 | var prevWidth = size.width 53 | var prevHeight = size.height 54 | addComponentListener(object : ComponentAdapter() { 55 | override fun componentResized(componentEvent: ComponentEvent) { 56 | val newSize = this@SvgView.size 57 | transform.translate( 58 | (newSize.width - prevWidth) / transform.scaleX / 2, 59 | (newSize.height - prevHeight) / transform.scaleY / 2 60 | ) 61 | prevWidth = newSize.width 62 | prevHeight = newSize.height 63 | } 64 | }) 65 | } 66 | 67 | fun setSvg(svg: SVGDiagram) { 68 | ApplicationManager.getApplication().invokeLater { 69 | val oldSvg = _svg 70 | if (oldSvg == null) { 71 | resetTransform(svg) 72 | } else { 73 | transform.translate((oldSvg.width - svg.width) / 2.0, (oldSvg.height - svg.height) / 2.0) 74 | } 75 | _svg = svg 76 | repaint() 77 | } 78 | } 79 | 80 | fun reset() { 81 | val svg = _svg ?: return 82 | ApplicationManager.getApplication().invokeLater { 83 | resetTransform(svg) 84 | repaint() 85 | } 86 | } 87 | 88 | private fun resetTransform(svg: SVGDiagram) { 89 | val scale = if (SystemInfo.isMac) { 90 | // The SVG library seems to have a bug on Mac where the width and height is doubled. 91 | 0.25 92 | } else { 93 | 0.5 94 | } 95 | transform.setTransform( 96 | scale, 97 | 0.0, 98 | 0.0, 99 | scale, 100 | (size.width - svg.width * scale) / 2, 101 | (size.height - svg.height * scale) / 2 102 | ) 103 | } 104 | 105 | override fun paintComponent(g: Graphics) { 106 | val svg = this._svg 107 | g.color = JBColor.PanelBackground 108 | g.fillRect(0, 0, size.width, size.height) 109 | if (g !is Graphics2D || svg == null) { 110 | return 111 | } 112 | g.transform(this.transform) 113 | g.setRenderingHints(renderHints) 114 | svg.render(g) 115 | } 116 | } 117 | 118 | private val renderHints: Map = ImmutableMap.builder() 119 | .put(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON) 120 | .put(RenderingHints.KEY_COLOR_RENDERING, RenderingHints.VALUE_COLOR_RENDER_QUALITY) 121 | .put(RenderingHints.KEY_DITHERING, RenderingHints.VALUE_DITHER_ENABLE) 122 | .put(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY) 123 | .put(KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BILINEAR) 124 | .put(RenderingHints.KEY_STROKE_CONTROL, RenderingHints.VALUE_STROKE_PURE) 125 | .put( 126 | RenderingHints.KEY_ALPHA_INTERPOLATION, 127 | RenderingHints.VALUE_ALPHA_INTERPOLATION_QUALITY 128 | ) 129 | .build() 130 | 131 | 132 | fun AffineTransform.scaleAt(pInScreenSpace: Point2D.Double, scale: Double) { 133 | val pInDrawSpace = this.createInverse().transform(pInScreenSpace) 134 | scale(scale, scale) 135 | val transformedPInScreenSpace = this.transform(pInDrawSpace) 136 | translate( 137 | (pInScreenSpace.x - transformedPInScreenSpace.x) / scaleX, 138 | (pInScreenSpace.y - transformedPInScreenSpace.y) / scaleY 139 | ) 140 | } 141 | 142 | private fun AffineTransform.transform(p: Point2D.Double): Point2D.Double { 143 | val result = Point2D.Double() 144 | transform(p, result) 145 | return result 146 | } 147 | 148 | -------------------------------------------------------------------------------- /src/main/kotlin/io/github/tgeng/firviewer/TableObjectRenderer.kt: -------------------------------------------------------------------------------- 1 | // Copyright 2021 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package io.github.tgeng.firviewer 16 | 17 | import com.intellij.navigation.ItemPresentation 18 | import com.intellij.openapi.module.impl.scopes.ModuleWithDependenciesScope 19 | import com.intellij.openapi.project.Project 20 | import com.intellij.psi.PsiElement 21 | import com.intellij.psi.PsiFile 22 | import com.intellij.psi.SingleRootFileViewProvider 23 | import com.intellij.psi.stubs.ObjectStubTree 24 | import com.intellij.psi.stubs.StubElement 25 | import org.jetbrains.kotlin.fir.FirElement 26 | import org.jetbrains.kotlin.fir.render 27 | import org.jetbrains.kotlin.fir.resolve.dfa.cfg.CFGNode 28 | import org.jetbrains.kotlin.fir.resolve.dfa.cfg.render 29 | import org.jetbrains.kotlin.util.ArrayMap 30 | import org.jetbrains.kotlin.util.AttributeArrayOwner 31 | import org.jetbrains.kotlin.analysis.api.ValidityTokenOwner 32 | import org.jetbrains.kotlin.analysis.api.symbols.KtSymbol 33 | import org.jetbrains.kotlin.analysis.api.symbols.markers.KtNamedSymbol 34 | import org.jetbrains.kotlin.analysis.api.tokens.HackToForceAllowRunningAnalyzeOnEDT 35 | import org.jetbrains.kotlin.analysis.api.tokens.hackyAllowRunningOnEdt 36 | import org.jetbrains.kotlin.analysis.api.types.KtType 37 | import org.jetbrains.kotlin.psi.KtDeclaration 38 | import java.awt.Component 39 | import javax.swing.Icon 40 | import javax.swing.JComponent 41 | import javax.swing.JTable 42 | import javax.swing.table.TableCellRenderer 43 | import kotlin.reflect.full.memberProperties 44 | import kotlin.reflect.jvm.isAccessible 45 | 46 | @OptIn(HackToForceAllowRunningAnalyzeOnEDT::class) 47 | object TableObjectRenderer : TableCellRenderer { 48 | override fun getTableCellRendererComponent( 49 | table: JTable, 50 | value: Any?, 51 | isSelected: Boolean, 52 | hasFocus: Boolean, 53 | row: Int, 54 | column: Int 55 | ): Component = hackyAllowRunningOnEdt { 56 | if (value is ValidityTokenOwner && !value.token.isValid() || value is PsiElement && !value.isValid) { 57 | return@hackyAllowRunningOnEdt label(value.getTypeAndId() + " is no longer valid", italic = true) 58 | } 59 | render(value).apply { 60 | if (table.isRowSelected(row)) { 61 | isOpaque = true 62 | background = table.selectionBackground 63 | } else { 64 | isOpaque = false 65 | background = table.background 66 | } 67 | } 68 | } 69 | 70 | private fun render(value: Any?, renderBracket: Boolean = false): JComponent { 71 | fun myLabel( 72 | s: String, 73 | bold: Boolean = false, 74 | italic: Boolean = false, 75 | multiline: Boolean = false, 76 | icon: Icon? = null, 77 | tooltipText: String? = null 78 | ) = label(if (renderBracket) "{ $s }" else s, bold, italic, multiline, icon, tooltipText) 79 | return when (value) { 80 | is JComponent -> value 81 | is Collection<*> -> { 82 | if (value.size == 1) { 83 | render(value.single(), renderBracket = true) 84 | } else { 85 | myLabel("size = " + value.size) 86 | } 87 | } 88 | is Map<*, *> -> { 89 | if (value.size == 1) { 90 | render(value.keys.single()) + label("->") + render(value.values.single()) 91 | } else { 92 | myLabel("size =" + value.size) 93 | } 94 | } 95 | is Array<*> -> myLabel("size = " + value.size) 96 | is BooleanArray -> myLabel("size = " + value.size) 97 | is ByteArray -> myLabel("size = " + value.size) 98 | is CharArray -> myLabel(String(value)) 99 | is ShortArray -> myLabel("size = " + value.size) 100 | is IntArray -> myLabel("size = " + value.size) 101 | is LongArray -> myLabel("size = " + value.size) 102 | is DoubleArray -> myLabel("size = " + value.size) 103 | is FloatArray -> myLabel("size = " + value.size) 104 | is CFGNode<*> -> myLabel(value.render()) 105 | is ItemPresentation -> myLabel(value.presentableText ?: "") 106 | is SingleRootFileViewProvider -> myLabel(value.virtualFile.toString()) 107 | is ObjectStubTree<*>, is StubElement<*>, is ModuleWithDependenciesScope -> myLabel( 108 | value.toString().replace(' ', '\n'), multiline = true 109 | ) 110 | is Project -> myLabel("Project: " + value.name) 111 | is PsiFile -> myLabel(value.name) 112 | is KtDeclaration -> myLabel(value.text.takeWhile { it != '\n' }) 113 | is PsiElement -> myLabel(value.text, multiline = true) 114 | is KtType -> myLabel(value.asStringForDebugging()) 115 | is KtNamedSymbol -> myLabel(value.name.asString()) 116 | is KtSymbol -> myLabel(value.psi?.text ?: "", multiline = true) 117 | is FirElement -> myLabel(value.render(), multiline = true) 118 | is AttributeArrayOwner<*, *> -> { 119 | val arrayMap = 120 | value::class.memberProperties.first { it.name == "arrayMap" }.apply { isAccessible = true } 121 | .call(value) as ArrayMap<*> 122 | myLabel("${arrayMap.size} attributes") 123 | } 124 | else -> myLabel(value.toString()) 125 | } 126 | } 127 | } -------------------------------------------------------------------------------- /src/main/kotlin/io/github/tgeng/firviewer/TableObjectViewer.kt: -------------------------------------------------------------------------------- 1 | // Copyright 2021 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package io.github.tgeng.firviewer 16 | 17 | import com.intellij.icons.AllIcons 18 | import com.intellij.openapi.project.Project 19 | import com.intellij.psi.PsiElement 20 | import com.intellij.ui.table.JBTable 21 | import org.jetbrains.kotlin.fir.declarations.FirControlFlowGraphOwner 22 | import org.jetbrains.kotlin.fir.resolve.dfa.cfg.CFGNode 23 | import org.jetbrains.kotlin.fir.resolve.dfa.dataFlowInfo 24 | import org.jetbrains.kotlin.util.AbstractArrayMapOwner 25 | import org.jetbrains.kotlin.util.ArrayMap 26 | import org.jetbrains.kotlin.util.TypeRegistry 27 | import org.jetbrains.kotlin.idea.caches.project.getModuleInfos 28 | import org.jetbrains.kotlin.analysis.low.level.api.fir.api.LLFirResolveSession 29 | import org.jetbrains.kotlin.analysis.low.level.api.fir.api.getOrBuildFir 30 | import org.jetbrains.kotlin.analysis.low.level.api.fir.api.getFirResolveSession 31 | import org.jetbrains.kotlin.analysis.api.* 32 | import org.jetbrains.kotlin.analysis.api.components.KtDiagnosticCheckerFilter 33 | import org.jetbrains.kotlin.analysis.api.symbols.KtSymbol 34 | import org.jetbrains.kotlin.analysis.api.tokens.AlwaysAccessibleValidityTokenFactory 35 | import org.jetbrains.kotlin.analysis.api.tokens.HackToForceAllowRunningAnalyzeOnEDT 36 | import org.jetbrains.kotlin.analysis.api.tokens.hackyAllowRunningOnEdt 37 | import org.jetbrains.kotlin.analysis.api.types.KtType 38 | import org.jetbrains.kotlin.analysis.project.structure.KtModule 39 | import org.jetbrains.kotlin.idea.references.KtFirReference 40 | import org.jetbrains.kotlin.idea.util.application.runReadAction 41 | import org.jetbrains.kotlin.psi.KtElement 42 | import org.jetbrains.kotlin.psi.KtFile 43 | import java.awt.* 44 | import java.awt.event.MouseAdapter 45 | import java.awt.event.MouseEvent 46 | import java.util.concurrent.ConcurrentHashMap 47 | import javax.swing.* 48 | import javax.swing.table.AbstractTableModel 49 | import javax.swing.table.TableCellRenderer 50 | import javax.swing.table.TableModel 51 | import kotlin.reflect.KCallable 52 | import kotlin.reflect.KClass 53 | import kotlin.reflect.KProperty1 54 | import kotlin.reflect.KVisibility 55 | import kotlin.reflect.full.memberProperties 56 | import kotlin.reflect.jvm.isAccessible 57 | 58 | 59 | class TableObjectViewer( 60 | project: Project, 61 | state: ObjectViewerUiState, 62 | index: Int, 63 | obj: Any, 64 | ktFile: KtFile, 65 | elementToAnalyze: KtElement? 66 | ) : 67 | ObjectViewer(project, state, index, ktFile, elementToAnalyze) { 68 | private val _model = ObjectTableModel(obj, elementToAnalyze) 69 | private val table = FittingTable(_model).apply { 70 | 71 | setDefaultRenderer(Any::class.java, TableObjectRenderer) 72 | rowSelectionAllowed = true 73 | setSelectionMode(ListSelectionModel.SINGLE_SELECTION) 74 | selectionModel.addListSelectionListener { e -> 75 | if (e.valueIsAdjusting) return@addListSelectionListener 76 | val row = selectedRow 77 | val name = _model.rows[row].name 78 | select(name.text) 79 | } 80 | val mouseListener = object : MouseAdapter() { 81 | override fun mouseMoved(e: MouseEvent) { 82 | val point = e.point 83 | updateToolTip(point) 84 | } 85 | 86 | override fun mousePressed(e: MouseEvent) { 87 | val row = rowAtPoint(e.point) 88 | if (row == -1) return 89 | if (row != selectedRow) { 90 | updateToolTip(e.point) 91 | return // Only do the triggering if the row is already selected. 92 | } 93 | val valueProvider = _model.rows[row].valueProvider ?: return 94 | val newValue = valueProvider.invoke() 95 | _model.rows[row].type = newValue?.getTypeAndId() 96 | _model.rows[row].value = newValue 97 | repaint() 98 | } 99 | 100 | private fun updateToolTip(point: Point) { 101 | val row = rowAtPoint(point) 102 | // Only do the triggering if the row is already selected. 103 | if (row != -1 && row == selectedRow && _model.rows[row].valueProvider != null) { 104 | cursor = Cursor(Cursor.HAND_CURSOR) 105 | toolTipText = "Click left button to trigger re-computation of this data." 106 | } else { 107 | cursor = Cursor.getDefaultCursor() 108 | toolTipText = "" 109 | } 110 | } 111 | 112 | } 113 | 114 | addMouseMotionListener(mouseListener) 115 | addMouseListener(mouseListener) 116 | } 117 | 118 | override val view: JComponent = when (obj) { 119 | else -> TableWrapper() 120 | } 121 | 122 | override fun selectAndGetObject(name: String): Any? { 123 | val index = _model.rows.indexOfFirst { it.name.text == name } 124 | if (index == -1) return null 125 | table.setRowSelectionInterval(index, index) 126 | return _model.rows[index].value?.also { 127 | highlightInEditor(it, project) 128 | } 129 | } 130 | 131 | private inner class TableWrapper : 132 | JPanel() { 133 | init { 134 | layout = BoxLayout(this, BoxLayout.Y_AXIS) 135 | add(table.tableHeader) 136 | add(table) 137 | } 138 | 139 | override fun getPreferredSize(): Dimension = 140 | Dimension(1, table.preferredSize.height + table.tableHeader.preferredSize.height) 141 | } 142 | } 143 | 144 | private class FittingTable(model: TableModel) : JBTable(model) { 145 | 146 | init { 147 | setMaxItemsForSizeCalculation(20) 148 | setAutoResizeMode(JTable.AUTO_RESIZE_LAST_COLUMN) 149 | getTableHeader().foreground = getTableHeader().background 150 | getTableHeader().background = Color.GRAY 151 | } 152 | 153 | private val measured: Array = Array(model.columnCount) { BooleanArray(model.rowCount) } 154 | 155 | override fun prepareRenderer(renderer: TableCellRenderer, row: Int, column: Int): Component { 156 | val component = super.prepareRenderer(renderer, row, column) 157 | if (measured[column][row]) return component 158 | measured[column][row] = true 159 | val rendererWidth = component.preferredSize.width 160 | val tableColumn = getColumnModel().getColumn(column) 161 | tableColumn.preferredWidth = 162 | Math.max(rendererWidth + intercellSpacing.width, tableColumn.preferredWidth) 163 | if (column == 2) { 164 | // set row height according to the last column 165 | val rendererHeight = component.preferredSize.height 166 | if (rendererHeight > 1) { 167 | setRowHeight(row, rendererHeight) 168 | } 169 | } 170 | return component 171 | } 172 | } 173 | 174 | private class ObjectTableModel( 175 | val obj: Any, 176 | private val elementToAnalyze: KtElement? 177 | ) : 178 | AbstractTableModel() { 179 | class RowData(val name: JLabel, var type: String?, var value: Any?, val valueProvider: (() -> Any?)? = null) 180 | 181 | @OptIn(HackToForceAllowRunningAnalyzeOnEDT::class) 182 | val rows: List = when (obj) { 183 | is Array<*> -> obj.mapIndexed { index, value -> 184 | RowData(label(index.toString()), value?.getTypeAndId(), value) 185 | } 186 | is BooleanArray -> obj.mapIndexed { index, value -> 187 | RowData(label(index.toString()), value.getTypeAndId(), value) 188 | } 189 | is ByteArray -> obj.mapIndexed { index, value -> 190 | RowData(label(index.toString()), value.getTypeAndId(), value) 191 | } 192 | is ShortArray -> obj.mapIndexed { index, value -> 193 | RowData(label(index.toString()), value.getTypeAndId(), value) 194 | } 195 | is IntArray -> obj.mapIndexed { index, value -> 196 | RowData(label(index.toString()), value.getTypeAndId(), value) 197 | } 198 | is LongArray -> obj.mapIndexed { index, value -> 199 | RowData(label(index.toString()), value.getTypeAndId(), value) 200 | } 201 | is FloatArray -> obj.mapIndexed { index, value -> 202 | RowData(label(index.toString()), value.getTypeAndId(), value) 203 | } 204 | is DoubleArray -> obj.mapIndexed { index, value -> 205 | RowData(label(index.toString()), value.getTypeAndId(), value) 206 | } 207 | is CharArray -> obj.mapIndexed { index, value -> 208 | RowData(label(index.toString()), value.getTypeAndId(), value) 209 | } 210 | is AbstractArrayMapOwner<*, *> -> getArrayMapOwnerBasedRows().sortedBy { it.name.text } 211 | is Iterable<*> -> obj.mapIndexed { index, value -> 212 | RowData(label(index.toString()), value?.getTypeAndId(), value) 213 | } 214 | is Sequence<*> -> hackyAllowRunningOnEdt { 215 | obj.mapIndexed { index, value -> 216 | RowData(label(index.toString()), value?.getTypeAndId(), value) 217 | }.toList() 218 | } 219 | is Map<*, *> -> obj.map { (k, v) -> 220 | RowData(label(k?.getForMapKey() ?: ""), v?.getTypeAndId(), v) 221 | }.sortedBy { it.name.text } 222 | is PsiElement, is KtType, is KtSymbol, is KtFirReference -> (getKtAnalysisSessionBasedRows() + getObjectPropertyMembersBasedRows() + getOtherExtensionProperties()).sortedBy { it.name.text } 223 | is CFGNode<*> -> (getObjectPropertyMembersBasedRows() + getCfgNodeProperties(obj)).sortedBy { it.name.text } 224 | else -> getObjectPropertyMembersBasedRows().sortedBy { it.name.text } 225 | } 226 | 227 | private fun getArrayMapOwnerBasedRows(): List { 228 | val arrayMap = 229 | obj::class.memberProperties.first { it.name == "arrayMap" }.apply { isAccessible = true } 230 | .call(obj) as ArrayMap<*> 231 | val typeRegistry = 232 | obj::class.memberProperties.first { it.name == "typeRegistry" } 233 | .apply { isAccessible = true } 234 | .call(obj) as TypeRegistry<*, *> 235 | val idPerType = 236 | TypeRegistry::class.members.first { it.name == "idPerType" }.apply { isAccessible = true } 237 | .call(typeRegistry) as ConcurrentHashMap, Int> 238 | return idPerType.mapNotNull { (key, id) -> 239 | val value = arrayMap[id] ?: return@mapNotNull null 240 | RowData(label(key.simpleName ?: key.toString()), value.getTypeAndId(), value) 241 | } 242 | } 243 | 244 | @OptIn(HackToForceAllowRunningAnalyzeOnEDT::class) 245 | private fun getObjectPropertyMembersBasedRows() = try { 246 | val result = mutableListOf() 247 | obj.traverseObjectProperty { name, value, valueProvider -> 248 | if (value == null || 249 | value is Collection<*> && value.isEmpty() || 250 | value is Map<*, *> && value.isEmpty() 251 | ) { 252 | return@traverseObjectProperty 253 | } 254 | result += RowData(label(name), value.getTypeAndId(), value, valueProvider) 255 | } 256 | result 257 | } catch (e: Throwable) { 258 | listOf(RowData(label(""), e.getTypeAndId(), e)) 259 | } 260 | 261 | @OptIn(HackToForceAllowRunningAnalyzeOnEDT::class) 262 | private fun getKtAnalysisSessionBasedRows(): List { 263 | if (elementToAnalyze == null) return emptyList() 264 | return hackyAllowRunningOnEdt { 265 | runReadAction { 266 | analyseWithCustomToken(elementToAnalyze, AlwaysAccessibleValidityTokenFactory) { 267 | KtAnalysisSession::class.members.filter { it.visibility == KVisibility.PUBLIC && it.parameters.size == 2 && it.name != "equals" } 268 | .mapNotNull { prop -> 269 | try { 270 | val value = prop.call(this, obj) 271 | RowData(label(prop.name, icon = AllIcons.Nodes.Favorite), value?.getTypeAndId(), value) { 272 | hackyAllowRunningOnEdt { 273 | prop.call(this, obj) 274 | } 275 | } 276 | } catch (e: Throwable) { 277 | null 278 | } 279 | } 280 | } 281 | } 282 | } 283 | } 284 | 285 | @OptIn(HackToForceAllowRunningAnalyzeOnEDT::class) 286 | private fun getCfgNodeProperties(node: CFGNode<*>): List { 287 | var cfg = node.owner 288 | while (true) { 289 | cfg = cfg.owner ?: break 290 | } 291 | val dataFlowInfos = (cfg.declaration as? FirControlFlowGraphOwner)?.controlFlowGraphReference?.dataFlowInfo 292 | ?: return emptyList() 293 | val flow = dataFlowInfos.flowOnNodes[node] ?: return emptyList() 294 | return listOf(RowData(label("flow", icon = AllIcons.Nodes.Favorite), flow.getTypeAndId(), flow)) 295 | } 296 | 297 | @OptIn(HackToForceAllowRunningAnalyzeOnEDT::class) 298 | private fun getOtherExtensionProperties(): List { 299 | val result = mutableListOf() 300 | when (obj) { 301 | is KtElement -> { 302 | fun getScopeContextForPosition() = try { 303 | hackyAllowRunningOnEdt { 304 | runReadAction { 305 | analyseWithCustomToken(obj.containingKtFile, AlwaysAccessibleValidityTokenFactory) { 306 | obj.containingKtFile.getScopeContextForPosition(obj) 307 | } 308 | } 309 | } 310 | } catch (e: Throwable) { 311 | e 312 | } 313 | 314 | var value: Any? = getScopeContextForPosition() 315 | result += RowData(label("scopeAtPosition"), value?.getTypeAndId(), value, ::getScopeContextForPosition) 316 | 317 | fun collectDiagnosticsForFile() = try { 318 | hackyAllowRunningOnEdt { 319 | runReadAction { 320 | analyseWithCustomToken(obj.containingKtFile, AlwaysAccessibleValidityTokenFactory) { 321 | obj.containingKtFile.collectDiagnosticsForFile(KtDiagnosticCheckerFilter.EXTENDED_AND_COMMON_CHECKERS) 322 | } 323 | } 324 | } 325 | } catch (e: Throwable) { 326 | e 327 | } 328 | value = collectDiagnosticsForFile() 329 | result += RowData(label("collectDiagnosticsForFile"), value?.getTypeAndId(), value, ::collectDiagnosticsForFile) 330 | 331 | fun getDiagnostics() = try { 332 | hackyAllowRunningOnEdt { 333 | runReadAction { 334 | analyseWithCustomToken(obj, AlwaysAccessibleValidityTokenFactory) { 335 | obj.getDiagnostics(KtDiagnosticCheckerFilter.EXTENDED_AND_COMMON_CHECKERS) 336 | } 337 | } 338 | } 339 | } catch (e: Throwable) { 340 | e 341 | } 342 | value = getDiagnostics() 343 | result += RowData(label("getDiagnostics"), value?.getTypeAndId(), value, ::getDiagnostics) 344 | 345 | fun getOrBuildFir() = try { 346 | hackyAllowRunningOnEdt { 347 | runReadAction { 348 | analyseWithCustomToken(obj, AlwaysAccessibleValidityTokenFactory) { 349 | val property = this::class.memberProperties.first { it.name == "firResolveState" } as KProperty1 350 | obj.getOrBuildFir(property.get(this)) 351 | } 352 | } 353 | } 354 | } catch (e: Throwable) { 355 | e 356 | } 357 | value = getOrBuildFir() 358 | result += RowData(label("getOrBuildFir"), value?.getTypeAndId(), value, ::getOrBuildFir) 359 | 360 | result += obj::getModuleInfos.toRowData() 361 | result += obj::getFirResolveSession.toRowData() 362 | } 363 | is KtModule -> { 364 | obj::getFirResolveSession.toRowData() 365 | } 366 | } 367 | return result.onEach { it.name.icon = AllIcons.Nodes.Favorite } 368 | } 369 | 370 | fun KCallable<*>.toRowData(): RowData { 371 | fun getValue() = try { 372 | this.call() 373 | } catch (e: Throwable) { 374 | e 375 | } 376 | 377 | val value = getValue() 378 | return RowData(label(name), value?.getTypeAndId(), value, ::getValue) 379 | } 380 | 381 | override fun getColumnName(column: Int): String = when (column) { 382 | 0 -> when (obj) { 383 | is Iterable<*> -> "index" 384 | is Map<*, *> -> "key" 385 | else -> "property" 386 | } 387 | 1 -> "type" 388 | 2 -> "value" 389 | else -> throw IllegalStateException() 390 | } 391 | 392 | override fun getRowCount(): Int = rows.size 393 | 394 | override fun getColumnCount(): Int = 3 395 | 396 | override fun getValueAt(rowIndex: Int, columnIndex: Int): Any? { 397 | return when (columnIndex) { 398 | 0 -> rows[rowIndex].name 399 | 1 -> rows[rowIndex].type 400 | 2 -> rows[rowIndex].value 401 | else -> throw IllegalStateException() 402 | } 403 | } 404 | } 405 | 406 | -------------------------------------------------------------------------------- /src/main/kotlin/io/github/tgeng/firviewer/TreeObjectRenderer.kt: -------------------------------------------------------------------------------- 1 | // Copyright 2021 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package io.github.tgeng.firviewer 16 | 17 | import com.intellij.icons.AllIcons 18 | import com.intellij.psi.PsiElement 19 | import com.intellij.psi.PsiFile 20 | import com.intellij.psi.util.elementType 21 | import org.jetbrains.kotlin.fir.FirLabel 22 | import org.jetbrains.kotlin.fir.FirPureAbstractElement 23 | import org.jetbrains.kotlin.fir.contracts.FirContractDescription 24 | import org.jetbrains.kotlin.fir.contracts.FirEffectDeclaration 25 | import org.jetbrains.kotlin.fir.declarations.FirAnonymousInitializer 26 | import org.jetbrains.kotlin.fir.declarations.FirConstructor 27 | import org.jetbrains.kotlin.fir.declarations.FirErrorFunction 28 | import org.jetbrains.kotlin.fir.declarations.FirFile 29 | import org.jetbrains.kotlin.fir.declarations.FirImport 30 | import org.jetbrains.kotlin.fir.declarations.FirProperty 31 | import org.jetbrains.kotlin.fir.declarations.FirPropertyAccessor 32 | import org.jetbrains.kotlin.fir.declarations.FirRegularClass 33 | import org.jetbrains.kotlin.fir.declarations.FirSimpleFunction 34 | import org.jetbrains.kotlin.fir.declarations.FirTypeAlias 35 | import org.jetbrains.kotlin.fir.declarations.FirTypeParameter 36 | import org.jetbrains.kotlin.fir.declarations.FirTypeParameterRef 37 | import org.jetbrains.kotlin.fir.declarations.FirVariable 38 | import org.jetbrains.kotlin.fir.declarations.impl.FirDeclarationStatusImpl 39 | import org.jetbrains.kotlin.fir.expressions.FirArgumentList 40 | import org.jetbrains.kotlin.fir.expressions.FirAssignmentOperatorStatement 41 | import org.jetbrains.kotlin.fir.expressions.FirAugmentedArraySetCall 42 | import org.jetbrains.kotlin.fir.expressions.FirCatch 43 | import org.jetbrains.kotlin.fir.expressions.FirDelegatedConstructorCall 44 | import org.jetbrains.kotlin.fir.expressions.FirExpression 45 | import org.jetbrains.kotlin.fir.expressions.FirLoop 46 | import org.jetbrains.kotlin.fir.expressions.FirVariableAssignment 47 | import org.jetbrains.kotlin.fir.expressions.FirWhenBranch 48 | import org.jetbrains.kotlin.fir.expressions.impl.FirStubStatement 49 | import org.jetbrains.kotlin.fir.references.FirReference 50 | import org.jetbrains.kotlin.fir.types.FirTypeProjection 51 | import org.jetbrains.kotlin.fir.types.FirTypeRef 52 | import org.jetbrains.kotlin.psi.* 53 | import java.awt.Component 54 | import javax.swing.JTree 55 | import javax.swing.tree.TreeCellRenderer 56 | 57 | class TreeObjectRenderer : TreeCellRenderer { 58 | override fun getTreeCellRendererComponent( 59 | tree: JTree, 60 | value: Any, 61 | selected: Boolean, 62 | expanded: Boolean, 63 | leaf: Boolean, 64 | row: Int, 65 | hasFocus: Boolean 66 | ): Component { 67 | val node = value as? TreeNode<*> ?: return label("nothing to show") 68 | return when (val e = node.t) { 69 | is FirAnonymousInitializer -> type(node) + render(e) 70 | is FirArgumentList -> type(node) + render(e) 71 | is FirAssignmentOperatorStatement -> type(node) + render(e) 72 | is FirAugmentedArraySetCall -> type(node) + render(e) 73 | is FirCatch -> type(node) + render(e) 74 | is FirConstructor -> type(node) + label("", icon = AllIcons.Nodes.Function) 75 | is FirContractDescription -> type(node) + render(e) 76 | is FirDeclarationStatusImpl -> type(node) + render(e) 77 | is FirDelegatedConstructorCall -> type(node) + render(e) 78 | is FirEffectDeclaration -> type(node) + render(e) 79 | is FirErrorFunction -> type(node) + render(e) 80 | is FirExpression -> type(node) + render(e) 81 | is FirFile -> type(node) + label(e.name) 82 | is FirImport -> type(node) + render(e) 83 | is FirLabel -> type(node) + render(e) 84 | is FirLoop -> type(node) + render(e) 85 | is FirPropertyAccessor -> type(node) + render(e) 86 | is FirReference -> type(node) + render(e) 87 | is FirRegularClass -> type(node) + label(e.name.asString(), icon = AllIcons.Nodes.Class) 88 | is FirSimpleFunction -> type(node) + label(e.name.asString(), icon = AllIcons.Nodes.Function) 89 | is FirStubStatement -> type(node) + render(e) 90 | is FirTypeAlias -> type(node) + render(e) 91 | is FirTypeParameter -> type(node) + render(e) 92 | is FirTypeProjection -> type(node) + render(e) 93 | is FirTypeRef -> type(node) + render(e) 94 | is FirProperty -> type(node) + label(e.name.asString(), icon = AllIcons.Nodes.Property) 95 | is FirVariable -> type(node) + label(e.name.asString(), icon = AllIcons.Nodes.Variable) 96 | is FirVariableAssignment -> type(node) + render(e) 97 | is FirWhenBranch -> type(node) + render(e) 98 | // is FirConstructedClassTypeParameterRef, 99 | // is FirOuterClassTypeParameterRef, 100 | is FirTypeParameterRef -> type(node) + render(e as FirPureAbstractElement) 101 | is PsiFile -> type(node) + label(e.name) 102 | is PsiElement -> type(node) + 103 | e.elementType?.let { label("[$it]", italic = true) } + 104 | when (e) { 105 | is KtDeclaration -> label( 106 | e.name ?: "", icon = when (e) { 107 | is KtClassOrObject -> AllIcons.Nodes.Class 108 | is KtFunction -> AllIcons.Nodes.Function 109 | is KtProperty -> AllIcons.Nodes.Property 110 | is KtVariableDeclaration -> AllIcons.Nodes.Variable 111 | else -> null 112 | } 113 | ) 114 | else -> label(e.text) 115 | } 116 | else -> label(e.toString()) 117 | } 118 | } 119 | } -------------------------------------------------------------------------------- /src/main/kotlin/io/github/tgeng/firviewer/TreeUiState.kt: -------------------------------------------------------------------------------- 1 | // Copyright 2021 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package io.github.tgeng.firviewer 16 | 17 | import com.intellij.ui.treeStructure.Tree 18 | import javax.swing.JComponent 19 | import javax.swing.tree.TreePath 20 | 21 | class TreeUiState( 22 | val pane: JComponent, 23 | val tree: Tree, 24 | val model: ObjectTreeModel<*>, 25 | val objectViewerState: ObjectViewerUiState 26 | ) { 27 | val expandedTreePaths = mutableSetOf>() 28 | var selectedTreePath: List? = null 29 | 30 | fun refreshTree() { 31 | val selectedTablePath = objectViewerState.selectedTablePath.toList() 32 | val tree = tree 33 | model.refresh() 34 | expandedTreePaths.forEach { tree.expandPath(it.adaptPath(model)) } 35 | tree.selectionPath = selectedTreePath?.adaptPath(model) 36 | for (name in selectedTablePath) { 37 | val objectViewer = objectViewerState.objectViewers.last() 38 | if (!objectViewer.select(name)) break 39 | } 40 | objectViewerState.tablePane.revalidate() 41 | objectViewerState.tablePane.repaint() 42 | } 43 | 44 | private fun List.adaptPath(model: ObjectTreeModel<*>): TreePath { 45 | var current = model.root 46 | val adaptedPathComponents = mutableListOf(current) 47 | // Skip first path, which is root. 48 | for (name in subList(1, size)) { 49 | current = current.currentChildren.firstOrNull { it.name == name } ?: break 50 | adaptedPathComponents += current 51 | } 52 | val treePath = TreePath(adaptedPathComponents.toTypedArray()) 53 | return treePath 54 | } 55 | } -------------------------------------------------------------------------------- /src/main/kotlin/io/github/tgeng/firviewer/uiUtils.kt: -------------------------------------------------------------------------------- 1 | // Copyright 2021 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package io.github.tgeng.firviewer 16 | 17 | import com.google.common.base.CaseFormat 18 | import com.google.common.primitives.Primitives 19 | import com.intellij.openapi.editor.Editor 20 | import com.intellij.openapi.editor.markup.EffectType 21 | import com.intellij.openapi.editor.markup.HighlighterLayer 22 | import com.intellij.openapi.editor.markup.HighlighterTargetArea 23 | import com.intellij.openapi.editor.markup.TextAttributes 24 | import com.intellij.openapi.fileEditor.FileEditorManager 25 | import com.intellij.openapi.project.Project 26 | import com.intellij.openapi.vfs.VirtualFile 27 | import com.intellij.psi.PsiElement 28 | import com.intellij.ui.components.JBLabel 29 | import com.intellij.ui.scale.JBUIScale 30 | import org.jetbrains.kotlin.KtPsiSourceElement 31 | import org.jetbrains.kotlin.fir.FirPureAbstractElement 32 | import org.jetbrains.kotlin.fir.FirElement 33 | import org.jetbrains.kotlin.fir.declarations.FirDeclaration 34 | import org.jetbrains.kotlin.fir.render 35 | import org.jetbrains.kotlin.fir.resolve.dfa.cfg.CFGNode 36 | import org.jetbrains.kotlin.util.AttributeArrayOwner 37 | import org.jetbrains.kotlin.analysis.api.tokens.HackToForceAllowRunningAnalyzeOnEDT 38 | import org.jetbrains.kotlin.analysis.api.tokens.hackyAllowRunningOnEdt 39 | import org.jetbrains.kotlin.name.Name 40 | import org.jetbrains.kotlin.psi.psiUtil.endOffset 41 | import org.jetbrains.kotlin.psi.psiUtil.startOffset 42 | import java.awt.Color 43 | import java.awt.FlowLayout 44 | import java.awt.Font 45 | import java.lang.reflect.Method 46 | import java.lang.reflect.Modifier 47 | import javax.swing.Icon 48 | import javax.swing.JComponent 49 | import javax.swing.JPanel 50 | import kotlin.reflect.KCallable 51 | import kotlin.reflect.KFunction 52 | import kotlin.reflect.KProperty 53 | import kotlin.reflect.KVisibility 54 | import kotlin.reflect.full.createType 55 | import kotlin.reflect.jvm.isAccessible 56 | import kotlin.reflect.jvm.javaGetter 57 | import kotlin.reflect.jvm.javaMethod 58 | 59 | fun label( 60 | s: String, 61 | bold: Boolean = false, 62 | italic: Boolean = false, 63 | multiline: Boolean = false, 64 | icon: Icon? = null, 65 | tooltipText: String? = null 66 | ) = JBLabel( 67 | if (multiline) ("" + s.replace("\n", "
").replace(" ", " ") + "") else s 68 | ).apply { 69 | this.icon = icon 70 | this.toolTipText = toolTipText 71 | font = font.deriveFont((if (bold) Font.BOLD else Font.PLAIN) + if (italic) Font.ITALIC else Font.PLAIN) 72 | } 73 | 74 | fun render(e: FirElement) = JBLabel(e.render()) 75 | fun type(e: TreeNode<*>): JComponent { 76 | val nameAndType = label( 77 | if (e.name == "" || e.name.startsWith('<')) { 78 | "" 79 | } else { 80 | e.name + ": " 81 | } + e.t::class.simpleName, 82 | bold = true 83 | ) 84 | val address = label("@" + Integer.toHexString(System.identityHashCode(e.t))) 85 | val nameTypeAndAddress = nameAndType + address 86 | return if (e.t is FirDeclaration) { 87 | nameTypeAndAddress + label(e.t.resolvePhase.toString(), italic = true) 88 | } else { 89 | nameTypeAndAddress 90 | } 91 | } 92 | 93 | private val twoPoint = JBUIScale.scale(2) 94 | 95 | operator fun JComponent.plus(that: JComponent?): JPanel { 96 | return if (this is JPanel) { 97 | add(that) 98 | this 99 | } else { 100 | JPanel(FlowLayout(FlowLayout.LEFT).apply { 101 | vgap = twoPoint 102 | }).apply { 103 | add(this@plus) 104 | if (that != null) add(that) 105 | isOpaque = false 106 | } 107 | } 108 | } 109 | 110 | fun highlightInEditor(obj: Any, project: Project) { 111 | val editorManager = FileEditorManager.getInstance(project) ?: return 112 | val editor: Editor = editorManager.selectedTextEditor ?: return 113 | editor.markupModel.removeAllHighlighters() 114 | val (vf, startOffset, endOffset) = when (obj) { 115 | is FirPureAbstractElement -> obj.source?.let { 116 | val source = it as? KtPsiSourceElement ?: return@let null 117 | FileLocation(source.psi.containingFile.virtualFile, it.startOffset, it.endOffset) 118 | } 119 | is PsiElement -> obj.textRange?.let { 120 | FileLocation( 121 | obj.containingFile.virtualFile, 122 | it.startOffset, 123 | it.endOffset 124 | ) 125 | } 126 | is CFGNode<*> -> obj.fir.source?.let { 127 | val source = it as? KtPsiSourceElement ?: return@let null 128 | FileLocation(source.psi.containingFile.virtualFile, it.startOffset, it.endOffset) 129 | } 130 | else -> null 131 | } ?: return 132 | if (vf != FileEditorManager.getInstance(project).selectedFiles.firstOrNull()) return 133 | 134 | val textAttributes = 135 | TextAttributes(null, null, Color.GRAY, EffectType.BOXED, Font.PLAIN) 136 | editor.markupModel.addRangeHighlighter( 137 | startOffset, 138 | endOffset, 139 | HighlighterLayer.CARET_ROW, 140 | textAttributes, 141 | HighlighterTargetArea.EXACT_RANGE 142 | ) 143 | } 144 | 145 | private data class FileLocation(val vf: VirtualFile, val startIndex: Int, val endIndex: Int) 146 | 147 | val unitType = Unit::class.createType() 148 | val skipMethodNames = setOf( 149 | "copy", 150 | "toString", 151 | "delete", 152 | "clone", 153 | "getUserDataString", 154 | "hashCode", 155 | "getClass", 156 | "component1", 157 | "component2", 158 | "component3", 159 | "component4", 160 | "component5" 161 | ) 162 | val psiElementMethods = PsiElement::class.java.methods.map { it.name }.toSet() - setOf( 163 | "getTextRange", 164 | "getTextRangeInParent", 165 | "getTextLength", 166 | "getText", 167 | "getResolveScope", 168 | "getUseScope", 169 | "getReferences", 170 | ) 171 | 172 | @OptIn(HackToForceAllowRunningAnalyzeOnEDT::class) 173 | fun Any.traverseObjectProperty( 174 | propFilter: (KCallable<*>) -> Boolean = { true }, methodFilter: (Method) -> Boolean = { true }, 175 | fn: (name: String, value: Any?, () -> Any?) -> Unit 176 | ) { 177 | try { 178 | this::class.members 179 | .filter { propFilter(it) && it.parameters.size == 1 && it.visibility == KVisibility.PUBLIC && it.returnType != unitType && it.name !in skipMethodNames && (this !is PsiElement || it.name !in psiElementMethods) } 180 | .sortedWith { m1, m2 -> 181 | fun KCallable<*>.declaringClass() = when (this) { 182 | is KFunction<*> -> javaMethod?.declaringClass 183 | is KProperty<*> -> javaGetter?.declaringClass 184 | else -> null 185 | } 186 | 187 | val m1Class = m1.declaringClass() 188 | val m2Class = m2.declaringClass() 189 | when { 190 | m1Class == m2Class -> 0 191 | m1Class == null -> 1 192 | m2Class == null -> -1 193 | m1Class.isAssignableFrom(m2Class) -> -1 194 | else -> 1 195 | } 196 | } 197 | .forEach { prop -> 198 | val value = try { 199 | prop.isAccessible = true 200 | hackyAllowRunningOnEdt { 201 | prop.call(this) 202 | } 203 | } catch (e: Throwable) { 204 | return@forEach 205 | } 206 | fn(prop.name, value) { 207 | hackyAllowRunningOnEdt { 208 | prop.call(this) 209 | } 210 | } 211 | } 212 | } catch (e: Throwable) { 213 | // fallback to traverse with Java reflection 214 | this::class.java.methods 215 | .filter { methodFilter(it) && it.name !in skipMethodNames && it.parameterCount == 0 && it.modifiers and Modifier.PUBLIC != 0 && it.returnType.simpleName != "void" && (this !is PsiElement || it.name !in psiElementMethods) } 216 | // methods in super class is at the beginning 217 | .sortedWith { m1, m2 -> 218 | when { 219 | m1.declaringClass == m2.declaringClass -> 0 220 | m1.declaringClass.isAssignableFrom(m2.declaringClass) -> -1 221 | else -> 1 222 | } 223 | } 224 | .distinctBy { it.name } 225 | .forEach { method -> 226 | val value = try { 227 | hackyAllowRunningOnEdt { 228 | method.invoke(this) 229 | } 230 | } catch (e: Throwable) { 231 | return@forEach 232 | } 233 | fn(CaseFormat.UPPER_CAMEL.to(CaseFormat.LOWER_CAMEL, method.name.removePrefix("get")), value) { 234 | hackyAllowRunningOnEdt { 235 | method.invoke(this) 236 | } 237 | } 238 | } 239 | } 240 | } 241 | 242 | fun Any.getTypeAndId(): String { 243 | return when { 244 | isData() -> this::class.simpleName 245 | ?: this::class.toString() 246 | else -> this::class.simpleName + " @" + Integer.toHexString(System.identityHashCode(this)) 247 | } 248 | } 249 | 250 | fun Any.getForMapKey(): String { 251 | return when { 252 | isData() -> toString() 253 | else -> this::class.simpleName + " @" + Integer.toHexString(System.identityHashCode(this)) + " | " + toString() 254 | } 255 | } 256 | 257 | fun Any.isData(): Boolean = try { 258 | this is Iterable<*> || this is Map<*, *> || this is AttributeArrayOwner<*, *> || 259 | this is Enum<*> || this::class.java.isPrimitive || Primitives.isWrapperType(this::class.java) || 260 | this::class.java == String::class.java || this::class.java == Name::class.java || 261 | this::class.isData || this::class.objectInstance != null 262 | } catch (e: Throwable) { 263 | false 264 | } 265 | 266 | 267 | //private class CfgGraphViewer(state: TreeUiState, index: Int, graph: ControlFlowGraph) : 268 | // ObjectViewer(state, index) { 269 | // 270 | // private val nodeNameMap = mutableMapOf, String>() 271 | // private val nodeClassCounter = mutableMapOf() 272 | // val CFGNode<*>.name:String get() = nodeNameMap.computeIfAbsent(this) { node -> 273 | // val nodeClassName = (node::class.simpleName?:node::class.toString()).removeSuffix("Node") 274 | // nodeClassName + nodeClassCounter.computeIfAbsent(nodeClassName) { AtomicInteger() }.getAndIncrement() 275 | // } 276 | // 277 | // private val graph = SingleGraph("foo").apply { 278 | // graph.nodes.forEach { node -> 279 | // addNode(node.name) 280 | // } 281 | // val edgeCounter = AtomicInteger() 282 | // val edgeNameMap = mutableMapOf() 283 | // graph.nodes.forEach { node -> 284 | // node.followingNodes.forEach { to -> 285 | // val edgeId = edgeCounter.getAndIncrement().toString() 286 | // addEdge(edgeId, node.name, to.name) 287 | // } 288 | // } 289 | // } 290 | // 291 | // data class EdgeData(val from:CFGNode<*>, val to: CFGNode<*>, val edge: Edge?) 292 | // 293 | // val viewer = SwingViewer(this.graph, Viewer.ThreadingModel.GRAPH_IN_ANOTHER_THREAD).apply { 294 | // enableAutoLayout() 295 | // } 296 | // override val view: JComponent = viewer.addDefaultView(false) as JComponent 297 | // 298 | // override fun selectAndGetObject(name: String): Any? { 299 | // return null 300 | // } 301 | //} 302 | -------------------------------------------------------------------------------- /src/main/resources/META-INF/plugin.xml: -------------------------------------------------------------------------------- 1 | 16 | 17 | 18 | org.example.FirViewer 19 | FirViewer 20 | Tianyu Geng 21 | 22 | Viewer of FIR structure of Kotlin code. To use this plugin, you must use the 23 | FIR-based Kotlin plugin, which is only available from the Kotlin dev repo for now. 24 | 25 | 26 | com.intellij.modules.platform 27 | org.jetbrains.kotlin 28 | 29 | 30 | 31 | 32 | 33 | 34 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | --------------------------------------------------------------------------------