diff --git a/codegpt-core/src/main/java/ee/carlrobert/embedding/CheckedFile.java b/codegpt-core/src/main/java/ee/carlrobert/embedding/CheckedFile.java index 76b0841d..a5ea9460 100644 --- a/codegpt-core/src/main/java/ee/carlrobert/embedding/CheckedFile.java +++ b/codegpt-core/src/main/java/ee/carlrobert/embedding/CheckedFile.java @@ -23,6 +23,12 @@ public class CheckedFile { } } + public CheckedFile(String fileName, String filePath, String fileContent) { + this.fileName = fileName; + this.filePath = filePath; + this.fileContent = fileContent; + } + public String getFileName() { return fileName; } diff --git a/src/main/java/ee/carlrobert/codegpt/CodeGPTKeys.java b/src/main/java/ee/carlrobert/codegpt/CodeGPTKeys.java new file mode 100644 index 00000000..1d93840c --- /dev/null +++ b/src/main/java/ee/carlrobert/codegpt/CodeGPTKeys.java @@ -0,0 +1,10 @@ +package ee.carlrobert.codegpt; + +import com.intellij.openapi.util.Key; +import ee.carlrobert.embedding.CheckedFile; +import java.util.List; + +public class CodeGPTKeys { + + public static final Key> SELECTED_FILES = Key.create("selectedFiles"); +} diff --git a/src/main/java/ee/carlrobert/codegpt/CodeGPTUpdateStartupActivity.java b/src/main/java/ee/carlrobert/codegpt/CodeGPTUpdateStartupActivity.java index 8094101d..2ceeadff 100644 --- a/src/main/java/ee/carlrobert/codegpt/CodeGPTUpdateStartupActivity.java +++ b/src/main/java/ee/carlrobert/codegpt/CodeGPTUpdateStartupActivity.java @@ -14,7 +14,7 @@ import com.intellij.openapi.updateSettings.impl.UpdateChecker; import com.intellij.openapi.updateSettings.impl.UpdateSettings; import com.intellij.util.concurrency.AppExecutorUtil; import ee.carlrobert.codegpt.settings.configuration.ConfigurationState; -import ee.carlrobert.codegpt.util.OverlayUtil; +import ee.carlrobert.codegpt.ui.OverlayUtil; import java.util.concurrent.TimeUnit; import org.jetbrains.annotations.NotNull; @@ -22,6 +22,10 @@ public class CodeGPTUpdateStartupActivity implements StartupActivity.Background @Override public void runActivity(@NotNull Project project) { + if (ApplicationManager.getApplication().isUnitTestMode()) { + return; + } + schedulePluginUpdateChecks(project); } diff --git a/src/main/java/ee/carlrobert/codegpt/EncodingManager.java b/src/main/java/ee/carlrobert/codegpt/EncodingManager.java index 6eb46d12..b25c60c3 100644 --- a/src/main/java/ee/carlrobert/codegpt/EncodingManager.java +++ b/src/main/java/ee/carlrobert/codegpt/EncodingManager.java @@ -2,6 +2,7 @@ package ee.carlrobert.codegpt; import com.intellij.openapi.application.ApplicationManager; import com.intellij.openapi.components.Service; +import com.intellij.openapi.diagnostic.Logger; import com.knuddels.jtokkit.Encodings; import com.knuddels.jtokkit.api.Encoding; import com.knuddels.jtokkit.api.EncodingRegistry; @@ -12,6 +13,8 @@ import ee.carlrobert.llm.client.openai.completion.request.OpenAIChatCompletionMe @Service public final class EncodingManager { + private static final Logger LOG = Logger.getInstance(EncodingManager.class); + private final EncodingRegistry registry = Encodings.newDefaultEncodingRegistry(); private final Encoding encoding = registry.getEncoding(EncodingType.CL100K_BASE); @@ -42,6 +45,11 @@ public final class EncodingManager { } public int countTokens(String text) { - return encoding.countTokens(text); + try { + return encoding.countTokens(text); + } catch (Exception ex) { + LOG.error(ex); + return 0; + } } } diff --git a/src/main/java/ee/carlrobert/codegpt/PluginStartupActivity.java b/src/main/java/ee/carlrobert/codegpt/PluginStartupActivity.java index f0895aaa..824a84b6 100644 --- a/src/main/java/ee/carlrobert/codegpt/PluginStartupActivity.java +++ b/src/main/java/ee/carlrobert/codegpt/PluginStartupActivity.java @@ -12,7 +12,7 @@ import ee.carlrobert.codegpt.completions.you.auth.YouAuthenticationService; import ee.carlrobert.codegpt.completions.you.auth.response.YouAuthenticationResponse; import ee.carlrobert.codegpt.credentials.YouCredentialsManager; import ee.carlrobert.codegpt.settings.state.SettingsState; -import ee.carlrobert.codegpt.util.OverlayUtil; +import ee.carlrobert.codegpt.ui.OverlayUtil; import org.jetbrains.annotations.NotNull; public class PluginStartupActivity implements StartupActivity { diff --git a/src/main/java/ee/carlrobert/codegpt/actions/GenerateGitCommitMessageAction.java b/src/main/java/ee/carlrobert/codegpt/actions/GenerateGitCommitMessageAction.java index 1011a39f..769a7ce0 100644 --- a/src/main/java/ee/carlrobert/codegpt/actions/GenerateGitCommitMessageAction.java +++ b/src/main/java/ee/carlrobert/codegpt/actions/GenerateGitCommitMessageAction.java @@ -27,7 +27,7 @@ import ee.carlrobert.codegpt.completions.CompletionClientProvider; import ee.carlrobert.codegpt.credentials.OpenAICredentialsManager; import ee.carlrobert.codegpt.settings.configuration.ConfigurationState; import ee.carlrobert.codegpt.settings.state.OpenAISettingsState; -import ee.carlrobert.codegpt.util.OverlayUtil; +import ee.carlrobert.codegpt.ui.OverlayUtil; import ee.carlrobert.llm.client.openai.completion.ErrorDetails; import ee.carlrobert.llm.client.openai.completion.request.OpenAIChatCompletionMessage; import ee.carlrobert.llm.client.openai.completion.request.OpenAIChatCompletionRequest; diff --git a/src/main/java/ee/carlrobert/codegpt/actions/IncludeFilesInContextAction.java b/src/main/java/ee/carlrobert/codegpt/actions/IncludeFilesInContextAction.java new file mode 100644 index 00000000..f0684b87 --- /dev/null +++ b/src/main/java/ee/carlrobert/codegpt/actions/IncludeFilesInContextAction.java @@ -0,0 +1,227 @@ +package ee.carlrobert.codegpt.actions; + +import static com.intellij.openapi.actionSystem.CommonDataKeys.VIRTUAL_FILE_ARRAY; +import static com.intellij.openapi.ui.DialogWrapper.OK_EXIT_CODE; +import static java.lang.String.format; + +import com.intellij.openapi.actionSystem.AnAction; +import com.intellij.openapi.actionSystem.AnActionEvent; +import com.intellij.openapi.actionSystem.CommonDataKeys; +import com.intellij.openapi.actionSystem.DataContext; +import com.intellij.openapi.diagnostic.Logger; +import com.intellij.openapi.project.Project; +import com.intellij.openapi.ui.DialogBuilder; +import com.intellij.openapi.vfs.VirtualFile; +import com.intellij.psi.PsiElement; +import com.intellij.ui.CheckboxTreeListener; +import com.intellij.ui.CheckedTreeNode; +import com.intellij.ui.ScrollPaneFactory; +import com.intellij.ui.components.JBLabel; +import com.intellij.ui.components.JBTextArea; +import com.intellij.util.ui.FormBuilder; +import com.intellij.util.ui.JBUI; +import ee.carlrobert.codegpt.CodeGPTBundle; +import ee.carlrobert.codegpt.CodeGPTKeys; +import ee.carlrobert.codegpt.EncodingManager; +import ee.carlrobert.codegpt.settings.state.IncludedFilesSettingsState; +import ee.carlrobert.codegpt.ui.UIUtil; +import ee.carlrobert.codegpt.ui.checkbox.FileCheckboxTree; +import ee.carlrobert.codegpt.ui.checkbox.PsiElementCheckboxTree; +import ee.carlrobert.codegpt.ui.checkbox.VirtualFileCheckboxTree; +import ee.carlrobert.embedding.CheckedFile; +import java.awt.Dimension; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Paths; +import java.util.List; +import javax.swing.JButton; +import javax.swing.JComponent; +import javax.swing.SwingUtilities; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +public class IncludeFilesInContextAction extends AnAction { + + private static final Logger LOG = Logger.getInstance(IncludeFilesInContextAction.class); + + public IncludeFilesInContextAction() { + super(CodeGPTBundle.get("action.includeFilesInContext.title")); + } + + @Override + public void actionPerformed(@NotNull AnActionEvent e) { + var project = e.getProject(); + if (project == null) { + return; + } + + var checkboxTree = getCheckboxTree(e.getDataContext()); + if (checkboxTree == null) { + throw new RuntimeException("Could not obtain file tree"); + } + + var totalTokensLabel = new TotalTokensLabel(checkboxTree.getCheckedFiles()); + checkboxTree.addCheckboxTreeListener(new CheckboxTreeListener() { + @Override + public void nodeStateChanged(@NotNull CheckedTreeNode node) { + totalTokensLabel.updateState(node); + } + }); + + var includedFilesSettings = IncludedFilesSettingsState.getInstance(); + var promptTemplateTextArea = UIUtil.createTextArea(includedFilesSettings.getPromptTemplate()); + var repeatableContextTextArea = + UIUtil.createTextArea(includedFilesSettings.getRepeatableContext()); + var show = showMultiFilePromptDialog( + project, + promptTemplateTextArea, + repeatableContextTextArea, + totalTokensLabel, + checkboxTree); + if (show == OK_EXIT_CODE) { + project.putUserData(CodeGPTKeys.SELECTED_FILES, checkboxTree.getCheckedFiles()); + project.getMessageBus() + .syncPublisher(IncludeFilesInContextNotifier.FILES_INCLUDED_IN_CONTEXT_TOPIC) + .filesIncluded(checkboxTree.getCheckedFiles()); + includedFilesSettings.setPromptTemplate(promptTemplateTextArea.getText()); + includedFilesSettings.setRepeatableContext(repeatableContextTextArea.getText()); + } + } + + private @Nullable FileCheckboxTree getCheckboxTree(DataContext dataContext) { + var psiElement = CommonDataKeys.PSI_ELEMENT.getData(dataContext); + if (psiElement != null) { + return new PsiElementCheckboxTree(psiElement); + } + + var selectedVirtualFiles = VIRTUAL_FILE_ARRAY.getData(dataContext); + if (selectedVirtualFiles != null) { + return new VirtualFileCheckboxTree(selectedVirtualFiles); + } + + return null; + } + + private static class TotalTokensLabel extends JBLabel { + + private static final EncodingManager encodingManager = EncodingManager.getInstance(); + + private int fileCount; + private int totalTokens; + + TotalTokensLabel(List checkedFiles) { + fileCount = checkedFiles.size(); + totalTokens = calculateTotalTokens(checkedFiles); + updateText(); + } + + void updateState(CheckedTreeNode checkedNode) { + var fileContent = getNodeFileContent(checkedNode); + if (fileContent != null) { + int tokenCount = encodingManager.countTokens(fileContent); + if (checkedNode.isChecked()) { + totalTokens += tokenCount; + fileCount++; + } else { + totalTokens -= tokenCount; + fileCount--; + } + + SwingUtilities.invokeLater(this::updateText); + } + } + + private @Nullable String getNodeFileContent(CheckedTreeNode checkedNode) { + var userObject = checkedNode.getUserObject(); + if (userObject instanceof PsiElement) { + var psiFile = ((PsiElement) userObject).getContainingFile(); + if (psiFile != null) { + var virtualFile = psiFile.getVirtualFile(); + if (virtualFile != null) { + return getVirtualFileContent(virtualFile); + } + } + } + if (userObject instanceof VirtualFile) { + return getVirtualFileContent((VirtualFile) userObject); + } + return null; + } + + private String getVirtualFileContent(VirtualFile virtualFile) { + try { + return new String(Files.readAllBytes(Paths.get(virtualFile.getPath()))); + } catch (IOException ex) { + LOG.error(ex); + } + return null; + } + + private void updateText() { + setText(format( + "%d %s totaling %d tokens", + fileCount, + fileCount == 1 ? "file" : "files", + totalTokens)); + } + + private int calculateTotalTokens(List checkedFiles) { + return checkedFiles.stream() + .mapToInt(file -> encodingManager.countTokens(file.getFileContent())) + .sum(); + } + } + + private static int showMultiFilePromptDialog( + Project project, + JBTextArea promptTemplateTextArea, + JBTextArea repeatableContextTextArea, + JBLabel totalTokensLabel, + JComponent component) { + var dialogBuilder = new DialogBuilder(project); + dialogBuilder.setTitle(CodeGPTBundle.get("action.includeFilesInContext.dialog.title")); + dialogBuilder.setActionDescriptors(); + var fileTreeScrollPane = ScrollPaneFactory.createScrollPane(component); + fileTreeScrollPane.setPreferredSize( + new Dimension(480, component.getPreferredSize().height + 48)); + dialogBuilder.setNorthPanel(FormBuilder.createFormBuilder() + .addLabeledComponent( + CodeGPTBundle.get("action.includeFilesInContext.dialog.promptTemplate.label"), + promptTemplateTextArea, + true) + .addLabeledComponent( + CodeGPTBundle.get("action.includeFilesInContext.dialog.repeatableContext.label"), + repeatableContextTextArea, + true) + .addVerticalGap(4) + .addComponent(JBUI.Panels.simplePanel() + .addToRight(getRestoreButton(promptTemplateTextArea, repeatableContextTextArea))) + .addVerticalGap(16) + .addComponent( + new JBLabel(CodeGPTBundle.get("action.includeFilesInContext.dialog.description")) + .setCopyable(false) + .setAllowAutoWrapping(true)) + .addVerticalGap(4) + .addLabeledComponent(totalTokensLabel, fileTreeScrollPane, true) + .addVerticalGap(16) + .getPanel()); + dialogBuilder.addOkAction().setText(CodeGPTBundle.get("dialog.continue")); + dialogBuilder.addCancelAction(); + return dialogBuilder.show(); + } + + private static JButton getRestoreButton(JBTextArea promptTemplateTextArea, + JBTextArea repeatableContextTextArea) { + var restoreButton = new JButton( + CodeGPTBundle.get("action.includeFilesInContext.dialog.restoreToDefaults.label")); + restoreButton.addActionListener(e -> { + var includedFilesSettings = IncludedFilesSettingsState.getInstance(); + includedFilesSettings.setPromptTemplate(IncludedFilesSettingsState.DEFAULT_PROMPT_TEMPLATE); + includedFilesSettings.setRepeatableContext( + IncludedFilesSettingsState.DEFAULT_REPEATABLE_CONTEXT); + promptTemplateTextArea.setText(IncludedFilesSettingsState.DEFAULT_PROMPT_TEMPLATE); + repeatableContextTextArea.setText(IncludedFilesSettingsState.DEFAULT_REPEATABLE_CONTEXT); + }); + return restoreButton; + } +} \ No newline at end of file diff --git a/src/main/java/ee/carlrobert/codegpt/actions/IncludeFilesInContextNotifier.java b/src/main/java/ee/carlrobert/codegpt/actions/IncludeFilesInContextNotifier.java new file mode 100644 index 00000000..1ca00ed7 --- /dev/null +++ b/src/main/java/ee/carlrobert/codegpt/actions/IncludeFilesInContextNotifier.java @@ -0,0 +1,13 @@ +package ee.carlrobert.codegpt.actions; + +import com.intellij.util.messages.Topic; +import ee.carlrobert.embedding.CheckedFile; +import java.util.List; + +public interface IncludeFilesInContextNotifier { + + Topic FILES_INCLUDED_IN_CONTEXT_TOPIC = + Topic.create("filesIncludedInContext", IncludeFilesInContextNotifier.class); + + void filesIncluded(List includedFiles); +} diff --git a/src/main/java/ee/carlrobert/codegpt/actions/editor/CustomPromptAction.java b/src/main/java/ee/carlrobert/codegpt/actions/editor/CustomPromptAction.java index a77a7233..a1978f1b 100644 --- a/src/main/java/ee/carlrobert/codegpt/actions/editor/CustomPromptAction.java +++ b/src/main/java/ee/carlrobert/codegpt/actions/editor/CustomPromptAction.java @@ -12,7 +12,7 @@ import com.intellij.util.ui.JBUI; import com.intellij.util.ui.UI; import ee.carlrobert.codegpt.conversations.message.Message; import ee.carlrobert.codegpt.toolwindow.chat.standard.StandardChatToolWindowContentManager; -import ee.carlrobert.codegpt.util.UIUtil; +import ee.carlrobert.codegpt.ui.UIUtil; import ee.carlrobert.codegpt.util.file.FileUtil; import java.awt.event.ActionEvent; import javax.swing.AbstractAction; @@ -21,7 +21,7 @@ import javax.swing.JTextArea; import javax.swing.SwingUtilities; import org.jetbrains.annotations.Nullable; -class CustomPromptAction extends BaseEditorAction { +public class CustomPromptAction extends BaseEditorAction { private static String previousUserPrompt = ""; @@ -47,7 +47,7 @@ class CustomPromptAction extends BaseEditorAction { } } - private static class CustomPromptDialog extends DialogWrapper { + public static class CustomPromptDialog extends DialogWrapper { private final JTextArea userPromptTextArea; 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 300d644e..feee78c1 100644 --- a/src/main/java/ee/carlrobert/codegpt/actions/editor/EditorActionsUtil.java +++ b/src/main/java/ee/carlrobert/codegpt/actions/editor/EditorActionsUtil.java @@ -10,12 +10,16 @@ import com.intellij.openapi.editor.Editor; import com.intellij.openapi.editor.impl.EditorImpl; import com.intellij.openapi.extensions.PluginId; import com.intellij.openapi.project.Project; +import ee.carlrobert.codegpt.CodeGPTKeys; import ee.carlrobert.codegpt.conversations.message.Message; import ee.carlrobert.codegpt.settings.configuration.ConfigurationState; import ee.carlrobert.codegpt.toolwindow.chat.standard.StandardChatToolWindowContentManager; import ee.carlrobert.codegpt.util.file.FileUtil; +import ee.carlrobert.embedding.CheckedFile; +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 { @@ -67,6 +71,12 @@ public class EditorActionsUtil { if (toolWindow != null) { toolWindow.show(); } + + message.setReferencedFilePaths( + Stream.ofNullable(project.getUserData(CodeGPTKeys.SELECTED_FILES)) + .flatMap(Collection::stream) + .map(CheckedFile::getFilePath) + .collect(toList())); toolWindowContentManager.sendMessage(message); } }; diff --git a/src/main/java/ee/carlrobert/codegpt/actions/toolwindow/DeleteConversationAction.java b/src/main/java/ee/carlrobert/codegpt/actions/toolwindow/DeleteConversationAction.java index a6bb8610..be822ad6 100644 --- a/src/main/java/ee/carlrobert/codegpt/actions/toolwindow/DeleteConversationAction.java +++ b/src/main/java/ee/carlrobert/codegpt/actions/toolwindow/DeleteConversationAction.java @@ -8,7 +8,7 @@ import ee.carlrobert.codegpt.actions.ActionType; import ee.carlrobert.codegpt.actions.editor.EditorActionsUtil; import ee.carlrobert.codegpt.conversations.ConversationsState; import ee.carlrobert.codegpt.telemetry.TelemetryAction; -import ee.carlrobert.codegpt.util.OverlayUtil; +import ee.carlrobert.codegpt.ui.OverlayUtil; import org.jetbrains.annotations.NotNull; public class DeleteConversationAction extends AnAction { diff --git a/src/main/java/ee/carlrobert/codegpt/completions/llama/LlamaServerAgent.java b/src/main/java/ee/carlrobert/codegpt/completions/llama/LlamaServerAgent.java index 7eee2994..feba3d26 100644 --- a/src/main/java/ee/carlrobert/codegpt/completions/llama/LlamaServerAgent.java +++ b/src/main/java/ee/carlrobert/codegpt/completions/llama/LlamaServerAgent.java @@ -20,7 +20,7 @@ import ee.carlrobert.codegpt.CodeGPTBundle; import ee.carlrobert.codegpt.CodeGPTPlugin; import ee.carlrobert.codegpt.settings.service.ServerProgressPanel; import ee.carlrobert.codegpt.settings.state.LlamaSettingsState; -import ee.carlrobert.codegpt.util.OverlayUtil; +import ee.carlrobert.codegpt.ui.OverlayUtil; import java.nio.charset.StandardCharsets; import java.util.List; import java.util.concurrent.CopyOnWriteArrayList; diff --git a/src/main/java/ee/carlrobert/codegpt/completions/you/auth/YouAuthenticationService.java b/src/main/java/ee/carlrobert/codegpt/completions/you/auth/YouAuthenticationService.java index 0b763260..a7d195ed 100644 --- a/src/main/java/ee/carlrobert/codegpt/completions/you/auth/YouAuthenticationService.java +++ b/src/main/java/ee/carlrobert/codegpt/completions/you/auth/YouAuthenticationService.java @@ -9,7 +9,7 @@ import ee.carlrobert.codegpt.completions.you.YouApiClient; import ee.carlrobert.codegpt.completions.you.YouSubscriptionNotifier; import ee.carlrobert.codegpt.completions.you.YouUserManager; import ee.carlrobert.codegpt.completions.you.auth.response.YouAuthenticationResponse; -import ee.carlrobert.codegpt.util.OverlayUtil; +import ee.carlrobert.codegpt.ui.OverlayUtil; import java.io.IOException; import okhttp3.Call; import okhttp3.Callback; 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 2e81e627..5ff03f71 100644 --- a/src/main/java/ee/carlrobert/codegpt/conversations/message/Message.java +++ b/src/main/java/ee/carlrobert/codegpt/conversations/message/Message.java @@ -10,10 +10,11 @@ import java.util.UUID; public class Message { private final UUID id; - private final String prompt; + private String prompt; private String response; private String userMessage; private List serpResults; + private List referencedFilePaths; public Message(String prompt, String response) { this(prompt); @@ -34,6 +35,10 @@ public class Message { return prompt; } + public void setPrompt(String prompt) { + this.prompt = prompt; + } + public String getResponse() { return response; } @@ -58,6 +63,14 @@ public class Message { this.serpResults = serpResults; } + public List getReferencedFilePaths() { + return referencedFilePaths; + } + + public void setReferencedFilePaths(List referencedFilePaths) { + this.referencedFilePaths = referencedFilePaths; + } + @Override public boolean equals(Object obj) { if (obj == this) { diff --git a/src/main/java/ee/carlrobert/codegpt/indexes/CodebaseIndexingAction.java b/src/main/java/ee/carlrobert/codegpt/indexes/CodebaseIndexingAction.java index 16aa71b7..055de0fe 100644 --- a/src/main/java/ee/carlrobert/codegpt/indexes/CodebaseIndexingAction.java +++ b/src/main/java/ee/carlrobert/codegpt/indexes/CodebaseIndexingAction.java @@ -5,7 +5,7 @@ import static com.intellij.openapi.ui.DialogWrapper.OK_EXIT_CODE; import com.intellij.icons.AllIcons; import com.intellij.openapi.actionSystem.AnAction; import com.intellij.openapi.actionSystem.AnActionEvent; -import ee.carlrobert.codegpt.util.OverlayUtil; +import ee.carlrobert.codegpt.ui.OverlayUtil; import org.jetbrains.annotations.NotNull; public class CodebaseIndexingAction extends AnAction { diff --git a/src/main/java/ee/carlrobert/codegpt/indexes/CodebaseIndexingTask.java b/src/main/java/ee/carlrobert/codegpt/indexes/CodebaseIndexingTask.java index d4faf076..ad550f2c 100644 --- a/src/main/java/ee/carlrobert/codegpt/indexes/CodebaseIndexingTask.java +++ b/src/main/java/ee/carlrobert/codegpt/indexes/CodebaseIndexingTask.java @@ -13,7 +13,7 @@ import com.intellij.openapi.project.Project; import ee.carlrobert.codegpt.CodeGPTBundle; import ee.carlrobert.codegpt.CodeGPTPlugin; import ee.carlrobert.codegpt.completions.CompletionClientProvider; -import ee.carlrobert.codegpt.util.OverlayUtil; +import ee.carlrobert.codegpt.ui.OverlayUtil; import ee.carlrobert.codegpt.util.file.FileUtil; import ee.carlrobert.embedding.CheckedFile; import ee.carlrobert.embedding.EmbeddingsService; diff --git a/src/main/java/ee/carlrobert/codegpt/settings/advanced/AdvancedSettingsComponent.java b/src/main/java/ee/carlrobert/codegpt/settings/advanced/AdvancedSettingsComponent.java index 68f60482..e0eac4fe 100644 --- a/src/main/java/ee/carlrobert/codegpt/settings/advanced/AdvancedSettingsComponent.java +++ b/src/main/java/ee/carlrobert/codegpt/settings/advanced/AdvancedSettingsComponent.java @@ -10,7 +10,7 @@ import com.intellij.util.ui.FormBuilder; import com.intellij.util.ui.JBUI; import com.intellij.util.ui.UI; import ee.carlrobert.codegpt.CodeGPTBundle; -import ee.carlrobert.codegpt.util.UIUtil; +import ee.carlrobert.codegpt.ui.UIUtil; import java.awt.event.ItemEvent; import java.net.Proxy; import javax.swing.BoxLayout; diff --git a/src/main/java/ee/carlrobert/codegpt/settings/configuration/ConfigurationComponent.java b/src/main/java/ee/carlrobert/codegpt/settings/configuration/ConfigurationComponent.java index eb8d144a..ab51ac2d 100644 --- a/src/main/java/ee/carlrobert/codegpt/settings/configuration/ConfigurationComponent.java +++ b/src/main/java/ee/carlrobert/codegpt/settings/configuration/ConfigurationComponent.java @@ -24,7 +24,7 @@ import com.intellij.util.ui.JBUI; import com.intellij.util.ui.UI; import ee.carlrobert.codegpt.CodeGPTBundle; import ee.carlrobert.codegpt.actions.editor.EditorActionsUtil; -import ee.carlrobert.codegpt.util.UIUtil; +import ee.carlrobert.codegpt.ui.UIUtil; import java.awt.Dimension; import java.util.Arrays; import java.util.LinkedHashMap; diff --git a/src/main/java/ee/carlrobert/codegpt/settings/service/LlamaServiceSelectionForm.java b/src/main/java/ee/carlrobert/codegpt/settings/service/LlamaServiceSelectionForm.java index 67e00210..3efafd61 100644 --- a/src/main/java/ee/carlrobert/codegpt/settings/service/LlamaServiceSelectionForm.java +++ b/src/main/java/ee/carlrobert/codegpt/settings/service/LlamaServiceSelectionForm.java @@ -20,7 +20,7 @@ import ee.carlrobert.codegpt.completions.HuggingFaceModel; import ee.carlrobert.codegpt.completions.llama.LlamaServerAgent; import ee.carlrobert.codegpt.completions.llama.LlamaServerStartupParams; import ee.carlrobert.codegpt.settings.state.LlamaSettingsState; -import ee.carlrobert.codegpt.util.OverlayUtil; +import ee.carlrobert.codegpt.ui.OverlayUtil; import java.awt.BorderLayout; import java.io.File; import java.util.Arrays; diff --git a/src/main/java/ee/carlrobert/codegpt/settings/service/ServiceSelectionForm.java b/src/main/java/ee/carlrobert/codegpt/settings/service/ServiceSelectionForm.java index 0b5735ce..ef6abb7a 100644 --- a/src/main/java/ee/carlrobert/codegpt/settings/service/ServiceSelectionForm.java +++ b/src/main/java/ee/carlrobert/codegpt/settings/service/ServiceSelectionForm.java @@ -20,7 +20,7 @@ import ee.carlrobert.codegpt.credentials.OpenAICredentialsManager; import ee.carlrobert.codegpt.settings.state.AzureSettingsState; import ee.carlrobert.codegpt.settings.state.OpenAISettingsState; import ee.carlrobert.codegpt.settings.state.YouSettingsState; -import ee.carlrobert.codegpt.util.UIUtil; +import ee.carlrobert.codegpt.ui.UIUtil; import ee.carlrobert.llm.client.openai.completion.OpenAIChatCompletionModel; import java.util.List; import java.util.Map; diff --git a/src/main/java/ee/carlrobert/codegpt/settings/service/YouServiceSelectionForm.java b/src/main/java/ee/carlrobert/codegpt/settings/service/YouServiceSelectionForm.java index cd67ef11..51d1bd12 100644 --- a/src/main/java/ee/carlrobert/codegpt/settings/service/YouServiceSelectionForm.java +++ b/src/main/java/ee/carlrobert/codegpt/settings/service/YouServiceSelectionForm.java @@ -22,7 +22,7 @@ import ee.carlrobert.codegpt.completions.you.auth.response.YouAuthenticationResp import ee.carlrobert.codegpt.completions.you.auth.response.YouUser; import ee.carlrobert.codegpt.credentials.YouCredentialsManager; import ee.carlrobert.codegpt.settings.state.SettingsState; -import ee.carlrobert.codegpt.util.UIUtil; +import ee.carlrobert.codegpt.ui.UIUtil; import java.awt.BorderLayout; import java.awt.FlowLayout; import java.util.regex.Pattern; diff --git a/src/main/java/ee/carlrobert/codegpt/settings/state/IncludedFilesSettingsState.java b/src/main/java/ee/carlrobert/codegpt/settings/state/IncludedFilesSettingsState.java new file mode 100644 index 00000000..3c5f6f2c --- /dev/null +++ b/src/main/java/ee/carlrobert/codegpt/settings/state/IncludedFilesSettingsState.java @@ -0,0 +1,57 @@ +package ee.carlrobert.codegpt.settings.state; + +import com.intellij.openapi.application.ApplicationManager; +import com.intellij.openapi.components.PersistentStateComponent; +import com.intellij.openapi.components.State; +import com.intellij.openapi.components.Storage; +import com.intellij.util.xmlb.XmlSerializerUtil; +import org.jetbrains.annotations.NotNull; + +@State( + name = "CodeGPT_IncludedFilesSettings", + storages = @Storage("CodeGPT_IncludedFilesSettings.xml")) +public class IncludedFilesSettingsState implements + PersistentStateComponent { + + public static final String DEFAULT_PROMPT_TEMPLATE = + "Use the following context to answer question at the end:\n\n" + + "{REPEATABLE_CONTEXT}\n\n" + + "Question: {QUESTION}"; + public static final String DEFAULT_REPEATABLE_CONTEXT = + "File Path: {FILE_PATH}\n" + + "File Content:\n" + + "{FILE_CONTENT}"; + + private String promptTemplate = DEFAULT_PROMPT_TEMPLATE; + private String repeatableContext = DEFAULT_REPEATABLE_CONTEXT; + + public static IncludedFilesSettingsState getInstance() { + return ApplicationManager.getApplication().getService(IncludedFilesSettingsState.class); + } + + @Override + public IncludedFilesSettingsState getState() { + return this; + } + + @Override + public void loadState(@NotNull IncludedFilesSettingsState state) { + XmlSerializerUtil.copyBean(state, this); + } + + public String getPromptTemplate() { + return promptTemplate; + } + + public void setPromptTemplate(String promptTemplate) { + this.promptTemplate = promptTemplate; + } + + public String getRepeatableContext() { + return repeatableContext; + } + + public void setRepeatableContext(String repeatableContext) { + this.repeatableContext = repeatableContext; + } +} diff --git a/src/main/java/ee/carlrobert/codegpt/toolwindow/chat/BaseChatToolWindowTabPanel.java b/src/main/java/ee/carlrobert/codegpt/toolwindow/chat/BaseChatToolWindowTabPanel.java index 10fd809e..04230a95 100644 --- a/src/main/java/ee/carlrobert/codegpt/toolwindow/chat/BaseChatToolWindowTabPanel.java +++ b/src/main/java/ee/carlrobert/codegpt/toolwindow/chat/BaseChatToolWindowTabPanel.java @@ -1,13 +1,16 @@ package ee.carlrobert.codegpt.toolwindow.chat; -import static ee.carlrobert.codegpt.util.UIUtil.createScrollPaneWithSmartScroller; +import static ee.carlrobert.codegpt.ui.UIUtil.createScrollPaneWithSmartScroller; import static java.lang.String.format; +import static java.util.stream.Collectors.joining; +import static java.util.stream.Collectors.toList; import com.intellij.openapi.diagnostic.Logger; import com.intellij.openapi.editor.impl.EditorImpl; import com.intellij.openapi.project.Project; import com.intellij.ui.JBColor; import com.intellij.util.ui.JBUI; +import ee.carlrobert.codegpt.CodeGPTKeys; import ee.carlrobert.codegpt.EncodingManager; import ee.carlrobert.codegpt.actions.ActionType; import ee.carlrobert.codegpt.completions.CompletionRequestHandler; @@ -16,20 +19,26 @@ import ee.carlrobert.codegpt.conversations.Conversation; import ee.carlrobert.codegpt.conversations.ConversationService; import ee.carlrobert.codegpt.conversations.message.Message; import ee.carlrobert.codegpt.settings.service.ServiceType; +import ee.carlrobert.codegpt.settings.state.IncludedFilesSettingsState; import ee.carlrobert.codegpt.settings.state.SettingsState; import ee.carlrobert.codegpt.telemetry.TelemetryAction; -import ee.carlrobert.codegpt.toolwindow.chat.components.ChatMessageResponseBody; -import ee.carlrobert.codegpt.toolwindow.chat.components.ResponsePanel; -import ee.carlrobert.codegpt.toolwindow.chat.components.TotalTokensPanel; -import ee.carlrobert.codegpt.toolwindow.chat.components.UserMessagePanel; -import ee.carlrobert.codegpt.toolwindow.chat.components.UserPromptTextArea; -import ee.carlrobert.codegpt.toolwindow.chat.components.UserPromptTextAreaHeader; import ee.carlrobert.codegpt.toolwindow.chat.standard.StandardChatToolWindowContentManager; +import ee.carlrobert.codegpt.toolwindow.chat.standard.StandardChatToolWindowPanel; +import ee.carlrobert.codegpt.toolwindow.chat.ui.ChatMessageResponseBody; +import ee.carlrobert.codegpt.toolwindow.chat.ui.ChatToolWindowScrollablePanel; +import ee.carlrobert.codegpt.toolwindow.chat.ui.ResponsePanel; +import ee.carlrobert.codegpt.toolwindow.chat.ui.UserMessagePanel; +import ee.carlrobert.codegpt.toolwindow.chat.ui.textarea.TotalTokensDetails; +import ee.carlrobert.codegpt.toolwindow.chat.ui.textarea.TotalTokensPanel; +import ee.carlrobert.codegpt.toolwindow.chat.ui.textarea.UserPromptTextArea; +import ee.carlrobert.codegpt.toolwindow.chat.ui.textarea.UserPromptTextAreaHeader; import ee.carlrobert.codegpt.util.EditorUtil; import ee.carlrobert.codegpt.util.file.FileUtil; +import ee.carlrobert.embedding.CheckedFile; import java.awt.BorderLayout; import java.awt.GridBagConstraints; import java.awt.GridBagLayout; +import java.util.List; import java.util.UUID; import javax.swing.JComponent; import javax.swing.JPanel; @@ -61,6 +70,7 @@ public abstract class BaseChatToolWindowTabPanel implements ChatToolWindowTabPan conversationService = ConversationService.getInstance(); toolWindowScrollablePanel = new ChatToolWindowScrollablePanel(); totalTokensPanel = new TotalTokensPanel( + project, conversation, EditorUtil.getSelectedEditorSelectedText(project), this); @@ -75,7 +85,7 @@ public abstract class BaseChatToolWindowTabPanel implements ChatToolWindowTabPan } @Override - public JPanel getContent() { + public JComponent getContent() { return rootPanel; } @@ -86,8 +96,20 @@ public abstract class BaseChatToolWindowTabPanel implements ChatToolWindowTabPan @Override public void sendMessage(Message message) { + var referencedFiles = project.getUserData(CodeGPTKeys.SELECTED_FILES); + if (referencedFiles != null && !referencedFiles.isEmpty()) { + var referencedFilePaths = referencedFiles.stream() + .map(CheckedFile::getFilePath) + .collect(toList()); + message.setReferencedFilePaths(referencedFilePaths); + message.setUserMessage(message.getPrompt()); + message.setPrompt(getPromptWithContext(referencedFiles, message.getPrompt())); + totalTokensPanel.updateReferencedFilesTokens(referencedFiles); + } + + var userMessagePanel = new UserMessagePanel(project, message, this); var messagePanel = toolWindowScrollablePanel.addMessage(message.getId()); - messagePanel.add(new UserMessagePanel(project, message, this)); + messagePanel.add(userMessagePanel); var responsePanel = new ResponsePanel() .withReloadAction(() -> reloadMessage(message, conversation)) .withDeleteAction(() -> removeMessage(message.getId(), conversation)) @@ -99,10 +121,29 @@ public abstract class BaseChatToolWindowTabPanel implements ChatToolWindowTabPan totalTokensPanel.updateConversationTokens(conversationTokens + userPromptTokens); call(message, responsePanel, false); + project.getService(StandardChatToolWindowContentManager.class) + .tryFindChatToolWindowPanel() + .ifPresent(StandardChatToolWindowPanel::clearSelectedFilesNotification); + } + + private String getPromptWithContext(List referencedFiles, String userPrompt) { + var includedFilesSettings = IncludedFilesSettingsState.getInstance(); + var repeatableContext = referencedFiles.stream() + .map(item -> includedFilesSettings.getRepeatableContext() + .replace("{FILE_PATH}", item.getFilePath()) + .replace("{FILE_CONTENT}", format( + "```%s\n%s\n```", + item.getFileExtension(), + item.getFileContent().trim()))) + .collect(joining("\n\n")); + + return includedFilesSettings.getPromptTemplate() + .replace("{REPEATABLE_CONTEXT}", repeatableContext) + .replace("{QUESTION}", userPrompt); } @Override - public TokenDetails getTokenDetails() { + public TotalTokensDetails getTokenDetails() { return totalTokensPanel.getTokenDetails(); } @@ -193,11 +234,10 @@ public abstract class BaseChatToolWindowTabPanel implements ChatToolWindowTabPan var fileExtension = FileUtil.getFileExtension( ((EditorImpl) editor).getVirtualFile().getName()); message = new Message(text + format("\n```%s\n%s\n```", fileExtension, selectedText)); - message.setUserMessage(text); selectionModel.removeSelection(); } } - + message.setUserMessage(text); sendMessage(message); } 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 d5c4d1b7..10b9271a 100644 --- a/src/main/java/ee/carlrobert/codegpt/toolwindow/chat/ChatToolWindowTabPanel.java +++ b/src/main/java/ee/carlrobert/codegpt/toolwindow/chat/ChatToolWindowTabPanel.java @@ -3,15 +3,17 @@ package ee.carlrobert.codegpt.toolwindow.chat; import com.intellij.openapi.Disposable; import ee.carlrobert.codegpt.conversations.Conversation; import ee.carlrobert.codegpt.conversations.message.Message; +import ee.carlrobert.codegpt.toolwindow.chat.ui.textarea.TotalTokensDetails; +import javax.swing.JComponent; import javax.swing.JPanel; public interface ChatToolWindowTabPanel extends Disposable { - JPanel getContent(); + JComponent getContent(); Conversation getConversation(); - TokenDetails getTokenDetails(); + TotalTokensDetails getTokenDetails(); void displayLandingView(); diff --git a/src/main/java/ee/carlrobert/codegpt/toolwindow/chat/StreamParseResponse.java b/src/main/java/ee/carlrobert/codegpt/toolwindow/chat/StreamParseResponse.java deleted file mode 100644 index f98f45c3..00000000 --- a/src/main/java/ee/carlrobert/codegpt/toolwindow/chat/StreamParseResponse.java +++ /dev/null @@ -1,20 +0,0 @@ -package ee.carlrobert.codegpt.toolwindow.chat; - -public class StreamParseResponse { - - private final StreamResponseType type; - private final String response; - - public StreamParseResponse(StreamResponseType type, String response) { - this.type = type; - this.response = response; - } - - public StreamResponseType getType() { - return type; - } - - public String getResponse() { - return response; - } -} diff --git a/src/main/java/ee/carlrobert/codegpt/toolwindow/chat/StreamParser.java b/src/main/java/ee/carlrobert/codegpt/toolwindow/chat/StreamParser.java index d59caace..2edc857c 100644 --- a/src/main/java/ee/carlrobert/codegpt/toolwindow/chat/StreamParser.java +++ b/src/main/java/ee/carlrobert/codegpt/toolwindow/chat/StreamParser.java @@ -51,4 +51,23 @@ public class StreamParser { messageBuilder.setLength(0); isProcessingCode = false; } + + public static class StreamParseResponse { + + private final StreamResponseType type; + private final String response; + + public StreamParseResponse(StreamResponseType type, String response) { + this.type = type; + this.response = response; + } + + public StreamResponseType getType() { + return type; + } + + public String getResponse() { + return response; + } + } } diff --git a/src/main/java/ee/carlrobert/codegpt/toolwindow/chat/ToolWindowCompletionResponseEventListener.java b/src/main/java/ee/carlrobert/codegpt/toolwindow/chat/ToolWindowCompletionResponseEventListener.java index e51b700d..5d8dca24 100644 --- a/src/main/java/ee/carlrobert/codegpt/toolwindow/chat/ToolWindowCompletionResponseEventListener.java +++ b/src/main/java/ee/carlrobert/codegpt/toolwindow/chat/ToolWindowCompletionResponseEventListener.java @@ -14,11 +14,11 @@ import ee.carlrobert.codegpt.settings.state.OpenAISettingsState; import ee.carlrobert.codegpt.settings.state.SettingsState; import ee.carlrobert.codegpt.settings.state.YouSettingsState; import ee.carlrobert.codegpt.telemetry.TelemetryAction; -import ee.carlrobert.codegpt.toolwindow.chat.components.ChatMessageResponseBody; -import ee.carlrobert.codegpt.toolwindow.chat.components.ResponsePanel; -import ee.carlrobert.codegpt.toolwindow.chat.components.TotalTokensPanel; -import ee.carlrobert.codegpt.toolwindow.chat.components.UserPromptTextArea; -import ee.carlrobert.codegpt.util.OverlayUtil; +import ee.carlrobert.codegpt.toolwindow.chat.ui.ChatMessageResponseBody; +import ee.carlrobert.codegpt.toolwindow.chat.ui.ResponsePanel; +import ee.carlrobert.codegpt.toolwindow.chat.ui.textarea.TotalTokensPanel; +import ee.carlrobert.codegpt.toolwindow.chat.ui.textarea.UserPromptTextArea; +import ee.carlrobert.codegpt.ui.OverlayUtil; import ee.carlrobert.llm.client.openai.completion.ErrorDetails; import ee.carlrobert.llm.client.you.completion.YouSerpResult; import java.util.HashMap; diff --git a/src/main/java/ee/carlrobert/codegpt/toolwindow/chat/contextual/ContextualChatToolWindowLandingPanel.java b/src/main/java/ee/carlrobert/codegpt/toolwindow/chat/contextual/ContextualChatToolWindowLandingPanel.java index b2bdaded..81490fcc 100644 --- a/src/main/java/ee/carlrobert/codegpt/toolwindow/chat/contextual/ContextualChatToolWindowLandingPanel.java +++ b/src/main/java/ee/carlrobert/codegpt/toolwindow/chat/contextual/ContextualChatToolWindowLandingPanel.java @@ -11,9 +11,9 @@ import ee.carlrobert.codegpt.indexes.CodebaseIndexingCompletedNotifier; import ee.carlrobert.codegpt.indexes.CodebaseIndexingTask; import ee.carlrobert.codegpt.indexes.FolderStructureTreePanel; import ee.carlrobert.codegpt.settings.SettingsConfigurable; -import ee.carlrobert.codegpt.toolwindow.chat.components.ResponsePanel; -import ee.carlrobert.codegpt.util.OverlayUtil; -import ee.carlrobert.codegpt.util.UIUtil; +import ee.carlrobert.codegpt.toolwindow.chat.ui.ResponsePanel; +import ee.carlrobert.codegpt.ui.OverlayUtil; +import ee.carlrobert.codegpt.ui.UIUtil; import ee.carlrobert.vector.VectorStore; import javax.swing.JTextPane; import javax.swing.event.HyperlinkEvent; diff --git a/src/main/java/ee/carlrobert/codegpt/toolwindow/chat/editor/ResponseEditorPanel.java b/src/main/java/ee/carlrobert/codegpt/toolwindow/chat/editor/ResponseEditorPanel.java index 1bfd4dc8..4856ad35 100644 --- a/src/main/java/ee/carlrobert/codegpt/toolwindow/chat/editor/ResponseEditorPanel.java +++ b/src/main/java/ee/carlrobert/codegpt/toolwindow/chat/editor/ResponseEditorPanel.java @@ -27,12 +27,12 @@ import com.intellij.ui.components.ActionLink; import com.intellij.util.ui.JBUI; import ee.carlrobert.codegpt.CodeGPTBundle; import ee.carlrobert.codegpt.actions.toolwindow.ReplaceCodeInMainEditorAction; -import ee.carlrobert.codegpt.toolwindow.chat.components.IconActionButton; import ee.carlrobert.codegpt.toolwindow.chat.editor.actions.CopyAction; import ee.carlrobert.codegpt.toolwindow.chat.editor.actions.DiffAction; import ee.carlrobert.codegpt.toolwindow.chat.editor.actions.EditAction; import ee.carlrobert.codegpt.toolwindow.chat.editor.actions.NewFileAction; import ee.carlrobert.codegpt.toolwindow.chat.editor.actions.ReplaceSelectionAction; +import ee.carlrobert.codegpt.ui.IconActionButton; import ee.carlrobert.codegpt.util.EditorUtil; import java.awt.BorderLayout; import java.awt.FlowLayout; diff --git a/src/main/java/ee/carlrobert/codegpt/toolwindow/chat/editor/actions/CopyAction.java b/src/main/java/ee/carlrobert/codegpt/toolwindow/chat/editor/actions/CopyAction.java index 25334e6e..3eb6bebf 100644 --- a/src/main/java/ee/carlrobert/codegpt/toolwindow/chat/editor/actions/CopyAction.java +++ b/src/main/java/ee/carlrobert/codegpt/toolwindow/chat/editor/actions/CopyAction.java @@ -6,7 +6,7 @@ import com.intellij.openapi.editor.Editor; import ee.carlrobert.codegpt.CodeGPTBundle; import ee.carlrobert.codegpt.actions.ActionType; import ee.carlrobert.codegpt.actions.TrackableAction; -import ee.carlrobert.codegpt.util.OverlayUtil; +import ee.carlrobert.codegpt.ui.OverlayUtil; import java.awt.Toolkit; import java.awt.datatransfer.Clipboard; import java.awt.datatransfer.StringSelection; diff --git a/src/main/java/ee/carlrobert/codegpt/toolwindow/chat/editor/actions/DiffAction.java b/src/main/java/ee/carlrobert/codegpt/toolwindow/chat/editor/actions/DiffAction.java index 502eaeb7..c7186b5a 100644 --- a/src/main/java/ee/carlrobert/codegpt/toolwindow/chat/editor/actions/DiffAction.java +++ b/src/main/java/ee/carlrobert/codegpt/toolwindow/chat/editor/actions/DiffAction.java @@ -14,8 +14,8 @@ import com.intellij.openapi.editor.ex.EditorEx; import com.intellij.openapi.fileEditor.FileEditorManager; import com.intellij.openapi.util.Pair; import ee.carlrobert.codegpt.CodeGPTBundle; +import ee.carlrobert.codegpt.ui.OverlayUtil; import ee.carlrobert.codegpt.util.EditorUtil; -import ee.carlrobert.codegpt.util.OverlayUtil; import ee.carlrobert.codegpt.util.file.FileUtil; import java.awt.Point; import java.awt.event.ActionEvent; diff --git a/src/main/java/ee/carlrobert/codegpt/toolwindow/chat/editor/actions/ReplaceSelectionAction.java b/src/main/java/ee/carlrobert/codegpt/toolwindow/chat/editor/actions/ReplaceSelectionAction.java index cefd5c1c..ae8ccf00 100644 --- a/src/main/java/ee/carlrobert/codegpt/toolwindow/chat/editor/actions/ReplaceSelectionAction.java +++ b/src/main/java/ee/carlrobert/codegpt/toolwindow/chat/editor/actions/ReplaceSelectionAction.java @@ -8,8 +8,8 @@ import com.intellij.openapi.editor.Editor; import ee.carlrobert.codegpt.CodeGPTBundle; import ee.carlrobert.codegpt.actions.ActionType; import ee.carlrobert.codegpt.actions.TrackableAction; +import ee.carlrobert.codegpt.ui.OverlayUtil; import ee.carlrobert.codegpt.util.EditorUtil; -import ee.carlrobert.codegpt.util.OverlayUtil; import org.jetbrains.annotations.NotNull; public class ReplaceSelectionAction extends TrackableAction { diff --git a/src/main/java/ee/carlrobert/codegpt/toolwindow/chat/standard/StandardChatToolWindowContentManager.java b/src/main/java/ee/carlrobert/codegpt/toolwindow/chat/standard/StandardChatToolWindowContentManager.java index 1b88cecb..54ea29fc 100644 --- a/src/main/java/ee/carlrobert/codegpt/toolwindow/chat/standard/StandardChatToolWindowContentManager.java +++ b/src/main/java/ee/carlrobert/codegpt/toolwindow/chat/standard/StandardChatToolWindowContentManager.java @@ -4,9 +4,13 @@ import static java.util.Objects.requireNonNull; import com.intellij.openapi.components.Service; import com.intellij.openapi.project.Project; +import com.intellij.openapi.ui.ComponentContainer; +import com.intellij.openapi.wm.RegisterToolWindowTask; import com.intellij.openapi.wm.ToolWindow; +import com.intellij.openapi.wm.ToolWindowAnchor; import com.intellij.openapi.wm.ToolWindowManager; import com.intellij.ui.content.Content; +import ee.carlrobert.codegpt.Icons; import ee.carlrobert.codegpt.conversations.Conversation; import ee.carlrobert.codegpt.conversations.ConversationService; import ee.carlrobert.codegpt.conversations.ConversationsState; @@ -26,10 +30,7 @@ public final class StandardChatToolWindowContentManager { } public void sendMessage(Message message) { - var toolWindow = getToolWindow(); - if (toolWindow != null) { - toolWindow.show(); - } + getToolWindow().show(); if (ConfigurationState.getInstance().isCreateNewChatOnEachAction() || ConversationsState.getCurrentConversation() == null) { @@ -80,16 +81,19 @@ public final class StandardChatToolWindowContentManager { public Optional tryFindChatTabbedPane() { var chatTabContent = tryFindFirstChatTabContent(); if (chatTabContent.isPresent()) { - var tabbedPane = Arrays.stream(chatTabContent.get().getComponent().getComponents()) - .filter(component -> component instanceof StandardChatToolWindowTabbedPane) - .findFirst(); - if (tabbedPane.isPresent()) { - return Optional.of((StandardChatToolWindowTabbedPane) tabbedPane.get()); - } + var chatToolWindowPanel = (StandardChatToolWindowPanel) chatTabContent.get().getComponent(); + return Optional.of(chatToolWindowPanel.getChatTabbedPane()); } return Optional.empty(); } + public Optional tryFindChatToolWindowPanel() { + return tryFindFirstChatTabContent() + .map(ComponentContainer::getComponent) + .filter(component -> component instanceof StandardChatToolWindowPanel) + .map(component -> (StandardChatToolWindowPanel) component); + } + public void resetAll() { tryFindChatTabbedPane().ifPresent(tabbedPane -> { tabbedPane.clearAll(); @@ -99,8 +103,19 @@ public final class StandardChatToolWindowContentManager { }); } - public ToolWindow getToolWindow() { - return requireNonNull(ToolWindowManager.getInstance(project).getToolWindow("CodeGPT")); + public @NotNull ToolWindow getToolWindow() { + var toolWindowManager = ToolWindowManager.getInstance(project); + var toolWindow = toolWindowManager.getToolWindow("CodeGPT"); + if (toolWindow == null) { + // https://intellij-support.jetbrains.com/hc/en-us/community/posts/11533368171026/comments/11538403084562 + return toolWindowManager + .registerToolWindow(RegisterToolWindowTask.closable( + "CodeGPT", + () -> "CodeGPT", + Icons.DefaultSmall, + ToolWindowAnchor.RIGHT)); + } + return toolWindow; } private Optional tryFindFirstChatTabContent() { diff --git a/src/main/java/ee/carlrobert/codegpt/toolwindow/chat/standard/StandardChatToolWindowLandingPanel.java b/src/main/java/ee/carlrobert/codegpt/toolwindow/chat/standard/StandardChatToolWindowLandingPanel.java index 17ec256d..79913f7b 100644 --- a/src/main/java/ee/carlrobert/codegpt/toolwindow/chat/standard/StandardChatToolWindowLandingPanel.java +++ b/src/main/java/ee/carlrobert/codegpt/toolwindow/chat/standard/StandardChatToolWindowLandingPanel.java @@ -6,8 +6,8 @@ import com.intellij.ui.components.ActionLink; import com.intellij.util.ui.JBUI; import ee.carlrobert.codegpt.Icons; import ee.carlrobert.codegpt.settings.state.SettingsState; -import ee.carlrobert.codegpt.toolwindow.chat.components.ResponsePanel; -import ee.carlrobert.codegpt.util.UIUtil; +import ee.carlrobert.codegpt.toolwindow.chat.ui.ResponsePanel; +import ee.carlrobert.codegpt.ui.UIUtil; import java.awt.BorderLayout; import javax.swing.Box; import javax.swing.BoxLayout; diff --git a/src/main/java/ee/carlrobert/codegpt/toolwindow/chat/standard/StandardChatToolWindowPanel.java b/src/main/java/ee/carlrobert/codegpt/toolwindow/chat/standard/StandardChatToolWindowPanel.java index 64b94d63..0c1e1f1a 100644 --- a/src/main/java/ee/carlrobert/codegpt/toolwindow/chat/standard/StandardChatToolWindowPanel.java +++ b/src/main/java/ee/carlrobert/codegpt/toolwindow/chat/standard/StandardChatToolWindowPanel.java @@ -7,32 +7,57 @@ import com.intellij.openapi.actionSystem.DefaultCompactActionGroup; import com.intellij.openapi.project.Project; import com.intellij.openapi.ui.SimpleToolWindowPanel; import com.intellij.openapi.util.Disposer; +import com.intellij.util.ui.JBUI; +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; import ee.carlrobert.codegpt.conversations.ConversationService; import ee.carlrobert.codegpt.conversations.ConversationsState; +import ee.carlrobert.codegpt.toolwindow.chat.ui.SelectedFilesNotification; +import ee.carlrobert.embedding.CheckedFile; import java.awt.BorderLayout; +import java.util.List; import javax.swing.JPanel; import org.jetbrains.annotations.NotNull; public class StandardChatToolWindowPanel extends SimpleToolWindowPanel { + private final SelectedFilesNotification selectedFilesNotification; + private StandardChatToolWindowTabbedPane tabbedPane; + public StandardChatToolWindowPanel( @NotNull Project project, @NotNull Disposable parentDisposable) { super(true); - init(project, parentDisposable); + selectedFilesNotification = new SelectedFilesNotification(project); + init(project, selectedFilesNotification, parentDisposable); + + project.getMessageBus() + .connect() + .subscribe(IncludeFilesInContextNotifier.FILES_INCLUDED_IN_CONTEXT_TOPIC, + (IncludeFilesInContextNotifier) this::displaySelectedFilesNotification); } - private void init(Project project, Disposable parentDisposable) { + public void displaySelectedFilesNotification(List checkedFiles) { + selectedFilesNotification.displaySelectedFilesNotification(checkedFiles); + } + + public void clearSelectedFilesNotification() { + selectedFilesNotification.clearSelectedFilesNotification(); + } + + private void init( + Project project, + SelectedFilesNotification selectedFilesNotification, + Disposable parentDisposable) { var conversation = ConversationsState.getCurrentConversation(); if (conversation == null) { conversation = ConversationService.getInstance().startConversation(); } var tabPanel = new StandardChatToolWindowTabPanel(project, conversation); - var tabbedPane = createTabbedPane(tabPanel, parentDisposable); + tabbedPane = createTabbedPane(tabPanel, parentDisposable); Runnable onAddNewTab = () -> { tabbedPane.addNewTab(new StandardChatToolWindowTabPanel( project, @@ -46,7 +71,8 @@ public class StandardChatToolWindowPanel extends SimpleToolWindowPanel { BorderLayout.LINE_START); setToolbar(actionToolbarPanel); - setContent(tabbedPane); + setContent( + JBUI.Panels.simplePanel(tabbedPane).addToBottom(selectedFilesNotification)); Disposer.register(parentDisposable, tabPanel); } @@ -75,4 +101,8 @@ public class StandardChatToolWindowPanel extends SimpleToolWindowPanel { tabbedPane.addNewTab(tabPanel); return tabbedPane; } + + public StandardChatToolWindowTabbedPane getChatTabbedPane() { + return tabbedPane; + } } diff --git a/src/main/java/ee/carlrobert/codegpt/toolwindow/chat/standard/StandardChatToolWindowTabPanel.java b/src/main/java/ee/carlrobert/codegpt/toolwindow/chat/standard/StandardChatToolWindowTabPanel.java index 1c3633fc..aa81e2f7 100644 --- a/src/main/java/ee/carlrobert/codegpt/toolwindow/chat/standard/StandardChatToolWindowTabPanel.java +++ b/src/main/java/ee/carlrobert/codegpt/toolwindow/chat/standard/StandardChatToolWindowTabPanel.java @@ -8,11 +8,11 @@ import ee.carlrobert.codegpt.conversations.Conversation; import ee.carlrobert.codegpt.conversations.message.Message; import ee.carlrobert.codegpt.settings.state.YouSettingsState; import ee.carlrobert.codegpt.toolwindow.chat.BaseChatToolWindowTabPanel; -import ee.carlrobert.codegpt.toolwindow.chat.components.ChatMessageResponseBody; -import ee.carlrobert.codegpt.toolwindow.chat.components.ResponsePanel; -import ee.carlrobert.codegpt.toolwindow.chat.components.UserMessagePanel; +import ee.carlrobert.codegpt.toolwindow.chat.ui.ChatMessageResponseBody; +import ee.carlrobert.codegpt.toolwindow.chat.ui.ResponsePanel; +import ee.carlrobert.codegpt.toolwindow.chat.ui.UserMessagePanel; +import ee.carlrobert.codegpt.ui.OverlayUtil; import ee.carlrobert.codegpt.util.EditorUtil; -import ee.carlrobert.codegpt.util.OverlayUtil; import ee.carlrobert.codegpt.util.file.FileUtil; import javax.swing.JComponent; import org.jetbrains.annotations.NotNull; diff --git a/src/main/java/ee/carlrobert/codegpt/toolwindow/chat/standard/StandardChatToolWindowTabbedPane.java b/src/main/java/ee/carlrobert/codegpt/toolwindow/chat/standard/StandardChatToolWindowTabbedPane.java index 775d1612..9873333b 100644 --- a/src/main/java/ee/carlrobert/codegpt/toolwindow/chat/standard/StandardChatToolWindowTabbedPane.java +++ b/src/main/java/ee/carlrobert/codegpt/toolwindow/chat/standard/StandardChatToolWindowTabbedPane.java @@ -11,10 +11,12 @@ import com.intellij.util.ui.JBUI; import ee.carlrobert.codegpt.conversations.ConversationService; import ee.carlrobert.codegpt.conversations.ConversationsState; import ee.carlrobert.codegpt.settings.state.SettingsState; +import ee.carlrobert.embedding.CheckedFile; import java.awt.Component; import java.awt.Dimension; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; +import java.util.List; import java.util.Map; import java.util.Optional; import java.util.TreeMap; diff --git a/src/main/java/ee/carlrobert/codegpt/toolwindow/chat/components/ChatMessageResponseBody.java b/src/main/java/ee/carlrobert/codegpt/toolwindow/chat/ui/ChatMessageResponseBody.java similarity index 98% rename from src/main/java/ee/carlrobert/codegpt/toolwindow/chat/components/ChatMessageResponseBody.java rename to src/main/java/ee/carlrobert/codegpt/toolwindow/chat/ui/ChatMessageResponseBody.java index 2664dd96..99b3a76d 100644 --- a/src/main/java/ee/carlrobert/codegpt/toolwindow/chat/components/ChatMessageResponseBody.java +++ b/src/main/java/ee/carlrobert/codegpt/toolwindow/chat/ui/ChatMessageResponseBody.java @@ -1,4 +1,4 @@ -package ee.carlrobert.codegpt.toolwindow.chat.components; +package ee.carlrobert.codegpt.toolwindow.chat.ui; import static ee.carlrobert.codegpt.toolwindow.chat.StreamResponseType.CODE; import static ee.carlrobert.codegpt.util.MarkdownUtil.convertMdToHtml; @@ -20,9 +20,9 @@ import ee.carlrobert.codegpt.settings.SettingsConfigurable; import ee.carlrobert.codegpt.telemetry.TelemetryAction; import ee.carlrobert.codegpt.toolwindow.chat.StreamParser; import ee.carlrobert.codegpt.toolwindow.chat.editor.ResponseEditorPanel; +import ee.carlrobert.codegpt.ui.UIUtil; import ee.carlrobert.codegpt.util.EditorUtil; import ee.carlrobert.codegpt.util.MarkdownUtil; -import ee.carlrobert.codegpt.util.UIUtil; import ee.carlrobert.llm.client.you.completion.YouSerpResult; import java.awt.BorderLayout; import java.util.List; diff --git a/src/main/java/ee/carlrobert/codegpt/toolwindow/chat/ChatToolWindowScrollablePanel.java b/src/main/java/ee/carlrobert/codegpt/toolwindow/chat/ui/ChatToolWindowScrollablePanel.java similarity index 90% rename from src/main/java/ee/carlrobert/codegpt/toolwindow/chat/ChatToolWindowScrollablePanel.java rename to src/main/java/ee/carlrobert/codegpt/toolwindow/chat/ui/ChatToolWindowScrollablePanel.java index 40860b29..3c8d9158 100644 --- a/src/main/java/ee/carlrobert/codegpt/toolwindow/chat/ChatToolWindowScrollablePanel.java +++ b/src/main/java/ee/carlrobert/codegpt/toolwindow/chat/ui/ChatToolWindowScrollablePanel.java @@ -1,8 +1,7 @@ -package ee.carlrobert.codegpt.toolwindow.chat; +package ee.carlrobert.codegpt.toolwindow.chat.ui; import com.intellij.openapi.roots.ui.componentsList.components.ScrollablePanel; import com.intellij.openapi.roots.ui.componentsList.layout.VerticalStackLayout; -import ee.carlrobert.codegpt.toolwindow.chat.components.ResponsePanel; import java.util.Arrays; import java.util.HashMap; import java.util.Map; @@ -15,7 +14,7 @@ public class ChatToolWindowScrollablePanel extends ScrollablePanel { private final Map visibleMessagePanels = new HashMap<>(); - ChatToolWindowScrollablePanel() { + public ChatToolWindowScrollablePanel() { super(new VerticalStackLayout()); } diff --git a/src/main/java/ee/carlrobert/codegpt/toolwindow/chat/components/ResponsePanel.java b/src/main/java/ee/carlrobert/codegpt/toolwindow/chat/ui/ResponsePanel.java similarity index 97% rename from src/main/java/ee/carlrobert/codegpt/toolwindow/chat/components/ResponsePanel.java rename to src/main/java/ee/carlrobert/codegpt/toolwindow/chat/ui/ResponsePanel.java index 897acd16..9c813fd9 100644 --- a/src/main/java/ee/carlrobert/codegpt/toolwindow/chat/components/ResponsePanel.java +++ b/src/main/java/ee/carlrobert/codegpt/toolwindow/chat/ui/ResponsePanel.java @@ -1,4 +1,4 @@ -package ee.carlrobert.codegpt.toolwindow.chat.components; +package ee.carlrobert.codegpt.toolwindow.chat.ui; import com.intellij.icons.AllIcons.Actions; import com.intellij.openapi.actionSystem.AnAction; @@ -8,6 +8,7 @@ import com.intellij.util.ui.JBFont; import com.intellij.util.ui.JBUI; import ee.carlrobert.codegpt.CodeGPTBundle; import ee.carlrobert.codegpt.Icons; +import ee.carlrobert.codegpt.ui.IconActionButton; import java.awt.BorderLayout; import java.awt.FlowLayout; import javax.swing.Box; 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 new file mode 100644 index 00000000..841e4059 --- /dev/null +++ b/src/main/java/ee/carlrobert/codegpt/toolwindow/chat/ui/SelectedFilesAccordion.java @@ -0,0 +1,79 @@ +package ee.carlrobert.codegpt.toolwindow.chat.ui; + +import static java.lang.String.format; +import static javax.swing.event.HyperlinkEvent.EventType.ACTIVATED; + +import com.intellij.icons.AllIcons.General; +import com.intellij.openapi.fileEditor.FileEditorManager; +import com.intellij.openapi.project.Project; +import com.intellij.openapi.util.io.FileUtil; +import com.intellij.openapi.vfs.LocalFileSystem; +import com.intellij.openapi.vfs.VirtualFile; +import com.intellij.util.ui.JBUI; +import ee.carlrobert.codegpt.ui.UIUtil; +import java.awt.BorderLayout; +import java.awt.event.ItemEvent; +import java.nio.file.Paths; +import java.util.List; +import java.util.Objects; +import java.util.stream.Collectors; +import javax.swing.BorderFactory; +import javax.swing.JPanel; +import javax.swing.JTextPane; +import javax.swing.JToggleButton; +import javax.swing.SwingConstants; +import org.jetbrains.annotations.NotNull; + +public class SelectedFilesAccordion extends JPanel { + + public SelectedFilesAccordion( + @NotNull Project project, + @NotNull List referencedFilePaths) { + super(new BorderLayout()); + setOpaque(false); + + var contentPanel = createContentPane(project, referencedFilePaths); + add(createToggleButton(contentPanel, referencedFilePaths.size()), BorderLayout.NORTH); + add(contentPanel, BorderLayout.CENTER); + } + + private JTextPane createContentPane(Project project, List referencedFilePaths) { + var textPane = UIUtil.createTextPane( + getHtml(referencedFilePaths), + false, + event -> { + if (FileUtil.exists(event.getDescription()) && ACTIVATED.equals(event.getEventType())) { + VirtualFile file = LocalFileSystem.getInstance().findFileByPath(event.getDescription()); + FileEditorManager.getInstance(project).openFile(Objects.requireNonNull(file), true); + } + }); + textPane.setBorder(JBUI.Borders.emptyBottom(8)); + textPane.setVisible(false); + return textPane; + } + + private String getHtml(List referencedFilePaths) { + var html = referencedFilePaths.stream() + .map(filePath -> format( + "
  • %s
  • ", + filePath, + Paths.get(filePath).getFileName().toString())) + .collect(Collectors.joining()); + return format("
      %s
    ", html); + } + + private JToggleButton createToggleButton(JTextPane contentPane, int fileCount) { + var accordionToggle = new JToggleButton( + format("Referenced files (+%d)", fileCount), General.ArrowDown); + accordionToggle.setFocusPainted(false); + accordionToggle.setContentAreaFilled(false); + accordionToggle.setBackground(getBackground()); + accordionToggle.setSelectedIcon(General.ArrowUp); + accordionToggle.setBorder(null); + accordionToggle.setHorizontalAlignment(SwingConstants.LEADING); + accordionToggle.setHorizontalTextPosition(SwingConstants.LEADING); + accordionToggle.addItemListener(e -> + contentPane.setVisible(e.getStateChange() == ItemEvent.SELECTED)); + return accordionToggle; + } +} diff --git a/src/main/java/ee/carlrobert/codegpt/toolwindow/chat/ui/SelectedFilesNotification.java b/src/main/java/ee/carlrobert/codegpt/toolwindow/chat/ui/SelectedFilesNotification.java new file mode 100644 index 00000000..77826acc --- /dev/null +++ b/src/main/java/ee/carlrobert/codegpt/toolwindow/chat/ui/SelectedFilesNotification.java @@ -0,0 +1,86 @@ +package ee.carlrobert.codegpt.toolwindow.chat.ui; + +import static java.lang.String.format; +import static java.util.Collections.emptyList; + +import com.intellij.icons.AllIcons.General; +import com.intellij.openapi.project.Project; +import com.intellij.ui.JBColor; +import com.intellij.ui.components.ActionLink; +import com.intellij.ui.components.JBLabel; +import com.intellij.util.ui.JBUI; +import com.intellij.util.ui.JBUI.CurrentTheme.NotificationInfo; +import ee.carlrobert.codegpt.CodeGPTKeys; +import ee.carlrobert.codegpt.actions.IncludeFilesInContextNotifier; +import ee.carlrobert.embedding.CheckedFile; +import java.awt.BorderLayout; +import java.nio.file.Paths; +import java.util.List; +import java.util.stream.Collectors; +import javax.swing.JPanel; +import javax.swing.SwingConstants; +import org.jetbrains.annotations.NotNull; + +public class SelectedFilesNotification extends JPanel { + + private final Project project; + private final JBLabel label; + + public SelectedFilesNotification(@NotNull Project project) { + super(new BorderLayout()); + this.project = project; + this.label = new JBLabel( + getSelectedFilesLabel(), + General.BalloonInformation, + SwingConstants.LEADING); + + setVisible(false); + setBorder(JBUI.Borders.compound( + JBUI.Borders.customLine(JBColor.border(), 1, 0, 0, 0), + JBUI.Borders.empty(8, 12))); + + setBackground(NotificationInfo.backgroundColor()); + setForeground(NotificationInfo.foregroundColor()); + add(label, BorderLayout.LINE_START); + add(new ActionLink("Remove", (event) -> { + clearSelectedFilesNotification(); + }), BorderLayout.LINE_END); + } + + public void displaySelectedFilesNotification(@NotNull List checkedFiles) { + if (checkedFiles.isEmpty()) { + return; + } + + label.setText(checkedFiles.size() + " files selected"); + var referencedFilePaths = checkedFiles.stream() + .map(CheckedFile::getFilePath) + .collect(Collectors.toList()); + label.setToolTipText(getHtml(referencedFilePaths)); + setVisible(true); + } + + public void clearSelectedFilesNotification() { + project.putUserData(CodeGPTKeys.SELECTED_FILES, emptyList()); + project.getMessageBus() + .syncPublisher(IncludeFilesInContextNotifier.FILES_INCLUDED_IN_CONTEXT_TOPIC) + .filesIncluded(emptyList()); + + label.setText("0 files selected"); + label.setToolTipText(null); + setVisible(false); + } + + private String getHtml(List referencedFilePaths) { + var html = referencedFilePaths.stream() + .map(filePath -> format("
  • %s
  • ", Paths.get(filePath).getFileName().toString())) + .collect(Collectors.joining()); + return format("
      %s
    ", html); + } + + private String getSelectedFilesLabel() { + var selectedFiles = project.getUserData(CodeGPTKeys.SELECTED_FILES); + var fileCount = selectedFiles == null ? 0 : selectedFiles.size(); + return fileCount + " files selected"; + } +} diff --git a/src/main/java/ee/carlrobert/codegpt/toolwindow/chat/components/SmartScroller.java b/src/main/java/ee/carlrobert/codegpt/toolwindow/chat/ui/SmartScroller.java similarity index 98% rename from src/main/java/ee/carlrobert/codegpt/toolwindow/chat/components/SmartScroller.java rename to src/main/java/ee/carlrobert/codegpt/toolwindow/chat/ui/SmartScroller.java index c9ed224c..8db5badb 100644 --- a/src/main/java/ee/carlrobert/codegpt/toolwindow/chat/components/SmartScroller.java +++ b/src/main/java/ee/carlrobert/codegpt/toolwindow/chat/ui/SmartScroller.java @@ -1,4 +1,4 @@ -package ee.carlrobert.codegpt.toolwindow.chat.components; +package ee.carlrobert.codegpt.toolwindow.chat.ui; import java.awt.Component; import java.awt.event.AdjustmentEvent; diff --git a/src/main/java/ee/carlrobert/codegpt/toolwindow/chat/components/UserMessagePanel.java b/src/main/java/ee/carlrobert/codegpt/toolwindow/chat/ui/UserMessagePanel.java similarity index 63% rename from src/main/java/ee/carlrobert/codegpt/toolwindow/chat/components/UserMessagePanel.java rename to src/main/java/ee/carlrobert/codegpt/toolwindow/chat/ui/UserMessagePanel.java index 964ea7ae..84a2a08f 100644 --- a/src/main/java/ee/carlrobert/codegpt/toolwindow/chat/components/UserMessagePanel.java +++ b/src/main/java/ee/carlrobert/codegpt/toolwindow/chat/ui/UserMessagePanel.java @@ -1,4 +1,4 @@ -package ee.carlrobert.codegpt.toolwindow.chat.components; +package ee.carlrobert.codegpt.toolwindow.chat.ui; import com.intellij.openapi.Disposable; import com.intellij.openapi.project.Project; @@ -18,20 +18,32 @@ public class UserMessagePanel extends JPanel { public UserMessagePanel(Project project, Message message, Disposable parentDisposable) { super(new BorderLayout()); + var headerPanel = new JPanel(new BorderLayout()); + headerPanel.setOpaque(false); + headerPanel.add(createDisplayNameLabel(), BorderLayout.LINE_START); setBorder(JBUI.Borders.compound( JBUI.Borders.customLine(JBColor.border(), 1, 0, 1, 0), JBUI.Borders.empty(12, 8, 8, 8))); setBackground(ColorUtil.brighter(getBackground(), 2)); - add(createDisplayNameLabel(), BorderLayout.NORTH); - add(createResponseBody(project, message, parentDisposable), BorderLayout.SOUTH); + add(headerPanel, BorderLayout.NORTH); + + var referencedFilePaths = message.getReferencedFilePaths(); + if (referencedFilePaths != null && !referencedFilePaths.isEmpty()) { + add(new SelectedFilesAccordion(project, referencedFilePaths), BorderLayout.CENTER); + add(createResponseBody( + project, + message.getUserMessage(), + parentDisposable), BorderLayout.SOUTH); + } else { + add(createResponseBody(project, message.getPrompt(), parentDisposable), BorderLayout.SOUTH); + } } private ChatMessageResponseBody createResponseBody( Project project, - Message message, + String prompt, Disposable parentDisposable) { - return new ChatMessageResponseBody(project, false, true, parentDisposable) - .withResponse(message.getPrompt()); + return new ChatMessageResponseBody(project, false, true, parentDisposable).withResponse(prompt); } private JBLabel createDisplayNameLabel() { diff --git a/src/main/java/ee/carlrobert/codegpt/toolwindow/chat/TokenDetails.java b/src/main/java/ee/carlrobert/codegpt/toolwindow/chat/ui/textarea/TotalTokensDetails.java similarity index 65% rename from src/main/java/ee/carlrobert/codegpt/toolwindow/chat/TokenDetails.java rename to src/main/java/ee/carlrobert/codegpt/toolwindow/chat/ui/textarea/TotalTokensDetails.java index 1d2ad9ad..ea5def9d 100644 --- a/src/main/java/ee/carlrobert/codegpt/toolwindow/chat/TokenDetails.java +++ b/src/main/java/ee/carlrobert/codegpt/toolwindow/chat/ui/textarea/TotalTokensDetails.java @@ -1,16 +1,17 @@ -package ee.carlrobert.codegpt.toolwindow.chat; +package ee.carlrobert.codegpt.toolwindow.chat.ui.textarea; import ee.carlrobert.codegpt.EncodingManager; import ee.carlrobert.codegpt.settings.configuration.ConfigurationState; -public class TokenDetails { +public class TotalTokensDetails { private final int systemPromptTokens; private int conversationTokens; private int userPromptTokens; private int highlightedTokens; + private int referencedFilesTokens; - public TokenDetails(EncodingManager encodingManager) { + public TotalTokensDetails(EncodingManager encodingManager) { systemPromptTokens = encodingManager.countTokens( ConfigurationState.getInstance().getSystemPrompt()); } @@ -43,7 +44,19 @@ public class TokenDetails { return highlightedTokens; } + public void setReferencedFilesTokens(int referencedFilesTokens) { + this.referencedFilesTokens = referencedFilesTokens; + } + + public int getReferencedFilesTokens() { + return referencedFilesTokens; + } + public int getTotal() { - return systemPromptTokens + conversationTokens + userPromptTokens + highlightedTokens; + return systemPromptTokens + + conversationTokens + + userPromptTokens + + highlightedTokens + + referencedFilesTokens; } } diff --git a/src/main/java/ee/carlrobert/codegpt/toolwindow/chat/components/TotalTokensPanel.java b/src/main/java/ee/carlrobert/codegpt/toolwindow/chat/ui/textarea/TotalTokensPanel.java similarity index 58% rename from src/main/java/ee/carlrobert/codegpt/toolwindow/chat/components/TotalTokensPanel.java rename to src/main/java/ee/carlrobert/codegpt/toolwindow/chat/ui/textarea/TotalTokensPanel.java index 80808ad5..ca4d2cb0 100644 --- a/src/main/java/ee/carlrobert/codegpt/toolwindow/chat/components/TotalTokensPanel.java +++ b/src/main/java/ee/carlrobert/codegpt/toolwindow/chat/ui/textarea/TotalTokensPanel.java @@ -1,4 +1,4 @@ -package ee.carlrobert.codegpt.toolwindow.chat.components; +package ee.carlrobert.codegpt.toolwindow.chat.ui.textarea; import static java.lang.String.format; @@ -9,15 +9,20 @@ 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.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.actions.IncludeFilesInContextNotifier; import ee.carlrobert.codegpt.conversations.Conversation; -import ee.carlrobert.codegpt.toolwindow.chat.TokenDetails; +import ee.carlrobert.codegpt.indexes.CodebaseIndexingCompletedNotifier; +import ee.carlrobert.embedding.CheckedFile; import java.awt.FlowLayout; import java.awt.event.MouseAdapter; import java.awt.event.MouseEvent; import java.util.LinkedHashMap; +import java.util.List; import java.util.Map; import java.util.stream.Collectors; import javax.swing.Box; @@ -27,25 +32,33 @@ import org.jetbrains.annotations.Nullable; public class TotalTokensPanel extends JPanel { - private final EncodingManager encodingManager; - private final TokenDetails tokenDetails; + private final EncodingManager encodingManager = EncodingManager.getInstance(); + private final TotalTokensDetails totalTokensDetails; private final JBLabel label; public TotalTokensPanel( + @NotNull Project project, Conversation conversation, @Nullable String highlightedText, Disposable parentDisposable) { super(new FlowLayout(FlowLayout.LEADING, 0, 0)); - setBorder(JBUI.Borders.empty(4)); - this.encodingManager = EncodingManager.getInstance(); - this.tokenDetails = createTokenDetails(conversation, highlightedText); - this.label = getLabel(tokenDetails); + this.totalTokensDetails = createTokenDetails( + conversation, + project.getUserData(CodeGPTKeys.SELECTED_FILES), + highlightedText); + this.label = getLabel(totalTokensDetails); + setBorder(JBUI.Borders.empty(4)); setOpaque(false); - add(getContextHelpIcon(tokenDetails)); + add(getContextHelpIcon(totalTokensDetails)); add(Box.createHorizontalStrut(4)); add(label); addSelectionListeners(parentDisposable); + + project.getMessageBus() + .connect() + .subscribe(IncludeFilesInContextNotifier.FILES_INCLUDED_IN_CONTEXT_TOPIC, + (IncludeFilesInContextNotifier) this::updateReferencedFilesTokens); } private void addSelectionListeners(Disposable parentDisposable) { @@ -70,12 +83,12 @@ public class TotalTokensPanel extends JPanel { }; } - public TokenDetails getTokenDetails() { - return tokenDetails; + public TotalTokensDetails getTokenDetails() { + return totalTokensDetails; } public void update() { - update(tokenDetails.getTotal()); + update(totalTokensDetails.getTotal()); } public void update(int total) { @@ -83,7 +96,7 @@ public class TotalTokensPanel extends JPanel { } public void updateConversationTokens(int total) { - tokenDetails.setConversationTokens(total); + totalTokensDetails.setConversationTokens(total); update(); } @@ -92,36 +105,50 @@ public class TotalTokensPanel extends JPanel { } public void updateUserPromptTokens(String userPrompt) { - tokenDetails.setUserPromptTokens(encodingManager.countTokens(userPrompt)); + totalTokensDetails.setUserPromptTokens(encodingManager.countTokens(userPrompt)); update(); } public void updateHighlightedTokens(String highlightedText) { - tokenDetails.setHighlightedTokens(encodingManager.countTokens(highlightedText)); + totalTokensDetails.setHighlightedTokens(encodingManager.countTokens(highlightedText)); update(); } - private TokenDetails createTokenDetails( + public void updateReferencedFilesTokens(List includedFiles) { + totalTokensDetails.setReferencedFilesTokens(includedFiles.stream() + .mapToInt(file -> encodingManager.countTokens(file.getFileContent())) + .sum()); + update(); + } + + private TotalTokensDetails createTokenDetails( Conversation conversation, + List includedFiles, @Nullable String highlightedText) { - var tokenDetails = new TokenDetails(encodingManager); + var tokenDetails = new TotalTokensDetails(encodingManager); tokenDetails.setConversationTokens(encodingManager.countConversationTokens(conversation)); + if (includedFiles != null) { + tokenDetails.setReferencedFilesTokens(includedFiles.stream() + .mapToInt(file -> encodingManager.countTokens(file.getFileContent())) + .sum()); + } if (highlightedText != null) { tokenDetails.setHighlightedTokens(encodingManager.countTokens(highlightedText)); } return tokenDetails; } - private JBLabel getContextHelpIcon(TokenDetails tokenDetails) { + private JBLabel getContextHelpIcon(TotalTokensDetails totalTokensDetails) { var iconLabel = new JBLabel(General.ContextHelp); iconLabel.addMouseListener(new MouseAdapter() { @Override public void mouseEntered(MouseEvent e) { var html = new LinkedHashMap<>(Map.of( - "System Prompt", tokenDetails.getSystemPromptTokens(), - "Conversation Tokens", tokenDetails.getConversationTokens(), - "Input Tokens", tokenDetails.getUserPromptTokens(), - "Highlighted Tokens", tokenDetails.getHighlightedTokens())) + "System Prompt", totalTokensDetails.getSystemPromptTokens(), + "Conversation Tokens", totalTokensDetails.getConversationTokens(), + "Input Tokens", totalTokensDetails.getUserPromptTokens(), + "Highlighted Tokens", totalTokensDetails.getHighlightedTokens(), + "Referenced Files Tokens", totalTokensDetails.getReferencedFilesTokens())) .entrySet().stream() .map(entry -> format( "

    %s: %d

    ", @@ -135,10 +162,10 @@ public class TotalTokensPanel extends JPanel { } private String getLabelHtml(int total) { - return format("Total Tokens: %d", total); + return format("Tokens: %d", total); } - private JBLabel getLabel(TokenDetails tokenDetails) { - return new JBLabel(getLabelHtml(tokenDetails.getTotal())); + private JBLabel getLabel(TotalTokensDetails totalTokensDetails) { + return new JBLabel(getLabelHtml(totalTokensDetails.getTotal())); } } diff --git a/src/main/java/ee/carlrobert/codegpt/toolwindow/chat/components/UserPromptTextArea.java b/src/main/java/ee/carlrobert/codegpt/toolwindow/chat/ui/textarea/UserPromptTextArea.java similarity index 98% rename from src/main/java/ee/carlrobert/codegpt/toolwindow/chat/components/UserPromptTextArea.java rename to src/main/java/ee/carlrobert/codegpt/toolwindow/chat/ui/textarea/UserPromptTextArea.java index 62f014ee..aca839b6 100644 --- a/src/main/java/ee/carlrobert/codegpt/toolwindow/chat/components/UserPromptTextArea.java +++ b/src/main/java/ee/carlrobert/codegpt/toolwindow/chat/ui/textarea/UserPromptTextArea.java @@ -1,4 +1,4 @@ -package ee.carlrobert.codegpt.toolwindow.chat.components; +package ee.carlrobert.codegpt.toolwindow.chat.ui.textarea; import com.intellij.icons.AllIcons; import com.intellij.openapi.diagnostic.Logger; @@ -11,7 +11,7 @@ import com.intellij.util.ui.JBUI; import ee.carlrobert.codegpt.CodeGPTBundle; import ee.carlrobert.codegpt.Icons; import ee.carlrobert.codegpt.completions.CompletionRequestHandler; -import ee.carlrobert.codegpt.util.UIUtil; +import ee.carlrobert.codegpt.ui.UIUtil; import java.awt.BasicStroke; import java.awt.BorderLayout; import java.awt.Cursor; diff --git a/src/main/java/ee/carlrobert/codegpt/toolwindow/chat/components/UserPromptTextAreaHeader.java b/src/main/java/ee/carlrobert/codegpt/toolwindow/chat/ui/textarea/UserPromptTextAreaHeader.java similarity index 97% rename from src/main/java/ee/carlrobert/codegpt/toolwindow/chat/components/UserPromptTextAreaHeader.java rename to src/main/java/ee/carlrobert/codegpt/toolwindow/chat/ui/textarea/UserPromptTextAreaHeader.java index 9e4ebd5f..6ce36373 100644 --- a/src/main/java/ee/carlrobert/codegpt/toolwindow/chat/components/UserPromptTextAreaHeader.java +++ b/src/main/java/ee/carlrobert/codegpt/toolwindow/chat/ui/textarea/UserPromptTextAreaHeader.java @@ -1,4 +1,4 @@ -package ee.carlrobert.codegpt.toolwindow.chat.components; +package ee.carlrobert.codegpt.toolwindow.chat.ui.textarea; import com.intellij.openapi.actionSystem.ActionPlaces; import com.intellij.openapi.application.ApplicationManager; diff --git a/src/main/java/ee/carlrobert/codegpt/toolwindow/chat/components/YouProCheckbox.java b/src/main/java/ee/carlrobert/codegpt/toolwindow/chat/ui/textarea/YouProCheckbox.java similarity index 95% rename from src/main/java/ee/carlrobert/codegpt/toolwindow/chat/components/YouProCheckbox.java rename to src/main/java/ee/carlrobert/codegpt/toolwindow/chat/ui/textarea/YouProCheckbox.java index 06b5ed58..34105216 100644 --- a/src/main/java/ee/carlrobert/codegpt/toolwindow/chat/components/YouProCheckbox.java +++ b/src/main/java/ee/carlrobert/codegpt/toolwindow/chat/ui/textarea/YouProCheckbox.java @@ -1,4 +1,4 @@ -package ee.carlrobert.codegpt.toolwindow.chat.components; +package ee.carlrobert.codegpt.toolwindow.chat.ui.textarea; import com.intellij.ui.components.JBCheckBox; import ee.carlrobert.codegpt.CodeGPTBundle; diff --git a/src/main/java/ee/carlrobert/codegpt/toolwindow/conversations/ConversationPanel.java b/src/main/java/ee/carlrobert/codegpt/toolwindow/conversations/ConversationPanel.java index c3a01f2e..9d058066 100644 --- a/src/main/java/ee/carlrobert/codegpt/toolwindow/conversations/ConversationPanel.java +++ b/src/main/java/ee/carlrobert/codegpt/toolwindow/conversations/ConversationPanel.java @@ -9,9 +9,9 @@ import ee.carlrobert.codegpt.actions.toolwindow.DeleteConversationAction; import ee.carlrobert.codegpt.conversations.Conversation; import ee.carlrobert.codegpt.conversations.ConversationsState; import ee.carlrobert.codegpt.settings.state.SettingsState; -import ee.carlrobert.codegpt.toolwindow.ModelIconLabel; -import ee.carlrobert.codegpt.toolwindow.chat.components.IconActionButton; import ee.carlrobert.codegpt.toolwindow.chat.standard.StandardChatToolWindowContentManager; +import ee.carlrobert.codegpt.ui.IconActionButton; +import ee.carlrobert.codegpt.ui.ModelIconLabel; import java.awt.BorderLayout; import java.awt.Cursor; import java.awt.GridBagConstraints; diff --git a/src/main/java/ee/carlrobert/codegpt/toolwindow/chat/components/IconActionButton.java b/src/main/java/ee/carlrobert/codegpt/ui/IconActionButton.java similarity index 93% rename from src/main/java/ee/carlrobert/codegpt/toolwindow/chat/components/IconActionButton.java rename to src/main/java/ee/carlrobert/codegpt/ui/IconActionButton.java index 6615d70d..bb093e10 100644 --- a/src/main/java/ee/carlrobert/codegpt/toolwindow/chat/components/IconActionButton.java +++ b/src/main/java/ee/carlrobert/codegpt/ui/IconActionButton.java @@ -1,4 +1,4 @@ -package ee.carlrobert.codegpt.toolwindow.chat.components; +package ee.carlrobert.codegpt.ui; import com.intellij.openapi.actionSystem.ActionPlaces; import com.intellij.openapi.actionSystem.ActionToolbar; diff --git a/src/main/java/ee/carlrobert/codegpt/toolwindow/ModelIconLabel.java b/src/main/java/ee/carlrobert/codegpt/ui/ModelIconLabel.java similarity index 96% rename from src/main/java/ee/carlrobert/codegpt/toolwindow/ModelIconLabel.java rename to src/main/java/ee/carlrobert/codegpt/ui/ModelIconLabel.java index d2d51813..75b6a100 100644 --- a/src/main/java/ee/carlrobert/codegpt/toolwindow/ModelIconLabel.java +++ b/src/main/java/ee/carlrobert/codegpt/ui/ModelIconLabel.java @@ -1,4 +1,4 @@ -package ee.carlrobert.codegpt.toolwindow; +package ee.carlrobert.codegpt.ui; import com.intellij.ui.components.JBLabel; import com.intellij.util.ui.JBFont; diff --git a/src/main/java/ee/carlrobert/codegpt/util/OverlayUtil.java b/src/main/java/ee/carlrobert/codegpt/ui/OverlayUtil.java similarity index 98% rename from src/main/java/ee/carlrobert/codegpt/util/OverlayUtil.java rename to src/main/java/ee/carlrobert/codegpt/ui/OverlayUtil.java index 8337ef1b..7e6f3215 100644 --- a/src/main/java/ee/carlrobert/codegpt/util/OverlayUtil.java +++ b/src/main/java/ee/carlrobert/codegpt/ui/OverlayUtil.java @@ -1,4 +1,4 @@ -package ee.carlrobert.codegpt.util; +package ee.carlrobert.codegpt.ui; import static com.intellij.openapi.ui.Messages.CANCEL; import static com.intellij.openapi.ui.Messages.OK; @@ -28,6 +28,7 @@ import ee.carlrobert.codegpt.CodeGPTBundle; import ee.carlrobert.codegpt.conversations.ConversationsState; import ee.carlrobert.codegpt.indexes.FolderStructureTreePanel; import ee.carlrobert.codegpt.settings.configuration.ConfigurationState; +import ee.carlrobert.codegpt.util.EditorUtil; import java.awt.Point; import java.awt.event.MouseEvent; import javax.swing.JComponent; diff --git a/src/main/java/ee/carlrobert/codegpt/util/UIUtil.java b/src/main/java/ee/carlrobert/codegpt/ui/UIUtil.java similarity index 86% rename from src/main/java/ee/carlrobert/codegpt/util/UIUtil.java rename to src/main/java/ee/carlrobert/codegpt/ui/UIUtil.java index be769289..919c5b5b 100644 --- a/src/main/java/ee/carlrobert/codegpt/util/UIUtil.java +++ b/src/main/java/ee/carlrobert/codegpt/ui/UIUtil.java @@ -1,16 +1,15 @@ -package ee.carlrobert.codegpt.util; +package ee.carlrobert.codegpt.ui; -import static com.intellij.util.ui.UIUtil.isUnderDarcula; import static javax.swing.event.HyperlinkEvent.EventType.ACTIVATED; import com.intellij.ide.BrowserUtil; import com.intellij.openapi.roots.ui.componentsList.components.ScrollablePanel; -import com.intellij.ui.ColorUtil; import com.intellij.ui.JBColor; import com.intellij.ui.ScrollPaneFactory; +import com.intellij.ui.components.JBTextArea; +import com.intellij.util.ui.JBUI; import com.intellij.util.ui.UI; -import ee.carlrobert.codegpt.toolwindow.chat.components.SmartScroller; -import java.awt.Color; +import ee.carlrobert.codegpt.toolwindow.chat.ui.SmartScroller; import java.awt.Dimension; import java.net.URISyntaxException; import javax.swing.AbstractAction; @@ -49,6 +48,16 @@ public class UIUtil { return textPane; } + public static JBTextArea createTextArea(String initialValue) { + var textArea = new JBTextArea(initialValue); + textArea.setRows(3); + textArea.setBorder(JBUI.Borders.compound( + JBUI.Borders.customLine(JBColor.border()), + JBUI.Borders.empty(4))); + textArea.setLineWrap(true); + return textArea; + } + public static JButton createIconButton(Icon icon) { var button = new JButton(icon); button.setBorder(BorderFactory.createEmptyBorder()); @@ -95,11 +104,5 @@ public class UIUtil { textArea.getInputMap().put(KeyStroke.getKeyStroke("ENTER"), "text-submit"); textArea.getActionMap().put("text-submit", onSubmit); } - - public static Color getPanelBackgroundColor() { - return isUnderDarcula() - ? ColorUtil.darker(JBColor.PanelBackground, 1) - : JBColor.PanelBackground.brighter(); - } } diff --git a/src/main/java/ee/carlrobert/codegpt/ui/checkbox/FileCheckboxTree.java b/src/main/java/ee/carlrobert/codegpt/ui/checkbox/FileCheckboxTree.java new file mode 100644 index 00000000..324df9f3 --- /dev/null +++ b/src/main/java/ee/carlrobert/codegpt/ui/checkbox/FileCheckboxTree.java @@ -0,0 +1,29 @@ +package ee.carlrobert.codegpt.ui.checkbox; + +import com.intellij.openapi.fileTypes.FileTypeManager; +import com.intellij.openapi.vfs.VirtualFile; +import com.intellij.ui.CheckboxTree; +import com.intellij.ui.CheckedTreeNode; +import com.intellij.ui.ColoredTreeCellRenderer; +import ee.carlrobert.codegpt.util.file.FileUtil; +import ee.carlrobert.embedding.CheckedFile; +import java.util.List; +import org.jetbrains.annotations.NotNull; + +public abstract class FileCheckboxTree extends CheckboxTree { + + public FileCheckboxTree(FileCheckboxTreeCellRenderer cellRenderer, CheckedTreeNode node) { + super(cellRenderer, node); + } + + public abstract List getCheckedFiles(); + + protected static void updateFilePresentation( + ColoredTreeCellRenderer textRenderer, + @NotNull VirtualFile virtualFile) { + var fileType = FileTypeManager.getInstance().getFileTypeByFile(virtualFile); + textRenderer.setIcon(fileType.getIcon()); + textRenderer.append(virtualFile.getName()); + textRenderer.append(" - " + FileUtil.convertFileSize(virtualFile.getLength())); + } +} diff --git a/src/main/java/ee/carlrobert/codegpt/ui/checkbox/FileCheckboxTreeCellRenderer.java b/src/main/java/ee/carlrobert/codegpt/ui/checkbox/FileCheckboxTreeCellRenderer.java new file mode 100644 index 00000000..c0ea01e6 --- /dev/null +++ b/src/main/java/ee/carlrobert/codegpt/ui/checkbox/FileCheckboxTreeCellRenderer.java @@ -0,0 +1,26 @@ +package ee.carlrobert.codegpt.ui.checkbox; + +import com.intellij.ui.CheckboxTree; +import com.intellij.ui.CheckedTreeNode; +import javax.swing.JTree; + +public abstract class FileCheckboxTreeCellRenderer extends CheckboxTree.CheckboxTreeCellRenderer { + + abstract void updatePresentation(Object userObject); + + @Override + public void customizeRenderer( + JTree tree, + Object value, + boolean selected, + boolean expanded, + boolean leaf, + int row, + boolean hasFocus) { + if (!(value instanceof CheckedTreeNode)) { + return; + } + + updatePresentation(((CheckedTreeNode) value).getUserObject()); + } +} diff --git a/src/main/java/ee/carlrobert/codegpt/ui/checkbox/PsiElementCheckboxTree.java b/src/main/java/ee/carlrobert/codegpt/ui/checkbox/PsiElementCheckboxTree.java new file mode 100644 index 00000000..9b250a69 --- /dev/null +++ b/src/main/java/ee/carlrobert/codegpt/ui/checkbox/PsiElementCheckboxTree.java @@ -0,0 +1,81 @@ +package ee.carlrobert.codegpt.ui.checkbox; + +import static java.util.stream.Collectors.toList; + +import com.intellij.icons.AllIcons; +import com.intellij.openapi.util.Iconable; +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.embedding.CheckedFile; +import java.io.File; +import java.util.Arrays; +import java.util.List; +import java.util.Optional; +import org.jetbrains.annotations.NotNull; + +public class PsiElementCheckboxTree extends FileCheckboxTree { + + public PsiElementCheckboxTree(@NotNull PsiElement rootElement) { + super(createFileTypesRenderer(), createNode(rootElement)); + setRootVisible(true); + } + + public List getCheckedFiles() { + return Arrays.stream(getCheckedNodes( + PsiElement.class, + node -> Optional.ofNullable(node.getContainingFile()) + .map(PsiFile::getVirtualFile) + .isPresent())) + .map(item -> new CheckedFile(new File(item.getContainingFile().getVirtualFile().getPath()))) + .collect(toList()); + } + + private static CheckedTreeNode createNode(PsiElement element) { + if (!(element instanceof PsiDirectory || element instanceof PsiFile)) { + return null; + } + + CheckedTreeNode node = new CheckedTreeNode(element); + if (element instanceof PsiDirectory) { + for (PsiElement child : element.getChildren()) { + CheckedTreeNode childNode = createNode(child); + if (childNode != null) { + node.add(childNode); + } + } + } + return node; + } + + private static @NotNull FileCheckboxTreeCellRenderer createFileTypesRenderer() { + return new FileCheckboxTreeCellRenderer() { + @Override + void updatePresentation(Object userObject) { + var psiElement = (PsiElement) userObject; + Optional.ofNullable(psiElement) + .map(PsiElement::getContainingFile) + .ifPresentOrElse( + item -> { + var virtualFile = item.getVirtualFile(); + if (virtualFile != null) { + updateFilePresentation(getTextRenderer(), virtualFile); + } + }, + () -> { + if (userObject instanceof PsiDirectoryImpl) { + updateFolderPresentation((PsiDirectoryImpl) userObject); + } + }); + } + + private void updateFolderPresentation(PsiDirectoryImpl psiDirectory) { + var icon = psiDirectory.getIcon(Iconable.ICON_FLAG_VISIBILITY); + getTextRenderer().setIcon(icon == null ? AllIcons.Nodes.Folder : icon); + getTextRenderer().append(psiDirectory.getName()); + } + }; + } +} diff --git a/src/main/java/ee/carlrobert/codegpt/ui/checkbox/VirtualFileCheckboxTree.java b/src/main/java/ee/carlrobert/codegpt/ui/checkbox/VirtualFileCheckboxTree.java new file mode 100644 index 00000000..406fedac --- /dev/null +++ b/src/main/java/ee/carlrobert/codegpt/ui/checkbox/VirtualFileCheckboxTree.java @@ -0,0 +1,66 @@ +package ee.carlrobert.codegpt.ui.checkbox; + +import static java.util.stream.Collectors.toList; + +import com.intellij.icons.AllIcons; +import com.intellij.openapi.vfs.VirtualFile; +import com.intellij.ui.CheckedTreeNode; +import com.intellij.util.PlatformIcons; +import ee.carlrobert.embedding.CheckedFile; +import java.io.File; +import java.util.Arrays; +import java.util.List; +import java.util.Objects; +import org.jetbrains.annotations.NotNull; + +public class VirtualFileCheckboxTree extends FileCheckboxTree { + + public VirtualFileCheckboxTree(@NotNull VirtualFile[] rootFiles) { + super(createFileTypesRenderer(), createRootNode(rootFiles)); + } + + public List getCheckedFiles() { + return Arrays.stream(getCheckedNodes(VirtualFile.class, Objects::nonNull)) + .map(item -> new CheckedFile(new File(item.getPath()))) + .collect(toList()); + } + + private static CheckedTreeNode createRootNode(VirtualFile[] files) { + CheckedTreeNode rootNode = new CheckedTreeNode(null); + for (VirtualFile file : files) { + rootNode.add(createNode(file)); + } + return rootNode; + } + + private static CheckedTreeNode createNode(VirtualFile file) { + CheckedTreeNode node = new CheckedTreeNode(file); + if (file.isDirectory()) { + VirtualFile[] children = file.getChildren(); + for (VirtualFile child : children) { + node.add(createNode(child)); + } + } + return node; + } + + private static @NotNull FileCheckboxTreeCellRenderer createFileTypesRenderer() { + return new FileCheckboxTreeCellRenderer() { + @Override + void updatePresentation(Object userObject) { + if (userObject instanceof VirtualFile) { + VirtualFile virtualFile = (VirtualFile) userObject; + if (virtualFile.isDirectory()) { + getTextRenderer().append(virtualFile.getName()); + getTextRenderer().setIcon(PlatformIcons.FOLDER_ICON); + } else { + updateFilePresentation(getTextRenderer(), virtualFile); + } + } else if (userObject == null) { + getTextRenderer().setIcon(AllIcons.Nodes.Folder); + getTextRenderer().append("[root]"); + } + } + }; + } +} diff --git a/src/main/resources/META-INF/plugin.xml b/src/main/resources/META-INF/plugin.xml index 2b0114a3..e635a9ca 100644 --- a/src/main/resources/META-INF/plugin.xml +++ b/src/main/resources/META-INF/plugin.xml @@ -30,6 +30,7 @@ + @@ -51,6 +52,22 @@ + + + + + + + { + assertThat(request.getUri().getPath()).isEqualTo("/v1/chat/completions"); + assertThat(request.getMethod()).isEqualTo("POST"); + assertThat(request.getHeaders().get(AUTHORIZATION).get(0)).isEqualTo("Bearer TEST_API_KEY"); + assertThat(request.getBody()) + .extracting( + "model", + "messages") + .containsExactly( + "gpt-4", + List.of( + Map.of("role", "system", "content", COMPLETION_SYSTEM_PROMPT), + Map.of("role", "user", "content", + "Use the following context to answer question at the end:\n\n" + + "File Path: TEST_FILE_PATH_1\n" + + "File Content:\n" + + "```TEST_FILE_NAME_1\n" + + "TEST_FILE_CONTENT_1\n" + + "```\n\n" + + "File Path: TEST_FILE_PATH_2\n" + + "File Content:\n" + + "```TEST_FILE_NAME_2\n" + + "TEST_FILE_CONTENT_2\n" + + "```\n\n" + + "File Path: TEST_FILE_PATH_3\n" + + "File Content:\n" + + "```TEST_FILE_NAME_3\n" + + "TEST_FILE_CONTENT_3\n" + + "```\n\n" + + "Question: Hello!"))); + return List.of( + jsonMapResponse("choices", jsonArray(jsonMap("delta", jsonMap("role", "assistant")))), + jsonMapResponse("choices", jsonArray(jsonMap("delta", jsonMap("content", "Hel")))), + jsonMapResponse("choices", jsonArray(jsonMap("delta", jsonMap("content", "lo")))), + jsonMapResponse("choices", jsonArray(jsonMap("delta", jsonMap("content", "!"))))); + }); + + panel.sendMessage(message); + + await().atMost(5, SECONDS) + .until(() -> { + var messages = conversation.getMessages(); + return !messages.isEmpty() && "Hello!".equals(messages.get(0).getResponse()); + }); + var encodingManager = EncodingManager.getInstance(); + assertThat(panel.getTokenDetails()).extracting( + "systemPromptTokens", + "conversationTokens", + "userPromptTokens", + "highlightedTokens") + .containsExactly( + encodingManager.countTokens(COMPLETION_SYSTEM_PROMPT), + encodingManager.countTokens(message.getPrompt()), + 0, + 0); + assertThat(panel.getConversation()) + .isNotNull() + .extracting("id", "model", "clientCode", "discardTokenLimit") + .containsExactly( + conversation.getId(), + conversation.getModel(), + conversation.getClientCode(), + false); + var messages = panel.getConversation().getMessages(); + assertThat(messages.size()).isOne(); + assertThat(messages.get(0)) + .extracting("id", "prompt", "response", "referencedFilePaths") + .containsExactly( + message.getId(), + message.getPrompt(), + message.getResponse(), + List.of("TEST_FILE_PATH_1", "TEST_FILE_PATH_2", "TEST_FILE_PATH_3")); + } } diff --git a/src/test/java/testsupport/IntegrationTest.java b/src/test/java/testsupport/IntegrationTest.java index 19f266a1..a3bd2349 100644 --- a/src/test/java/testsupport/IntegrationTest.java +++ b/src/test/java/testsupport/IntegrationTest.java @@ -1,7 +1,9 @@ package testsupport; import com.intellij.testFramework.fixtures.BasePlatformTestCase; +import ee.carlrobert.codegpt.CodeGPTKeys; import ee.carlrobert.llm.client.mixin.ExternalServiceTestMixin; +import java.util.Collections; import org.junit.jupiter.api.AfterEach; import testsupport.mixin.ShortcutsTestMixin; @@ -13,6 +15,12 @@ public class IntegrationTest extends BasePlatformTestCase implements ExternalServiceTestMixin.init(); } + @Override + protected void setUp() throws Exception { + super.setUp(); + getProject().putUserData(CodeGPTKeys.SELECTED_FILES, Collections.emptyList()); + } + @AfterEach public void cleanUpEach() { ExternalServiceTestMixin.clearAll();