Skip to content

Commit e16400a

Browse files
committed
feat: add token tree panel
1 parent 7ae7804 commit e16400a

25 files changed

+575
-77
lines changed

build.gradle.kts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ plugins {
1010
alias(libs.plugins.changelog) // Gradle Changelog Plugin
1111
alias(libs.plugins.qodana) // Gradle Qodana Plugin
1212
alias(libs.plugins.kover) // Gradle Kover Plugin
13+
kotlin("plugin.serialization") version "2.1.20"
1314
}
1415

1516
group = providers.gradleProperty("pluginGroup").get()
@@ -32,6 +33,8 @@ repositories {
3233

3334
// Dependencies are managed with Gradle version catalog - read more: https://docs.gradle.org/current/userguide/platforms.html#sub:version-catalog
3435
dependencies {
36+
compileOnly("org.jetbrains.kotlinx:kotlinx-serialization-json:1.8.1")
37+
3538
testImplementation(libs.junit)
3639
testImplementation(libs.opentest4j)
3740

gradle.properties

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ pluginGroup = com.github.xepozz.php_dump
44
pluginName = PHP Dump
55
pluginRepositoryUrl = https://github.com/xepozz/php-opcodes-plugin
66
# SemVer format -> https://semver.org
7-
pluginVersion = 2025.0.2
7+
pluginVersion = 2025.0.3
88

99
# Supported build number ranges and IntelliJ Platform versions -> https://plugins.jetbrains.com/docs/intellij/build-number-ranges.html
1010
pluginSinceBuild = 231

playground/simple.php

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
<?php
2+
3+
echo 1;

src/main/kotlin/com/github/xepozz/php_dump/CompositeWindowFactory.kt

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
package com.github.xepozz.php_dump
22

33
import com.github.xepozz.php_dump.panel.OpcodesTerminalPanel
4+
import com.github.xepozz.php_dump.panel.TokenTreePanel
45
import com.github.xepozz.php_dump.panel.TokensObjectTerminalPanel
6+
import com.github.xepozz.php_dump.panel.TokensObjectTreeTerminalPanel
57
import com.github.xepozz.php_dump.panel.TokensTerminalPanel
68
import com.intellij.openapi.project.DumbAware
79
import com.intellij.openapi.project.Project
@@ -17,6 +19,9 @@ open class CompositeWindowFactory : ToolWindowFactory, DumbAware {
1719
val opcodesTerminalLayout = OpcodesTerminalPanel(project)
1820
val tokensTerminalLayout = TokensTerminalPanel(project)
1921
val tokensObjectTerminalLayout = TokensObjectTerminalPanel(project)
22+
val tokensObjectTreeTerminalLayout = TokensObjectTreeTerminalPanel(project)
23+
24+
val treeWindow = TokenTreePanel(project)
2025

2126
contentFactory.apply {
2227
this.createContent(opcodesTerminalLayout, "Opcodes", false).apply {
@@ -43,6 +48,22 @@ open class CompositeWindowFactory : ToolWindowFactory, DumbAware {
4348
}
4449
)
4550
}
51+
this.createContent(tokensObjectTreeTerminalLayout, "Tokens Object Tree", false).apply {
52+
contentManager.addContent(
53+
this.apply {
54+
this.isPinnable = true
55+
this.isCloseable = false
56+
}
57+
)
58+
}
59+
this.createContent(treeWindow.component, "Tree panel", false).apply {
60+
contentManager.addContent(
61+
this.apply {
62+
this.isPinnable = true
63+
this.isCloseable = false
64+
}
65+
)
66+
}
4667
}
4768
}
4869
}
Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
package com.github.xepozz.php_dump
22

33
import com.intellij.openapi.util.IconLoader
4-
import kotlin.jvm.java
54

