diff --git a/plugin-idea/build.gradle.kts b/plugin-idea/build.gradle.kts index 67cf2cb..ffb6db8 100644 --- a/plugin-idea/build.gradle.kts +++ b/plugin-idea/build.gradle.kts @@ -51,10 +51,10 @@ intellijPlatform { dependencies { intellijPlatform { - val type = providers.gradleProperty("platformType") - val version = providers.gradleProperty("platformVersion") + val type = providers.gradleProperty("platformType").getOrElse("IC") + val version = providers.gradleProperty("platformVersion").getOrElse("2025.2") - intellijIdea(version) + create(type, version) bundledPlugins(listOf("org.jetbrains.plugins.yaml","Git4Idea")) } @@ -124,4 +124,4 @@ signing { useInMemoryPgpKeys(keyText, keyPass) sign(publishing.publications["pluginDistribution"]) } -} \ No newline at end of file +} diff --git a/plugin-idea/src/main/kotlin/org/openprojectx/ai/plugin/AiTestSettingsConfigurable.kt b/plugin-idea/src/main/kotlin/org/openprojectx/ai/plugin/AiTestSettingsConfigurable.kt index 74bfb16..4d67dca 100644 --- a/plugin-idea/src/main/kotlin/org/openprojectx/ai/plugin/AiTestSettingsConfigurable.kt +++ b/plugin-idea/src/main/kotlin/org/openprojectx/ai/plugin/AiTestSettingsConfigurable.kt @@ -25,6 +25,7 @@ import javax.swing.JTabbedPane import javax.swing.JTextArea import javax.swing.JTextField import javax.swing.SwingConstants +import javax.swing.SwingUtilities class AiTestSettingsConfigurable( private val project: Project @@ -251,19 +252,106 @@ class AiTestSettingsConfigurable( layout = BoxLayout(this, BoxLayout.Y_AXIS) border = BorderFactory.createEmptyBorder(12, 12, 12, 12) add(infoBanner("Built-in prompts remain the defaults. Edit any field below to override the default template saved in .ai-test.yaml.")) - add(formSection("Generation Wrapper", listOf( - "Template" to JScrollPane(generationPromptWrapperField) - ))) - add(formSection("Generation Rules", listOf( - "Rest Assured rules" to JScrollPane(generationPromptRestAssuredField), - "Karate rules" to JScrollPane(generationPromptKarateField) - ))) - add(formSection("AI Actions", listOf( - "Commit message prompt" to JScrollPane(commitPromptField), - "Pull request prompt" to JScrollPane(pullRequestPromptField) - ))) + add(buildPromptManagerPanel()) }).apply { border = BorderFactory.createEmptyBorder() } + private data class PromptItem( + val name: String, + val area: JTextArea, + val defaultValue: String + ) + + private fun buildPromptManagerPanel(): JComponent { + val items = listOf( + PromptItem("Generation Wrapper", generationPromptWrapperField, AiPromptDefaults.GENERATION_WRAPPER), + PromptItem("Rest Assured Rules", generationPromptRestAssuredField, AiPromptDefaults.GENERATION_REST_ASSURED), + PromptItem("Karate Rules", generationPromptKarateField, AiPromptDefaults.GENERATION_KARATE), + PromptItem("Commit Message", commitPromptField, AiPromptDefaults.COMMIT_MESSAGE), + PromptItem("Pull Request", pullRequestPromptField, AiPromptDefaults.PULL_REQUEST) + ) + + val host = JPanel().apply { + layout = BoxLayout(this, BoxLayout.Y_AXIS) + border = BorderFactory.createCompoundBorder( + BorderFactory.createTitledBorder("Prompt Manager"), + BorderFactory.createEmptyBorder(8, 8, 8, 8) + ) + } + + val hiddenNames = linkedSetOf() + val addCombo = JComboBox() + val addButton = JButton("Add Prompt") + + val rowsByName = linkedMapOf() + + fun refreshAddCombo() { + addCombo.removeAllItems() + hiddenNames.forEach { addCombo.addItem(it) } + addCombo.isEnabled = hiddenNames.isNotEmpty() + addButton.isEnabled = hiddenNames.isNotEmpty() + } + + fun addPromptRow(item: PromptItem) { + val titleButton = JButton(item.name).apply { + horizontalAlignment = SwingConstants.LEFT + } + val removeButton = JButton("Delete") + val content = JScrollPane(item.area).apply { isVisible = false } + val header = JPanel(BorderLayout(8, 0)).apply { + add(titleButton, BorderLayout.CENTER) + add(removeButton, BorderLayout.EAST) + } + val row = JPanel(BorderLayout(0, 6)).apply { + border = BorderFactory.createCompoundBorder( + BorderFactory.createMatteBorder(0, 0, 1, 0, java.awt.Color(75, 75, 75)), + BorderFactory.createEmptyBorder(6, 0, 6, 0) + ) + add(header, BorderLayout.NORTH) + add(content, BorderLayout.CENTER) + } + + titleButton.addActionListener { + content.isVisible = !content.isVisible + row.revalidate() + } + removeButton.addActionListener { + item.area.text = item.defaultValue + host.remove(row) + rowsByName.remove(item.name) + hiddenNames.add(item.name) + refreshAddCombo() + host.revalidate() + host.repaint() + } + + rowsByName[item.name] = row + host.add(row) + } + + val addRow = JPanel(BorderLayout(8, 0)).apply { + border = BorderFactory.createEmptyBorder(0, 0, 8, 0) + add(addCombo, BorderLayout.CENTER) + add(addButton, BorderLayout.EAST) + } + + addButton.addActionListener { + val selected = addCombo.selectedItem as? String ?: return@addActionListener + val item = items.firstOrNull { it.name == selected } ?: return@addActionListener + hiddenNames.remove(selected) + addPromptRow(item) + refreshAddCombo() + SwingUtilities.invokeLater { + host.revalidate() + host.repaint() + } + } + + host.add(addRow) + items.forEach { addPromptRow(it) } + refreshAddCombo() + return host + } + private fun formSection(title: String, rows: List>): JComponent { val panel = JPanel(GridBagLayout()) panel.border = BorderFactory.createCompoundBorder( diff --git a/plugin-idea/src/main/kotlin/org/openprojectx/ai/plugin/ContextBoxStateService.kt b/plugin-idea/src/main/kotlin/org/openprojectx/ai/plugin/ContextBoxStateService.kt new file mode 100644 index 0000000..0555391 --- /dev/null +++ b/plugin-idea/src/main/kotlin/org/openprojectx/ai/plugin/ContextBoxStateService.kt @@ -0,0 +1,59 @@ +package org.openprojectx.ai.plugin + +import com.intellij.openapi.components.Service +import com.intellij.openapi.components.service +import com.intellij.openapi.project.Project +import com.intellij.util.messages.Topic +import java.time.LocalDateTime +import java.time.format.DateTimeFormatter + +@Service(Service.Level.PROJECT) +class ContextBoxStateService(private val project: Project) { + + data class Snapshot( + val latestResult: String + ) + + companion object { + val TOPIC: Topic = Topic.create("Context Box Updates", ContextBoxListener::class.java) + + fun getInstance(project: Project): ContextBoxStateService = project.service() + } + + private val formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss") + private var latestResult: String = "No result yet." + + fun snapshot(): Snapshot = Snapshot(latestResult) + + fun recordGeneration(className: String, targetPath: String, diff: String) { + val now = LocalDateTime.now().format(formatter) + latestResult = buildString { + appendLine("Type: Generated Code") + appendLine("Time: $now") + appendLine("Class: $className") + appendLine("Target: $targetPath") + appendLine() + appendLine("Code Diff:") + append(diff.ifBlank { "No diff generated." }) + }.trimEnd() + project.messageBus.syncPublisher(TOPIC).stateUpdated(snapshot()) + } + + fun recordBranchSummary(targetBranch: String, sourceBranch: String, summary: String) { + val now = LocalDateTime.now().format(formatter) + latestResult = buildString { + appendLine("Type: Branch Analysis") + appendLine("Time: $now") + appendLine("Target Branch: $targetBranch") + appendLine("Source Branch: $sourceBranch") + appendLine() + appendLine("Analysis:") + append(summary.trim()) + }.trimEnd() + project.messageBus.syncPublisher(TOPIC).stateUpdated(snapshot()) + } +} + +fun interface ContextBoxListener { + fun stateUpdated(snapshot: ContextBoxStateService.Snapshot) +} diff --git a/plugin-idea/src/main/kotlin/org/openprojectx/ai/plugin/ContextBoxToolWindowFactory.kt b/plugin-idea/src/main/kotlin/org/openprojectx/ai/plugin/ContextBoxToolWindowFactory.kt new file mode 100644 index 0000000..59acef6 --- /dev/null +++ b/plugin-idea/src/main/kotlin/org/openprojectx/ai/plugin/ContextBoxToolWindowFactory.kt @@ -0,0 +1,251 @@ +package org.openprojectx.ai.plugin + +import com.intellij.openapi.project.DumbAware +import com.intellij.openapi.project.Project +import com.intellij.openapi.wm.ToolWindow +import com.intellij.openapi.wm.ToolWindowFactory +import com.intellij.ui.components.JBScrollPane +import com.intellij.ui.content.ContentFactory +import java.awt.BorderLayout +import java.awt.Color +import java.awt.Font +import javax.swing.BorderFactory +import javax.swing.BoxLayout +import javax.swing.JButton +import javax.swing.JComboBox +import javax.swing.JLabel +import javax.swing.JPanel +import javax.swing.JTabbedPane +import javax.swing.JTextArea +import javax.swing.UIManager + +class ContextBoxToolWindowFactory : ToolWindowFactory, DumbAware { + + override fun shouldBeAvailable(project: Project): Boolean = true + + override fun createToolWindowContent(project: Project, toolWindow: ToolWindow) { + val stateService = ContextBoxStateService.getInstance(project) + + val commonFont = UIManager.getFont("Label.font") + ?.deriveFont(Font.PLAIN, 13f) + ?: Font("SansSerif", Font.PLAIN, 13) + val bgColor = Color(0x0D, 0x0D, 0x0D) + val fgColor = Color(0xFF, 0xFF, 0xFF) + val borderColor = Color(0x22, 0x22, 0x22) + + val resultArea = JTextArea().apply { + isEditable = false + lineWrap = true + wrapStyleWord = true + font = commonFont + background = bgColor + foreground = fgColor + caretColor = fgColor + border = BorderFactory.createEmptyBorder(10, 10, 10, 10) + } + + fun styledScrollPane(component: java.awt.Component): JBScrollPane = + JBScrollPane(component).apply { + viewport.background = bgColor + background = bgColor + border = BorderFactory.createLineBorder(borderColor) + } + + val panel = JPanel(BorderLayout()).apply { + val tabs = JTabbedPane().apply { + addTab("Results", styledScrollPane(resultArea)) + addTab("Prompts", styledScrollPane(buildPromptManagerPanel(project, commonFont, bgColor, fgColor, borderColor))) + background = bgColor + foreground = fgColor + font = commonFont + } + add(tabs, BorderLayout.CENTER) + background = bgColor + foreground = fgColor + } + + fun render(snapshot: ContextBoxStateService.Snapshot) { + resultArea.text = snapshot.latestResult + resultArea.caretPosition = 0 + } + + render(stateService.snapshot()) + + project.messageBus.connect(toolWindow.disposable).subscribe( + ContextBoxStateService.TOPIC, + ContextBoxListener { snapshot -> + render(snapshot) + } + ) + + val content = ContentFactory.getInstance().createContent(panel, "", false) + toolWindow.contentManager.addContent(content) + } + + private data class PromptItem( + val name: String, + val area: JTextArea, + val defaultValue: String + ) + + private fun buildPromptManagerPanel( + project: Project, + font: Font, + bgColor: Color, + fgColor: Color, + borderColor: Color + ): JPanel { + val settings = LlmSettingsLoader.loadSettingsModel(project) + val wrapperArea = promptTextArea(settings.generationPromptWrapper, font, bgColor, fgColor) + val restArea = promptTextArea(settings.generationPromptRestAssured, font, bgColor, fgColor) + val karateArea = promptTextArea(settings.generationPromptKarate, font, bgColor, fgColor) + val commitArea = promptTextArea(settings.commitPrompt, font, bgColor, fgColor) + val prArea = promptTextArea(settings.pullRequestPrompt, font, bgColor, fgColor) + + val items = listOf( + PromptItem("Generation Wrapper", wrapperArea, AiPromptDefaults.GENERATION_WRAPPER), + PromptItem("Rest Assured Rules", restArea, AiPromptDefaults.GENERATION_REST_ASSURED), + PromptItem("Karate Rules", karateArea, AiPromptDefaults.GENERATION_KARATE), + PromptItem("Commit Message", commitArea, AiPromptDefaults.COMMIT_MESSAGE), + PromptItem("Pull Request", prArea, AiPromptDefaults.PULL_REQUEST) + ) + + val host = JPanel().apply { + layout = BoxLayout(this, BoxLayout.Y_AXIS) + background = bgColor + foreground = fgColor + border = BorderFactory.createEmptyBorder(8, 8, 8, 8) + } + + val hint = JLabel("Click prompt name to expand.").apply { + foreground = fgColor + this.font = font + } + + val toolbar = JPanel(BorderLayout(8, 0)).apply { + isOpaque = true + background = bgColor + border = BorderFactory.createEmptyBorder(0, 0, 8, 0) + } + val addCombo = JComboBox() + val addButton = JButton("Add") + val saveButton = JButton("Save") + val reloadButton = JButton("Reload") + val hiddenNames = linkedSetOf() + + val rowsByName = linkedMapOf() + + fun refreshAddCombo() { + addCombo.removeAllItems() + hiddenNames.forEach { addCombo.addItem(it) } + addCombo.isEnabled = hiddenNames.isNotEmpty() + addButton.isEnabled = hiddenNames.isNotEmpty() + } + + fun addPromptRow(item: PromptItem) { + val titleButton = JButton(item.name).apply { + horizontalAlignment = javax.swing.SwingConstants.LEFT + } + val removeButton = JButton("Delete") + val content = JBScrollPane(item.area).apply { + viewport.background = bgColor + background = bgColor + border = BorderFactory.createLineBorder(borderColor) + isVisible = false + } + val header = JPanel(BorderLayout(8, 0)).apply { + isOpaque = true + background = bgColor + add(titleButton, BorderLayout.CENTER) + add(removeButton, BorderLayout.EAST) + } + val row = JPanel(BorderLayout(0, 6)).apply { + isOpaque = true + background = bgColor + border = BorderFactory.createCompoundBorder( + BorderFactory.createMatteBorder(0, 0, 1, 0, borderColor), + BorderFactory.createEmptyBorder(6, 0, 6, 0) + ) + add(header, BorderLayout.NORTH) + add(content, BorderLayout.CENTER) + } + + titleButton.addActionListener { + content.isVisible = !content.isVisible + row.revalidate() + } + + removeButton.addActionListener { + item.area.text = item.defaultValue + host.remove(row) + rowsByName.remove(item.name) + hiddenNames.add(item.name) + refreshAddCombo() + host.revalidate() + host.repaint() + } + + rowsByName[item.name] = row + host.add(row) + } + + addButton.addActionListener { + val selected = addCombo.selectedItem as? String ?: return@addActionListener + val item = items.firstOrNull { it.name == selected } ?: return@addActionListener + hiddenNames.remove(selected) + addPromptRow(item) + refreshAddCombo() + host.revalidate() + host.repaint() + } + + saveButton.addActionListener { + val current = LlmSettingsLoader.loadSettingsModel(project) + val updated = current.copy( + generationPromptWrapper = wrapperArea.text, + generationPromptRestAssured = restArea.text, + generationPromptKarate = karateArea.text, + commitPrompt = commitArea.text, + pullRequestPrompt = prArea.text + ) + LlmSettingsLoader.saveSettingsModel(project, updated) + Notifications.info(project, "Prompt Manager", "Prompts saved.") + } + + reloadButton.addActionListener { + val latest = LlmSettingsLoader.loadSettingsModel(project) + wrapperArea.text = latest.generationPromptWrapper + restArea.text = latest.generationPromptRestAssured + karateArea.text = latest.generationPromptKarate + commitArea.text = latest.commitPrompt + prArea.text = latest.pullRequestPrompt + Notifications.info(project, "Prompt Manager", "Prompts reloaded.") + } + + toolbar.add(addCombo, BorderLayout.CENTER) + toolbar.add(JPanel(BorderLayout(6, 0)).apply { + isOpaque = true + background = bgColor + add(addButton, BorderLayout.WEST) + add(saveButton, BorderLayout.CENTER) + add(reloadButton, BorderLayout.EAST) + }, BorderLayout.EAST) + + host.add(hint) + host.add(toolbar) + items.forEach { addPromptRow(it) } + refreshAddCombo() + return host + } + + private fun promptTextArea(text: String, font: Font, bgColor: Color, fgColor: Color): JTextArea = + JTextArea(text, 8, 60).apply { + lineWrap = true + wrapStyleWord = true + this.font = font + background = bgColor + foreground = fgColor + caretColor = fgColor + border = BorderFactory.createEmptyBorder(8, 8, 8, 8) + } +} diff --git a/plugin-idea/src/main/kotlin/org/openprojectx/ai/plugin/GenerateTestsService.kt b/plugin-idea/src/main/kotlin/org/openprojectx/ai/plugin/GenerateTestsService.kt index 9134b34..1742c10 100644 --- a/plugin-idea/src/main/kotlin/org/openprojectx/ai/plugin/GenerateTestsService.kt +++ b/plugin-idea/src/main/kotlin/org/openprojectx/ai/plugin/GenerateTestsService.kt @@ -90,7 +90,6 @@ class GenerateTestsService(private val project: Project) { indicator.fraction = 0.9 writeGenerated( project = project, - contractFile = file, framework = effectiveFramework, location = effectiveLocation, packageName = packageName, @@ -125,7 +124,6 @@ class GenerateTestsService(private val project: Project) { private fun writeGenerated( project: Project, - contractFile: VirtualFile, framework: Framework, location: String, packageName: String?, @@ -147,6 +145,7 @@ class GenerateTestsService(private val project: Project) { } var targetFile: VirtualFile? = null + var previousContent: String? = null WriteCommandAction.runWriteCommandAction(project) { val targetDir = if (normalizedLocation.isBlank()) { @@ -157,6 +156,7 @@ class GenerateTestsService(private val project: Project) { } val existing = targetDir.findChild(fileName) + previousContent = existing?.inputStream?.bufferedReader(Charsets.UTF_8)?.use { it.readText() } val target = existing ?: targetDir.createChildData(this, fileName) target.setBinaryContent(code.toByteArray(Charsets.UTF_8)) targetFile = target @@ -171,6 +171,37 @@ class GenerateTestsService(private val project: Project) { message = relativePath, file = createdFile ) + + ContextBoxStateService.getInstance(project).recordGeneration( + className = cls, + targetPath = relativePath, + diff = buildCodeDiff(relativePath, previousContent, code) + ) + } + + + private fun buildCodeDiff(path: String, before: String?, after: String): String { + val beforeText = before ?: "" + if (beforeText == after) { + return "No content change for $path" + } + + val beforeLines = beforeText.lines() + val afterLines = after.lines() + + val builder = StringBuilder() + builder.append("--- a/").append(path).append("\n") + builder.append("+++ b/").append(path).append("\n") + builder.append("@@ -1,").append(beforeLines.size).append(" +1,").append(afterLines.size).append(" @@\n") + + if (beforeLines.isNotEmpty()) { + beforeLines.forEach { builder.append('-').append(it).append("\n") } + } + if (afterLines.isNotEmpty()) { + afterLines.forEach { builder.append('+').append(it).append("\n") } + } + + return builder.toString().trimEnd() } private fun resolveOutputLocation( diff --git a/plugin-idea/src/main/kotlin/org/openprojectx/ai/plugin/OpenProjectXIcons.kt b/plugin-idea/src/main/kotlin/org/openprojectx/ai/plugin/OpenProjectXIcons.kt index 6211c73..8fbcd25 100644 --- a/plugin-idea/src/main/kotlin/org/openprojectx/ai/plugin/OpenProjectXIcons.kt +++ b/plugin-idea/src/main/kotlin/org/openprojectx/ai/plugin/OpenProjectXIcons.kt @@ -5,7 +5,7 @@ import com.intellij.openapi.util.IconLoader object OpenProjectXIcons { val GenerateTests = IconLoader.getIcon( - "/icons/aiTestGen.svg", + "/icons/blue-bulb.svg", OpenProjectXIcons::class.java ) -} \ No newline at end of file +} diff --git a/plugin-idea/src/main/kotlin/org/openprojectx/ai/plugin/SummarizeBranchDiffAction.kt b/plugin-idea/src/main/kotlin/org/openprojectx/ai/plugin/SummarizeBranchDiffAction.kt index 59956c7..ba4bd4a 100644 --- a/plugin-idea/src/main/kotlin/org/openprojectx/ai/plugin/SummarizeBranchDiffAction.kt +++ b/plugin-idea/src/main/kotlin/org/openprojectx/ai/plugin/SummarizeBranchDiffAction.kt @@ -1,19 +1,18 @@ package org.openprojectx.ai.plugin import com.intellij.openapi.actionSystem.AnActionEvent -import com.intellij.openapi.actionSystem.CommonDataKeys import com.intellij.openapi.actionSystem.DumbAwareAction import com.intellij.openapi.application.ApplicationManager import com.intellij.openapi.util.IconLoader import com.intellij.openapi.progress.ProgressIndicator import com.intellij.openapi.progress.ProgressManager import com.intellij.openapi.progress.Task -import com.intellij.openapi.ui.Messages import com.intellij.vcs.log.VcsLogDataKeys +import git4idea.repo.GitRepositoryManager open class SummarizeBranchDiffAction( tooltip: String = "Summarize Branch Differences (Default)", - iconPath: String = "/icons/git-probe-02.svg", + iconPath: String = "/icons/blue-bulb.svg", private val sourceTag: String = "default" ) : DumbAwareAction( null, @@ -23,35 +22,33 @@ open class SummarizeBranchDiffAction( override fun actionPerformed(e: AnActionEvent) { val project = e.project ?: return - val branches = resolveComparedBranches(e) - val changedFiles = resolveChangedFiles(e) + val sourceBranch = resolveCurrentBranch(project) + val targetRef = resolveTargetRef(e, sourceBranch) - if (branches == null && changedFiles.isEmpty()) { + if (targetRef == null) { Notifications.warn( project, "Summarize Branch Diff", - "[$sourceTag] Cannot detect diff context. Open a branch/file diff and try again." + "[$sourceTag] Please select a branch or commit in VCS Log to compare with current branch $sourceBranch." ) return } - val sourceBranch = branches?.first ?: "working-tree" - val targetBranch = branches?.second ?: "HEAD" - ProgressManager.getInstance().run(object : Task.Backgroundable(project, "Summarizing Branch Diff", false) { override fun run(indicator: ProgressIndicator) { try { - indicator.text = "Collecting branch diff for $sourceBranch vs $targetBranch..." - val diff = when { - changedFiles.isNotEmpty() -> GitDiffProvider.getDiffForFiles(project, changedFiles) - else -> GitDiffProvider.getDiffBetweenBranches(project, sourceBranch, targetBranch) - } + indicator.text = "Collecting branch diff for $sourceBranch vs $targetRef..." + val diff = GitDiffProvider.getDiffBetweenBranches( + project, + sourceBranch = sourceBranch, + targetBranch = targetRef + ) if (diff.isBlank()) { Notifications.info( project, "Summarize Branch Diff", - "[$sourceTag] No changes found between $sourceBranch and $targetBranch." + "[$sourceTag] No changes found between $sourceBranch and $targetRef." ) return } @@ -59,12 +56,21 @@ open class SummarizeBranchDiffAction( indicator.text = "Generating summary..." val summary = AiBranchDiffSummaryService(project).generate( sourceBranch = sourceBranch, - targetBranch = targetBranch, - diff = buildSummaryInput(diff, changedFiles) + targetBranch = targetRef, + diff = diff ) ApplicationManager.getApplication().invokeLater { - Messages.showInfoMessage(project, summary.trim(), "Branch Diff Summary") + ContextBoxStateService.getInstance(project).recordBranchSummary( + targetBranch = targetRef, + sourceBranch = sourceBranch, + summary = summary + ) + Notifications.info( + project, + "Branch Diff Summary", + "Summary updated in AI Context Box > Branch Analysis." + ) } } catch (ex: Exception) { Notifications.error( @@ -81,22 +87,22 @@ open class SummarizeBranchDiffAction( e.presentation.isEnabledAndVisible = e.project != null } - private fun resolveComparedBranches(e: AnActionEvent): Pair? { + private fun resolveTargetRef(e: AnActionEvent, currentBranch: String): String? { val rawBranches = e.getData(VcsLogDataKeys.VCS_LOG_BRANCHES) as? Collection<*> - ?: return null - - val branches = LinkedHashSet() - for (rawBranch in rawBranches) { - val name = extractBranchName(rawBranch)?.trim().orEmpty() - if (name.isNotEmpty()) { - branches.add(name) + if (rawBranches != null) { + val branches = linkedSetOf() + for (rawBranch in rawBranches) { + val name = extractBranchName(rawBranch)?.trim().orEmpty() + if (name.isNotEmpty()) { + branches.add(name) + } } - } - if (branches.size != 2) return null + branches.firstOrNull { it != currentBranch }?.let { return it } + branches.firstOrNull()?.let { return it } + } - val values = branches.toList() - return Pair(values[0], values[1]) + return resolveSelectedCommitHash(e) } private fun extractBranchName(value: Any?): String? { @@ -109,28 +115,46 @@ open class SummarizeBranchDiffAction( } } - private fun resolveChangedFiles(e: AnActionEvent): List { - val files = linkedSetOf() - e.getData(CommonDataKeys.VIRTUAL_FILE_ARRAY) - ?.mapTo(files) { it.path } + private fun resolveCurrentBranch(project: com.intellij.openapi.project.Project): String { + val repo = GitRepositoryManager.getInstance(project).repositories.firstOrNull() + return repo?.currentBranchName ?: "HEAD" + } - e.getData(CommonDataKeys.VIRTUAL_FILE) - ?.path - ?.let { files.add(it) } + private fun resolveSelectedCommitHash(e: AnActionEvent): String? { + val vcsLog = e.getData(VcsLogDataKeys.VCS_LOG) ?: return null + val selectedCommits = runCatching { + vcsLog.javaClass.getMethod("getSelectedCommits").invoke(vcsLog) as? Collection<*> + }.getOrNull() ?: return null - return files.toList() + return selectedCommits.firstNotNullOfOrNull { extractCommitHash(it) } } - private fun buildSummaryInput(diff: String, changedFiles: List): String { - if (changedFiles.isEmpty()) return diff - val filesSection = changedFiles.joinToString(separator = "\n") { "- $it" } - return "Changed files:\n$filesSection\n\nDiff:\n$diff" + private fun extractCommitHash(value: Any?): String? { + if (value == null) return null + if (value is String && value.matches(Regex("^[0-9a-fA-F]{7,40}$"))) return value + + val hashObject = runCatching { + value.javaClass.getMethod("getHash").invoke(value) + }.getOrNull() ?: return null + + return runCatching { + hashObject.javaClass.getMethod("asString").invoke(hashObject) as? String + }.getOrNull() + ?: runCatching { + hashObject.javaClass.getMethod("toShortString").invoke(hashObject) as? String + }.getOrNull() + ?: hashObject.toString() } - } class SummarizeProbeVcsLogInternalToolbarAction : SummarizeBranchDiffAction( tooltip = "Summarize Branch Differences [Probe Vcs.Log.Toolbar.Internal]", - iconPath = "/icons/git-probe-02.svg", + iconPath = "/icons/blue-bulb.svg", sourceTag = "Vcs.Log.Toolbar.Internal" ) + +class SummarizeProbeVcsLogContextMenuAction : SummarizeBranchDiffAction( + tooltip = "Analyze Current Branch vs Selected Branch/Commit", + iconPath = "/icons/blue-bulb.svg", + sourceTag = "Vcs.Log.ContextMenu" +) diff --git a/plugin-idea/src/main/resources/META-INF/plugin.xml b/plugin-idea/src/main/resources/META-INF/plugin.xml index e9296f5..2c6d29c 100644 --- a/plugin-idea/src/main/resources/META-INF/plugin.xml +++ b/plugin-idea/src/main/resources/META-INF/plugin.xml @@ -13,6 +13,11 @@ + @@ -38,5 +43,15 @@ description="Summarize [Probe Vcs.Log.Toolbar.Internal]"> + + + + + + diff --git a/plugin-idea/src/main/resources/icons/blue-bulb.svg b/plugin-idea/src/main/resources/icons/blue-bulb.svg new file mode 100644 index 0000000..9884664 --- /dev/null +++ b/plugin-idea/src/main/resources/icons/blue-bulb.svg @@ -0,0 +1,11 @@ + + + + + +