diff --git a/src/main/java/ee/carlrobert/codegpt/actions/GenerateGitCommitMessageAction.java b/src/main/java/ee/carlrobert/codegpt/actions/GenerateGitCommitMessageAction.java deleted file mode 100644 index 79d40759c..000000000 --- a/src/main/java/ee/carlrobert/codegpt/actions/GenerateGitCommitMessageAction.java +++ /dev/null @@ -1,234 +0,0 @@ -package ee.carlrobert.codegpt.actions; - -import static com.intellij.openapi.ui.Messages.OK; -import static com.intellij.util.ObjectUtils.tryCast; -import static ee.carlrobert.codegpt.settings.service.ServiceType.YOU; -import static java.util.stream.Collectors.joining; - -import com.intellij.notification.Notification; -import com.intellij.notification.NotificationType; -import com.intellij.notification.Notifications; -import com.intellij.openapi.actionSystem.ActionUpdateThread; -import com.intellij.openapi.actionSystem.AnAction; -import com.intellij.openapi.actionSystem.AnActionEvent; -import com.intellij.openapi.application.ApplicationManager; -import com.intellij.openapi.command.WriteCommandAction; -import com.intellij.openapi.editor.Document; -import com.intellij.openapi.editor.Editor; -import com.intellij.openapi.editor.ex.EditorEx; -import com.intellij.openapi.project.Project; -import com.intellij.openapi.vcs.FilePath; -import com.intellij.openapi.vcs.VcsDataKeys; -import com.intellij.openapi.vcs.changes.Change; -import com.intellij.openapi.vcs.ui.CommitMessage; -import com.intellij.openapi.vfs.VirtualFile; -import com.intellij.vcs.commit.CommitWorkflowUi; -import ee.carlrobert.codegpt.CodeGPTBundle; -import ee.carlrobert.codegpt.EncodingManager; -import ee.carlrobert.codegpt.Icons; -import ee.carlrobert.codegpt.completions.CompletionRequestService; -import ee.carlrobert.codegpt.settings.GeneralSettings; -import ee.carlrobert.codegpt.settings.configuration.CommitMessageTemplate; -import ee.carlrobert.codegpt.ui.OverlayUtil; -import ee.carlrobert.llm.client.openai.completion.ErrorDetails; -import ee.carlrobert.llm.completion.CompletionEventListener; -import java.io.BufferedReader; -import java.io.File; -import java.io.IOException; -import java.io.InputStreamReader; -import java.nio.file.Files; -import java.nio.file.Path; -import java.util.ArrayList; -import java.util.List; -import java.util.Map; -import java.util.Objects; -import java.util.Optional; -import okhttp3.sse.EventSource; -import org.jetbrains.annotations.NotNull; - -public class GenerateGitCommitMessageAction extends AnAction { - - public static final int MAX_TOKEN_COUNT_WARNING = 4096; - private final EncodingManager encodingManager; - - public GenerateGitCommitMessageAction() { - super( - CodeGPTBundle.get("action.generateCommitMessage.title"), - CodeGPTBundle.get("action.generateCommitMessage.description"), - Icons.Sparkle); - encodingManager = EncodingManager.getInstance(); - } - - @Override - public void update(@NotNull AnActionEvent event) { - var commitWorkflowUi = event.getData(VcsDataKeys.COMMIT_WORKFLOW_UI); - var selectedService = GeneralSettings.getCurrentState().getSelectedService(); - if (selectedService == YOU || commitWorkflowUi == null) { - event.getPresentation().setVisible(false); - return; - } - - var callAllowed = CompletionRequestService.isRequestAllowed( - GeneralSettings.getCurrentState().getSelectedService()); - event.getPresentation().setEnabled(callAllowed - && new CommitWorkflowChanges(commitWorkflowUi).isFilesSelected()); - event.getPresentation().setText(CodeGPTBundle.get(callAllowed - ? "action.generateCommitMessage.title" - : "action.generateCommitMessage.missingCredentials")); - } - - @Override - public void actionPerformed(@NotNull AnActionEvent event) { - var project = event.getProject(); - if (project == null || project.getBasePath() == null) { - return; - } - - var gitDiff = getGitDiff(event, project); - var tokenCount = encodingManager.countTokens(gitDiff); - if (tokenCount > MAX_TOKEN_COUNT_WARNING - && OverlayUtil.showTokenSoftLimitWarningDialog(tokenCount) != OK) { - return; - } - - var editor = getCommitMessageEditor(event); - if (editor != null) { - ((EditorEx) editor).setCaretVisible(false); - CompletionRequestService.getInstance() - .generateCommitMessageAsync( - project.getService(CommitMessageTemplate.class).getSystemPrompt(), - gitDiff, - getEventListener(project, editor.getDocument())); - } - } - - @Override - public @NotNull ActionUpdateThread getActionUpdateThread() { - return ActionUpdateThread.EDT; - } - - private CompletionEventListener getEventListener(Project project, Document document) { - return new CompletionEventListener<>() { - private final StringBuilder messageBuilder = new StringBuilder(); - - @Override - public void onMessage(String message, EventSource eventSource) { - messageBuilder.append(message); - var application = ApplicationManager.getApplication(); - application.invokeLater(() -> - application.runWriteAction(() -> - WriteCommandAction.runWriteCommandAction(project, () -> - document.setText(messageBuilder)))); - } - - @Override - public void onError(ErrorDetails error, Throwable ex) { - Notifications.Bus.notify(new Notification( - "CodeGPT Notification Group", - "CodeGPT", - error.getMessage(), - NotificationType.ERROR)); - } - }; - } - - private Editor getCommitMessageEditor(AnActionEvent event) { - var commitMessage = tryCast( - event.getData(VcsDataKeys.COMMIT_MESSAGE_CONTROL), - CommitMessage.class); - return commitMessage != null ? commitMessage.getEditorField().getEditor() : null; - } - - private String getGitDiff(AnActionEvent event, Project project) { - var commitWorkflowUi = Optional.ofNullable(event.getData(VcsDataKeys.COMMIT_WORKFLOW_UI)) - .orElseThrow(() -> new IllegalStateException("Could not retrieve commit workflow ui.")); - var changes = new CommitWorkflowChanges(commitWorkflowUi); - var projectBasePath = project.getBasePath(); - var gitDiff = getGitDiff(projectBasePath, changes.getIncludedVersionedFilePaths(), false); - var stagedGitDiff = getGitDiff(projectBasePath, changes.getIncludedVersionedFilePaths(), true); - var newFilesContent = - getNewFilesDiff(projectBasePath, changes.getIncludedUnversionedFilePaths()); - - return Map.of( - "Git diff", gitDiff, - "Staged git diff", stagedGitDiff, - "New files", newFilesContent) - .entrySet().stream() - .filter(entry -> !entry.getValue().isEmpty()) - .map(entry -> "%s:%n%s".formatted(entry.getKey(), entry.getValue())) - .collect(joining("\n\n")); - } - - private String getGitDiff(String projectPath, List filePaths, boolean cached) { - if (filePaths.isEmpty()) { - return ""; - } - - var process = createGitDiffProcess(projectPath, filePaths, cached); - return new BufferedReader(new InputStreamReader(process.getInputStream())) - .lines() - .collect(joining("\n")); - } - - private String getNewFilesDiff(String projectPath, List filePaths) { - return filePaths.stream() - .map(pathString -> { - var filePath = Path.of(pathString); - var relativePath = Path.of(projectPath).relativize(filePath); - try { - return "New file '" + relativePath + "' content:\n" + Files.readString(filePath); - } catch (IOException ignored) { - return null; - } - }) - .filter(Objects::nonNull) - .collect(joining("\n")); - } - - private Process createGitDiffProcess(String projectPath, List filePaths, boolean cached) { - var command = new ArrayList(); - command.add("git"); - command.add("diff"); - if (cached) { - command.add("--cached"); - } - command.addAll(filePaths); - - var processBuilder = new ProcessBuilder(command); - processBuilder.directory(new File(projectPath)); - try { - return processBuilder.start(); - } catch (IOException ex) { - throw new RuntimeException("Unable to start git diff process", ex); - } - } - - static class CommitWorkflowChanges { - - private final List includedVersionedFilePaths; - private final List includedUnversionedFilePaths; - - CommitWorkflowChanges(CommitWorkflowUi commitWorkflowUi) { - includedVersionedFilePaths = commitWorkflowUi.getIncludedChanges().stream() - .map(Change::getVirtualFile) - .filter(Objects::nonNull) - .map(VirtualFile::getPath) - .toList(); - includedUnversionedFilePaths = commitWorkflowUi.getIncludedUnversionedFiles().stream() - .map(FilePath::getPath) - .toList(); - } - - public List getIncludedVersionedFilePaths() { - return includedVersionedFilePaths; - } - - public List getIncludedUnversionedFilePaths() { - return includedUnversionedFilePaths; - } - - public boolean isFilesSelected() { - return !includedVersionedFilePaths.isEmpty() || !includedUnversionedFilePaths.isEmpty(); - } - } -} diff --git a/src/main/java/ee/carlrobert/codegpt/actions/IncludeFilesInContextAction.java b/src/main/java/ee/carlrobert/codegpt/actions/IncludeFilesInContextAction.java deleted file mode 100644 index 924d24ec3..000000000 --- a/src/main/java/ee/carlrobert/codegpt/actions/IncludeFilesInContextAction.java +++ /dev/null @@ -1,237 +0,0 @@ -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 ee.carlrobert.codegpt.settings.IncludedFilesSettingsState.DEFAULT_PROMPT_TEMPLATE; -import static ee.carlrobert.codegpt.settings.IncludedFilesSettingsState.DEFAULT_REPEATABLE_CONTEXT; -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 com.intellij.util.ui.UI.PanelFactory; -import ee.carlrobert.codegpt.CodeGPTBundle; -import ee.carlrobert.codegpt.CodeGPTKeys; -import ee.carlrobert.codegpt.EncodingManager; -import ee.carlrobert.codegpt.ReferencedFile; -import ee.carlrobert.codegpt.settings.IncludedFilesSettings; -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.codegpt.util.file.FileUtil; -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() { - this("action.includeFilesInContext.title"); - } - - public IncludeFilesInContextAction(String customTitleKey) { - super(CodeGPTBundle.get(customTitleKey)); - } - - @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.getReferencedFiles()); - checkboxTree.addCheckboxTreeListener(new CheckboxTreeListener() { - @Override - public void nodeStateChanged(@NotNull CheckedTreeNode node) { - totalTokensLabel.updateState(node); - } - }); - - var includedFilesSettings = IncludedFilesSettings.getCurrentState(); - 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.getReferencedFiles()); - project.getMessageBus() - .syncPublisher(IncludeFilesInContextNotifier.FILES_INCLUDED_IN_CONTEXT_TOPIC) - .filesIncluded(checkboxTree.getReferencedFiles()); - includedFilesSettings.setPromptTemplate(promptTemplateTextArea.getText()); - includedFilesSettings.setRepeatableContext(repeatableContextTextArea.getText()); - } - } - - private @Nullable FileCheckboxTree getCheckboxTree(DataContext dataContext) { - 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 referencedFiles) { - fileCount = referencedFiles.size(); - totalTokens = calculateTotalTokens(referencedFiles); - 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 psiElement) { - var psiFile = psiElement.getContainingFile(); - if (psiFile != null) { - var virtualFile = psiFile.getVirtualFile(); - if (virtualFile != null) { - return getVirtualFileContent(virtualFile); - } - } - } - if (userObject instanceof VirtualFile virtualFile) { - return getVirtualFileContent(virtualFile); - } - 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 %s tokens", - fileCount, - fileCount == 1 ? "file" : "files", - FileUtil.convertLongValue(totalTokens))); - } - - private int calculateTotalTokens(List referencedFiles) { - return referencedFiles.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("shared.promptTemplate"), - PanelFactory.panel(promptTemplateTextArea).withComment( - "