65
object PhpDumpIcons {
76
@JvmStatic
87
val POT = IconLoader.getIcon("/icons/pot.svg", PhpDumpIcons::class.java)
8+
@JvmStatic
9+
val POT_DARK = IconLoader.getIcon("/icons/pot_dark.svg", PhpDumpIcons::class.java)
910
}
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
package com.github.xepozz.php_dump
2+
3+
import com.intellij.ide.plugins.PluginManagerCore.isUnitTestMode
4+
import com.intellij.openapi.Disposable
5+
import com.intellij.openapi.application.ModalityState
6+
import com.intellij.openapi.application.ReadAction
7+
import com.intellij.openapi.components.Service
8+
import com.intellij.openapi.project.Project
9+
import com.intellij.util.concurrency.AppExecutorUtil
10+
import java.util.concurrent.Callable
11+
12+
13+
inline fun <R> Project.nonBlocking(crossinline block: () -> R, crossinline uiContinuation: (R) -> Unit) {
14+
if (isUnitTestMode) {
15+
val result = block()
16+
uiContinuation(result)
17+
} else {
18+
ReadAction.nonBlocking(Callable { block() })
19+
.inSmartMode(this)
20+
.expireWith(DumpPluginDisposable.getInstance(this))
21+
.finishOnUiThread(ModalityState.current()) { uiContinuation(it) }
22+
.submit(AppExecutorUtil.getAppExecutorService())
23+
}
24+
}
25+
26+
@Service(Service.Level.PROJECT)
27+
class DumpPluginDisposable : Disposable {
28+
companion object {
29+
@JvmStatic
30+
fun getInstance(project: Project): Disposable = project.getService(DumpPluginDisposable::class.java)
31+
}
32+
33+
override fun dispose() {}
34+
}

src/main/kotlin/com/github/xepozz/php_dump/actions/RunDumpCommandAction.kt

Lines changed: 0 additions & 22 deletions
This file was deleted.

src/main/kotlin/com/github/xepozz/php_dump/actions/RunDumpTokensCommandAction.kt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import com.intellij.openapi.actionSystem.ActionUpdateThread
66
import com.intellij.openapi.actionSystem.AnAction
77
import com.intellij.openapi.actionSystem.AnActionEvent
88
import com.intellij.openapi.fileEditor.FileEditorManager
9+
import kotlinx.coroutines.runBlocking
910

