From 94d0bcd0a0479ea55ca32b0242e75bc7429c7a79 Mon Sep 17 00:00:00 2001 From: Carl-Robert Linnupuu Date: Wed, 11 Sep 2024 12:31:38 +0300 Subject: [PATCH] feat: support quick way of including git commit diffs in the prompt (closes #688) --- .../java/ee/carlrobert/codegpt/Icons.java | 2 + .../chat/ChatToolWindowTabPanel.java | 54 +++++++++++-- .../textarea/PromptTextFieldInlayRenderer.kt | 5 ++ .../suggestion/SuggestionsPopupManager.kt | 1 + .../suggestion/item/SuggestionActionItems.kt | 33 +++++++- .../suggestion/item/SuggestionGroupItems.kt | 28 +++++-- .../renderer/SuggestionItemRenderer.kt | 17 ++++ .../ee/carlrobert/codegpt/util/GitUtil.kt | 80 +++++++++++++++++-- src/main/resources/icons/vcs.svg | 7 ++ src/main/resources/icons/vcs_dark.svg | 7 ++ .../resources/messages/codegpt.properties | 1 + 11 files changed, 214 insertions(+), 21 deletions(-) create mode 100644 src/main/resources/icons/vcs.svg create mode 100644 src/main/resources/icons/vcs_dark.svg diff --git a/src/main/java/ee/carlrobert/codegpt/Icons.java b/src/main/java/ee/carlrobert/codegpt/Icons.java index ce9e6ed4..0b2042c6 100644 --- a/src/main/java/ee/carlrobert/codegpt/Icons.java +++ b/src/main/java/ee/carlrobert/codegpt/Icons.java @@ -34,5 +34,7 @@ public final class Icons { IconLoader.getIcon("/icons/openNewTab.svg", Icons.class); public static final Icon AddFile = IconLoader.getIcon("/icons/addFile.svg", Icons.class); + public static final Icon VCS = + IconLoader.getIcon("/icons/vcs.svg", Icons.class); public static final Icon StatusBarCompletionInProgress = new AnimatedIcon.Default(); } diff --git a/src/main/java/ee/carlrobert/codegpt/toolwindow/chat/ChatToolWindowTabPanel.java b/src/main/java/ee/carlrobert/codegpt/toolwindow/chat/ChatToolWindowTabPanel.java index 94e5d315..312795c4 100644 --- a/src/main/java/ee/carlrobert/codegpt/toolwindow/chat/ChatToolWindowTabPanel.java +++ b/src/main/java/ee/carlrobert/codegpt/toolwindow/chat/ChatToolWindowTabPanel.java @@ -37,6 +37,7 @@ import ee.carlrobert.codegpt.ui.textarea.AppliedSuggestionActionInlay; import ee.carlrobert.codegpt.ui.textarea.UserInputPanel; import ee.carlrobert.codegpt.ui.textarea.suggestion.item.CreateDocumentationActionItem; import ee.carlrobert.codegpt.ui.textarea.suggestion.item.DocumentationActionItem; +import ee.carlrobert.codegpt.ui.textarea.suggestion.item.GitCommitActionItem; import ee.carlrobert.codegpt.ui.textarea.suggestion.item.PersonaActionItem; import ee.carlrobert.codegpt.ui.textarea.suggestion.item.WebSearchActionItem; import ee.carlrobert.codegpt.util.EditorUtil; @@ -47,6 +48,7 @@ import java.nio.file.Files; import java.nio.file.Path; import java.util.List; import java.util.UUID; +import java.util.function.Function; import javax.swing.JComponent; import javax.swing.JPanel; import kotlin.Unit; @@ -299,7 +301,8 @@ public class ChatToolWindowTabPanel implements Disposable { if (action instanceof AppliedSuggestionActionInlay) { processSuggestionActions( message, - filterActions(appliedInlayActions, AppliedSuggestionActionInlay.class)); + filterActions(appliedInlayActions, AppliedSuggestionActionInlay.class), + text); } else if (action instanceof AppliedCodeActionInlay) { processCodeActions( message, @@ -325,10 +328,12 @@ public class ChatToolWindowTabPanel implements Disposable { private void processSuggestionActions( Message message, - List actions) { + List actions, + String text) { message.setWebSearchIncluded(containsWebSearchActionInlay(actions)); processDocumentationAction(message, actions); processPersonaAction(message, actions); + processGitCommitAction(message, actions, text); } private void processDocumentationAction( @@ -358,20 +363,24 @@ public class ChatToolWindowTabPanel implements Disposable { } } - private void processCodeActions(Message message, List actions, - String text, Editor editor) { + private void processActions( + Message message, + List actions, + String text, + Function codeExtractor, + Function languageExtractor) { var stringBuilder = new StringBuilder(text); var resultStringBuilder = new StringBuilder(); int lastProcessedIndex = 0; for (var actionInlay : actions) { var inlayOffset = actionInlay.getInlay().getOffset(); - var fileExtension = FileUtil.getFileExtension(editor.getVirtualFile().getName()); resultStringBuilder .append(stringBuilder, lastProcessedIndex, Math.min(stringBuilder.length(), inlayOffset)) .append('\n') - .append(formatCodeBlock(fileExtension, actionInlay.getCode())) + .append(formatCodeBlock(languageExtractor.apply(actionInlay), + codeExtractor.apply(actionInlay))) .append('\n'); lastProcessedIndex = inlayOffset; @@ -384,6 +393,39 @@ public class ChatToolWindowTabPanel implements Disposable { message.setPrompt(result); } + private void processGitCommitAction( + Message message, + List actions, + String text) { + var gitCommitInlays = actions.stream() + .filter(it -> it.getSuggestion() instanceof GitCommitActionItem) + .toList(); + + if (!gitCommitInlays.isEmpty()) { + processActions( + message, + gitCommitInlays, + text, + action -> ((GitCommitActionItem) action.getSuggestion()).getDiffString(), + action -> "shell" + ); + } + } + + private void processCodeActions( + Message message, + List actions, + String text, + Editor editor) { + processActions( + message, + actions, + text, + AppliedCodeActionInlay::getCode, + action -> FileUtil.getFileExtension(editor.getVirtualFile().getName()) + ); + } + private String formatCodeBlock(String fileExtension, String code) { return String.format("```%s\n%s\n```", fileExtension, code); } diff --git a/src/main/kotlin/ee/carlrobert/codegpt/ui/textarea/PromptTextFieldInlayRenderer.kt b/src/main/kotlin/ee/carlrobert/codegpt/ui/textarea/PromptTextFieldInlayRenderer.kt index 27e0dd9f..280b7a3f 100644 --- a/src/main/kotlin/ee/carlrobert/codegpt/ui/textarea/PromptTextFieldInlayRenderer.kt +++ b/src/main/kotlin/ee/carlrobert/codegpt/ui/textarea/PromptTextFieldInlayRenderer.kt @@ -45,6 +45,11 @@ class PromptTextFieldInlayRenderer( val font = editor.colorsScheme.getFont(EditorFontType.PLAIN) val textWidth = editor.component.getFontMetrics(font) .stringWidth(actionPrefix + (if (text != null) ":$text" else "")) + + if (tooltipText.isNullOrEmpty()) { + return textWidth + closeIcon.iconWidth + JBUI.scale(10) + } + return textWidth + closeIcon.iconWidth + JBUI.scale(10) + helpIcon.iconWidth + JBUI.scale(10) } diff --git a/src/main/kotlin/ee/carlrobert/codegpt/ui/textarea/suggestion/SuggestionsPopupManager.kt b/src/main/kotlin/ee/carlrobert/codegpt/ui/textarea/suggestion/SuggestionsPopupManager.kt index 61b9784d..3cee69d9 100644 --- a/src/main/kotlin/ee/carlrobert/codegpt/ui/textarea/suggestion/SuggestionsPopupManager.kt +++ b/src/main/kotlin/ee/carlrobert/codegpt/ui/textarea/suggestion/SuggestionsPopupManager.kt @@ -34,6 +34,7 @@ class SuggestionsPopupManager( private val defaultActions: MutableList = mutableListOf( FileSuggestionGroupItem(project), FolderSuggestionGroupItem(project), + GitSuggestionGroupItem(project), PersonaSuggestionGroupItem(), DocumentationSuggestionGroupItem(), WebSearchActionItem(), diff --git a/src/main/kotlin/ee/carlrobert/codegpt/ui/textarea/suggestion/item/SuggestionActionItems.kt b/src/main/kotlin/ee/carlrobert/codegpt/ui/textarea/suggestion/item/SuggestionActionItems.kt index 8a0db5fc..eff134c0 100644 --- a/src/main/kotlin/ee/carlrobert/codegpt/ui/textarea/suggestion/item/SuggestionActionItems.kt +++ b/src/main/kotlin/ee/carlrobert/codegpt/ui/textarea/suggestion/item/SuggestionActionItems.kt @@ -1,13 +1,15 @@ package ee.carlrobert.codegpt.ui.textarea.suggestion.item import com.intellij.icons.AllIcons +import com.intellij.openapi.application.ReadAction import com.intellij.openapi.components.service import com.intellij.openapi.options.ShowSettingsUtil import com.intellij.openapi.project.Project import com.intellij.openapi.vfs.VirtualFile +import com.intellij.util.concurrency.AppExecutorUtil import ee.carlrobert.codegpt.CodeGPTBundle import ee.carlrobert.codegpt.CodeGPTKeys -import ee.carlrobert.codegpt.Icons +import ee.carlrobert.codegpt.EncodingManager import ee.carlrobert.codegpt.settings.GeneralSettings import ee.carlrobert.codegpt.settings.documentation.DocumentationSettings import ee.carlrobert.codegpt.settings.documentation.DocumentationsConfigurable @@ -18,6 +20,8 @@ import ee.carlrobert.codegpt.ui.AddDocumentationDialog import ee.carlrobert.codegpt.ui.DocumentationDetails import ee.carlrobert.codegpt.ui.textarea.FileSearchService import ee.carlrobert.codegpt.ui.textarea.PromptTextField +import ee.carlrobert.codegpt.util.GitUtil +import git4idea.GitCommit class FileActionItem(val file: VirtualFile) : SuggestionActionItem { override val displayName = file.name @@ -86,6 +90,33 @@ class CreateDocumentationActionItem : SuggestionActionItem { } } +class GitCommitActionItem( + private val project: Project, + val gitCommit: GitCommit, +) : SuggestionActionItem { + + companion object { + private const val MAX_TOKENS = 4096 + } + val description: String = gitCommit.id.asString().take(6) + + override val displayName: String = gitCommit.subject + override val icon = AllIcons.Vcs.CommitNode + + override fun execute(project: Project, textPane: PromptTextField) { + textPane.addInlayElement("commit", gitCommit.id.asString().take(6), this) + } + + fun getDiffString(): String { + return ReadAction.nonBlocking { + val repository = GitUtil.getProjectRepository(project) ?: return@nonBlocking "" + val diff = GitUtil.getCommitDiff(project, repository, gitCommit.id.asString()) + .joinToString("\n") + service().truncateText(diff, MAX_TOKENS, true) + }.submit(AppExecutorUtil.getAppExecutorService()).get() + } +} + class ViewAllDocumentationsActionItem : SuggestionActionItem { override val displayName: String = "${CodeGPTBundle.get("suggestionActionItem.viewDocumentations.displayName")} →" diff --git a/src/main/kotlin/ee/carlrobert/codegpt/ui/textarea/suggestion/item/SuggestionGroupItems.kt b/src/main/kotlin/ee/carlrobert/codegpt/ui/textarea/suggestion/item/SuggestionGroupItems.kt index 1eb87dec..f19c2cb5 100644 --- a/src/main/kotlin/ee/carlrobert/codegpt/ui/textarea/suggestion/item/SuggestionGroupItems.kt +++ b/src/main/kotlin/ee/carlrobert/codegpt/ui/textarea/suggestion/item/SuggestionGroupItems.kt @@ -8,18 +8,21 @@ import com.intellij.openapi.project.Project import com.intellij.openapi.roots.ProjectFileIndex import com.intellij.openapi.vfs.VirtualFile import ee.carlrobert.codegpt.CodeGPTBundle +import ee.carlrobert.codegpt.Icons import ee.carlrobert.codegpt.settings.GeneralSettings import ee.carlrobert.codegpt.settings.documentation.DocumentationSettings import ee.carlrobert.codegpt.settings.persona.PersonaDetails import ee.carlrobert.codegpt.settings.persona.PersonaSettings import ee.carlrobert.codegpt.settings.service.ServiceType import ee.carlrobert.codegpt.ui.DocumentationDetails +import ee.carlrobert.codegpt.util.GitUtil import ee.carlrobert.codegpt.util.ResourceUtil.getDefaultPersonas import ee.carlrobert.codegpt.util.file.FileUtil import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext import java.time.Instant import java.time.format.DateTimeParseException +import javax.swing.Icon class FileSuggestionGroupItem(private val project: Project) : SuggestionGroupItem { override val displayName: String = CodeGPTBundle.get("suggestionGroupItem.files.displayName") @@ -88,11 +91,7 @@ class PersonaSuggestionGroupItem : SuggestionGroupItem { .toMutableList() return (userCreatedPersonas + getDefaultPersonas()) .filter { - if (searchText.isNullOrEmpty()) { - true - } else { - it.name.contains(searchText, true) - } + searchText.isNullOrEmpty() || it.name.contains(searchText, true) } .map { PersonaActionItem(it) } .take(10) + listOf(CreatePersonaActionItem()) @@ -128,4 +127,21 @@ class DocumentationSuggestionGroupItem : SuggestionGroupItem { } } ?: Instant.EPOCH } -} \ No newline at end of file +} + +class GitSuggestionGroupItem(private val project: Project) : SuggestionGroupItem { + override val displayName: String = CodeGPTBundle.get("suggestionGroupItem.git.displayName") + override val icon: Icon = Icons.VCS + + override suspend fun getSuggestions(searchText: String?): List { + return withContext(Dispatchers.Default) { + GitUtil.getProjectRepository(project)?.let { + GitUtil.getAllRecentCommits(project, it, searchText) + .take(10) + .map { commit -> + GitCommitActionItem(project, commit) + } + } ?: emptyList() + } + } +} diff --git a/src/main/kotlin/ee/carlrobert/codegpt/ui/textarea/suggestion/renderer/SuggestionItemRenderer.kt b/src/main/kotlin/ee/carlrobert/codegpt/ui/textarea/suggestion/renderer/SuggestionItemRenderer.kt index e10c2b6f..a0dee3d6 100644 --- a/src/main/kotlin/ee/carlrobert/codegpt/ui/textarea/suggestion/renderer/SuggestionItemRenderer.kt +++ b/src/main/kotlin/ee/carlrobert/codegpt/ui/textarea/suggestion/renderer/SuggestionItemRenderer.kt @@ -8,6 +8,7 @@ import com.intellij.ui.dsl.builder.AlignX import com.intellij.ui.dsl.builder.panel import com.intellij.ui.dsl.gridLayout.UnscaledGaps import com.intellij.util.ui.JBUI +import ee.carlrobert.codegpt.EncodingManager import ee.carlrobert.codegpt.settings.persona.PersonaSettings import ee.carlrobert.codegpt.ui.textarea.PromptTextField import ee.carlrobert.codegpt.ui.textarea.suggestion.item.* @@ -133,6 +134,21 @@ class PersonaItemRenderer(textPane: PromptTextField) : BaseItemRenderer(textPane } } +class GitCommitItemRenderer(textPane: PromptTextField) : BaseItemRenderer(textPane) { + override fun render(component: JLabel, value: SuggestionItem): JPanel { + val item = value as GitCommitActionItem + val author = item.gitCommit.author.name + val truncatedAuthor = if (author.length > 32) "${author.take(32)}..." else author + return createPanel( + component, + item.icon, + item.displayName, + "${item.gitCommit.id.asString().take(6)} • $truncatedAuthor", + null, + ) + } +} + class DocumentationItemRenderer(textPane: PromptTextField) : BaseItemRenderer(textPane) { override fun render(component: JLabel, value: SuggestionItem): JPanel { val item = value as DocumentationActionItem @@ -152,6 +168,7 @@ class RendererFactory(private val textPane: PromptTextField) { is FileActionItem -> FileItemRenderer(textPane) is FolderActionItem -> FolderItemRenderer(textPane) is PersonaActionItem -> PersonaItemRenderer(textPane) + is GitCommitActionItem -> GitCommitItemRenderer(textPane) is DocumentationActionItem -> DocumentationItemRenderer(textPane) else -> DefaultItemRenderer(textPane) } diff --git a/src/main/kotlin/ee/carlrobert/codegpt/util/GitUtil.kt b/src/main/kotlin/ee/carlrobert/codegpt/util/GitUtil.kt index f05e70c9..bf4889c1 100644 --- a/src/main/kotlin/ee/carlrobert/codegpt/util/GitUtil.kt +++ b/src/main/kotlin/ee/carlrobert/codegpt/util/GitUtil.kt @@ -1,14 +1,21 @@ package ee.carlrobert.codegpt.util +import com.intellij.openapi.components.service +import com.intellij.openapi.diagnostic.thisLogger import com.intellij.openapi.project.Project import com.intellij.openapi.vcs.VcsException +import git4idea.GitCommit import git4idea.commands.Git import git4idea.commands.GitCommand import git4idea.commands.GitLineHandler +import git4idea.history.GitHistoryUtils import git4idea.repo.GitRepository +import git4idea.repo.GitRepositoryManager object GitUtil { + private val logger = thisLogger() + @Throws(VcsException::class) @JvmStatic fun getStagedDiff( @@ -51,14 +58,71 @@ object GitUtil { } val commandResult = Git.getInstance().runCommand(handler) - return commandResult.output.filter { - listOf( - "diff --git", - "index ", - "---", - "- ", - "+++" - ).none { prefix -> it.startsWith(prefix) } + return filterDiffOutput(commandResult.output) + } + + @Throws(VcsException::class) + fun getProjectRepository(project: Project): GitRepository? { + val repositoryManager = project.service() + return repositoryManager.getRepositoryForFile(project.workspaceFile) + ?: repositoryManager.repositories.firstOrNull() + } + + @Throws(VcsException::class) + fun getCommitDiff( + project: Project, + gitRepository: GitRepository, + commitHash: String + ): List { + val handler = GitLineHandler(project, gitRepository.root, GitCommand.SHOW) + handler.addParameters( + commitHash, + "--unified=2", + "--no-prefix", + "--no-color" + ) + + val commandResult = Git.getInstance().runCommand(handler) + return filterDiffOutput(commandResult.output) + } + + @Throws(VcsException::class) + fun getAllRecentCommits( + project: Project, + repository: GitRepository, + searchText: String? = "", + limit: Int = 250 + ): List { + val result = mutableListOf() + + try { + GitHistoryUtils + .loadDetails(project, repository.root, { commit -> + if (searchText.isNullOrEmpty()) { + result.add(commit) + } else { + if (commit.id.asString().contains(searchText, true) + || commit.fullMessage.contains(searchText, true) + ) { + result.add(commit) + } + } + }, "-n", "$limit") + } catch (e: VcsException) { + logger.error("Error fetching commit history: {}", e.message) + } + + return result + } + + private fun filterDiffOutput(output: List): List { + return output.filter { + !it.startsWith("diff --git") && + !it.startsWith("index ") && + !it.startsWith("---") && + !it.startsWith("+++") && + !it.startsWith("- ") && + !it.startsWith("commit ") } } } diff --git a/src/main/resources/icons/vcs.svg b/src/main/resources/icons/vcs.svg new file mode 100644 index 00000000..37f8363f --- /dev/null +++ b/src/main/resources/icons/vcs.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/src/main/resources/icons/vcs_dark.svg b/src/main/resources/icons/vcs_dark.svg new file mode 100644 index 00000000..d906e8df --- /dev/null +++ b/src/main/resources/icons/vcs_dark.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/src/main/resources/messages/codegpt.properties b/src/main/resources/messages/codegpt.properties index c1892cdb..2ba25746 100644 --- a/src/main/resources/messages/codegpt.properties +++ b/src/main/resources/messages/codegpt.properties @@ -265,6 +265,7 @@ suggestionGroupItem.files.displayName=Files suggestionGroupItem.folders.displayName=Folders suggestionGroupItem.personas.displayName=Personas suggestionGroupItem.docs.displayName=Docs +suggestionGroupItem.git.displayName=Git suggestionActionItem.webSearch.displayName=Web suggestionActionItem.viewDocumentations.displayName=View all docs suggestionActionItem.createPersona.displayName=Create new persona