diff --git a/src/main/java/ee/carlrobert/codegpt/CodeGPTKeys.java b/src/main/java/ee/carlrobert/codegpt/CodeGPTKeys.java index 3420ebad..cc842eeb 100644 --- a/src/main/java/ee/carlrobert/codegpt/CodeGPTKeys.java +++ b/src/main/java/ee/carlrobert/codegpt/CodeGPTKeys.java @@ -2,26 +2,17 @@ package ee.carlrobert.codegpt; import com.intellij.openapi.util.Key; import ee.carlrobert.codegpt.predictions.CodeSuggestionDiffViewer; -import ee.carlrobert.codegpt.settings.prompts.PersonaDetails; -import ee.carlrobert.codegpt.ui.DocumentationDetails; import ee.carlrobert.llm.client.codegpt.CodeGPTUserDetails; -import java.util.List; import okhttp3.Call; public class CodeGPTKeys { public static final Key PREVIOUS_INLAY_TEXT = Key.create("codegpt.editor.inlay.prev-value"); - public static final Key> SELECTED_FILES = - Key.create("codegpt.selectedFiles"); public static final Key IMAGE_ATTACHMENT_FILE_PATH = Key.create("codegpt.imageAttachmentFilePath"); public static final Key CODEGPT_USER_DETAILS = Key.create("codegpt.userDetails"); - public static final Key ADDED_DOCUMENTATION = - Key.create("codegpt.addedDocumentation"); - public static final Key ADDED_PERSONA = - Key.create("codegpt.addedPersona"); public static final Key REMAINING_EDITOR_COMPLETION = Key.create("codegpt.editorCompletionLines"); public static final Key IS_FETCHING_COMPLETION = diff --git a/src/main/java/ee/carlrobert/codegpt/Icons.java b/src/main/java/ee/carlrobert/codegpt/Icons.java index e3eca2d4..596b3958 100644 --- a/src/main/java/ee/carlrobert/codegpt/Icons.java +++ b/src/main/java/ee/carlrobert/codegpt/Icons.java @@ -13,7 +13,6 @@ public final class Icons { IconLoader.getIcon("/icons/codegpt-model.svg", Icons.class); public static final Icon Anthropic = IconLoader.getIcon("/icons/anthropic.svg", Icons.class); public static final Icon Azure = IconLoader.getIcon("/icons/azure.svg", Icons.class); - public static final Icon Databricks = IconLoader.getIcon("/icons/dbrx.svg", Icons.class); public static final Icon DeepSeek = IconLoader.getIcon("/icons/deepseek.png", Icons.class); public static final Icon Qwen = IconLoader.getIcon("/icons/qwen.png", Icons.class); public static final Icon Google = IconLoader.getIcon("/icons/google.svg", Icons.class); @@ -24,10 +23,8 @@ public final class Icons { public static final Icon Send = IconLoader.getIcon("/icons/send.svg", Icons.class); public static final Icon Sparkle = IconLoader.getIcon("/icons/sparkle.svg", Icons.class); public static final Icon You = IconLoader.getIcon("/icons/you.svg", Icons.class); - public static final Icon YouSmall = IconLoader.getIcon("/icons/you_small.png", Icons.class); public static final Icon Ollama = IconLoader.getIcon("/icons/ollama.svg", Icons.class); public static final Icon User = IconLoader.getIcon("/icons/user.svg", Icons.class); - public static final Icon Upload = IconLoader.getIcon("/icons/upload.svg", Icons.class); public static final Icon Lightning = IconLoader.getIcon("/icons/lightning.svg", Icons.class); public static final Icon LightningDisabled = IconLoader.getIcon("/icons/lightning.svg", Icons.class); @@ -45,5 +42,7 @@ public final class Icons { IconLoader.getIcon("/icons/questionMark.svg", Icons.class); public static final Icon ListFiles = IconLoader.getIcon("/icons/listFiles.svg", Icons.class); + public static final Icon InSelection = + IconLoader.getIcon("/icons/inSelection.svg", Icons.class); public static final Icon StatusBarCompletionInProgress = new AnimatedIcon.Default(); } diff --git a/src/main/java/ee/carlrobert/codegpt/actions/IncludeFilesInContextAction.java b/src/main/java/ee/carlrobert/codegpt/actions/IncludeFilesInContextAction.java index 624e86d7..1455a079 100644 --- a/src/main/java/ee/carlrobert/codegpt/actions/IncludeFilesInContextAction.java +++ b/src/main/java/ee/carlrobert/codegpt/actions/IncludeFilesInContextAction.java @@ -23,10 +23,8 @@ import com.intellij.util.ui.FormBuilder; import com.intellij.util.ui.JBUI; import com.intellij.util.ui.UI.PanelFactory; import ee.carlrobert.codegpt.CodeGPTBundle; -import ee.carlrobert.codegpt.CodeGPTKeys; import ee.carlrobert.codegpt.EncodingManager; import ee.carlrobert.codegpt.Icons; -import ee.carlrobert.codegpt.ReferencedFile; import ee.carlrobert.codegpt.settings.IncludedFilesSettings; import ee.carlrobert.codegpt.ui.UIUtil; import ee.carlrobert.codegpt.ui.checkbox.FileCheckboxTree; @@ -82,7 +80,6 @@ public class IncludeFilesInContextAction extends AnAction { totalTokensLabel, checkboxTree); if (show == OK_EXIT_CODE) { - project.putUserData(CodeGPTKeys.SELECTED_FILES, checkboxTree.getReferencedFiles()); project.getMessageBus() .syncPublisher(IncludeFilesInContextNotifier.FILES_INCLUDED_IN_CONTEXT_TOPIC) .filesIncluded(checkboxTree.getReferencedFiles()); @@ -107,7 +104,7 @@ public class IncludeFilesInContextAction extends AnAction { private int fileCount; private int totalTokens; - TotalTokensLabel(List referencedFiles) { + TotalTokensLabel(List referencedFiles) { fileCount = referencedFiles.size(); totalTokens = calculateTotalTokens(referencedFiles); updateText(); @@ -165,9 +162,16 @@ public class IncludeFilesInContextAction extends AnAction { FileUtil.convertLongValue(totalTokens))); } - private int calculateTotalTokens(List referencedFiles) { + private int calculateTotalTokens(List referencedFiles) { return referencedFiles.stream() - .mapToInt(file -> encodingManager.countTokens(file.fileContent())) + .mapToInt(file -> { + try { + return encodingManager.countTokens( + new String(file.contentsToByteArray(), file.getCharset())); + } catch (IOException e) { + throw new RuntimeException("Failed to read file content", e); + } + }) .sum(); } } diff --git a/src/main/java/ee/carlrobert/codegpt/actions/IncludeFilesInContextNotifier.java b/src/main/java/ee/carlrobert/codegpt/actions/IncludeFilesInContextNotifier.java index 796a0090..c2f3b2f3 100644 --- a/src/main/java/ee/carlrobert/codegpt/actions/IncludeFilesInContextNotifier.java +++ b/src/main/java/ee/carlrobert/codegpt/actions/IncludeFilesInContextNotifier.java @@ -1,7 +1,7 @@ package ee.carlrobert.codegpt.actions; +import com.intellij.openapi.vfs.VirtualFile; import com.intellij.util.messages.Topic; -import ee.carlrobert.codegpt.ReferencedFile; import java.util.List; public interface IncludeFilesInContextNotifier { @@ -9,5 +9,5 @@ public interface IncludeFilesInContextNotifier { Topic FILES_INCLUDED_IN_CONTEXT_TOPIC = Topic.create("filesIncludedInContext", IncludeFilesInContextNotifier.class); - void filesIncluded(List includedFiles); + void filesIncluded(List includedFiles); } diff --git a/src/main/java/ee/carlrobert/codegpt/actions/editor/EditorActionsUtil.java b/src/main/java/ee/carlrobert/codegpt/actions/editor/EditorActionsUtil.java index 3eb1aecc..0e85afa6 100644 --- a/src/main/java/ee/carlrobert/codegpt/actions/editor/EditorActionsUtil.java +++ b/src/main/java/ee/carlrobert/codegpt/actions/editor/EditorActionsUtil.java @@ -9,16 +9,12 @@ import com.intellij.openapi.application.ApplicationManager; import com.intellij.openapi.editor.Editor; import com.intellij.openapi.extensions.PluginId; import com.intellij.openapi.project.Project; -import ee.carlrobert.codegpt.CodeGPTKeys; -import ee.carlrobert.codegpt.ReferencedFile; import ee.carlrobert.codegpt.conversations.message.Message; import ee.carlrobert.codegpt.settings.prompts.PromptsSettings; import ee.carlrobert.codegpt.toolwindow.chat.ChatToolWindowContentManager; import ee.carlrobert.codegpt.util.file.FileUtil; -import java.util.Collection; import java.util.LinkedHashMap; import java.util.Map; -import java.util.stream.Stream; import org.apache.commons.text.CaseUtils; public class EditorActionsUtil { @@ -31,15 +27,6 @@ public class EditorActionsUtil { "Refactor", "Refactor the selected code {{selectedCode}}", "Optimize", "Optimize the selected code {{selectedCode}}")); - public static final String[][] DEFAULT_ACTIONS_ARRAY = toArray(DEFAULT_ACTIONS); - - public static String[][] toArray(Map actionsMap) { - return actionsMap.entrySet() - .stream() - .map(entry -> new String[]{entry.getKey(), entry.getValue()}) - .toArray(String[][]::new); - } - public static void refreshActions() { AnAction actionGroup = ActionManager.getInstance().getAction("CodeGPT.MyEditorActionsGroup"); @@ -65,11 +52,6 @@ public class EditorActionsUtil { var message = new Message(prompt.replace( "{SELECTION}", format("%n```%s%n%s%n```", fileExtension, selectedText))); - message.setReferencedFilePaths( - Stream.ofNullable(project.getUserData(CodeGPTKeys.SELECTED_FILES)) - .flatMap(Collection::stream) - .map(ReferencedFile::filePath) - .toList()); toolWindowContentManager.sendMessage(message); } }; diff --git a/src/main/java/ee/carlrobert/codegpt/completions/ToolwindowChatCompletionRequestHandler.java b/src/main/java/ee/carlrobert/codegpt/completions/ToolwindowChatCompletionRequestHandler.java index 891ce84c..4505bbaa 100644 --- a/src/main/java/ee/carlrobert/codegpt/completions/ToolwindowChatCompletionRequestHandler.java +++ b/src/main/java/ee/carlrobert/codegpt/completions/ToolwindowChatCompletionRequestHandler.java @@ -1,6 +1,5 @@ package ee.carlrobert.codegpt.completions; -import com.intellij.openapi.application.ApplicationManager; import ee.carlrobert.codegpt.settings.GeneralSettings; import ee.carlrobert.codegpt.telemetry.TelemetryAction; import ee.carlrobert.llm.client.openai.completion.ErrorDetails; @@ -17,17 +16,15 @@ public class ToolwindowChatCompletionRequestHandler { } public void call(ChatCompletionParameters callParameters) { - ApplicationManager.getApplication().executeOnPooledThread(() -> { - try { - eventSource = startCall(callParameters); - } catch (TotalUsageExceededException e) { - completionResponseEventListener.handleTokensExceeded( - callParameters.getConversation(), - callParameters.getMessage()); - } finally { - sendInfo(callParameters); - } - }); + try { + eventSource = startCall(callParameters); + } catch (TotalUsageExceededException e) { + completionResponseEventListener.handleTokensExceeded( + callParameters.getConversation(), + callParameters.getMessage()); + } finally { + sendInfo(callParameters); + } } public void cancel() { diff --git a/src/main/java/ee/carlrobert/codegpt/conversations/message/Message.java b/src/main/java/ee/carlrobert/codegpt/conversations/message/Message.java index f15ae3c7..62dd34e4 100644 --- a/src/main/java/ee/carlrobert/codegpt/conversations/message/Message.java +++ b/src/main/java/ee/carlrobert/codegpt/conversations/message/Message.java @@ -16,9 +16,10 @@ public class Message { private String prompt; private String response; private List referencedFilePaths; - private @Nullable String imageFilePath; + private String imageFilePath; private boolean webSearchIncluded; private DocumentationDetails documentationDetails; + private String personaName; public Message(String prompt, String response) { this(prompt); @@ -51,7 +52,7 @@ public class Message { this.response = response; } - public List getReferencedFilePaths() { + public @Nullable List getReferencedFilePaths() { return referencedFilePaths; } @@ -75,7 +76,7 @@ public class Message { this.webSearchIncluded = webSearchIncluded; } - public DocumentationDetails getDocumentationDetails() { + public @Nullable DocumentationDetails getDocumentationDetails() { return documentationDetails; } @@ -83,6 +84,14 @@ public class Message { this.documentationDetails = documentationDetails; } + public @Nullable String getPersonaName() { + return personaName; + } + + public void setPersonaName(String personaName) { + this.personaName = personaName; + } + @Override public boolean equals(Object obj) { if (obj == this) { diff --git a/src/main/java/ee/carlrobert/codegpt/toolwindow/chat/ChatToolWindowPanel.java b/src/main/java/ee/carlrobert/codegpt/toolwindow/chat/ChatToolWindowPanel.java index e9aa0866..656afe2b 100644 --- a/src/main/java/ee/carlrobert/codegpt/toolwindow/chat/ChatToolWindowPanel.java +++ b/src/main/java/ee/carlrobert/codegpt/toolwindow/chat/ChatToolWindowPanel.java @@ -1,8 +1,5 @@ package ee.carlrobert.codegpt.toolwindow.chat; -import static java.lang.String.format; -import static java.util.Collections.emptyList; - import com.intellij.ide.BrowserUtil; import com.intellij.openapi.Disposable; import com.intellij.openapi.actionSystem.ActionManager; @@ -20,8 +17,6 @@ import com.intellij.openapi.util.Disposer; import com.intellij.ui.components.ActionLink; import com.intellij.util.ui.JBUI; import ee.carlrobert.codegpt.CodeGPTKeys; -import ee.carlrobert.codegpt.ReferencedFile; -import ee.carlrobert.codegpt.actions.IncludeFilesInContextNotifier; import ee.carlrobert.codegpt.actions.toolwindow.ClearChatWindowAction; import ee.carlrobert.codegpt.actions.toolwindow.CreateNewConversationAction; import ee.carlrobert.codegpt.actions.toolwindow.OpenInEditorAction; @@ -38,9 +33,6 @@ import ee.carlrobert.codegpt.toolwindow.chat.ui.textarea.AttachImageNotifier; import ee.carlrobert.llm.client.codegpt.PricingPlan; import java.awt.BorderLayout; import java.nio.file.Path; -import java.nio.file.Paths; -import java.util.List; -import java.util.stream.Collectors; import javax.swing.BoxLayout; import javax.swing.JComponent; import javax.swing.JPanel; @@ -48,7 +40,6 @@ import org.jetbrains.annotations.NotNull; public class ChatToolWindowPanel extends SimpleToolWindowPanel { - private final ToolWindowFooterNotification selectedFilesNotification; private final ToolWindowFooterNotification imageFileAttachmentNotification; private final ActionLink upgradePlanLink; private ChatToolWindowTabbedPane tabbedPane; @@ -57,8 +48,6 @@ public class ChatToolWindowPanel extends SimpleToolWindowPanel { @NotNull Project project, @NotNull Disposable parentDisposable) { super(true); - selectedFilesNotification = new ToolWindowFooterNotification( - () -> clearSelectedFilesNotification(project)); imageFileAttachmentNotification = new ToolWindowFooterNotification(() -> project.putUserData(CodeGPTKeys.IMAGE_ATTACHMENT_FILE_PATH, "")); upgradePlanLink = new ActionLink("Upgrade your plan", event -> { @@ -71,8 +60,6 @@ public class ChatToolWindowPanel extends SimpleToolWindowPanel { init(project, parentDisposable); var messageBusConnection = project.getMessageBus().connect(); - messageBusConnection.subscribe(IncludeFilesInContextNotifier.FILES_INCLUDED_IN_CONTEXT_TOPIC, - (IncludeFilesInContextNotifier) this::updateSelectedFilesNotification); messageBusConnection.subscribe(AttachImageNotifier.IMAGE_ATTACHMENT_FILE_PATH_TOPIC, (AttachImageNotifier) filePath -> imageFileAttachmentNotification.show( Path.of(filePath).getFileName().toString(), @@ -103,33 +90,10 @@ public class ChatToolWindowPanel extends SimpleToolWindowPanel { return tabbedPane; } - public void updateSelectedFilesNotification(List referencedFiles) { - if (referencedFiles.isEmpty()) { - selectedFilesNotification.hideNotification(); - return; - } - - var referencedFilePaths = referencedFiles.stream() - .map(ReferencedFile::filePath) - .toList(); - selectedFilesNotification.show( - referencedFiles.size() + " files selected", - selectedFilesNotificationDescription(referencedFilePaths)); - } - - private String selectedFilesNotificationDescription(List referencedFilePaths) { - var html = referencedFilePaths.stream() - .map(filePath -> format("
  • %s
  • ", Paths.get(filePath).getFileName().toString())) - .collect(Collectors.joining()); - return format("
      %s
    ", html); - } - - public void clearNotifications(Project project) { - selectedFilesNotification.hideNotification(); + public void clearImageNotifications(Project project) { imageFileAttachmentNotification.hideNotification(); project.putUserData(CodeGPTKeys.IMAGE_ATTACHMENT_FILE_PATH, ""); - project.putUserData(CodeGPTKeys.SELECTED_FILES, emptyList()); } private void init(Project project, Disposable parentDisposable) { @@ -156,7 +120,6 @@ public class ChatToolWindowPanel extends SimpleToolWindowPanel { setToolbar(actionToolbarPanel); var notificationContainer = new JPanel(new BorderLayout()); notificationContainer.setLayout(new BoxLayout(notificationContainer, BoxLayout.PAGE_AXIS)); - notificationContainer.add(selectedFilesNotification); notificationContainer.add(imageFileAttachmentNotification); setContent(JBUI.Panels.simplePanel(tabbedPane).addToBottom(notificationContainer)); @@ -190,13 +153,6 @@ public class ChatToolWindowPanel extends SimpleToolWindowPanel { return tabbedPane; } - public void clearSelectedFilesNotification(Project project) { - project.putUserData(CodeGPTKeys.SELECTED_FILES, emptyList()); - project.getMessageBus() - .syncPublisher(IncludeFilesInContextNotifier.FILES_INCLUDED_IN_CONTEXT_TOPIC) - .filesIncluded(emptyList()); - } - private static class SelectedPersonaActionLink extends DumbAwareAction implements CustomComponentAction { 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 ec96ea26..3b853477 100644 --- a/src/main/java/ee/carlrobert/codegpt/toolwindow/chat/ChatToolWindowTabPanel.java +++ b/src/main/java/ee/carlrobert/codegpt/toolwindow/chat/ChatToolWindowTabPanel.java @@ -21,6 +21,8 @@ import ee.carlrobert.codegpt.completions.ToolwindowChatCompletionRequestHandler; import ee.carlrobert.codegpt.conversations.Conversation; import ee.carlrobert.codegpt.conversations.ConversationService; import ee.carlrobert.codegpt.conversations.message.Message; +import ee.carlrobert.codegpt.settings.GeneralSettings; +import ee.carlrobert.codegpt.settings.service.ServiceType; import ee.carlrobert.codegpt.telemetry.TelemetryAction; import ee.carlrobert.codegpt.toolwindow.chat.editor.actions.CopyAction; import ee.carlrobert.codegpt.toolwindow.chat.ui.ChatMessageResponseBody; @@ -31,18 +33,18 @@ import ee.carlrobert.codegpt.toolwindow.ui.ChatToolWindowLandingPanel; import ee.carlrobert.codegpt.toolwindow.ui.ResponseMessagePanel; import ee.carlrobert.codegpt.toolwindow.ui.UserMessagePanel; import ee.carlrobert.codegpt.ui.OverlayUtil; -import ee.carlrobert.codegpt.ui.textarea.AppliedActionInlay; import ee.carlrobert.codegpt.ui.textarea.UserInputPanel; +import ee.carlrobert.codegpt.ui.textarea.header.FileTagDetails; +import ee.carlrobert.codegpt.ui.textarea.header.GitCommitTagDetails; +import ee.carlrobert.codegpt.ui.textarea.header.HeaderTagDetails; +import ee.carlrobert.codegpt.ui.textarea.header.PersonaTagDetails; import ee.carlrobert.codegpt.util.EditorUtil; import ee.carlrobert.codegpt.util.file.FileUtil; import git4idea.GitCommit; import java.awt.BorderLayout; -import java.io.File; -import java.util.ArrayList; -import java.util.LinkedHashMap; import java.util.List; +import java.util.Optional; import java.util.UUID; -import java.util.stream.Stream; import javax.swing.JComponent; import javax.swing.JPanel; import kotlin.Unit; @@ -80,6 +82,7 @@ public class ChatToolWindowTabPanel implements Disposable { project, conversation, totalTokensPanel, + this, this::handleSubmit, this::handleCancel); userInputPanel.requestFocus(); @@ -125,50 +128,56 @@ public class ChatToolWindowTabPanel implements Disposable { userInputPanel.addCommitReferences(gitCommits); } - public List getReferencedFiles() { - var referencedFiles = new LinkedHashMap(); + public List getSelectedTags() { + return userInputPanel.getSelectedTags(); + } - conversation.getMessages().stream() - .flatMap(prevMessage -> { - if (prevMessage.getReferencedFilePaths() != null) { - return prevMessage.getReferencedFilePaths().stream(); - } - return Stream.empty(); - }) - .forEach(filePath -> { - try { - referencedFiles.put(filePath, ReferencedFile.from(new File(filePath))); - } catch (Exception ex) { - LOG.error("Failed to create referenced file for path: " + filePath, ex); - } - }); + private ChatCompletionParameters getCallParameters( + Message message, + ConversationType conversationType) { + var selectedTags = userInputPanel.getSelectedTags(); + var builder = ChatCompletionParameters.builder(conversation, message) + .sessionId(chatSession.getId()) + .conversationType(conversationType) + .imageDetailsFromPath(CodeGPTKeys.IMAGE_ATTACHMENT_FILE_PATH.get(project)) + .referencedFiles(getReferencedFiles(selectedTags)); - List selectedFiles = project.getUserData(CodeGPTKeys.SELECTED_FILES); - if (selectedFiles != null) { - selectedFiles.forEach(file -> referencedFiles.put(file.filePath(), file)); - } + findTagOfType(selectedTags, PersonaTagDetails.class) + .ifPresent(tag -> builder.personaDetails(tag.getPersonaDetails())); - return new ArrayList<>(referencedFiles.values()); + findTagOfType(selectedTags, GitCommitTagDetails.class) + .ifPresent(tag -> builder.gitDiff(tag.getGitCommit().getFullMessage())); + + return builder.build(); + } + + private List getReferencedFiles() { + return getReferencedFiles(userInputPanel.getSelectedTags()); + } + + private List getReferencedFiles(List tags) { + return tags.stream() + .filter(FileTagDetails.class::isInstance) + .map(it -> ReferencedFile.from(((FileTagDetails) it).getVirtualFile())) + .toList(); + } + + private Optional findTagOfType( + List tags, + Class tagClass) { + return tags.stream() + .filter(tagClass::isInstance) + .map(tagClass::cast) + .findFirst(); } public void sendMessage(Message message, ConversationType conversationType) { ApplicationManager.getApplication().invokeLater(() -> { - var callParameters = ChatCompletionParameters.builder(conversation, message) - .sessionId(chatSession.getId()) - .conversationType(conversationType) - .imageDetailsFromPath(CodeGPTKeys.IMAGE_ATTACHMENT_FILE_PATH.get(project)) - .persona(CodeGPTKeys.ADDED_PERSONA.get(project)) - .referencedFiles(getReferencedFiles()) - .build(); - - CodeGPTKeys.ADDED_PERSONA.set(project, null); - - var referencedFiles = callParameters.getReferencedFiles(); - if ((referencedFiles != null && !referencedFiles.isEmpty()) - || callParameters.getImageDetails() != null) { + var callParameters = getCallParameters(message, conversationType); + if (callParameters.getImageDetails() != null) { project.getService(ChatToolWindowContentManager.class) .tryFindChatToolWindowPanel() - .ifPresent(panel -> panel.clearNotifications(project)); + .ifPresent(panel -> panel.clearImageNotifications(project)); } totalTokensPanel.updateConversationTokens(conversation); @@ -294,13 +303,14 @@ public class ChatToolWindowTabPanel implements Disposable { userMessagePanel.disableActions(List.of("RELOAD", "DELETE")); responseMessagePanel.disableActions(List.of("COPY")); - requestHandler.call(callParameters); + ApplicationManager.getApplication() + .executeOnPooledThread(() -> requestHandler.call(callParameters)); } - private Unit handleSubmit(String text, List appliedInlayActions) { + private Unit handleSubmit(String text, List appliedTags) { var messageBuilder = new MessageBuilder(project, text) .withSelectedEditorContent() - .withInlays(appliedInlayActions); + .withInlays(appliedTags); List referencedFiles = getReferencedFiles(); if (!referencedFiles.isEmpty()) { @@ -328,9 +338,11 @@ public class ChatToolWindowTabPanel implements Disposable { panel.setBorder(JBUI.Borders.compound( JBUI.Borders.customLine(JBColor.border(), 1, 0, 0, 0), JBUI.Borders.empty(8))); - panel.add(JBUI.Panels.simplePanel(totalTokensPanel) - .withBorder(JBUI.Borders.emptyBottom(8)), BorderLayout.NORTH); - panel.add(JBUI.Panels.simplePanel(userInputPanel), BorderLayout.CENTER); + if (GeneralSettings.getSelectedService() != ServiceType.CODEGPT) { + panel.add(JBUI.Panels.simplePanel(totalTokensPanel) + .withBorder(JBUI.Borders.emptyBottom(8)), BorderLayout.NORTH); + } + panel.add(userInputPanel, BorderLayout.CENTER); return panel; } @@ -399,4 +411,4 @@ public class ChatToolWindowTabPanel implements Disposable { rootPanel.add(createUserPromptPanel(), BorderLayout.SOUTH); return rootPanel; } -} +} \ No newline at end of file diff --git a/src/main/java/ee/carlrobert/codegpt/toolwindow/chat/ChatToolWindowTabbedPane.java b/src/main/java/ee/carlrobert/codegpt/toolwindow/chat/ChatToolWindowTabbedPane.java index ca7551f5..c7694ef0 100644 --- a/src/main/java/ee/carlrobert/codegpt/toolwindow/chat/ChatToolWindowTabbedPane.java +++ b/src/main/java/ee/carlrobert/codegpt/toolwindow/chat/ChatToolWindowTabbedPane.java @@ -23,7 +23,6 @@ import javax.swing.BorderFactory; import javax.swing.JButton; import javax.swing.JPanel; import javax.swing.JPopupMenu; -import javax.swing.SwingUtilities; public class ChatToolWindowTabbedPane extends JBTabbedPane { @@ -65,7 +64,7 @@ public class ChatToolWindowTabbedPane extends JBTabbedPane { if (nextIndex > 0) { setTabComponentAt(nextIndex, createCloseableTabButtonPanel(title)); - SwingUtilities.invokeLater(toolWindowPanel::requestFocusForTextArea); + toolWindowPanel.requestFocusForTextArea(); } Disposer.register(parentDisposable, toolWindowPanel); 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 deleted file mode 100644 index 3beaad34..00000000 --- a/src/main/java/ee/carlrobert/codegpt/toolwindow/chat/actionprocessor/ActionProcessor.java +++ /dev/null @@ -1,9 +0,0 @@ -package ee.carlrobert.codegpt.toolwindow.chat.actionprocessor; - -import ee.carlrobert.codegpt.conversations.message.Message; -import ee.carlrobert.codegpt.ui.textarea.AppliedActionInlay; - -public interface ActionProcessor { - - void process(Message message, AppliedActionInlay action, 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 deleted file mode 100644 index fb75927f..00000000 --- a/src/main/java/ee/carlrobert/codegpt/toolwindow/chat/actionprocessor/ActionProcessorFactory.java +++ /dev/null @@ -1,18 +0,0 @@ -package ee.carlrobert.codegpt.toolwindow.chat.actionprocessor; - -import com.intellij.openapi.project.Project; -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(Project project, AppliedActionInlay action) { - if (action instanceof AppliedSuggestionActionInlay) { - return new SuggestionActionProcessor(project); - } 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 deleted file mode 100644 index 13d64d49..00000000 --- a/src/main/java/ee/carlrobert/codegpt/toolwindow/chat/actionprocessor/CodeActionProcessor.java +++ /dev/null @@ -1,24 +0,0 @@ -package ee.carlrobert.codegpt.toolwindow.chat.actionprocessor; - -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, StringBuilder promptBuilder) { - if (!(action instanceof AppliedCodeActionInlay codeAction)) { - throw new IllegalArgumentException("Invalid action type"); - } - processCodeAction(codeAction, promptBuilder); - } - - private void processCodeAction(AppliedCodeActionInlay action, StringBuilder promptBuilder) { - promptBuilder - .append("\n```%s\n".formatted(FileUtil.getFileExtension(action.getEditorFile().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 deleted file mode 100644 index 796cb4c6..00000000 --- a/src/main/java/ee/carlrobert/codegpt/toolwindow/chat/actionprocessor/SuggestionActionProcessor.java +++ /dev/null @@ -1,63 +0,0 @@ -package ee.carlrobert.codegpt.toolwindow.chat.actionprocessor; - -import com.intellij.openapi.project.Project; -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.WebSearchActionItem; - -public class SuggestionActionProcessor implements ActionProcessor { - - private final Project project; - - public SuggestionActionProcessor(Project project) { - this.project = project; - } - - @Override - public void process( - Message message, - AppliedActionInlay action, - StringBuilder promptBuilder) { - if (!(action instanceof AppliedSuggestionActionInlay suggestionAction)) { - throw new IllegalArgumentException("Invalid action type"); - } - processSuggestionAction(message, suggestionAction, promptBuilder); - } - - private void processSuggestionAction( - Message message, - AppliedSuggestionActionInlay action, - StringBuilder promptBuilder) { - message.setWebSearchIncluded(action.getSuggestion() instanceof WebSearchActionItem); - - processDocumentationAction(message, action); - processGitCommitAction(action, promptBuilder); - } - - private void processDocumentationAction(Message message, AppliedSuggestionActionInlay action) { - 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 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/SelectedFilesAccordion.java b/src/main/java/ee/carlrobert/codegpt/toolwindow/chat/ui/SelectedFilesAccordion.java index 59e83d24..b1fbaead 100644 --- a/src/main/java/ee/carlrobert/codegpt/toolwindow/chat/ui/SelectedFilesAccordion.java +++ b/src/main/java/ee/carlrobert/codegpt/toolwindow/chat/ui/SelectedFilesAccordion.java @@ -4,7 +4,6 @@ import static java.lang.String.format; import com.intellij.icons.AllIcons.General; import com.intellij.openapi.fileEditor.FileEditorManager; -import com.intellij.openapi.fileTypes.FileTypeManager; import com.intellij.openapi.project.Project; import com.intellij.openapi.vfs.LocalFileSystem; import com.intellij.ui.components.ActionLink; @@ -50,8 +49,7 @@ public class SelectedFilesAccordion extends JPanel { FileEditorManager.getInstance(project) .openFile(Objects.requireNonNull(virtualFile), true); }); - actionLink.setIcon( - FileTypeManager.getInstance().getFileTypeByFile(virtualFile).getIcon()); + actionLink.setIcon(virtualFile.getFileType().getIcon()); return actionLink; }) .forEach(link -> { @@ -63,16 +61,18 @@ public class SelectedFilesAccordion extends JPanel { private JToggleButton createToggleButton(JPanel contentPane, int fileCount) { var accordionToggle = new JToggleButton( - format("Referenced files (+%d)", fileCount), General.ArrowDown); + format("Referenced files (+%d)", fileCount), General.ArrowUp); accordionToggle.setFocusPainted(false); accordionToggle.setContentAreaFilled(false); accordionToggle.setBackground(getBackground()); - accordionToggle.setSelectedIcon(General.ArrowUp); + accordionToggle.setSelectedIcon(General.ArrowDown); accordionToggle.setBorder(null); - accordionToggle.setHorizontalAlignment(SwingConstants.LEADING); - accordionToggle.setHorizontalTextPosition(SwingConstants.LEADING); + accordionToggle.setSelected(true); + accordionToggle.setHorizontalAlignment(SwingConstants.LEFT); + accordionToggle.setHorizontalTextPosition(SwingConstants.RIGHT); + accordionToggle.setIconTextGap(4); accordionToggle.addItemListener(e -> contentPane.setVisible(e.getStateChange() == ItemEvent.SELECTED)); return accordionToggle; } -} +} \ No newline at end of file diff --git a/src/main/java/ee/carlrobert/codegpt/toolwindow/chat/ui/textarea/ModelComboBoxAction.java b/src/main/java/ee/carlrobert/codegpt/toolwindow/chat/ui/textarea/ModelComboBoxAction.java index 20bdacda..a05e9fa6 100644 --- a/src/main/java/ee/carlrobert/codegpt/toolwindow/chat/ui/textarea/ModelComboBoxAction.java +++ b/src/main/java/ee/carlrobert/codegpt/toolwindow/chat/ui/textarea/ModelComboBoxAction.java @@ -18,6 +18,7 @@ import com.intellij.openapi.actionSystem.DefaultActionGroup; import com.intellij.openapi.actionSystem.Presentation; import com.intellij.openapi.actionSystem.ex.ComboBoxAction; import com.intellij.openapi.application.ApplicationManager; +import com.intellij.openapi.editor.colors.EditorColorsManager; import com.intellij.openapi.project.DumbAwareAction; import com.intellij.openapi.project.Project; import com.intellij.openapi.ui.popup.JBPopup; @@ -87,6 +88,8 @@ public class ModelComboBoxAction extends ComboBoxAction { @NotNull Presentation presentation, @NotNull String place) { ComboBoxButton button = createComboBoxButton(presentation); + button.setForeground( + EditorColorsManager.getInstance().getGlobalScheme().getDefaultForeground()); button.setBorder(null); button.putClientProperty("JButton.backgroundColor", new Color(0, 0, 0, 0)); return button; diff --git a/src/main/java/ee/carlrobert/codegpt/toolwindow/chat/ui/textarea/TotalTokensPanel.java b/src/main/java/ee/carlrobert/codegpt/toolwindow/chat/ui/textarea/TotalTokensPanel.java index 7a7400a2..c9a325d2 100644 --- a/src/main/java/ee/carlrobert/codegpt/toolwindow/chat/ui/textarea/TotalTokensPanel.java +++ b/src/main/java/ee/carlrobert/codegpt/toolwindow/chat/ui/textarea/TotalTokensPanel.java @@ -14,12 +14,10 @@ import com.intellij.openapi.editor.event.SelectionListener; import com.intellij.openapi.project.Project; import com.intellij.ui.components.JBLabel; import com.intellij.util.ui.JBUI; -import ee.carlrobert.codegpt.CodeGPTKeys; import ee.carlrobert.codegpt.EncodingManager; import ee.carlrobert.codegpt.ReferencedFile; import ee.carlrobert.codegpt.actions.IncludeFilesInContextNotifier; import ee.carlrobert.codegpt.conversations.Conversation; -import ee.carlrobert.codegpt.conversations.message.Message; import ee.carlrobert.codegpt.settings.GeneralSettings; import ee.carlrobert.codegpt.settings.prompts.PromptsSettings; import ee.carlrobert.codegpt.settings.service.ServiceType; @@ -47,10 +45,7 @@ public class TotalTokensPanel extends JPanel { @Nullable String highlightedText, Disposable parentDisposable) { super(new FlowLayout(FlowLayout.LEADING, 0, 0)); - this.totalTokensDetails = createTokenDetails( - conversation, - project.getUserData(CodeGPTKeys.SELECTED_FILES), - highlightedText); + this.totalTokensDetails = createTokenDetails(conversation, highlightedText); this.label = getLabel(totalTokensDetails); setBorder(JBUI.Borders.empty(4)); @@ -63,7 +58,9 @@ public class TotalTokensPanel extends JPanel { project.getMessageBus() .connect() .subscribe(IncludeFilesInContextNotifier.FILES_INCLUDED_IN_CONTEXT_TOPIC, - (IncludeFilesInContextNotifier) this::updateReferencedFilesTokens); + (IncludeFilesInContextNotifier) includedFiles -> + updateReferencedFilesTokens( + includedFiles.stream().map(ReferencedFile::from).toList())); } private void addSelectionListeners(Disposable parentDisposable) { @@ -107,13 +104,6 @@ public class TotalTokensPanel extends JPanel { label.setText(getLabelHtml(total)); } - public void updateConversationTokens(Conversation conversation, Message message) { - totalTokensDetails.setConversationTokens( - encodingManager.countConversationTokens(conversation) - + encodingManager.countMessageTokens("user", message.getPrompt())); - update(); - } - public void updateConversationTokens(Conversation conversation) { totalTokensDetails.setConversationTokens(encodingManager.countConversationTokens(conversation)); update(); @@ -138,16 +128,10 @@ public class TotalTokensPanel extends JPanel { private TotalTokensDetails createTokenDetails( Conversation conversation, - List includedFiles, @Nullable String highlightedText) { var tokenDetails = new TotalTokensDetails( encodingManager.countTokens(PromptsSettings.getSelectedPersonaSystemPrompt())); tokenDetails.setConversationTokens(encodingManager.countConversationTokens(conversation)); - if (includedFiles != null) { - tokenDetails.setReferencedFilesTokens(includedFiles.stream() - .mapToInt(file -> encodingManager.countTokens(file.fileContent())) - .sum()); - } if (highlightedText != null) { tokenDetails.setHighlightedTokens(encodingManager.countTokens(highlightedText)); } diff --git a/src/main/java/ee/carlrobert/codegpt/ui/checkbox/FileCheckboxTree.java b/src/main/java/ee/carlrobert/codegpt/ui/checkbox/FileCheckboxTree.java index f88a0d0d..ff9158d6 100644 --- a/src/main/java/ee/carlrobert/codegpt/ui/checkbox/FileCheckboxTree.java +++ b/src/main/java/ee/carlrobert/codegpt/ui/checkbox/FileCheckboxTree.java @@ -16,7 +16,7 @@ public abstract class FileCheckboxTree extends CheckboxTree { super(cellRenderer, node); } - public abstract List getReferencedFiles(); + public abstract List getReferencedFiles(); protected static void updateFilePresentation( ColoredTreeCellRenderer textRenderer, diff --git a/src/main/java/ee/carlrobert/codegpt/ui/checkbox/PsiElementCheckboxTree.java b/src/main/java/ee/carlrobert/codegpt/ui/checkbox/PsiElementCheckboxTree.java index 73229577..43adff29 100644 --- a/src/main/java/ee/carlrobert/codegpt/ui/checkbox/PsiElementCheckboxTree.java +++ b/src/main/java/ee/carlrobert/codegpt/ui/checkbox/PsiElementCheckboxTree.java @@ -2,13 +2,12 @@ package ee.carlrobert.codegpt.ui.checkbox; import com.intellij.icons.AllIcons; import com.intellij.openapi.util.Iconable; +import com.intellij.openapi.vfs.VirtualFile; import com.intellij.psi.PsiDirectory; import com.intellij.psi.PsiElement; import com.intellij.psi.PsiFile; import com.intellij.psi.impl.file.PsiDirectoryImpl; import com.intellij.ui.CheckedTreeNode; -import ee.carlrobert.codegpt.ReferencedFile; -import java.io.File; import java.util.Arrays; import java.util.List; import java.util.Optional; @@ -21,7 +20,7 @@ public class PsiElementCheckboxTree extends FileCheckboxTree { setRootVisible(true); } - public List getReferencedFiles() { + public List getReferencedFiles() { var checkedNodes = getCheckedNodes( PsiElement.class, node -> Optional.ofNullable(node.getContainingFile()) @@ -32,7 +31,7 @@ public class PsiElementCheckboxTree extends FileCheckboxTree { } return Arrays.stream(checkedNodes) - .map(item -> ReferencedFile.from(item.getContainingFile().getVirtualFile())) + .map(item -> item.getContainingFile().getVirtualFile()) .toList(); } diff --git a/src/main/java/ee/carlrobert/codegpt/ui/checkbox/VirtualFileCheckboxTree.java b/src/main/java/ee/carlrobert/codegpt/ui/checkbox/VirtualFileCheckboxTree.java index 69c14468..93fbea00 100644 --- a/src/main/java/ee/carlrobert/codegpt/ui/checkbox/VirtualFileCheckboxTree.java +++ b/src/main/java/ee/carlrobert/codegpt/ui/checkbox/VirtualFileCheckboxTree.java @@ -19,7 +19,7 @@ public class VirtualFileCheckboxTree extends FileCheckboxTree { super(createFileTypesRenderer(), createRootNode(rootFiles)); } - public List getReferencedFiles() { + public List getReferencedFiles() { var checkedNodes = getCheckedNodes(VirtualFile.class, Objects::nonNull); if (checkedNodes.length > 1024) { OverlayUtil.showNotification("Too many files selected.", NotificationType.ERROR); @@ -27,13 +27,6 @@ public class VirtualFileCheckboxTree extends FileCheckboxTree { } return Arrays.stream(checkedNodes) - .map(item -> { - var file = new File(item.getPath()); - if (file.isFile()) { - return ReferencedFile.from(item); - } - return null; - }) .filter(Objects::nonNull) .toList(); } diff --git a/src/main/kotlin/ee/carlrobert/codegpt/CodeGPTEditorFactoryListener.kt b/src/main/kotlin/ee/carlrobert/codegpt/CodeGPTEditorFactoryListener.kt new file mode 100644 index 00000000..b3cbe017 --- /dev/null +++ b/src/main/kotlin/ee/carlrobert/codegpt/CodeGPTEditorFactoryListener.kt @@ -0,0 +1,70 @@ +package ee.carlrobert.codegpt + +import com.intellij.openapi.editor.Editor +import com.intellij.openapi.editor.EditorKind +import com.intellij.openapi.editor.SelectionModel +import com.intellij.openapi.editor.event.EditorFactoryEvent +import com.intellij.openapi.editor.event.EditorFactoryListener +import com.intellij.openapi.editor.event.SelectionEvent +import com.intellij.openapi.editor.event.SelectionListener +import com.intellij.openapi.vfs.VirtualFile +import com.intellij.util.messages.Topic + +sealed interface EditorNotifier { + interface SelectionChange : EditorNotifier { + fun selectionChanged(selectionModel: SelectionModel, virtualFile: VirtualFile) + + companion object { + val TOPIC = Topic.create("codegpt.editorSelectionChanged", SelectionChange::class.java) + } + } + + interface Released : EditorNotifier { + fun editorReleased(editor: Editor) + + companion object { + val TOPIC = Topic.create("codegpt.editorReleased", Released::class.java) + } + } + + interface Created : EditorNotifier { + fun editorCreated(editor: Editor) + + companion object { + val TOPIC = Topic.create("codegpt.editorCreated", Created::class.java) + } + } +} + +class CodeGPTEditorFactoryListener : EditorFactoryListener { + + override fun editorCreated(event: EditorFactoryEvent) { + if (event.editor.editorKind != EditorKind.MAIN_EDITOR) { + return + } + + val project = event.editor.project ?: return + project.messageBus + .syncPublisher(EditorNotifier.Created.TOPIC) + .editorCreated(event.editor) + event.editor.selectionModel.addSelectionListener(object : SelectionListener { + override fun selectionChanged(e: SelectionEvent) { + val virtualFile = e.editor.virtualFile ?: return + project.messageBus + .syncPublisher(EditorNotifier.SelectionChange.TOPIC) + .selectionChanged(e.editor.selectionModel, virtualFile) + } + }) + } + + override fun editorReleased(event: EditorFactoryEvent) { + if (event.editor.editorKind != EditorKind.MAIN_EDITOR) { + return + } + + val project = event.editor.project ?: return + project.messageBus + .syncPublisher(EditorNotifier.Released.TOPIC) + .editorReleased(event.editor) + } +} \ No newline at end of file diff --git a/src/main/kotlin/ee/carlrobert/codegpt/completions/CompletionParameters.kt b/src/main/kotlin/ee/carlrobert/codegpt/completions/CompletionParameters.kt index 890d9c4d..97b0edad 100644 --- a/src/main/kotlin/ee/carlrobert/codegpt/completions/CompletionParameters.kt +++ b/src/main/kotlin/ee/carlrobert/codegpt/completions/CompletionParameters.kt @@ -19,7 +19,7 @@ class ChatCompletionParameters private constructor( var retry: Boolean, var imageDetails: ImageDetails?, var referencedFiles: List?, - var persona: PersonaDetails?, + var personaDetails: PersonaDetails?, ) : CompletionParameters { fun toBuilder(): Builder { @@ -29,7 +29,7 @@ class ChatCompletionParameters private constructor( retry(this@ChatCompletionParameters.retry) imageDetails(this@ChatCompletionParameters.imageDetails) referencedFiles(this@ChatCompletionParameters.referencedFiles) - persona(this@ChatCompletionParameters.persona) + personaDetails(this@ChatCompletionParameters.personaDetails) } } @@ -39,7 +39,8 @@ class ChatCompletionParameters private constructor( private var retry: Boolean = false private var imageDetails: ImageDetails? = null private var referencedFiles: List? = null - private var persona: PersonaDetails? = null + private var personaDetails: PersonaDetails? = null + private var gitDiff: String = "" fun sessionId(sessionId: UUID?) = apply { this.sessionId = sessionId } fun conversationType(conversationType: ConversationType) = @@ -56,10 +57,12 @@ class ChatCompletionParameters private constructor( } } + fun gitDiff(gitDiff: String) = apply { this.gitDiff = gitDiff } + fun referencedFiles(referencedFiles: List?) = apply { this.referencedFiles = referencedFiles } - fun persona(persona: PersonaDetails?) = apply { this.persona = persona } + fun personaDetails(personaDetails: PersonaDetails?) = apply { this.personaDetails = personaDetails } fun build(): ChatCompletionParameters { return ChatCompletionParameters( @@ -70,7 +73,7 @@ class ChatCompletionParameters private constructor( retry, imageDetails, referencedFiles, - persona + personaDetails ) } } diff --git a/src/main/kotlin/ee/carlrobert/codegpt/completions/factory/CodeGPTRequestFactory.kt b/src/main/kotlin/ee/carlrobert/codegpt/completions/factory/CodeGPTRequestFactory.kt index dff0b373..5434b564 100644 --- a/src/main/kotlin/ee/carlrobert/codegpt/completions/factory/CodeGPTRequestFactory.kt +++ b/src/main/kotlin/ee/carlrobert/codegpt/completions/factory/CodeGPTRequestFactory.kt @@ -41,17 +41,15 @@ class CodeGPTRequestFactory : BaseRequestFactory() { if (params.message.isWebSearchIncluded) { requestBuilder.setWebSearchIncluded(true) } - val documentationDetails = params.message.documentationDetails - if (documentationDetails != null) { + params.message.documentationDetails?.let { requestBuilder.setDocumentationDetails( - DocumentationDetails(documentationDetails.name, documentationDetails.url) + DocumentationDetails(it.name, it.url) ) } params.referencedFiles?.let { - val fileContexts = it.map { file -> + requestBuilder.setContext(AdditionalRequestContext(it.map { file -> ContextFile(file.fileName(), file.fileContent()) - } - requestBuilder.setContext(AdditionalRequestContext(fileContexts)) + })) } return requestBuilder.build() diff --git a/src/main/kotlin/ee/carlrobert/codegpt/completions/factory/OpenAIRequestFactory.kt b/src/main/kotlin/ee/carlrobert/codegpt/completions/factory/OpenAIRequestFactory.kt index 44230989..72b177f7 100644 --- a/src/main/kotlin/ee/carlrobert/codegpt/completions/factory/OpenAIRequestFactory.kt +++ b/src/main/kotlin/ee/carlrobert/codegpt/completions/factory/OpenAIRequestFactory.kt @@ -155,7 +155,7 @@ class OpenAIRequestFactory : CompletionRequestFactory { val role = if ("o1-mini" == model || "o1-preview" == model) "user" else "system" if (callParameters.conversationType == ConversationType.DEFAULT) { - val sessionPersonaDetails = callParameters.persona + val sessionPersonaDetails = callParameters.personaDetails if (sessionPersonaDetails == null) { messages.add( OpenAIChatCompletionStandardMessage( diff --git a/src/main/kotlin/ee/carlrobert/codegpt/settings/documentation/DocumentationSettings.kt b/src/main/kotlin/ee/carlrobert/codegpt/settings/documentation/DocumentationSettings.kt index 24072598..95d3afbc 100644 --- a/src/main/kotlin/ee/carlrobert/codegpt/settings/documentation/DocumentationSettings.kt +++ b/src/main/kotlin/ee/carlrobert/codegpt/settings/documentation/DocumentationSettings.kt @@ -36,6 +36,7 @@ class DocumentationDetailsState : BaseState() { } private val DEFAULT_DOCUMENTATIONS = mutableListOf( + getDocState("Spring Framework API", "https://docs.spring.io/spring-framework/docs/current/javadoc-api/"), getDocState("Astro Runtime API", "https://docs.astro.build/en/reference/api-reference/"), getDocState("Flask API", "https://flask.palletsprojects.com/en/3.0.x/api/"), getDocState("Flutter API", "https://api.flutter.dev/"), diff --git a/src/main/kotlin/ee/carlrobert/codegpt/toolwindow/chat/MessageBuilder.kt b/src/main/kotlin/ee/carlrobert/codegpt/toolwindow/chat/MessageBuilder.kt index d4e18df7..11d4f080 100644 --- a/src/main/kotlin/ee/carlrobert/codegpt/toolwindow/chat/MessageBuilder.kt +++ b/src/main/kotlin/ee/carlrobert/codegpt/toolwindow/chat/MessageBuilder.kt @@ -4,8 +4,8 @@ import com.intellij.openapi.editor.Editor import com.intellij.openapi.project.Project import ee.carlrobert.codegpt.ReferencedFile import ee.carlrobert.codegpt.conversations.message.Message -import ee.carlrobert.codegpt.toolwindow.chat.actionprocessor.ActionProcessorFactory -import ee.carlrobert.codegpt.ui.textarea.AppliedActionInlay +import ee.carlrobert.codegpt.ui.textarea.TagProcessorFactory +import ee.carlrobert.codegpt.ui.textarea.header.HeaderTagDetails import ee.carlrobert.codegpt.util.EditorUtil.getSelectedEditor class MessageBuilder(private val project: Project, private val text: String) { @@ -20,9 +20,9 @@ class MessageBuilder(private val project: Project, private val text: String) { return this } - fun withInlays(inlays: List): MessageBuilder { - if (inlays.isNotEmpty()) { - inlayContent = processInlays(message, inlays) + fun withInlays(appliedTags: List): MessageBuilder { + if (appliedTags.isNotEmpty()) { + inlayContent = processTags(message, appliedTags) } return this } @@ -65,15 +65,12 @@ class MessageBuilder(private val project: Project, private val text: String) { } ?: "" } - private fun processInlays( + private fun processTags( message: Message, - inlays: List + tags: List ): String = buildString { - inlays - .sortedBy { it.inlay.offset } - .forEach { actionInlay -> - ActionProcessorFactory.getProcessor(project, actionInlay) - .process(message, actionInlay, this) - } + tags.forEach { + TagProcessorFactory.getProcessor(project, it).process(message, it, this) + } } } diff --git a/src/main/kotlin/ee/carlrobert/codegpt/toolwindow/ui/UserMessagePanel.kt b/src/main/kotlin/ee/carlrobert/codegpt/toolwindow/ui/UserMessagePanel.kt index 57c22b47..e096fab6 100644 --- a/src/main/kotlin/ee/carlrobert/codegpt/toolwindow/ui/UserMessagePanel.kt +++ b/src/main/kotlin/ee/carlrobert/codegpt/toolwindow/ui/UserMessagePanel.kt @@ -13,7 +13,6 @@ import com.intellij.util.ui.JBFont import com.intellij.util.ui.JBUI import com.intellij.util.ui.components.BorderLayoutPanel import ee.carlrobert.codegpt.CodeGPTBundle -import ee.carlrobert.codegpt.CodeGPTKeys import ee.carlrobert.codegpt.Icons import ee.carlrobert.codegpt.conversations.message.Message import ee.carlrobert.codegpt.events.WebSearchEventDetails @@ -26,10 +25,7 @@ import java.io.IOException import java.nio.file.Files import java.nio.file.Paths import java.util.* -import javax.swing.DefaultListModel -import javax.swing.JPanel -import javax.swing.SwingConstants - +import javax.swing.* class UserMessagePanel( private val project: Project, @@ -128,46 +124,62 @@ class UserMessagePanel( } private fun getAdditionalContextPanel(project: Project?, message: Message): JPanel? { - val addedDocumentation = CodeGPTKeys.ADDED_DOCUMENTATION[project] + val documentationDetails = message.documentationDetails val referencedFilePaths = message.referencedFilePaths ?: emptyList() - if (addedDocumentation == null && referencedFilePaths.isEmpty()) { + if (documentationDetails == null && referencedFilePaths.isEmpty() && message.personaName.isNullOrEmpty()) { return null } return BorderLayoutPanel().apply { isOpaque = false - if (addedDocumentation != null) { - val listModel = DefaultListModel() - listModel.addElement( - WebSearchEventDetails( - UUID.randomUUID(), addedDocumentation.name, - addedDocumentation.url, addedDocumentation.url + val additionalContextPanel = JPanel().apply { + layout = BoxLayout(this, BoxLayout.Y_AXIS) + isOpaque = false + } + addToTop(additionalContextPanel) + + message.personaName?.let { + additionalContextPanel.add( + createAdditionalContextPanel( + CodeGPTBundle.get("userMessagePanel.persona.title"), + BorderLayoutPanel() + .addToTop(JBLabel(it, AllIcons.General.User, SwingUtilities.LEADING)) + .withBorder(JBUI.Borders.emptyBottom(8)) + .andTransparent() + ) + ) + } + + documentationDetails?.let { + val listModel = DefaultListModel().apply { + addElement(WebSearchEventDetails(UUID.randomUUID(), it.name, it.url, it.url)) + } + additionalContextPanel.add( + createAdditionalContextPanel( + CodeGPTBundle.get("userMessagePanel.documentation.title"), + WebpageList(listModel) ) ) - addToTop(createWebpageListPanel(WebpageList(listModel))) } if (referencedFilePaths.isNotEmpty()) { - addToTop(SelectedFilesAccordion(project!!, referencedFilePaths)) + additionalContextPanel.add(SelectedFilesAccordion(project!!, referencedFilePaths)) } } } - private fun createWebpageListPanel(webpageList: WebpageList): JPanel { + private fun createAdditionalContextPanel(title: String, component: JComponent): JPanel { return BorderLayoutPanel().apply { isOpaque = false addToTop(BorderLayoutPanel().apply { isOpaque = false border = JBUI.Borders.empty(8, 0) - addToLeft( - JBLabel(CodeGPTBundle.get("userMessagePanel.documentation.title")) - .withFont(JBUI.Fonts.miniFont()) - ) + addToLeft(JBLabel(title).withFont(JBUI.Fonts.miniFont())) }) addToCenter(BorderLayoutPanel().apply { isOpaque = false - addToLeft(webpageList) + addToLeft(component) }) } } diff --git a/src/main/kotlin/ee/carlrobert/codegpt/ui/AddDocumentationPopup.kt b/src/main/kotlin/ee/carlrobert/codegpt/ui/AddDocumentationPopup.kt index 2f338cda..5224aa0d 100644 --- a/src/main/kotlin/ee/carlrobert/codegpt/ui/AddDocumentationPopup.kt +++ b/src/main/kotlin/ee/carlrobert/codegpt/ui/AddDocumentationPopup.kt @@ -11,12 +11,11 @@ import com.intellij.ui.dsl.builder.LabelPosition import com.intellij.ui.dsl.builder.TopGap import com.intellij.ui.dsl.builder.panel import ee.carlrobert.codegpt.CodeGPTBundle -import ee.carlrobert.codegpt.CodeGPTKeys import ee.carlrobert.codegpt.settings.documentation.DocumentationDetailsState import ee.carlrobert.codegpt.settings.documentation.DocumentationSettings import javax.swing.JComponent -class AddDocumentationDialog(private val project: Project) : DialogWrapper(project) { +class AddDocumentationDialog(project: Project) : DialogWrapper(project) { private var nameField = JBTextField("", 40).apply { emptyText.text = "CodeGPT docs" @@ -55,15 +54,11 @@ class AddDocumentationDialog(private val project: Project) : DialogWrapper(proje } override fun doOKAction() { - val documentationDetails = DocumentationDetails(nameField.text, urlField.text) - project.putUserData(CodeGPTKeys.ADDED_DOCUMENTATION, documentationDetails) - if (saveCheckbox.isSelected) { - val newState = DocumentationDetailsState() - newState.url = documentationDetails.url - newState.name = documentationDetails.name - - service().state.documentations.add(newState) + service().state.documentations.add(DocumentationDetailsState().apply { + url = urlField.text + name = nameField.text + }) } super.doOKAction() diff --git a/src/main/kotlin/ee/carlrobert/codegpt/ui/textarea/FileSearchService.kt b/src/main/kotlin/ee/carlrobert/codegpt/ui/textarea/FileSearchService.kt deleted file mode 100644 index e11ce6c9..00000000 --- a/src/main/kotlin/ee/carlrobert/codegpt/ui/textarea/FileSearchService.kt +++ /dev/null @@ -1,45 +0,0 @@ -package ee.carlrobert.codegpt.ui.textarea - -import com.intellij.openapi.components.Service -import com.intellij.openapi.diagnostic.thisLogger -import com.intellij.openapi.project.Project -import com.intellij.openapi.vfs.VirtualFile -import com.intellij.openapi.vfs.readText -import ee.carlrobert.codegpt.CodeGPTKeys -import ee.carlrobert.codegpt.ReferencedFile -import ee.carlrobert.codegpt.actions.IncludeFilesInContextNotifier -import java.io.File - -@Service -class FileSearchService private constructor(val project: Project) { - - companion object { - private val logger = thisLogger() - } - - fun addFileToSession(file: VirtualFile) { - addFilesToSession(listOf(file)) - } - - fun addFilesToSession(files: List) { - val filesIncluded = - project.getUserData(CodeGPTKeys.SELECTED_FILES).orEmpty().toMutableList() - files.forEach { file -> - try { - filesIncluded.add(ReferencedFile(file.name, file.path, file.readText())) - } catch (e: Exception) { - logger.error("Failed to add file to session", e) - } - } - updateFilesInSession(filesIncluded) - } - - fun removeFilesFromSession() = updateFilesInSession(mutableListOf()) - - private fun updateFilesInSession(files: MutableList) { - project.putUserData(CodeGPTKeys.SELECTED_FILES, files) - project.messageBus - .syncPublisher(IncludeFilesInContextNotifier.FILES_INCLUDED_IN_CONTEXT_TOPIC) - .filesIncluded(files) - } -} \ No newline at end of file diff --git a/src/main/kotlin/ee/carlrobert/codegpt/ui/textarea/PromptTextField.kt b/src/main/kotlin/ee/carlrobert/codegpt/ui/textarea/PromptTextField.kt index dbadd020..df07ba99 100644 --- a/src/main/kotlin/ee/carlrobert/codegpt/ui/textarea/PromptTextField.kt +++ b/src/main/kotlin/ee/carlrobert/codegpt/ui/textarea/PromptTextField.kt @@ -5,14 +5,13 @@ import com.intellij.openapi.Disposable import com.intellij.openapi.application.runInEdt import com.intellij.openapi.application.runUndoTransparentWriteAction import com.intellij.openapi.components.service -import com.intellij.openapi.editor.Inlay +import com.intellij.openapi.editor.colors.EditorColorsManager import com.intellij.openapi.editor.event.DocumentEvent import com.intellij.openapi.editor.event.DocumentListener import com.intellij.openapi.editor.ex.EditorEx import com.intellij.openapi.fileTypes.FileTypes import com.intellij.openapi.project.Project import com.intellij.openapi.util.TextRange -import com.intellij.openapi.vfs.VirtualFile import com.intellij.openapi.wm.ToolWindowManager import com.intellij.ui.ComponentUtil.findParentByCondition import com.intellij.ui.EditorTextField @@ -20,8 +19,6 @@ import com.intellij.util.ui.JBUI import ee.carlrobert.codegpt.CodeGPTBundle import ee.carlrobert.codegpt.CodeGPTKeys.IS_PROMPT_TEXT_FIELD_DOCUMENT import ee.carlrobert.codegpt.ui.textarea.suggestion.SuggestionsPopupManager -import ee.carlrobert.codegpt.ui.textarea.suggestion.item.SuggestionActionItem -import ee.carlrobert.codegpt.ui.textarea.suggestion.item.SuggestionItem import java.awt.AWTEvent import java.awt.Dimension import java.awt.KeyboardFocusManager @@ -30,41 +27,24 @@ import java.awt.event.KeyEvent import java.awt.event.MouseEvent import java.util.* -interface AppliedActionInlay { - val inlay: Inlay -} - -data class AppliedSuggestionActionInlay( - override val inlay: Inlay, - val suggestion: SuggestionItem?, -) : AppliedActionInlay - -data class AppliedCodeActionInlay( - override val inlay: Inlay, - val code: String, - val editorFile: VirtualFile -) : AppliedActionInlay - const val AT_CHAR = '@' class PromptTextField( private val project: Project, + private val suggestionsPopupManager: SuggestionsPopupManager, private val onTextChanged: (String) -> Unit, - private val onSubmit: (String, List) -> Unit + private val onSubmit: (String) -> Unit ) : EditorTextField(project, FileTypes.PLAIN_TEXT), Disposable { val dispatcherId: UUID = UUID.randomUUID() - private val appliedInlays: MutableList = mutableListOf() - private val suggestionsPopupManager = SuggestionsPopupManager(project, this) - init { isOneLineMode = false IS_PROMPT_TEXT_FIELD_DOCUMENT.set(document, true) setPlaceholder(CodeGPTBundle.get("toolwindow.chat.textArea.emptyText")) IdeEventQueue.getInstance().addDispatcher( - PromptTextFieldEventDispatcher(this, suggestionsPopupManager, appliedInlays) { - onSubmit(text, appliedInlays) + PromptTextFieldEventDispatcher(this, suggestionsPopupManager) { + onSubmit(text) }, this ) @@ -73,77 +53,13 @@ class PromptTextField( fun clear() { runInEdt { text = "" - clearInlays() - } - } - - fun addInlayElement(actionPrefix: String, text: String?, actionItem: SuggestionActionItem?) { - editor?.let { - var startOffset = it.document.text.lastIndexOf(AT_CHAR) - if (startOffset == -1) { - startOffset = it.document.textLength - } - - addInlayElement(startOffset, actionPrefix, text, actionItem) - } - } - - fun addInlayElement( - actionPrefix: String, - text: String, - editorFile: VirtualFile? = null, - tooltipText: String? = null - ) { - editor?.let { - addInlayElement( - it.caretModel.offset, - actionPrefix, - text, - editorFile = editorFile, - tooltipText = tooltipText - ) - } - } - - private fun addInlayElement( - startOffset: Int, - actionPrefix: String, - text: String?, - actionItem: SuggestionActionItem? = null, - editorFile: VirtualFile? = null, - tooltipText: String? = null - ) { - runUndoTransparentWriteAction { - document.deleteString(startOffset, document.textLength) - document.setText(document.text + " ") - val inlay = editor?.inlayModel?.addInlineElement( - startOffset, - true, - PromptTextFieldInlayRenderer( - project, - actionPrefix, - text, - editorFile?.name ?: "", - tooltipText - ) { inlay -> - appliedInlays.removeIf { appliedInlay -> appliedInlay.inlay == inlay } - inlay.dispose() - }) - if (inlay != null) { - // TODO - if (tooltipText != null && editorFile != null) { - appliedInlays.add(AppliedCodeActionInlay(inlay, tooltipText, editorFile)) - } else { - appliedInlays.add(AppliedSuggestionActionInlay(inlay, actionItem)) - } - editor?.caretModel?.moveToOffset(document.textLength) - } } } override fun createEditor(): EditorEx { val editorEx = super.createEditor() editorEx.settings.isUseSoftWraps = true + editorEx.backgroundColor = service().globalScheme.defaultBackground setupDocumentListener(editorEx) return editorEx } @@ -154,13 +70,7 @@ class PromptTextField( override fun dispose() { clear() - } - - private fun clearInlays() { - runUndoTransparentWriteAction { - appliedInlays.forEach { it.inlay.dispose() } - appliedInlays.clear() - } + suggestionsPopupManager.hidePopup() } private fun setupDocumentListener(editor: EditorEx) { @@ -171,7 +81,6 @@ class PromptTextField( if (event.document.text.isEmpty()) { suggestionsPopupManager.hidePopup() - clearInlays() return } } @@ -199,7 +108,6 @@ class PromptTextField( class PromptTextFieldEventDispatcher( private val textField: PromptTextField, private val suggestionsPopupManager: SuggestionsPopupManager, - private val appliedInlays: MutableList, private val onSubmit: () -> Unit ) : IdeEventQueue.EventDispatcher { @@ -220,14 +128,6 @@ class PromptTextFieldEventDispatcher( if (textField.text.let { it.isNotEmpty() && it.last() == AT_CHAR }) { suggestionsPopupManager.reset() } - - val appliedInlay = appliedInlays.find { - it.inlay.offset == owner.caretModel.offset - 1 - } - if (appliedInlay != null) { - appliedInlay.inlay.dispose() - appliedInlays.remove(appliedInlay) - } } KeyEvent.VK_TAB -> { diff --git a/src/main/kotlin/ee/carlrobert/codegpt/ui/textarea/TagProcessor.kt b/src/main/kotlin/ee/carlrobert/codegpt/ui/textarea/TagProcessor.kt new file mode 100644 index 00000000..695a6c2b --- /dev/null +++ b/src/main/kotlin/ee/carlrobert/codegpt/ui/textarea/TagProcessor.kt @@ -0,0 +1,8 @@ +package ee.carlrobert.codegpt.ui.textarea + +import ee.carlrobert.codegpt.conversations.message.Message +import ee.carlrobert.codegpt.ui.textarea.header.HeaderTagDetails + +interface TagProcessor { + fun process(message: Message, tagDetails: HeaderTagDetails, promptBuilder: StringBuilder) +} diff --git a/src/main/kotlin/ee/carlrobert/codegpt/ui/textarea/TagProcessorFactory.kt b/src/main/kotlin/ee/carlrobert/codegpt/ui/textarea/TagProcessorFactory.kt new file mode 100644 index 00000000..bfb00c4d --- /dev/null +++ b/src/main/kotlin/ee/carlrobert/codegpt/ui/textarea/TagProcessorFactory.kt @@ -0,0 +1,165 @@ +package ee.carlrobert.codegpt.ui.textarea + +import com.intellij.openapi.components.service +import com.intellij.openapi.progress.ProgressManager +import com.intellij.openapi.project.Project +import com.intellij.openapi.vfs.VirtualFile +import ee.carlrobert.codegpt.EncodingManager +import ee.carlrobert.codegpt.conversations.message.Message +import ee.carlrobert.codegpt.ui.textarea.header.* +import ee.carlrobert.codegpt.util.EditorUtil +import ee.carlrobert.codegpt.util.GitUtil +import ee.carlrobert.codegpt.util.file.FileUtil.getFileExtension +import git4idea.GitCommit + +object TagProcessorFactory { + + fun getProcessor(project: Project, tagDetails: HeaderTagDetails): TagProcessor { + return when (tagDetails) { + is FileTagDetails -> FileTagProcessor() + is SelectionTagDetails -> SelectionTagProcessor(project) + is DocumentationTagDetails -> DocumentationTagProcessor() + is PersonaTagDetails -> PersonaTagProcessor() + is FolderTagDetails -> FolderTagProcessor() + is WebTagDetails -> WebTagProcessor() + is GitCommitTagDetails -> GitCommitTagProcessor(project) + else -> throw IllegalArgumentException("Unknown tag type: ${tagDetails::class.simpleName}") + } + } +} + +class FileTagProcessor : TagProcessor { + override fun process( + message: Message, + tagDetails: HeaderTagDetails, + promptBuilder: StringBuilder + ) { + if (tagDetails !is FileTagDetails) { + return + } + if (message.referencedFilePaths == null) { + message.referencedFilePaths = mutableListOf() + } + message.referencedFilePaths?.add(tagDetails.virtualFile.path) + } +} + +class SelectionTagProcessor(private val project: Project) : TagProcessor { + override fun process( + message: Message, + tagDetails: HeaderTagDetails, + promptBuilder: StringBuilder + ) { + if (tagDetails !is SelectionTagDetails) { + return + } + + EditorUtil.getSelectedEditor(project)?.let { selectedEditor -> + val fileExtension = getFileExtension(selectedEditor.virtualFile.name) + promptBuilder + .append("\n```$fileExtension\n") + .append(tagDetails.selectedText) + .append("\n```\n") + } + } +} + +class DocumentationTagProcessor : TagProcessor { + override fun process( + message: Message, + tagDetails: HeaderTagDetails, + promptBuilder: StringBuilder + ) { + if (tagDetails !is DocumentationTagDetails) { + return + } + message.documentationDetails = tagDetails.documentationDetails + } +} + +class PersonaTagProcessor : TagProcessor { + override fun process( + message: Message, + tagDetails: HeaderTagDetails, + promptBuilder: StringBuilder + ) { + if (tagDetails !is PersonaTagDetails) { + return + } + message.personaName = tagDetails.personaDetails.name + } +} + +class FolderTagProcessor : TagProcessor { + override fun process( + message: Message, + tagDetails: HeaderTagDetails, + promptBuilder: StringBuilder + ) { + if (tagDetails !is FolderTagDetails) { + return + } + + if (message.referencedFilePaths == null) { + message.referencedFilePaths = mutableListOf() + } + + processFolder(tagDetails.folder, message.referencedFilePaths ?: mutableListOf()) + } + + private fun processFolder(folder: VirtualFile, referencedFilePaths: MutableList) { + folder.children.forEach { child -> + when { + child.isDirectory -> processFolder(child, referencedFilePaths) + else -> referencedFilePaths.add(child.path) + } + } + } +} + +class WebTagProcessor : TagProcessor { + override fun process( + message: Message, + tagDetails: HeaderTagDetails, + promptBuilder: StringBuilder + ) { + if (tagDetails !is WebTagDetails) { + return + } + message.isWebSearchIncluded = true + } +} + +class GitCommitTagProcessor(private val project: Project) : TagProcessor { + override fun process( + message: Message, + tagDetails: HeaderTagDetails, + promptBuilder: StringBuilder + ) { + if (tagDetails !is GitCommitTagDetails) { + return + } + promptBuilder + .append("\n```shell\n") + .append(getDiffString(project, tagDetails.gitCommit)) + .append("\n```\n") + } + + private fun getDiffString(project: Project, gitCommit: GitCommit): String { + return ProgressManager.getInstance().runProcessWithProgressSynchronously( + { + val repository = GitUtil.getProjectRepository(project) + ?: return@runProcessWithProgressSynchronously "" + + val commitId = gitCommit.id.asString() + val diff = GitUtil.getCommitDiffs(project, repository, commitId) + .joinToString("\n") + + service().truncateText(diff, 8192, true) + }, + "Getting Commit Diff", + true, + project + ) + } +} \ No newline at end of file diff --git a/src/main/kotlin/ee/carlrobert/codegpt/ui/textarea/UserInputPanel.kt b/src/main/kotlin/ee/carlrobert/codegpt/ui/textarea/UserInputPanel.kt index aeac75b9..5f046cab 100644 --- a/src/main/kotlin/ee/carlrobert/codegpt/ui/textarea/UserInputPanel.kt +++ b/src/main/kotlin/ee/carlrobert/codegpt/ui/textarea/UserInputPanel.kt @@ -1,6 +1,7 @@ package ee.carlrobert.codegpt.ui.textarea import com.intellij.icons.AllIcons +import com.intellij.openapi.Disposable import com.intellij.openapi.actionSystem.ActionPlaces import com.intellij.openapi.actionSystem.AnAction import com.intellij.openapi.actionSystem.AnActionEvent @@ -8,15 +9,17 @@ import com.intellij.openapi.application.invokeLater import com.intellij.openapi.application.runInEdt import com.intellij.openapi.components.service import com.intellij.openapi.editor.SelectionModel +import com.intellij.openapi.editor.colors.EditorColorsManager import com.intellij.openapi.observable.properties.AtomicBooleanProperty import com.intellij.openapi.project.Project +import com.intellij.openapi.util.Disposer import com.intellij.openapi.vfs.VirtualFile import com.intellij.ui.components.AnActionLink import com.intellij.ui.dsl.builder.AlignX import com.intellij.ui.dsl.builder.RightGap import com.intellij.ui.dsl.builder.panel +import com.intellij.util.IconUtil import com.intellij.util.ui.JBUI -import com.intellij.util.ui.UIUtil import ee.carlrobert.codegpt.CodeGPTBundle import ee.carlrobert.codegpt.Icons import ee.carlrobert.codegpt.actions.AttachImageAction @@ -30,7 +33,11 @@ import ee.carlrobert.codegpt.toolwindow.chat.ChatToolWindowContentManager import ee.carlrobert.codegpt.toolwindow.chat.ui.textarea.ModelComboBoxAction import ee.carlrobert.codegpt.toolwindow.chat.ui.textarea.TotalTokensPanel import ee.carlrobert.codegpt.ui.IconActionButton -import ee.carlrobert.codegpt.ui.textarea.suggestion.item.GitCommitActionItem +import ee.carlrobert.codegpt.ui.textarea.header.GitCommitTagDetails +import ee.carlrobert.codegpt.ui.textarea.header.HeaderTagDetails +import ee.carlrobert.codegpt.ui.textarea.header.SelectionTagDetails +import ee.carlrobert.codegpt.ui.textarea.header.UserInputHeaderPanel +import ee.carlrobert.codegpt.ui.textarea.suggestion.SuggestionsPopupManager import ee.carlrobert.llm.client.openai.completion.OpenAIChatCompletionModel import git4idea.GitCommit import java.awt.* @@ -43,7 +50,8 @@ class UserInputPanel( private val project: Project, private val conversation: Conversation, private val totalTokensPanel: TotalTokensPanel, - private val onSubmit: (String, List?) -> Unit, + parentDisposable: Disposable, + private val onSubmit: (String, List) -> Unit, private val onStop: () -> Unit ) : JPanel(BorderLayout()) { @@ -51,19 +59,23 @@ class UserInputPanel( private const val CORNER_RADIUS = 16 } - val text: String - get() = promptTextField.text - - private val promptTextField = PromptTextField(project, ::updateUserTokens, ::handleSubmit) + private val suggestionsPopupManager = SuggestionsPopupManager(project, this) + private val userInputHeaderPanel = UserInputHeaderPanel( + project, + suggestionsPopupManager + ) + private val promptTextField = + PromptTextField(project, suggestionsPopupManager, ::updateUserTokens) { + handleSubmit(it, userInputHeaderPanel.getSelectedTags()) + } private val submitButton = IconActionButton( object : AnAction( CodeGPTBundle.get("smartTextPane.submitButton.title"), CodeGPTBundle.get("smartTextPane.submitButton.description"), - Icons.Send + IconUtil.scale(Icons.Send, null, 0.85f) ) { override fun actionPerformed(e: AnActionEvent) { - handleSubmit(promptTextField.text) - promptTextField.clear() + handleSubmit(promptTextField.text, userInputHeaderPanel.getSelectedTags()) } }, "SUBMIT" @@ -82,10 +94,20 @@ class UserInputPanel( ).apply { isEnabled = false } private val imageActionSupported = AtomicBooleanProperty(isImageActionSupported()) + val text: String + get() = promptTextField.text + init { - background = UIUtil.getTextFieldBackground() + background = service().globalScheme.defaultBackground + add(userInputHeaderPanel, BorderLayout.NORTH) add(promptTextField, BorderLayout.CENTER) add(getFooter(), BorderLayout.SOUTH) + + Disposer.register(parentDisposable, promptTextField) + } + + fun getSelectedTags(): List { + return userInputHeaderPanel.getSelectedTags() } fun setSubmitEnabled(enabled: Boolean) { @@ -93,6 +115,38 @@ class UserInputPanel( stopButton.isEnabled = !enabled } + fun addSelection(editorFile: VirtualFile, selectionModel: SelectionModel) { + addTag(SelectionTagDetails(editorFile, selectionModel, selectionModel.selectedText)) + promptTextField.requestFocusInWindow() + selectionModel.removeSelection() + } + + fun addCommitReferences(gitCommits: List) { + runInEdt { + if (promptTextField.text.isEmpty()) { + promptTextField.text = if (gitCommits.size == 1) { + "Explain the commit `${gitCommits[0].id.toShortString()}`" + } else { + "Explain the commits ${gitCommits.joinToString(", ") { "`${it.id.toShortString()}`" }}" + } + } + + gitCommits.forEach { + addTag(GitCommitTagDetails(it)) + } + promptTextField.requestFocusInWindow() + promptTextField.editor?.caretModel?.moveToOffset(promptTextField.text.length) + } + } + + fun addTag(tagDetails: HeaderTagDetails) { + userInputHeaderPanel.addTag(tagDetails) + val text = promptTextField.text + if (text.isNotEmpty() && text.last() == '@') { + promptTextField.text = text.substring(0, text.length - 1) + } + } + override fun requestFocus() { invokeLater { promptTextField.requestFocusInWindow() @@ -123,11 +177,10 @@ class UserInputPanel( g2.dispose() } - override fun paintBorder(g: Graphics) { val g2 = g.create() as Graphics2D g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON) - g2.color = JBUI.CurrentTheme.ActionButton.focusedBorder() + g2.color = JBUI.CurrentTheme.Focus.defaultButtonColor() if (promptTextField.isFocusOwner) { g2.stroke = BasicStroke(1.5F) } @@ -137,9 +190,9 @@ class UserInputPanel( override fun getInsets(): Insets = JBUI.insets(4) - private fun handleSubmit(text: String, appliedInlays: List? = emptyList()) { + private fun handleSubmit(text: String, appliedTags: List = emptyList()) { if (text.isNotEmpty() && submitButton.isEnabled) { - onSubmit(text, appliedInlays) + onSubmit(text, appliedTags) promptTextField.clear() } } @@ -218,37 +271,64 @@ class UserInputPanel( else -> false } } +} - fun addSelection(editorFile: VirtualFile, selectionModel: SelectionModel) { - val fileName = editorFile.name - promptTextField.addInlayElement( - "code", - "$fileName (${selectionModel.selectionStartPosition?.line}:${selectionModel.selectionEndPosition?.line})", - editorFile = editorFile, - tooltipText = selectionModel.selectedText - ) - promptTextField.requestFocusInWindow() - selectionModel.removeSelection() +class WrapLayout(align: Int, hgap: Int, vgap: Int) : FlowLayout(align, hgap, vgap) { + + override fun preferredLayoutSize(target: Container): Dimension { + return layoutSize(target, true) } - fun addCommitReferences(gitCommits: List) { - runInEdt { - if (promptTextField.text.isEmpty()) { - promptTextField.text = if (gitCommits.size == 1) { - "Explain the commit: " - } else { - "Explain the commits: " + override fun minimumLayoutSize(target: Container): Dimension { + return layoutSize(target, false) + } + + private fun layoutSize(target: Container, preferred: Boolean): Dimension { + synchronized(target.treeLock) { + val targetWidth = target.width + var width = targetWidth + if (targetWidth == 0) { + width = Int.MAX_VALUE + } + + val insets = target.insets + val horizontalInsetsAndGap = insets.left + insets.right + (hgap * 2) + val maxWidth = width - horizontalInsetsAndGap + + val dim = Dimension(0, 0) + var rowWidth = 0 + var rowHeight = 0 + + for (i in 0 until target.componentCount) { + val m = target.getComponent(i) + if (m.isVisible) { + val d = if (preferred) m.preferredSize else m.minimumSize + if (rowWidth + d.width > maxWidth) { + addRow(dim, rowWidth, rowHeight) + rowWidth = 0 + rowHeight = 0 + } + if (rowWidth != 0) { + rowWidth += hgap + } + rowWidth += d.width + rowHeight = maxOf(rowHeight, d.height) } } + addRow(dim, rowWidth, rowHeight) - gitCommits.forEach { - promptTextField.addInlayElement( - "commit", - it.id.toShortString(), - GitCommitActionItem(project, it) - ) - } - promptTextField.requestFocusInWindow() + dim.width += horizontalInsetsAndGap + dim.height += insets.top + insets.bottom + vgap * 2 + + return dim } } -} + + private fun addRow(dim: Dimension, rowWidth: Int, rowHeight: Int) { + dim.width = maxOf(dim.width, rowWidth) + if (dim.height > 0) { + dim.height += vgap + } + dim.height += rowHeight + } +} \ No newline at end of file diff --git a/src/main/kotlin/ee/carlrobert/codegpt/ui/textarea/header/AddButton.kt b/src/main/kotlin/ee/carlrobert/codegpt/ui/textarea/header/AddButton.kt new file mode 100644 index 00000000..a1526d62 --- /dev/null +++ b/src/main/kotlin/ee/carlrobert/codegpt/ui/textarea/header/AddButton.kt @@ -0,0 +1,31 @@ +package ee.carlrobert.codegpt.ui.textarea.header + +import com.intellij.icons.AllIcons +import com.intellij.util.IconUtil +import java.awt.Cursor +import java.awt.Dimension +import java.awt.Graphics +import javax.swing.JButton + +class AddButton(onAdd: () -> Unit) : JButton() { + init { + addActionListener { + onAdd() + } + + cursor = Cursor(Cursor.HAND_CURSOR) + preferredSize = Dimension(20, 20) + isContentAreaFilled = false + isOpaque = false + border = null + toolTipText = "Add Context" + icon = IconUtil.scale(AllIcons.General.InlineAdd, null, 0.75f) + rolloverIcon = IconUtil.scale(AllIcons.General.InlineAddHover, null, 0.75f) + pressedIcon = IconUtil.scale(AllIcons.General.InlineAddHover, null, 0.75f) + } + + override fun paintComponent(g: Graphics) { + PaintUtil.drawRoundedBackground(g, this, true) + super.paintComponent(g) + } +} \ No newline at end of file diff --git a/src/main/kotlin/ee/carlrobert/codegpt/ui/textarea/header/CloseButton.kt b/src/main/kotlin/ee/carlrobert/codegpt/ui/textarea/header/CloseButton.kt new file mode 100644 index 00000000..e10b2cb0 --- /dev/null +++ b/src/main/kotlin/ee/carlrobert/codegpt/ui/textarea/header/CloseButton.kt @@ -0,0 +1,21 @@ +package ee.carlrobert.codegpt.ui.textarea.header + +import com.intellij.icons.AllIcons +import com.intellij.icons.AllIcons.Actions.Close +import com.intellij.util.ui.JBUI +import java.awt.Dimension +import javax.swing.JButton + +class CloseButton(onClose: () -> Unit) : JButton(Close) { + init { + addActionListener { + onClose() + } + + preferredSize = Dimension(Close.iconWidth, Close.iconHeight) + border = JBUI.Borders.emptyLeft(4) + isContentAreaFilled = false + toolTipText = "Remove" + rolloverIcon = AllIcons.Actions.CloseHovered + } +} \ No newline at end of file diff --git a/src/main/kotlin/ee/carlrobert/codegpt/ui/textarea/header/HeaderTag.kt b/src/main/kotlin/ee/carlrobert/codegpt/ui/textarea/header/HeaderTag.kt new file mode 100644 index 00000000..20dabd58 --- /dev/null +++ b/src/main/kotlin/ee/carlrobert/codegpt/ui/textarea/header/HeaderTag.kt @@ -0,0 +1,216 @@ +package ee.carlrobert.codegpt.ui.textarea.header + +import com.intellij.icons.AllIcons +import com.intellij.openapi.components.service +import com.intellij.openapi.editor.Editor +import com.intellij.openapi.editor.SelectionModel +import com.intellij.openapi.editor.colors.EditorColorsManager +import com.intellij.openapi.fileEditor.FileEditorManager +import com.intellij.openapi.project.Project +import com.intellij.openapi.vfs.VirtualFile +import com.intellij.ui.components.JBLabel +import com.intellij.util.IconUtil +import com.intellij.util.ui.JBUI +import com.jetbrains.rd.util.UUID +import ee.carlrobert.codegpt.Icons +import ee.carlrobert.codegpt.settings.prompts.PersonaDetails +import ee.carlrobert.codegpt.ui.DocumentationDetails +import ee.carlrobert.codegpt.util.EditorUtil +import java.awt.Cursor +import java.awt.FlowLayout +import java.awt.Graphics +import java.awt.event.MouseAdapter +import java.awt.event.MouseEvent +import javax.swing.Icon +import javax.swing.JPanel +import javax.swing.SwingUtilities +import git4idea.GitCommit as Git4IdeaGitCommit + +open class HeaderTagDetails( + open val name: String, + val icon: Icon? = null, + open var selected: Boolean = true +) { + val id: UUID = UUID.randomUUID() + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other !is HeaderTagDetails) return false + return id == other.id + } + + override fun hashCode(): Int { + return id.hashCode() + } +} + +data class FileTagDetails(var virtualFile: VirtualFile, override var selected: Boolean = true) : + HeaderTagDetails(virtualFile.name, virtualFile.fileType.icon) + +data class SelectionTagDetails( + var virtualFile: VirtualFile?, + var selectionModel: SelectionModel?, + var selectedText: String? +) : + HeaderTagDetails( + "${virtualFile?.name} (${selectionModel?.selectionStartPosition?.line}:${selectionModel?.selectionEndPosition?.line})", + Icons.InSelection + ) + +data class DocumentationTagDetails(var documentationDetails: DocumentationDetails) : + HeaderTagDetails(documentationDetails.name, AllIcons.Toolwindows.Documentation) + +data class PersonaTagDetails(var personaDetails: PersonaDetails) : + HeaderTagDetails(personaDetails.name, AllIcons.General.User) + +data class GitCommitTagDetails(var gitCommit: Git4IdeaGitCommit) : + HeaderTagDetails(gitCommit.id.asString().take(6), AllIcons.Vcs.CommitNode) + +data class FolderTagDetails(var folder: VirtualFile) : + HeaderTagDetails(folder.name, AllIcons.Nodes.Folder) + +class WebTagDetails : HeaderTagDetails("Web", AllIcons.General.Web) + +abstract class HeaderTag(val tagDetails: HeaderTagDetails, private val selectable: Boolean = true) : + JPanel() { + + val id: UUID = tagDetails.id + + private val label = createLabel(tagDetails) + private val closeButton = CloseButton { + isVisible = true + onClose() + }.apply { + isVisible = tagDetails.selected + } + + init { + setupUI() + } + + abstract fun onClose() + + abstract fun onSelect(tagDetails: HeaderTagDetails) + + fun updateLabel(text: String, icon: Icon) { + label.text = text + label.icon = IconUtil.scale(icon, null, 0.65f) + } + + open fun select() { + onSelect(tagDetails) + + if (!tagDetails.selected) { + tagDetails.selected = true + closeButton.isVisible = true + label.foreground = service().globalScheme.defaultForeground + } + } + + override fun paintComponent(g: Graphics) { + super.paintComponent(g) + PaintUtil.drawRoundedBackground(g, this, tagDetails.selected) + } + + private fun createLabel(tagDetails: HeaderTagDetails): JBLabel { + return (if (tagDetails.icon == null) { + JBLabel(tagDetails.name) + } else { + JBLabel(tagDetails.name, tagDetails.icon.scale(), SwingUtilities.LEADING) + }).apply { + foreground = if (tagDetails.selected) { + service().globalScheme.defaultForeground + } else { + JBUI.CurrentTheme.Label.disabledForeground(false) + } + font = JBUI.Fonts.miniFont() + } + } + + private fun setupUI() { + isOpaque = false + layout = FlowLayout(FlowLayout.LEFT, 0, 2) + border = JBUI.Borders.empty(0, 6, 0, 4) + cursor = if (selectable) Cursor(Cursor.HAND_CURSOR) else Cursor(Cursor.DEFAULT_CURSOR) + + add(label) + add(closeButton) + if (selectable) { + addMouseListener(object : MouseAdapter() { + override fun mousePressed(e: MouseEvent) { + select() + repaint() + } + }) + } + } +} + +abstract class SelectedFileHeaderTag( + private val project: Project, + var virtualFile: VirtualFile? = project.getSelectedEditorFile() +) : HeaderTag( + HeaderTagDetails( + virtualFile?.name ?: "", + virtualFile?.fileType?.icon + ) +) { + + init { + isVisible = project.getSelectedEditorFile() != null + } + + override fun onSelect(tagDetails: HeaderTagDetails) { + if (tagDetails is FileTagDetails) { + project.service().openFile(tagDetails.virtualFile) + } + } + + fun update(virtualFile: VirtualFile) { + this.virtualFile = virtualFile + isVisible = true + updateLabel(virtualFile.name, virtualFile.fileType.icon) + } +} + +class SelectionHeaderTag( + private val project: Project, + var selectedEditor: Editor? = project.getSelectedEditor() +) : HeaderTag( + SelectionTagDetails( + selectedEditor?.virtualFile, + selectedEditor?.selectionModel, + selectedEditor?.selectionModel?.selectedText + ), + false +) { + + init { + isVisible = selectedEditor?.selectionModel?.hasSelection() ?: false + } + + override fun onSelect(tagDetails: HeaderTagDetails) { + } + + override fun onClose() { + selectedEditor?.selectionModel?.removeSelection() + } + + fun update(virtualFile: VirtualFile, selectionModel: SelectionModel) { + isVisible = selectionModel.hasSelection() + updateLabel( + "${virtualFile.name}:${selectionModel.selectionStart}-${selectionModel.selectionEnd}", + virtualFile.fileType.icon + ) + } +} + +fun Icon.scale() = IconUtil.scale(this, null, 0.65f) + +fun Project.getSelectedEditorFile(): VirtualFile? { + return this.getSelectedEditor()?.virtualFile +} + +fun Project.getSelectedEditor(): Editor? { + return EditorUtil.getSelectedEditor(this) +} \ No newline at end of file diff --git a/src/main/kotlin/ee/carlrobert/codegpt/ui/textarea/header/PaintUtil.kt b/src/main/kotlin/ee/carlrobert/codegpt/ui/textarea/header/PaintUtil.kt new file mode 100644 index 00000000..8bfc077d --- /dev/null +++ b/src/main/kotlin/ee/carlrobert/codegpt/ui/textarea/header/PaintUtil.kt @@ -0,0 +1,71 @@ +package ee.carlrobert.codegpt.ui.textarea.header + +import com.intellij.openapi.components.service +import com.intellij.openapi.editor.colors.EditorColorsManager +import com.intellij.util.ui.JBUI +import com.intellij.util.ui.UIUtil +import java.awt.* +import java.awt.geom.RoundRectangle2D +import javax.swing.JComponent + +object PaintUtil { + + private const val ARC_WIDTH = 8f + private const val ARC_HEIGHT = 8f + private const val STROKE_WIDTH = 1f + + fun drawRoundedBackground( + g: Graphics, + component: JComponent, + selected: Boolean = true + ) { + val g2 = g.create() as Graphics2D + try { + g2.setRenderingHint( + RenderingHints.KEY_ANTIALIASING, + RenderingHints.VALUE_ANTIALIAS_ON + ) + + val rect = createRoundedRectangle(component) + + if (!selected) { + g2.composite = AlphaComposite.getInstance(AlphaComposite.SRC_OVER, 0.5f) + } + + g2.color = service().globalScheme.defaultBackground + g2.fill(rect) + + drawBorder(g2, rect, selected) + } finally { + g2.dispose() + } + } + + private fun createRoundedRectangle(component: JComponent): RoundRectangle2D.Float { + return RoundRectangle2D.Float( + STROKE_WIDTH / 2f, + STROKE_WIDTH / 2f, + component.width.toFloat() - STROKE_WIDTH, + component.height.toFloat() - STROKE_WIDTH, + ARC_WIDTH, + ARC_HEIGHT + ) + } + + private fun drawBorder(g2: Graphics2D, rect: RoundRectangle2D.Float, selected: Boolean) { + g2.color = JBUI.CurrentTheme.Focus.defaultButtonColor() + g2.stroke = if (selected) { + BasicStroke(STROKE_WIDTH) + } else { + BasicStroke( + STROKE_WIDTH, + BasicStroke.CAP_BUTT, + BasicStroke.JOIN_BEVEL, + 0f, + floatArrayOf(10f, 5f), + 0f + ) + } + g2.draw(rect) + } +} diff --git a/src/main/kotlin/ee/carlrobert/codegpt/ui/textarea/header/TagUtil.kt b/src/main/kotlin/ee/carlrobert/codegpt/ui/textarea/header/TagUtil.kt new file mode 100644 index 00000000..917be1bc --- /dev/null +++ b/src/main/kotlin/ee/carlrobert/codegpt/ui/textarea/header/TagUtil.kt @@ -0,0 +1,29 @@ +package ee.carlrobert.codegpt.ui.textarea.header + +import com.intellij.openapi.components.service +import com.intellij.openapi.project.Project +import ee.carlrobert.codegpt.toolwindow.chat.ChatToolWindowContentManager + +object TagUtil { + fun isTagTypePresent( + project: Project, + tagClass: Class + ): Boolean { + return project.service() + .tryFindActiveChatTabPanel() + .map { it.selectedTags } + .orElse(emptyList()) + .any { tagClass.isInstance(it) } + } + + fun getExistingTags( + project: Project, + tagClass: Class + ): List { + return project.service() + .tryFindActiveChatTabPanel() + .map { it.selectedTags } + .orElse(emptyList()) + .filterIsInstance(tagClass) + } +} \ No newline at end of file diff --git a/src/main/kotlin/ee/carlrobert/codegpt/ui/textarea/header/UserInputHeaderPanel.kt b/src/main/kotlin/ee/carlrobert/codegpt/ui/textarea/header/UserInputHeaderPanel.kt new file mode 100644 index 00000000..2e8b8c46 --- /dev/null +++ b/src/main/kotlin/ee/carlrobert/codegpt/ui/textarea/header/UserInputHeaderPanel.kt @@ -0,0 +1,310 @@ +package ee.carlrobert.codegpt.ui.textarea.header + +import com.intellij.openapi.application.runReadAction +import com.intellij.openapi.components.service +import com.intellij.openapi.editor.Editor +import com.intellij.openapi.editor.SelectionModel +import com.intellij.openapi.fileEditor.FileEditorManager +import com.intellij.openapi.fileEditor.FileEditorManagerEvent +import com.intellij.openapi.fileEditor.FileEditorManagerListener +import com.intellij.openapi.project.Project +import com.intellij.openapi.vfs.VirtualFile +import com.intellij.ui.components.JBLabel +import com.intellij.util.ui.JBUI +import com.jetbrains.rd.util.UUID +import ee.carlrobert.codegpt.EditorNotifier +import ee.carlrobert.codegpt.actions.IncludeFilesInContextNotifier +import ee.carlrobert.codegpt.ui.textarea.WrapLayout +import ee.carlrobert.codegpt.ui.textarea.suggestion.SuggestionsPopupManager +import ee.carlrobert.codegpt.util.EditorUtil +import ee.carlrobert.codegpt.util.EditorUtil.getSelectedEditorFile +import java.awt.FlowLayout +import javax.swing.JPanel +import kotlin.math.max + +class UserInputHeaderPanel( + private val project: Project, + suggestionsPopupManager: SuggestionsPopupManager +) : JPanel(WrapLayout(FlowLayout.LEFT, 4, 4)) { + + companion object { + private const val MAX_VISIBLE_TAGS = 3 + private const val INITIAL_VISIBLE_FILES = 2 + private const val TAG_INSERTION_OFFSET = 4 + } + + private val tags = mutableSetOf() + private val selectedFileTags = mutableSetOf() + private val selectedFileHeaderTag = object : SelectedFileHeaderTag(project) { + override fun onClose() { + this.isVisible = false + if (tags.isEmpty()) { + emptyText.isVisible = true + } + } + } + private val emptyText = JBLabel("No context included").apply { + foreground = JBUI.CurrentTheme.Label.disabledForeground() + font = JBUI.Fonts.smallFont() + border = JBUI.Borders.emptyLeft(2) + isVisible = project.getSelectedEditor() == null + } + private val selectionHeaderTag = SelectionHeaderTag(project) + + init { + initializeUI(suggestionsPopupManager) + initializeEventListeners() + } + + fun getSelectedTags(): List { + val selectedTags: MutableList = + tags.filter { it.selected }.toMutableList() + + val selectedFile = selectedFileHeaderTag.virtualFile + if (selectedFileHeaderTag.isVisible && selectedFile != null) { + selectedTags.add(FileTagDetails(selectedFile)) + } + + selectionHeaderTag.selectedEditor?.let { editor -> + val selectionFile = editor.virtualFile + if (!editor.isDisposed && selectionHeaderTag.isVisible && selectionFile != null) { + selectedTags.add( + runReadAction { + SelectionTagDetails( + selectionFile, + editor.selectionModel, + editor.selectionModel.selectedText + ) + } + ) + } + } + + return selectedTags + } + + fun addTag(tagDetails: HeaderTagDetails) { + if (selectedFileHeaderTag.isVisible + && tagDetails is FileTagDetails + && selectedFileHeaderTag.virtualFile == tagDetails.virtualFile + ) { + return + } + + val tag = object : HeaderTag(tagDetails, tagDetails is FileTagDetails) { + override fun onSelect(tagDetails: HeaderTagDetails) { + if (tagDetails is FileTagDetails) { + if (tagDetails.selected) { + project.service().openFile(tagDetails.virtualFile) + } else { + selectedFileTags.add(tagDetails) + } + } + } + + override fun onClose() { + removeTag(tagDetails.id) + } + + override fun select() { + updateTagPosition(this) + super.select() + if (tags.filterIsInstance().filter { !it.selected }.size <= 2) { + addNextOpenFile() + } + } + } + + if (tags.add(tagDetails)) { + emptyText.isVisible = false + + if (tagDetails is FileTagDetails) { + selectedFileTags.add(tagDetails) + } + + val lastSelectionTagIndex = getLastSelectedTagIndex() + if (lastSelectionTagIndex != -1) { + add(tag, lastSelectionTagIndex + TAG_INSERTION_OFFSET + 1) + } else { + add(tag, TAG_INSERTION_OFFSET) + } + + val unselectedTags = components + .filter { it !is SelectedFileHeaderTag && it !is SelectionHeaderTag } + .filterIsInstance() + .filter { !it.tagDetails.selected } + if (unselectedTags.size > 2) { + removeTag(unselectedTags.last().tagDetails.id) + } + + revalidate() + repaint() + } + } + + private fun updateTagPosition(tag: HeaderTag) { + remove(tag) + val lastSelectionTagIndex = getLastSelectedTagIndex() + if (lastSelectionTagIndex != -1) { + add(tag, lastSelectionTagIndex + TAG_INSERTION_OFFSET + 1) + } else { + add(tag, max(getFirstUnselectedTagIndex(), TAG_INSERTION_OFFSET)) + } + } + + private fun getFilteredHeaderTags(): List = components + .filter { it !is SelectedFileHeaderTag && it !is SelectionHeaderTag } + .filterIsInstance() + + private fun getLastSelectedTagIndex(): Int = + getFilteredHeaderTags().indexOfLast { it.tagDetails.selected } + + private fun getFirstUnselectedTagIndex(): Int = + getFilteredHeaderTags().indexOfFirst { !it.tagDetails.selected } + + private fun getSelectedFileTag(file: VirtualFile): FileTagDetails? = + selectedFileTags.find { it.virtualFile == file } + + private fun getFileTag(file: VirtualFile): FileTagDetails? = + tags.filterIsInstance().find { it.virtualFile == file } + + private fun getSortedOpenFiles(project: Project): MutableList = + EditorUtil.getOpenLocalFiles(project) + .filterNot { it == selectedFileHeaderTag.virtualFile } + .map { FileTagDetails(it) } + .toMutableList() + + private fun addNextOpenFile() { + val file = EditorUtil.getOpenLocalFiles(project).firstOrNull { + !tags.filterIsInstance().any { tag -> tag.virtualFile == it } + && it != selectedFileHeaderTag.virtualFile + } ?: return + addTag(FileTagDetails(file, false)) + } + + private fun removeFileTag(virtualFile: VirtualFile) { + getFileTag(virtualFile)?.let { + removeTag(it.id) + } + } + + private fun removeTag(id: UUID) { + val tagToRemove = + tags.find { it.id == id } ?: throw IllegalArgumentException("Tag with id $id not found") + if (tags.removeIf { it.id == tagToRemove.id }) { + val componentToRemove = components.find { it is HeaderTag && it.id == id } ?: return + remove(componentToRemove) + + if (tags.isEmpty() && !selectedFileHeaderTag.isVisible) { + emptyText.isVisible = true + } + + revalidate() + repaint() + } + } + + private fun initializeUI(suggestionsPopupManager: SuggestionsPopupManager) { + isOpaque = false + border = JBUI.Borders.empty() + + add(AddButton { + if (suggestionsPopupManager.isPopupVisible()) { + suggestionsPopupManager.hidePopup() + } else { + suggestionsPopupManager.showPopup(this) + } + }) + add(emptyText) + add(selectionHeaderTag) + add(selectedFileHeaderTag) + + if (tags.size <= 2) { + val selectedFile = EditorUtil.getSelectedEditor(project)?.virtualFile + getSortedOpenFiles(project) + .take(INITIAL_VISIBLE_FILES) + .forEach { + addTag(FileTagDetails(it.virtualFile, selectedFile == it.virtualFile)) + } + } + } + + private fun initializeEventListeners() { + project.messageBus.connect().apply { + subscribe(EditorNotifier.SelectionChange.TOPIC, EditorSelectionChangeListener()) + subscribe(EditorNotifier.Created.TOPIC, EditorCreatedListener()) + subscribe(EditorNotifier.Released.TOPIC, EditorReleasedListener()) + subscribe(FileEditorManagerListener.FILE_EDITOR_MANAGER, FileSelectionListener()) + subscribe( + IncludeFilesInContextNotifier.FILES_INCLUDED_IN_CONTEXT_TOPIC, + IncludedFilesListener() + ) + } + } + + private inner class EditorSelectionChangeListener : EditorNotifier.SelectionChange { + override fun selectionChanged(selectionModel: SelectionModel, virtualFile: VirtualFile) { + handleSelectionChange(selectionModel, virtualFile) + } + + private fun handleSelectionChange( + selectionModel: SelectionModel, + virtualFile: VirtualFile + ) { + selectionHeaderTag.update(virtualFile, selectionModel) + } + } + + private inner class EditorCreatedListener : EditorNotifier.Created { + override fun editorCreated(editor: Editor) { + editor.virtualFile?.let { editorFile -> + if (selectedFileHeaderTag.isVisible && selectedFileHeaderTag.virtualFile == editorFile) { + return + } + + addTag(FileTagDetails(editorFile, false)) + } + } + } + + private inner class EditorReleasedListener : EditorNotifier.Released { + override fun editorReleased(editor: Editor) { + removeFileTag(editor.virtualFile) + + if (tags.isEmpty() && project.getSelectedEditorFile() == null) { + selectedFileHeaderTag.isVisible = false + emptyText.isVisible = true + } + } + } + + private inner class FileSelectionListener : FileEditorManagerListener { + + override fun selectionChanged(event: FileEditorManagerEvent) { + val fileTags = tags.filterIsInstance() + event.newFile?.let { newFile -> + if (fileTags.any { it.virtualFile == newFile }) { + removeFileTag(newFile) + } + selectedFileHeaderTag.update(newFile) + emptyText.isVisible = false + + event.oldFile?.let { oldFile -> + val prevSelectedTag = getSelectedFileTag(oldFile) + addTag(prevSelectedTag ?: FileTagDetails(oldFile, false)) + } + } + } + } + + private inner class IncludedFilesListener : IncludeFilesInContextNotifier { + override fun filesIncluded(includedFiles: MutableList) { + val selectedEditorFile = getSelectedEditorFile(project) + includedFiles.forEach { + if (getFileTag(it) == null && selectedEditorFile != it) { + addTag(FileTagDetails(it)) + } + } + } + } +} \ No newline at end of file diff --git a/src/main/kotlin/ee/carlrobert/codegpt/ui/textarea/suggestion/SuggestionList.kt b/src/main/kotlin/ee/carlrobert/codegpt/ui/textarea/suggestion/SuggestionList.kt index 86bc2ecd..7924153c 100644 --- a/src/main/kotlin/ee/carlrobert/codegpt/ui/textarea/suggestion/SuggestionList.kt +++ b/src/main/kotlin/ee/carlrobert/codegpt/ui/textarea/suggestion/SuggestionList.kt @@ -2,7 +2,7 @@ package ee.carlrobert.codegpt.ui.textarea.suggestion import com.intellij.ui.components.JBList import com.intellij.util.ui.JBUI -import ee.carlrobert.codegpt.ui.textarea.PromptTextField +import ee.carlrobert.codegpt.ui.textarea.UserInputPanel import ee.carlrobert.codegpt.ui.textarea.suggestion.item.SuggestionItem import ee.carlrobert.codegpt.ui.textarea.suggestion.renderer.SuggestionListCellRenderer import java.awt.KeyboardFocusManager @@ -15,7 +15,7 @@ import javax.swing.ListSelectionModel class SuggestionList( listModel: DefaultListModel, - private val textPane: PromptTextField, + private val userInputPanel: UserInputPanel, private val onSelected: (SuggestionItem) -> Unit ) : JBList(listModel) { @@ -43,7 +43,7 @@ class SuggestionList( private fun setupUI() { border = JBUI.Borders.empty() selectionMode = ListSelectionModel.SINGLE_SELECTION - cellRenderer = SuggestionListCellRenderer(textPane) + cellRenderer = SuggestionListCellRenderer(userInputPanel) } private fun setupKeyboardFocusManager() { 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 d9162d26..0fc0a318 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 @@ -3,7 +3,7 @@ package ee.carlrobert.codegpt.ui.textarea.suggestion import com.intellij.openapi.project.Project import com.intellij.openapi.ui.popup.JBPopup import com.intellij.vcsUtil.showAbove -import ee.carlrobert.codegpt.ui.textarea.PromptTextField +import ee.carlrobert.codegpt.ui.textarea.UserInputPanel import ee.carlrobert.codegpt.ui.textarea.suggestion.item.* import kotlinx.coroutines.* import java.awt.Dimension @@ -15,7 +15,7 @@ import javax.swing.event.ListDataListener class SuggestionsPopupManager( private val project: Project, - private val textField: PromptTextField, + private val userInputPanel: UserInputPanel, ) { private val scope = CoroutineScope(Dispatchers.Default + SupervisorJob()) private var selectedActionGroup: SuggestionGroupItem? = null @@ -28,16 +28,16 @@ class SuggestionsPopupManager( override fun contentsChanged(e: ListDataEvent) {} }) } - private val list = SuggestionList(listModel, textField) { + private val list = SuggestionList(listModel, userInputPanel) { handleSuggestionItemSelection(it) } private val defaultActions: MutableList = mutableListOf( FileSuggestionGroupItem(project), FolderSuggestionGroupItem(project), GitSuggestionGroupItem(project), - PersonaSuggestionGroupItem(), - DocumentationSuggestionGroupItem(), - WebSearchActionItem(), + PersonaSuggestionGroupItem(project), + DocumentationSuggestionGroupItem(project), + WebSearchActionItem(project), ) fun showPopup(component: JComponent? = null) { @@ -48,8 +48,8 @@ class SuggestionsPopupManager( true } .build(list) - popup?.showAbove(textField) - originalLocation = textField.locationOnScreen + popup?.showAbove(userInputPanel) + originalLocation = userInputPanel.locationOnScreen reset(true) selectNext() } @@ -98,13 +98,13 @@ class SuggestionsPopupManager( when (item) { is SuggestionActionItem -> { hidePopup() - item.execute(project, textField) + item.execute(project, userInputPanel) } is SuggestionGroupItem -> { selectedActionGroup = item updateSuggestions() - textField.requestFocus() + userInputPanel.requestFocus() } } } 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 39c85c32..1691ef9c 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 @@ -4,13 +4,9 @@ import com.intellij.icons.AllIcons import com.intellij.openapi.components.service import com.intellij.openapi.fileEditor.FileEditorManager 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.openapi.vfs.isFile import ee.carlrobert.codegpt.CodeGPTBundle -import ee.carlrobert.codegpt.CodeGPTKeys -import ee.carlrobert.codegpt.EncodingManager import ee.carlrobert.codegpt.Icons import ee.carlrobert.codegpt.settings.GeneralSettings import ee.carlrobert.codegpt.settings.documentation.DocumentationSettings @@ -20,9 +16,8 @@ import ee.carlrobert.codegpt.settings.prompts.PromptsConfigurable import ee.carlrobert.codegpt.settings.service.ServiceType 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 ee.carlrobert.codegpt.ui.textarea.UserInputPanel +import ee.carlrobert.codegpt.ui.textarea.header.* import git4idea.GitCommit import javax.swing.Icon @@ -30,9 +25,8 @@ class FileActionItem(val file: VirtualFile) : SuggestionActionItem { override val displayName = file.name override val icon = file.fileType.icon ?: AllIcons.FileTypes.Any_type - override fun execute(project: Project, textPane: PromptTextField) { - project.getService(FileSearchService::class.java).addFileToSession(file) - textPane.addInlayElement("file", file.name, this) + override fun execute(project: Project, userInputPanel: UserInputPanel) { + userInputPanel.addTag(FileTagDetails(file)) } } @@ -41,10 +35,15 @@ class IncludeOpenFilesActionItem : SuggestionActionItem { CodeGPTBundle.get("suggestionActionItem.includeOpenFiles.displayName") override val icon: Icon = Icons.ListFiles - override fun execute(project: Project, textPane: PromptTextField) { - val openFiles = project.service().openFiles.toList() - project.service().addFilesToSession(openFiles) - textPane.addInlayElement("files", "Open Files", this) + override fun execute(project: Project, userInputPanel: UserInputPanel) { + val fileTags = userInputPanel.getSelectedTags().filterIsInstance() + project.service().openFiles + .filter { openFile -> + fileTags.none { it.virtualFile == openFile } + } + .forEach { + userInputPanel.addTag(FileTagDetails(it)) + } } } @@ -52,12 +51,8 @@ class FolderActionItem(val folder: VirtualFile) : SuggestionActionItem { override val displayName = folder.name override val icon = AllIcons.Nodes.Folder - override fun execute(project: Project, textPane: PromptTextField) { - val fileSearchService = project.service() - folder.children - .filter { !it.isDirectory } - .forEach { fileSearchService.addFileToSession(it) } - textPane.addInlayElement("folder", folder.path, this) + override fun execute(project: Project, userInputPanel: UserInputPanel) { + userInputPanel.addTag(FolderTagDetails(folder)) } } @@ -65,9 +60,8 @@ class PersonaActionItem(val personaDetails: PersonaDetails) : SuggestionActionIt override val displayName = personaDetails.name override val icon = AllIcons.General.User - override fun execute(project: Project, textPane: PromptTextField) { - CodeGPTKeys.ADDED_PERSONA.set(project, personaDetails) - textPane.addInlayElement("persona", personaDetails.name, this) + override fun execute(project: Project, userInputPanel: UserInputPanel) { + userInputPanel.addTag(PersonaTagDetails(personaDetails)) } } @@ -78,10 +72,9 @@ class DocumentationActionItem( override val icon = AllIcons.Toolwindows.Documentation override val enabled = GeneralSettings.getSelectedService() == ServiceType.CODEGPT - override fun execute(project: Project, textPane: PromptTextField) { - CodeGPTKeys.ADDED_DOCUMENTATION.set(project, documentationDetails) + override fun execute(project: Project, userInputPanel: UserInputPanel) { service().updateLastUsedDateTime(documentationDetails.url) - textPane.addInlayElement("doc", documentationDetails.name, this) + userInputPanel.addTag(DocumentationTagDetails(documentationDetails)) } } @@ -91,54 +84,27 @@ class CreateDocumentationActionItem : SuggestionActionItem { override val icon = AllIcons.General.Add override val enabled = GeneralSettings.getSelectedService() == ServiceType.CODEGPT - override fun execute(project: Project, textPane: PromptTextField) { + override fun execute(project: Project, userInputPanel: UserInputPanel) { val addDocumentationDialog = AddDocumentationDialog(project) if (addDocumentationDialog.showAndGet()) { service() .updateLastUsedDateTime(addDocumentationDialog.documentationDetails.url) - textPane.addInlayElement( - "doc", - addDocumentationDialog.documentationDetails.name, - this - ) + userInputPanel.addTag(DocumentationTagDetails(addDocumentationDialog.documentationDetails)) } } } 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 ProgressManager.getInstance().runProcessWithProgressSynchronously( - { - val repository = GitUtil.getProjectRepository(project) - ?: return@runProcessWithProgressSynchronously "" - - val commitId = gitCommit.id.asString() - val diff = GitUtil.getCommitDiffs(project, repository, commitId) - .joinToString("\n") - - service().truncateText(diff, MAX_TOKENS, true) - }, - "Getting Diff", - true, - project - ) + override fun execute(project: Project, userInputPanel: UserInputPanel) { + userInputPanel.addTag(GitCommitTagDetails(gitCommit)) } } @@ -148,7 +114,7 @@ class ViewAllDocumentationsActionItem : SuggestionActionItem { override val icon = null override val enabled = GeneralSettings.getSelectedService() == ServiceType.CODEGPT - override fun execute(project: Project, textPane: PromptTextField) { + override fun execute(project: Project, userInputPanel: UserInputPanel) { service().showSettingsDialog( project, DocumentationsConfigurable::class.java @@ -161,7 +127,7 @@ class CreatePersonaActionItem : SuggestionActionItem { CodeGPTBundle.get("suggestionActionItem.createPersona.displayName") override val icon = AllIcons.General.Add - override fun execute(project: Project, textPane: PromptTextField) { + override fun execute(project: Project, userInputPanel: UserInputPanel) { service().showSettingsDialog( project, PromptsConfigurable::class.java @@ -169,13 +135,21 @@ class CreatePersonaActionItem : SuggestionActionItem { } } -class WebSearchActionItem : SuggestionActionItem { +class WebSearchActionItem(private val project: Project) : SuggestionActionItem { override val displayName: String = CodeGPTBundle.get("suggestionActionItem.webSearch.displayName") override val icon = AllIcons.General.Web - override val enabled = GeneralSettings.getSelectedService() == ServiceType.CODEGPT + override val enabled: Boolean + get() = enabled() - override fun execute(project: Project, textPane: PromptTextField) { - textPane.addInlayElement("web", null, this) + fun enabled(): Boolean { + if (GeneralSettings.getSelectedService() != ServiceType.CODEGPT) { + return false + } + return !TagUtil.isTagTypePresent(project, WebTagDetails::class.java) + } + + override fun execute(project: Project, userInputPanel: UserInputPanel) { + userInputPanel.addTag(WebTagDetails()) } } \ No newline at end of file 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 eec7261e..d6d2e3ba 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 @@ -15,6 +15,10 @@ import ee.carlrobert.codegpt.settings.prompts.PersonaDetails import ee.carlrobert.codegpt.settings.prompts.PromptsSettings import ee.carlrobert.codegpt.settings.service.ServiceType import ee.carlrobert.codegpt.ui.DocumentationDetails +import ee.carlrobert.codegpt.ui.textarea.header.DocumentationTagDetails +import ee.carlrobert.codegpt.ui.textarea.header.FileTagDetails +import ee.carlrobert.codegpt.ui.textarea.header.PersonaTagDetails +import ee.carlrobert.codegpt.ui.textarea.header.TagUtil import ee.carlrobert.codegpt.util.GitUtil import ee.carlrobert.codegpt.util.file.FileUtil import kotlinx.coroutines.Dispatchers @@ -39,7 +43,12 @@ class FileSuggestionGroupItem(private val project: Project) : SuggestionGroupIte return FileUtil.searchProjectFiles(project, searchText).toFileSuggestions() } - private fun Iterable.toFileSuggestions() = take(10).map { FileActionItem(it) } + IncludeOpenFilesActionItem() + private fun Iterable.toFileSuggestions(): List { + val selectedFileTags = TagUtil.getExistingTags(project, FileTagDetails::class.java) + return filter { file -> selectedFileTags.none { it.virtualFile == file } } + .take(10) + .map { FileActionItem(it) } + IncludeOpenFilesActionItem() + } } class FolderSuggestionGroupItem(private val project: Project) : SuggestionGroupItem { @@ -78,9 +87,11 @@ class FolderSuggestionGroupItem(private val project: Project) : SuggestionGroupI } } -class PersonaSuggestionGroupItem : SuggestionGroupItem { +class PersonaSuggestionGroupItem(private val project: Project) : SuggestionGroupItem { override val displayName: String = CodeGPTBundle.get("suggestionGroupItem.personas.displayName") override val icon = AllIcons.General.User + override val enabled: Boolean + get() = !TagUtil.isTagTypePresent(project, PersonaTagDetails::class.java) override suspend fun getSuggestions(searchText: String?): List { return service().state.personas.prompts @@ -95,10 +106,18 @@ class PersonaSuggestionGroupItem : SuggestionGroupItem { } } -class DocumentationSuggestionGroupItem : SuggestionGroupItem { +class DocumentationSuggestionGroupItem(private val project: Project) : SuggestionGroupItem { override val displayName: String = CodeGPTBundle.get("suggestionGroupItem.docs.displayName") override val icon = AllIcons.Toolwindows.Documentation - override val enabled = GeneralSettings.getSelectedService() == ServiceType.CODEGPT + override val enabled: Boolean + get() = enabled() + + fun enabled(): Boolean { + if (GeneralSettings.getSelectedService() != ServiceType.CODEGPT) { + return false + } + return !TagUtil.isTagTypePresent(project, DocumentationTagDetails::class.java) + } override suspend fun getSuggestions(searchText: String?): List = service().state.documentations @@ -135,9 +154,7 @@ class GitSuggestionGroupItem(private val project: Project) : SuggestionGroupItem GitUtil.getProjectRepository(project)?.let { GitUtil.getAllRecentCommits(project, it, searchText) .take(10) - .map { commit -> - GitCommitActionItem(project, commit) - } + .map { commit -> GitCommitActionItem(commit) } } ?: emptyList() } } diff --git a/src/main/kotlin/ee/carlrobert/codegpt/ui/textarea/suggestion/item/SuggestionItem.kt b/src/main/kotlin/ee/carlrobert/codegpt/ui/textarea/suggestion/item/SuggestionItem.kt index 9cae2cfd..dc75abdd 100644 --- a/src/main/kotlin/ee/carlrobert/codegpt/ui/textarea/suggestion/item/SuggestionItem.kt +++ b/src/main/kotlin/ee/carlrobert/codegpt/ui/textarea/suggestion/item/SuggestionItem.kt @@ -1,7 +1,7 @@ package ee.carlrobert.codegpt.ui.textarea.suggestion.item import com.intellij.openapi.project.Project -import ee.carlrobert.codegpt.ui.textarea.PromptTextField +import ee.carlrobert.codegpt.ui.textarea.UserInputPanel import javax.swing.Icon interface SuggestionItem { @@ -12,7 +12,7 @@ interface SuggestionItem { } interface SuggestionActionItem : SuggestionItem { - fun execute(project: Project, textPane: PromptTextField) + fun execute(project: Project, userInputPanel: UserInputPanel) } interface SuggestionGroupItem : SuggestionItem { 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 5d3c9e0b..e2ace7b8 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 @@ -9,7 +9,7 @@ import com.intellij.ui.dsl.builder.panel import com.intellij.ui.dsl.gridLayout.UnscaledGaps import com.intellij.util.ui.JBUI import ee.carlrobert.codegpt.settings.prompts.PromptsSettings -import ee.carlrobert.codegpt.ui.textarea.PromptTextField +import ee.carlrobert.codegpt.ui.textarea.UserInputPanel import ee.carlrobert.codegpt.ui.textarea.suggestion.item.* import ee.carlrobert.codegpt.ui.textarea.suggestion.renderer.SuggestionItemRendererTextUtils.highlightSearchText import ee.carlrobert.codegpt.ui.textarea.suggestion.renderer.SuggestionItemRendererTextUtils.searchText @@ -24,7 +24,7 @@ interface ItemRenderer { fun render(component: JLabel, value: SuggestionItem): JPanel } -abstract class BaseItemRenderer(private val textField: PromptTextField) : ItemRenderer { +abstract class BaseItemRenderer(private val userInputPanel: UserInputPanel) : ItemRenderer { protected fun createPanel( label: JLabel, icon: Icon, @@ -33,7 +33,7 @@ abstract class BaseItemRenderer(private val textField: PromptTextField) : ItemRe toolTipText: String?, truncateFromStart: Boolean = false ): JPanel { - val searchText = textField.text.searchText() + val searchText = userInputPanel.text.searchText() label.apply { this.icon = icon disabledIcon = icon @@ -60,7 +60,7 @@ abstract class BaseItemRenderer(private val textField: PromptTextField) : ItemRe } } -class FileItemRenderer(textPane: PromptTextField) : BaseItemRenderer(textPane) { +class FileItemRenderer(userInputPanel: UserInputPanel) : BaseItemRenderer(userInputPanel) { override fun render(component: JLabel, value: SuggestionItem): JPanel { val item = value as FileActionItem val icon = @@ -71,7 +71,7 @@ class FileItemRenderer(textPane: PromptTextField) : BaseItemRenderer(textPane) { } } -class FolderItemRenderer(textPane: PromptTextField) : BaseItemRenderer(textPane) { +class FolderItemRenderer(userInputPanel: UserInputPanel) : BaseItemRenderer(userInputPanel) { override fun render(component: JLabel, value: SuggestionItem): JPanel { val item = value as FolderActionItem return createPanel( @@ -85,7 +85,7 @@ class FolderItemRenderer(textPane: PromptTextField) : BaseItemRenderer(textPane) } } -class DefaultItemRenderer(textPane: PromptTextField) : BaseItemRenderer(textPane) { +class DefaultItemRenderer(userInputPanel: UserInputPanel) : BaseItemRenderer(userInputPanel) { companion object { private val EMPTY_ICON = ImageIcon(BufferedImage(16, 16, BufferedImage.TYPE_INT_ARGB)) } @@ -120,7 +120,7 @@ class DefaultItemRenderer(textPane: PromptTextField) : BaseItemRenderer(textPane } } -class PersonaItemRenderer(textPane: PromptTextField) : BaseItemRenderer(textPane) { +class PersonaItemRenderer(userInputPanel: UserInputPanel) : BaseItemRenderer(userInputPanel) { override fun render(component: JLabel, value: SuggestionItem): JPanel { val item = value as PersonaActionItem return createPanel( @@ -133,7 +133,7 @@ class PersonaItemRenderer(textPane: PromptTextField) : BaseItemRenderer(textPane } } -class GitCommitItemRenderer(textPane: PromptTextField) : BaseItemRenderer(textPane) { +class GitCommitItemRenderer(userInputPanel: UserInputPanel) : BaseItemRenderer(userInputPanel) { override fun render(component: JLabel, value: SuggestionItem): JPanel { val item = value as GitCommitActionItem val author = item.gitCommit.author.name @@ -148,7 +148,7 @@ class GitCommitItemRenderer(textPane: PromptTextField) : BaseItemRenderer(textPa } } -class DocumentationItemRenderer(textPane: PromptTextField) : BaseItemRenderer(textPane) { +class DocumentationItemRenderer(userInputPanel: UserInputPanel) : BaseItemRenderer(userInputPanel) { override fun render(component: JLabel, value: SuggestionItem): JPanel { val item = value as DocumentationActionItem return createPanel( @@ -161,15 +161,15 @@ class DocumentationItemRenderer(textPane: PromptTextField) : BaseItemRenderer(te } } -class RendererFactory(private val textPane: PromptTextField) { +class RendererFactory(private val userInputPanel: UserInputPanel) { fun getRenderer(item: SuggestionItem): ItemRenderer { return when (item) { - is FileActionItem -> FileItemRenderer(textPane) - is FolderActionItem -> FolderItemRenderer(textPane) - is PersonaActionItem -> PersonaItemRenderer(textPane) - is GitCommitActionItem -> GitCommitItemRenderer(textPane) - is DocumentationActionItem -> DocumentationItemRenderer(textPane) - else -> DefaultItemRenderer(textPane) + is FileActionItem -> FileItemRenderer(userInputPanel) + is FolderActionItem -> FolderItemRenderer(userInputPanel) + is PersonaActionItem -> PersonaItemRenderer(userInputPanel) + is GitCommitActionItem -> GitCommitItemRenderer(userInputPanel) + is DocumentationActionItem -> DocumentationItemRenderer(userInputPanel) + else -> DefaultItemRenderer(userInputPanel) } } } \ No newline at end of file diff --git a/src/main/kotlin/ee/carlrobert/codegpt/ui/textarea/suggestion/renderer/SuggestionListCellRenderer.kt b/src/main/kotlin/ee/carlrobert/codegpt/ui/textarea/suggestion/renderer/SuggestionListCellRenderer.kt index c564a837..14c7f09e 100644 --- a/src/main/kotlin/ee/carlrobert/codegpt/ui/textarea/suggestion/renderer/SuggestionListCellRenderer.kt +++ b/src/main/kotlin/ee/carlrobert/codegpt/ui/textarea/suggestion/renderer/SuggestionListCellRenderer.kt @@ -1,14 +1,14 @@ package ee.carlrobert.codegpt.ui.textarea.suggestion.renderer import com.intellij.util.ui.JBUI -import ee.carlrobert.codegpt.ui.textarea.PromptTextField +import ee.carlrobert.codegpt.ui.textarea.UserInputPanel import ee.carlrobert.codegpt.ui.textarea.suggestion.item.SuggestionItem import java.awt.Component import java.awt.Dimension import javax.swing.* -class SuggestionListCellRenderer(textPane: PromptTextField) : DefaultListCellRenderer() { - private val rendererFactory = RendererFactory(textPane) +class SuggestionListCellRenderer(userInputPanel: UserInputPanel) : DefaultListCellRenderer() { + private val rendererFactory = RendererFactory(userInputPanel) override fun getListCellRendererComponent( list: JList<*>?, diff --git a/src/main/kotlin/ee/carlrobert/codegpt/util/EditorUtil.kt b/src/main/kotlin/ee/carlrobert/codegpt/util/EditorUtil.kt index 9201cc14..f79af9ac 100644 --- a/src/main/kotlin/ee/carlrobert/codegpt/util/EditorUtil.kt +++ b/src/main/kotlin/ee/carlrobert/codegpt/util/EditorUtil.kt @@ -10,6 +10,8 @@ import com.intellij.openapi.editor.Document import com.intellij.openapi.editor.Editor import com.intellij.openapi.editor.EditorFactory import com.intellij.openapi.editor.EditorKind +import com.intellij.openapi.editor.colors.EditorColorsManager +import com.intellij.openapi.editor.ex.EditorEx import com.intellij.openapi.fileEditor.FileDocumentManager import com.intellij.openapi.fileEditor.FileEditor import com.intellij.openapi.fileEditor.FileEditorManager @@ -18,6 +20,7 @@ import com.intellij.openapi.fileEditor.impl.FileEditorManagerImpl import com.intellij.openapi.project.Project import com.intellij.openapi.util.TextRange import com.intellij.openapi.util.text.StringUtil +import com.intellij.openapi.vfs.VirtualFile import com.intellij.psi.PsiDocumentManager import com.intellij.psi.codeStyle.CodeStyleManager import com.intellij.testFramework.LightVirtualFile @@ -37,8 +40,10 @@ object EditorUtil { ) val editorFactory = EditorFactory.getInstance() val document = editorFactory.createDocument(code) - return editorFactory + val editor= editorFactory .createEditor(document, project, lightVirtualFile, true, EditorKind.MAIN_EDITOR) + (editor as EditorEx).backgroundColor = service().globalScheme.defaultBackground + return editor } @JvmStatic @@ -76,6 +81,18 @@ object EditorUtil { return FileEditorManager.getInstance(project)?.selectedTextEditor } + fun getSelectedEditorFile(project: Project): VirtualFile? { + return getSelectedEditor(project)?.virtualFile + } + + @JvmStatic + fun getOpenLocalFiles(project: Project): List { + return FileEditorManager.getInstance(project).openFiles + .filter { it.isValid && it.isInLocalFileSystem } + .sortedBy { it.modificationStamp } + .toList() + } + @JvmStatic fun getOpenFiles(project: Project): List { val fileDocumentManager = FileDocumentManager.getInstance() diff --git a/src/main/kotlin/ee/carlrobert/codegpt/util/file/FileUtil.kt b/src/main/kotlin/ee/carlrobert/codegpt/util/file/FileUtil.kt index db147036..58704ef8 100644 --- a/src/main/kotlin/ee/carlrobert/codegpt/util/file/FileUtil.kt +++ b/src/main/kotlin/ee/carlrobert/codegpt/util/file/FileUtil.kt @@ -5,8 +5,6 @@ import com.fasterxml.jackson.core.type.TypeReference import com.fasterxml.jackson.databind.ObjectMapper import com.intellij.openapi.components.service import com.intellij.openapi.diagnostic.Logger -import com.intellij.openapi.editor.Editor -import com.intellij.openapi.fileEditor.FileDocumentManager import com.intellij.openapi.progress.ProgressIndicator import com.intellij.openapi.project.Project import com.intellij.openapi.roots.ProjectFileIndex @@ -14,6 +12,8 @@ import com.intellij.openapi.util.io.FileUtil.createDirectory import com.intellij.openapi.vfs.VirtualFile import com.intellij.openapi.vfs.VirtualFileFilter import ee.carlrobert.codegpt.settings.service.llama.LlamaSettings.getLlamaModelsPath +import ee.carlrobert.codegpt.toolwindow.chat.ChatToolWindowContentManager +import ee.carlrobert.codegpt.ui.textarea.header.FileTagDetails import java.io.File import java.io.FileOutputStream import java.io.IOException @@ -246,7 +246,8 @@ object FileUtil { } }) - return results.sortedByDescending { it.score } + return results + .sortedByDescending { it.score } .take(maxResults) .map { it.file } } diff --git a/src/main/resources/META-INF/plugin.xml b/src/main/resources/META-INF/plugin.xml index 9277b249..106f44ae 100644 --- a/src/main/resources/META-INF/plugin.xml +++ b/src/main/resources/META-INF/plugin.xml @@ -26,6 +26,7 @@ + - - - + + + + + + + + diff --git a/src/main/resources/icons/inSelection_dark.svg b/src/main/resources/icons/inSelection_dark.svg new file mode 100644 index 00000000..676adc5a --- /dev/null +++ b/src/main/resources/icons/inSelection_dark.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/src/main/resources/messages/codegpt.properties b/src/main/resources/messages/codegpt.properties index ed1f9162..9452449a 100644 --- a/src/main/resources/messages/codegpt.properties +++ b/src/main/resources/messages/codegpt.properties @@ -291,6 +291,7 @@ addDocumentation.popup.form.url.label=URL: addDocumentation.popup.form.url.comment=Enter the full web address of the documentation. addDocumentation.popup.form.saveCheckbox.label=Save for future reference userMessagePanel.documentation.title=DOCUMENTATION +userMessagePanel.persona.title=PERSONA suggestionGroupItem.files.displayName=Files suggestionGroupItem.folders.displayName=Folders suggestionGroupItem.personas.displayName=Personas diff --git a/src/test/kotlin/ee/carlrobert/codegpt/toolwindow/chat/ChatToolWindowTabPanelTest.kt b/src/test/kotlin/ee/carlrobert/codegpt/toolwindow/chat/ChatToolWindowTabPanelTest.kt index 89a57257..c01dcbcf 100644 --- a/src/test/kotlin/ee/carlrobert/codegpt/toolwindow/chat/ChatToolWindowTabPanelTest.kt +++ b/src/test/kotlin/ee/carlrobert/codegpt/toolwindow/chat/ChatToolWindowTabPanelTest.kt @@ -1,9 +1,11 @@ package ee.carlrobert.codegpt.toolwindow.chat import com.intellij.openapi.components.service +import com.intellij.testFramework.LightVirtualFile import ee.carlrobert.codegpt.CodeGPTKeys import ee.carlrobert.codegpt.EncodingManager -import ee.carlrobert.codegpt.ReferencedFile +import ee.carlrobert.codegpt.actions.IncludeFilesInContextNotifier +import ee.carlrobert.codegpt.actions.IncludeFilesInContextNotifier.FILES_INCLUDED_IN_CONTEXT_TOPIC import ee.carlrobert.codegpt.completions.ConversationType import ee.carlrobert.codegpt.completions.HuggingFaceModel import ee.carlrobert.codegpt.completions.llama.PromptTemplate.LLAMA @@ -96,13 +98,6 @@ class ChatToolWindowTabPanelTest : IntegrationTest() { } fun testSendingOpenAIMessageWithReferencedContext() { - project.putUserData( - CodeGPTKeys.SELECTED_FILES, listOf( - ReferencedFile("TEST_FILE_NAME_1", "TEST_FILE_PATH_1", "TEST_FILE_CONTENT_1"), - ReferencedFile("TEST_FILE_NAME_2", "TEST_FILE_PATH_2", "TEST_FILE_CONTENT_2"), - ReferencedFile("TEST_FILE_NAME_3", "TEST_FILE_PATH_3", "TEST_FILE_CONTENT_3") - ) - ) useOpenAIService() service().state.personas.selectedPersona.instructions = "TEST_SYSTEM_PROMPT" @@ -111,6 +106,15 @@ class ChatToolWindowTabPanelTest : IntegrationTest() { listOf("TEST_FILE_PATH_1", "TEST_FILE_PATH_2", "TEST_FILE_PATH_3") val conversation = ConversationService.getInstance().startConversation() val panel = ChatToolWindowTabPanel(project, conversation) + project.messageBus + .syncPublisher(FILES_INCLUDED_IN_CONTEXT_TOPIC) + .filesIncluded( + listOf( + LightVirtualFile("TEST_FILE_NAME_1", "TEST_FILE_CONTENT_1"), + LightVirtualFile("TEST_FILE_NAME_2", "TEST_FILE_CONTENT_2"), + LightVirtualFile("TEST_FILE_NAME_3", "TEST_FILE_CONTENT_3"), + ) + ) expectOpenAI(StreamHttpExchange { request: RequestEntity -> assertThat(request.uri.path).isEqualTo("/v1/chat/completions") assertThat(request.method).isEqualTo("POST") @@ -125,28 +129,29 @@ class ChatToolWindowTabPanelTest : IntegrationTest() { listOf( mapOf("role" to "system", "content" to "TEST_SYSTEM_PROMPT"), mapOf( - "role" to "user", "content" to """ - Use the following context to answer question at the end: + "role" to "user", + "content" to """ + Use the following context to answer question at the end: - File Path: TEST_FILE_PATH_1 - File Content: - ```TEST_FILE_NAME_1 - TEST_FILE_CONTENT_1 - ``` - - File Path: TEST_FILE_PATH_2 - File Content: - ```TEST_FILE_NAME_2 - TEST_FILE_CONTENT_2 - ``` - - File Path: TEST_FILE_PATH_3 - File Content: - ```TEST_FILE_NAME_3 - TEST_FILE_CONTENT_3 - ``` - - Question: TEST_MESSAGE""".trimIndent() + File Path: /TEST_FILE_NAME_1 + File Content: + ```TEST_FILE_NAME_1 + TEST_FILE_CONTENT_1 + ``` + + File Path: /TEST_FILE_NAME_2 + File Content: + ```TEST_FILE_NAME_2 + TEST_FILE_CONTENT_2 + ``` + + File Path: /TEST_FILE_NAME_3 + File Content: + ```TEST_FILE_NAME_3 + TEST_FILE_CONTENT_3 + ``` + + Question: TEST_MESSAGE""".trimIndent() ) ) ) @@ -293,13 +298,6 @@ class ChatToolWindowTabPanelTest : IntegrationTest() { } fun testFixCompileErrorsWithOpenAIService() { - project.putUserData( - CodeGPTKeys.SELECTED_FILES, listOf( - ReferencedFile("TEST_FILE_NAME_1", "TEST_FILE_PATH_1", "TEST_FILE_CONTENT_1"), - ReferencedFile("TEST_FILE_NAME_2", "TEST_FILE_PATH_2", "TEST_FILE_CONTENT_2"), - ReferencedFile("TEST_FILE_NAME_3", "TEST_FILE_PATH_3", "TEST_FILE_CONTENT_3") - ) - ) useOpenAIService() service().state.personas.selectedPersona.instructions = "TEST_SYSTEM_PROMPT" @@ -308,6 +306,15 @@ class ChatToolWindowTabPanelTest : IntegrationTest() { listOf("TEST_FILE_PATH_1", "TEST_FILE_PATH_2", "TEST_FILE_PATH_3") val conversation = ConversationService.getInstance().startConversation() val panel = ChatToolWindowTabPanel(project, conversation) + project.messageBus + .syncPublisher(FILES_INCLUDED_IN_CONTEXT_TOPIC) + .filesIncluded( + listOf( + LightVirtualFile("TEST_FILE_NAME_1", "TEST_FILE_CONTENT_1"), + LightVirtualFile("TEST_FILE_NAME_2", "TEST_FILE_CONTENT_2"), + LightVirtualFile("TEST_FILE_NAME_3", "TEST_FILE_CONTENT_3"), + ) + ) expectOpenAI(StreamHttpExchange { request: RequestEntity -> assertThat(request.uri.path).isEqualTo("/v1/chat/completions") assertThat(request.method).isEqualTo("POST") @@ -325,28 +332,29 @@ class ChatToolWindowTabPanelTest : IntegrationTest() { "content" to service().state.coreActions.fixCompileErrors.instructions ), mapOf( - "role" to "user", "content" to """ - Use the following context to answer question at the end: + "role" to "user", + "content" to """ + Use the following context to answer question at the end: - File Path: TEST_FILE_PATH_1 - File Content: - ```TEST_FILE_NAME_1 - TEST_FILE_CONTENT_1 - ``` - - File Path: TEST_FILE_PATH_2 - File Content: - ```TEST_FILE_NAME_2 - TEST_FILE_CONTENT_2 - ``` - - File Path: TEST_FILE_PATH_3 - File Content: - ```TEST_FILE_NAME_3 - TEST_FILE_CONTENT_3 - ``` - - Question: TEST_MESSAGE""".trimIndent() + File Path: /TEST_FILE_NAME_1 + File Content: + ```TEST_FILE_NAME_1 + TEST_FILE_CONTENT_1 + ``` + + File Path: /TEST_FILE_NAME_2 + File Content: + ```TEST_FILE_NAME_2 + TEST_FILE_CONTENT_2 + ``` + + File Path: /TEST_FILE_NAME_3 + File Content: + ```TEST_FILE_NAME_3 + TEST_FILE_CONTENT_3 + ``` + + Question: TEST_MESSAGE""".trimIndent() ) ) ) diff --git a/src/test/kotlin/testsupport/IntegrationTest.kt b/src/test/kotlin/testsupport/IntegrationTest.kt index 28da2eca..571043c6 100644 --- a/src/test/kotlin/testsupport/IntegrationTest.kt +++ b/src/test/kotlin/testsupport/IntegrationTest.kt @@ -8,26 +8,25 @@ import testsupport.mixin.ShortcutsTestMixin open class IntegrationTest : BasePlatformTestCase(), ExternalServiceTestMixin, ShortcutsTestMixin { - @Throws(Exception::class) - override fun tearDown() { - ExternalServiceTestMixin.clearAll() - clearKeys() - super.tearDown() - } - - private fun clearKeys() { - putUserData(CodeGPTKeys.SELECTED_FILES, emptyList()) - putUserData(CodeGPTKeys.PREVIOUS_INLAY_TEXT, "") - putUserData(CodeGPTKeys.IMAGE_ATTACHMENT_FILE_PATH, "") - } - - private fun putUserData(key: Key, value: T) { - project.putUserData(key, value) - } - - companion object { - init { - ExternalServiceTestMixin.init() + @Throws(Exception::class) + override fun tearDown() { + ExternalServiceTestMixin.clearAll() + clearKeys() + super.tearDown() + } + + private fun clearKeys() { + putUserData(CodeGPTKeys.PREVIOUS_INLAY_TEXT, "") + putUserData(CodeGPTKeys.IMAGE_ATTACHMENT_FILE_PATH, "") + } + + private fun putUserData(key: Key, value: T) { + project.putUserData(key, value) + } + + companion object { + init { + ExternalServiceTestMixin.init() + } } - } }