From 5194355879d8645beae15a42a54f725df76491f5 Mon Sep 17 00:00:00 2001 From: Carl-Robert Linnupuu Date: Wed, 11 Sep 2024 19:12:53 +0300 Subject: [PATCH] refactor: improve inlay action UI rendering --- .../chat/ChatToolWindowTabPanel.java | 187 +++--------------- .../chat/actionprocessor/ActionProcessor.java | 11 ++ .../ActionProcessorFactory.java | 17 ++ .../actionprocessor/CodeActionProcessor.java | 27 +++ .../SuggestionActionProcessor.java | 70 +++++++ .../chat/ui/ChatMessageResponseBody.java | 18 +- .../suggestion/item/SuggestionActionItems.kt | 25 ++- 7 files changed, 184 insertions(+), 171 deletions(-) create mode 100644 src/main/java/ee/carlrobert/codegpt/toolwindow/chat/actionprocessor/ActionProcessor.java create mode 100644 src/main/java/ee/carlrobert/codegpt/toolwindow/chat/actionprocessor/ActionProcessorFactory.java create mode 100644 src/main/java/ee/carlrobert/codegpt/toolwindow/chat/actionprocessor/CodeActionProcessor.java create mode 100644 src/main/java/ee/carlrobert/codegpt/toolwindow/chat/actionprocessor/SuggestionActionProcessor.java 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 312795c4..1d07a37e 100644 --- a/src/main/java/ee/carlrobert/codegpt/toolwindow/chat/ChatToolWindowTabPanel.java +++ b/src/main/java/ee/carlrobert/codegpt/toolwindow/chat/ChatToolWindowTabPanel.java @@ -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 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 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 List filterActions( - List actions, - Class actionClass) { - return actions.stream() - .filter(actionClass::isInstance) - .map(actionClass::cast) - .toList(); - } - - private boolean containsWebSearchActionInlay(List actions) { - return actions.stream().anyMatch(it -> it.getSuggestion() instanceof WebSearchActionItem); - } - - private void processSuggestionActions( - Message message, - List actions, - String text) { - message.setWebSearchIncluded(containsWebSearchActionInlay(actions)); - processDocumentationAction(message, actions); - processPersonaAction(message, actions); - processGitCommitAction(message, actions, text); - } - - private void processDocumentationAction( - Message message, - List 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 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 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) { + 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 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); + sendMessage(message, ConversationType.DEFAULT, processEditorSelection(editor, message)); + return Unit.INSTANCE; } private Unit handleCancel() { diff --git a/src/main/java/ee/carlrobert/codegpt/toolwindow/chat/actionprocessor/ActionProcessor.java b/src/main/java/ee/carlrobert/codegpt/toolwindow/chat/actionprocessor/ActionProcessor.java new file mode 100644 index 00000000..9c24aaac --- /dev/null +++ b/src/main/java/ee/carlrobert/codegpt/toolwindow/chat/actionprocessor/ActionProcessor.java @@ -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); +} diff --git a/src/main/java/ee/carlrobert/codegpt/toolwindow/chat/actionprocessor/ActionProcessorFactory.java b/src/main/java/ee/carlrobert/codegpt/toolwindow/chat/actionprocessor/ActionProcessorFactory.java new file mode 100644 index 00000000..bc698004 --- /dev/null +++ b/src/main/java/ee/carlrobert/codegpt/toolwindow/chat/actionprocessor/ActionProcessorFactory.java @@ -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"); + } +} diff --git a/src/main/java/ee/carlrobert/codegpt/toolwindow/chat/actionprocessor/CodeActionProcessor.java b/src/main/java/ee/carlrobert/codegpt/toolwindow/chat/actionprocessor/CodeActionProcessor.java new file mode 100644 index 00000000..66980439 --- /dev/null +++ b/src/main/java/ee/carlrobert/codegpt/toolwindow/chat/actionprocessor/CodeActionProcessor.java @@ -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"); + } +} diff --git a/src/main/java/ee/carlrobert/codegpt/toolwindow/chat/actionprocessor/SuggestionActionProcessor.java b/src/main/java/ee/carlrobert/codegpt/toolwindow/chat/actionprocessor/SuggestionActionProcessor.java new file mode 100644 index 00000000..838ea980 --- /dev/null +++ b/src/main/java/ee/carlrobert/codegpt/toolwindow/chat/actionprocessor/SuggestionActionProcessor.java @@ -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"); + } + } +} diff --git a/src/main/java/ee/carlrobert/codegpt/toolwindow/chat/ui/ChatMessageResponseBody.java b/src/main/java/ee/carlrobert/codegpt/toolwindow/chat/ui/ChatMessageResponseBody.java index f5613936..e8694881 100644 --- a/src/main/java/ee/carlrobert/codegpt/toolwindow/chat/ui/ChatMessageResponseBody.java +++ b/src/main/java/ee/carlrobert/codegpt/toolwindow/chat/ui/ChatMessageResponseBody.java @@ -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); } 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 eff134c0..5bc621b0 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,12 +1,11 @@ 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.progress.ProgressManager 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.EncodingManager @@ -98,6 +97,7 @@ class GitCommitActionItem( companion object { private const val MAX_TOKENS = 4096 } + val description: String = gitCommit.id.asString().take(6) override val displayName: String = gitCommit.subject @@ -108,12 +108,21 @@ class GitCommitActionItem( } 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() + return ProgressManager.getInstance().runProcessWithProgressSynchronously( + { + val repository = GitUtil.getProjectRepository(project) + ?: return@runProcessWithProgressSynchronously "" + + val commitId = gitCommit.id.asString() + val diff = GitUtil.getCommitDiff(project, repository, commitId) + .joinToString("\n") + + service().truncateText(diff, MAX_TOKENS, true) + }, + "Getting Diff", + true, + project + ) } }