├── .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 |
5 |
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 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 |
83 |
84 |
85 |
86 |
87 |
88 |
89 |
90 |
91 |
92 |
93 |
94 |
95 |
98 |
99 |
100 |
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 | 
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 | 
32 | 
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 |
--------------------------------------------------------------------------------