Skip to content

Commit c864bbe

Browse files
committed
feat(github): add GitHub issues browser action #408
Add ShowGitHubIssuesAction to display and select GitHub issues from current repository with popup interface and detailed issue information display.
1 parent dde0c55 commit c864bbe

File tree

2 files changed

+194
-0
lines changed

2 files changed

+194
-0
lines changed

core/src/233/main/resources/META-INF/autodev-core.xml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -437,6 +437,14 @@
437437
<add-to-group group-id="EditorPopupMenu" anchor="first"/>
438438
</group>
439439

440+
<!-- <action id="autodev.Vcs.ShowGitHubIssues"-->
441+
<!-- class="cc.unitmesh.devti.actions.github.ShowGitHubIssuesAction"-->
442+
<!-- icon="cc.unitmesh.devti.AutoDevIcons.AI_COPILOT"-->
443+
<!-- description="Fetch GitHub issues for AI">-->
444+
445+
<!-- <add-to-group group-id="Vcs.MessageActionGroup"/>-->
446+
<!-- </action>-->
447+
440448
<action id="cc.unitmesh.devti.QuickAssistant"
441449
class="cc.unitmesh.devti.actions.quick.QuickAssistantAction"
442450
description="You can custom any assistant as you want!"
Lines changed: 186 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,186 @@
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("\nDescription:")
180+
appendLine(issue.body)
181+
}
182+
}
183+
184+
Messages.showInfoMessage(project, message, "GitHub Issue Details")
185+
}
186+
}

0 commit comments

Comments
 (0)