├── .gitignore
├── README.md
├── build.gradle
├── gradle.properties.example
├── gradle
└── wrapper
│ ├── gradle-wrapper.jar
│ └── gradle-wrapper.properties
├── gradlew
├── gradlew.bat
├── settings.gradle
└── src
└── main
├── kotlin
└── callgraph
│ ├── CallGraphToolWindow.form
│ ├── CallGraphToolWindow.kt
│ ├── CallGraphToolWindowFactory.kt
│ ├── CallGraphToolWindowProjectService.kt
│ ├── Canvas.kt
│ ├── CanvasBuilder.kt
│ ├── CanvasConfig.kt
│ ├── Colors.kt
│ ├── ComboBoxOptions.kt
│ ├── Dependency.kt
│ ├── Edge.kt
│ ├── Graph.kt
│ ├── MouseEventHandler.kt
│ ├── Node.kt
│ ├── Utils.kt
│ ├── ViewDownstreamAction.kt
│ ├── ViewUpstreamAction.kt
│ └── ViewUpstreamDownstreamAction.kt
└── resources
├── META-INF
├── plugin.xml
├── pluginIcon.svg
└── pluginIcon_dark.svg
└── icons
├── build.png
├── color.png
├── compress-x.png
├── compress-y.png
├── downstream.png
├── expand-x.png
├── expand-y.png
├── filter.png
├── fit-ratio.png
├── fit-viewport.png
├── icon.svg
├── navigation.png
├── node-selection.png
├── play.png
├── search.png
├── statistics.png
├── upstream-downstream.png
├── upstream.png
├── view-source-code.png
└── view.png
/.gitignore:
--------------------------------------------------------------------------------
1 | .gradle
2 | .idea
3 | /build/
4 | .DS_Store
5 |
6 | # Ignore Gradle GUI config
7 | gradle-app.setting
8 |
9 | # Avoid ignoring Gradle wrapper jar file (.jar files are usually ignored)
10 | !gradle-wrapper.jar
11 |
12 | # Cache of project
13 | .gradletasknamecache
14 |
15 | # # Work around https://youtrack.jetbrains.com/issue/IDEA-116898
16 | # gradle/wrapper/gradle-wrapper.properties
17 |
18 | # JetBrains account credentials
19 | gradle.properties
20 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Call Graph Intellij Plugin
2 |
3 | This is the open-sourced repo for the [IntelliJ Call Graph plugin](https://plugins.jetbrains.com/plugin/12304-call-graph). Please feel free to leave comments, feedback, and bug reports (on the [Issues tab](https://github.com/Chentai-Kao/call-graph-plugin/issues)).
4 |
5 | Pull requests are welcome!
6 |
7 | ## How to build the plugin (using IntelliJ)
8 | 1. Install IntelliJ from the [official website](https://www.jetbrains.com/idea/download/) or whatever makes sense for your operating system.
9 | 2. Copy the file `gradle.properties.example` and rename it to `gradle.properties`. This file holds the credential (publish token) for you to publish your local build to Idea plugin repository, and is ignored in version control. This file is required in the Gradle build process, but feel free to leave the sample token value as is. Just remember to replace it with the actual token if you decide to upload the build to the Idea plugin repository.
10 | 3. Use IntelliJ to **Open** the root folder of this repo. A Gradle daemon should start building the project.
11 | 4. In the Gradle menu, select `call-graph-plugin / Tasks / intelliJ / buildPlugin` to build the plugin.
12 |
--------------------------------------------------------------------------------
/build.gradle:
--------------------------------------------------------------------------------
1 | plugins {
2 | id 'java'
3 | id 'org.jetbrains.intellij' version '1.17.2'
4 | id "org.jetbrains.kotlin.jvm" version "1.9.22"
5 | }
6 |
7 | group 'com.jetbrains'
8 | //version '0.0.0' // plugin version, update me! (specified in plugins.xml instead)
9 |
10 | repositories {
11 | mavenCentral()
12 | }
13 |
14 | dependencies {
15 | implementation 'guru.nidi:graphviz-java:0.18.1'
16 | implementation 'org.apache.logging.log4j:log4j-core:2.23.0'
17 | implementation 'org.apache.logging.log4j:log4j-slf4j-impl:2.23.0'
18 | testImplementation 'junit:junit:4.13.2'
19 | }
20 |
21 | apply plugin: 'idea'
22 | apply plugin: 'org.jetbrains.intellij'
23 | apply plugin: 'kotlin'
24 |
25 | intellij {
26 | version.set('2023.3') // Intellij version to build against
27 | pluginName.set('call-graph')
28 | intellij.updateSinceUntilBuild.set(false) // Disables updating since-build attribute in plugin.xml
29 | plugins.set(['java']) // declaring a dependency on the Java functionality
30 | }
31 |
32 | publishPlugin {
33 | token.set(System.getenv("ORG_GRADLE_PROJECT_intellijPublishToken"))
34 | }
35 |
--------------------------------------------------------------------------------
/gradle.properties.example:
--------------------------------------------------------------------------------
1 | intellijPublishToken=your-publish-token-here-looks-like-perm:A1B2C3
2 |
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Chentai-Kao/call-graph-plugin/a8f8b2955502deb0e1eff485e0f03b75eb4a93c8/gradle/wrapper/gradle-wrapper.jar
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------
1 | # suppress inspection "UnusedProperty" for whole file
2 | #Thu Jan 03 23:30:10 CST 2019
3 | distributionBase=GRADLE_USER_HOME
4 | distributionPath=wrapper/dists
5 | zipStoreBase=GRADLE_USER_HOME
6 | zipStorePath=wrapper/dists
7 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.6-all.zip
8 |
--------------------------------------------------------------------------------
/gradlew:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env sh
2 |
3 | ##############################################################################
4 | ##
5 | ## Gradle start up script for UN*X
6 | ##
7 | ##############################################################################
8 |
9 | # Attempt to set APP_HOME
10 | # Resolve links: $0 may be a link
11 | PRG="$0"
12 | # Need this for relative symlinks.
13 | while [ -h "$PRG" ] ; do
14 | ls=`ls -ld "$PRG"`
15 | link=`expr "$ls" : '.*-> \(.*\)$'`
16 | if expr "$link" : '/.*' > /dev/null; then
17 | PRG="$link"
18 | else
19 | PRG=`dirname "$PRG"`"/$link"
20 | fi
21 | done
22 | SAVED="`pwd`"
23 | cd "`dirname \"$PRG\"`/" >/dev/null
24 | APP_HOME="`pwd -P`"
25 | cd "$SAVED" >/dev/null
26 |
27 | APP_NAME="Gradle"
28 | APP_BASE_NAME=`basename "$0"`
29 |
30 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
31 | DEFAULT_JVM_OPTS=""
32 |
33 | # Use the maximum available, or set MAX_FD != -1 to use that value.
34 | MAX_FD="maximum"
35 |
36 | warn () {
37 | echo "$*"
38 | }
39 |
40 | die () {
41 | echo
42 | echo "$*"
43 | echo
44 | exit 1
45 | }
46 |
47 | # OS specific support (must be 'true' or 'false').
48 | cygwin=false
49 | msys=false
50 | darwin=false
51 | nonstop=false
52 | case "`uname`" in
53 | CYGWIN* )
54 | cygwin=true
55 | ;;
56 | Darwin* )
57 | darwin=true
58 | ;;
59 | MINGW* )
60 | msys=true
61 | ;;
62 | NONSTOP* )
63 | nonstop=true
64 | ;;
65 | esac
66 |
67 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
68 |
69 | # Determine the Java command to use to start the JVM.
70 | if [ -n "$JAVA_HOME" ] ; then
71 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
72 | # IBM's JDK on AIX uses strange locations for the executables
73 | JAVACMD="$JAVA_HOME/jre/sh/java"
74 | else
75 | JAVACMD="$JAVA_HOME/bin/java"
76 | fi
77 | if [ ! -x "$JAVACMD" ] ; then
78 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
79 |
80 | Please set the JAVA_HOME variable in your environment to match the
81 | location of your Java installation."
82 | fi
83 | else
84 | JAVACMD="java"
85 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
86 |
87 | Please set the JAVA_HOME variable in your environment to match the
88 | location of your Java installation."
89 | fi
90 |
91 | # Increase the maximum file descriptors if we can.
92 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
93 | MAX_FD_LIMIT=`ulimit -H -n`
94 | if [ $? -eq 0 ] ; then
95 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
96 | MAX_FD="$MAX_FD_LIMIT"
97 | fi
98 | ulimit -n $MAX_FD
99 | if [ $? -ne 0 ] ; then
100 | warn "Could not set maximum file descriptor limit: $MAX_FD"
101 | fi
102 | else
103 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
104 | fi
105 | fi
106 |
107 | # For Darwin, add options to specify how the application appears in the dock
108 | if $darwin; then
109 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
110 | fi
111 |
112 | # For Cygwin, switch paths to Windows format before running java
113 | if $cygwin ; then
114 | APP_HOME=`cygpath --path --mixed "$APP_HOME"`
115 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
116 | JAVACMD=`cygpath --unix "$JAVACMD"`
117 |
118 | # We build the pattern for arguments to be converted via cygpath
119 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
120 | SEP=""
121 | for dir in $ROOTDIRSRAW ; do
122 | ROOTDIRS="$ROOTDIRS$SEP$dir"
123 | SEP="|"
124 | done
125 | OURCYGPATTERN="(^($ROOTDIRS))"
126 | # Add a user-defined pattern to the cygpath arguments
127 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then
128 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
129 | fi
130 | # Now convert the arguments - kludge to limit ourselves to /bin/sh
131 | i=0
132 | for arg in "$@" ; do
133 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
134 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
135 |
136 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
137 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
138 | else
139 | eval `echo args$i`="\"$arg\""
140 | fi
141 | i=$((i+1))
142 | done
143 | case $i in
144 | (0) set -- ;;
145 | (1) set -- "$args0" ;;
146 | (2) set -- "$args0" "$args1" ;;
147 | (3) set -- "$args0" "$args1" "$args2" ;;
148 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;;
149 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
150 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
151 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
152 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
153 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
154 | esac
155 | fi
156 |
157 | # Escape application args
158 | save () {
159 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
160 | echo " "
161 | }
162 | APP_ARGS=$(save "$@")
163 |
164 | # Collect all arguments for the java command, following the shell quoting and substitution rules
165 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
166 |
167 | # by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong
168 | if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then
169 | cd "$(dirname "$0")"
170 | fi
171 |
172 | exec "$JAVACMD" "$@"
173 |
--------------------------------------------------------------------------------
/gradlew.bat:
--------------------------------------------------------------------------------
1 | @if "%DEBUG%" == "" @echo off
2 | @rem ##########################################################################
3 | @rem
4 | @rem Gradle startup script for Windows
5 | @rem
6 | @rem ##########################################################################
7 |
8 | @rem Set local scope for the variables with windows NT shell
9 | if "%OS%"=="Windows_NT" setlocal
10 |
11 | set DIRNAME=%~dp0
12 | if "%DIRNAME%" == "" set DIRNAME=.
13 | set APP_BASE_NAME=%~n0
14 | set APP_HOME=%DIRNAME%
15 |
16 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
17 | set DEFAULT_JVM_OPTS=
18 |
19 | @rem Find java.exe
20 | if defined JAVA_HOME goto findJavaFromJavaHome
21 |
22 | set JAVA_EXE=java.exe
23 | %JAVA_EXE% -version >NUL 2>&1
24 | if "%ERRORLEVEL%" == "0" goto init
25 |
26 | echo.
27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
28 | echo.
29 | echo Please set the JAVA_HOME variable in your environment to match the
30 | echo location of your Java installation.
31 |
32 | goto fail
33 |
34 | :findJavaFromJavaHome
35 | set JAVA_HOME=%JAVA_HOME:"=%
36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe
37 |
38 | if exist "%JAVA_EXE%" goto init
39 |
40 | echo.
41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
42 | echo.
43 | echo Please set the JAVA_HOME variable in your environment to match the
44 | echo location of your Java installation.
45 |
46 | goto fail
47 |
48 | :init
49 | @rem Get command-line arguments, handling Windows variants
50 |
51 | if not "%OS%" == "Windows_NT" goto win9xME_args
52 |
53 | :win9xME_args
54 | @rem Slurp the command line arguments.
55 | set CMD_LINE_ARGS=
56 | set _SKIP=2
57 |
58 | :win9xME_args_slurp
59 | if "x%~1" == "x" goto execute
60 |
61 | set CMD_LINE_ARGS=%*
62 |
63 | :execute
64 | @rem Setup the command line
65 |
66 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
67 |
68 | @rem Execute Gradle
69 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
70 |
71 | :end
72 | @rem End local scope for the variables with windows NT shell
73 | if "%ERRORLEVEL%"=="0" goto mainEnd
74 |
75 | :fail
76 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
77 | rem the _cmd.exe /c_ return code!
78 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
79 | exit /b 1
80 |
81 | :mainEnd
82 | if "%OS%"=="Windows_NT" endlocal
83 |
84 | :omega
85 |
--------------------------------------------------------------------------------
/settings.gradle:
--------------------------------------------------------------------------------
1 | rootProject.name = 'call-graph-plugin'
2 |
3 |
--------------------------------------------------------------------------------
/src/main/kotlin/callgraph/CallGraphToolWindow.form:
--------------------------------------------------------------------------------
1 |
2 |
610 |
--------------------------------------------------------------------------------
/src/main/kotlin/callgraph/CallGraphToolWindow.kt:
--------------------------------------------------------------------------------
1 | package callgraph
2 |
3 | import com.intellij.ide.util.EditorHelper
4 | import com.intellij.psi.PsiMethod
5 | import java.awt.Dimension
6 | import java.awt.event.KeyEvent
7 | import java.awt.event.KeyListener
8 | import java.awt.geom.Point2D
9 | import javax.swing.*
10 |
11 | class CallGraphToolWindow {
12 | private lateinit var runButton: JButton
13 | private lateinit var callGraphToolWindowContent: JPanel
14 | private lateinit var canvasPanel: JPanel
15 | private lateinit var projectScopeButton: JRadioButton
16 | private lateinit var moduleScopeButton: JRadioButton
17 | private lateinit var directoryScopeButton: JRadioButton
18 | private lateinit var directoryScopeTextField: JTextField
19 | private lateinit var moduleScopeComboBox: JComboBox
20 | private lateinit var mainTabbedPanel: JTabbedPane
21 | private lateinit var includeTestFilesCheckBox: JCheckBox
22 | private lateinit var buildTypeLabel: JLabel
23 | private lateinit var loadingProgressBar: JProgressBar
24 | private lateinit var showOnlyUpstreamButton: JButton
25 | private lateinit var showOnlyDownstreamButton: JButton
26 | private lateinit var showOnlyUpstreamDownstreamButton: JButton
27 | private lateinit var upstreamDownstreamScopeCheckbox: JCheckBox
28 | private lateinit var fitGraphToViewButton: JButton
29 | private lateinit var fitGraphToBestRatioButton: JButton
30 | private lateinit var increaseXGridButton: JButton
31 | private lateinit var decreaseXGridButton: JButton
32 | private lateinit var increaseYGridButton: JButton
33 | private lateinit var decreaseYGridButton: JButton
34 | private lateinit var statsLabel: JLabel
35 | private lateinit var viewSourceCodeButton: JButton
36 | private lateinit var viewPackageNameComboBox: JComboBox
37 | private lateinit var viewFilePathComboBox: JComboBox
38 | private lateinit var nodeSelectionComboBox: JComboBox
39 | private lateinit var searchTextField: JTextField
40 | private lateinit var nodeColorComboBox: JComboBox
41 | private lateinit var filterExternalCheckbox: JCheckBox
42 | private lateinit var filterAccessPublicCheckbox: JCheckBox
43 | private lateinit var filterAccessProtectedCheckbox: JCheckBox
44 | private lateinit var filterAccessPackageLocalCheckbox: JCheckBox
45 | private lateinit var filterAccessPrivateCheckbox: JCheckBox
46 |
47 | private val canvasBuilder = CanvasBuilder()
48 | private val canvas: Canvas = Canvas(this)
49 | private val focusedMethods = mutableSetOf()
50 | private val filterCheckboxes = listOf(
51 | this.filterExternalCheckbox,
52 | this.filterAccessPublicCheckbox,
53 | this.filterAccessProtectedCheckbox,
54 | this.filterAccessPackageLocalCheckbox,
55 | this.filterAccessPrivateCheckbox
56 | )
57 |
58 | init {
59 | // drop-down options
60 | val viewComboBoxOptions = listOf(
61 | ComboBoxOptions.VIEW_ALWAYS,
62 | ComboBoxOptions.VIEW_HOVERED,
63 | ComboBoxOptions.VIEW_NEVER
64 | )
65 | viewComboBoxOptions.forEach { this.viewPackageNameComboBox.addItem(it.text) }
66 | this.viewPackageNameComboBox.selectedItem = ComboBoxOptions.VIEW_HOVERED.text
67 | viewComboBoxOptions.forEach { this.viewFilePathComboBox.addItem(it.text) }
68 | this.viewFilePathComboBox.selectedItem = ComboBoxOptions.VIEW_NEVER.text
69 | val nodeSelectionComboBoxOptions = listOf(
70 | ComboBoxOptions.NODE_SELECTION_SINGLE,
71 | ComboBoxOptions.NODE_SELECTION_MULTIPLE
72 | )
73 | nodeSelectionComboBoxOptions.forEach { this.nodeSelectionComboBox.addItem(it.text) }
74 | this.nodeSelectionComboBox.selectedItem = ComboBoxOptions.NODE_SELECTION_SINGLE.text
75 | val nodeColorComboBoxOptions = listOf(
76 | ComboBoxOptions.NODE_COLOR_NONE,
77 | ComboBoxOptions.NODE_COLOR_ACCESS,
78 | ComboBoxOptions.NODE_COLOR_CLASS
79 | )
80 | nodeColorComboBoxOptions.forEach { option -> this.nodeColorComboBox.addItem(option.text) }
81 | this.nodeColorComboBox.selectedItem = ComboBoxOptions.NODE_COLOR_NONE.text
82 |
83 | // search field
84 | this.searchTextField.addKeyListener(object: KeyListener {
85 | override fun keyTyped(keyEvent: KeyEvent) {
86 | }
87 |
88 | override fun keyPressed(keyEvent: KeyEvent) {
89 | }
90 |
91 | override fun keyReleased(keyEvent: KeyEvent) {
92 | this@CallGraphToolWindow.canvas.repaint()
93 | }
94 | })
95 | this.filterCheckboxes.forEach { it.addActionListener { this.canvas.filterChangeHandler() } }
96 |
97 | // click handlers for buttons
98 | this.projectScopeButton.addActionListener { projectScopeButtonHandler() }
99 | this.moduleScopeButton.addActionListener { moduleScopeButtonHandler() }
100 | this.directoryScopeButton.addActionListener { directoryScopeButtonHandler() }
101 | this.runButton.addActionListener {
102 | this.focusedMethods.clear()
103 | run(getSelectedBuildType())
104 | }
105 | this.viewPackageNameComboBox.addActionListener { this.canvas.repaint() }
106 | this.viewFilePathComboBox.addActionListener { this.canvas.repaint() }
107 | this.nodeColorComboBox.addActionListener { this.canvas.repaint() }
108 | this.showOnlyUpstreamButton.addActionListener { run(CanvasConfig.BuildType.UPSTREAM) }
109 | this.showOnlyDownstreamButton.addActionListener { run(CanvasConfig.BuildType.DOWNSTREAM) }
110 | this.showOnlyUpstreamDownstreamButton.addActionListener { run(CanvasConfig.BuildType.UPSTREAM_DOWNSTREAM) }
111 | this.viewSourceCodeButton.addActionListener { viewSourceCodeHandler() }
112 | this.fitGraphToViewButton.addActionListener { this.canvas.fitCanvasToView() }
113 | this.fitGraphToBestRatioButton.addActionListener { this.canvas.fitCanvasToBestRatio() }
114 | this.increaseXGridButton.addActionListener { gridSizeButtonHandler(isXGrid = true, isIncrease = true) }
115 | this.decreaseXGridButton.addActionListener { gridSizeButtonHandler(isXGrid = true, isIncrease = false) }
116 | this.increaseYGridButton.addActionListener { gridSizeButtonHandler(isXGrid = false, isIncrease = true) }
117 | this.decreaseYGridButton.addActionListener { gridSizeButtonHandler(isXGrid = false, isIncrease = false) }
118 |
119 | // attach event listeners to canvas
120 | val mouseEventHandler = MouseEventHandler(this.canvas)
121 | this.canvas.addMouseListener(mouseEventHandler)
122 | this.canvas.addMouseMotionListener(mouseEventHandler)
123 | this.canvas.addMouseWheelListener(mouseEventHandler)
124 | this.canvas.isVisible = false
125 | this.canvasPanel.add(this.canvas)
126 | }
127 |
128 | fun getContent() = this.callGraphToolWindowContent
129 |
130 | fun isFocusedMethod(method: PsiMethod) = this.focusedMethods.contains(method)
131 |
132 | fun toggleFocusedMethod(method: PsiMethod): CallGraphToolWindow {
133 | if (this.focusedMethods.contains(method)) {
134 | // clicked on a selected node
135 | this.focusedMethods.remove(method)
136 | } else {
137 | // clicked on an un-selected node
138 | if (getSelectedComboBoxOption(this.nodeSelectionComboBox) == ComboBoxOptions.NODE_SELECTION_SINGLE) {
139 | this.focusedMethods.clear()
140 | }
141 | this.focusedMethods.add(method)
142 | }
143 | enableFocusedMethodButtons()
144 | return this
145 | }
146 |
147 | fun clearFocusedMethods(): CallGraphToolWindow {
148 | this.focusedMethods.clear()
149 | enableFocusedMethodButtons()
150 | return this
151 | }
152 |
153 | fun resetProgressBar(maximum: Int) {
154 | this.loadingProgressBar.isIndeterminate = false
155 | this.loadingProgressBar.maximum = maximum
156 | this.loadingProgressBar.value = 0
157 | }
158 |
159 | fun incrementProgressBar() {
160 | val newValue = this.loadingProgressBar.value + 1
161 | this.loadingProgressBar.value = newValue
162 | val text =
163 | if (this.loadingProgressBar.isIndeterminate) "$newValue functions processed"
164 | else "$newValue functions processed (total ${this.loadingProgressBar.maximum})"
165 | this.loadingProgressBar.string = text
166 | }
167 |
168 | fun isRenderFunctionPackageName(isNodeHovered: Boolean): Boolean {
169 | val option = getSelectedComboBoxOption(this.viewPackageNameComboBox)
170 | return option == ComboBoxOptions.VIEW_ALWAYS || (option == ComboBoxOptions.VIEW_HOVERED && isNodeHovered)
171 | }
172 |
173 | fun isRenderFunctionFilePath(isNodeHovered: Boolean): Boolean {
174 | val option = getSelectedComboBoxOption(this.viewFilePathComboBox)
175 | return option == ComboBoxOptions.VIEW_ALWAYS || (option == ComboBoxOptions.VIEW_HOVERED && isNodeHovered)
176 | }
177 |
178 | fun isQueried(text: String): Boolean {
179 | val searchQuery = this.searchTextField.text.toLowerCase()
180 | return searchQuery.isNotEmpty() && text.toLowerCase().contains(searchQuery)
181 | }
182 |
183 | fun isNodeColorByAccess() = getSelectedComboBoxOption(this.nodeColorComboBox) == ComboBoxOptions.NODE_COLOR_ACCESS
184 |
185 | fun isNodeColorByClassName() = getSelectedComboBoxOption(this.nodeColorComboBox) == ComboBoxOptions.NODE_COLOR_CLASS
186 |
187 | fun isFilterExternalChecked() = this.filterExternalCheckbox.isSelected
188 |
189 | fun isFilterAccessPublicChecked() = this.filterAccessPublicCheckbox.isSelected
190 |
191 | fun isFilterAccessProtectedChecked() = this.filterAccessProtectedCheckbox.isSelected
192 |
193 | fun isFilterAccessPackageLocalChecked() = this.filterAccessPackageLocalCheckbox.isSelected
194 |
195 | fun isFilterAccessPrivateChecked() = this.filterAccessPrivateCheckbox.isSelected
196 |
197 | fun isLegendNeeded() = getSelectedComboBoxOption(this.nodeColorComboBox) != ComboBoxOptions.NODE_COLOR_NONE
198 |
199 | fun getCanvasSize(): Dimension = this.canvasPanel.size
200 |
201 | fun run(buildType: CanvasConfig.BuildType) {
202 | val project = Utils.getActiveProject()
203 | if (project != null) {
204 | Utils.runBackgroundTask(project, Runnable {
205 | // set up the config object
206 | val canvasConfig = CanvasConfig(
207 | project,
208 | buildType,
209 | this.canvas,
210 | this@CallGraphToolWindow.moduleScopeComboBox.selectedItem as String? ?: "",
211 | this@CallGraphToolWindow.directoryScopeTextField.text,
212 | this@CallGraphToolWindow.focusedMethods,
213 | this@CallGraphToolWindow
214 | )
215 | // start building graph
216 | setupUiBeforeRun(buildType)
217 | this@CallGraphToolWindow.canvasBuilder.build(canvasConfig)
218 | setupUiAfterRun()
219 | })
220 | }
221 | }
222 |
223 | private fun getSelectedComboBoxOption(comboBox: JComboBox): ComboBoxOptions {
224 | val selectedText = comboBox.selectedItem as String?
225 | return if (selectedText == null) ComboBoxOptions.DUMMY else ComboBoxOptions.fromText(selectedText)
226 | }
227 |
228 | private fun disableAllSecondaryOptions() {
229 | this.includeTestFilesCheckBox.isEnabled = false
230 | this.moduleScopeComboBox.isEnabled = false
231 | this.directoryScopeTextField.isEnabled = false
232 | }
233 |
234 | private fun projectScopeButtonHandler() {
235 | disableAllSecondaryOptions()
236 | this.includeTestFilesCheckBox.isEnabled = true
237 | }
238 |
239 | private fun moduleScopeButtonHandler() {
240 | val project = Utils.getActiveProject()
241 | if (project != null) {
242 | // set up modules drop-down
243 | this.moduleScopeComboBox.removeAllItems()
244 | Utils.getActiveModules(project)
245 | .forEach { this.moduleScopeComboBox.addItem(it.name) }
246 | disableAllSecondaryOptions()
247 | this.moduleScopeComboBox.isEnabled = true
248 | }
249 | }
250 |
251 | private fun directoryScopeButtonHandler() {
252 | val project = Utils.getActiveProject()
253 | if (project != null) {
254 | // set up directory option text field
255 | disableAllSecondaryOptions()
256 | this.directoryScopeTextField.text = project.basePath
257 | this.directoryScopeTextField.isEnabled = true
258 | }
259 | }
260 |
261 | private fun gridSizeButtonHandler(isXGrid: Boolean, isIncrease: Boolean) {
262 | val zoomFactor = if (isIncrease) 1.25f else 1 / 1.25f
263 | val xZoomFactor = if (isXGrid) zoomFactor else 1f
264 | val yZoomFactor = if (isXGrid) 1f else zoomFactor
265 | val zoomCenter = Point2D.Float(
266 | 0.5f * this.canvasPanel.width.toFloat(),
267 | 0.5f * this.canvasPanel.height.toFloat()
268 | )
269 | this.canvas.zoomAtPoint(zoomCenter, xZoomFactor, yZoomFactor)
270 | }
271 |
272 | private fun viewSourceCodeHandler() {
273 | this.focusedMethods.forEach { EditorHelper.openInEditor(it) }
274 | }
275 |
276 | private fun setupUiBeforeRun(buildType: CanvasConfig.BuildType) {
277 | // focus on the 'graph tab
278 | this.mainTabbedPanel.getComponentAt(1).isEnabled = true
279 | this.mainTabbedPanel.selectedIndex = 1
280 | // stats label
281 | this.statsLabel.text = "..."
282 | // build-type label
283 | when (buildType) {
284 | CanvasConfig.BuildType.WHOLE_PROJECT_WITH_TEST_LIMITED,
285 | CanvasConfig.BuildType.WHOLE_PROJECT_WITH_TEST,
286 | CanvasConfig.BuildType.WHOLE_PROJECT_WITHOUT_TEST_LIMITED,
287 | CanvasConfig.BuildType.WHOLE_PROJECT_WITHOUT_TEST -> this.buildTypeLabel.text = buildType.label
288 | CanvasConfig.BuildType.MODULE_LIMITED,
289 | CanvasConfig.BuildType.MODULE -> {
290 | val moduleName = this.moduleScopeComboBox.selectedItem as String
291 | this.buildTypeLabel.text = "Module $moduleName"
292 | }
293 | CanvasConfig.BuildType.DIRECTORY_LIMITED,
294 | CanvasConfig.BuildType.DIRECTORY -> {
295 | val path = this.directoryScopeTextField.text
296 | this.buildTypeLabel.text = "Directory $path"
297 | }
298 | CanvasConfig.BuildType.UPSTREAM,
299 | CanvasConfig.BuildType.DOWNSTREAM,
300 | CanvasConfig.BuildType.UPSTREAM_DOWNSTREAM -> {
301 | val functionNames = this.focusedMethods.joinToString { it.name }
302 | this.buildTypeLabel.text = "${buildType.label} of function $functionNames"
303 | }
304 | }
305 | // disable some checkboxes and buttons
306 | listOf(
307 | this.viewPackageNameComboBox,
308 | this.viewFilePathComboBox,
309 | this.nodeSelectionComboBox,
310 | this.nodeColorComboBox,
311 | this.fitGraphToBestRatioButton,
312 | this.fitGraphToViewButton,
313 | this.increaseXGridButton,
314 | this.decreaseXGridButton,
315 | this.increaseYGridButton,
316 | this.decreaseYGridButton,
317 | this.viewSourceCodeButton,
318 | this.showOnlyUpstreamButton,
319 | this.showOnlyDownstreamButton,
320 | this.showOnlyUpstreamDownstreamButton,
321 | this.searchTextField
322 | ).forEach { (it as JComponent).isEnabled = false }
323 | // filter-related checkboxes
324 | this.filterCheckboxes.forEach {
325 | it.isEnabled = false
326 | it.isSelected = true
327 | }
328 | // progress bar
329 | this.loadingProgressBar.isVisible = true
330 | // clear the canvas panel, ready for new graph
331 | this.canvas.isVisible = false
332 | }
333 |
334 | private fun setupUiAfterRun() {
335 | // hide progress bar
336 | this.loadingProgressBar.isVisible = false
337 | // show the rendered canvas
338 | this.canvas.isVisible = true
339 | this.canvasPanel.updateUI()
340 | // stats label
341 | this.statsLabel.text = "${this.canvas.getNodesCount()} methods"
342 | // enable some checkboxes and buttons
343 | enableFocusedMethodButtons()
344 | listOf(
345 | this.viewPackageNameComboBox,
346 | this.viewFilePathComboBox,
347 | this.nodeSelectionComboBox,
348 | this.nodeColorComboBox,
349 | this.fitGraphToBestRatioButton,
350 | this.fitGraphToViewButton,
351 | this.increaseXGridButton,
352 | this.decreaseXGridButton,
353 | this.increaseYGridButton,
354 | this.decreaseYGridButton,
355 | this.searchTextField
356 | ).forEach { (it as JComponent).isEnabled = true }
357 | // filter-related checkboxes
358 | this.filterCheckboxes.forEach { it.isEnabled = true }
359 | }
360 |
361 | private fun getSelectedBuildType(): CanvasConfig.BuildType {
362 | val isLimitedScope = this.upstreamDownstreamScopeCheckbox.isSelected
363 | return when {
364 | this.projectScopeButton.isSelected -> {
365 | if (this.includeTestFilesCheckBox.isSelected) {
366 | if (isLimitedScope) CanvasConfig.BuildType.WHOLE_PROJECT_WITH_TEST_LIMITED
367 | else CanvasConfig.BuildType.WHOLE_PROJECT_WITH_TEST
368 | }
369 | if (isLimitedScope) CanvasConfig.BuildType.WHOLE_PROJECT_WITHOUT_TEST_LIMITED
370 | else CanvasConfig.BuildType.WHOLE_PROJECT_WITHOUT_TEST
371 | }
372 | this.moduleScopeButton.isSelected ->
373 | if (isLimitedScope) CanvasConfig.BuildType.MODULE_LIMITED
374 | else CanvasConfig.BuildType.MODULE
375 | this.directoryScopeButton.isSelected ->
376 | if (isLimitedScope) CanvasConfig.BuildType.DIRECTORY_LIMITED
377 | else CanvasConfig.BuildType.DIRECTORY
378 | else -> CanvasConfig.BuildType.WHOLE_PROJECT_WITH_TEST
379 | }
380 | }
381 |
382 | private fun enableFocusedMethodButtons() {
383 | listOf(
384 | this.showOnlyUpstreamButton,
385 | this.showOnlyDownstreamButton,
386 | this.showOnlyUpstreamDownstreamButton,
387 | this.viewSourceCodeButton
388 | ).forEach { it.isEnabled = this.focusedMethods.isNotEmpty() }
389 | }
390 | }
391 |
--------------------------------------------------------------------------------
/src/main/kotlin/callgraph/CallGraphToolWindowFactory.kt:
--------------------------------------------------------------------------------
1 | package callgraph
2 |
3 | import com.intellij.openapi.components.service
4 | import com.intellij.openapi.project.Project
5 | import com.intellij.openapi.wm.ToolWindow
6 | import com.intellij.ui.content.ContentFactory
7 |
8 | class CallGraphToolWindowFactory: com.intellij.openapi.wm.ToolWindowFactory {
9 | // Create the tool window content.
10 | override fun createToolWindowContent(project: Project, toolWindow: ToolWindow) {
11 | val callGraphToolWindow = CallGraphToolWindow()
12 |
13 | // register the call graph tool window as a project service, so it can be accessed by editor menu actions.
14 | val callGraphToolWindowProjectService = project.service()
15 | callGraphToolWindowProjectService.callGraphToolWindow = callGraphToolWindow
16 |
17 | // register the tool window content
18 | val content = ContentFactory.getInstance().createContent(callGraphToolWindow.getContent(), "", false)
19 | toolWindow.contentManager.addContent(content)
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/src/main/kotlin/callgraph/CallGraphToolWindowProjectService.kt:
--------------------------------------------------------------------------------
1 | package callgraph
2 |
3 | import com.intellij.openapi.components.Service
4 |
5 | // Project service holds a reference to the tool window, which is accessible by an action (editor menu)
6 | @Service(Service.Level.PROJECT)
7 | class CallGraphToolWindowProjectService {
8 | lateinit var callGraphToolWindow: CallGraphToolWindow
9 | }
10 |
--------------------------------------------------------------------------------
/src/main/kotlin/callgraph/Canvas.kt:
--------------------------------------------------------------------------------
1 | package callgraph
2 |
3 | import com.intellij.psi.PsiModifier
4 | import java.awt.*
5 | import java.awt.geom.Arc2D
6 | import java.awt.geom.Ellipse2D
7 | import java.awt.geom.Line2D
8 | import java.awt.geom.Point2D
9 | import javax.swing.JPanel
10 |
11 | class Canvas(private val callGraphToolWindow: CallGraphToolWindow): JPanel() {
12 | private val defaultCameraOrigin = Point2D.Float(0f, 0f)
13 | val cameraOrigin = Point2D.Float(defaultCameraOrigin.x, defaultCameraOrigin.y)
14 | private val defaultZoomRatio = 1f
15 | private val zoomRatio = Point2D.Float(defaultZoomRatio, defaultZoomRatio)
16 | private val nodeRadius = 5f
17 | private val regularLineWidth = 1f
18 | private val solidLineStroke = BasicStroke(regularLineWidth)
19 | private val visibleNodes = mutableSetOf()
20 | private val visibleEdges = mutableSetOf()
21 | private val nodeShapesMap = mutableMapOf()
22 | private val methodAccessColorMap = mapOf(
23 | PsiModifier.PUBLIC to Colors.GREEN.color,
24 | PsiModifier.PROTECTED to Colors.LIGHT_ORANGE.color,
25 | PsiModifier.PACKAGE_LOCAL to Colors.BLUE.color,
26 | PsiModifier.PRIVATE to Colors.RED.color
27 | )
28 | private val methodAccessLabelMap = mapOf(
29 | PsiModifier.PUBLIC to "public",
30 | PsiModifier.PROTECTED to "protected",
31 | PsiModifier.PACKAGE_LOCAL to "package local",
32 | PsiModifier.PRIVATE to "private"
33 | )
34 | private val heatMapColors = listOf(
35 | Colors.DEEP_BLUE.color,
36 | Colors.BLUE.color,
37 | Colors.LIGHT_BLUE.color,
38 | Colors.CYAN.color,
39 | Colors.GREEN.color,
40 | Colors.LIGHT_GREEN.color,
41 | Colors.YELLOW.color,
42 | Colors.LIGHT_ORANGE.color,
43 | Colors.ORANGE.color,
44 | Colors.RED.color
45 | )
46 |
47 | private var graph = Graph()
48 | private var hoveredNode: Node? = null
49 |
50 | override fun paintComponent(graphics: Graphics) {
51 | super.paintComponent(graphics)
52 |
53 | // set up the drawing panel
54 | val graphics2D = graphics as Graphics2D
55 | graphics2D.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON)
56 | graphics2D.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON)
57 |
58 | // fill the background for entire canvas
59 | graphics2D.color = Colors.BACKGROUND_COLOR.color
60 | graphics2D.fillRect(0, 0, this.width, this.height)
61 |
62 | // draw un-highlighted and highlighted self loops
63 | this.visibleEdges
64 | .filter { it.sourceNode === it.targetNode }
65 | .forEach { drawSelfLoopEdge(graphics2D, it, isNodeHighlighted(it.sourceNode)) }
66 |
67 | // draw un-highlighted edgesMap
68 | this.visibleEdges
69 | .filter { it.sourceNode !== it.targetNode &&
70 | !isNodeHighlighted(it.sourceNode) && !isNodeHighlighted(it.targetNode) }
71 | .forEach { drawNonLoopEdge(graphics2D, it, Colors.UN_HIGHLIGHTED_COLOR.color) }
72 |
73 | // draw upstream/downstream edgesMap
74 | val highlightedNodes = this.visibleNodes.filter { isNodeHighlighted(it) }.toSet()
75 | val upstreamEdges = highlightedNodes.flatMap { it.inEdges.values }.toSet()
76 | val downstreamEdges = highlightedNodes.flatMap { it.outEdges.values }.toSet()
77 | upstreamEdges.forEach { drawNonLoopEdge(graphics2D, it, Colors.UPSTREAM_COLOR.color) }
78 | downstreamEdges.forEach { drawNonLoopEdge(graphics2D, it, Colors.DOWNSTREAM_COLOR.color) }
79 |
80 | // draw un-highlighted labels
81 | val upstreamNodes = upstreamEdges.map { it.sourceNode }.toSet()
82 | val downstreamNodes = downstreamEdges.map { it.targetNode }.toSet()
83 | val unHighlightedNodes = this.visibleNodes
84 | .filter { !isNodeHighlighted(it) && !upstreamNodes.contains(it) && !downstreamNodes.contains(it) }
85 | .toSet()
86 | unHighlightedNodes.forEach { drawNodeLabels(graphics2D, it, Colors.NEUTRAL_COLOR.color, false) }
87 |
88 | // draw un-highlighted nodesMap (upstream/downstream nodesMap are excluded)
89 | this.nodeShapesMap.clear()
90 | unHighlightedNodes
91 | .filter { !upstreamNodes.contains(it) && !downstreamNodes.contains(it) }
92 | .forEach { drawNode(graphics2D, it, Colors.UN_HIGHLIGHTED_COLOR.color) }
93 |
94 | // draw upstream/downstream label and nodesMap
95 | upstreamNodes.forEach { drawNodeLabels(graphics2D, it, Colors.UPSTREAM_COLOR.color, false) }
96 | downstreamNodes.forEach { drawNodeLabels(graphics2D, it, Colors.DOWNSTREAM_COLOR.color, false) }
97 | upstreamNodes.forEach { drawNode(graphics2D, it, Colors.UPSTREAM_COLOR.color) }
98 | downstreamNodes.forEach { drawNode(graphics2D, it, Colors.DOWNSTREAM_COLOR.color) }
99 |
100 | // draw highlighted node and label
101 | this.visibleNodes
102 | .filter { isNodeHighlighted(it) }
103 | .forEach {
104 | drawNode(graphics2D, it, Colors.HIGHLIGHTED_COLOR.color)
105 | drawNodeLabels(graphics2D, it, Colors.HIGHLIGHTED_COLOR.color, true)
106 | }
107 |
108 | // draw legend
109 | if (this.callGraphToolWindow.isLegendNeeded()) {
110 | val legend = if (this.callGraphToolWindow.isNodeColorByAccess()) {
111 | listOf(PsiModifier.PUBLIC, PsiModifier.PROTECTED, PsiModifier.PACKAGE_LOCAL, PsiModifier.PRIVATE)
112 | .map { this.methodAccessLabelMap.getValue(it) to this.methodAccessColorMap.getValue(it) }
113 | } else {
114 | emptyList()
115 | }
116 | drawLegend(graphics2D, legend)
117 | }
118 | }
119 |
120 | fun reset(graph: Graph) {
121 | this.graph = graph
122 | this.visibleNodes.clear()
123 | this.visibleNodes.addAll(graph.getNodes())
124 | this.visibleEdges.clear()
125 | this.visibleEdges.addAll(graph.getEdges())
126 | this.nodeShapesMap.clear()
127 | this.hoveredNode = null
128 | this.cameraOrigin.setLocation(defaultCameraOrigin)
129 | this.zoomRatio.setLocation(this.defaultZoomRatio, this.defaultZoomRatio)
130 | }
131 |
132 | fun setHoveredNode(node: Node?): Canvas {
133 | if (this.hoveredNode !== node) {
134 | this.hoveredNode = node
135 | repaint()
136 | }
137 | return this
138 | }
139 |
140 | fun toggleClickedNode(node: Node) {
141 | this.callGraphToolWindow.toggleFocusedMethod(node.method)
142 | repaint()
143 | }
144 |
145 | fun clearClickedNodes() {
146 | this.callGraphToolWindow.clearFocusedMethods()
147 | repaint()
148 | }
149 |
150 | fun zoomAtPoint(point: Point2D.Float, xZoomFactor: Float, yZoomFactor: Float) {
151 | this.cameraOrigin.setLocation(
152 | xZoomFactor * this.cameraOrigin.x + (xZoomFactor - 1) * point.x,
153 | yZoomFactor * this.cameraOrigin.y + (yZoomFactor - 1) * point.y
154 | )
155 | this.zoomRatio.x *= xZoomFactor
156 | this.zoomRatio.y *= yZoomFactor
157 | repaint()
158 | }
159 |
160 | fun getNodeUnderPoint(point: Point2D): Node? {
161 | return this.nodeShapesMap
162 | .filter { (shape, _) -> shape.contains(point.x, point.y) }
163 | .values
164 | .firstOrNull()
165 | }
166 |
167 | fun fitCanvasToView() {
168 | val blueprint = this.graph.getNodes().associateBy({ it.id }, { it.rawLayoutPoint })
169 | val bestFitBlueprint = Utils.fitLayoutToViewport(blueprint)
170 | Utils.applyLayoutBlueprintToGraph(bestFitBlueprint, this.graph)
171 | this.cameraOrigin.setLocation(defaultCameraOrigin)
172 | this.zoomRatio.setLocation(defaultZoomRatio, defaultZoomRatio)
173 | repaint()
174 | }
175 |
176 | fun fitCanvasToBestRatio() {
177 | // set every node coordinate to its original raw layout by GraphViz
178 | this.graph.getNodes().forEach { it.point.setLocation(it.rawLayoutPoint) }
179 | this.cameraOrigin.setLocation(defaultCameraOrigin)
180 | this.zoomRatio.setLocation(defaultZoomRatio, defaultZoomRatio)
181 | repaint()
182 | }
183 |
184 | fun getNodesCount() = this.graph.getNodes().size
185 |
186 | fun filterChangeHandler() {
187 | this.visibleNodes.clear()
188 | this.visibleNodes.addAll(this.graph.getNodes()
189 | .filter { node ->
190 | val method = node.method
191 | val isVisibleAccessLevel = when {
192 | Utils.isPublic(method) -> this.callGraphToolWindow.isFilterAccessPublicChecked()
193 | Utils.isProtected(method) -> this.callGraphToolWindow.isFilterAccessProtectedChecked()
194 | Utils.isPackageLocal(method) -> this.callGraphToolWindow.isFilterAccessPackageLocalChecked()
195 | Utils.isPrivate(method) -> this.callGraphToolWindow.isFilterAccessPrivateChecked()
196 | else -> true
197 | }
198 | val isExternalMethod = Utils.getSourceRoot(method.containingFile.virtualFile) == null
199 | val isVisibleExternal = !isExternalMethod || this.callGraphToolWindow.isFilterExternalChecked()
200 |
201 | isVisibleAccessLevel && isVisibleExternal
202 | })
203 | this.visibleEdges.clear()
204 | this.visibleEdges.addAll(graph.getEdges()
205 | .filter { this.visibleNodes.contains(it.sourceNode) && this.visibleNodes.contains(it.targetNode) })
206 | repaint()
207 | }
208 |
209 | private fun toCameraView(point: Point2D.Float): Point2D.Float {
210 | val canvasSize = this.callGraphToolWindow.getCanvasSize()
211 | return Point2D.Float(
212 | this.zoomRatio.x * point.x * canvasSize.width - this.cameraOrigin.x,
213 | this.zoomRatio.y * point.y * canvasSize.height - this.cameraOrigin.y
214 | )
215 | }
216 |
217 | private fun isNodeHighlighted(node: Node): Boolean {
218 | return this.hoveredNode === node || this.callGraphToolWindow.isFocusedMethod(node.method)
219 | }
220 |
221 | private fun drawLegend(graphics2D: Graphics2D, labels: List>) {
222 | val singleLabelHeight = graphics2D.fontMetrics.ascent + graphics2D.fontMetrics.descent
223 | val boundingBoxLowerLeft = Point2D.Float(
224 | 0f,
225 | labels.size * singleLabelHeight.toFloat()
226 | )
227 | drawLabels(graphics2D, boundingBoxLowerLeft, labels, Colors.BACKGROUND_COLOR.color,
228 | Colors.UN_HIGHLIGHTED_COLOR.color, 1)
229 | }
230 |
231 | private fun drawSelfLoopEdge(graphics2D: Graphics2D, edge: Edge, isHighlighted: Boolean) {
232 | val sourceNodeCenter = toCameraView(edge.sourceNode.point)
233 | drawSelfLoop(graphics2D, sourceNodeCenter, isHighlighted)
234 | }
235 |
236 | private fun drawNonLoopEdge(graphics2D: Graphics2D, edge: Edge, color: Color) {
237 | val sourceNodeCenter = toCameraView(edge.sourceNode.point)
238 | val targetNodeCenter = toCameraView(edge.targetNode.point)
239 | drawLine(graphics2D, sourceNodeCenter, targetNodeCenter, color)
240 | drawLineArrow(graphics2D, sourceNodeCenter, targetNodeCenter, color)
241 | }
242 |
243 | private fun drawNode(graphics2D: Graphics2D, node: Node, outlineColor: Color) {
244 | val nodeCenter = toCameraView(node.point)
245 | val backgroundColor = getNodeBackgroundColor(node)
246 | val nodeShape = drawCircle(graphics2D, nodeCenter, backgroundColor, outlineColor)
247 | this.nodeShapesMap[nodeShape] = node
248 | }
249 |
250 | private fun getNodeBackgroundColor(node: Node): Color {
251 | if (this.callGraphToolWindow.isNodeColorByAccess()) {
252 | return this.methodAccessColorMap.entries
253 | .firstOrNull { (accessLevel, _) -> node.method.modifierList.hasModifierProperty(accessLevel) }
254 | ?.value
255 | ?: Colors.BACKGROUND_COLOR.color
256 | } else if (this.callGraphToolWindow.isNodeColorByClassName()) {
257 | val psiClass = node.method.containingClass
258 | if (psiClass != null) {
259 | val hashIndex = psiClass.hashCode() % this.heatMapColors.size
260 | return this.heatMapColors[hashIndex]
261 | }
262 | }
263 | return Colors.BACKGROUND_COLOR.color
264 | }
265 |
266 | private fun createNodeLabels(node: Node, signatureColor: Color, isNodeHovered: Boolean): List> {
267 | // draw labels in top-down order
268 | val labels = mutableListOf>()
269 | // file path
270 | if (this.callGraphToolWindow.isRenderFunctionFilePath(isNodeHovered)) {
271 | labels.add(node.filePath to Colors.UN_HIGHLIGHTED_COLOR.color)
272 | }
273 | // package name
274 | if (this.callGraphToolWindow.isRenderFunctionPackageName(isNodeHovered)) {
275 | labels.add(node.packageName to Colors.UN_HIGHLIGHTED_COLOR.color)
276 | }
277 | // function signature
278 | val signature = if (isNodeHovered) node.signature else node.method.name
279 | labels.add(signature to signatureColor)
280 | return labels
281 | }
282 |
283 | private fun drawNodeLabels(graphics2D: Graphics2D, node: Node, labelColor: Color, isNodeHovered: Boolean) {
284 | // create labels
285 | val labels = createNodeLabels(node, labelColor, isNodeHovered)
286 | val fontMetrics = graphics2D.fontMetrics
287 | val halfLabelHeight = 0.5f * (fontMetrics.ascent + fontMetrics.descent)
288 | val nodeCenter = toCameraView(node.point)
289 | val boundingBoxLowerLeft = Point2D.Float(
290 | nodeCenter.x + 4 * nodeRadius,
291 | nodeCenter.y + halfLabelHeight
292 | )
293 | val backgroundColor =
294 | if (this.callGraphToolWindow.isQueried(node.method.name)) Colors.HIGHLIGHTED_BACKGROUND_COLOR.color
295 | else Colors.BACKGROUND_COLOR.color
296 | val borderColor = if (isNodeHovered) Colors.UN_HIGHLIGHTED_COLOR.color else Colors.BACKGROUND_COLOR.color
297 | drawLabels(graphics2D, boundingBoxLowerLeft, labels, backgroundColor, borderColor, 2)
298 | }
299 |
300 | private fun drawLabels(
301 | graphics2D: Graphics2D,
302 | boundingBoxLowerLeft: Point2D.Float,
303 | labels: List>,
304 | backgroundColor: Color,
305 | borderColor: Color,
306 | padding: Int
307 | ) {
308 | val fontMetrics = graphics2D.fontMetrics
309 | val singleLabelHeight = fontMetrics.ascent + fontMetrics.descent
310 | val boundingBoxWidth = labels
311 | .map { (text, _) -> fontMetrics.getStringBounds(text, graphics2D).width.toInt() }
312 | .maxOrNull()
313 | ?: 0
314 | val boundingBoxHeight = labels.size * singleLabelHeight
315 | val boundingBoxUpperLeft = Point2D.Float(
316 | boundingBoxLowerLeft.x,
317 | boundingBoxLowerLeft.y - 2 * padding - boundingBoxHeight
318 | )
319 | val boundingBoxUpperRight = Point2D.Float(
320 | boundingBoxUpperLeft.x + 2 * padding + boundingBoxWidth,
321 | boundingBoxUpperLeft.y
322 | )
323 | val boundingBoxLowerRight = Point2D.Float(
324 | boundingBoxUpperRight.x,
325 | boundingBoxLowerLeft.y
326 | )
327 | // fill background to overall bounding box
328 | graphics2D.color = backgroundColor
329 | graphics2D.fillRect(
330 | (boundingBoxUpperLeft.x + 1).toInt(),
331 | (boundingBoxUpperLeft.y + 1).toInt(),
332 | 2 * padding + boundingBoxWidth,
333 | 2 * padding + boundingBoxHeight
334 | )
335 | // draw border if the node is hovered
336 | drawLine(graphics2D, boundingBoxLowerLeft, boundingBoxUpperLeft, borderColor)
337 | drawLine(graphics2D, boundingBoxUpperLeft, boundingBoxUpperRight, borderColor)
338 | drawLine(graphics2D, boundingBoxUpperRight, boundingBoxLowerRight, borderColor)
339 | drawLine(graphics2D, boundingBoxLowerRight, boundingBoxLowerLeft, borderColor)
340 | // draw text
341 | labels.reversed().mapIndexed { index, (text, color) ->
342 | val labelLowerLeft = Point2D.Float(
343 | boundingBoxLowerLeft.x + padding,
344 | boundingBoxLowerLeft.y - padding - fontMetrics.descent - index * singleLabelHeight
345 | )
346 | drawText(graphics2D, labelLowerLeft, text, color)
347 | }
348 | }
349 |
350 | private fun drawCircle(
351 | graphics2D: Graphics2D,
352 | circleCenter: Point2D.Float,
353 | backgroundColor: Color,
354 | outlineColor: Color): Shape {
355 | // create node shape
356 | val upperLeft = Point2D.Float(
357 | circleCenter.x - this.nodeRadius,
358 | circleCenter.y - this.nodeRadius
359 | )
360 | val diameter = 2 * this.nodeRadius
361 | val shape = Ellipse2D.Float(
362 | upperLeft.x,
363 | upperLeft.y,
364 | diameter,
365 | diameter
366 | )
367 | // fill node with color
368 | graphics2D.color = backgroundColor
369 | graphics2D.fill(shape)
370 | // draw the outline
371 | graphics2D.color = outlineColor
372 | val strokedShape = this.solidLineStroke.createStrokedShape(shape)
373 | graphics2D.draw(strokedShape)
374 | return shape
375 | }
376 |
377 | private fun drawText(graphics2D: Graphics2D, textLowerLeft: Point2D.Float, text: String, textColor: Color) {
378 | graphics2D.color = textColor
379 | graphics2D.drawString(text, textLowerLeft.x, textLowerLeft.y)
380 | }
381 |
382 | private fun drawLine(
383 | graphics2D: Graphics2D,
384 | sourcePoint: Point2D.Float,
385 | targetPoint: Point2D.Float,
386 | lineColor: Color) {
387 | val shape = Line2D.Float(sourcePoint, targetPoint)
388 | val strokedShape = this.solidLineStroke.createStrokedShape(shape)
389 | graphics2D.color = lineColor
390 | graphics2D.draw(strokedShape)
391 | }
392 |
393 | private fun drawSelfLoop(graphics2D: Graphics2D, nodeCenter: Point2D.Float, isHighlighted: Boolean) {
394 | // draw circle shape
395 | val selfLoopRadius = 10f
396 | val selfLoopDiameter = 2 * selfLoopRadius
397 | val loopUpperLeft = Point2D.Float(
398 | nodeCenter.x - selfLoopRadius,
399 | nodeCenter.y - selfLoopDiameter
400 | )
401 | val upstreamHalfArc = Arc2D.Float(
402 | loopUpperLeft.x,
403 | loopUpperLeft.y,
404 | selfLoopDiameter,
405 | selfLoopDiameter,
406 | 90f,
407 | 180f,
408 | Arc2D.OPEN
409 | )
410 | val downstreamHalfArc = Arc2D.Float(
411 | loopUpperLeft.x,
412 | loopUpperLeft.y,
413 | selfLoopDiameter,
414 | selfLoopDiameter,
415 | 270f,
416 | 180f,
417 | Arc2D.OPEN
418 | )
419 | val strokedUpstreamHalfShape = this.solidLineStroke.createStrokedShape(upstreamHalfArc)
420 | val strokedDownstreamHalfShape = this.solidLineStroke.createStrokedShape(downstreamHalfArc)
421 | val upstreamHalfLoopColor =
422 | if (isHighlighted) Colors.UPSTREAM_COLOR.color
423 | else Colors.UN_HIGHLIGHTED_COLOR.color
424 | val downstreamHalfLoopColor =
425 | if (isHighlighted) Colors.DOWNSTREAM_COLOR.color
426 | else Colors.UN_HIGHLIGHTED_COLOR.color
427 | graphics2D.color = upstreamHalfLoopColor
428 | graphics2D.draw(strokedUpstreamHalfShape)
429 | graphics2D.color = downstreamHalfLoopColor
430 | graphics2D.draw(strokedDownstreamHalfShape)
431 | // draw arrow
432 | val arrowCenter = Point2D.Float(nodeCenter.x, nodeCenter.y - selfLoopDiameter)
433 | drawArrow(graphics2D, arrowCenter, Math.PI, downstreamHalfLoopColor)
434 | }
435 |
436 | private fun drawLineArrow(
437 | graphics2D: Graphics2D,
438 | sourcePoint: Point2D.Float,
439 | targetPoint: Point2D.Float,
440 | arrowColor: Color
441 | ) {
442 | val angle = Math.atan2((targetPoint.y - sourcePoint.y).toDouble(), (targetPoint.x - sourcePoint.x).toDouble())
443 | val arrowCenter = Point2D.Float(
444 | 0.5f * (sourcePoint.x + targetPoint.x),
445 | 0.5f * (sourcePoint.y + targetPoint.y)
446 | )
447 | drawArrow(graphics2D, arrowCenter, angle, arrowColor)
448 | }
449 |
450 | private fun drawArrow(
451 | graphics2D: Graphics2D,
452 | center: Point2D.Float,
453 | angle: Double,
454 | arrowColor: Color
455 | ) {
456 | val arrowSize = 5f
457 | val midPoint = Point2D.Float(
458 | center.x + arrowSize * Math.cos(angle).toFloat(),
459 | center.y + arrowSize * Math.sin(angle).toFloat()
460 | )
461 | val upperTipAngle = angle + Math.PI * 2 / 3
462 | val upperTipPoint = Point2D.Float(
463 | center.x + arrowSize * Math.cos(upperTipAngle).toFloat(),
464 | center.y + arrowSize * Math.sin(upperTipAngle).toFloat()
465 | )
466 | val lowerTipAngle = angle - Math.PI * 2 / 3
467 | val lowerTipPoint = Point2D.Float(
468 | center.x + arrowSize * Math.cos(lowerTipAngle).toFloat(),
469 | center.y + arrowSize * Math.sin(lowerTipAngle).toFloat()
470 | )
471 | val points = listOf(midPoint, upperTipPoint, lowerTipPoint, midPoint)
472 | val xPoints = points.map { Math.round(it.x) }
473 | val yPoints = points.map { Math.round(it.y) }
474 | graphics2D.color = arrowColor
475 | graphics2D.fillPolygon(xPoints.toIntArray(), yPoints.toIntArray(), xPoints.size)
476 | }
477 | }
478 |
--------------------------------------------------------------------------------
/src/main/kotlin/callgraph/CanvasBuilder.kt:
--------------------------------------------------------------------------------
1 | package callgraph
2 |
3 | import com.intellij.openapi.progress.ProgressIndicator
4 | import com.intellij.openapi.progress.ProgressIndicatorProvider
5 | import com.intellij.psi.PsiFile
6 | import com.intellij.psi.PsiMethod
7 |
8 | class CanvasBuilder {
9 | private val fileModifiedTimeCache = mutableMapOf()
10 |
11 | private var progressIndicator: ProgressIndicator? = null
12 | private var dependenciesCache = emptySet()
13 |
14 | fun build(canvasConfig: CanvasConfig) {
15 | // cancel existing progress if any
16 | this.progressIndicator?.cancel()
17 | this.progressIndicator = ProgressIndicatorProvider.getGlobalProgressIndicator()
18 |
19 | // build a dependency snapshot for the entire code base
20 | val dependencies = getDependencies(canvasConfig, this.dependenciesCache, this.fileModifiedTimeCache)
21 |
22 | // visualize the viewing part as graph
23 | val sourceCodeRoots = Utils.getSourceCodeRoots(canvasConfig)
24 | val files = Utils.getSourceCodeFiles(canvasConfig.project, sourceCodeRoots)
25 | val methods = Utils.getMethodsInScope(canvasConfig, files)
26 | val dependencyView = Utils.getDependencyView(canvasConfig, methods, dependencies)
27 | val graph = buildGraph(methods, dependencyView)
28 | canvasConfig.canvas.reset(graph)
29 | }
30 |
31 | private fun buildGraph(methods: Set, dependencyView: Set): Graph {
32 | val graph = Graph()
33 | methods.forEach { graph.addNode(it) }
34 | dependencyView.forEach {
35 | val caller = it.caller.element
36 | val callee = it.callee.element
37 | if (caller != null && callee != null) {
38 | graph.addNode(caller)
39 | graph.addNode(callee)
40 | graph.addEdge(caller, callee)
41 | }
42 | }
43 | Utils.layout(graph)
44 | return graph
45 | }
46 |
47 | private fun getDependencies(
48 | canvasConfig: CanvasConfig,
49 | dependenciesCache: Set,
50 | fileModifiedTimeCache: Map
51 | ): Set {
52 | val allFiles = Utils.getAllSourceCodeFiles(canvasConfig.project)
53 | val newFiles = allFiles.filter { !fileModifiedTimeCache.containsKey(it) }
54 | val changedFiles = allFiles
55 | .filter { fileModifiedTimeCache.containsKey(it) && fileModifiedTimeCache[it] != it.modificationStamp }
56 | .toSet()
57 | val validDependencies = dependenciesCache
58 | .filter {
59 | !changedFiles.contains(it.caller.containingFile) && !changedFiles.contains(it.callee.containingFile)
60 | }
61 | .toSet()
62 | val invalidFiles = dependenciesCache
63 | .filter { !validDependencies.contains(it) }
64 | .flatMap { listOf(it.caller.containingFile, it.callee.containingFile) }
65 | .filterNotNull()
66 | .toSet()
67 | val filesToParse = newFiles.union(invalidFiles)
68 | val methodsToParse = Utils.getMethodsFromFiles(filesToParse)
69 |
70 | // parse method dependencies
71 | canvasConfig.callGraphToolWindow.resetProgressBar(methodsToParse.size)
72 | val newDependencies = methodsToParse
73 | .flatMap {
74 | canvasConfig.callGraphToolWindow.incrementProgressBar()
75 | Utils.getDependenciesFromMethod(it)
76 | }
77 | .toSet()
78 | val dependencies = validDependencies.union(newDependencies)
79 |
80 | // cache the dependencies for next use
81 | this.dependenciesCache = dependencies
82 | this.fileModifiedTimeCache.clear()
83 | this.fileModifiedTimeCache.putAll(allFiles.associateBy({ it }, { it.modificationStamp }))
84 |
85 | return dependencies
86 | }
87 | }
88 |
--------------------------------------------------------------------------------
/src/main/kotlin/callgraph/CanvasConfig.kt:
--------------------------------------------------------------------------------
1 | package callgraph
2 |
3 | import com.intellij.openapi.project.Project
4 | import com.intellij.psi.PsiMethod
5 |
6 | data class CanvasConfig(
7 | val project: Project,
8 | val buildType: BuildType,
9 | val canvas: Canvas,
10 | val selectedModuleName: String,
11 | val selectedDirectoryPath: String,
12 | val focusedMethods: Set,
13 | val callGraphToolWindow: CallGraphToolWindow
14 | ) {
15 | enum class BuildType(val label: String) {
16 | WHOLE_PROJECT_WITH_TEST_LIMITED("Whole project (test files included), limited upstream/downstream scope"),
17 | WHOLE_PROJECT_WITHOUT_TEST_LIMITED("Whole project (test files excluded), limited upstream/downstream scope"),
18 | MODULE_LIMITED("Module, limited upstream/downstream scope"),
19 | DIRECTORY_LIMITED("Directory, limited upstream/downstream scope"),
20 | WHOLE_PROJECT_WITH_TEST("Whole project (test files included)"),
21 | WHOLE_PROJECT_WITHOUT_TEST("Whole project (test files excluded)"),
22 | MODULE("Module"),
23 | DIRECTORY("Directory"),
24 | UPSTREAM("Upstream"),
25 | DOWNSTREAM("Downstream"),
26 | UPSTREAM_DOWNSTREAM("Upstream & downstream")
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/src/main/kotlin/callgraph/Colors.kt:
--------------------------------------------------------------------------------
1 | package callgraph
2 |
3 | import com.intellij.ui.JBColor
4 | import java.awt.Color
5 |
6 | enum class Colors(val color: JBColor) {
7 | BACKGROUND_COLOR(JBColor(Color(0xFDFEFF), Color(0x292B2D))),
8 | UN_HIGHLIGHTED_COLOR(JBColor(Color(0xC6C8CA), Color(0x585A5C))),
9 | NEUTRAL_COLOR(JBColor(Color(0x626466), Color(0x949698))),
10 | HIGHLIGHTED_COLOR(JBColor(Color(0x4285F4), Color(0x589DEF))),
11 | HIGHLIGHTED_BACKGROUND_COLOR(JBColor(Color(0xFFFF00), Color(0xFFFF00))),
12 | UPSTREAM_COLOR(JBColor(Color(0xFBBC05), Color(0xBE9117))),
13 | DOWNSTREAM_COLOR(JBColor(Color(0x34A853), Color(0x538863))),
14 |
15 | DEEP_BLUE(JBColor(Color(0x0000FF), Color(0x0000FF))),
16 | BLUE(JBColor(Color(0x0088FF), Color(0x0088FF))),
17 | LIGHT_BLUE(JBColor(Color(0x00FFFF), Color(0x00FFFF))),
18 | CYAN(JBColor(Color(0x00FF88), Color(0x00FF88))),
19 | GREEN(JBColor(Color(0x00FF00), Color(0x00FF00))),
20 | LIGHT_GREEN(JBColor(Color(0x88FF00), Color(0x88FF00))),
21 | YELLOW(JBColor(Color(0xFFFF00), Color(0xFFFF00))),
22 | LIGHT_ORANGE(JBColor(Color(0xFFAA00), Color(0xFFAA00))),
23 | ORANGE(JBColor(Color(0xFF6600), Color(0xFF6600))),
24 | RED(JBColor(Color(0xFF0000), Color(0xFF0000)))
25 | }
26 |
--------------------------------------------------------------------------------
/src/main/kotlin/callgraph/ComboBoxOptions.kt:
--------------------------------------------------------------------------------
1 | package callgraph
2 |
3 | enum class ComboBoxOptions(val text: String) {
4 | VIEW_ALWAYS("Always show"),
5 | VIEW_HOVERED("When hovered"),
6 | VIEW_NEVER("Hide"),
7 | NODE_SELECTION_SINGLE("Single node"),
8 | NODE_SELECTION_MULTIPLE("Multiple nodes"),
9 | NODE_COLOR_NONE("None"),
10 | NODE_COLOR_ACCESS("By access level"),
11 | NODE_COLOR_CLASS("By class name"),
12 | DUMMY("(Dummy value)");
13 |
14 | companion object {
15 | private val reverseMap = entries.associateBy(ComboBoxOptions::text)
16 |
17 | fun fromText(text: String) = reverseMap.getValue(text)
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/src/main/kotlin/callgraph/Dependency.kt:
--------------------------------------------------------------------------------
1 | package callgraph
2 |
3 | import com.intellij.psi.PsiMethod
4 | import com.intellij.psi.SmartPsiElementPointer
5 |
6 | data class Dependency(val caller: SmartPsiElementPointer, val callee: SmartPsiElementPointer)
7 |
--------------------------------------------------------------------------------
/src/main/kotlin/callgraph/Edge.kt:
--------------------------------------------------------------------------------
1 | package callgraph
2 |
3 | data class Edge(val id: String, val sourceNode: Node, val targetNode: Node)
4 |
--------------------------------------------------------------------------------
/src/main/kotlin/callgraph/Graph.kt:
--------------------------------------------------------------------------------
1 | package callgraph
2 |
3 | import com.intellij.psi.PsiElement
4 | import com.intellij.psi.PsiMethod
5 | import java.util.*
6 |
7 | data class Graph(
8 | val nodesMap: MutableMap = mutableMapOf(),
9 | val edgesMap: MutableMap = mutableMapOf()
10 | ) {
11 | val connectedComponents: Set by lazy {
12 | val visitedNodes = mutableSetOf()
13 | this.getNodes()
14 | .map { traverseBfs(it, visitedNodes) }
15 | .filter { it.isNotEmpty() }
16 | .map { component ->
17 | val componentNodes = this.nodesMap.filterValues { component.contains(it) }.toMutableMap()
18 | val componentEdges = this.edgesMap.filterValues {
19 | component.contains(it.sourceNode) || component.contains(it.targetNode)
20 | }.toMutableMap()
21 | Graph(componentNodes, componentEdges)
22 | }
23 | .toSet()
24 | }
25 |
26 | fun addNode(method: PsiMethod) {
27 | val nodeId = getNodeHash(method)
28 | if (!this.nodesMap.containsKey(nodeId)) {
29 | val node = Node(nodeId, method)
30 | this.nodesMap[nodeId] = node
31 | }
32 | }
33 |
34 | fun addEdge(sourceMethod: PsiMethod, targetMethod: PsiMethod) {
35 | val sourceNodeId = getNodeHash(sourceMethod)
36 | val targetNodeId = getNodeHash(targetMethod)
37 | val edgeId = getEdgeHash(sourceNodeId, targetNodeId)
38 | if (!this.edgesMap.containsKey(edgeId)) {
39 | val sourceNode = this.nodesMap.getValue(sourceNodeId)
40 | val targetNode = this.nodesMap.getValue(targetNodeId)
41 | val edge = Edge(edgeId, sourceNode, targetNode)
42 | this.edgesMap[edgeId] = edge
43 | sourceNode.addOutEdge(edge)
44 | targetNode.addInEdge(edge)
45 | }
46 | }
47 |
48 | fun getNode(nodeId: String) = this.nodesMap.getValue(nodeId)
49 |
50 | fun getNodes() = this.nodesMap.values.toSet()
51 |
52 | fun getEdges() = this.edgesMap.values.toSet()
53 |
54 | private fun traverseBfs(root: Node, visitedNodes: MutableSet): Set {
55 | if (visitedNodes.contains(root)) {
56 | return Collections.emptySet()
57 | }
58 | val path = mutableSetOf()
59 | val queue = mutableSetOf(root)
60 | while (queue.isNotEmpty()) {
61 | visitedNodes.addAll(queue)
62 | path.addAll(queue)
63 | val newQueue = queue
64 | .flatMap { it.getNeighbors() }
65 | .filter { !visitedNodes.contains(it) }
66 | queue.clear()
67 | queue.addAll(newQueue)
68 | }
69 | return path
70 | }
71 |
72 | private fun getNodeHash(element: PsiElement) = element.hashCode().toString()
73 |
74 | private fun getEdgeHash(sourceNodeId: String, targetNodeId: String) = "$sourceNodeId-$targetNodeId"
75 | }
76 |
--------------------------------------------------------------------------------
/src/main/kotlin/callgraph/MouseEventHandler.kt:
--------------------------------------------------------------------------------
1 | package callgraph
2 |
3 | import java.awt.event.*
4 | import java.awt.geom.Point2D
5 |
6 | class MouseEventHandler(private val canvas: Canvas): MouseListener, MouseMotionListener, MouseWheelListener {
7 | private val lastMousePosition = Point2D.Float()
8 |
9 | override fun mouseClicked(event: MouseEvent) {
10 | val node = this.canvas.getNodeUnderPoint(event.point)
11 | if (node == null) {
12 | this.canvas.clearClickedNodes()
13 | } else {
14 | this.canvas.toggleClickedNode(node)
15 | }
16 | }
17 |
18 | override fun mousePressed(event: MouseEvent) {
19 | this.lastMousePosition.setLocation(event.x.toFloat(), event.y.toFloat())
20 | }
21 |
22 | override fun mouseReleased(event: MouseEvent) {
23 | }
24 |
25 | override fun mouseEntered(event: MouseEvent) {
26 | }
27 |
28 | override fun mouseExited(event: MouseEvent) {
29 | }
30 |
31 | override fun mouseDragged(event: MouseEvent) {
32 | val currentMousePosition = Point2D.Float(event.x.toFloat(), event.y.toFloat())
33 | if (currentMousePosition != this.lastMousePosition) {
34 | val currentCameraOrigin = this.canvas.cameraOrigin
35 | val newCameraOrigin = Point2D.Float(
36 | currentCameraOrigin.x - currentMousePosition.x + this.lastMousePosition.x,
37 | currentCameraOrigin.y - currentMousePosition.y + this.lastMousePosition.y
38 | )
39 | this.canvas.cameraOrigin.setLocation(newCameraOrigin)
40 | this.canvas.repaint()
41 | this.lastMousePosition.setLocation(currentMousePosition)
42 | }
43 | }
44 |
45 | override fun mouseMoved(event: MouseEvent) {
46 | val node = this.canvas.getNodeUnderPoint(event.point)
47 | this.canvas.setHoveredNode(node)
48 | }
49 |
50 | override fun mouseWheelMoved(event: MouseWheelEvent) {
51 | val scrollRotation = event.wheelRotation // 1 if scroll down, -1 otherwise
52 | val zoomFactor = Math.pow(1.25, -scrollRotation.toDouble()).toFloat()
53 | val mousePosition = Point2D.Float(event.x.toFloat(), event.y.toFloat())
54 | this.canvas.zoomAtPoint(mousePosition, zoomFactor, zoomFactor)
55 | }
56 | }
57 |
--------------------------------------------------------------------------------
/src/main/kotlin/callgraph/Node.kt:
--------------------------------------------------------------------------------
1 | package callgraph
2 |
3 | import com.intellij.psi.PsiMethod
4 | import java.awt.geom.Point2D
5 |
6 | data class Node(val id: String, val method: PsiMethod) {
7 | val outEdges = mutableMapOf()
8 | val inEdges = mutableMapOf()
9 | val filePath = Utils.getMethodFilePath(method) ?: "(no file)"
10 | val packageName = Utils.getMethodPackageName(method)
11 | val signature = Utils.getMethodSignature(method)
12 | val point = Point2D.Float()
13 | val rawLayoutPoint = Point2D.Float()
14 |
15 | fun addInEdge(edge: Edge) {
16 | if (!this.inEdges.containsKey(edge.id)) {
17 | this.inEdges[edge.id] = edge
18 | }
19 | }
20 |
21 | fun addOutEdge(edge: Edge) {
22 | if (!this.outEdges.containsKey(edge.id)) {
23 | this.outEdges[edge.id] = edge
24 | }
25 | }
26 |
27 | fun getNeighbors(): List {
28 | val upstreamNodes = this.inEdges.values.map { it.sourceNode }
29 | val downstreamNodes = this.outEdges.values.map { it.targetNode }
30 | return upstreamNodes.union(downstreamNodes).toList()
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/src/main/kotlin/callgraph/Utils.kt:
--------------------------------------------------------------------------------
1 | package callgraph
2 |
3 | import com.intellij.openapi.actionSystem.AnActionEvent
4 | import com.intellij.openapi.actionSystem.CommonDataKeys
5 | import com.intellij.openapi.application.ApplicationManager
6 | import com.intellij.openapi.components.service
7 | import com.intellij.openapi.module.Module
8 | import com.intellij.openapi.module.ModuleManager
9 | import com.intellij.openapi.progress.ProgressIndicator
10 | import com.intellij.openapi.progress.ProgressManager
11 | import com.intellij.openapi.progress.Task
12 | import com.intellij.openapi.project.Project
13 | import com.intellij.openapi.project.ProjectManager
14 | import com.intellij.openapi.roots.ModuleRootManager
15 | import com.intellij.openapi.roots.ProjectRootManager
16 | import com.intellij.openapi.vfs.LocalFileSystem
17 | import com.intellij.openapi.vfs.VfsUtilCore
18 | import com.intellij.openapi.vfs.VirtualFile
19 | import com.intellij.openapi.wm.ToolWindowManager
20 | import com.intellij.openapi.wm.WindowManager
21 | import com.intellij.psi.*
22 | import com.intellij.psi.SmartPointerManager
23 | import com.intellij.psi.util.PsiTreeUtil
24 | import guru.nidi.graphviz.attribute.Rank
25 | import guru.nidi.graphviz.engine.Format
26 | import guru.nidi.graphviz.engine.Graphviz
27 | import guru.nidi.graphviz.model.Factory.mutGraph
28 | import guru.nidi.graphviz.model.Factory.mutNode
29 | import java.awt.geom.Point2D
30 |
31 | object Utils {
32 | private const val NORMALIZED_GRID_SIZE = 0.1f
33 |
34 | fun getActiveProject(): Project? {
35 | return ProjectManager.getInstance()
36 | .openProjects
37 | .firstOrNull { WindowManager.getInstance().suggestParentWindow(it)?.isActive ?: false }
38 | }
39 |
40 | fun getActiveModules(project: Project): List {
41 | return ModuleManager.getInstance(project).modules.toList()
42 | }
43 |
44 | fun getDependencyView(
45 | canvasConfig: CanvasConfig,
46 | methods: Set,
47 | dependencies: Set
48 | ): Set {
49 | return when (canvasConfig.buildType) {
50 | CanvasConfig.BuildType.WHOLE_PROJECT_WITH_TEST_LIMITED,
51 | CanvasConfig.BuildType.WHOLE_PROJECT_WITHOUT_TEST_LIMITED,
52 | CanvasConfig.BuildType.MODULE_LIMITED,
53 | CanvasConfig.BuildType.DIRECTORY_LIMITED -> dependencies
54 | .filter { methods.contains(it.caller.element) && methods.contains(it.callee.element) }
55 | .toSet()
56 | CanvasConfig.BuildType.WHOLE_PROJECT_WITH_TEST,
57 | CanvasConfig.BuildType.WHOLE_PROJECT_WITHOUT_TEST,
58 | CanvasConfig.BuildType.MODULE,
59 | CanvasConfig.BuildType.DIRECTORY -> dependencies
60 | .filter { methods.contains(it.caller.element) || methods.contains(it.callee.element) }
61 | .toSet()
62 | CanvasConfig.BuildType.UPSTREAM ->
63 | getNestedDependencyView(dependencies, methods, mutableSetOf(), true)
64 | CanvasConfig.BuildType.DOWNSTREAM ->
65 | getNestedDependencyView(dependencies, methods, mutableSetOf(), false)
66 | CanvasConfig.BuildType.UPSTREAM_DOWNSTREAM -> {
67 | val upstream = getNestedDependencyView(dependencies, methods, mutableSetOf(), true)
68 | val downstream = getNestedDependencyView(dependencies, methods, mutableSetOf(), false)
69 | upstream.union(downstream)
70 | }
71 | }
72 | }
73 |
74 | fun getMethodsInScope(canvasConfig: CanvasConfig, files: Set): Set {
75 | return when (canvasConfig.buildType) {
76 | CanvasConfig.BuildType.WHOLE_PROJECT_WITH_TEST_LIMITED,
77 | CanvasConfig.BuildType.WHOLE_PROJECT_WITHOUT_TEST_LIMITED,
78 | CanvasConfig.BuildType.MODULE_LIMITED,
79 | CanvasConfig.BuildType.DIRECTORY_LIMITED,
80 | CanvasConfig.BuildType.WHOLE_PROJECT_WITH_TEST,
81 | CanvasConfig.BuildType.WHOLE_PROJECT_WITHOUT_TEST,
82 | CanvasConfig.BuildType.MODULE,
83 | CanvasConfig.BuildType.DIRECTORY -> getMethodsFromFiles(files)
84 | CanvasConfig.BuildType.UPSTREAM,
85 | CanvasConfig.BuildType.DOWNSTREAM,
86 | CanvasConfig.BuildType.UPSTREAM_DOWNSTREAM -> canvasConfig.focusedMethods
87 | }
88 | }
89 |
90 | fun getMethodsFromFiles(files: Set) =
91 | files
92 | .flatMap { // get all classes
93 | try {
94 | (it as PsiJavaFile).classes.toList()
95 | }
96 | catch (e: ClassCastException) { // when file conversion to Java file type fails
97 | emptyList()
98 | }
99 | }
100 | .flatMap { it.methods.toList() } // get all methods
101 | .toSet()
102 |
103 | fun getDependenciesFromMethod(method: PsiMethod): List {
104 | val methodPointer = toSmartPsiElementPointer(method)
105 | return PsiTreeUtil
106 | .findChildrenOfType(method, PsiIdentifier::class.java)
107 | .mapNotNull { it.context }
108 | .flatMap { it.references.toList() }
109 | .map { it.resolve() }
110 | .filterIsInstance()
111 | .map { Dependency(methodPointer, toSmartPsiElementPointer(it)) }
112 | }
113 |
114 | private fun toSmartPsiElementPointer(method: PsiMethod) =
115 | SmartPointerManager.getInstance(method.project).createSmartPsiElementPointer(method)
116 |
117 | fun layout(graph: Graph) {
118 | // get connected components from the graph, and render each part separately
119 | val subGraphBlueprints = graph.connectedComponents
120 | .map { this.getLayoutFromGraphViz(it) }
121 | .map { this.normalizeBlueprintGridSize(it) }
122 | .toList()
123 |
124 | // merge all connected components to a single graph, then adjust node coordinates, so they fit in the view
125 | val mergedBlueprint = this.mergeNormalizedLayouts(subGraphBlueprints)
126 | applyRawLayoutBlueprintToGraph(mergedBlueprint, graph)
127 | applyLayoutBlueprintToGraph(mergedBlueprint, graph)
128 | }
129 |
130 | fun runBackgroundTask(project: Project, runnable: Runnable) {
131 | ProgressManager.getInstance()
132 | .run(object: Task.Backgroundable(project, "Call graph") {
133 | override fun run(progressIndicator: ProgressIndicator) {
134 | ApplicationManager
135 | .getApplication()
136 | .invokeLater(runnable)
137 | }
138 | })
139 | }
140 |
141 | fun getMethodPackageName(method: PsiMethod): String {
142 | // get package name
143 | val psiJavaFile = method.containingFile as PsiJavaFile
144 | val packageName = psiJavaFile.packageStatement?.packageName ?: ""
145 | // get class name
146 | val className = method.containingClass?.qualifiedName ?: ""
147 | return if (packageName.isBlank() || className.startsWith(packageName)) className else "$packageName.$className"
148 | }
149 |
150 | fun getMethodFilePath(method: PsiMethod): String? {
151 | val file = method.containingFile.virtualFile
152 | val sourceRoot = getSourceRoot(file)
153 | return if (sourceRoot == null) null else VfsUtilCore.getRelativePath(file, sourceRoot)
154 | }
155 |
156 | fun getSourceRoot(file: VirtualFile): VirtualFile? {
157 | val project = getActiveProject()
158 | return if (project == null) null else ProjectRootManager.getInstance(project).fileIndex.getContentRootForFile(file)
159 | }
160 |
161 | fun getMethodSignature(method: PsiMethod): String {
162 | val parameterNames = method.parameterList.parameters.map { it.name }.joinToString()
163 | val parameters = if (parameterNames.isEmpty()) "" else "($parameterNames)"
164 | return "${method.name}$parameters"
165 | }
166 |
167 | fun fitLayoutToViewport(blueprint: Map): Map {
168 | val maxPoint = blueprint.values.reduce { a, b -> Point2D.Float(maxOf(a.x, b.x), maxOf(a.y, b.y)) }
169 | val minPoint = blueprint.values.reduce { a, b -> Point2D.Float(minOf(a.x, b.x), minOf(a.y, b.y)) }
170 | val graphSize = Point2D.Float(maxPoint.x - minPoint.x, maxPoint.y - minPoint.y)
171 | val bestFitBaseline = 0.1f // make the best fit window between 0.1 - 0.9 of the viewport
172 | val bestFitSize = 1 - 2 * bestFitBaseline
173 | return blueprint.mapValues { (_, point) ->
174 | Point2D.Float(
175 | (point.x - minPoint.x) / graphSize.x * bestFitSize + bestFitBaseline,
176 | (point.y - minPoint.y) / graphSize.y * bestFitSize + bestFitBaseline
177 | )
178 | }
179 | }
180 |
181 | fun runCallGraphFromAction(anActionEvent: AnActionEvent, buildType: CanvasConfig.BuildType) {
182 | val project = anActionEvent.project
183 | val psiElement = anActionEvent.getData(CommonDataKeys.PSI_ELEMENT) // get the element under editor caret
184 | if (project != null && psiElement is PsiMethod) {
185 | ToolWindowManager.getInstance(project)
186 | .getToolWindow("Call Graph")
187 | ?.activate {
188 | project.service()
189 | .callGraphToolWindow
190 | .clearFocusedMethods()
191 | .toggleFocusedMethod(psiElement)
192 | .run(buildType)
193 | }
194 | }
195 | }
196 |
197 | fun setActionEnabledAndVisibleByContext(anActionEvent: AnActionEvent) {
198 | val project = anActionEvent.project
199 | val psiElement = anActionEvent.getData(CommonDataKeys.PSI_ELEMENT)
200 | anActionEvent.presentation.isEnabledAndVisible = project != null && psiElement is PsiMethod
201 | }
202 |
203 | fun isPublic(method: PsiMethod) = method.modifierList.hasModifierProperty(PsiModifier.PUBLIC)
204 |
205 | fun isProtected(method: PsiMethod) = method.modifierList.hasModifierProperty(PsiModifier.PROTECTED)
206 |
207 | fun isPackageLocal(method: PsiMethod) = method.modifierList.hasModifierProperty(PsiModifier.PACKAGE_LOCAL)
208 |
209 | fun isPrivate(method: PsiMethod) = method.modifierList.hasModifierProperty(PsiModifier.PRIVATE)
210 |
211 | fun getAllSourceCodeFiles(project: Project): Set {
212 | val sourceCodeRoots = getAllSourceCodeRoots(project)
213 | return getSourceCodeFiles(project, sourceCodeRoots)
214 | }
215 |
216 | fun getSourceCodeFiles(project: Project, sourceCodeRoots: Set) =
217 | sourceCodeRoots
218 | .flatMap { contentSourceRoot ->
219 | val childrenVirtualFiles = mutableListOf()
220 | VfsUtilCore.iterateChildrenRecursively(contentSourceRoot, null) {
221 | if (it.isValid && !it.isDirectory) {
222 | val extension = it.extension
223 | if (extension.equals("java")) {
224 | childrenVirtualFiles.add(it)
225 | }
226 | }
227 | true
228 | }
229 | childrenVirtualFiles
230 | }
231 | .mapNotNull { PsiManager.getInstance(project).findFile(it) }
232 | .toSet()
233 |
234 | fun applyLayoutBlueprintToGraph(blueprint: Map, graph: Graph) {
235 | blueprint.forEach { (nodeId, point) -> graph.getNode(nodeId).point.setLocation(point) }
236 | }
237 |
238 | fun getSourceCodeRoots(canvasConfig: CanvasConfig) =
239 | when (canvasConfig.buildType) {
240 | CanvasConfig.BuildType.WHOLE_PROJECT_WITH_TEST_LIMITED,
241 | CanvasConfig.BuildType.WHOLE_PROJECT_WITH_TEST ->
242 | getAllSourceCodeRoots(canvasConfig.project)
243 | CanvasConfig.BuildType.WHOLE_PROJECT_WITHOUT_TEST_LIMITED,
244 | CanvasConfig.BuildType.WHOLE_PROJECT_WITHOUT_TEST ->
245 | getActiveModules(canvasConfig.project)
246 | .flatMap { ModuleRootManager.getInstance(it).getSourceRoots(false).toSet() }
247 | .toSet()
248 | CanvasConfig.BuildType.MODULE_LIMITED, CanvasConfig.BuildType.MODULE ->
249 | getSelectedModules(canvasConfig.project, canvasConfig.selectedModuleName)
250 | .flatMap { ModuleRootManager.getInstance(it).sourceRoots.toSet() }
251 | .toSet()
252 | CanvasConfig.BuildType.DIRECTORY_LIMITED,
253 | CanvasConfig.BuildType.DIRECTORY -> {
254 | val directoryPath = canvasConfig.selectedDirectoryPath
255 | listOfNotNull(LocalFileSystem.getInstance().findFileByPath(directoryPath)).toSet()
256 | }
257 | else -> emptySet()
258 | }
259 |
260 | private fun getNestedDependencyView(
261 | dependencies: Set,
262 | methods: Set,
263 | seenMethods: MutableSet,
264 | isUpstream: Boolean
265 | ): Set {
266 | if (methods.isEmpty()) {
267 | return emptySet()
268 | }
269 | val directPairs = dependencies.filter {
270 | methods.contains(if (isUpstream) it.callee.element else it.caller.element)
271 | }.toSet()
272 | val nextBatchMethods = directPairs
273 | .mapNotNull { if (isUpstream) it.caller.element else it.callee.element }
274 | .filter { !seenMethods.contains(it) }
275 | .toSet()
276 | seenMethods.addAll(nextBatchMethods)
277 | val nestedPairs = getNestedDependencyView(dependencies, nextBatchMethods, seenMethods, isUpstream)
278 | return directPairs + nestedPairs
279 | }
280 |
281 | private fun getLayoutFromGraphViz(graph: Graph): Map {
282 | // if graph only has one node, just set its coordinate to (0.5, 0.5), no need to call GraphViz
283 | if (graph.getNodes().size == 1) {
284 | return graph.getNodes()
285 | .map { it.id to Point2D.Float(0.5f, 0.5f) }
286 | .toMap()
287 | }
288 | // construct the GraphViz graph
289 | val gvGraph = mutGraph("test")
290 | .setDirected(true)
291 | .graphAttrs()
292 | .add(Rank.dir(Rank.RankDir.LEFT_TO_RIGHT))
293 | graph.getNodes()
294 | .sortedBy { it.method.name }
295 | .forEach { node ->
296 | val gvNode = mutNode(node.id)
297 | node.outEdges.values
298 | .map { it.targetNode }
299 | .sortedBy { it.method.name }
300 | .forEach { gvNode.addLink(it.id) }
301 | gvGraph.add(gvNode)
302 | }
303 |
304 | // parse the GraphViz layout as a mapping from "node name" to "x-y coordinate (percent of full graph size)"
305 | // GraphViz doc: https://graphviz.gitlab.io/_pages/doc/info/output.html#d:plain
306 | val layoutRawText = Graphviz.fromGraph(gvGraph).render(Format.PLAIN).toString()
307 | return layoutRawText.split("\n")
308 | .filter { it.startsWith("node") }
309 | .map { it.split(" ") }
310 | .map { it[1] to Point2D.Float(it[2].toFloat(), it[3].toFloat()) } // (x, y)
311 | .toMap()
312 | }
313 |
314 | private fun normalizeBlueprintGridSize(blueprint: Map): Map {
315 | if (blueprint.size < 2) {
316 | return blueprint
317 | }
318 | val gridSize = getGridSize(blueprint)
319 | val desiredGridSize = Point2D.Float(NORMALIZED_GRID_SIZE, NORMALIZED_GRID_SIZE)
320 | val xFactor = if (gridSize.x == 0f) 1f else desiredGridSize.x / gridSize.x
321 | val yFactor = if (gridSize.y == 0f) 1f else desiredGridSize.y / gridSize.y
322 | return blueprint.mapValues { (_, point) -> Point2D.Float(point.x * xFactor, point.y * yFactor) }
323 | }
324 |
325 | private fun getGridSize(blueprint: Map): Point2D.Float {
326 | val precisionFactor = 1000
327 | val xUniqueValues = blueprint.values.map { Math.round(precisionFactor * it.x) }.toSet()
328 | val yUniqueValues = blueprint.values.map { Math.round(precisionFactor * it.y) }.toSet()
329 | return Point2D.Float(
330 | getAverageElementDifference(xUniqueValues) / precisionFactor,
331 | getAverageElementDifference(yUniqueValues) / precisionFactor
332 | )
333 | }
334 |
335 | private fun getAverageElementDifference(elements: Set): Float {
336 | val max = elements.maxOrNull()
337 | val min = elements.minOrNull()
338 | return if (elements.size < 2 || max == null || min == null) 0f else (max - min) / (elements.size - 1).toFloat()
339 | }
340 |
341 | private fun mergeNormalizedLayouts(blueprints: List
16 | The goal is to make codebase extremely easy to understand, necessary for code navigation and debugging.
17 | Currently it only supports Java. If you want a similar tool for Typescript, Javascript, or Python, I highly recommend Codemap, another tool I built.
18 | See also:
19 |
20 | -
21 | Source code: pull requests are welcome!
22 |
23 | -
24 | Demo video: a quick glance of its features.
25 |
26 | -
27 | Love this plugin? Please leave a review, or consider
28 | donation
29 | to support the developer.
30 |
31 |
32 | ]]>
33 |
34 |
36 | 0.1:
38 |
39 | -
40 | Initial release.
41 |
42 | -
43 | Support
Java
.
44 |
45 | -
46 | Support building call graph from functions of the entire project, a single module, or a single folder path.
47 |
48 | -
49 | Support two graph layouts: fit to best ratio and fit to viewport.
50 | You can also tweak the grid size yourself.
51 |
52 | -
53 | Support selecting a single node or multiple nodes to visualize upstream/downstream calls.
54 |
55 | -
56 | Support code to graph: build call graph by right-clicking any function in the source code.
57 |
58 | -
59 | Support graph to code: jump to function definition in the source code from any node in the graph.
60 |
61 | -
62 | Support function name search: highlight nodes in the graph whose function name matches your search query.
63 |
64 | -
65 | Support color-coding nodes by function access level (public, protected, package local, private) and class name.
66 |
67 | -
68 | Support filtering nodes by function access level (public, protected, package local, private).
69 |
70 |
71 | ]]>
72 |
73 |
75 | Chentai Kao
76 |
77 |
78 | com.intellij.modules.lang
79 | com.intellij.modules.java
80 |
81 |
84 |
85 |
86 |
89 |
90 |
91 |
92 |
93 |
94 |
96 |
97 |
98 |
99 |
103 |
104 |
105 |
106 |
107 |
108 |
109 |
110 |
115 |
116 |
117 |
119 |
120 |
121 |
122 |
123 |
124 |
129 |
130 |
131 |
132 |
143 |
148 |
155 |
160 |
161 |
166 |
167 |
172 |
173 |
181 |
182 |
183 |
184 |
185 |
187 |
188 |
189 |
190 |
191 |
194 |
198 |
199 |
204 |
205 |
211 |
212 |
213 |
216 |
217 |
222 |
223 |
228 |
231 |
232 |
--------------------------------------------------------------------------------
/src/main/resources/META-INF/pluginIcon.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/src/main/resources/META-INF/pluginIcon_dark.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/src/main/resources/icons/build.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Chentai-Kao/call-graph-plugin/a8f8b2955502deb0e1eff485e0f03b75eb4a93c8/src/main/resources/icons/build.png
--------------------------------------------------------------------------------
/src/main/resources/icons/color.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Chentai-Kao/call-graph-plugin/a8f8b2955502deb0e1eff485e0f03b75eb4a93c8/src/main/resources/icons/color.png
--------------------------------------------------------------------------------
/src/main/resources/icons/compress-x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Chentai-Kao/call-graph-plugin/a8f8b2955502deb0e1eff485e0f03b75eb4a93c8/src/main/resources/icons/compress-x.png
--------------------------------------------------------------------------------
/src/main/resources/icons/compress-y.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Chentai-Kao/call-graph-plugin/a8f8b2955502deb0e1eff485e0f03b75eb4a93c8/src/main/resources/icons/compress-y.png
--------------------------------------------------------------------------------
/src/main/resources/icons/downstream.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Chentai-Kao/call-graph-plugin/a8f8b2955502deb0e1eff485e0f03b75eb4a93c8/src/main/resources/icons/downstream.png
--------------------------------------------------------------------------------
/src/main/resources/icons/expand-x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Chentai-Kao/call-graph-plugin/a8f8b2955502deb0e1eff485e0f03b75eb4a93c8/src/main/resources/icons/expand-x.png
--------------------------------------------------------------------------------
/src/main/resources/icons/expand-y.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Chentai-Kao/call-graph-plugin/a8f8b2955502deb0e1eff485e0f03b75eb4a93c8/src/main/resources/icons/expand-y.png
--------------------------------------------------------------------------------
/src/main/resources/icons/filter.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Chentai-Kao/call-graph-plugin/a8f8b2955502deb0e1eff485e0f03b75eb4a93c8/src/main/resources/icons/filter.png
--------------------------------------------------------------------------------
/src/main/resources/icons/fit-ratio.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Chentai-Kao/call-graph-plugin/a8f8b2955502deb0e1eff485e0f03b75eb4a93c8/src/main/resources/icons/fit-ratio.png
--------------------------------------------------------------------------------
/src/main/resources/icons/fit-viewport.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Chentai-Kao/call-graph-plugin/a8f8b2955502deb0e1eff485e0f03b75eb4a93c8/src/main/resources/icons/fit-viewport.png
--------------------------------------------------------------------------------
/src/main/resources/icons/icon.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/src/main/resources/icons/navigation.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Chentai-Kao/call-graph-plugin/a8f8b2955502deb0e1eff485e0f03b75eb4a93c8/src/main/resources/icons/navigation.png
--------------------------------------------------------------------------------
/src/main/resources/icons/node-selection.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Chentai-Kao/call-graph-plugin/a8f8b2955502deb0e1eff485e0f03b75eb4a93c8/src/main/resources/icons/node-selection.png
--------------------------------------------------------------------------------
/src/main/resources/icons/play.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Chentai-Kao/call-graph-plugin/a8f8b2955502deb0e1eff485e0f03b75eb4a93c8/src/main/resources/icons/play.png
--------------------------------------------------------------------------------
/src/main/resources/icons/search.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Chentai-Kao/call-graph-plugin/a8f8b2955502deb0e1eff485e0f03b75eb4a93c8/src/main/resources/icons/search.png
--------------------------------------------------------------------------------
/src/main/resources/icons/statistics.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Chentai-Kao/call-graph-plugin/a8f8b2955502deb0e1eff485e0f03b75eb4a93c8/src/main/resources/icons/statistics.png
--------------------------------------------------------------------------------
/src/main/resources/icons/upstream-downstream.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Chentai-Kao/call-graph-plugin/a8f8b2955502deb0e1eff485e0f03b75eb4a93c8/src/main/resources/icons/upstream-downstream.png
--------------------------------------------------------------------------------
/src/main/resources/icons/upstream.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Chentai-Kao/call-graph-plugin/a8f8b2955502deb0e1eff485e0f03b75eb4a93c8/src/main/resources/icons/upstream.png
--------------------------------------------------------------------------------
/src/main/resources/icons/view-source-code.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Chentai-Kao/call-graph-plugin/a8f8b2955502deb0e1eff485e0f03b75eb4a93c8/src/main/resources/icons/view-source-code.png
--------------------------------------------------------------------------------
/src/main/resources/icons/view.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Chentai-Kao/call-graph-plugin/a8f8b2955502deb0e1eff485e0f03b75eb4a93c8/src/main/resources/icons/view.png
--------------------------------------------------------------------------------