The template that will be used to create the final prompt. " - + "The {REPEATABLE_CONTEXT} placeholder must be included " - + "to correctly map the file contents.

") - .createPanel(), - true) - .addVerticalGap(4) - .addLabeledComponent( - CodeGPTBundle.get("action.includeFilesInContext.dialog.repeatableContext.label"), - PanelFactory.panel(repeatableContextTextArea).withComment( - "

The context that will be repeated for each included file. " - + "Acceptable placeholders include {FILE_PATH} and " - + "{FILE_CONTENT}.

") - .createPanel(), - true) - .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 = IncludedFilesSettings.getCurrentState(); - includedFilesSettings.setPromptTemplate(DEFAULT_PROMPT_TEMPLATE); - includedFilesSettings.setRepeatableContext(DEFAULT_REPEATABLE_CONTEXT); - promptTemplateTextArea.setText(DEFAULT_PROMPT_TEMPLATE); - repeatableContextTextArea.setText(DEFAULT_REPEATABLE_CONTEXT); - }); - return restoreButton; - } -} diff --git a/src/main/java/ee/carlrobert/codegpt/actions/IncludeFilesInContextNotifier.java b/src/main/java/ee/carlrobert/codegpt/actions/IncludeFilesInContextNotifier.java deleted file mode 100644 index 796a00902..000000000 --- a/src/main/java/ee/carlrobert/codegpt/actions/IncludeFilesInContextNotifier.java +++ /dev/null @@ -1,13 +0,0 @@ -package ee.carlrobert.codegpt.actions; - -import com.intellij.util.messages.Topic; -import ee.carlrobert.codegpt.ReferencedFile; -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/OpenSettingsAction.java b/src/main/java/ee/carlrobert/codegpt/actions/OpenSettingsAction.java deleted file mode 100644 index abba90648..000000000 --- a/src/main/java/ee/carlrobert/codegpt/actions/OpenSettingsAction.java +++ /dev/null @@ -1,23 +0,0 @@ -package ee.carlrobert.codegpt.actions; - -import com.intellij.icons.AllIcons.General; -import com.intellij.openapi.actionSystem.AnAction; -import com.intellij.openapi.actionSystem.AnActionEvent; -import com.intellij.openapi.options.ShowSettingsUtil; -import ee.carlrobert.codegpt.CodeGPTBundle; -import ee.carlrobert.codegpt.settings.service.ServiceConfigurable; -import org.jetbrains.annotations.NotNull; - -public class OpenSettingsAction extends AnAction { - - public OpenSettingsAction() { - super(CodeGPTBundle.get("action.openSettings.title"), - CodeGPTBundle.get("action.openSettings.description"), - General.Settings); - } - - @Override - public void actionPerformed(@NotNull AnActionEvent e) { - ShowSettingsUtil.getInstance().showSettingsDialog(e.getProject(), ServiceConfigurable.class); - } -} diff --git a/src/main/java/ee/carlrobert/codegpt/actions/TrackableAction.java b/src/main/java/ee/carlrobert/codegpt/actions/TrackableAction.java deleted file mode 100644 index 66ad38ee9..000000000 --- a/src/main/java/ee/carlrobert/codegpt/actions/TrackableAction.java +++ /dev/null @@ -1,46 +0,0 @@ -package ee.carlrobert.codegpt.actions; - -import com.intellij.openapi.actionSystem.AnAction; -import com.intellij.openapi.actionSystem.AnActionEvent; -import com.intellij.openapi.editor.Editor; -import ee.carlrobert.codegpt.telemetry.TelemetryAction; -import javax.swing.Icon; -import org.jetbrains.annotations.NotNull; - -public abstract class TrackableAction extends AnAction { - - private final ActionType actionType; - protected final Editor editor; - - public TrackableAction( - @NotNull Editor editor, - String text, - String description, - Icon icon, - ActionType actionType) { - super(text, description, icon); - this.editor = editor; - this.actionType = actionType; - } - - public abstract void handleAction(@NotNull AnActionEvent e); - - @Override - public void actionPerformed(@NotNull AnActionEvent e) { - try { - handleAction(e); - } catch (Exception ex) { - TelemetryAction.IDE_ACTION_ERROR - .createActionMessage() - .error(ex) - .send(); - throw ex; - } finally { - TelemetryAction.IDE_ACTION - .createActionMessage() - .property("group", null) - .property("action", actionType.name()) - .send(); - } - } -} \ No newline at end of file diff --git a/src/main/java/ee/carlrobert/codegpt/actions/editor/AskAction.java b/src/main/java/ee/carlrobert/codegpt/actions/editor/AskAction.java deleted file mode 100644 index b83f4b888..000000000 --- a/src/main/java/ee/carlrobert/codegpt/actions/editor/AskAction.java +++ /dev/null @@ -1,40 +0,0 @@ -package ee.carlrobert.codegpt.actions.editor; - -import com.intellij.openapi.actionSystem.ActionUpdateThread; -import com.intellij.openapi.actionSystem.AnAction; -import com.intellij.openapi.actionSystem.AnActionEvent; -import ee.carlrobert.codegpt.Icons; -import ee.carlrobert.codegpt.conversations.ConversationsState; -import ee.carlrobert.codegpt.toolwindow.chat.ChatToolWindowContentManager; -import org.jetbrains.annotations.NotNull; - -public class AskAction extends AnAction { - - public AskAction() { - super("New Chat", "Chat with CodeGPT", Icons.Sparkle); - EditorActionsUtil.registerAction(this); - } - - @Override - public void update(@NotNull AnActionEvent event) { - event.getPresentation().setEnabled(event.getProject() != null); - } - - @Override - public void actionPerformed(@NotNull AnActionEvent event) { - var project = event.getProject(); - if (project != null) { - ConversationsState.getInstance().setCurrentConversation(null); - var tabPanel = - project.getService(ChatToolWindowContentManager.class).createNewTabPanel(); - if (tabPanel != null) { - tabPanel.displayLandingView(); - } - } - } - - @Override - public @NotNull ActionUpdateThread getActionUpdateThread() { - return ActionUpdateThread.BGT; - } -} diff --git a/src/main/java/ee/carlrobert/codegpt/actions/editor/BaseEditorAction.java b/src/main/java/ee/carlrobert/codegpt/actions/editor/BaseEditorAction.java deleted file mode 100644 index 8b0dd4603..000000000 --- a/src/main/java/ee/carlrobert/codegpt/actions/editor/BaseEditorAction.java +++ /dev/null @@ -1,54 +0,0 @@ -package ee.carlrobert.codegpt.actions.editor; - -import com.intellij.openapi.actionSystem.ActionUpdateThread; -import com.intellij.openapi.actionSystem.AnAction; -import com.intellij.openapi.actionSystem.AnActionEvent; -import com.intellij.openapi.actionSystem.PlatformDataKeys; -import com.intellij.openapi.editor.Editor; -import com.intellij.openapi.project.Project; -import com.intellij.openapi.util.NlsActions; -import javax.swing.Icon; -import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; - -abstract class BaseEditorAction extends AnAction { - - BaseEditorAction( - @Nullable @NlsActions.ActionText String text, - @Nullable @NlsActions.ActionDescription String description, - @Nullable Icon icon) { - super(text, description, icon); - EditorActionsUtil.registerAction(this); - } - - BaseEditorAction( - @Nullable @NlsActions.ActionText String text, - @Nullable @NlsActions.ActionDescription String description) { - this(text, description, null); - } - - protected abstract void actionPerformed(Project project, Editor editor, String selectedText); - - public void actionPerformed(@NotNull AnActionEvent event) { - var project = event.getProject(); - var editor = event.getData(PlatformDataKeys.EDITOR); - if (editor != null && project != null) { - actionPerformed(project, editor, editor.getSelectionModel().getSelectedText()); - } - } - - public void update(AnActionEvent event) { - Project project = event.getProject(); - Editor editor = event.getData(PlatformDataKeys.EDITOR); - boolean menuAllowed = false; - if (editor != null && project != null) { - menuAllowed = editor.getSelectionModel().getSelectedText() != null; - } - event.getPresentation().setEnabled(menuAllowed); - } - - @Override - public @NotNull ActionUpdateThread getActionUpdateThread() { - return ActionUpdateThread.EDT; - } -} diff --git a/src/main/java/ee/carlrobert/codegpt/actions/editor/CustomPromptAction.java b/src/main/java/ee/carlrobert/codegpt/actions/editor/CustomPromptAction.java deleted file mode 100644 index 5d89428a7..000000000 --- a/src/main/java/ee/carlrobert/codegpt/actions/editor/CustomPromptAction.java +++ /dev/null @@ -1,93 +0,0 @@ -package ee.carlrobert.codegpt.actions.editor; - -import static java.lang.String.format; - -import com.intellij.icons.AllIcons; -import com.intellij.openapi.editor.Editor; -import com.intellij.openapi.editor.impl.EditorImpl; -import com.intellij.openapi.project.Project; -import com.intellij.openapi.ui.DialogWrapper; -import com.intellij.util.ui.FormBuilder; -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.ChatToolWindowContentManager; -import ee.carlrobert.codegpt.ui.UIUtil; -import ee.carlrobert.codegpt.util.file.FileUtil; -import java.awt.event.ActionEvent; -import javax.swing.AbstractAction; -import javax.swing.JComponent; -import javax.swing.JTextArea; -import javax.swing.SwingUtilities; -import org.jetbrains.annotations.Nullable; - -public class CustomPromptAction extends BaseEditorAction { - - private static String previousUserPrompt = ""; - - CustomPromptAction() { - super("Custom Prompt", "Custom prompt description", AllIcons.Actions.Run_anything); - EditorActionsUtil.registerAction(this); - } - - @Override - protected void actionPerformed(Project project, Editor editor, String selectedText) { - if (selectedText != null && !selectedText.isEmpty()) { - var fileExtension = FileUtil.getFileExtension(editor.getVirtualFile().getName()); - var dialog = new CustomPromptDialog(previousUserPrompt); - if (dialog.showAndGet()) { - previousUserPrompt = dialog.getUserPrompt(); - var message = new Message( - format("%s%n```%s%n%s%n```", previousUserPrompt, fileExtension, selectedText)); - message.setUserMessage(previousUserPrompt); - SwingUtilities.invokeLater(() -> - project.getService(ChatToolWindowContentManager.class).sendMessage(message)); - } - } - } - - public static class CustomPromptDialog extends DialogWrapper { - - private final JTextArea userPromptTextArea; - - public CustomPromptDialog(String previousUserPrompt) { - super(true); - this.userPromptTextArea = new JTextArea(previousUserPrompt); - this.userPromptTextArea.setCaretPosition(previousUserPrompt.length()); - setTitle("Custom Prompt"); - setSize(400, getRootPane().getPreferredSize().height); - init(); - } - - @Nullable - public JComponent getPreferredFocusedComponent() { - return userPromptTextArea; - } - - @Nullable - @Override - protected JComponent createCenterPanel() { - userPromptTextArea.setLineWrap(true); - userPromptTextArea.setWrapStyleWord(true); - userPromptTextArea.setMargin(JBUI.insets(5)); - UIUtil.addShiftEnterInputMap(userPromptTextArea, new AbstractAction() { - @Override - public void actionPerformed(ActionEvent e) { - clickDefaultButton(); - } - }); - - return FormBuilder.createFormBuilder() - .addComponent(UI.PanelFactory.panel(userPromptTextArea) - .withLabel("Prefix:") - .moveLabelOnTop() - .withComment("Example: Find bugs in the following code") - .createPanel()) - .getPanel(); - } - - public String getUserPrompt() { - return userPromptTextArea.getText(); - } - } -} diff --git a/src/main/java/ee/carlrobert/codegpt/actions/editor/EditorActionsUtil.java b/src/main/java/ee/carlrobert/codegpt/actions/editor/EditorActionsUtil.java deleted file mode 100644 index 39e5f8c6a..000000000 --- a/src/main/java/ee/carlrobert/codegpt/actions/editor/EditorActionsUtil.java +++ /dev/null @@ -1,95 +0,0 @@ -package ee.carlrobert.codegpt.actions.editor; - -import static java.lang.String.format; - -import com.intellij.openapi.actionSystem.ActionManager; -import com.intellij.openapi.actionSystem.AnAction; -import com.intellij.openapi.actionSystem.DefaultActionGroup; -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.ReferencedFile; -import ee.carlrobert.codegpt.actions.IncludeFilesInContextAction; -import ee.carlrobert.codegpt.conversations.message.Message; -import ee.carlrobert.codegpt.settings.configuration.ConfigurationSettings; -import ee.carlrobert.codegpt.toolwindow.chat.ChatToolWindowContentManager; -import ee.carlrobert.codegpt.util.file.FileUtil; -import java.util.Collection; -import java.util.LinkedHashMap; -import java.util.Map; -import java.util.stream.Stream; -import org.apache.commons.text.CaseUtils; - -public class EditorActionsUtil { - - public static final Map DEFAULT_ACTIONS = new LinkedHashMap<>(Map.of( - "Find Bugs", "Find bugs and output code with bugs " - + "fixed in the following code: {{selectedCode}}", - "Write Tests", "Write Tests for the selected code {{selectedCode}}", - "Explain", "Explain the selected code {{selectedCode}}", - "Refactor", "Refactor the selected code {{selectedCode}}", - "Optimize", "Optimize the selected code {{selectedCode}}")); - - public static final String[][] DEFAULT_ACTIONS_ARRAY = toArray(DEFAULT_ACTIONS); - - public static String[][] toArray(Map actionsMap) { - return actionsMap.entrySet() - .stream() - .map(entry -> new String[]{entry.getKey(), entry.getValue()}) - .toArray(String[][]::new); - } - - public static void refreshActions() { - AnAction actionGroup = - ActionManager.getInstance().getAction("action.editor.group.EditorActionGroup"); - if (actionGroup instanceof DefaultActionGroup group) { - group.removeAll(); - group.add(new AskAction()); - group.add(new CustomPromptAction()); - group.addSeparator(); - - var configuredActions = ConfigurationSettings.getCurrentState().getTableData(); - configuredActions.forEach((label, prompt) -> { - // using label as action description to prevent com.intellij.diagnostic.PluginException - // https://github.com/carlrobertoh/CodeGPT/issues/95 - var action = new BaseEditorAction(label, label) { - @Override - protected void actionPerformed(Project project, Editor editor, String selectedText) { - var fileExtension = FileUtil.getFileExtension(editor.getVirtualFile().getName()); - var message = new Message(prompt.replace( - "{{selectedCode}}", - format("%n```%s%n%s%n```", fileExtension, selectedText))); - message.setUserMessage(prompt.replace("{{selectedCode}}", "")); - var toolWindowContentManager = - project.getService(ChatToolWindowContentManager.class); - toolWindowContentManager.getToolWindow().show(); - - message.setReferencedFilePaths( - Stream.ofNullable(project.getUserData(CodeGPTKeys.SELECTED_FILES)) - .flatMap(Collection::stream) - .map(ReferencedFile::getFilePath) - .toList()); - toolWindowContentManager.sendMessage(message); - } - }; - group.add(action); - }); - group.addSeparator(); - group.add(new IncludeFilesInContextAction("action.includeFileInContext.title")); - } - } - - public static void registerAction(AnAction action) { - ActionManager actionManager = ActionManager.getInstance(); - var actionId = convertToId(action.getTemplateText()); - if (actionManager.getAction(actionId) == null) { - actionManager.registerAction(actionId, action, PluginId.getId("ee.carlrobert.chatgpt")); - } - } - - public static String convertToId(String label) { - return "codegpt." + CaseUtils.toCamelCase(label, true); - } -} diff --git a/src/main/java/ee/carlrobert/codegpt/actions/toolwindow/ClearChatWindowAction.java b/src/main/java/ee/carlrobert/codegpt/actions/toolwindow/ClearChatWindowAction.java deleted file mode 100644 index 04933ed9c..000000000 --- a/src/main/java/ee/carlrobert/codegpt/actions/toolwindow/ClearChatWindowAction.java +++ /dev/null @@ -1,46 +0,0 @@ -package ee.carlrobert.codegpt.actions.toolwindow; - -import com.intellij.icons.AllIcons; -import com.intellij.openapi.actionSystem.ActionUpdateThread; -import com.intellij.openapi.actionSystem.AnAction; -import com.intellij.openapi.actionSystem.AnActionEvent; -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 org.jetbrains.annotations.NotNull; - -public class ClearChatWindowAction extends AnAction { - - private final Runnable onActionPerformed; - - public ClearChatWindowAction(Runnable onActionPerformed) { - super("Clear Window", "Clears a chat window", AllIcons.General.Reset); - this.onActionPerformed = onActionPerformed; - EditorActionsUtil.registerAction(this); - } - - @Override - public void update(@NotNull AnActionEvent event) { - super.update(event); - var currentConversation = ConversationsState.getCurrentConversation(); - var isEnabled = currentConversation != null && !currentConversation.getMessages().isEmpty(); - event.getPresentation().setEnabled(isEnabled); - } - - @Override - public void actionPerformed(@NotNull AnActionEvent event) { - try { - onActionPerformed.run(); - } finally { - TelemetryAction.IDE_ACTION.createActionMessage() - .property("action", ActionType.CLEAR_CHAT_WINDOW.name()) - .send(); - } - } - - @Override - public @NotNull ActionUpdateThread getActionUpdateThread() { - return ActionUpdateThread.BGT; - } -} \ No newline at end of file diff --git a/src/main/java/ee/carlrobert/codegpt/actions/toolwindow/CreateNewConversationAction.java b/src/main/java/ee/carlrobert/codegpt/actions/toolwindow/CreateNewConversationAction.java deleted file mode 100644 index 76fb518fd..000000000 --- a/src/main/java/ee/carlrobert/codegpt/actions/toolwindow/CreateNewConversationAction.java +++ /dev/null @@ -1,34 +0,0 @@ -package ee.carlrobert.codegpt.actions.toolwindow; - -import com.intellij.icons.AllIcons; -import com.intellij.openapi.actionSystem.AnAction; -import com.intellij.openapi.actionSystem.AnActionEvent; -import ee.carlrobert.codegpt.actions.ActionType; -import ee.carlrobert.codegpt.actions.editor.EditorActionsUtil; -import ee.carlrobert.codegpt.telemetry.TelemetryAction; -import org.jetbrains.annotations.NotNull; - -public class CreateNewConversationAction extends AnAction { - - private final Runnable onCreate; - - public CreateNewConversationAction(Runnable onCreate) { - super("Create New Chat", "Create new chat", AllIcons.General.Add); - this.onCreate = onCreate; - EditorActionsUtil.registerAction(this); - } - - @Override - public void actionPerformed(@NotNull AnActionEvent event) { - try { - var project = event.getProject(); - if (project != null) { - onCreate.run(); - } - } finally { - TelemetryAction.IDE_ACTION.createActionMessage() - .property("action", ActionType.CREATE_NEW_CHAT.name()) - .send(); - } - } -} diff --git a/src/main/java/ee/carlrobert/codegpt/actions/toolwindow/DeleteAllConversationsAction.java b/src/main/java/ee/carlrobert/codegpt/actions/toolwindow/DeleteAllConversationsAction.java deleted file mode 100644 index 6490853a6..000000000 --- a/src/main/java/ee/carlrobert/codegpt/actions/toolwindow/DeleteAllConversationsAction.java +++ /dev/null @@ -1,62 +0,0 @@ -package ee.carlrobert.codegpt.actions.toolwindow; - -import static ee.carlrobert.codegpt.Icons.Default; - -import com.intellij.icons.AllIcons; -import com.intellij.openapi.actionSystem.ActionUpdateThread; -import com.intellij.openapi.actionSystem.AnAction; -import com.intellij.openapi.actionSystem.AnActionEvent; -import com.intellij.openapi.ui.Messages; -import ee.carlrobert.codegpt.actions.ActionType; -import ee.carlrobert.codegpt.actions.editor.EditorActionsUtil; -import ee.carlrobert.codegpt.conversations.ConversationService; -import ee.carlrobert.codegpt.telemetry.TelemetryAction; -import ee.carlrobert.codegpt.toolwindow.chat.ChatToolWindowContentManager; -import org.jetbrains.annotations.NotNull; - -public class DeleteAllConversationsAction extends AnAction { - - private final Runnable onRefresh; - - public DeleteAllConversationsAction(Runnable onRefresh) { - super("Delete All", "Delete all conversations", AllIcons.Actions.GC); - this.onRefresh = onRefresh; - EditorActionsUtil.registerAction(this); - } - - @Override - public void update(@NotNull AnActionEvent event) { - var project = event.getProject(); - if (project != null) { - var sortedConversations = ConversationService.getInstance().getSortedConversations(); - event.getPresentation().setEnabled(!sortedConversations.isEmpty()); - } - } - - @Override - public void actionPerformed(@NotNull AnActionEvent event) { - int answer = Messages.showYesNoDialog( - "Are you sure you want to delete all conversations?", - "Clear History", - Default); - if (answer == Messages.YES) { - var project = event.getProject(); - if (project != null) { - try { - ConversationService.getInstance().clearAll(); - project.getService(ChatToolWindowContentManager.class).resetAll(); - } finally { - TelemetryAction.IDE_ACTION.createActionMessage() - .property("action", ActionType.DELETE_ALL_CONVERSATIONS.name()) - .send(); - } - } - this.onRefresh.run(); - } - } - - @Override - public @NotNull ActionUpdateThread getActionUpdateThread() { - return ActionUpdateThread.BGT; - } -} diff --git a/src/main/java/ee/carlrobert/codegpt/actions/toolwindow/DeleteConversationAction.java b/src/main/java/ee/carlrobert/codegpt/actions/toolwindow/DeleteConversationAction.java deleted file mode 100644 index d56d3b594..000000000 --- a/src/main/java/ee/carlrobert/codegpt/actions/toolwindow/DeleteConversationAction.java +++ /dev/null @@ -1,47 +0,0 @@ -package ee.carlrobert.codegpt.actions.toolwindow; - -import com.intellij.icons.AllIcons; -import com.intellij.openapi.actionSystem.ActionUpdateThread; -import com.intellij.openapi.actionSystem.AnAction; -import com.intellij.openapi.actionSystem.AnActionEvent; -import com.intellij.openapi.ui.Messages; -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.ui.OverlayUtil; -import org.jetbrains.annotations.NotNull; - -public class DeleteConversationAction extends AnAction { - - private final Runnable onDelete; - - public DeleteConversationAction(Runnable onDelete) { - super("Delete Conversation", "Delete single conversation", AllIcons.Actions.GC); - this.onDelete = onDelete; - EditorActionsUtil.registerAction(this); - } - - @Override - public void update(@NotNull AnActionEvent event) { - event.getPresentation().setEnabled(ConversationsState.getCurrentConversation() != null); - } - - @Override - public void actionPerformed(@NotNull AnActionEvent event) { - if (OverlayUtil.showDeleteConversationDialog() == Messages.YES) { - var project = event.getProject(); - if (project != null) { - TelemetryAction.IDE_ACTION.createActionMessage() - .property("action", ActionType.DELETE_CONVERSATION.name()) - .send(); - onDelete.run(); - } - } - } - - @Override - public @NotNull ActionUpdateThread getActionUpdateThread() { - return ActionUpdateThread.EDT; - } -} diff --git a/src/main/java/ee/carlrobert/codegpt/actions/toolwindow/MoveAction.java b/src/main/java/ee/carlrobert/codegpt/actions/toolwindow/MoveAction.java deleted file mode 100644 index ab3ba2004..000000000 --- a/src/main/java/ee/carlrobert/codegpt/actions/toolwindow/MoveAction.java +++ /dev/null @@ -1,45 +0,0 @@ -package ee.carlrobert.codegpt.actions.toolwindow; - -import com.intellij.openapi.actionSystem.ActionUpdateThread; -import com.intellij.openapi.actionSystem.AnAction; -import com.intellij.openapi.actionSystem.AnActionEvent; -import com.intellij.openapi.project.Project; -import ee.carlrobert.codegpt.conversations.Conversation; -import ee.carlrobert.codegpt.conversations.ConversationsState; -import java.util.Optional; -import javax.swing.Icon; -import org.jetbrains.annotations.NotNull; - -public abstract class MoveAction extends AnAction { - - private final Runnable onRefresh; - - protected abstract Optional getConversation(@NotNull Project project); - - protected MoveAction(String text, String description, Icon icon, Runnable onRefresh) { - super(text, description, icon); - this.onRefresh = onRefresh; - } - - @Override - public void update(@NotNull AnActionEvent event) { - event.getPresentation().setEnabled(ConversationsState.getCurrentConversation() != null); - } - - @Override - public void actionPerformed(@NotNull AnActionEvent event) { - var project = event.getProject(); - if (project != null) { - getConversation(project) - .ifPresent(conversation -> { - ConversationsState.getInstance().setCurrentConversation(conversation); - onRefresh.run(); - }); - } - } - - @Override - public @NotNull ActionUpdateThread getActionUpdateThread() { - return ActionUpdateThread.BGT; - } -} diff --git a/src/main/java/ee/carlrobert/codegpt/actions/toolwindow/MoveDownAction.java b/src/main/java/ee/carlrobert/codegpt/actions/toolwindow/MoveDownAction.java deleted file mode 100644 index 9784cac88..000000000 --- a/src/main/java/ee/carlrobert/codegpt/actions/toolwindow/MoveDownAction.java +++ /dev/null @@ -1,22 +0,0 @@ -package ee.carlrobert.codegpt.actions.toolwindow; - -import com.intellij.icons.AllIcons; -import com.intellij.openapi.project.Project; -import ee.carlrobert.codegpt.actions.editor.EditorActionsUtil; -import ee.carlrobert.codegpt.conversations.Conversation; -import ee.carlrobert.codegpt.conversations.ConversationService; -import java.util.Optional; -import org.jetbrains.annotations.NotNull; - -public class MoveDownAction extends MoveAction { - - public MoveDownAction(Runnable onRefresh) { - super("Move Down", "Move Down", AllIcons.Actions.MoveDown, onRefresh); - EditorActionsUtil.registerAction(this); - } - - @Override - protected Optional getConversation(@NotNull Project project) { - return ConversationService.getInstance().getPreviousConversation(); - } -} diff --git a/src/main/java/ee/carlrobert/codegpt/actions/toolwindow/MoveUpAction.java b/src/main/java/ee/carlrobert/codegpt/actions/toolwindow/MoveUpAction.java deleted file mode 100644 index 1af04037e..000000000 --- a/src/main/java/ee/carlrobert/codegpt/actions/toolwindow/MoveUpAction.java +++ /dev/null @@ -1,22 +0,0 @@ -package ee.carlrobert.codegpt.actions.toolwindow; - -import com.intellij.icons.AllIcons; -import com.intellij.openapi.project.Project; -import ee.carlrobert.codegpt.actions.editor.EditorActionsUtil; -import ee.carlrobert.codegpt.conversations.Conversation; -import ee.carlrobert.codegpt.conversations.ConversationService; -import java.util.Optional; -import org.jetbrains.annotations.NotNull; - -public class MoveUpAction extends MoveAction { - - public MoveUpAction(Runnable onRefresh) { - super("Move Up", "Move up", AllIcons.Actions.MoveUp, onRefresh); - EditorActionsUtil.registerAction(this); - } - - @Override - protected Optional getConversation(@NotNull Project project) { - return ConversationService.getInstance().getNextConversation(); - } -} diff --git a/src/main/java/ee/carlrobert/codegpt/actions/toolwindow/OpenInEditorAction.java b/src/main/java/ee/carlrobert/codegpt/actions/toolwindow/OpenInEditorAction.java deleted file mode 100644 index 40bcb7cc8..000000000 --- a/src/main/java/ee/carlrobert/codegpt/actions/toolwindow/OpenInEditorAction.java +++ /dev/null @@ -1,69 +0,0 @@ -package ee.carlrobert.codegpt.actions.toolwindow; - -import static java.lang.String.format; -import static java.util.Objects.requireNonNull; - -import com.intellij.icons.AllIcons; -import com.intellij.openapi.actionSystem.ActionUpdateThread; -import com.intellij.openapi.actionSystem.AnAction; -import com.intellij.openapi.actionSystem.AnActionEvent; -import com.intellij.openapi.fileEditor.FileEditorManager; -import com.intellij.openapi.vfs.VirtualFile; -import com.intellij.openapi.wm.ToolWindowManager; -import com.intellij.testFramework.LightVirtualFile; -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 java.time.format.DateTimeFormatter; -import java.util.stream.Collectors; -import org.jetbrains.annotations.NotNull; - -public class OpenInEditorAction extends AnAction { - - public OpenInEditorAction() { - super("Open In Editor", "Open conversation in editor", AllIcons.Actions.SplitVertically); - EditorActionsUtil.registerAction(this); - } - - @Override - public void update(@NotNull AnActionEvent event) { - super.update(event); - var currentConversation = ConversationsState.getCurrentConversation(); - var isEnabled = currentConversation != null && !currentConversation.getMessages().isEmpty(); - event.getPresentation().setEnabled(isEnabled); - } - - @Override - public void actionPerformed(@NotNull AnActionEvent e) { - try { - var project = e.getProject(); - var currentConversation = ConversationsState.getCurrentConversation(); - if (project != null && currentConversation != null) { - var dateTimeStamp = currentConversation.getUpdatedOn() - .format(DateTimeFormatter.ofPattern("yyyyMMddHHmm")); - var fileName = format("%s_%s.md", currentConversation.getModel(), dateTimeStamp); - var fileContent = currentConversation - .getMessages() - .stream() - .map(it -> format("### User:%n%s%n### CodeGPT:%n%s%n", it.getPrompt(), - it.getResponse())) - .collect(Collectors.joining()); - VirtualFile file = new LightVirtualFile(fileName, fileContent); - FileEditorManager.getInstance(project).openFile(file, true); - var toolWindow = requireNonNull( - ToolWindowManager.getInstance(project).getToolWindow("CodeGPT")); - toolWindow.hide(); - } - } finally { - TelemetryAction.IDE_ACTION.createActionMessage() - .property("action", ActionType.OPEN_CONVERSATION_IN_EDITOR.name()) - .send(); - } - } - - @Override - public @NotNull ActionUpdateThread getActionUpdateThread() { - return ActionUpdateThread.BGT; - } -} diff --git a/src/main/java/ee/carlrobert/codegpt/actions/toolwindow/ReplaceCodeInMainEditorAction.java b/src/main/java/ee/carlrobert/codegpt/actions/toolwindow/ReplaceCodeInMainEditorAction.java deleted file mode 100644 index 22ff3a49f..000000000 --- a/src/main/java/ee/carlrobert/codegpt/actions/toolwindow/ReplaceCodeInMainEditorAction.java +++ /dev/null @@ -1,43 +0,0 @@ -package ee.carlrobert.codegpt.actions.toolwindow; - -import static java.util.Objects.requireNonNull; - -import com.intellij.icons.AllIcons; -import com.intellij.openapi.actionSystem.ActionUpdateThread; -import com.intellij.openapi.actionSystem.AnAction; -import com.intellij.openapi.actionSystem.AnActionEvent; -import com.intellij.openapi.actionSystem.PlatformDataKeys; -import ee.carlrobert.codegpt.actions.editor.EditorActionsUtil; -import ee.carlrobert.codegpt.util.EditorUtil; -import org.jetbrains.annotations.NotNull; - -public class ReplaceCodeInMainEditorAction extends AnAction { - - public ReplaceCodeInMainEditorAction() { - super("Replace in Main Editor", "Replace code in main editor", AllIcons.Actions.Replace); - EditorActionsUtil.registerAction(this); - } - - @Override - public void update(@NotNull AnActionEvent event) { - event.getPresentation().setEnabled( - EditorUtil.isMainEditorTextSelected(requireNonNull(event.getProject())) - && EditorUtil.hasSelection(event.getData(PlatformDataKeys.EDITOR))); - } - - @Override - public void actionPerformed(@NotNull AnActionEvent event) { - var project = event.getProject(); - var toolWindowEditor = event.getData(PlatformDataKeys.EDITOR); - if (project != null && toolWindowEditor != null) { - EditorUtil.replaceMainEditorSelection( - project, - requireNonNull(toolWindowEditor.getSelectionModel().getSelectedText())); - } - } - - @Override - public @NotNull ActionUpdateThread getActionUpdateThread() { - return ActionUpdateThread.EDT; - } -} diff --git a/src/main/java/ee/carlrobert/codegpt/toolwindow/chat/ChatToolWindowPanel.java b/src/main/java/ee/carlrobert/codegpt/toolwindow/chat/ChatToolWindowPanel.java index 75a53d730..694505b83 100644 --- a/src/main/java/ee/carlrobert/codegpt/toolwindow/chat/ChatToolWindowPanel.java +++ b/src/main/java/ee/carlrobert/codegpt/toolwindow/chat/ChatToolWindowPanel.java @@ -62,7 +62,7 @@ public ChatToolWindowTabbedPane getChatTabbedPane() { return tabbedPane; } - public void displaySelectedFilesNotification(List referencedFiles) { + public void displaySelectedFilesNotification(List referencedFiles) { if (referencedFiles.isEmpty()) { return; } diff --git a/src/main/java/ee/carlrobert/codegpt/toolwindow/chat/ui/textarea/TotalTokensPanel.java b/src/main/java/ee/carlrobert/codegpt/toolwindow/chat/ui/textarea/TotalTokensPanel.java index c9b0da5c2..2d5be0fdf 100644 --- a/src/main/java/ee/carlrobert/codegpt/toolwindow/chat/ui/textarea/TotalTokensPanel.java +++ b/src/main/java/ee/carlrobert/codegpt/toolwindow/chat/ui/textarea/TotalTokensPanel.java @@ -115,7 +115,7 @@ public void updateHighlightedTokens(String highlightedText) { update(); } - public void updateReferencedFilesTokens(List includedFiles) { + public void updateReferencedFilesTokens(List includedFiles) { totalTokensDetails.setReferencedFilesTokens(includedFiles.stream() .mapToInt(file -> encodingManager.countTokens(file.getFileContent())) .sum()); diff --git a/src/main/java/ee/carlrobert/codegpt/actions/ActionType.java b/src/main/kotlin/ee/carlrobert/codegpt/actions/ActionType.kt similarity index 80% rename from src/main/java/ee/carlrobert/codegpt/actions/ActionType.java rename to src/main/kotlin/ee/carlrobert/codegpt/actions/ActionType.kt index 0ba9b3131..4f391052a 100644 --- a/src/main/java/ee/carlrobert/codegpt/actions/ActionType.java +++ b/src/main/kotlin/ee/carlrobert/codegpt/actions/ActionType.kt @@ -1,7 +1,6 @@ -package ee.carlrobert.codegpt.actions; - -public enum ActionType { +package ee.carlrobert.codegpt.actions +enum class ActionType { CLEAR_CHAT_WINDOW, CREATE_NEW_CHAT, DELETE_ALL_CONVERSATIONS, diff --git a/src/main/kotlin/ee/carlrobert/codegpt/actions/CodeCompletionFeatureToggleActions.kt b/src/main/kotlin/ee/carlrobert/codegpt/actions/CodeCompletionFeatureToggleActions.kt index 6a0b29d00..fc1e94559 100644 --- a/src/main/kotlin/ee/carlrobert/codegpt/actions/CodeCompletionFeatureToggleActions.kt +++ b/src/main/kotlin/ee/carlrobert/codegpt/actions/CodeCompletionFeatureToggleActions.kt @@ -19,8 +19,8 @@ abstract class CodeCompletionFeatureToggleActions( override fun actionPerformed(e: AnActionEvent) { when (GeneralSettings.getCurrentState().selectedService) { - CODEGPT -> - service().state.codeCompletionSettings.codeCompletionsEnabled + CODEGPT -> service().state.codeCompletionSettings + .codeCompletionsEnabled = enableFeatureAction OPENAI -> OpenAISettings.getCurrentState().isCodeCompletionsEnabled = enableFeatureAction @@ -29,8 +29,8 @@ abstract class CodeCompletionFeatureToggleActions( LlamaSettings.getCurrentState().isCodeCompletionsEnabled = enableFeatureAction OLLAMA -> service().state.codeCompletionsEnabled = enableFeatureAction - CUSTOM_OPENAI -> service().state - .codeCompletionSettings + + CUSTOM_OPENAI -> service().state.codeCompletionSettings .codeCompletionsEnabled = enableFeatureAction ANTHROPIC, @@ -45,7 +45,7 @@ abstract class CodeCompletionFeatureToggleActions( override fun update(e: AnActionEvent) { val selectedService = GeneralSettings.getCurrentState().selectedService val codeCompletionEnabled = - service().isCodeCompletionsEnabled(selectedService) + e.project?.service()?.isCodeCompletionsEnabled(selectedService) ?: false e.presentation.isVisible = codeCompletionEnabled != enableFeatureAction e.presentation.isEnabled = when (selectedService) { CODEGPT, diff --git a/src/main/kotlin/ee/carlrobert/codegpt/actions/GenerateGitCommitMessageAction.kt b/src/main/kotlin/ee/carlrobert/codegpt/actions/GenerateGitCommitMessageAction.kt new file mode 100644 index 000000000..95afb8222 --- /dev/null +++ b/src/main/kotlin/ee/carlrobert/codegpt/actions/GenerateGitCommitMessageAction.kt @@ -0,0 +1,196 @@ +package ee.carlrobert.codegpt.actions + +import com.intellij.notification.Notification +import com.intellij.notification.NotificationType +import com.intellij.notification.Notifications +import com.intellij.openapi.actionSystem.ActionUpdateThread +import com.intellij.openapi.actionSystem.AnAction +import com.intellij.openapi.actionSystem.AnActionEvent +import com.intellij.openapi.application.ApplicationManager +import com.intellij.openapi.command.WriteCommandAction +import com.intellij.openapi.editor.Document +import com.intellij.openapi.editor.Editor +import com.intellij.openapi.editor.ex.EditorEx +import com.intellij.openapi.project.Project +import com.intellij.openapi.ui.Messages +import com.intellij.openapi.vcs.VcsDataKeys +import com.intellij.openapi.vcs.ui.CommitMessage +import com.intellij.vcs.commit.CommitWorkflowUi +import ee.carlrobert.codegpt.CodeGPTBundle +import ee.carlrobert.codegpt.EncodingManager +import ee.carlrobert.codegpt.Icons +import ee.carlrobert.codegpt.completions.CompletionRequestService +import ee.carlrobert.codegpt.completions.CompletionRequestService.isRequestAllowed +import ee.carlrobert.codegpt.settings.GeneralSettings +import ee.carlrobert.codegpt.settings.configuration.CommitMessageTemplate +import ee.carlrobert.codegpt.settings.service.ServiceType +import ee.carlrobert.codegpt.ui.OverlayUtil +import ee.carlrobert.llm.client.openai.completion.ErrorDetails +import ee.carlrobert.llm.completion.CompletionEventListener +import okhttp3.sse.EventSource +import java.io.BufferedReader +import java.io.File +import java.io.IOException +import java.io.InputStreamReader +import java.nio.file.Files +import java.nio.file.Path +import java.util.Objects +import java.util.Optional +import java.util.stream.Collectors.joining + +class GenerateGitCommitMessageAction : AnAction( + CodeGPTBundle.get("action.generateCommitMessage.title"), + CodeGPTBundle.get("action.generateCommitMessage.description"), + Icons.Sparkle) { + + private val encodingManager: EncodingManager = EncodingManager.getInstance() + + override fun update(event: AnActionEvent) { + val commitWorkflowUi = event.getData(VcsDataKeys.COMMIT_WORKFLOW_UI) + val selectedService = GeneralSettings.getCurrentState().selectedService + if (selectedService == ServiceType.YOU || commitWorkflowUi == null) { + event.presentation.isVisible = false + return + } + + val callAllowed = isRequestAllowed(GeneralSettings.getCurrentState().selectedService) + event.presentation.isEnabled = callAllowed + && CommitWorkflowChanges(commitWorkflowUi).isFilesSelected + event.presentation.text = CodeGPTBundle.get( + if (callAllowed) + "action.generateCommitMessage.title" + else "action.generateCommitMessage.missingCredentials") + } + + override fun actionPerformed(event: AnActionEvent) { + val project = event.project ?: return + project.basePath ?: return + + val gitDiff = getGitDiff(event, project) + val tokenCount = encodingManager.countTokens(gitDiff) + if (tokenCount > MAX_TOKEN_COUNT_WARNING + && OverlayUtil.showTokenSoftLimitWarningDialog(tokenCount) != Messages.OK) { + return + } + + val editor = getCommitMessageEditor(event) as? EditorEx ?: return + editor.setCaretVisible(false) + CompletionRequestService.getInstance().generateCommitMessageAsync( + project.getService(CommitMessageTemplate::class.java).getSystemPrompt(), + gitDiff, + getEventListener(project, editor.document)) + } + + override fun getActionUpdateThread(): ActionUpdateThread { + return ActionUpdateThread.EDT + } + + private fun getEventListener(project: Project, document: Document): CompletionEventListener { + return object : CompletionEventListener { + private val messageBuilder = StringBuilder() + + override fun onMessage(message: String?, eventSource: EventSource) { + messageBuilder.append(message) + val application = ApplicationManager.getApplication() + application.invokeLater { + application.runWriteAction { + WriteCommandAction.runWriteCommandAction(project) { + document.setText(messageBuilder) + } + } + } + } + + override fun onError(error: ErrorDetails, ex: Throwable) { + Notifications.Bus.notify(Notification( + "CodeGPT Notification Group", + "CodeGPT", + error.message, + NotificationType.ERROR)) + } + } + } + + private fun getCommitMessageEditor(event: AnActionEvent): Editor? { + return (event.getData(VcsDataKeys.COMMIT_MESSAGE_CONTROL) as? CommitMessage)?.editorField?.editor + } + + private fun getGitDiff(event: AnActionEvent, project: Project): String { + val commitWorkflowUi = Optional.ofNullable(event.getData(VcsDataKeys.COMMIT_WORKFLOW_UI)) + .orElseThrow { IllegalStateException("Could not retrieve commit workflow ui.") } + val changes = CommitWorkflowChanges(commitWorkflowUi) + val projectBasePath = project.basePath + val gitDiff = getGitDiff(projectBasePath, changes.includedVersionedFilePaths, false) + val stagedGitDiff = getGitDiff(projectBasePath, changes.includedVersionedFilePaths, true) + val newFilesContent = getNewFilesDiff(projectBasePath, changes.includedUnversionedFilePaths) + + return mapOf( + "Git diff" to gitDiff, + "Staged git diff" to stagedGitDiff, + "New files" to newFilesContent) + .entries.stream() + .filter { !it.value.isNullOrBlank() } + .map { "%s:%n%s".formatted(it.key, it.value) } + .collect(joining("\n\n")) + } + + private fun getGitDiff(projectPath: String?, filePaths: List, cached: Boolean): String { + if (filePaths.isEmpty()) { + return "" + } + + val process = createGitDiffProcess(projectPath, filePaths, cached) + return BufferedReader(InputStreamReader(process.inputStream)).lines().collect(joining("\n")) + } + + private fun getNewFilesDiff(projectPath: String?, filePaths: List): String? { + return filePaths.stream() + .map { pathString -> + val filePath = Path.of(pathString) + val relativePath = Path.of(projectPath).relativize(filePath) + try { + return@map """ + New file '$relativePath' content: + ${Files.readString(filePath)} + """.trimIndent() + } catch (ignored: IOException) { + return@map null + } + } + .filter(Objects::nonNull) + .collect(joining("\n")) + } + + private fun createGitDiffProcess(projectPath: String?, filePaths: List, cached: Boolean): Process { + val command = mutableListOf("git", "diff") + if (cached) { + command.add("--cached") + } + command.addAll(filePaths) + + val processBuilder = ProcessBuilder(command) + processBuilder.directory(File(projectPath)) + try { + return processBuilder.start() + } catch (ex: IOException) { + throw RuntimeException("Unable to start git diff process", ex) + } + } + + internal class CommitWorkflowChanges(commitWorkflowUi: CommitWorkflowUi) { + val includedVersionedFilePaths: List = commitWorkflowUi.getIncludedChanges().stream() + .map { it.virtualFile } + .filter(Objects::nonNull) + .map { it!!.path } + .toList() + val includedUnversionedFilePaths: List = + commitWorkflowUi.getIncludedUnversionedFiles().stream().map { it.path }.toList() + + val isFilesSelected = + includedVersionedFilePaths.isNotEmpty() || includedUnversionedFilePaths.isNotEmpty() + } + + companion object { + const val MAX_TOKEN_COUNT_WARNING: Int = 4096 + } +} diff --git a/src/main/kotlin/ee/carlrobert/codegpt/actions/IncludeFilesInContextAction.kt b/src/main/kotlin/ee/carlrobert/codegpt/actions/IncludeFilesInContextAction.kt new file mode 100644 index 000000000..3f51d207d --- /dev/null +++ b/src/main/kotlin/ee/carlrobert/codegpt/actions/IncludeFilesInContextAction.kt @@ -0,0 +1,205 @@ +package ee.carlrobert.codegpt.actions + +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.ui.DialogWrapper +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 com.intellij.util.ui.UI.PanelFactory +import ee.carlrobert.codegpt.CodeGPTBundle +import ee.carlrobert.codegpt.CodeGPTKeys +import ee.carlrobert.codegpt.EncodingManager +import ee.carlrobert.codegpt.ReferencedFile +import ee.carlrobert.codegpt.settings.IncludedFilesSettings +import ee.carlrobert.codegpt.settings.IncludedFilesSettingsState +import ee.carlrobert.codegpt.ui.UIUtil +import ee.carlrobert.codegpt.ui.checkbox.FileCheckboxTree +import ee.carlrobert.codegpt.ui.checkbox.VirtualFileCheckboxTree +import ee.carlrobert.codegpt.util.file.FileUtil.convertLongValue +import java.awt.Dimension +import java.io.IOException +import java.nio.file.Files +import java.nio.file.Paths +import javax.swing.JButton +import javax.swing.JComponent +import javax.swing.SwingUtilities + +class IncludeFilesInContextAction @JvmOverloads constructor( + customTitleKey: String? = "action.includeFilesInContext.title") : + AnAction(CodeGPTBundle.get(customTitleKey!!)) { + + override fun actionPerformed(e: AnActionEvent) { + val project = e.project ?: return + + val checkboxTree = getCheckboxTree(e.dataContext) ?: throw RuntimeException("Could not obtain file tree") + + val totalTokensLabel = TotalTokensLabel(checkboxTree.referencedFiles) + checkboxTree.addCheckboxTreeListener(object : CheckboxTreeListener { + override fun nodeStateChanged(node: CheckedTreeNode) { + totalTokensLabel.updateState(node) + } + }) + + val includedFilesSettings = IncludedFilesSettings.getCurrentState() + val promptTemplateTextArea = UIUtil.createTextArea(includedFilesSettings.promptTemplate) + val repeatableContextTextArea = UIUtil.createTextArea(includedFilesSettings.repeatableContext) + val show = showMultiFilePromptDialog( + project, + promptTemplateTextArea, + repeatableContextTextArea, + totalTokensLabel, + checkboxTree) + if (show == DialogWrapper.OK_EXIT_CODE) { + project.putUserData(CodeGPTKeys.SELECTED_FILES, checkboxTree.referencedFiles) + project.messageBus + .syncPublisher(IncludeFilesInContextNotifier.FILES_INCLUDED_IN_CONTEXT_TOPIC) + .filesIncluded(checkboxTree.referencedFiles) + includedFilesSettings.promptTemplate = promptTemplateTextArea.text + includedFilesSettings.repeatableContext = repeatableContextTextArea.text + } + } + + private fun getCheckboxTree(dataContext: DataContext): FileCheckboxTree? { + return CommonDataKeys.VIRTUAL_FILE_ARRAY.getData(dataContext)?.let(::VirtualFileCheckboxTree) + } + + private class TotalTokensLabel(referencedFiles: List) : JBLabel() { + private var fileCount = referencedFiles.size + private var totalTokens: Int + + init { + totalTokens = calculateTotalTokens(referencedFiles) + updateText() + } + + fun updateState(checkedNode: CheckedTreeNode) { + val fileContent = getNodeFileContent(checkedNode) ?: return + val tokenCount = encodingManager.countTokens(fileContent) + if (checkedNode.isChecked) { + totalTokens += tokenCount + fileCount++ + } else { + totalTokens -= tokenCount + fileCount-- + } + SwingUtilities.invokeLater(::updateText) + } + + private fun getNodeFileContent(checkedNode: CheckedTreeNode): String? { + val userObject = checkedNode.userObject + if (userObject is PsiElement) { + return userObject.containingFile?.virtualFile?.let { getVirtualFileContent(it) } + } + return (userObject as? VirtualFile)?.let { getVirtualFileContent(it) } + } + + private fun getVirtualFileContent(virtualFile: VirtualFile): String? { + try { + return String(Files.readAllBytes(Paths.get(virtualFile.path))) + } catch (ex: IOException) { + LOG.error(ex) + } + return null + } + + private fun updateText() { + text = String.format( + "%d %s totaling %s tokens", + fileCount, + if (fileCount == 1) "file" else "files", + convertLongValue(totalTokens.toLong())) + } + + private fun calculateTotalTokens(referencedFiles: List): Int { + return referencedFiles.stream().mapToInt { encodingManager.countTokens(it.fileContent) }.sum() + } + + companion object { + private val encodingManager + get() = EncodingManager.getInstance() + } + } + + companion object { + private val LOG = Logger.getInstance(IncludeFilesInContextAction::class.java) + + private fun showMultiFilePromptDialog( + project: Project, + promptTemplateTextArea: JBTextArea, + repeatableContextTextArea: JBTextArea, + totalTokensLabel: JBLabel, + component: JComponent + ): Int { + val dialogBuilder = DialogBuilder(project) + dialogBuilder.setTitle(CodeGPTBundle.get("action.includeFilesInContext.dialog.title")) + dialogBuilder.setActionDescriptors() + val fileTreeScrollPane = ScrollPaneFactory.createScrollPane(component) + fileTreeScrollPane.preferredSize = Dimension(480, component.preferredSize.height + 48) + dialogBuilder.setNorthPanel( + FormBuilder.createFormBuilder() + .addLabeledComponent( + CodeGPTBundle.get("shared.promptTemplate"), + PanelFactory.panel(promptTemplateTextArea).withComment( + "

