mirror of
https://github.com/carlrobertoh/ProxyAI.git
synced 2026-05-10 20:30:24 +00:00
feat: support quick way of including git commit diffs in the prompt (closes #688)
This commit is contained in:
parent
757afeff9e
commit
94d0bcd0a0
11 changed files with 214 additions and 21 deletions
|
|
@ -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();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<AppliedSuggestionActionInlay> actions) {
|
||||
List<AppliedSuggestionActionInlay> 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<AppliedCodeActionInlay> actions,
|
||||
String text, Editor editor) {
|
||||
private <T extends AppliedActionInlay> void processActions(
|
||||
Message message,
|
||||
List<T> actions,
|
||||
String text,
|
||||
Function<T, String> codeExtractor,
|
||||
Function<T, String> 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<AppliedSuggestionActionInlay> 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<AppliedCodeActionInlay> 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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -34,6 +34,7 @@ class SuggestionsPopupManager(
|
|||
private val defaultActions: MutableList<SuggestionItem> = mutableListOf(
|
||||
FileSuggestionGroupItem(project),
|
||||
FolderSuggestionGroupItem(project),
|
||||
GitSuggestionGroupItem(project),
|
||||
PersonaSuggestionGroupItem(),
|
||||
DocumentationSuggestionGroupItem(),
|
||||
WebSearchActionItem(),
|
||||
|
|
|
|||
|
|
@ -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<String> {
|
||||
val repository = GitUtil.getProjectRepository(project) ?: return@nonBlocking ""
|
||||
val diff = GitUtil.getCommitDiff(project, repository, gitCommit.id.asString())
|
||||
.joinToString("\n")
|
||||
service<EncodingManager>().truncateText(diff, MAX_TOKENS, true)
|
||||
}.submit(AppExecutorUtil.getAppExecutorService()).get()
|
||||
}
|
||||
}
|
||||
|
||||
class ViewAllDocumentationsActionItem : SuggestionActionItem {
|
||||
override val displayName: String =
|
||||
"${CodeGPTBundle.get("suggestionActionItem.viewDocumentations.displayName")} →"
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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<SuggestionActionItem> {
|
||||
return withContext(Dispatchers.Default) {
|
||||
GitUtil.getProjectRepository(project)?.let {
|
||||
GitUtil.getAllRecentCommits(project, it, searchText)
|
||||
.take(10)
|
||||
.map { commit ->
|
||||
GitCommitActionItem(project, commit)
|
||||
}
|
||||
} ?: emptyList()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<GitRepositoryManager>()
|
||||
return repositoryManager.getRepositoryForFile(project.workspaceFile)
|
||||
?: repositoryManager.repositories.firstOrNull()
|
||||
}
|
||||
|
||||
@Throws(VcsException::class)
|
||||
fun getCommitDiff(
|
||||
project: Project,
|
||||
gitRepository: GitRepository,
|
||||
commitHash: String
|
||||
): List<String> {
|
||||
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<GitCommit> {
|
||||
val result = mutableListOf<GitCommit>()
|
||||
|
||||
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<String>): List<String> {
|
||||
return output.filter {
|
||||
!it.startsWith("diff --git") &&
|
||||
!it.startsWith("index ") &&
|
||||
!it.startsWith("---") &&
|
||||
!it.startsWith("+++") &&
|
||||
!it.startsWith("- ") &&
|
||||
!it.startsWith("commit ")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
7
src/main/resources/icons/vcs.svg
Normal file
7
src/main/resources/icons/vcs.svg
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
<!-- Copyright 2000-2023 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. -->
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<circle cx="4.5" cy="4" r="2" stroke="#6C707E"/>
|
||||
<path d="M4.5 11.5H8.5C9.60457 11.5 10.5 10.6046 10.5 9.5V9.5V8" stroke="#6C707E"/>
|
||||
<path d="M4.5 6.5L4.5 14.5" stroke="#6C707E" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<circle cx="10.5" cy="6" r="2" stroke="#6C707E"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 507 B |
7
src/main/resources/icons/vcs_dark.svg
Normal file
7
src/main/resources/icons/vcs_dark.svg
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
<!-- Copyright 2000-2023 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. -->
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<circle cx="4.5" cy="4" r="2" stroke="#CED0D6"/>
|
||||
<path d="M4.5 11H8.5C9.60457 11 10.5 10.1046 10.5 9V9V7.5" stroke="#CED0D6"/>
|
||||
<path d="M4.5 6.5L4.5 14.5" stroke="#CED0D6" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<circle cx="10.5" cy="6" r="2" stroke="#CED0D6"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 501 B |
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue