Skip to content

Commit 813bf7d

Browse files
committed
feat: add custom tabs
1 parent ca3d19d commit 813bf7d

File tree

7 files changed

+356
-6
lines changed

7 files changed

+356
-6
lines changed
Lines changed: 21 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,29 @@
11
package com.github.xepozz.php_dump
22

3+
import com.github.xepozz.php_dump.actions.CreateTabAction
34
import com.github.xepozz.php_dump.panel.OpcacheSettingsPanel
45
import com.github.xepozz.php_dump.panel.OpcodesTerminalPanel
56
import com.github.xepozz.php_dump.panel.TokenTreePanel
67
import com.github.xepozz.php_dump.panel.TokensTerminalPanel
8+
import com.github.xepozz.php_dump.panel.tabs.CompositeWindowTabsState
9+
import com.github.xepozz.php_dump.panel.tabs.TabsUtil
710
import com.intellij.openapi.project.DumbAware
11+
import com.intellij.openapi.project.DumbService
812
import com.intellij.openapi.project.Project
913
import com.intellij.openapi.wm.ToolWindow
14+
import com.intellij.openapi.wm.ToolWindowContentUiType
1015
import com.intellij.openapi.wm.ToolWindowFactory
16+
import com.intellij.openapi.wm.ex.ToolWindowEx
1117
import com.intellij.ui.content.ContentFactory
18+
import com.intellij.ui.content.impl.ContentManagerImpl
1219