1011
class RunDumpTokensCommandAction(
1112
val dumpService: DumperServiceInterface
@@ -17,7 +18,7 @@ class RunDumpTokensCommandAction(
1718
val file = editor.virtualFile ?: return
1819
println("file $file")
1920

20-
dumpService.dump(file)
21+
runBlocking { dumpService.dump(file) }
2122
}
2223

2324
override fun getActionUpdateThread() = ActionUpdateThread.BGT

src/main/kotlin/com/github/xepozz/php_dump/panel/OpcodesTerminalPanel.kt

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,14 @@
11
package com.github.xepozz.php_dump.panel
22

3-
import com.github.xepozz.php_dump.actions.RunDumpCommandAction
3+
import com.github.xepozz.php_dump.actions.RunDumpTokensCommandAction
44
import com.github.xepozz.php_dump.services.OpcodesDumperService
55
import com.intellij.execution.filters.TextConsoleBuilderFactory
66
import com.intellij.openapi.actionSystem.ActionManager
77
import com.intellij.openapi.actionSystem.DefaultActionGroup
88
import com.intellij.openapi.fileEditor.FileEditorManager
99
import com.intellij.openapi.project.Project
1010
import com.intellij.openapi.ui.SimpleToolWindowPanel
11+
import kotlinx.coroutines.runBlocking
1112
import java.awt.BorderLayout
1213
import java.awt.GridLayout
1314
import java.awt.event.ComponentAdapter
@@ -34,7 +35,7 @@ class OpcodesTerminalPanel(
3435

3536
private fun createToolBar() {
3637
val actionGroup = DefaultActionGroup()
37-
actionGroup.add(RunDumpCommandAction())
38+
actionGroup.add(RunDumpTokensCommandAction(service))
3839
actionGroup.addSeparator()
3940
// actionGroup.add(OpenSettingsAction())
4041

@@ -64,6 +65,6 @@ class OpcodesTerminalPanel(
6465
val editor = FileEditorManager.getInstance(project).selectedTextEditor ?: return
6566
val virtualFile = editor.virtualFile ?: return
6667

67-
service.dump(virtualFile.path)
68+
runBlocking { service.dump(virtualFile) }
6869
}
6970
}
Lines changed: 185 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,185 @@
1+
package com.github.xepozz.php_dump.panel
2+
3+
import com.github.xepozz.php_dump.actions.RunDumpTokensCommandAction
4+
import com.github.xepozz.php_dump.nonBlocking
5+
import com.github.xepozz.php_dump.services.TokensObjectTreeDumperService
6+
import com.github.xepozz.php_dump.stubs.token_object.TokensList
7+
import com.github.xepozz.php_dump.tree.RootNode
8+
import com.github.xepozz.php_dump.tree.TokenNode
9+
import com.github.xepozz.php_dump.tree.TokensTreeStructure
10+
import com.intellij.ide.util.treeView.AbstractTreeStructure
11+
import com.intellij.openapi.Disposable
12+
import com.intellij.openapi.actionSystem.ActionManager
13+
import com.intellij.openapi.actionSystem.DefaultActionGroup
14+
import com.intellij.openapi.editor.markup.EffectType
15+
import com.intellij.openapi.editor.markup.HighlighterLayer
16+
import com.intellij.openapi.editor.markup.HighlighterTargetArea
17+
import com.intellij.openapi.editor.markup.TextAttributes
18+
import com.intellij.openapi.fileEditor.FileEditorManager
19+
import com.intellij.openapi.project.Project
20+
import com.intellij.openapi.ui.SimpleToolWindowPanel
21+
import com.intellij.pom.Navigatable
22+
import com.intellij.psi.PsiDocumentManager
23+
import com.intellij.ui.JBColor
24+
import com.intellij.ui.TreeUIHelper
25+
import com.intellij.ui.components.JBScrollPane
26+
import com.intellij.ui.tree.AsyncTreeModel
27+
import com.intellij.ui.tree.StructureTreeModel
28+
import com.intellij.ui.treeStructure.Tree
29+
import com.intellij.util.ui.tree.TreeUtil
30+
import kotlinx.coroutines.runBlocking
31+
import org.jdesktop.swingx.VerticalLayout
32+
import java.awt.BorderLayout
33+
import javax.swing.JPanel
34+
import javax.swing.JProgressBar
35+
import javax.swing.SwingUtilities
36+
import javax.swing.tree.DefaultMutableTreeNode
37+
import javax.swing.tree.DefaultTreeModel
38+
import javax.swing.tree.TreePath
39+
40+
class TokenTreePanel(private val project: Project) :
41+
SimpleToolWindowPanel(false, false),
42+
RefreshablePanel, Disposable {
43+
private val progressBar = JProgressBar()
44+
45+
private val treeModel = StructureTreeModel(TokensTreeStructure(RootNode(null)), this)
46+
private val tree = Tree(DefaultTreeModel(DefaultMutableTreeNode())).apply {
47+
setModel(AsyncTreeModel(treeModel, this@TokenTreePanel))
48+
isRootVisible = true
49+
showsRootHandles = true
50+
51+
TreeUIHelper.getInstance()
52+
.installTreeSpeedSearch(this, { path ->
53+
val treeNode = path.lastPathComponent as? DefaultMutableTreeNode
54+
val tokenNode = treeNode?.userObject as? TokenNode
55+
56+
tokenNode?.node?.value
57+
}, true)
58+
}
59+
val service: TokensObjectTreeDumperService = project.getService(TokensObjectTreeDumperService::class.java)
60+
61+
62+
init {
63+
treeModel.invalidateAsync()
64+
65+
createToolbar()
66+
createContent()
67+
68+
addTreeListeners()
69+
70+
SwingUtilities.invokeLater { refreshData() }
71+
}
72+
73+
fun createToolbar() {
74+
val actionGroup = DefaultActionGroup().apply {
75+
add(RunDumpTokensCommandAction(service))
76+
addSeparator()
77+
}
78+
79+
val actionToolbar = ActionManager.getInstance().createActionToolbar("Tree Toolbar", actionGroup, false)
80+
actionToolbar.targetComponent = this
81+
82+
val toolBarPanel = JPanel(VerticalLayout()).apply {
83+
add(
84+
JPanel(VerticalLayout()).apply {
85+
add(createRefreshButton { refreshData() })
86+
add(createExpandsAll(tree))
87+
add(createCollapseAll(tree))
88+
}
89+
)
90+
91+
add(actionToolbar.component)
92+
}
93+
// searchTextField.addDocumentListener(this)
94+
95+
toolbar = toolBarPanel
96+
}
97+
98+
private fun createContent() {
99+
val responsivePanel = JPanel(BorderLayout())
100+
responsivePanel.add(progressBar, BorderLayout.NORTH)
101+
responsivePanel.add(JBScrollPane(tree))
102+
103+
setContent(responsivePanel)
104+
}
105+
106+
private fun addTreeListeners() {
107+
tree.addTreeSelectionListener { event ->
108+
event.path?.let { navigation(it) }
109+
}
110+
}
111+
112+
private fun navigation(closestPathForLocation: TreePath?) {
113+
val lastUserObject = TreeUtil.getLastUserObject(closestPathForLocation)
114+
if (lastUserObject is TokenNode) {
115+
val fileEditor = FileEditorManager.getInstance(project)
116+
val textEditor = fileEditor.selectedTextEditor!!
117+
val psiFile = PsiDocumentManager.getInstance(project).getPsiFile(textEditor.document)
118+
val node = lastUserObject.node
119+
val element = psiFile?.findElementAt(node.pos)
120+
if (element is Navigatable) {
121+
val markupModel = textEditor.markupModel
122+
val textAttributes = TextAttributes().apply {
123+
effectType = EffectType.BOXED
124+
effectColor = JBColor.RED
125+
backgroundColor = null
126+
}
127+
128+
markupModel.removeAllHighlighters()
129+
markupModel.addRangeHighlighter(
130+
node.pos,
131+
node.endPos,
132+
HighlighterLayer.SELECTION + 1,
133+
textAttributes,
134+
HighlighterTargetArea.EXACT_RANGE
135+
)
136+
}
137+
}
138+
}
139+
140+
private fun refreshData() {
141+
progressBar.setIndeterminate(true)
142+
progressBar.isVisible = true
143+
tree.emptyText.text = "Loading..."
144+
145+
146+
project.nonBlocking({ getViewData() }) { result ->
147+
tree.emptyText.text = "Nothing to show"
148+
rebuildTree(result)
149+
150+
progressBar.setIndeterminate(false)
151+
progressBar.isVisible = false
152+
}
153+
}
154+
155+
private fun rebuildTree(list: TokensList?) {
156+
val treeModel = StructureTreeModel<AbstractTreeStructure>(TokensTreeStructure(RootNode(list)), this)
157+
tree.setModel(AsyncTreeModel(treeModel, this))
158+
tree.setRootVisible(false)
159+
treeModel.invalidateAsync()
160+
161+
TreeUtil.expandAll(tree)
162+
}
163+
164+
private fun getViewData(): TokensList {
165+
val result = TokensList()
166+
val editor = FileEditorManager.getInstance(project).selectedTextEditor ?: return result
167+
val virtualFile = editor.virtualFile ?: return result
168+
169+
val runBlocking = runBlocking {
170+
val a = service.dump(virtualFile)
171+
172+
a
173+
}
174+
println("result is $runBlocking")
175+
176+
return runBlocking as? TokensList ?: result
177+
}
178+
179+
override fun refresh(project: Project) {
180+
refreshData()
181+
}
182+
183+
override fun dispose() {
184+
}
185+
}

0 commit comments

Comments
 (0)