refactor: improve inlay action UI rendering

This commit is contained in:
Carl-Robert Linnupuu 2024-09-11 19:12:53 +03:00
parent 36ba3616de
commit 74608ec930
7 changed files with 183 additions and 171 deletions

View file

@ -23,6 +23,7 @@ import ee.carlrobert.codegpt.conversations.Conversation;
import ee.carlrobert.codegpt.conversations.ConversationService;
import ee.carlrobert.codegpt.conversations.message.Message;
import ee.carlrobert.codegpt.telemetry.TelemetryAction;
import ee.carlrobert.codegpt.toolwindow.chat.actionprocessor.ActionProcessorFactory;
import ee.carlrobert.codegpt.toolwindow.chat.ui.ChatMessageResponseBody;
import ee.carlrobert.codegpt.toolwindow.chat.ui.ChatToolWindowScrollablePanel;
import ee.carlrobert.codegpt.toolwindow.chat.ui.ResponsePanel;
@ -32,14 +33,7 @@ import ee.carlrobert.codegpt.toolwindow.chat.ui.textarea.TotalTokensPanel;
import ee.carlrobert.codegpt.toolwindow.ui.ChatToolWindowLandingPanel;
import ee.carlrobert.codegpt.ui.OverlayUtil;
import ee.carlrobert.codegpt.ui.textarea.AppliedActionInlay;
import ee.carlrobert.codegpt.ui.textarea.AppliedCodeActionInlay;
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;
import ee.carlrobert.codegpt.util.file.FileUtil;
import java.awt.BorderLayout;
@ -48,7 +42,6 @@ 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;
@ -272,162 +265,46 @@ public class ChatToolWindowTabPanel implements Disposable {
requestHandler.call(callParameters);
}
private String processEditorSelection(Editor editor, Message message) {
if (editor == null) {
return null;
}
SelectionModel selectionModel = editor.getSelectionModel();
String selectedText = selectionModel.getSelectedText();
if (selectedText == null || selectedText.isEmpty()) {
return null;
}
String fileExtension = FileUtil.getFileExtension(editor.getVirtualFile().getName());
message.setPrompt(
message.getPrompt() + String.format("%n```%s%n%s%n```", fileExtension, selectedText));
selectionModel.removeSelection();
return selectedText;
}
private Unit handleSubmit(String text, List<? extends AppliedActionInlay> appliedInlayActions) {
var message = new Message(text);
var editor = EditorUtil.getSelectedEditor(project);
String highlightedText = null;
if (editor != null) {
var selectionModel = editor.getSelectionModel();
var selectedText = selectionModel.getSelectedText();
if (selectedText != null && !selectedText.isEmpty()) {
var fileExtension = FileUtil.getFileExtension(editor.getVirtualFile().getName());
message = new Message(text + format("%n```%s%n%s%n```", fileExtension, selectedText));
highlightedText = selectedText;
selectionModel.removeSelection();
}
}
message.setUserMessage(text);
processAppliedInlayActions(message, appliedInlayActions, text, editor);
sendMessage(message, ConversationType.DEFAULT, highlightedText);
return Unit.INSTANCE;
}
private void processAppliedInlayActions(
Message message,
List<? extends AppliedActionInlay> appliedInlayActions,
String text,
Editor editor) {
for (var action : appliedInlayActions) {
if (action instanceof AppliedSuggestionActionInlay) {
processSuggestionActions(
message,
filterActions(appliedInlayActions, AppliedSuggestionActionInlay.class),
text);
} else if (action instanceof AppliedCodeActionInlay) {
processCodeActions(
message,
filterActions(appliedInlayActions, AppliedCodeActionInlay.class),
text,
editor);
}
}
}
var remainingText = new StringBuilder(text);
var promptBuilder = new StringBuilder();
private <T extends AppliedActionInlay> List<T> filterActions(
List<? extends AppliedActionInlay> actions,
Class<T> actionClass) {
return actions.stream()
.filter(actionClass::isInstance)
.map(actionClass::cast)
.toList();
}
private boolean containsWebSearchActionInlay(List<AppliedSuggestionActionInlay> actions) {
return actions.stream().anyMatch(it -> it.getSuggestion() instanceof WebSearchActionItem);
}
private void processSuggestionActions(
Message message,
List<AppliedSuggestionActionInlay> actions,
String text) {
message.setWebSearchIncluded(containsWebSearchActionInlay(actions));
processDocumentationAction(message, actions);
processPersonaAction(message, actions);
processGitCommitAction(message, actions, text);
}
private void processDocumentationAction(
Message message,
List<AppliedSuggestionActionInlay> actions) {
var addedDocumentation = CodeGPTKeys.ADDED_DOCUMENTATION.get(project);
var appliedInlayExists = actions.stream().anyMatch(it -> {
var suggestion = it.getSuggestion();
return suggestion instanceof DocumentationActionItem
|| suggestion instanceof CreateDocumentationActionItem;
});
if (addedDocumentation != null && appliedInlayExists) {
message.setDocumentationDetails(addedDocumentation);
CodeGPTKeys.ADDED_DOCUMENTATION.set(project, null);
}
}
private void processPersonaAction(Message message, List<AppliedSuggestionActionInlay> actions) {
var addedPersona = CodeGPTKeys.ADDED_PERSONA.get(project);
var personaInlayExists = actions.stream()
.anyMatch(it -> it.getSuggestion() instanceof PersonaActionItem);
if (addedPersona != null && personaInlayExists) {
message.setPersonaDetails(addedPersona);
CodeGPTKeys.ADDED_PERSONA.set(project, null);
}
}
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) {
for (var actionInlay : appliedInlayActions) {
var inlayOffset = actionInlay.getInlay().getOffset();
resultStringBuilder
.append(stringBuilder, lastProcessedIndex, Math.min(stringBuilder.length(), inlayOffset))
.append('\n')
.append(formatCodeBlock(languageExtractor.apply(actionInlay),
codeExtractor.apply(actionInlay)))
.append('\n');
lastProcessedIndex = inlayOffset;
promptBuilder.append(remainingText, 0, Math.min(inlayOffset, remainingText.length()))
.append("\n");
ActionProcessorFactory.getProcessor(actionInlay)
.process(message, actionInlay, editor, promptBuilder);
remainingText.delete(0, inlayOffset);
}
promptBuilder.append(remainingText);
resultStringBuilder.append(stringBuilder, lastProcessedIndex, stringBuilder.length());
message.setUserMessage(promptBuilder.toString());
message.setPrompt(promptBuilder.toString());
var result = resultStringBuilder.toString();
message.setUserMessage(result);
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);
sendMessage(message, ConversationType.DEFAULT, processEditorSelection(editor, message));
return Unit.INSTANCE;
}
private Unit handleCancel() {

View file

@ -0,0 +1,11 @@
package ee.carlrobert.codegpt.toolwindow.chat.actionprocessor;
import com.intellij.openapi.editor.Editor;
import ee.carlrobert.codegpt.conversations.message.Message;
import ee.carlrobert.codegpt.ui.textarea.AppliedActionInlay;
public interface ActionProcessor {
void process(Message message, AppliedActionInlay action, Editor editor,
StringBuilder promptBuilder);
}

View file

@ -0,0 +1,17 @@
package ee.carlrobert.codegpt.toolwindow.chat.actionprocessor;
import ee.carlrobert.codegpt.ui.textarea.AppliedActionInlay;
import ee.carlrobert.codegpt.ui.textarea.AppliedCodeActionInlay;
import ee.carlrobert.codegpt.ui.textarea.AppliedSuggestionActionInlay;
public class ActionProcessorFactory {
public static ActionProcessor getProcessor(AppliedActionInlay action) {
if (action instanceof AppliedSuggestionActionInlay) {
return new SuggestionActionProcessor();
} else if (action instanceof AppliedCodeActionInlay) {
return new CodeActionProcessor();
}
throw new IllegalArgumentException("Unknown action type");
}
}

View file

@ -0,0 +1,27 @@
package ee.carlrobert.codegpt.toolwindow.chat.actionprocessor;
import com.intellij.openapi.editor.Editor;
import ee.carlrobert.codegpt.conversations.message.Message;
import ee.carlrobert.codegpt.ui.textarea.AppliedActionInlay;
import ee.carlrobert.codegpt.ui.textarea.AppliedCodeActionInlay;
import ee.carlrobert.codegpt.util.file.FileUtil;
public class CodeActionProcessor implements ActionProcessor {
@Override
public void process(Message message, AppliedActionInlay action, Editor editor,
StringBuilder promptBuilder) {
if (!(action instanceof AppliedCodeActionInlay codeAction)) {
throw new IllegalArgumentException("Invalid action type");
}
processCodeAction(codeAction, editor, promptBuilder);
}
private void processCodeAction(AppliedCodeActionInlay action, Editor editor,
StringBuilder promptBuilder) {
promptBuilder
.append("\n```%s\n".formatted(FileUtil.getFileExtension(editor.getVirtualFile().getName())))
.append(action.getCode())
.append("\n```\n");
}
}

View file

@ -0,0 +1,70 @@
package ee.carlrobert.codegpt.toolwindow.chat.actionprocessor;
import com.intellij.openapi.editor.Editor;
import ee.carlrobert.codegpt.CodeGPTKeys;
import ee.carlrobert.codegpt.conversations.message.Message;
import ee.carlrobert.codegpt.ui.textarea.AppliedActionInlay;
import ee.carlrobert.codegpt.ui.textarea.AppliedSuggestionActionInlay;
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;
public class SuggestionActionProcessor implements ActionProcessor {
@Override
public void process(Message message, AppliedActionInlay action, Editor editor,
StringBuilder promptBuilder) {
if (!(action instanceof AppliedSuggestionActionInlay suggestionAction)) {
throw new IllegalArgumentException("Invalid action type");
}
processSuggestionAction(message, suggestionAction, editor, promptBuilder);
}
private void processSuggestionAction(
Message message,
AppliedSuggestionActionInlay action,
Editor editor,
StringBuilder promptBuilder) {
message.setWebSearchIncluded(action.getSuggestion() instanceof WebSearchActionItem);
processDocumentationAction(message, action, editor);
processPersonaAction(message, action, editor);
processGitCommitAction(action, promptBuilder);
}
private void processDocumentationAction(Message message, AppliedSuggestionActionInlay action,
Editor editor) {
var project = editor.getProject();
var addedDocumentation = CodeGPTKeys.ADDED_DOCUMENTATION.get(project);
var appliedInlayExists = action.getSuggestion() instanceof DocumentationActionItem
|| action.getSuggestion() instanceof CreateDocumentationActionItem;
if (addedDocumentation != null && appliedInlayExists) {
message.setDocumentationDetails(addedDocumentation);
CodeGPTKeys.ADDED_DOCUMENTATION.set(project, null);
}
}
private void processPersonaAction(Message message, AppliedSuggestionActionInlay action,
Editor editor) {
var project = editor.getProject();
var addedPersona = CodeGPTKeys.ADDED_PERSONA.get(project);
var personaInlayExists = action.getSuggestion() instanceof PersonaActionItem;
if (addedPersona != null && personaInlayExists) {
message.setPersonaDetails(addedPersona);
CodeGPTKeys.ADDED_PERSONA.set(project, null);
}
}
private void processGitCommitAction(
AppliedSuggestionActionInlay action,
StringBuilder promptBuilder) {
if (action.getSuggestion() instanceof GitCommitActionItem gitCommitActionItem) {
promptBuilder
.append("\n```shell\n")
.append(gitCommitActionItem.getDiffString())
.append("\n```\n");
}
}
}

View file

@ -114,6 +114,8 @@ public class ChatMessageResponseBody extends JPanel {
public ChatMessageResponseBody withResponse(String response) {
for (var message : MarkdownUtil.splitCodeBlocks(response)) {
currentlyProcessedEditorPanel = null;
currentlyProcessedTextPane = null;
processResponse(message, message.startsWith("```"), false);
}
@ -250,12 +252,12 @@ public class ChatMessageResponseBody extends JPanel {
var codeBlock = ((FencedCodeBlock) child);
var code = codeBlock.getContentChars().unescape();
if (!code.isEmpty()) {
ApplicationManager.getApplication().invokeLater(() -> {
if (currentlyProcessedEditorPanel == null) {
if (currentlyProcessedEditorPanel == null) {
ApplicationManager.getApplication().invokeAndWait(() -> {
prepareProcessingCode(code, codeBlock.getInfo().unescape());
}
EditorUtil.updateEditorDocument(currentlyProcessedEditorPanel.getEditor(), code);
});
});
}
EditorUtil.updateEditorDocument(currentlyProcessedEditorPanel.getEditor(), code);
}
}
}
@ -283,9 +285,9 @@ public class ChatMessageResponseBody extends JPanel {
private void prepareProcessingCode(String code, String markdownLanguage) {
hideCaret();
currentlyProcessedTextPane = null;
currentlyProcessedEditorPanel =
new ResponseEditorPanel(project, code, markdownLanguage, readOnly, highlightedText,
parentDisposable);
currentlyProcessedEditorPanel = new ResponseEditorPanel(project, code, markdownLanguage,
readOnly, highlightedText,
parentDisposable);
add(currentlyProcessedEditorPanel);
}

View file

@ -1,15 +1,14 @@
package ee.carlrobert.codegpt.ui.textarea.suggestion.item
import com.intellij.icons.AllIcons
import com.intellij.openapi.application.ReadAction
import com.intellij.openapi.application.readAction
import com.intellij.openapi.components.service
import com.intellij.openapi.options.ShowSettingsUtil
import com.intellij.openapi.progress.ProgressManager
import com.intellij.openapi.project.Project
import com.intellij.openapi.roots.ProjectFileIndex
import com.intellij.openapi.vcs.changes.VcsIgnoreManager
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.EncodingManager
@ -121,12 +120,21 @@ class GitCommitActionItem(
}
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()
return ProgressManager.getInstance().runProcessWithProgressSynchronously<String, Exception>(
{
val repository = GitUtil.getProjectRepository(project)
?: return@runProcessWithProgressSynchronously ""
val commitId = gitCommit.id.asString()
val diff = GitUtil.getCommitDiff(project, repository, commitId)
.joinToString("\n")
service<EncodingManager>().truncateText(diff, MAX_TOKENS, true)
},
"Getting Diff",
true,
project
)
}
}