1320
open class CompositeWindowFactory : ToolWindowFactory, DumbAware {
1421
override fun createToolWindowContent(project: Project, toolWindow: ToolWindow) {
22+
val toolWindow = toolWindow as ToolWindowEx
1523
val contentFactory = ContentFactory.getInstance()
16-
val contentManager = toolWindow.contentManager
24+
val contentManager = toolWindow.contentManager as ContentManagerImpl
25+
println("can close: ${contentManager.canCloseContents()}")
26+
val tabsState = CompositeWindowTabsState.getInstance(project)
1727

1828
val opcodesTerminalLayout = OpcodesTerminalPanel(project)
1929
val opcodesSettingsLayout = OpcacheSettingsPanel(project)
@@ -24,27 +34,32 @@ open class CompositeWindowFactory : ToolWindowFactory, DumbAware {
2434
createContent(opcodesTerminalLayout, "Opcodes", false).apply {
2535
isPinnable = true
2636
isCloseable = false
27-
2837
contentManager.addContent(this)
2938
}
3039
createContent(opcodesSettingsLayout, "Opcache", false).apply {
3140
isPinnable = true
3241
isCloseable = false
33-
3442
contentManager.addContent(this)
3543
}
3644
createContent(tokensTerminalLayout, "Plain Tokens", false).apply {
3745
isPinnable = true
3846
isCloseable = false
39-
4047
contentManager.addContent(this)
4148
}
4249
createContent(tokenTreeLayout.component, "Tokens Tree", false).apply {
4350
isPinnable = true
4451
isCloseable = false
45-
4652
contentManager.addContent(this)
4753
}
54+
55+
DumbService.getInstance(project).runWhenSmart {
56+
tabsState.state.tabs.forEach { tabConfig ->
57+
TabsUtil.createTab(project, contentManager, tabConfig)
58+
}
59+
}
4860
}
61+
62+
toolWindow.setTabActions(CreateTabAction(project, contentManager))
63+
toolWindow.setDefaultContentUiType(ToolWindowContentUiType.COMBO)
4964
}
50-
}
65+
}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
package com.github.xepozz.php_dump.actions
2+
3+
import com.github.xepozz.php_dump.panel.tabs.TabsUtil
4+
import com.intellij.icons.AllIcons
5+
import com.intellij.openapi.actionSystem.AnAction
6+
import com.intellij.openapi.actionSystem.AnActionEvent
7+
import com.intellij.openapi.project.Project
8+
import com.intellij.ui.content.ContentManager
9+
10+
class CreateTabAction(
11+
private val project: Project,
12+
private val contentManager: ContentManager
13+
) : AnAction("Add Tab", "Add new tab", AllIcons.General.Add) {
14+
override fun actionPerformed(e: AnActionEvent) {
15+
TabsUtil.createTab(project, contentManager)
16+
}
17+
}
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.CollapseTreeAction
4+
import com.github.xepozz.php_dump.actions.ExpandTreeAction
5+
import com.github.xepozz.php_dump.actions.OpenPhpSettingsAction
6+
import com.github.xepozz.php_dump.actions.RefreshAction
7+
import com.github.xepozz.php_dump.panel.tabs.CompositeWindowTabsState
8+
import com.github.xepozz.php_dump.services.CustomTreeDumperService
9+
import com.github.xepozz.php_dump.stubs.token_object.TokensList
10+
import com.github.xepozz.php_dump.tree.RootNode
11+
import com.github.xepozz.php_dump.tree.TokenNode
12+
import com.github.xepozz.php_dump.tree.TokensTreeStructure
13+
import com.intellij.ide.util.treeView.AbstractTreeStructure
14+
import com.intellij.openapi.Disposable
15+
import com.intellij.openapi.actionSystem.ActionManager
16+
import com.intellij.openapi.actionSystem.DefaultActionGroup
17+
import com.intellij.openapi.application.EDT
18+
import com.intellij.openapi.editor.markup.EffectType
19+
import com.intellij.openapi.editor.markup.HighlighterLayer
20+
import com.intellij.openapi.editor.markup.HighlighterTargetArea
21+
import com.intellij.openapi.editor.markup.TextAttributes
22+
import com.intellij.openapi.fileEditor.FileEditorManager
23+
import com.intellij.openapi.project.Project
24+
import com.intellij.openapi.ui.SimpleToolWindowPanel
25+
import com.intellij.pom.Navigatable
26+
import com.intellij.psi.PsiDocumentManager
27+
import com.intellij.ui.JBColor
28+
import com.intellij.ui.TreeUIHelper
29+
import com.intellij.ui.components.JBScrollPane
30+
import com.intellij.ui.tree.AsyncTreeModel
31+
import com.intellij.ui.tree.StructureTreeModel
32+
import com.intellij.ui.treeStructure.Tree
33+
import com.intellij.util.ui.tree.TreeUtil
34+
import kotlinx.coroutines.CoroutineScope
35+
import kotlinx.coroutines.Dispatchers
36+
import kotlinx.coroutines.launch
37+
import java.awt.BorderLayout
38+
import java.awt.GridLayout
39+
import javax.swing.JPanel
40+
import javax.swing.JProgressBar
41+
import javax.swing.SwingUtilities
42+
import javax.swing.tree.DefaultMutableTreeNode
43+
import javax.swing.tree.DefaultTreeModel
44+
import javax.swing.tree.TreePath
45+
46+
class CustomTreePanel(
47+
private val tabConfig: CompositeWindowTabsState.TabConfig,
48+
private val project: Project,
49+
) :
50+
SimpleToolWindowPanel(false, false),
51+
RefreshablePanel, Disposable {
52+
val fileEditorManager = FileEditorManager.getInstance(project)
53+
private val progressBar = JProgressBar()
54+
55+
private val treeModel = StructureTreeModel(TokensTreeStructure(RootNode(null)), this)
56+
private val tree = Tree(DefaultTreeModel(DefaultMutableTreeNode())).apply {
57+
setModel(AsyncTreeModel(treeModel, this@CustomTreePanel))
58+
isRootVisible = true
59+
showsRootHandles = true
60+
61+
TreeUIHelper.getInstance()
62+
.installTreeSpeedSearch(this, { path ->
63+
val treeNode = path.lastPathComponent as? DefaultMutableTreeNode
64+
val tokenNode = treeNode?.userObject as? TokenNode
65+
66+
tokenNode?.node?.value
67+
}, true)
68+
}
69+
val service: CustomTreeDumperService = project.getService(CustomTreeDumperService::class.java)
70+
71+
72+
init {
73+
treeModel.invalidateAsync()
74+
75+
createToolbar()
76+
createContent()
77+
78+
addTreeListeners()
79+
80+
SwingUtilities.invokeLater { refreshData() }
81+
}
82+
83+
fun createToolbar() {
84+
val actionGroup = DefaultActionGroup().apply {
85+
add(RefreshAction { refreshData() })
86+
addSeparator()
87+
add(ExpandTreeAction(tree))
88+
add(CollapseTreeAction(tree))
89+
add(OpenPhpSettingsAction())
90+
}
91+
92+
val actionToolbar = ActionManager.getInstance().createActionToolbar("Tree Toolbar", actionGroup, false)
93+
actionToolbar.targetComponent = this
94+
95+
val toolBarPanel = JPanel(GridLayout())
96+
toolBarPanel.add(actionToolbar.component)
97+
98+
toolbar = toolBarPanel
99+
}
100+
101+
private fun createContent() {
102+
val responsivePanel = JPanel(BorderLayout())
103+
responsivePanel.add(progressBar, BorderLayout.NORTH)
104+
responsivePanel.add(JBScrollPane(tree), BorderLayout.CENTER)
105+
106+
setContent(responsivePanel)
107+
}
108+
109+
private fun addTreeListeners() {
110+
tree.addTreeSelectionListener { event ->
111+
event.path?.let { navigation(it) }
112+
}
113+
}
114+
115+
private fun navigation(closestPathForLocation: TreePath) {
116+
val lastUserObject = TreeUtil.getLastUserObject(closestPathForLocation)
117+
if (lastUserObject is TokenNode) {
118+
val fileEditor = FileEditorManager.getInstance(project)
119+
val textEditor = fileEditor.selectedTextEditor!!
120+
val psiFile = PsiDocumentManager.getInstance(project).getPsiFile(textEditor.document)
121+
val node = lastUserObject.node
122+
val element = psiFile?.findElementAt(node.pos)
123+
if (element is Navigatable) {
124+
val markupModel = textEditor.markupModel
125+
val textAttributes = TextAttributes().apply {
126+
effectType = EffectType.BOXED
127+
effectColor = JBColor.RED
128+
backgroundColor = null
129+
}
130+
131+
markupModel.removeAllHighlighters()
132+
markupModel.addRangeHighlighter(
133+
node.pos,
134+
node.endPos,
135+
HighlighterLayer.SELECTION + 1,
136+
textAttributes,
137+
HighlighterTargetArea.EXACT_RANGE
138+
)
139+
}
140+
}
141+
}
142+
143+
private fun refreshData() {
144+
CoroutineScope(Dispatchers.EDT).launch {
145+
progressBar.setIndeterminate(true)
146+
progressBar.isVisible = true
147+
tree.emptyText.text = "Loading..."
148+
149+
val result = getViewData()
150+
tree.emptyText.text = "Nothing to show"
151+
rebuildTree(result)
152+
153+
progressBar.setIndeterminate(false)
154+
progressBar.isVisible = false
155+
}
156+
}
157+
158+
private fun rebuildTree(list: TokensList?) {
159+
val treeModel = StructureTreeModel<AbstractTreeStructure>(TokensTreeStructure(RootNode(list)), this)
160+
tree.setModel(AsyncTreeModel(treeModel, this))
161+
tree.setRootVisible(false)
162+
treeModel.invalidateAsync()
163+
164+
TreeUtil.expandAll(tree)
165+
}
166+
167+
private suspend fun getViewData(): TokensList {
168+
val result = TokensList()
169+
val editor = fileEditorManager.selectedTextEditor ?: return result
170+
val virtualFile = editor.virtualFile ?: return result
171+
172+
service.phpSnippet = tabConfig.snippet
173+
val runBlocking = service.dump(virtualFile)
174+
// println("result is $runBlocking")
175+
176+
return runBlocking as? TokensList ?: result
177+
}
178+
179+
override fun refresh(project: Project, type: RefreshType) {
180+
refreshData()
181+
}
182+
183+
override fun dispose() {
184+
}
185+
}
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
package com.github.xepozz.php_dump.panel.tabs
2+
3+
import com.intellij.openapi.components.PersistentStateComponent
4+
import com.intellij.openapi.components.Service
5+
import com.intellij.openapi.components.State
6+
import com.intellij.openapi.components.Storage
7+
import com.intellij.openapi.project.Project
8+
9+
@Service(Service.Level.PROJECT)
10+
@State(
11+
name = "CompositeWindowTabs",
12+
storages = [Storage("php-dump.tabs.xml")]
13+
)
14+
class CompositeWindowTabsState : PersistentStateComponent<CompositeWindowTabsState.State> {
15+
data class State(
16+
var tabs: MutableList<TabConfig> = mutableListOf()
17+
)
18+
19+
data class TabConfig(
20+
var name: String = "",
21+
var snippet: String = ""
22+
)
23+
24+
private var state = State()
25+
26+
override fun getState(): State = state
27+
28+
override fun loadState(state: State) {
29+
this.state = state
30+
}
31+
32+
companion object {
33+
fun getInstance(project: Project) = project.getService(CompositeWindowTabsState::class.java)
34+
}
35+
}
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
package com.github.xepozz.php_dump.panel.tabs
2+
3+
import com.github.xepozz.php_dump.panel.CustomTreePanel
4+
import com.intellij.openapi.project.Project
5+
import com.intellij.ui.content.ContentFactory
6+
import com.intellij.ui.content.ContentManager
7+
8+
object TabsUtil {
9+
// language=injectablephp
10+
private val SNIPPET = $$"""
11+
/**
12+
* $argv[0] – Program name
13+
* $argv[1] – File path
14+
*/
15+
16+
echo json_encode(
17+
array_map(
18+
function (PhpToken $token) {
19+
return [
20+
'line' => $token->line,
21+
'pos' => $token->pos,
22+
'name' => $token->getTokenName(),
23+
'value' => $token->text,
24+
];
25+
},
26+
\PhpToken::tokenize(file_get_contents($argv[1])),
27+
)
28+
);
29+
""".trimIndent()
30+
31+
fun createTab(project: Project, contentManager: ContentManager) {
32+
val tabsState = CompositeWindowTabsState.getInstance(project)
33+
val tabNumber = tabsState.state.tabs.size + 1
34+
val tabName = "Dump $tabNumber"
35+
val tabConfig = CompositeWindowTabsState.TabConfig(
36+
name = tabName,
37+
snippet = SNIPPET,
38+
)
39+
40+
createTab(project, contentManager, tabConfig)
41+
}
42+
43+
fun createTab(project: Project, contentManager: ContentManager, tabConfig: CompositeWindowTabsState.TabConfig) {
44+
val contentFactory = ContentFactory.getInstance()
45+
val tabsState = CompositeWindowTabsState.getInstance(project)
46+
tabsState.state.tabs.add(tabConfig)
47+
48+
val panel = CustomTreePanel(tabConfig, project)
49+
val content = contentFactory.createContent(panel, tabConfig.name, false)
50+
content.isCloseable = true
51+
52+
contentManager.addContent(content)
53+
contentManager.setSelectedContent(content)
54+
}
55+
}

0 commit comments

Comments
 (0)