feat: support quick way of including git commit diffs in the prompt (closes #688)

This commit is contained in:
Carl-Robert Linnupuu 2024-09-11 12:31:38 +03:00
parent 757afeff9e
commit 94d0bcd0a0
11 changed files with 214 additions and 21 deletions

View file

@ -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();
}

View file

@ -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);
}

View file

@ -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)
}

View file

@ -34,6 +34,7 @@ class SuggestionsPopupManager(
private val defaultActions: MutableList<SuggestionItem> = mutableListOf(
FileSuggestionGroupItem(project),
FolderSuggestionGroupItem(project),
GitSuggestionGroupItem(project),
PersonaSuggestionGroupItem(),
DocumentationSuggestionGroupItem(),
WebSearchActionItem(),

View file

@ -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")}"

View file

@ -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()
}
}
}

View file

@ -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)
}

View file

@ -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 ")
}
}
}

View 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

View 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

View file

@ -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