The template that will be used to create the final prompt. " + + "The {REPEATABLE_CONTEXT} placeholder must be included " + + "to correctly map the file contents.

" + ) + .createPanel(), + true + ) + .addVerticalGap(4) + .addLabeledComponent( + CodeGPTBundle.get("action.includeFilesInContext.dialog.repeatableContext.label"), + PanelFactory.panel(repeatableContextTextArea).withComment( + "

The context that will be repeated for each included file. " + + "Acceptable placeholders include {FILE_PATH} and " + + "{FILE_CONTENT}.

" + ) + .createPanel(), + true + ) + .addComponent(JBUI.Panels.simplePanel() + .addToRight(getRestoreButton(promptTemplateTextArea, repeatableContextTextArea)) + ) + .addVerticalGap(16) + .addComponent(JBLabel(CodeGPTBundle.get("action.includeFilesInContext.dialog.description")) + .setCopyable(false) + .setAllowAutoWrapping(true) + ) + .addVerticalGap(4) + .addLabeledComponent(totalTokensLabel, fileTreeScrollPane, true) + .addVerticalGap(16) + .panel + ) + dialogBuilder.addOkAction().setText(CodeGPTBundle.get("dialog.continue")) + dialogBuilder.addCancelAction() + return dialogBuilder.show() + } + + private fun getRestoreButton(promptTemplateTextArea: JBTextArea, + repeatableContextTextArea: JBTextArea): JButton { + val restoreButton = JButton( + CodeGPTBundle.get("action.includeFilesInContext.dialog.restoreToDefaults.label")) + restoreButton.addActionListener { _ -> + val includedFilesSettings = IncludedFilesSettings.getCurrentState() + includedFilesSettings.promptTemplate = IncludedFilesSettingsState.DEFAULT_PROMPT_TEMPLATE + includedFilesSettings.repeatableContext = IncludedFilesSettingsState.DEFAULT_REPEATABLE_CONTEXT + promptTemplateTextArea.text = IncludedFilesSettingsState.DEFAULT_PROMPT_TEMPLATE + repeatableContextTextArea.text = IncludedFilesSettingsState.DEFAULT_REPEATABLE_CONTEXT + } + return restoreButton + } + } +} diff --git a/src/main/kotlin/ee/carlrobert/codegpt/actions/IncludeFilesInContextNotifier.kt b/src/main/kotlin/ee/carlrobert/codegpt/actions/IncludeFilesInContextNotifier.kt new file mode 100644 index 000000000..4ba9c6051 --- /dev/null +++ b/src/main/kotlin/ee/carlrobert/codegpt/actions/IncludeFilesInContextNotifier.kt @@ -0,0 +1,14 @@ +package ee.carlrobert.codegpt.actions + +import com.intellij.util.messages.Topic +import ee.carlrobert.codegpt.ReferencedFile + +fun interface IncludeFilesInContextNotifier { + fun filesIncluded(includedFiles: List) + + companion object { + @JvmField + val FILES_INCLUDED_IN_CONTEXT_TOPIC: Topic = + Topic.create("filesIncludedInContext", IncludeFilesInContextNotifier::class.java) + } +} diff --git a/src/main/kotlin/ee/carlrobert/codegpt/actions/OpenSettingsAction.kt b/src/main/kotlin/ee/carlrobert/codegpt/actions/OpenSettingsAction.kt new file mode 100644 index 000000000..ff91b6227 --- /dev/null +++ b/src/main/kotlin/ee/carlrobert/codegpt/actions/OpenSettingsAction.kt @@ -0,0 +1,17 @@ +package ee.carlrobert.codegpt.actions + +import com.intellij.icons.AllIcons +import com.intellij.openapi.actionSystem.AnAction +import com.intellij.openapi.actionSystem.AnActionEvent +import com.intellij.openapi.options.ShowSettingsUtil +import ee.carlrobert.codegpt.CodeGPTBundle +import ee.carlrobert.codegpt.settings.service.ServiceConfigurable + +class OpenSettingsAction : AnAction( + CodeGPTBundle.get("action.openSettings.title"), + CodeGPTBundle.get("action.openSettings.description"), + AllIcons.General.Settings) { + override fun actionPerformed(e: AnActionEvent) { + ShowSettingsUtil.getInstance().showSettingsDialog(e.project, ServiceConfigurable::class.java) + } +} diff --git a/src/main/kotlin/ee/carlrobert/codegpt/actions/TrackableAction.kt b/src/main/kotlin/ee/carlrobert/codegpt/actions/TrackableAction.kt new file mode 100644 index 000000000..257bc4461 --- /dev/null +++ b/src/main/kotlin/ee/carlrobert/codegpt/actions/TrackableAction.kt @@ -0,0 +1,37 @@ +package ee.carlrobert.codegpt.actions + +import com.intellij.openapi.actionSystem.AnAction +import com.intellij.openapi.actionSystem.AnActionEvent +import com.intellij.openapi.editor.Editor +import ee.carlrobert.codegpt.telemetry.TelemetryAction +import javax.swing.Icon + +abstract class TrackableAction( + @JvmField protected val editor: Editor, + text: String?, + description: String?, + icon: Icon?, + private val actionType: ActionType +) : AnAction(text, description, icon) { + + abstract fun handleAction(e: AnActionEvent) + + override fun actionPerformed(e: AnActionEvent) { + try { + handleAction(e) + TelemetryAction.IDE_ACTION + .createActionMessage() + .property("group", null) + .property("action", actionType.name) + .send() + } catch (ex: Exception) { + TelemetryAction.IDE_ACTION_ERROR + .createActionMessage() + .property("group", null) + .property("action", actionType.name) + .error(ex) + .send() + throw ex + } + } +} diff --git a/src/main/kotlin/ee/carlrobert/codegpt/actions/editor/AskAction.kt b/src/main/kotlin/ee/carlrobert/codegpt/actions/editor/AskAction.kt new file mode 100644 index 000000000..6a41a17c7 --- /dev/null +++ b/src/main/kotlin/ee/carlrobert/codegpt/actions/editor/AskAction.kt @@ -0,0 +1,29 @@ +package ee.carlrobert.codegpt.actions.editor + +import com.intellij.openapi.actionSystem.ActionUpdateThread +import com.intellij.openapi.actionSystem.AnAction +import com.intellij.openapi.actionSystem.AnActionEvent +import ee.carlrobert.codegpt.Icons +import ee.carlrobert.codegpt.conversations.ConversationsState +import ee.carlrobert.codegpt.toolwindow.chat.ChatToolWindowContentManager + +class AskAction : AnAction("New Chat", "Chat with CodeGPT", Icons.Sparkle) { + init { + EditorActionsUtil.registerAction(this) + } + + override fun update(event: AnActionEvent) { + event.presentation.isEnabled = event.project != null + } + + override fun actionPerformed(event: AnActionEvent) { + val project = event.project ?: return + ConversationsState.getInstance().setCurrentConversation(null) + project.getService(ChatToolWindowContentManager::class.java) + .createNewTabPanel()?.displayLandingView() + } + + override fun getActionUpdateThread(): ActionUpdateThread { + return ActionUpdateThread.BGT + } +} diff --git a/src/main/kotlin/ee/carlrobert/codegpt/actions/editor/BaseEditorAction.kt b/src/main/kotlin/ee/carlrobert/codegpt/actions/editor/BaseEditorAction.kt new file mode 100644 index 000000000..c1305b199 --- /dev/null +++ b/src/main/kotlin/ee/carlrobert/codegpt/actions/editor/BaseEditorAction.kt @@ -0,0 +1,37 @@ +package ee.carlrobert.codegpt.actions.editor + +import com.intellij.openapi.actionSystem.ActionUpdateThread +import com.intellij.openapi.actionSystem.AnAction +import com.intellij.openapi.actionSystem.AnActionEvent +import com.intellij.openapi.actionSystem.CommonDataKeys +import com.intellij.openapi.editor.Editor +import com.intellij.openapi.project.Project +import com.intellij.openapi.util.NlsActions +import javax.swing.Icon + +abstract class BaseEditorAction @JvmOverloads constructor( + text: @NlsActions.ActionText String?, + description: @NlsActions.ActionDescription String?, + icon: Icon? = null +) : AnAction(text, description, icon) { + init { + EditorActionsUtil.registerAction(this) + } + + protected abstract fun actionPerformed(project: Project, editor: Editor, selectedText: String?) + + override fun actionPerformed(event: AnActionEvent) { + val project = event.project ?: return + val editor = event.getData(CommonDataKeys.EDITOR) ?: return + actionPerformed(project, editor, editor.selectionModel.selectedText) + } + + override fun update(event: AnActionEvent) { + event.presentation.isEnabled = event.project != null + && event.getData(CommonDataKeys.EDITOR)?.selectionModel?.selectedText != null + } + + override fun getActionUpdateThread(): ActionUpdateThread { + return ActionUpdateThread.EDT + } +} diff --git a/src/main/kotlin/ee/carlrobert/codegpt/actions/editor/CustomPromptAction.kt b/src/main/kotlin/ee/carlrobert/codegpt/actions/editor/CustomPromptAction.kt new file mode 100644 index 000000000..44e2a7160 --- /dev/null +++ b/src/main/kotlin/ee/carlrobert/codegpt/actions/editor/CustomPromptAction.kt @@ -0,0 +1,82 @@ +package ee.carlrobert.codegpt.actions.editor + +import com.intellij.icons.AllIcons +import com.intellij.openapi.editor.Editor +import com.intellij.openapi.project.Project +import com.intellij.openapi.ui.DialogWrapper +import com.intellij.util.ui.FormBuilder +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.ChatToolWindowContentManager +import ee.carlrobert.codegpt.ui.UIUtil +import ee.carlrobert.codegpt.util.file.FileUtil.getFileExtension +import java.awt.event.ActionEvent +import javax.swing.AbstractAction +import javax.swing.JComponent +import javax.swing.JTextArea +import javax.swing.SwingUtilities + +class CustomPromptAction internal constructor() : + BaseEditorAction("Custom Prompt", "Custom prompt description", AllIcons.Actions.Run_anything) { + init { + EditorActionsUtil.registerAction(this) + } + + override fun actionPerformed(project: Project, editor: Editor, selectedText: String?) { + if (!selectedText.isNullOrBlank()) { + val fileExtension = getFileExtension(editor.virtualFile.name) + val dialog = CustomPromptDialog(previousUserPrompt) + if (dialog.showAndGet()) { + previousUserPrompt = dialog.userPrompt + val message = Message( + String.format("%s%n```%s%n%s%n```", previousUserPrompt, fileExtension, selectedText)) + message.userMessage = previousUserPrompt + SwingUtilities.invokeLater { + project.getService(ChatToolWindowContentManager::class.java).sendMessage(message) + } + } + } + } + + class CustomPromptDialog(previousUserPrompt: String) : DialogWrapper(true) { + private val userPromptTextArea = JTextArea(previousUserPrompt) + + init { + userPromptTextArea.caretPosition = previousUserPrompt.length + title = "Custom Prompt" + setSize(400, rootPane.preferredSize.height) + init() + } + + override fun getPreferredFocusedComponent(): JComponent? { + return userPromptTextArea + } + + override fun createCenterPanel(): JComponent? { + userPromptTextArea.lineWrap = true + userPromptTextArea.wrapStyleWord = true + userPromptTextArea.margin = JBUI.insets(5) + UIUtil.addShiftEnterInputMap(userPromptTextArea, object : AbstractAction() { + override fun actionPerformed(e: ActionEvent) { + clickDefaultButton() + } + }) + + return FormBuilder.createFormBuilder() + .addComponent(UI.PanelFactory.panel(userPromptTextArea) + .withLabel("Prefix:") + .moveLabelOnTop() + .withComment("Example: Find bugs in the following code") + .createPanel()) + .panel + } + + val userPrompt: String + get() = userPromptTextArea.text + } + + companion object { + private var previousUserPrompt = "" + } +} diff --git a/src/main/kotlin/ee/carlrobert/codegpt/actions/editor/EditorActionsUtil.kt b/src/main/kotlin/ee/carlrobert/codegpt/actions/editor/EditorActionsUtil.kt new file mode 100644 index 000000000..9b0402386 --- /dev/null +++ b/src/main/kotlin/ee/carlrobert/codegpt/actions/editor/EditorActionsUtil.kt @@ -0,0 +1,88 @@ +package ee.carlrobert.codegpt.actions.editor + +import com.intellij.openapi.actionSystem.ActionManager +import com.intellij.openapi.actionSystem.AnAction +import com.intellij.openapi.actionSystem.DefaultActionGroup +import com.intellij.openapi.editor.Editor +import com.intellij.openapi.extensions.PluginId +import com.intellij.openapi.project.Project +import ee.carlrobert.codegpt.CodeGPTKeys +import ee.carlrobert.codegpt.ReferencedFile +import ee.carlrobert.codegpt.actions.IncludeFilesInContextAction +import ee.carlrobert.codegpt.conversations.message.Message +import ee.carlrobert.codegpt.settings.configuration.ConfigurationSettings +import ee.carlrobert.codegpt.toolwindow.chat.ChatToolWindowContentManager +import ee.carlrobert.codegpt.util.file.FileUtil.getFileExtension +import org.apache.commons.text.CaseUtils +import java.util.stream.Stream + +object EditorActionsUtil { + @JvmField + val DEFAULT_ACTIONS: Map = LinkedHashMap(mapOf( + "Find Bugs" to "Find bugs and output code with bugs fixed in the following code: {{selectedCode}}", + "Write Tests" to "Write Tests for the selected code {{selectedCode}}", + "Explain" to "Explain the selected code {{selectedCode}}", + "Refactor" to "Refactor the selected code {{selectedCode}}", + "Optimize" to "Optimize the selected code {{selectedCode}}")) + + @JvmField + val DEFAULT_ACTIONS_ARRAY = toArray(DEFAULT_ACTIONS) + + @JvmStatic + fun toArray(actionsMap: Map): Array> { + return actionsMap.entries.stream() + .map { arrayOf(it.key, it.value) } + .toArray { arrayOfNulls>(it) } + } + + @JvmStatic + fun refreshActions() { + val actionGroup = ActionManager.getInstance() + .getAction("action.editor.group.EditorActionGroup") as? DefaultActionGroup ?: return + actionGroup.removeAll() + actionGroup.add(AskAction()) + actionGroup.add(CustomPromptAction()) + actionGroup.addSeparator() + + val configuredActions = ConfigurationSettings.getCurrentState().tableData + configuredActions.forEach { (label: String?, prompt: String) -> + // using label as action description to prevent com.intellij.diagnostic.PluginException + // https://github.com/carlrobertoh/CodeGPT/issues/95 + val action: BaseEditorAction = object : BaseEditorAction(label, label) { + override fun actionPerformed(project: Project, editor: Editor, selectedText: String?) { + val fileExtension = getFileExtension(editor.virtualFile.name) + val message = Message(prompt.replace( + "{{selectedCode}}", + String.format("%n```%s%n%s%n```", fileExtension, selectedText))) + message.userMessage = prompt.replace("{{selectedCode}}", "") + val toolWindowContentManager = + project.getService(ChatToolWindowContentManager::class.java) + toolWindowContentManager.toolWindow.show() + + message.referencedFilePaths = Stream.ofNullable(project.getUserData(CodeGPTKeys.SELECTED_FILES)) + .flatMap(MutableList::stream) + .map { it.filePath } + .toList() + toolWindowContentManager.sendMessage(message) + } + } + actionGroup.add(action) + } + actionGroup.addSeparator() + actionGroup.add(IncludeFilesInContextAction("action.includeFileInContext.title")) + } + + @JvmStatic + fun registerAction(action: AnAction) { + val actionManager = ActionManager.getInstance() + val actionId = convertToId(action.templateText) + if (actionManager.getAction(actionId) == null) { + actionManager.registerAction(actionId, action, PluginId.getId("ee.carlrobert.chatgpt")) + } + } + + @JvmStatic + fun convertToId(label: String?): String { + return "codegpt." + CaseUtils.toCamelCase(label, true) + } +} diff --git a/src/main/kotlin/ee/carlrobert/codegpt/actions/toolwindow/ClearChatWindowAction.kt b/src/main/kotlin/ee/carlrobert/codegpt/actions/toolwindow/ClearChatWindowAction.kt new file mode 100644 index 000000000..12e04248d --- /dev/null +++ b/src/main/kotlin/ee/carlrobert/codegpt/actions/toolwindow/ClearChatWindowAction.kt @@ -0,0 +1,36 @@ +package ee.carlrobert.codegpt.actions.toolwindow + +import com.intellij.icons.AllIcons +import com.intellij.openapi.actionSystem.ActionUpdateThread +import com.intellij.openapi.actionSystem.AnAction +import com.intellij.openapi.actionSystem.AnActionEvent +import ee.carlrobert.codegpt.actions.ActionType +import ee.carlrobert.codegpt.actions.editor.EditorActionsUtil.registerAction +import ee.carlrobert.codegpt.conversations.ConversationsState.getCurrentConversation +import ee.carlrobert.codegpt.telemetry.TelemetryAction + +class ClearChatWindowAction(private val onActionPerformed: Runnable) : + AnAction("Clear Window", "Clears a chat window", AllIcons.General.Reset) { + init { + registerAction(this) + } + + override fun update(event: AnActionEvent) { + super.update(event) + event.presentation.isEnabled = getCurrentConversation()?.messages?.isNotEmpty() == true + } + + override fun actionPerformed(event: AnActionEvent) { + try { + onActionPerformed.run() + } finally { + TelemetryAction.IDE_ACTION.createActionMessage() + .property("action", ActionType.CLEAR_CHAT_WINDOW.name) + .send() + } + } + + override fun getActionUpdateThread(): ActionUpdateThread { + return ActionUpdateThread.BGT + } +} diff --git a/src/main/kotlin/ee/carlrobert/codegpt/actions/toolwindow/CreateNewConversationAction.kt b/src/main/kotlin/ee/carlrobert/codegpt/actions/toolwindow/CreateNewConversationAction.kt new file mode 100644 index 000000000..582daf46d --- /dev/null +++ b/src/main/kotlin/ee/carlrobert/codegpt/actions/toolwindow/CreateNewConversationAction.kt @@ -0,0 +1,27 @@ +package ee.carlrobert.codegpt.actions.toolwindow + +import com.intellij.icons.AllIcons +import com.intellij.openapi.actionSystem.AnAction +import com.intellij.openapi.actionSystem.AnActionEvent +import ee.carlrobert.codegpt.actions.ActionType +import ee.carlrobert.codegpt.actions.editor.EditorActionsUtil.registerAction +import ee.carlrobert.codegpt.telemetry.TelemetryAction + +class CreateNewConversationAction(private val onCreate: Runnable) : + AnAction("Create New Chat", "Create new chat", AllIcons.General.Add) { + + init { + registerAction(this) + } + + override fun actionPerformed(event: AnActionEvent) { + try { + event.project ?: return + onCreate.run() + } finally { + TelemetryAction.IDE_ACTION.createActionMessage() + .property("action", ActionType.CREATE_NEW_CHAT.name) + .send() + } + } +} diff --git a/src/main/kotlin/ee/carlrobert/codegpt/actions/toolwindow/DeleteAllConversationsAction.kt b/src/main/kotlin/ee/carlrobert/codegpt/actions/toolwindow/DeleteAllConversationsAction.kt new file mode 100644 index 000000000..5a3029e12 --- /dev/null +++ b/src/main/kotlin/ee/carlrobert/codegpt/actions/toolwindow/DeleteAllConversationsAction.kt @@ -0,0 +1,50 @@ +package ee.carlrobert.codegpt.actions.toolwindow + +import com.intellij.icons.AllIcons +import com.intellij.openapi.actionSystem.ActionUpdateThread +import com.intellij.openapi.actionSystem.AnAction +import com.intellij.openapi.actionSystem.AnActionEvent +import com.intellij.openapi.ui.Messages +import ee.carlrobert.codegpt.Icons +import ee.carlrobert.codegpt.actions.ActionType +import ee.carlrobert.codegpt.actions.editor.EditorActionsUtil.registerAction +import ee.carlrobert.codegpt.conversations.ConversationService +import ee.carlrobert.codegpt.telemetry.TelemetryAction +import ee.carlrobert.codegpt.toolwindow.chat.ChatToolWindowContentManager + +class DeleteAllConversationsAction(private val onRefresh: Runnable) : + AnAction("Delete All", "Delete all conversations", AllIcons.Actions.GC) { + init { + registerAction(this) + } + + override fun update(event: AnActionEvent) { + event.project ?: return + event.presentation.isEnabled = ConversationService.getInstance().sortedConversations.isNotEmpty() + } + + override fun actionPerformed(event: AnActionEvent) { + val answer = Messages.showYesNoDialog( + "Are you sure you want to delete all conversations?", + "Clear History", + Icons.Default) + if (answer == Messages.YES) { + val project = event.project + if (project != null) { + try { + ConversationService.getInstance().clearAll() + project.getService(ChatToolWindowContentManager::class.java).resetAll() + } finally { + TelemetryAction.IDE_ACTION.createActionMessage() + .property("action", ActionType.DELETE_ALL_CONVERSATIONS.name) + .send() + } + } + onRefresh.run() + } + } + + override fun getActionUpdateThread(): ActionUpdateThread { + return ActionUpdateThread.BGT + } +} diff --git a/src/main/kotlin/ee/carlrobert/codegpt/actions/toolwindow/DeleteConversationAction.kt b/src/main/kotlin/ee/carlrobert/codegpt/actions/toolwindow/DeleteConversationAction.kt new file mode 100644 index 000000000..4d1a67921 --- /dev/null +++ b/src/main/kotlin/ee/carlrobert/codegpt/actions/toolwindow/DeleteConversationAction.kt @@ -0,0 +1,36 @@ +package ee.carlrobert.codegpt.actions.toolwindow + +import com.intellij.icons.AllIcons +import com.intellij.openapi.actionSystem.ActionUpdateThread +import com.intellij.openapi.actionSystem.AnAction +import com.intellij.openapi.actionSystem.AnActionEvent +import com.intellij.openapi.ui.Messages +import ee.carlrobert.codegpt.actions.ActionType +import ee.carlrobert.codegpt.actions.editor.EditorActionsUtil.registerAction +import ee.carlrobert.codegpt.conversations.ConversationsState +import ee.carlrobert.codegpt.telemetry.TelemetryAction +import ee.carlrobert.codegpt.ui.OverlayUtil + +class DeleteConversationAction(private val onDelete: Runnable) : + AnAction("Delete Conversation", "Delete single conversation", AllIcons.Actions.GC) { + init { + registerAction(this) + } + + override fun update(event: AnActionEvent) { + event.presentation.isEnabled = ConversationsState.getCurrentConversation() != null + } + + override fun actionPerformed(event: AnActionEvent) { + if (OverlayUtil.showDeleteConversationDialog() == Messages.YES && event.project != null) { + TelemetryAction.IDE_ACTION.createActionMessage() + .property("action", ActionType.DELETE_CONVERSATION.name) + .send() + onDelete.run() + } + } + + override fun getActionUpdateThread(): ActionUpdateThread { + return ActionUpdateThread.EDT + } +} diff --git a/src/main/kotlin/ee/carlrobert/codegpt/actions/toolwindow/MoveAction.kt b/src/main/kotlin/ee/carlrobert/codegpt/actions/toolwindow/MoveAction.kt new file mode 100644 index 000000000..f57051406 --- /dev/null +++ b/src/main/kotlin/ee/carlrobert/codegpt/actions/toolwindow/MoveAction.kt @@ -0,0 +1,32 @@ +package ee.carlrobert.codegpt.actions.toolwindow + +import com.intellij.openapi.actionSystem.ActionUpdateThread +import com.intellij.openapi.actionSystem.AnAction +import com.intellij.openapi.actionSystem.AnActionEvent +import com.intellij.openapi.project.Project +import ee.carlrobert.codegpt.conversations.Conversation +import ee.carlrobert.codegpt.conversations.ConversationsState +import java.util.Optional +import javax.swing.Icon + +abstract class MoveAction protected constructor( + text: String?, description: String?, icon: Icon?, private val onRefresh: Runnable) : AnAction(text, description, icon) { + + protected abstract fun getConversation(project: Project): Optional + + override fun update(event: AnActionEvent) { + event.presentation.isEnabled = ConversationsState.getCurrentConversation() != null + } + + override fun actionPerformed(event: AnActionEvent) { + val project = event.project ?: return + getConversation(project).ifPresent { + ConversationsState.getInstance().setCurrentConversation(it) + onRefresh.run() + } + } + + override fun getActionUpdateThread(): ActionUpdateThread { + return ActionUpdateThread.BGT + } +} diff --git a/src/main/kotlin/ee/carlrobert/codegpt/actions/toolwindow/MoveDownAction.kt b/src/main/kotlin/ee/carlrobert/codegpt/actions/toolwindow/MoveDownAction.kt new file mode 100644 index 000000000..d7c1c90f3 --- /dev/null +++ b/src/main/kotlin/ee/carlrobert/codegpt/actions/toolwindow/MoveDownAction.kt @@ -0,0 +1,19 @@ +package ee.carlrobert.codegpt.actions.toolwindow + +import com.intellij.icons.AllIcons +import com.intellij.openapi.project.Project +import ee.carlrobert.codegpt.actions.editor.EditorActionsUtil.registerAction +import ee.carlrobert.codegpt.conversations.Conversation +import ee.carlrobert.codegpt.conversations.ConversationService +import java.util.Optional + +class MoveDownAction(onRefresh: Runnable?) : + MoveAction("Move Down", "Move Down", AllIcons.Actions.MoveDown, onRefresh!!) { + init { + registerAction(this) + } + + override fun getConversation(project: Project): Optional { + return ConversationService.getInstance().previousConversation + } +} diff --git a/src/main/kotlin/ee/carlrobert/codegpt/actions/toolwindow/MoveUpAction.kt b/src/main/kotlin/ee/carlrobert/codegpt/actions/toolwindow/MoveUpAction.kt new file mode 100644 index 000000000..b035be0e8 --- /dev/null +++ b/src/main/kotlin/ee/carlrobert/codegpt/actions/toolwindow/MoveUpAction.kt @@ -0,0 +1,18 @@ +package ee.carlrobert.codegpt.actions.toolwindow + +import com.intellij.icons.AllIcons +import com.intellij.openapi.project.Project +import ee.carlrobert.codegpt.actions.editor.EditorActionsUtil.registerAction +import ee.carlrobert.codegpt.conversations.Conversation +import ee.carlrobert.codegpt.conversations.ConversationService +import java.util.Optional + +class MoveUpAction(onRefresh: Runnable?) : MoveAction("Move Up", "Move up", AllIcons.Actions.MoveUp, onRefresh!!) { + init { + registerAction(this) + } + + override fun getConversation(project: Project): Optional { + return ConversationService.getInstance().nextConversation + } +} diff --git a/src/main/kotlin/ee/carlrobert/codegpt/actions/toolwindow/OpenInEditorAction.kt b/src/main/kotlin/ee/carlrobert/codegpt/actions/toolwindow/OpenInEditorAction.kt new file mode 100644 index 000000000..1c01abb23 --- /dev/null +++ b/src/main/kotlin/ee/carlrobert/codegpt/actions/toolwindow/OpenInEditorAction.kt @@ -0,0 +1,53 @@ +package ee.carlrobert.codegpt.actions.toolwindow + +import com.intellij.icons.AllIcons +import com.intellij.openapi.actionSystem.ActionUpdateThread +import com.intellij.openapi.actionSystem.AnAction +import com.intellij.openapi.actionSystem.AnActionEvent +import com.intellij.openapi.fileEditor.FileEditorManager +import com.intellij.openapi.vfs.VirtualFile +import com.intellij.openapi.wm.ToolWindowManager +import com.intellij.testFramework.LightVirtualFile +import ee.carlrobert.codegpt.actions.ActionType +import ee.carlrobert.codegpt.actions.editor.EditorActionsUtil.registerAction +import ee.carlrobert.codegpt.conversations.ConversationsState.getCurrentConversation +import ee.carlrobert.codegpt.telemetry.TelemetryAction +import java.time.format.DateTimeFormatter +import java.util.Objects.requireNonNull +import java.util.stream.Collectors + +class OpenInEditorAction : AnAction("Open In Editor", "Open conversation in editor", AllIcons.Actions.SplitVertically) { + init { + registerAction(this) + } + + override fun update(event: AnActionEvent) { + super.update(event) + event.presentation.isEnabled = getCurrentConversation()?.messages?.isNotEmpty() == true + } + + override fun actionPerformed(e: AnActionEvent) { + try { + val project = e.project ?: return + val currentConversation = getCurrentConversation() ?: return + val dateTimeStamp = currentConversation.updatedOn + .format(DateTimeFormatter.ofPattern("yyyyMMddHHmm")) + val fileName = String.format("%s_%s.md", currentConversation.model, dateTimeStamp) + val fileContent = currentConversation.messages.stream() + .map { String.format("### User:%n%s%n### CodeGPT:%n%s%n", it.prompt, it.response) } + .collect(Collectors.joining()) + val file: VirtualFile = LightVirtualFile(fileName, fileContent) + FileEditorManager.getInstance(project).openFile(file, true) + requireNonNull(ToolWindowManager.getInstance(project).getToolWindow("CodeGPT"))!! + .hide() + } finally { + TelemetryAction.IDE_ACTION.createActionMessage() + .property("action", ActionType.OPEN_CONVERSATION_IN_EDITOR.name) + .send() + } + } + + override fun getActionUpdateThread(): ActionUpdateThread { + return ActionUpdateThread.BGT + } +} diff --git a/src/main/kotlin/ee/carlrobert/codegpt/actions/toolwindow/ReplaceCodeInMainEditorAction.kt b/src/main/kotlin/ee/carlrobert/codegpt/actions/toolwindow/ReplaceCodeInMainEditorAction.kt new file mode 100644 index 000000000..0bfec89e1 --- /dev/null +++ b/src/main/kotlin/ee/carlrobert/codegpt/actions/toolwindow/ReplaceCodeInMainEditorAction.kt @@ -0,0 +1,34 @@ +package ee.carlrobert.codegpt.actions.toolwindow + +import com.intellij.icons.AllIcons +import com.intellij.openapi.actionSystem.ActionUpdateThread +import com.intellij.openapi.actionSystem.AnAction +import com.intellij.openapi.actionSystem.AnActionEvent +import com.intellij.openapi.actionSystem.PlatformDataKeys +import ee.carlrobert.codegpt.actions.editor.EditorActionsUtil.registerAction +import ee.carlrobert.codegpt.util.EditorUtil.hasSelection +import ee.carlrobert.codegpt.util.EditorUtil.isMainEditorTextSelected +import ee.carlrobert.codegpt.util.EditorUtil.replaceMainEditorSelection +import java.util.Objects.requireNonNull + +class ReplaceCodeInMainEditorAction : + AnAction("Replace in Main Editor", "Replace code in main editor", AllIcons.Actions.Replace) { + init { + registerAction(this) + } + + override fun update(event: AnActionEvent) { + event.presentation.isEnabled = isMainEditorTextSelected(requireNonNull(event.project)!!) + && hasSelection(event.getData(PlatformDataKeys.EDITOR)) + } + + override fun actionPerformed(event: AnActionEvent) { + val project = event.project ?: return + val toolWindowEditor = event.getData(PlatformDataKeys.EDITOR) ?: return + replaceMainEditorSelection(project, requireNonNull(toolWindowEditor.selectionModel.selectedText)!!) + } + + override fun getActionUpdateThread(): ActionUpdateThread { + return ActionUpdateThread.EDT + } +}