1
+ // Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
2
+ package cc.unitmesh.devti.actions.github
3
+
4
+ import cc.unitmesh.devti.flow.kanban.impl.GitHubIssue
5
+ import com.intellij.icons.AllIcons
6
+ import com.intellij.ide.TextCopyProvider
7
+ import com.intellij.openapi.actionSystem.ActionUpdateThread
8
+ import com.intellij.openapi.actionSystem.AnActionEvent
9
+ import com.intellij.openapi.actionSystem.PlatformDataKeys.COPY_PROVIDER
10
+ import com.intellij.openapi.application.ApplicationManager
11
+ import com.intellij.openapi.project.DumbAwareAction
12
+ import com.intellij.openapi.project.Project
13
+ import com.intellij.openapi.ui.Messages
14
+ import com.intellij.openapi.ui.popup.JBPopup
15
+ import com.intellij.openapi.ui.popup.JBPopupFactory
16
+ import com.intellij.openapi.ui.popup.JBPopupListener
17
+ import com.intellij.openapi.ui.popup.LightweightWindowEvent
18
+ import com.intellij.ui.ColoredListCellRenderer
19
+ import com.intellij.ui.speedSearch.SpeedSearchUtil.applySpeedSearchHighlighting
20
+ import com.intellij.util.containers.nullize
21
+ import org.kohsuke.github.GHIssue
22
+ import org.kohsuke.github.GHIssueState
23
+ import javax.swing.JList
24
+ import javax.swing.ListSelectionModel.SINGLE_SELECTION
25
+
26
+ class ShowGitHubIssuesAction : DumbAwareAction () {
27
+ data class IssueDisplayItem (val issue : GHIssue , val displayText : String )
28
+
29
+ init {
30
+ isEnabledInModalContext = true
31
+ }
32
+
33
+ override fun update (e : AnActionEvent ) {
34
+ val project = e.project
35
+ e.presentation.icon = AllIcons .Vcs .Branch
36
+ e.presentation.text = " Show GitHub Issues"
37
+ e.presentation.description = " Show and select GitHub issues from current repository"
38
+
39
+ e.presentation.isVisible = project != null && isGitHubProject(project)
40
+ e.presentation.isEnabled = e.presentation.isVisible
41
+ }
42
+
43
+ override fun getActionUpdateThread (): ActionUpdateThread = ActionUpdateThread .BGT
44
+
45
+ override fun actionPerformed (e : AnActionEvent ) {
46
+ val project = e.project!!
47
+
48
+ ApplicationManager .getApplication().executeOnPooledThread {
49
+ try {
50
+ val issues = fetchGitHubIssues(project)
51
+ if (issues.isEmpty()) {
52
+ ApplicationManager .getApplication().invokeLater {
53
+ Messages .showInfoMessage(
54
+ project,
55
+ " No issues found in this GitHub repository." ,
56
+ " GitHub Issues"
57
+ )
58
+ }
59
+ return @executeOnPooledThread
60
+ }
61
+
62
+ ApplicationManager .getApplication().invokeLater {
63
+ createIssuesPopup(project, issues).showInBestPositionFor(e.dataContext)
64
+ }
65
+ } catch (ex: Exception ) {
66
+ ApplicationManager .getApplication().invokeLater {
67
+ Messages .showErrorDialog(
68
+ project,
69
+ " Failed to fetch GitHub issues: ${ex.message} " ,
70
+ " GitHub Issues Error"
71
+ )
72
+ }
73
+ }
74
+ }
75
+ }
76
+
77
+ private fun isGitHubProject (project : Project ): Boolean {
78
+ return try {
79
+ GitHubIssue .parseGitHubRepository(project) != null
80
+ } catch (e: Exception ) {
81
+ false
82
+ }
83
+ }
84
+
85
+ private fun fetchGitHubIssues (project : Project ): List <IssueDisplayItem > {
86
+ val ghRepository = GitHubIssue .parseGitHubRepository(project)
87
+ ? : throw IllegalStateException (" Not a GitHub repository" )
88
+
89
+ val repoUrl = " ${ghRepository.ownerName} /${ghRepository.name} "
90
+
91
+ val repository = GitHubIssue .createGitHubConnection(project).getRepository(repoUrl)
92
+ val issues = repository.getIssues(GHIssueState .OPEN ).toList()
93
+
94
+ return issues.map { issue ->
95
+ val displayText = " #${issue.number} - ${issue.title} "
96
+ IssueDisplayItem (issue, displayText)
97
+ }
98
+ }
99
+
100
+ private fun createIssuesPopup (project : Project , issues : List <IssueDisplayItem >): JBPopup {
101
+ var chosenIssue: IssueDisplayItem ? = null
102
+ var selectedIssue: IssueDisplayItem ? = null
103
+
104
+ return JBPopupFactory .getInstance().createPopupChooserBuilder(issues)
105
+ .setTitle(" Select GitHub Issue" )
106
+ .setVisibleRowCount(10 )
107
+ .setSelectionMode(SINGLE_SELECTION )
108
+ .setItemSelectedCallback { selectedIssue = it }
109
+ .setItemChosenCallback { chosenIssue = it }
110
+ .setRenderer(object : ColoredListCellRenderer <IssueDisplayItem >() {
111
+ override fun customizeCellRenderer (
112
+ list : JList <out IssueDisplayItem >,
113
+ value : IssueDisplayItem ,
114
+ index : Int ,
115
+ selected : Boolean ,
116
+ hasFocus : Boolean
117
+ ) {
118
+ // Show issue number and title
119
+ append(" #${value.issue.number} " , com.intellij.ui.SimpleTextAttributes .GRAYED_ATTRIBUTES )
120
+ append(value.issue.title)
121
+
122
+ // Add labels if any
123
+ val labels = value.issue.labels.map { it.name }
124
+ if (labels.isNotEmpty()) {
125
+ append(" " )
126
+ labels.forEach { label ->
127
+ append(" [$label ] " , com.intellij.ui.SimpleTextAttributes .GRAY_ITALIC_ATTRIBUTES )
128
+ }
129
+ }
130
+
131
+ applySpeedSearchHighlighting(list, this , true , selected)
132
+ }
133
+ })
134
+ .addListener(object : JBPopupListener {
135
+ override fun onClosed (event : LightweightWindowEvent ) {
136
+ chosenIssue?.let { issue ->
137
+ handleIssueSelection(project, issue)
138
+ }
139
+ }
140
+ })
141
+ .setNamerForFiltering { it.displayText }
142
+ .setAutoPackHeightOnFiltering(true )
143
+ .createPopup()
144
+ .apply {
145
+ setDataProvider { dataId ->
146
+ when (dataId) {
147
+ // default list action does not work as "CopyAction" is invoked first, but with other copy provider
148
+ COPY_PROVIDER .name -> object : TextCopyProvider () {
149
+ override fun getActionUpdateThread (): ActionUpdateThread = ActionUpdateThread .EDT
150
+ override fun getTextLinesToCopy () = listOfNotNull(selectedIssue?.displayText).nullize()
151
+ }
152
+ else -> null
153
+ }
154
+ }
155
+ }
156
+ }
157
+
158
+ private fun handleIssueSelection (project : Project , issueItem : IssueDisplayItem ) {
159
+ val issue = issueItem.issue
160
+ val message = buildString {
161
+ appendLine(" Selected Issue: #${issue.number} " )
162
+ appendLine(" Title: ${issue.title} " )
163
+ appendLine(" State: ${issue.state} " )
164
+ appendLine(" Author: ${issue.user?.login ? : " Unknown" } " )
165
+ issue.assignees?.let { assignees ->
166
+ if (assignees.isNotEmpty()) {
167
+ appendLine(" Assignees: ${assignees.joinToString(" , " ) { it.login }} " )
168
+ }
169
+ }
170
+ val labels = issue.labels.map { it.name }
171
+ if (labels.isNotEmpty()) {
172
+ appendLine(" Labels: ${labels.joinToString(" , " )} " )
173
+ }
174
+ issue.milestone?.let { milestone ->
175
+ appendLine(" Milestone: ${milestone.title} " )
176
+ }
177
+ appendLine(" URL: ${issue.htmlUrl} " )
178
+ if (! issue.body.isNullOrBlank()) {
179
+ appendLine(" \n Description:" )
180
+ appendLine(issue.body)
181
+ }
182
+ }
183
+
184
+ Messages .showInfoMessage(project, message, " GitHub Issue Details" )
185
+ }
186
+ }
0 commit comments