diff --git a/CHANGELOG.md b/CHANGELOG.md index 8b16c74e..6c1c1d66 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,13 @@ ## [Unreleased] +## [0.1.9] + +### Added +- Dev mode now adds a API request preview/testing UI for each request +- Various UI polish +- Refactored code so Actions are independent components + ## [0.1.8] ### Added diff --git a/build.gradle.kts b/build.gradle.kts index da813e83..40bf5e59 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -28,6 +28,7 @@ repositories { // Set the JVM language level used to build project. Use Java 11 for 2020.3+, and Java 17 for 2022.2+. kotlin { +// jvmToolchain(17) jvmToolchain(11) } diff --git a/gradle.properties b/gradle.properties index c375c43e..51d9e425 100644 --- a/gradle.properties +++ b/gradle.properties @@ -4,7 +4,7 @@ pluginGroup = com.github.simiacryptus pluginName = intellij-aicoder pluginRepositoryUrl = https://github.com/SimiaCryptus/intellij-aicoder # SemVer format -> https://semver.org -pluginVersion = 0.1.8 +pluginVersion = 0.1.9 # Supported build number ranges and IntelliJ Platform versions -> https://plugins.jetbrains.com/docs/intellij/build-number-ranges.html pluginSinceBuild = 203 @@ -13,6 +13,7 @@ pluginUntilBuild = 223.* # IntelliJ Platform Properties -> https://plugins.jetbrains.com/docs/intellij/tools-gradle-intellij-plugin.html#configuration-intellij-extension platformType = IC platformVersion = 2021.3.3 +#platformVersion = 2022.3.1 # Plugin Dependencies -> https://plugins.jetbrains.com/docs/intellij/plugin-dependencies.html # Example: platformPlugins = com.intellij.java, com.jetbrains.php:203.4449.22 diff --git a/src/main/java/com/github/simiacryptus/aicoder/EditorMenu.java b/src/main/java/com/github/simiacryptus/aicoder/EditorMenu.java deleted file mode 100644 index cc430afa..00000000 --- a/src/main/java/com/github/simiacryptus/aicoder/EditorMenu.java +++ /dev/null @@ -1,688 +0,0 @@ -package com.github.simiacryptus.aicoder; - -import com.github.simiacryptus.aicoder.config.AppSettingsState; -import com.github.simiacryptus.aicoder.openai.CompletionRequest; -import com.github.simiacryptus.aicoder.openai.ModerationException; -import com.github.simiacryptus.aicoder.psi.PsiClassContext; -import com.github.simiacryptus.aicoder.psi.PsiMarkdownContext; -import com.github.simiacryptus.aicoder.psi.PsiUtil; -import com.github.simiacryptus.aicoder.util.*; -import com.intellij.core.CoreBundle; -import com.intellij.openapi.actionSystem.ActionGroup; -import com.intellij.openapi.actionSystem.AnAction; -import com.intellij.openapi.actionSystem.AnActionEvent; -import com.intellij.openapi.actionSystem.CommonDataKeys; -import com.intellij.openapi.diagnostic.Logger; -import com.intellij.openapi.editor.Caret; -import com.intellij.openapi.editor.CaretModel; -import com.intellij.openapi.editor.Document; -import com.intellij.openapi.editor.Editor; -import com.intellij.openapi.ide.CopyPasteManager; -import com.intellij.openapi.util.TextRange; -import com.intellij.openapi.vfs.VirtualFile; -import com.intellij.psi.PsiElement; -import com.intellij.psi.PsiFile; -import org.jetbrains.annotations.Nls; -import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; - -import javax.swing.*; -import java.awt.datatransfer.DataFlavor; -import java.io.IOException; -import java.util.*; -import java.util.stream.Collectors; -import java.util.stream.Stream; - - -public class EditorMenu extends ActionGroup { - - private static final Logger log = Logger.getInstance(EditorMenu.class); - public static final @NotNull - @Nls String DEFAULT_ACTION_MESSAGE = CoreBundle.message("command.name.undefined"); - - /** - * This method is used to get the children of the action. - *

- * This Java code is an override of the getChildren() method. It is used to create an array of AnAction objects that will be used to create a context menu for a file. - * The code first gets the extension of the file and sets the computer language and comment line prefix based on the extension. - * It then checks if the copy/paste manager has data flavors available and calls the pasteAction() method if it does. - * It then checks the extension and calls the docAction() method if it is either Java or Scala. - * It then checks if there is a selection and calls the customTranslation(), autoImplementationAction(), describeAction(), and standardCodeActions() methods if there is. - * Finally, it returns an array of AnAction objects. - * - * @param e AnActionEvent object that contains the necessary data to perform the action. - * @return An array of AnAction objects that are the children of the action. - */ - @Override - public AnAction @NotNull [] getChildren(@NotNull AnActionEvent e) { - AppSettingsState settings = AppSettingsState.getInstance(); - String inputHumanLanguage = settings.humanLanguage; - String outputHumanLanguage = settings.humanLanguage; - VirtualFile file = e.getData(CommonDataKeys.VIRTUAL_FILE); - if (file == null) return new AnAction[]{}; - String extension = file.getExtension() != null ? file.getExtension().toLowerCase() : ""; - ArrayList children = new ArrayList<>(); - ComputerLanguage language = ComputerLanguage.findByExtension(extension); - - Caret caret = e.getData(CommonDataKeys.CARET); - - if (null != caret) { - addIfNotNull(children, redoLast()); - if (!caret.hasSelection()) { - children.add(genericInsert()); - } else { - children.add(genericAppend()); - } - } - - - if (language != null) { - - addIfNotNull(children, rewordCommentAction(e, language, inputHumanLanguage)); - - if (settings.devActions) { - addIfNotNull(children, printTreeAction(e)); - } - - if (CopyPasteManager.getInstance().areDataFlavorsAvailable(DataFlavor.stringFlavor)) { - children.add(pasteAction(language.name())); - } - - if (language.docStyle.length() > 0) { - children.add(docAction(extension, language)); - } - - if (language == ComputerLanguage.Markdown) { - addIfNotNull(children, markdownListAction(e)); - addIfNotNull(children, markdownNewTableRowsAction(e)); - addIfNotNull(children, markdownNewTableColsAction(e)); - addIfNotNull(children, markdownNewTableColAction(e)); - } - - if (null != caret) { - if (caret.hasSelection()) { - children.add(customEdit(language.name())); - children.add(recentEdits(language.name())); - switch (language) { - case Markdown: - addIfNotNull(children, markdownContextAction(e, inputHumanLanguage)); - break; - default: - addIfNotNull(children, psiClassContextAction(e, language, inputHumanLanguage)); - break; - } - children.add(describeAction(outputHumanLanguage, language)); - children.add(addCodeCommentsAction(outputHumanLanguage, language)); - children.add(fromHumanLanguageAction(inputHumanLanguage, language)); - children.add(toHumanLanguageAction(outputHumanLanguage, language)); - } - } - } - - return children.toArray(AnAction[]::new); - } - - @NotNull - protected AnAction toHumanLanguageAction(String outputHumanLanguage, ComputerLanguage language) { - String computerLanguage = language.name(); - String description = String.format("Describe %s -> %s", outputHumanLanguage, computerLanguage); - return TextReplacementAction.create("_To " + outputHumanLanguage, description, null, - (event, string) -> { - AppSettingsState settings = AppSettingsState.getInstance(); - return settings.createTranslationRequest() - .setInstruction(UITools.getInstruction("Describe this code")) - .setInputText(string) - .setInputType(computerLanguage) - .setInputAttribute("type", "input") - .setOutputType(outputHumanLanguage.toLowerCase()) - .setOutputAttrute("type", "output") - .setOutputAttrute("style", settings.style) - .buildCompletionRequest(); - }); - } - - @NotNull - protected AnAction fromHumanLanguageAction(String inputHumanLanguage, ComputerLanguage language) { - String computerLanguage = language.name(); - String description = String.format("Implement %s -> %s", inputHumanLanguage, computerLanguage); - return TextReplacementAction.create("_From " + inputHumanLanguage, description, null, (event, string) -> - AppSettingsState.getInstance().createTranslationRequest() - .setInputType(inputHumanLanguage.toLowerCase()) - .setOutputType(computerLanguage) - .setInstruction("Implement this specification") - .setInputAttribute("type", "input") - .setOutputAttrute("type", "output") - .setInputText(string) - .buildCompletionRequest()); - } - - @NotNull - protected AnAction addCodeCommentsAction(CharSequence outputHumanLanguage, ComputerLanguage language) { - String computerLanguage = language.name(); - return TextReplacementAction.create("Add Code _Comments", "Add Code Comments", null, (event, string) -> { - AppSettingsState settings = AppSettingsState.getInstance(); - return settings.createTranslationRequest() - .setInputType(computerLanguage) - .setOutputType(computerLanguage) - .setInstruction(UITools.getInstruction("Rewrite to include detailed " + outputHumanLanguage + " code comments for every line")) - .setInputAttribute("type", "uncommented") - .setOutputAttrute("type", "commented") - .setOutputAttrute("style", settings.style) - .setInputText(string) - .buildCompletionRequest(); - }); - } - - @NotNull - protected AnAction describeAction(String outputHumanLanguage, ComputerLanguage language) { - return TextReplacementAction.create("_Describe Code and Prepend Comment", "Add JavaDoc Comments", null, new TextReplacementAction.ActionTextEditorFunction() { - @Override - public CompletionRequest apply(AnActionEvent event, String inputString) throws IOException, ModerationException { - AppSettingsState settings = AppSettingsState.getInstance(); - return settings.createTranslationRequest() - .setInputType(language.name()) - .setOutputType(outputHumanLanguage) - .setInstruction(UITools.getInstruction("Explain this " + language.name() + " in " + outputHumanLanguage)) - .setInputAttribute("type", "code") - .setOutputAttrute("type", "description") - .setOutputAttrute("style", settings.style) - .setInputText(IndentedText.fromString(inputString).getTextBlock().trim()) - .buildCompletionRequest(); - } - - @Override - public CharSequence postTransform(AnActionEvent event, CharSequence prompt, CharSequence completion) { - CharSequence indent = UITools.getIndent(event); - String wrapping = StringTools.lineWrapping(completion.toString().trim(), 120); - return "\n" + indent + language.blockComment.fromString(wrapping).withIndent(indent) + "\n" + indent + prompt; - } - }); - } - - @NotNull - protected AnAction genericInsert() { - return new AnAction("_Insert Text", "Insert Text", null) { - @Override - public void actionPerformed(@NotNull AnActionEvent event) { - Caret caret = event.getData(CommonDataKeys.CARET); - Document document = caret.getEditor().getDocument(); - int caretPosition = caret.getOffset(); - CharSequence before = StringTools.getSuffixForContext(document.getText(new TextRange(0, caretPosition))); - CharSequence after = StringTools.getPrefixForContext(document.getText(new TextRange(caretPosition, document.getTextLength()))); - AppSettingsState settings = AppSettingsState.getInstance(); - CompletionRequest completionRequest = settings.createCompletionRequest() - .appendPrompt(before) - .setSuffix(after); - UITools.redoableRequest(completionRequest, "", event, complete -> { - final Editor editor = event.getRequiredData(CommonDataKeys.EDITOR); - return UITools.insertString(editor.getDocument(), caretPosition, complete); - }); - } - }; - } - - @NotNull - protected AnAction genericAppend() { - return new AnAction("_Append Text", "Append Text", null) { - @Override - public void actionPerformed(@NotNull AnActionEvent event) { - Caret caret = event.getData(CommonDataKeys.CARET); - CharSequence before = caret.getSelectedText(); - AppSettingsState settings = AppSettingsState.getInstance(); - CompletionRequest completionRequest = settings.createCompletionRequest() - .appendPrompt(before); - UITools.redoableRequest(completionRequest, "", event, complete -> { - final Editor editor = event.getRequiredData(CommonDataKeys.EDITOR); - return UITools.insertString(editor.getDocument(), caret.getSelectionEnd(), complete); - }); - } - }; - } - - @Nullable - protected AnAction redoLast() { - if(UITools.retry.isEmpty()) return null; - return new AnAction("_Redo Last", "Redo last", null) { - @Override - public void actionPerformed(@NotNull AnActionEvent event) { - UITools.retry.pop().run(); - } - }; - } - - protected AnAction customEdit(String computerLanguage) { - return TextReplacementAction.create("_Edit...", "Edit...", null, (event, string) -> { - String instruction = JOptionPane.showInputDialog(null, "Instruction:", "Edit Code", JOptionPane.QUESTION_MESSAGE); - AppSettingsState settings = AppSettingsState.getInstance(); - settings.addInstructionToHistory(instruction); - return settings.createTranslationRequest() - .setInputType(computerLanguage) - .setOutputType(computerLanguage) - .setInstruction(instruction) - .setInputAttribute("type", "before") - .setOutputAttrute("type", "after") - .setInputText(IndentedText.fromString(string).getTextBlock()) - .buildCompletionRequest(); - }); - } - - /* - * - * Creates a new ActionGroup for recent edits. - * - * @param computerLanguage the language of the edit - * @return a new ActionGroup for recent edits - */ - @NotNull - protected ActionGroup recentEdits(String computerLanguage) { - return new ActionGroup("Recent Edits", true) { - @Override - public AnAction @NotNull [] getChildren(@Nullable AnActionEvent e) { - ArrayList children = new ArrayList<>(); - AppSettingsState.getInstance().getEditHistory().forEach(instruction -> children.add(customEdit(computerLanguage, instruction))); - return children.toArray(AnAction[]::new); - } - }; - } - - @NotNull - protected AnAction docAction(String extension, ComputerLanguage language) { - return new AnAction("_Add " + language.docStyle + " Comments", "Add " + language.docStyle + " Comments", null) { - @Override - public void actionPerformed(@NotNull final AnActionEvent event) { - Caret caret = event.getData(CommonDataKeys.CARET); - PsiFile psiFile = event.getRequiredData(CommonDataKeys.PSI_FILE); - PsiElement smallestIntersectingMethod = PsiUtil.getSmallestIntersecting(psiFile, caret.getSelectionStart(), caret.getSelectionEnd()); - if (null == smallestIntersectingMethod) return; - AppSettingsState settings = AppSettingsState.getInstance(); - String code = smallestIntersectingMethod.getText(); - IndentedText indentedInput = IndentedText.fromString(code); - CharSequence indent = indentedInput.getIndent(); - CompletionRequest completionRequest = settings.createTranslationRequest() - .setInputType(extension) - .setOutputType(extension) - .setInstruction(UITools.getInstruction("Rewrite to include detailed " + language.docStyle)) - .setInputAttribute("type", "uncommented") - .setOutputAttrute("type", "commented") - .setOutputAttrute("style", settings.style) - .setInputText(indentedInput.getTextBlock()) - .buildCompletionRequest() - .addStops(language.getMultilineCommentSuffix()); - int startOffset = smallestIntersectingMethod.getTextRange().getStartOffset(); - int endOffset = smallestIntersectingMethod.getTextRange().getEndOffset(); - UITools.redoableRequest(completionRequest, "", event, (CharSequence docString) -> { - TextBlock reindented = language.docComment.fromString(docString.toString().trim()).withIndent(indent); - final CharSequence newText = reindented + "\n" + indent + StringTools.trimPrefix(indentedInput.toString()); - final Editor editor = event.getRequiredData(CommonDataKeys.EDITOR); - return UITools.replaceString(editor.getDocument(), startOffset, endOffset, newText); - }); - } - }; - } - - /** - * Creates a paste action for the given language. - * - * @param language the language to paste into - * @return a {@link TextReplacementAction} that pastes the contents of the clipboard into the given language - */ - @NotNull - protected AnAction pasteAction(@NotNull CharSequence language) { - return TextReplacementAction.create("_Paste", "Paste", null, (event, string) -> { - String text = CopyPasteManager.getInstance().getContents(DataFlavor.stringFlavor).toString().trim(); - return AppSettingsState.getInstance().createTranslationRequest() - .setInputType("source") - .setOutputType("translated") - .setInstruction("Translate this input into " + language) - .setInputAttribute("language", "autodetect") - .setOutputAttrute("language", language) - .setInputText(text) - .buildCompletionRequest(); - }); - } - - @NotNull - protected AnAction customEdit(CharSequence computerLanguage, CharSequence instruction) { - return TextReplacementAction.create(instruction, instruction, null, (event, string) -> { - AppSettingsState settings = AppSettingsState.getInstance(); - settings.addInstructionToHistory(instruction); - return settings.createTranslationRequest() - .setInputType(computerLanguage) - .setOutputType(computerLanguage) - .setInstruction(instruction) - .setInputAttribute("type", "before") - .setOutputAttrute("type", "after") - .setInputText(IndentedText.fromString(string).getTextBlock()) - .buildCompletionRequest(); - }); - } - - @Nullable - protected AnAction markdownListAction(@NotNull AnActionEvent e) { - try { - Caret caret = e.getData(CommonDataKeys.CARET); - if (null == caret) return null; - PsiFile psiFile = e.getData(CommonDataKeys.PSI_FILE); - if (null == psiFile) return null; - PsiElement list = PsiUtil.getSmallestIntersecting(psiFile, caret.getSelectionStart(), caret.getSelectionEnd(), "MarkdownListImpl"); - if (null == list) return null; - return new AnAction("Add _List Items", "Add list items", null) { - @Override - public void actionPerformed(@NotNull AnActionEvent event) { - AppSettingsState settings = AppSettingsState.getInstance(); - List items = StringTools.trim(PsiUtil.getAll(list, "MarkdownListItemImpl") - .stream().map(item -> PsiUtil.getAll(item, "MarkdownParagraphImpl").get(0).getText()).collect(Collectors.toList()), 10, false); - CharSequence indent = UITools.getIndent(caret); - CharSequence n = Integer.toString(items.size() * 2); - int endOffset = list.getTextRange().getEndOffset(); - String listPrefix = "* "; - CompletionRequest completionRequest = settings.createTranslationRequest() - .setInstruction(UITools.getInstruction("List " + n + " items")) - .setInputType("instruction") - .setInputText("List " + n + " items") - .setOutputType("list") - .setOutputAttrute("style", settings.style) - .buildCompletionRequest() - .appendPrompt(items.stream().map(x2 -> listPrefix + x2).collect(Collectors.joining("\n")) + "\n" + listPrefix); - UITools.redoableRequest(completionRequest, "", event, complete -> { - List newItems = Arrays.stream(complete.toString().split("\n")).map(String::trim) - .filter(x1 -> x1 != null && x1.length() > 0).map(x1 -> StringTools.stripPrefix(x1, listPrefix)).collect(Collectors.toList()); - String strippedList = Arrays.stream(list.getText().split("\n")) - .map(String::trim).filter(x -> x.length() > 0).collect(Collectors.joining("\n")); - String bulletString = Stream.of("- [ ] ", "- ", "* ") - .filter(strippedList::startsWith).findFirst().orElse("1. "); - CharSequence itemText = indent + newItems.stream().map(x -> bulletString + x) - .collect(Collectors.joining("\n" + indent)); - final Editor editor = event.getRequiredData(CommonDataKeys.EDITOR); - return UITools.insertString(editor.getDocument(), endOffset, "\n" + itemText); - }); - } - }; - } catch (Exception ex) { - log.error(ex); - return null; - } - } - - /** - * This method creates an action to add new columns to a Markdown table. - * - * @param e The action event - * @return An action to add new columns to a Markdown table, or null if the action cannot be created - */ - @Nullable - protected AnAction markdownNewTableColsAction(@NotNull AnActionEvent e) { - Caret caret = e.getData(CommonDataKeys.CARET); - if (null == caret) return null; - PsiFile psiFile = e.getData(CommonDataKeys.PSI_FILE); - if (null == psiFile) return null; - PsiElement table = PsiUtil.getSmallestIntersecting(psiFile, caret.getSelectionStart(), caret.getSelectionEnd(), "MarkdownTableImpl"); - if (null == table) return null; - List rows = Arrays.asList(StringTools.transposeMarkdownTable(PsiUtil.getAll(table, "MarkdownTableRowImpl") - .stream().map(PsiElement::getText).collect(Collectors.joining("\n")), false, false).split("\n")); - CharSequence n = Integer.toString(rows.size() * 2); - return new AnAction("Add _Table Columns", "Add table columns", null) { - @Override - public void actionPerformed(@NotNull AnActionEvent event) { - CharSequence originalText = table.getText(); - AppSettingsState settings = AppSettingsState.getInstance(); - CharSequence indent = UITools.getIndent(caret); - UITools.redoableRequest(newRowsRequest(settings, n, rows, ""), - "", - event, - (CharSequence complete) -> { - List newRows = Arrays.stream(("" + complete).split("\n")).map(String::trim) - .filter(x -> x.length() > 0).collect(Collectors.toList()); - String newTableTxt = StringTools.transposeMarkdownTable(Stream.concat(rows.stream(), newRows.stream()) - .collect(Collectors.joining("\n")), false, true); - final Editor editor = event.getRequiredData(CommonDataKeys.EDITOR); - return UITools.replaceString( - editor.getDocument(), - table.getTextRange().getStartOffset(), - table.getTextRange().getEndOffset(), - newTableTxt.replace("\n", "\n" + indent)); - }); - } - }; - } - - /** - * Creates an action to add a new column to a Markdown table. - * - * @param e The action event. - * @return An action to add a new column to a Markdown table, or null if the action cannot be created. - */ - @Nullable - protected AnAction markdownNewTableColAction(@NotNull AnActionEvent e) { - Caret caret = e.getData(CommonDataKeys.CARET); - if (null == caret) return null; - PsiFile psiFile = e.getData(CommonDataKeys.PSI_FILE); - if (null == psiFile) return null; - PsiElement table = PsiUtil.getSmallestIntersecting(psiFile, caret.getSelectionStart(), caret.getSelectionEnd(), "MarkdownTableImpl"); - if (null == table) return null; - List rows = Arrays.asList(StringTools.transposeMarkdownTable(PsiUtil.getAll(table, "MarkdownTableRowImpl") - .stream().map(PsiElement::getText).collect(Collectors.joining("\n")), false, false).split("\n")); - CharSequence n = Integer.toString(rows.size() * 2); - return new AnAction("Add Table _Column...", "Add table column...", null) { - @Override - public void actionPerformed(@NotNull AnActionEvent event) { - AppSettingsState settings = AppSettingsState.getInstance(); - CharSequence indent = UITools.getIndent(caret); - CharSequence columnName = JOptionPane.showInputDialog(null, "Column Name:", "Add Column", JOptionPane.QUESTION_MESSAGE); - UITools.redoableRequest( - newRowsRequest(settings, n, rows, "| " + columnName + " | "), - "", - event, - (CharSequence complete) -> { - List newRows = Arrays.stream(("" + complete).split("\n")) - .map(String::trim).filter(x -> x.length() > 0).collect(Collectors.toList()); - String newTableTxt = StringTools.transposeMarkdownTable(Stream.concat(rows.stream(), - newRows.stream()).collect(Collectors.joining("\n")), false, true); - final Editor editor = event.getRequiredData(CommonDataKeys.EDITOR); - return UITools.replaceString( - editor.getDocument(), - table.getTextRange().getStartOffset(), - table.getTextRange().getEndOffset(), - newTableTxt.replace("\n", "\n" + indent)); - }); - } - }; - } - - @Nullable - protected AnAction markdownNewTableRowsAction(@NotNull AnActionEvent e) { - Caret caret = e.getData(CommonDataKeys.CARET); - if (null == caret) return null; - PsiFile psiFile = e.getData(CommonDataKeys.PSI_FILE); - if (null == psiFile) return null; - PsiElement table = PsiUtil.getSmallestIntersecting(psiFile, caret.getSelectionStart(), caret.getSelectionEnd(), "MarkdownTableImpl"); - if (null == table) return null; - if (null != table) { - List rows = StringTools.trim(PsiUtil.getAll(table, "MarkdownTableRowImpl") - .stream().map(PsiElement::getText).collect(Collectors.toList()), 10, true); - CharSequence n = Integer.toString(rows.size() * 2); - return new AnAction("Add _Table Rows", "Add table rows", null) { - @Override - public void actionPerformed(@NotNull AnActionEvent event) { - AppSettingsState settings = AppSettingsState.getInstance(); - CharSequence indent = UITools.getIndent(caret); - UITools.redoableRequest(newRowsRequest(settings, n, rows, ""), - "", - event, - (CharSequence complete) -> { - List newRows = Arrays.stream(("" + complete).split("\n")) - .map(String::trim).filter(x -> x.length() > 0).collect(Collectors.toList()); - CharSequence itemText = indent + newRows.stream().collect(Collectors.joining("\n" + indent)); - final Editor editor = event.getRequiredData(CommonDataKeys.EDITOR); - return UITools.insertString(editor.getDocument(), table.getTextRange().getEndOffset(), "\n" + itemText); - }); - } - }; - } - return null; - } - - @NotNull - protected CompletionRequest newRowsRequest(AppSettingsState settings, CharSequence n, List rows, CharSequence rowPrefix) { - return settings.createTranslationRequest() - .setInstruction(UITools.getInstruction("List " + n + " items")) - .setInputType("instruction") - .setInputText("List " + n + " items") - .setOutputType("markdown") - .setOutputAttrute("style", settings.style) - .buildCompletionRequest() - .appendPrompt("\n" + String.join("\n", rows) + "\n" + rowPrefix); - } - - /** - * Creates a {@link TextReplacementAction} for the given {@link AnActionEvent} and human language. - * - * @param e the action event - * @param humanLanguage the human language - * @return the {@link TextReplacementAction} or {@code null} if no action can be created - */ - @Nullable - protected AnAction markdownContextAction(@NotNull AnActionEvent e, CharSequence humanLanguage) { - Caret caret = e.getData(CommonDataKeys.CARET); - if (null != caret) { - int selectionStart = caret.getSelectionStart(); - int selectionEnd = caret.getSelectionEnd(); - if (selectionStart < selectionEnd) { - return TextReplacementAction.create("E_xecute Directive", "Execute Directive", null, (event, humanDescription) -> { - AppSettingsState settings = AppSettingsState.getInstance(); - PsiFile psiFile = e.getRequiredData(CommonDataKeys.PSI_FILE); - String context = PsiMarkdownContext.getContext(psiFile, selectionStart, selectionEnd).toString(selectionEnd); - context = context + "\n\n"; - context = context + "\n"; - return settings.createTranslationRequest() - .setOutputType("markdown") - .setInstruction(UITools.getInstruction(String.format("Using Markdown and %s", humanLanguage))) - .setInputType("instruction") - .setInputText(humanDescription) - .setOutputAttrute("type", "document") - .setOutputAttrute("style", settings.style) - .buildCompletionRequest() - .appendPrompt(context); - }); - } - } - return null; - } - - public static void addIfNotNull(@NotNull ArrayList children, AnAction action) { - if (null != action) children.add(action); - } - - @Nullable - protected AnAction printTreeAction(@NotNull AnActionEvent e) { - Caret caret = e.getData(CommonDataKeys.CARET); - if (null == caret) return null; - PsiElement psiFile = e.getData(CommonDataKeys.PSI_FILE); - if (null == psiFile) return null; - int selectionStart = caret.getSelectionStart(); - int selectionEnd = caret.getSelectionEnd(); - PsiElement largestContainedEntity = PsiUtil.getLargestContainedEntity(psiFile, selectionStart, selectionEnd); - if (largestContainedEntity != null) psiFile = largestContainedEntity; - PsiElement finalPsiFile = psiFile; - return new AnAction("Print PSI Tree", "Print PSI Tree", null) { - @Override - public void actionPerformed(@NotNull final AnActionEvent e1) { - log.warn(PsiUtil.printTree(finalPsiFile)); - } - - }; - } - - @Nullable - protected AnAction rewordCommentAction(@NotNull AnActionEvent e, ComputerLanguage computerLanguage, String humanLanguage) { - PsiFile psiFile = e.getData(CommonDataKeys.PSI_FILE); - if (null == psiFile) return null; - Caret caret = e.getData(CommonDataKeys.CARET); - if (null == caret) return null; - int selectionStart = caret.getSelectionStart(); - int selectionEnd = caret.getSelectionEnd(); - PsiElement largestIntersectingComment = PsiUtil.getLargestIntersectingComment(psiFile, selectionStart, selectionEnd); - if (largestIntersectingComment == null) return null; - return new AnAction("_Reword Comment", "Reword Comment", null) { - @Override - public void actionPerformed(@NotNull final AnActionEvent e1) { - final Editor editor = e1.getRequiredData(CommonDataKeys.EDITOR); - AppSettingsState settings = AppSettingsState.getInstance(); - String text = largestIntersectingComment.getText(); - TextBlockFactory commentModel = computerLanguage.getCommentModel(text); - String commentText = commentModel.fromString(text.trim()).stream() - .map(Object::toString) - .map(String::trim) - .filter(x -> !x.isEmpty()) - .reduce((a, b) -> a + "\n" + b).get(); - int startOffset = largestIntersectingComment.getTextRange().getStartOffset(); - int endOffset = largestIntersectingComment.getTextRange().getEndOffset(); - CharSequence indent = UITools.getIndent(caret); - UITools.redoableRequest(settings.createTranslationRequest() - .setInstruction(UITools.getInstruction("Reword")) - .setInputText(commentText) - .setInputType(humanLanguage) - .setOutputAttrute("type", "input") - .setOutputType(humanLanguage) - .setOutputAttrute("type", "output") - .setOutputAttrute("style", settings.style) - .buildCompletionRequest(), "", e1, (CharSequence result) -> { - String lineWrapping = StringTools.lineWrapping(result, 120); - CharSequence finalResult = indent.toString() + commentModel.fromString(lineWrapping).withIndent(indent); - return UITools.replaceString(editor.getDocument(), startOffset, endOffset, finalResult); - }); - } - - }; - } - - @Nullable - protected AnAction psiClassContextAction(@NotNull AnActionEvent e, ComputerLanguage computerLanguage, String humanLanguage) { - PsiFile psiFile = e.getData(CommonDataKeys.PSI_FILE); - if (null == psiFile) return null; - Caret caret = e.getData(CommonDataKeys.CARET); - if (null == caret) return null; - int selectionStart = caret.getSelectionStart(); - int selectionEnd = caret.getSelectionEnd(); - PsiElement largestIntersectingComment = PsiUtil.getLargestIntersectingComment(psiFile, selectionStart, selectionEnd); - if (largestIntersectingComment == null) return null; - return new AnAction("Insert _Implementation", "Insert Implementation", null) { - @Override - public void actionPerformed(@NotNull final AnActionEvent e1) { - final Editor editor = e1.getRequiredData(CommonDataKeys.EDITOR); - final CaretModel caretModel = editor.getCaretModel(); - final Caret primaryCaret = caretModel.getPrimaryCaret(); - @NotNull String selectedText = primaryCaret.getSelectedText(); - AppSettingsState settings = AppSettingsState.getInstance(); - - String instruct = (selectedText.split(" ").length > 4 ? selectedText : largestIntersectingComment.getText()).trim(); - String specification = computerLanguage.getCommentModel(instruct).fromString(instruct).stream() - .map(Object::toString) - .map(String::trim) - .filter(x -> !x.isEmpty()) - .reduce((a, b) -> a + " " + b).get(); - int endOffset = largestIntersectingComment.getTextRange().getEndOffset(); - UITools.redoableRequest(settings.createTranslationRequest() - .setInstruction("Implement " + humanLanguage + " as " + computerLanguage.name() + " code") - .setInputType(humanLanguage) - .setInputAttribute("type", "instruction") - .setInputText(specification) - .setOutputType(computerLanguage.name()) - .setOutputAttrute("type", "code") - .setOutputAttrute("style", settings.style) - .buildCompletionRequest() - .appendPrompt(PsiClassContext.getContext(psiFile, selectionStart, selectionEnd) + "\n"), - UITools.getIndent(caret), - e1, - (CharSequence result) -> UITools.insertString(editor.getDocument(), endOffset, "\n" + result)); - } - }; - } - - - @Override - public void update(@NotNull AnActionEvent e) { - e.getPresentation().setEnabledAndVisible(true); - super.update(e); - } -} \ No newline at end of file diff --git a/src/main/java/com/github/simiacryptus/aicoder/actions/CommentsAction.java b/src/main/java/com/github/simiacryptus/aicoder/actions/CommentsAction.java new file mode 100644 index 00000000..35113ec6 --- /dev/null +++ b/src/main/java/com/github/simiacryptus/aicoder/actions/CommentsAction.java @@ -0,0 +1,61 @@ +package com.github.simiacryptus.aicoder.actions; + +import com.github.simiacryptus.aicoder.config.AppSettingsState; +import com.github.simiacryptus.aicoder.openai.CompletionRequest; +import com.github.simiacryptus.aicoder.util.ComputerLanguage; +import com.github.simiacryptus.aicoder.util.UITools; +import com.intellij.openapi.actionSystem.AnAction; +import com.intellij.openapi.actionSystem.AnActionEvent; +import com.intellij.openapi.actionSystem.CommonDataKeys; +import com.intellij.openapi.editor.Caret; +import com.intellij.openapi.editor.CaretModel; +import com.intellij.openapi.editor.Editor; +import org.jetbrains.annotations.NotNull; + +import static com.github.simiacryptus.aicoder.util.UITools.replaceString; +import static java.util.Objects.requireNonNull; + +public class CommentsAction extends AnAction { + + @Override + public void update(@NotNull AnActionEvent e) { + e.getPresentation().setEnabledAndVisible(isEnabled(e)); + super.update(e); + } + + private static boolean isEnabled(@NotNull AnActionEvent e) { + final Editor editor = e.getRequiredData(CommonDataKeys.EDITOR); + final CaretModel caretModel = editor.getCaretModel(); + final Caret primaryCaret = caretModel.getPrimaryCaret(); + int selectionStart = primaryCaret.getSelectionStart(); + int selectionEnd = primaryCaret.getSelectionEnd(); + if(selectionStart == selectionEnd) return false; + if (null == ComputerLanguage.getComputerLanguage(e)) return false; + return true; + } + + @Override + public void actionPerformed(@NotNull final AnActionEvent e) { + final Editor editor = e.getRequiredData(CommonDataKeys.EDITOR); + final CaretModel caretModel = editor.getCaretModel(); + final Caret primaryCaret = caretModel.getPrimaryCaret(); + int selectionStart = primaryCaret.getSelectionStart(); + int selectionEnd = primaryCaret.getSelectionEnd(); + String selectedText = primaryCaret.getSelectedText(); + String outputHumanLanguage = AppSettingsState.getInstance().humanLanguage; + ComputerLanguage language = ComputerLanguage.getComputerLanguage(e); + AppSettingsState settings = AppSettingsState.getInstance(); + CompletionRequest request = settings.createTranslationRequest() + .setInputType(requireNonNull(language).name()) + .setOutputType(language.name()) + .setInstruction(UITools.getInstruction("Rewrite to include detailed " + outputHumanLanguage + " code comments for every line")) + .setInputAttribute("type", "uncommented") + .setOutputAttrute("type", "commented") + .setOutputAttrute("style", settings.style) + .setInputText(selectedText) + .buildCompletionRequest(); + Caret caret = e.getData(CommonDataKeys.CARET); + CharSequence indent = UITools.getIndent(caret); + UITools.redoableRequest(request, indent, e, newText -> replaceString(editor.getDocument(), selectionStart, selectionEnd, newText)); + } +} diff --git a/src/main/java/com/github/simiacryptus/aicoder/actions/CustomEditAction.java b/src/main/java/com/github/simiacryptus/aicoder/actions/CustomEditAction.java new file mode 100644 index 00000000..40be79e0 --- /dev/null +++ b/src/main/java/com/github/simiacryptus/aicoder/actions/CustomEditAction.java @@ -0,0 +1,57 @@ +package com.github.simiacryptus.aicoder.actions; + +import com.github.simiacryptus.aicoder.util.ComputerLanguage; +import com.github.simiacryptus.aicoder.config.AppSettingsState; +import com.github.simiacryptus.aicoder.openai.CompletionRequest; +import com.github.simiacryptus.aicoder.util.IndentedText; +import com.github.simiacryptus.aicoder.util.UITools; +import com.intellij.openapi.actionSystem.AnAction; +import com.intellij.openapi.actionSystem.AnActionEvent; +import com.intellij.openapi.actionSystem.CommonDataKeys; +import com.intellij.openapi.editor.Caret; +import com.intellij.openapi.editor.CaretModel; +import com.intellij.openapi.editor.Editor; +import org.jetbrains.annotations.NotNull; + +import javax.swing.*; + +import static com.github.simiacryptus.aicoder.util.UITools.replaceString; +import static java.util.Objects.requireNonNull; + +public class CustomEditAction extends AnAction { + + @Override + public void update(@NotNull AnActionEvent e) { + e.getPresentation().setEnabledAndVisible(isEnabled(e)); + super.update(e); + } + + private static boolean isEnabled(@NotNull AnActionEvent e) { + return null != ComputerLanguage.getComputerLanguage(e); + } + + @Override + public void actionPerformed(@NotNull final AnActionEvent e) { + final Editor editor = e.getRequiredData(CommonDataKeys.EDITOR); + final CaretModel caretModel = editor.getCaretModel(); + final Caret primaryCaret = caretModel.getPrimaryCaret(); + int selectionStart = primaryCaret.getSelectionStart(); + int selectionEnd = primaryCaret.getSelectionEnd(); + String selectedText = primaryCaret.getSelectedText(); + String computerLanguage = requireNonNull(ComputerLanguage.getComputerLanguage(e)).name(); + String instruction = JOptionPane.showInputDialog(null, "Instruction:", "Edit Code", JOptionPane.QUESTION_MESSAGE); + AppSettingsState settings = AppSettingsState.getInstance(); + settings.addInstructionToHistory(instruction); + CompletionRequest request = settings.createTranslationRequest() + .setInputType(computerLanguage) + .setOutputType(computerLanguage) + .setInstruction(instruction) + .setInputAttribute("type", "before") + .setOutputAttrute("type", "after") + .setInputText(IndentedText.fromString(selectedText).getTextBlock()) + .buildCompletionRequest(); + Caret caret = e.getData(CommonDataKeys.CARET); + CharSequence indent = UITools.getIndent(caret); + UITools.redoableRequest(request, indent, e, newText -> replaceString(editor.getDocument(), selectionStart, selectionEnd, newText)); + } +} diff --git a/src/main/java/com/github/simiacryptus/aicoder/actions/DescribeAction.java b/src/main/java/com/github/simiacryptus/aicoder/actions/DescribeAction.java new file mode 100644 index 00000000..c5a86822 --- /dev/null +++ b/src/main/java/com/github/simiacryptus/aicoder/actions/DescribeAction.java @@ -0,0 +1,82 @@ +package com.github.simiacryptus.aicoder.actions; + +import com.github.simiacryptus.aicoder.util.*; +import com.github.simiacryptus.aicoder.config.AppSettingsState; +import com.github.simiacryptus.aicoder.openai.CompletionRequest; +import com.intellij.openapi.actionSystem.AnAction; +import com.intellij.openapi.actionSystem.AnActionEvent; +import com.intellij.openapi.actionSystem.CommonDataKeys; +import com.intellij.openapi.editor.Caret; +import com.intellij.openapi.editor.CaretModel; +import com.intellij.openapi.editor.Document; +import com.intellij.openapi.editor.Editor; +import org.jetbrains.annotations.NotNull; + +import java.util.Objects; + +import static com.github.simiacryptus.aicoder.util.UITools.replaceString; + +public class DescribeAction extends AnAction { + + @Override + public void update(@NotNull AnActionEvent e) { + e.getPresentation().setEnabledAndVisible(isEnabled(e)); + super.update(e); + } + + private static boolean isEnabled(@NotNull AnActionEvent e) { + return null != ComputerLanguage.getComputerLanguage(e); + } + + @Override + public void actionPerformed(@NotNull final AnActionEvent event) { + final Editor editor = event.getRequiredData(CommonDataKeys.EDITOR); + final CaretModel caretModel = editor.getCaretModel(); + final Caret primaryCaret = caretModel.getPrimaryCaret(); + int selectionStart = primaryCaret.getSelectionStart(); + int selectionEnd = primaryCaret.getSelectionEnd(); + String selectedText = primaryCaret.getSelectedText(); + ComputerLanguage language = ComputerLanguage.getComputerLanguage(event); + assert language != null; + + if(null == selectedText || selectedText.isEmpty()) { + Document document = editor.getDocument(); + int lineNumber = document.getLineNumber(selectionStart); + int lineStartOffset = document.getLineStartOffset(lineNumber); + int lineEndOffset = document.getLineEndOffset(lineNumber); + String currentLine = document.getText().substring(lineStartOffset, lineEndOffset); + selectionStart = lineStartOffset; + selectionEnd = lineEndOffset; + selectedText = currentLine; + } + + actionPerformed(event, editor, selectionStart, selectionEnd, selectedText, language); + } + + private static void actionPerformed(@NotNull AnActionEvent event, Editor editor, int selectionStart, int selectionEnd, String selectedText, ComputerLanguage language) { + CharSequence indent = UITools.getIndent(event); + AppSettingsState settings = AppSettingsState.getInstance(); + CompletionRequest request = settings.createTranslationRequest() + .setInputType(Objects.requireNonNull(language).name()) + .setOutputType(settings.humanLanguage) + .setInstruction(UITools.getInstruction("Explain this " + language.name() + " in " + settings.humanLanguage)) + .setInputAttribute("type", "code") + .setOutputAttrute("type", "description") + .setOutputAttrute("style", settings.style) + .setInputText(IndentedText.fromString(selectedText).getTextBlock().trim()) + .buildCompletionRequest(); + UITools.redoableRequest(request, indent, event, newText -> transformCompletion(selectedText, language, indent, newText), newText -> replaceString(editor.getDocument(), selectionStart, selectionEnd, newText)); + } + + @NotNull + private static CharSequence transformCompletion(String selectedText, ComputerLanguage language, CharSequence indent, CharSequence x) { + String wrapping = StringTools.lineWrapping(x.toString().trim(), 120); + TextBlockFactory commentStyle; + if(wrapping.trim().split("\n").length == 1) { + commentStyle = language.lineComment; + } else { + commentStyle = language.blockComment; + } + return "\n" + indent + Objects.requireNonNull(commentStyle).fromString(wrapping).withIndent(indent) + "\n" + indent + selectedText; + } +} diff --git a/src/main/java/com/github/simiacryptus/aicoder/actions/DocAction.java b/src/main/java/com/github/simiacryptus/aicoder/actions/DocAction.java new file mode 100644 index 00000000..6dab4f01 --- /dev/null +++ b/src/main/java/com/github/simiacryptus/aicoder/actions/DocAction.java @@ -0,0 +1,67 @@ +package com.github.simiacryptus.aicoder.actions; + +import com.github.simiacryptus.aicoder.config.AppSettingsState; +import com.github.simiacryptus.aicoder.openai.CompletionRequest; +import com.github.simiacryptus.aicoder.util.psi.PsiUtil; +import com.github.simiacryptus.aicoder.util.*; +import com.intellij.openapi.actionSystem.AnAction; +import com.intellij.openapi.actionSystem.AnActionEvent; +import com.intellij.openapi.actionSystem.CommonDataKeys; +import com.intellij.openapi.editor.Caret; +import com.intellij.openapi.editor.Document; +import com.intellij.psi.PsiElement; +import com.intellij.psi.PsiFile; +import org.jetbrains.annotations.NotNull; + +import java.util.Objects; + +import static com.github.simiacryptus.aicoder.util.UITools.replaceString; + +public class DocAction extends AnAction { + + @Override + public void update(@NotNull AnActionEvent e) { + e.getPresentation().setEnabledAndVisible(isEnabled(e)); + super.update(e); + } + + private static boolean isEnabled(@NotNull AnActionEvent e) { + ComputerLanguage computerLanguage = ComputerLanguage.getComputerLanguage(e); + if (null == computerLanguage) return false; + if (null == computerLanguage.docStyle || computerLanguage.docStyle.isEmpty()) return false; + return true; + } + + @Override + public void actionPerformed(@NotNull final AnActionEvent event) { + ComputerLanguage language = ComputerLanguage.getComputerLanguage(event); + Caret caret = event.getData(CommonDataKeys.CARET); + PsiFile psiFile = event.getRequiredData(CommonDataKeys.PSI_FILE); + PsiElement smallestIntersectingMethod = PsiUtil.getSmallestIntersecting(psiFile, Objects.requireNonNull(caret).getSelectionStart(), caret.getSelectionEnd()); + if (null == smallestIntersectingMethod) return; + AppSettingsState settings = AppSettingsState.getInstance(); + String code = smallestIntersectingMethod.getText(); + IndentedText indentedInput = IndentedText.fromString(code); + CharSequence indent = indentedInput.getIndent(); + int startOffset = smallestIntersectingMethod.getTextRange().getStartOffset(); + int endOffset = smallestIntersectingMethod.getTextRange().getEndOffset(); + CompletionRequest completionRequest = settings.createTranslationRequest() + .setInputType(Objects.requireNonNull(language).name()) + .setOutputType(language.name()) + .setInstruction(UITools.getInstruction("Rewrite to include detailed " + language.docStyle)) + .setInputAttribute("type", "uncommented") + .setOutputAttrute("type", "commented") + .setOutputAttrute("style", settings.style) + .setInputText(indentedInput.getTextBlock()) + .buildCompletionRequest() + .addStops(Objects.requireNonNull(language.getMultilineCommentSuffix())); + Document document = event.getRequiredData(CommonDataKeys.EDITOR).getDocument(); + UITools.redoableRequest(completionRequest, "", event, docString -> transformCompletion(language, indentedInput, indent, docString), docString -> replaceString(document, startOffset, endOffset, docString)); + } + + @NotNull + private static CharSequence transformCompletion(ComputerLanguage language, IndentedText indentedInput, CharSequence indent, CharSequence docString) { + TextBlock reindented = Objects.requireNonNull(language.docComment).fromString(docString.toString().trim()).withIndent(indent); + return reindented + "\n" + indent + StringTools.trimPrefix(indentedInput.toString()); + } +} diff --git a/src/main/java/com/github/simiacryptus/aicoder/actions/FromHumanLanguageAction.java b/src/main/java/com/github/simiacryptus/aicoder/actions/FromHumanLanguageAction.java new file mode 100644 index 00000000..4c2c9c6f --- /dev/null +++ b/src/main/java/com/github/simiacryptus/aicoder/actions/FromHumanLanguageAction.java @@ -0,0 +1,44 @@ +package com.github.simiacryptus.aicoder.actions; + +import com.github.simiacryptus.aicoder.util.ComputerLanguage; +import com.github.simiacryptus.aicoder.config.AppSettingsState; +import com.github.simiacryptus.aicoder.openai.CompletionRequest; +import com.github.simiacryptus.aicoder.util.UITools; +import com.intellij.openapi.actionSystem.AnAction; +import com.intellij.openapi.actionSystem.AnActionEvent; +import com.intellij.openapi.actionSystem.CommonDataKeys; +import com.intellij.openapi.editor.Caret; +import com.intellij.openapi.editor.CaretModel; +import com.intellij.openapi.editor.Editor; +import org.jetbrains.annotations.NotNull; + +import static com.github.simiacryptus.aicoder.util.UITools.replaceString; +import static java.util.Objects.requireNonNull; + +public class FromHumanLanguageAction extends AnAction { + + public FromHumanLanguageAction() { + super("", "", null); + } + + @Override + public void actionPerformed(@NotNull final AnActionEvent event) { + final Editor editor = event.getRequiredData(CommonDataKeys.EDITOR); + final CaretModel caretModel = editor.getCaretModel(); + final Caret primaryCaret = caretModel.getPrimaryCaret(); + int selectionStart = primaryCaret.getSelectionStart(); + int selectionEnd = primaryCaret.getSelectionEnd(); + String selectedText = primaryCaret.getSelectedText(); + CompletionRequest request = AppSettingsState.getInstance().createTranslationRequest() + .setInputType(AppSettingsState.getInstance().humanLanguage.toLowerCase()) + .setOutputType(requireNonNull(ComputerLanguage.getComputerLanguage(event)).name()) + .setInstruction("Implement this specification") + .setInputAttribute("type", "input") + .setOutputAttrute("type", "output") + .setInputText(selectedText) + .buildCompletionRequest(); + Caret caret = event.getData(CommonDataKeys.CARET); + CharSequence indent = UITools.getIndent(caret); + UITools.redoableRequest(request, indent, event, newText -> replaceString(editor.getDocument(), selectionStart, selectionEnd, newText)); + } +} diff --git a/src/main/java/com/github/simiacryptus/aicoder/actions/GenericAppend.java b/src/main/java/com/github/simiacryptus/aicoder/actions/GenericAppend.java new file mode 100644 index 00000000..06529d4d --- /dev/null +++ b/src/main/java/com/github/simiacryptus/aicoder/actions/GenericAppend.java @@ -0,0 +1,40 @@ +package com.github.simiacryptus.aicoder.actions; + +import com.github.simiacryptus.aicoder.config.AppSettingsState; +import com.github.simiacryptus.aicoder.openai.CompletionRequest; +import com.github.simiacryptus.aicoder.util.UITools; +import com.intellij.openapi.actionSystem.AnAction; +import com.intellij.openapi.actionSystem.AnActionEvent; +import com.intellij.openapi.actionSystem.CommonDataKeys; +import com.intellij.openapi.editor.Caret; +import com.intellij.openapi.editor.Document; +import org.jetbrains.annotations.NotNull; + +import java.util.Objects; + +public class GenericAppend extends AnAction { + + @Override + public void update(@NotNull AnActionEvent e) { + e.getPresentation().setEnabledAndVisible(isEnabled(e)); + super.update(e); + } + + @SuppressWarnings("unused") + private static boolean isEnabled(@NotNull AnActionEvent e) { + Caret data = e.getData(CommonDataKeys.CARET); + if (!data.hasSelection()) return false; + return true; + } + + @Override + public void actionPerformed(@NotNull AnActionEvent event) { + Caret caret = event.getData(CommonDataKeys.CARET); + CharSequence before = Objects.requireNonNull(caret).getSelectedText(); + AppSettingsState settings = AppSettingsState.getInstance(); + CompletionRequest completionRequest = settings.createCompletionRequest().appendPrompt(before); + Document document = event.getRequiredData(CommonDataKeys.EDITOR).getDocument(); + int selectionEnd = caret.getSelectionEnd(); + UITools.redoableRequest(completionRequest, "", event, newText -> UITools.insertString(document, selectionEnd, newText)); + } +} diff --git a/src/main/java/com/github/simiacryptus/aicoder/actions/GenericInsert.java b/src/main/java/com/github/simiacryptus/aicoder/actions/GenericInsert.java new file mode 100644 index 00000000..53f63d2a --- /dev/null +++ b/src/main/java/com/github/simiacryptus/aicoder/actions/GenericInsert.java @@ -0,0 +1,45 @@ +package com.github.simiacryptus.aicoder.actions; + +import com.github.simiacryptus.aicoder.config.AppSettingsState; +import com.github.simiacryptus.aicoder.openai.CompletionRequest; +import com.github.simiacryptus.aicoder.util.StringTools; +import com.github.simiacryptus.aicoder.util.UITools; +import com.intellij.openapi.actionSystem.AnAction; +import com.intellij.openapi.actionSystem.AnActionEvent; +import com.intellij.openapi.actionSystem.CommonDataKeys; +import com.intellij.openapi.editor.Caret; +import com.intellij.openapi.editor.Document; +import com.intellij.openapi.util.TextRange; +import org.jetbrains.annotations.NotNull; + +import java.util.Objects; + +public class GenericInsert extends AnAction { + + @Override + public void update(@NotNull AnActionEvent e) { + e.getPresentation().setEnabledAndVisible(isEnabled(e)); + super.update(e); + } + + @SuppressWarnings("unused") + private static boolean isEnabled(@NotNull AnActionEvent e) { + Caret data = e.getData(CommonDataKeys.CARET); + if(data.hasSelection()) return false; + return true; + } + + @Override + public void actionPerformed(@NotNull AnActionEvent event) { + Caret caret = event.getData(CommonDataKeys.CARET); + Document document = Objects.requireNonNull(caret).getEditor().getDocument(); + int caretPosition = caret.getOffset(); + CharSequence before = StringTools.getSuffixForContext(document.getText(new TextRange(0, caretPosition))); + CharSequence after = StringTools.getPrefixForContext(document.getText(new TextRange(caretPosition, document.getTextLength()))); + AppSettingsState settings = AppSettingsState.getInstance(); + CompletionRequest completionRequest = settings.createCompletionRequest() + .appendPrompt(before) + .setSuffix(after); + UITools.redoableRequest(completionRequest, "", event, newText -> UITools.insertString(document, caretPosition, newText)); + } +} diff --git a/src/main/java/com/github/simiacryptus/aicoder/actions/MarkdownContextAction.java b/src/main/java/com/github/simiacryptus/aicoder/actions/MarkdownContextAction.java new file mode 100644 index 00000000..6211c886 --- /dev/null +++ b/src/main/java/com/github/simiacryptus/aicoder/actions/MarkdownContextAction.java @@ -0,0 +1,86 @@ +package com.github.simiacryptus.aicoder.actions; + +import com.github.simiacryptus.aicoder.config.AppSettingsState; +import com.github.simiacryptus.aicoder.openai.CompletionRequest; +import com.github.simiacryptus.aicoder.util.psi.PsiMarkdownContext; +import com.github.simiacryptus.aicoder.util.UITools; +import com.intellij.openapi.actionSystem.AnAction; +import com.intellij.openapi.actionSystem.AnActionEvent; +import com.intellij.openapi.actionSystem.CommonDataKeys; +import com.intellij.openapi.editor.Caret; +import com.intellij.openapi.editor.CaretModel; +import com.intellij.openapi.editor.Editor; +import com.intellij.psi.PsiFile; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import static com.github.simiacryptus.aicoder.util.UITools.replaceString; +import static java.util.Objects.requireNonNull; + +public class MarkdownContextAction extends AnAction { + + @Override + public void update(@NotNull AnActionEvent e) { + e.getPresentation().setEnabledAndVisible(isEnabled(e)); + super.update(e); + } + + private static boolean isEnabled(@NotNull AnActionEvent e) { + return null != getMarkdownContextParams(e, AppSettingsState.getInstance().humanLanguage); + } + + @Nullable + public static MarkdownContextParams getMarkdownContextParams(@NotNull AnActionEvent e, CharSequence humanLanguage) { + Caret caret = e.getData(CommonDataKeys.CARET); + if (null != caret) { + int selectionStart = caret.getSelectionStart(); + int selectionEnd = caret.getSelectionEnd(); + if (selectionStart < selectionEnd) { + return new MarkdownContextParams(humanLanguage, selectionStart, selectionEnd); + } + } + return null; + } + + @Override + public void actionPerformed(@NotNull final AnActionEvent event) { + final Editor editor = event.getRequiredData(CommonDataKeys.EDITOR); + final CaretModel caretModel = editor.getCaretModel(); + final Caret primaryCaret = caretModel.getPrimaryCaret(); + int selectionStart = primaryCaret.getSelectionStart(); + int selectionEnd = primaryCaret.getSelectionEnd(); + String selectedText = primaryCaret.getSelectedText(); + String humanLanguage = AppSettingsState.getInstance().humanLanguage; + MarkdownContextParams markdownContextParams = getMarkdownContextParams(event, humanLanguage); + AppSettingsState settings = AppSettingsState.getInstance(); + PsiFile psiFile = event.getRequiredData(CommonDataKeys.PSI_FILE); + String context = PsiMarkdownContext.getContext(psiFile, requireNonNull(markdownContextParams).selectionStart, markdownContextParams.selectionEnd).toString(markdownContextParams.selectionEnd); + context = context + "\n\n"; + context = context + "\n"; + CompletionRequest request = settings.createTranslationRequest() + .setOutputType("markdown") + .setInstruction(UITools.getInstruction(String.format("Using Markdown and %s", markdownContextParams.humanLanguage))) + .setInputType("instruction") + .setInputText(selectedText) + .setOutputAttrute("type", "document") + .setOutputAttrute("style", settings.style) + .buildCompletionRequest() + .appendPrompt(context); + Caret caret = event.getData(CommonDataKeys.CARET); + CharSequence indent = UITools.getIndent(caret); + UITools.redoableRequest(request, indent, event, newText -> replaceString(editor.getDocument(), selectionStart, selectionEnd, newText)); + } + + public static class MarkdownContextParams { + public final CharSequence humanLanguage; + public final int selectionStart; + public final int selectionEnd; + + private MarkdownContextParams(CharSequence humanLanguage, int selectionStart, int selectionEnd) { + this.humanLanguage = humanLanguage; + this.selectionStart = selectionStart; + this.selectionEnd = selectionEnd; + } + + } +} diff --git a/src/main/java/com/github/simiacryptus/aicoder/actions/MarkdownListAction.java b/src/main/java/com/github/simiacryptus/aicoder/actions/MarkdownListAction.java new file mode 100644 index 00000000..f6e1eccb --- /dev/null +++ b/src/main/java/com/github/simiacryptus/aicoder/actions/MarkdownListAction.java @@ -0,0 +1,94 @@ +package com.github.simiacryptus.aicoder.actions; + +import com.github.simiacryptus.aicoder.config.AppSettingsState; +import com.github.simiacryptus.aicoder.openai.CompletionRequest; +import com.github.simiacryptus.aicoder.util.psi.PsiUtil; +import com.github.simiacryptus.aicoder.util.StringTools; +import com.github.simiacryptus.aicoder.util.UITools; +import com.intellij.openapi.actionSystem.AnAction; +import com.intellij.openapi.actionSystem.AnActionEvent; +import com.intellij.openapi.actionSystem.CommonDataKeys; +import com.intellij.openapi.editor.Caret; +import com.intellij.openapi.editor.Document; +import com.intellij.psi.PsiElement; +import com.intellij.psi.PsiFile; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.Arrays; +import java.util.List; +import java.util.Objects; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import static com.github.simiacryptus.aicoder.util.UITools.insertString; + +public class MarkdownListAction extends AnAction { + + @Override + public void update(@NotNull AnActionEvent e) { + e.getPresentation().setEnabledAndVisible(isEnabled(e)); + super.update(e); + } + + private static boolean isEnabled(@NotNull AnActionEvent e) { + return null != getMarkdownListParams(e); + } + + @Nullable + public static MarkdownListParams getMarkdownListParams(@NotNull AnActionEvent e) { + Caret caret = e.getData(CommonDataKeys.CARET); + if (null == caret) return null; + PsiFile psiFile = e.getData(CommonDataKeys.PSI_FILE); + if (null == psiFile) return null; + PsiElement list = PsiUtil.getSmallestIntersecting(psiFile, caret.getSelectionStart(), caret.getSelectionEnd(), "MarkdownListImpl"); + if (null == list) return null; + return new MarkdownListParams(caret, list); + } + + @Override + public void actionPerformed(@NotNull AnActionEvent event) { + MarkdownListParams markdownListParams = getMarkdownListParams(event); + AppSettingsState settings = AppSettingsState.getInstance(); + List items = StringTools.trim(PsiUtil.getAll(Objects.requireNonNull(markdownListParams).list, "MarkdownListItemImpl") + .stream().map(item -> PsiUtil.getAll(item, "MarkdownParagraphImpl").get(0).getText()).collect(Collectors.toList()), 10, false); + CharSequence indent = UITools.getIndent(markdownListParams.caret); + CharSequence n = Integer.toString(items.size() * 2); + int endOffset = markdownListParams.list.getTextRange().getEndOffset(); + String listPrefix = "* "; + CompletionRequest completionRequest = settings.createTranslationRequest() + .setInstruction(UITools.getInstruction("List " + n + " items")) + .setInputType("instruction") + .setInputText("List " + n + " items") + .setOutputType("list") + .setOutputAttrute("style", settings.style) + .buildCompletionRequest() + .appendPrompt(items.stream().map(x2 -> listPrefix + x2).collect(Collectors.joining("\n")) + "\n" + listPrefix); + Document document = event.getRequiredData(CommonDataKeys.EDITOR).getDocument(); + UITools.redoableRequest(completionRequest, "", event, newText -> transformCompletion(markdownListParams, indent, listPrefix, newText), newText -> insertString(document, endOffset, newText)); + } + + @NotNull + private static String transformCompletion(MarkdownListParams markdownListParams, CharSequence indent, String listPrefix, CharSequence complete) { + List newItems = Arrays.stream(complete.toString().split("\n")).map(String::trim) + .filter(x1 -> x1.length() > 0).map(x1 -> StringTools.stripPrefix(x1, listPrefix)).collect(Collectors.toList()); + String strippedList = Arrays.stream(markdownListParams.list.getText().split("\n")) + .map(String::trim).filter(x -> x.length() > 0).collect(Collectors.joining("\n")); + String bulletString = Stream.of("- [ ] ", "- ", "* ") + .filter(strippedList::startsWith).findFirst().orElse("1. "); + CharSequence itemText = indent + newItems.stream().map(x -> bulletString + x) + .collect(Collectors.joining("\n" + indent)); + return "\n" + itemText; + } + + public static class MarkdownListParams { + public final Caret caret; + public final PsiElement list; + + private MarkdownListParams(Caret caret, PsiElement list) { + this.caret = caret; + this.list = list; + } + + } +} diff --git a/src/main/java/com/github/simiacryptus/aicoder/actions/MarkdownNewTableColAction.java b/src/main/java/com/github/simiacryptus/aicoder/actions/MarkdownNewTableColAction.java new file mode 100644 index 00000000..4b8b92c6 --- /dev/null +++ b/src/main/java/com/github/simiacryptus/aicoder/actions/MarkdownNewTableColAction.java @@ -0,0 +1,106 @@ +package com.github.simiacryptus.aicoder.actions; + +import com.github.simiacryptus.aicoder.config.AppSettingsState; +import com.github.simiacryptus.aicoder.openai.CompletionRequest; +import com.github.simiacryptus.aicoder.util.StringTools; +import com.github.simiacryptus.aicoder.util.UITools; +import com.github.simiacryptus.aicoder.util.psi.PsiUtil; +import com.intellij.openapi.actionSystem.AnAction; +import com.intellij.openapi.actionSystem.AnActionEvent; +import com.intellij.openapi.actionSystem.CommonDataKeys; +import com.intellij.openapi.editor.Caret; +import com.intellij.openapi.editor.Document; +import com.intellij.openapi.util.TextRange; +import com.intellij.psi.PsiElement; +import com.intellij.psi.PsiFile; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import javax.swing.*; +import java.util.Arrays; +import java.util.List; +import java.util.Objects; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import static com.github.simiacryptus.aicoder.util.UITools.replaceString; + +public class MarkdownNewTableColAction extends AnAction { + + @Override + public void update(@NotNull AnActionEvent e) { + e.getPresentation().setEnabledAndVisible(isEnabled(e)); + super.update(e); + } + + private static boolean isEnabled(@NotNull AnActionEvent e) { + return null != getMarkdownNewTableColParams(e); + } + + @Nullable + public static MarkdownNewTableColParams getMarkdownNewTableColParams(@NotNull AnActionEvent e) { + Caret caret = e.getData(CommonDataKeys.CARET); + if (null == caret) return null; + PsiFile psiFile = e.getData(CommonDataKeys.PSI_FILE); + if (null == psiFile) return null; + PsiElement table = PsiUtil.getSmallestIntersecting(psiFile, caret.getSelectionStart(), caret.getSelectionEnd(), "MarkdownTableImpl"); + if (null == table) return null; + List rows = Arrays.asList(StringTools.transposeMarkdownTable(PsiUtil.getAll(table, "MarkdownTableRowImpl") + .stream().map(PsiElement::getText).collect(Collectors.joining("\n")), false, false).split("\n")); + CharSequence n = Integer.toString(rows.size() * 2); + return new MarkdownNewTableColParams(caret, table, rows, n); + } + + @NotNull + public static CompletionRequest newRowsRequest(@NotNull AppSettingsState settings, CharSequence n, @NotNull List rows, CharSequence rowPrefix) { + return settings.createTranslationRequest() + .setInstruction(UITools.getInstruction("List " + n + " items")) + .setInputType("instruction") + .setInputText("List " + n + " items") + .setOutputType("markdown") + .setOutputAttrute("style", settings.style) + .buildCompletionRequest() + .appendPrompt("\n" + String.join("\n", rows) + "\n" + rowPrefix); + } + + @Override + public void actionPerformed(@NotNull AnActionEvent event) { + MarkdownNewTableColParams markdownNewTableColParams = getMarkdownNewTableColParams(event); + AppSettingsState settings = AppSettingsState.getInstance(); + CharSequence columnName = JOptionPane.showInputDialog(null, "Column Name:", "Add Column", JOptionPane.QUESTION_MESSAGE).trim(); + CompletionRequest request = newRowsRequest(settings, Objects.requireNonNull(markdownNewTableColParams).n, markdownNewTableColParams.rows, "| " + columnName + " | "); + Document document = event.getRequiredData(CommonDataKeys.EDITOR).getDocument(); + TextRange textRange = markdownNewTableColParams.table.getTextRange(); + int startOffset = textRange.getStartOffset(); + int endOffset = textRange.getEndOffset(); + UITools.redoableRequest(request, "", event, + newText -> transformCompletion(markdownNewTableColParams, newText, columnName), + newText -> replaceString(document, startOffset, endOffset, newText)); + } + + @NotNull + private static String transformCompletion(MarkdownNewTableColParams markdownNewTableColParams, CharSequence complete, CharSequence columnName) { + complete = "| " + columnName + " | " + complete; + CharSequence indent = UITools.getIndent(markdownNewTableColParams.caret); + List newRows = Arrays.stream(("" + complete).split("\n")) + .map(String::trim).filter(x -> x.length() > 0).collect(Collectors.toList()); + String newTableTxt = StringTools.transposeMarkdownTable(Stream.concat(markdownNewTableColParams.rows.stream(), + newRows.stream()).collect(Collectors.joining("\n")), false, true); + return newTableTxt.replace("\n", "\n" + indent); + } + + public static class MarkdownNewTableColParams { + public final Caret caret; + public final PsiElement table; + public final List rows; + public final CharSequence n; + + private MarkdownNewTableColParams(Caret caret, PsiElement table, List rows, CharSequence n) { + this.caret = caret; + this.table = table; + this.rows = rows; + this.n = n; + } + + } +} diff --git a/src/main/java/com/github/simiacryptus/aicoder/actions/MarkdownNewTableColsAction.java b/src/main/java/com/github/simiacryptus/aicoder/actions/MarkdownNewTableColsAction.java new file mode 100644 index 00000000..f5a17c7e --- /dev/null +++ b/src/main/java/com/github/simiacryptus/aicoder/actions/MarkdownNewTableColsAction.java @@ -0,0 +1,89 @@ +package com.github.simiacryptus.aicoder.actions; + +import com.github.simiacryptus.aicoder.config.AppSettingsState; +import com.github.simiacryptus.aicoder.openai.CompletionRequest; +import com.github.simiacryptus.aicoder.util.psi.PsiUtil; +import com.github.simiacryptus.aicoder.util.StringTools; +import com.github.simiacryptus.aicoder.util.UITools; +import com.intellij.openapi.actionSystem.AnAction; +import com.intellij.openapi.actionSystem.AnActionEvent; +import com.intellij.openapi.actionSystem.CommonDataKeys; +import com.intellij.openapi.editor.Caret; +import com.intellij.psi.PsiElement; +import com.intellij.psi.PsiFile; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.Arrays; +import java.util.List; +import java.util.Objects; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import static com.github.simiacryptus.aicoder.util.UITools.replaceString; + +public class MarkdownNewTableColsAction extends AnAction { + + @Override + public void update(@NotNull AnActionEvent e) { + e.getPresentation().setEnabledAndVisible(isEnabled(e)); + super.update(e); + } + + private static boolean isEnabled(@NotNull AnActionEvent e) { + return null != getMarkdownNewTableColsParams(e); + } + + @Nullable + public static MarkdownNewTableColsParams getMarkdownNewTableColsParams(@NotNull AnActionEvent e) { + Caret caret = e.getData(CommonDataKeys.CARET); + if (null != caret) { + PsiFile psiFile = e.getData(CommonDataKeys.PSI_FILE); + if (null != psiFile) { + PsiElement table = PsiUtil.getSmallestIntersecting(psiFile, caret.getSelectionStart(), caret.getSelectionEnd(), "MarkdownTableImpl"); + if (null != table) { + List rows = Arrays.asList(StringTools.transposeMarkdownTable(PsiUtil.getAll(table, "MarkdownTableRowImpl") + .stream().map(PsiElement::getText).collect(Collectors.joining("\n")), false, false).split("\n")); + CharSequence n = Integer.toString(rows.size() * 2); + return new MarkdownNewTableColsParams(caret, table, rows, n); + } + } + } + return null; + } + + @Override + public void actionPerformed(@NotNull AnActionEvent event) { + MarkdownNewTableColsParams markdownNewTableColsParams = getMarkdownNewTableColsParams(event); + AppSettingsState settings = AppSettingsState.getInstance(); + CharSequence indent = UITools.getIndent(Objects.requireNonNull(markdownNewTableColsParams).caret); + CompletionRequest request = MarkdownNewTableColAction.newRowsRequest(settings, markdownNewTableColsParams.n, markdownNewTableColsParams.rows, ""); + UITools.redoableRequest(request, "", event, newText -> transformCompletion(markdownNewTableColsParams, indent, newText), newText -> replaceString(event.getRequiredData(CommonDataKeys.EDITOR).getDocument(), + markdownNewTableColsParams.table.getTextRange().getStartOffset(), + markdownNewTableColsParams.table.getTextRange().getEndOffset(), newText)); + } + + @NotNull + private static String transformCompletion(MarkdownNewTableColsParams markdownNewTableColsParams, CharSequence indent, CharSequence complete) { + List newRows = Arrays.stream(("" + complete).split("\n")).map(String::trim) + .filter(x -> x.length() > 0).collect(Collectors.toList()); + String newTableTxt = StringTools.transposeMarkdownTable(Stream.concat(markdownNewTableColsParams.rows.stream(), newRows.stream()) + .collect(Collectors.joining("\n")), false, true); + return newTableTxt.replace("\n", "\n" + indent); + } + + public static class MarkdownNewTableColsParams { + public final Caret caret; + public final PsiElement table; + public final List rows; + public final CharSequence n; + + private MarkdownNewTableColsParams(Caret caret, PsiElement table, List rows, CharSequence n) { + this.caret = caret; + this.table = table; + this.rows = rows; + this.n = n; + } + + } +} diff --git a/src/main/java/com/github/simiacryptus/aicoder/actions/MarkdownNewTableRowsAction.java b/src/main/java/com/github/simiacryptus/aicoder/actions/MarkdownNewTableRowsAction.java new file mode 100644 index 00000000..c6ae5edd --- /dev/null +++ b/src/main/java/com/github/simiacryptus/aicoder/actions/MarkdownNewTableRowsAction.java @@ -0,0 +1,83 @@ +package com.github.simiacryptus.aicoder.actions; + +import com.github.simiacryptus.aicoder.config.AppSettingsState; +import com.github.simiacryptus.aicoder.util.StringTools; +import com.github.simiacryptus.aicoder.util.UITools; +import com.github.simiacryptus.aicoder.util.psi.PsiUtil; +import com.intellij.openapi.actionSystem.AnAction; +import com.intellij.openapi.actionSystem.AnActionEvent; +import com.intellij.openapi.actionSystem.CommonDataKeys; +import com.intellij.openapi.editor.Caret; +import com.intellij.openapi.editor.Document; +import com.intellij.psi.PsiElement; +import com.intellij.psi.PsiFile; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.Arrays; +import java.util.List; +import java.util.Objects; +import java.util.stream.Collectors; + +import static com.github.simiacryptus.aicoder.util.UITools.insertString; + +public class MarkdownNewTableRowsAction extends AnAction { + + @Override + public void update(@NotNull AnActionEvent e) { + e.getPresentation().setEnabledAndVisible(isEnabled(e)); + super.update(e); + } + + private static boolean isEnabled(@NotNull AnActionEvent e) { + return null != getMarkdownNewTableRowsParams(e); + } + + @Nullable + public static MarkdownNewTableRowsParams getMarkdownNewTableRowsParams(@NotNull AnActionEvent e) { + Caret caret = e.getData(CommonDataKeys.CARET); + if (null != caret) { + PsiFile psiFile = e.getData(CommonDataKeys.PSI_FILE); + if (null != psiFile) { + PsiElement table = PsiUtil.getSmallestIntersecting(psiFile, caret.getSelectionStart(), caret.getSelectionEnd(), "MarkdownTableImpl"); + if (null != table) { + return new MarkdownNewTableRowsParams(caret, table); + } + } + } + return null; + } + + @Override + public void actionPerformed(@NotNull AnActionEvent event) { + MarkdownNewTableRowsParams markdownNewTableRowsParams = getMarkdownNewTableRowsParams(event); + List rows = StringTools.trim(PsiUtil.getAll(Objects.requireNonNull(markdownNewTableRowsParams).table, "MarkdownTableRowImpl") + .stream().map(PsiElement::getText).collect(Collectors.toList()), 10, true); + CharSequence n = Integer.toString(rows.size() * 2); + AppSettingsState settings = AppSettingsState.getInstance(); + int endOffset = markdownNewTableRowsParams.table.getTextRange().getEndOffset(); + Document document = event.getRequiredData(CommonDataKeys.EDITOR).getDocument(); + UITools.redoableRequest(MarkdownNewTableColAction.newRowsRequest(settings, n, rows, ""), "", event, + newText -> transformCompletion(markdownNewTableRowsParams, newText), + newText -> insertString(document, endOffset, newText)); + } + + @NotNull + private static String transformCompletion(MarkdownNewTableRowsParams markdownNewTableRowsParams, CharSequence complete) { + CharSequence indent = UITools.getIndent(markdownNewTableRowsParams.caret); + List newRows = Arrays.stream(("" + complete).split("\n")) + .map(String::trim).filter(x -> x.length() > 0).collect(Collectors.toList()); + return "\n" + indent + newRows.stream().collect(Collectors.joining("\n" + indent)); + } + + public static class MarkdownNewTableRowsParams { + public final Caret caret; + public final PsiElement table; + + private MarkdownNewTableRowsParams(Caret caret, PsiElement table) { + this.caret = caret; + this.table = table; + } + + } +} diff --git a/src/main/java/com/github/simiacryptus/aicoder/actions/PasteAction.java b/src/main/java/com/github/simiacryptus/aicoder/actions/PasteAction.java new file mode 100644 index 00000000..f214fef6 --- /dev/null +++ b/src/main/java/com/github/simiacryptus/aicoder/actions/PasteAction.java @@ -0,0 +1,67 @@ +package com.github.simiacryptus.aicoder.actions; + +import com.github.simiacryptus.aicoder.util.ComputerLanguage; +import com.github.simiacryptus.aicoder.config.AppSettingsState; +import com.github.simiacryptus.aicoder.openai.CompletionRequest; +import com.github.simiacryptus.aicoder.util.UITools; +import com.intellij.openapi.actionSystem.AnAction; +import com.intellij.openapi.actionSystem.AnActionEvent; +import com.intellij.openapi.actionSystem.CommonDataKeys; +import com.intellij.openapi.editor.Caret; +import com.intellij.openapi.editor.CaretModel; +import com.intellij.openapi.editor.Editor; +import com.intellij.openapi.ide.CopyPasteManager; +import org.jetbrains.annotations.NotNull; + +import java.awt.datatransfer.DataFlavor; +import java.util.Objects; + +import static com.github.simiacryptus.aicoder.util.UITools.insertString; +import static com.github.simiacryptus.aicoder.util.UITools.replaceString; + +public class PasteAction extends AnAction { + + public PasteAction() { + super("", "Paste", null); + } + + @Override + public void update(@NotNull AnActionEvent e) { + e.getPresentation().setEnabledAndVisible(isEnabled(e)); + super.update(e); + } + + private static boolean isEnabled(@NotNull AnActionEvent e) { + if(CopyPasteManager.getInstance().getContents(DataFlavor.stringFlavor) == null) return false; + return null != ComputerLanguage.getComputerLanguage(e); + } + + @Override + public void actionPerformed(@NotNull final AnActionEvent event) { + final Editor editor = event.getRequiredData(CommonDataKeys.EDITOR); + final CaretModel caretModel = editor.getCaretModel(); + final Caret primaryCaret = caretModel.getPrimaryCaret(); + int selectionStart = primaryCaret.getSelectionStart(); + int selectionEnd = primaryCaret.getSelectionEnd(); + String selectedText = primaryCaret.getSelectedText(); + String language = Objects.requireNonNull(ComputerLanguage.getComputerLanguage(event)).name(); + String text = Objects.requireNonNull(CopyPasteManager.getInstance().getContents(DataFlavor.stringFlavor)).toString().trim(); + CompletionRequest request = AppSettingsState.getInstance().createTranslationRequest() + .setInputType("source") + .setOutputType("translated") + .setInstruction("Translate this input into " + language) + .setInputAttribute("language", "autodetect") + .setOutputAttrute("language", language) + .setInputText(text) + .buildCompletionRequest(); + Caret caret = event.getData(CommonDataKeys.CARET); + CharSequence indent = UITools.getIndent(caret); + UITools.redoableRequest(request, indent, event, newText -> { + if(selectedText == null) { + return insertString(editor.getDocument(), selectionStart, newText); + } else { + return replaceString(editor.getDocument(), selectionStart, selectionEnd, newText); + } + }); + } +} diff --git a/src/main/java/com/github/simiacryptus/aicoder/actions/PrintTreeAction.java b/src/main/java/com/github/simiacryptus/aicoder/actions/PrintTreeAction.java new file mode 100644 index 00000000..d3cdf1f0 --- /dev/null +++ b/src/main/java/com/github/simiacryptus/aicoder/actions/PrintTreeAction.java @@ -0,0 +1,31 @@ +package com.github.simiacryptus.aicoder.actions; + +import com.github.simiacryptus.aicoder.config.AppSettingsState; +import com.github.simiacryptus.aicoder.util.psi.PsiUtil; +import com.intellij.openapi.actionSystem.AnAction; +import com.intellij.openapi.actionSystem.AnActionEvent; +import com.intellij.openapi.diagnostic.Logger; +import org.jetbrains.annotations.NotNull; + +import java.util.Objects; + +public class PrintTreeAction extends AnAction { + public static final Logger log = Logger.getInstance(PrintTreeAction.class); + + @Override + public void update(@NotNull AnActionEvent e) { + e.getPresentation().setEnabledAndVisible(isEnabled(e)); + super.update(e); + } + + private static boolean isEnabled(@NotNull AnActionEvent e) { + if(!AppSettingsState.getInstance().devActions) return false; + return null != PsiUtil.getPsiFile(e); + } + + @Override + public void actionPerformed(@NotNull final AnActionEvent e1) { + log.warn(PsiUtil.printTree(Objects.requireNonNull(PsiUtil.getPsiFile(e1)))); + } + +} diff --git a/src/main/java/com/github/simiacryptus/aicoder/actions/PsiClassContextAction.java b/src/main/java/com/github/simiacryptus/aicoder/actions/PsiClassContextAction.java new file mode 100644 index 00000000..d5f3d6d5 --- /dev/null +++ b/src/main/java/com/github/simiacryptus/aicoder/actions/PsiClassContextAction.java @@ -0,0 +1,110 @@ +package com.github.simiacryptus.aicoder.actions; + +import com.github.simiacryptus.aicoder.openai.CompletionRequest; +import com.github.simiacryptus.aicoder.util.ComputerLanguage; +import com.github.simiacryptus.aicoder.config.AppSettingsState; +import com.github.simiacryptus.aicoder.util.psi.PsiClassContext; +import com.github.simiacryptus.aicoder.util.psi.PsiUtil; +import com.github.simiacryptus.aicoder.util.UITools; +import com.intellij.openapi.actionSystem.AnAction; +import com.intellij.openapi.actionSystem.AnActionEvent; +import com.intellij.openapi.actionSystem.CommonDataKeys; +import com.intellij.openapi.editor.Caret; +import com.intellij.openapi.editor.CaretModel; +import com.intellij.openapi.editor.Editor; +import com.intellij.psi.PsiElement; +import com.intellij.psi.PsiFile; +import org.jetbrains.annotations.NotNull; + +import java.util.Objects; +import java.util.Optional; + +import static com.github.simiacryptus.aicoder.util.UITools.insertString; + +public class PsiClassContextAction extends AnAction { + + @Override + public void update(@NotNull AnActionEvent e) { + e.getPresentation().setEnabledAndVisible(isEnabled(e)); + super.update(e); + } + + private static boolean isEnabled(@NotNull AnActionEvent e) { + return getPsiClassContextActionParams(e).isPresent(); + } + + public static @NotNull Optional getPsiClassContextActionParams(@NotNull AnActionEvent e) { + PsiFile psiFile = e.getData(CommonDataKeys.PSI_FILE); + if (null != psiFile) { + Caret caret = e.getData(CommonDataKeys.CARET); + if (null != caret) { + int selectionStart = caret.getSelectionStart(); + int selectionEnd = caret.getSelectionEnd(); + PsiElement largestIntersectingComment = PsiUtil.getLargestIntersectingComment(psiFile, selectionStart, selectionEnd); + if (largestIntersectingComment != null) { + return Optional.of(new PsiClassContextActionParams(psiFile, caret, selectionStart, selectionEnd, largestIntersectingComment)); + } + } + } + return Optional.empty(); + } + + @Override + public void actionPerformed(@NotNull final AnActionEvent event) { + String humanLanguage = AppSettingsState.getInstance().humanLanguage; + ComputerLanguage computerLanguage = ComputerLanguage.getComputerLanguage(event); + PsiClassContextActionParams psiClassContextActionParams = getPsiClassContextActionParams(event).get(); + final Editor editor = event.getRequiredData(CommonDataKeys.EDITOR); + final CaretModel caretModel = editor.getCaretModel(); + final Caret primaryCaret = caretModel.getPrimaryCaret(); + AppSettingsState settings = AppSettingsState.getInstance(); + + String instruct = psiClassContextActionParams.largestIntersectingComment.getText().trim(); + if (primaryCaret.getSelectionEnd() > primaryCaret.getSelectionStart()) { + @NotNull String selectedText = Objects.requireNonNull(primaryCaret.getSelectedText()); + if (Objects.requireNonNull(selectedText).split(" ").length > 4) { + instruct = selectedText.trim(); + } + } + assert computerLanguage != null; + String specification = Objects.requireNonNull(computerLanguage.getCommentModel(instruct)).fromString(instruct).stream() + .map(Object::toString) + .map(String::trim) + .filter(x -> !x.isEmpty()) + .reduce((a, b) -> a + " " + b).get(); + int endOffset = psiClassContextActionParams.largestIntersectingComment.getTextRange().getEndOffset(); + CompletionRequest request = settings.createTranslationRequest() + .setInstruction("Implement " + humanLanguage + " as " + computerLanguage.name() + " code") + .setInputType(humanLanguage) + .setInputAttribute("type", "instruction") + .setInputText(specification) + .setOutputType(computerLanguage.name()) + .setOutputAttrute("type", "code") + .setOutputAttrute("style", settings.style) + .buildCompletionRequest() + .appendPrompt(PsiClassContext.getContext(psiClassContextActionParams.psiFile, psiClassContextActionParams.selectionStart, psiClassContextActionParams.selectionEnd) + "\n"); + UITools.redoableRequest(request, UITools.getIndent(psiClassContextActionParams.caret), event, newText -> transformCompletion(newText), newText -> insertString(editor.getDocument(), endOffset, newText)); + } + + @NotNull + private static String transformCompletion(CharSequence result) { + return "\n" + result; + } + + public static class PsiClassContextActionParams { + public final PsiFile psiFile; + public final Caret caret; + public final int selectionStart; + public final int selectionEnd; + public final PsiElement largestIntersectingComment; + + private PsiClassContextActionParams(PsiFile psiFile, Caret caret, int selectionStart, int selectionEnd, PsiElement largestIntersectingComment) { + this.psiFile = psiFile; + this.caret = caret; + this.selectionStart = selectionStart; + this.selectionEnd = selectionEnd; + this.largestIntersectingComment = largestIntersectingComment; + } + + } +} diff --git a/src/main/java/com/github/simiacryptus/aicoder/actions/RecentEditsAction.java b/src/main/java/com/github/simiacryptus/aicoder/actions/RecentEditsAction.java new file mode 100644 index 00000000..09cc4ba3 --- /dev/null +++ b/src/main/java/com/github/simiacryptus/aicoder/actions/RecentEditsAction.java @@ -0,0 +1,78 @@ +package com.github.simiacryptus.aicoder.actions; + +import com.github.simiacryptus.aicoder.config.AppSettingsState; +import com.github.simiacryptus.aicoder.openai.CompletionRequest; +import com.github.simiacryptus.aicoder.util.ComputerLanguage; +import com.github.simiacryptus.aicoder.util.IndentedText; +import com.github.simiacryptus.aicoder.util.UITools; +import com.intellij.openapi.actionSystem.ActionGroup; +import com.intellij.openapi.actionSystem.AnAction; +import com.intellij.openapi.actionSystem.AnActionEvent; +import com.intellij.openapi.actionSystem.CommonDataKeys; +import com.intellij.openapi.editor.Caret; +import com.intellij.openapi.editor.CaretModel; +import com.intellij.openapi.editor.Document; +import com.intellij.openapi.editor.Editor; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.ArrayList; + +import static com.github.simiacryptus.aicoder.util.UITools.replaceString; +import static java.util.Objects.requireNonNull; + +public class RecentEditsAction extends ActionGroup { + + @Override + public void update(@NotNull AnActionEvent e) { + e.getPresentation().setEnabledAndVisible(isEnabled(e)); + super.update(e); + } + + private static boolean isEnabled(@NotNull AnActionEvent e) { + return null != ComputerLanguage.getComputerLanguage(e); + } + + @Override + public AnAction @NotNull [] getChildren(@Nullable AnActionEvent event) { + assert event != null; + String computerLanguage = requireNonNull(ComputerLanguage.getComputerLanguage(event)).name(); + ArrayList children = new ArrayList<>(); + for (String instruction : AppSettingsState.getInstance().getEditHistory()) { + int id = children.size() + 1; + String text; + if(id<10) { + text = String.format("_%d: %s", id, instruction); + } else { + text = String.format("%d: %s", id, instruction); + } + children.add(new AnAction(text, instruction, null) { + @Override + public void actionPerformed(@NotNull final AnActionEvent event1) { + final Editor editor = event1.getRequiredData(CommonDataKeys.EDITOR); + final CaretModel caretModel = editor.getCaretModel(); + final Caret primaryCaret = caretModel.getPrimaryCaret(); + int selectionStart = primaryCaret.getSelectionStart(); + int selectionEnd = primaryCaret.getSelectionEnd(); + String selectedText = primaryCaret.getSelectedText(); + AppSettingsState settings = AppSettingsState.getInstance(); + settings.addInstructionToHistory(instruction); + CompletionRequest request = settings.createTranslationRequest() + .setInputType(computerLanguage) + .setOutputType(computerLanguage) + .setInstruction(instruction) + .setInputAttribute("type", "before") + .setOutputAttrute("type", "after") + .setInputText(IndentedText.fromString(selectedText).getTextBlock()) + .buildCompletionRequest(); + Caret caret = event1.getData(CommonDataKeys.CARET); + CharSequence indent = UITools.getIndent(caret); + Document document = editor.getDocument(); + UITools.redoableRequest(request, indent, event1, + newText -> replaceString(document, selectionStart, selectionEnd, newText)); + } + }); + } + return children.toArray(AnAction[]::new); + } +} diff --git a/src/main/java/com/github/simiacryptus/aicoder/actions/RedoLast.java b/src/main/java/com/github/simiacryptus/aicoder/actions/RedoLast.java new file mode 100644 index 00000000..2f05bb3e --- /dev/null +++ b/src/main/java/com/github/simiacryptus/aicoder/actions/RedoLast.java @@ -0,0 +1,29 @@ +package com.github.simiacryptus.aicoder.actions; + +import com.github.simiacryptus.aicoder.util.UITools; +import com.intellij.openapi.actionSystem.AnAction; +import com.intellij.openapi.actionSystem.AnActionEvent; +import com.intellij.openapi.actionSystem.CommonDataKeys; +import org.jetbrains.annotations.NotNull; + +public class RedoLast extends AnAction { + + public RedoLast() { + super("_Redo Last", "Redo last", null); + } + + @Override + public void update(@NotNull AnActionEvent e) { + e.getPresentation().setEnabledAndVisible(isEnabled(e)); + super.update(e); + } + + private static boolean isEnabled(@NotNull AnActionEvent e) { + return null != UITools.retry.get(e.getRequiredData(CommonDataKeys.EDITOR).getDocument()); + } + + @Override + public void actionPerformed(@NotNull AnActionEvent e) { + UITools.retry.get(e.getRequiredData(CommonDataKeys.EDITOR).getDocument()).run(); + } +} diff --git a/src/main/java/com/github/simiacryptus/aicoder/actions/RewordCommentAction.java b/src/main/java/com/github/simiacryptus/aicoder/actions/RewordCommentAction.java new file mode 100644 index 00000000..959feeb9 --- /dev/null +++ b/src/main/java/com/github/simiacryptus/aicoder/actions/RewordCommentAction.java @@ -0,0 +1,98 @@ +package com.github.simiacryptus.aicoder.actions; + +import com.github.simiacryptus.aicoder.openai.CompletionRequest; +import com.github.simiacryptus.aicoder.util.ComputerLanguage; +import com.github.simiacryptus.aicoder.config.AppSettingsState; +import com.github.simiacryptus.aicoder.util.psi.PsiUtil; +import com.github.simiacryptus.aicoder.util.StringTools; +import com.github.simiacryptus.aicoder.util.TextBlockFactory; +import com.github.simiacryptus.aicoder.util.UITools; +import com.intellij.openapi.actionSystem.AnAction; +import com.intellij.openapi.actionSystem.AnActionEvent; +import com.intellij.openapi.actionSystem.CommonDataKeys; +import com.intellij.openapi.editor.Caret; +import com.intellij.openapi.editor.Document; +import com.intellij.openapi.editor.Editor; +import com.intellij.psi.PsiElement; +import com.intellij.psi.PsiFile; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.Objects; + +import static com.github.simiacryptus.aicoder.util.UITools.replaceString; + +public class RewordCommentAction extends AnAction { + + @Override + public void update(@NotNull AnActionEvent e) { + e.getPresentation().setEnabledAndVisible(isEnabled(e)); + super.update(e); + } + + private static boolean isEnabled(@NotNull AnActionEvent e) { + return null != getRewordCommentParams(e); + } + + @Nullable + public static RewordCommentParams getRewordCommentParams(@NotNull AnActionEvent e) { + PsiFile psiFile = e.getData(CommonDataKeys.PSI_FILE); + if (null == psiFile) return null; + Caret caret = e.getData(CommonDataKeys.CARET); + if (null == caret) return null; + int selectionStart = caret.getSelectionStart(); + int selectionEnd = caret.getSelectionEnd(); + PsiElement largestIntersectingComment = PsiUtil.getLargestIntersectingComment(psiFile, selectionStart, selectionEnd); + if (largestIntersectingComment == null) return null; + return new RewordCommentParams(caret, largestIntersectingComment); + } + + @Override + public void actionPerformed(@NotNull final AnActionEvent event) { + String humanLanguage = AppSettingsState.getInstance().humanLanguage; + ComputerLanguage computerLanguage = ComputerLanguage.getComputerLanguage(event); + RewordCommentParams rewordCommentParams = getRewordCommentParams(event); + final Editor editor = event.getRequiredData(CommonDataKeys.EDITOR); + AppSettingsState settings = AppSettingsState.getInstance(); + String text = Objects.requireNonNull(rewordCommentParams).largestIntersectingComment.getText(); + TextBlockFactory commentModel = Objects.requireNonNull(computerLanguage).getCommentModel(text); + String commentText = Objects.requireNonNull(commentModel).fromString(text.trim()).stream() + .map(Object::toString) + .map(String::trim) + .filter(x -> !x.isEmpty()) + .reduce((a, b) -> a + "\n" + b).get(); + int startOffset = rewordCommentParams.largestIntersectingComment.getTextRange().getStartOffset(); + int endOffset = rewordCommentParams.largestIntersectingComment.getTextRange().getEndOffset(); + CharSequence indent = UITools.getIndent(rewordCommentParams.caret); + CompletionRequest request = settings.createTranslationRequest() + .setInstruction(UITools.getInstruction("Reword")) + .setInputText(commentText) + .setInputType(humanLanguage) + .setOutputAttrute("type", "input") + .setOutputType(humanLanguage) + .setOutputAttrute("type", "output") + .setOutputAttrute("style", settings.style) + .buildCompletionRequest(); + Document document = editor.getDocument(); + UITools.redoableRequest(request, "", event, + newText -> transformCompletion(commentModel, indent, newText), + newText -> replaceString(document, startOffset, endOffset, newText)); + } + + @NotNull + private static CharSequence transformCompletion(TextBlockFactory commentModel, CharSequence indent, CharSequence result) { + String lineWrapping = StringTools.lineWrapping(result, 120); + return indent.toString() + commentModel.fromString(lineWrapping).withIndent(indent); + } + + public static class RewordCommentParams { + public final Caret caret; + public final PsiElement largestIntersectingComment; + + private RewordCommentParams(Caret caret, PsiElement largestIntersectingComment) { + this.caret = caret; + this.largestIntersectingComment = largestIntersectingComment; + } + + } +} diff --git a/src/main/java/com/github/simiacryptus/aicoder/actions/ToHumanLanguageAction.java b/src/main/java/com/github/simiacryptus/aicoder/actions/ToHumanLanguageAction.java new file mode 100644 index 00000000..bf9b3722 --- /dev/null +++ b/src/main/java/com/github/simiacryptus/aicoder/actions/ToHumanLanguageAction.java @@ -0,0 +1,60 @@ +package com.github.simiacryptus.aicoder.actions; + +import com.github.simiacryptus.aicoder.util.ComputerLanguage; +import com.github.simiacryptus.aicoder.config.AppSettingsState; +import com.github.simiacryptus.aicoder.openai.CompletionRequest; +import com.github.simiacryptus.aicoder.util.UITools; +import com.intellij.openapi.actionSystem.AnAction; +import com.intellij.openapi.actionSystem.AnActionEvent; +import com.intellij.openapi.actionSystem.CommonDataKeys; +import com.intellij.openapi.editor.Caret; +import com.intellij.openapi.editor.CaretModel; +import com.intellij.openapi.editor.Document; +import com.intellij.openapi.editor.Editor; +import org.jetbrains.annotations.NotNull; + +import static com.github.simiacryptus.aicoder.util.UITools.replaceString; +import static java.util.Objects.requireNonNull; + +public class ToHumanLanguageAction extends AnAction { + + public ToHumanLanguageAction() { + super("", "", null); + } + + @Override + public void update(@NotNull AnActionEvent e) { + e.getPresentation().setEnabledAndVisible(isEnabled(e)); + super.update(e); + } + + private static boolean isEnabled(@NotNull AnActionEvent e) { + return null != ComputerLanguage.getComputerLanguage(e); + } + + @Override + public void actionPerformed(@NotNull final AnActionEvent event) { + final Editor editor = event.getRequiredData(CommonDataKeys.EDITOR); + final CaretModel caretModel = editor.getCaretModel(); + final Caret primaryCaret = caretModel.getPrimaryCaret(); + int selectionStart = primaryCaret.getSelectionStart(); + int selectionEnd = primaryCaret.getSelectionEnd(); + String selectedText = primaryCaret.getSelectedText(); + ComputerLanguage language = ComputerLanguage.getComputerLanguage(event); + String computerLanguage = requireNonNull(language).name(); + AppSettingsState settings = AppSettingsState.getInstance(); + CompletionRequest request = settings.createTranslationRequest() + .setInstruction(UITools.getInstruction("Describe this code")) + .setInputText(selectedText) + .setInputType(computerLanguage) + .setInputAttribute("type", "input") + .setOutputType(AppSettingsState.getInstance().humanLanguage.toLowerCase()) + .setOutputAttrute("type", "output") + .setOutputAttrute("style", settings.style) + .buildCompletionRequest(); + Caret caret = event.getData(CommonDataKeys.CARET); + CharSequence indent = UITools.getIndent(caret); + Document document = editor.getDocument(); + UITools.redoableRequest(request, indent, event, newText -> replaceString(document, selectionStart, selectionEnd, newText)); + } +} diff --git a/src/main/java/com/github/simiacryptus/aicoder/config/AppSettingsComponent.java b/src/main/java/com/github/simiacryptus/aicoder/config/AppSettingsComponent.java index 105405a5..c8b8936f 100644 --- a/src/main/java/com/github/simiacryptus/aicoder/config/AppSettingsComponent.java +++ b/src/main/java/com/github/simiacryptus/aicoder/config/AppSettingsComponent.java @@ -1,6 +1,5 @@ package com.github.simiacryptus.aicoder.config; -import com.fasterxml.jackson.databind.JsonNode; import com.github.simiacryptus.aicoder.util.StyleUtil; import com.github.simiacryptus.aicoder.openai.OpenAI_API; import com.intellij.openapi.diagnostic.Logger; @@ -15,79 +14,68 @@ import java.awt.event.ActionEvent; import java.util.Arrays; -public class AppSettingsComponent extends SimpleSettingsComponent { +public class AppSettingsComponent { private static final Logger log = Logger.getInstance(AppSettingsComponent.class); - @Name("API Base") - public final JBTextField apiBase = new JBTextField(); - @Name("API Key") - public final JBPasswordField apiKey = new JBPasswordField(); - @Name("Model") - public final JComponent model = getModelSelector(); - - @NotNull - private static JComponent getModelSelector() { - AppSettingsState settings = AppSettingsState.getInstance(); - CharSequence apiKey = settings.apiKey; - if (null != apiKey && apiKey.toString().trim().length() > 0) { - try { - ComboBox comboBox = new ComboBox<>(new CharSequence[]{settings.model}); - OpenAI_API.onSuccess(OpenAI_API.INSTANCE.getEngines(), engines -> { - JsonNode data = engines.get("data"); - CharSequence[] items = new CharSequence[data.size()]; - for (int i = 0; i < data.size(); i++) { - items[i] = data.get(i).get("id").asText(); - } - Arrays.sort(items); - Arrays.stream(items).forEach(comboBox::addItem); - }); - return comboBox; - } catch (Throwable e) { - log.warn(e); - } - } - return new JBTextField(); - } @Name("Style") public final JBTextField style = new JBTextField(); - @Name("Human Language") - public final JBTextField humanLanguage = new JBTextField(); - @Name("Max Prompt (Characters)") - public final JBTextField maxPrompt = new JBTextField(); - @Name("Max Tokens") - public final JBTextField maxTokens = new JBTextField(); - @Name("History Limit") - public final JBTextField historyLimit = new JBTextField(); - @Name("Temperature") - public final JBTextField temperature = new JBTextField(); + @SuppressWarnings("unused") public final JButton randomizeStyle = new JButton(new AbstractAction("Randomize Style") { @Override public void actionPerformed(ActionEvent e) { style.setText(StyleUtil.randomStyle()); } }); + @SuppressWarnings("unused") public final JButton testStyle = new JButton(new AbstractAction("Test Style") { @Override public void actionPerformed(ActionEvent e) { StyleUtil.demoStyle(style.getText()); } }); + @Name("Token Counter") public final JBTextField tokenCounter = new JBTextField(); + @SuppressWarnings("unused") public final JButton clearCounter = new JButton(new AbstractAction("Clear Token Counter") { @Override public void actionPerformed(ActionEvent e) { tokenCounter.setText("0"); } }); + + @SuppressWarnings("unused") + @Name("Human Language") + public final JBTextField humanLanguage = new JBTextField(); + @SuppressWarnings("unused") + @Name("History Limit") + public final JBTextField historyLimit = new JBTextField(); + @SuppressWarnings("unused") @Name("Developer Tools") public final JBCheckBox devActions = new JBCheckBox(); + @SuppressWarnings("unused") @Name("API Log Level") - public final ComboBox apiLogLevel = new ComboBox(Arrays.stream(LogLevel.values()).map(x -> x.name()).toArray(CharSequence[]::new)); + public final ComboBox apiLogLevel = new ComboBox<>(Arrays.stream(LogLevel.values()).map(Enum::name).toArray(String[]::new)); + + @SuppressWarnings("unused") + @Name("Temperature") + public final JBTextField temperature = new JBTextField(); + @SuppressWarnings("unused") + @Name("Max Tokens") + public final JBTextField maxTokens = new JBTextField(); + @SuppressWarnings("unused") + @Name("Max Prompt (Characters)") + public final JBTextField maxPrompt = new JBTextField(); + @SuppressWarnings("unused") + @Name("Model") + public final JComponent model = OpenAI_API.INSTANCE.getModelSelector(); -// @Name("API Envelope") -// public final ComboBox translationRequestTemplate = new ComboBox(Arrays.stream(TranslationRequestTemplate.values()).map(x->x.name()).toArray(String[]::new)); + @Name("API Key") + public final JBPasswordField apiKey = new JBPasswordField(); + @SuppressWarnings("unused") + @Name("API Base") + public final JBTextField apiBase = new JBTextField(); public AppSettingsComponent() { tokenCounter.setEditable(false); diff --git a/src/main/java/com/github/simiacryptus/aicoder/config/AppSettingsConfigurable.java b/src/main/java/com/github/simiacryptus/aicoder/config/AppSettingsConfigurable.java index 80ad3a45..7c600272 100644 --- a/src/main/java/com/github/simiacryptus/aicoder/config/AppSettingsConfigurable.java +++ b/src/main/java/com/github/simiacryptus/aicoder/config/AppSettingsConfigurable.java @@ -1,11 +1,14 @@ package com.github.simiacryptus.aicoder.config; +import com.github.simiacryptus.aicoder.util.UITools; import com.intellij.openapi.options.Configurable; +import com.intellij.util.ui.FormBuilder; import org.jetbrains.annotations.Nls; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import javax.swing.*; +import java.util.Objects; /** * Provides controller functionality for application settings. @@ -13,6 +16,7 @@ public class AppSettingsConfigurable implements Configurable { @Nullable AppSettingsComponent settingsComponent; + private volatile @Nullable JPanel mainPanel = null; public AppSettingsConfigurable() { // A default constructor with no arguments is required because this implementation is registered as an applicationConfigurable EP @@ -26,31 +30,46 @@ public AppSettingsConfigurable() { @Override public JComponent getPreferredFocusedComponent() { - return settingsComponent.getPreferredFocusedComponent(); + return Objects.requireNonNull(settingsComponent).getPreferredFocusedComponent(); } @Nullable @Override public JComponent createComponent() { - settingsComponent = new AppSettingsComponent(); - return settingsComponent.getPanel(); + if (null == mainPanel) { + synchronized (this) { + if (null == mainPanel) { + FormBuilder formBuilder = FormBuilder.createFormBuilder(); + settingsComponent = new AppSettingsComponent(); + UITools.addFields(settingsComponent, formBuilder); + mainPanel = formBuilder.addComponentFillVertically(new JPanel(), 0).getPanel(); + } + } + } + return mainPanel; } @Override public boolean isModified() { AppSettingsState buffer = new AppSettingsState(); - this.settingsComponent.getProperties(buffer); + if (this.settingsComponent != null) { + UITools.readUI(this.settingsComponent, buffer); + } return !buffer.equals(AppSettingsState.getInstance()); } @Override public void apply() { - this.settingsComponent.getProperties(AppSettingsState.getInstance()); + if (this.settingsComponent != null) { + UITools.readUI(this.settingsComponent, AppSettingsState.getInstance()); + } } @Override public void reset() { - settingsComponent.setProperties(AppSettingsState.getInstance()); + if (settingsComponent != null) { + UITools.writeUI(settingsComponent, AppSettingsState.getInstance()); + } } @Override diff --git a/src/main/java/com/github/simiacryptus/aicoder/config/AppSettingsState.java b/src/main/java/com/github/simiacryptus/aicoder/config/AppSettingsState.java index 2df7891b..f5c7e4fe 100644 --- a/src/main/java/com/github/simiacryptus/aicoder/config/AppSettingsState.java +++ b/src/main/java/com/github/simiacryptus/aicoder/config/AppSettingsState.java @@ -32,14 +32,15 @@ public class AppSettingsState implements PersistentStateComponent mostUsedHistory = new HashMap<>(); - private @NotNull List mostRecentHistory = new ArrayList<>(); + private final @NotNull Map mostUsedHistory = new HashMap<>(); + private final @NotNull List mostRecentHistory = new ArrayList<>(); public int historyLimit = 10; public @NotNull String humanLanguage = "English"; public int maxPrompt = 5000; - public TranslationRequestTemplate translationRequestTemplate = TranslationRequestTemplate.XML; - public LogLevel apiLogLevel = LogLevel.Debug; + public @NotNull TranslationRequestTemplate translationRequestTemplate = TranslationRequestTemplate.XML; + public @NotNull LogLevel apiLogLevel = LogLevel.Debug; public boolean devActions = false; public AppSettingsState() { @@ -53,7 +54,7 @@ public TranslationRequest createTranslationRequest() { return translationRequestTemplate.get(this); } - public CompletionRequest createCompletionRequest() { + public @NotNull CompletionRequest createCompletionRequest() { return new CompletionRequest( "", temperature, @@ -97,7 +98,7 @@ public int hashCode() { return Objects.hash(apiBase, apiKey, model, maxTokens, temperature, translationRequestTemplate, apiLogLevel, devActions, style); } - public void addInstructionToHistory(CharSequence instruction) { + public void addInstructionToHistory(@NotNull CharSequence instruction) { synchronized (mostRecentHistory) { mostRecentHistory.add(instruction.toString()); while(mostRecentHistory.size() > historyLimit) { diff --git a/src/main/java/com/github/simiacryptus/aicoder/config/SimpleSettingsComponent.java b/src/main/java/com/github/simiacryptus/aicoder/config/SimpleSettingsComponent.java deleted file mode 100644 index fd3f283b..00000000 --- a/src/main/java/com/github/simiacryptus/aicoder/config/SimpleSettingsComponent.java +++ /dev/null @@ -1,165 +0,0 @@ -package com.github.simiacryptus.aicoder.config; - -import com.intellij.openapi.diagnostic.Logger; -import com.intellij.openapi.ui.ComboBox; -import com.intellij.ui.components.JBLabel; -import com.intellij.util.ui.FormBuilder; -import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; - -import javax.swing.*; -import javax.swing.text.JTextComponent; -import java.lang.reflect.Field; -import java.lang.reflect.Modifier; - -public class SimpleSettingsComponent { - private static final Logger log = Logger.getInstance(SimpleSettingsComponent.class); - protected final boolean verbose = false; - private volatile @Nullable JPanel mainPanel = null; - - /** - * Builds the main panel for the form. - * - * @return the main panel for the form - */ - private JPanel buildMainPanel() { - FormBuilder formBuilder = FormBuilder.createFormBuilder(); - for (Field field : this.getClass().getDeclaredFields()) { - if(Modifier.isStatic(field.getModifiers())) continue; - try { - field.setAccessible(true); - Name nameAnnotation = field.getDeclaredAnnotation(Name.class); - JComponent component = (JComponent) field.get(this); - if (null == component) continue; - if (nameAnnotation != null) { - formBuilder.addLabeledComponent(new JBLabel(nameAnnotation.value() + ": "), component, 1, false); - } else { - formBuilder.addComponentToRightColumn(component, 1); - } - } catch (IllegalAccessException e) { - throw new RuntimeException(e); - } catch (Throwable e) { - log.warn("Error processing " + field.getName(), e); - } - } - return formBuilder.addComponentFillVertically(new JPanel(), 0).getPanel(); - } - - public void getProperties(@NotNull T settings) { - for (Field settingsField : settings.getClass().getDeclaredFields()) { - settingsField.setAccessible(true); - String settingsFieldName = settingsField.getName(); - try { - Object newSettingsValue = null; - Field uiField = this.getClass().getDeclaredField(settingsFieldName); - Object uiFieldVal = uiField.get(this); - switch (settingsField.getType().getName()) { - case "java.lang.String": - if (uiFieldVal instanceof JTextComponent) { - newSettingsValue = ((JTextComponent) uiFieldVal).getText(); - } else if (uiFieldVal instanceof ComboBox) { - newSettingsValue = ((ComboBox) uiFieldVal).getItem(); - } - break; - case "int": - if (uiFieldVal instanceof JTextComponent) { - newSettingsValue = Integer.parseInt(((JTextComponent) uiFieldVal).getText()); - } - break; - case "long": - if (uiFieldVal instanceof JTextComponent) { - newSettingsValue = Long.parseLong(((JTextComponent) uiFieldVal).getText()); - } - break; - case "double": - if (uiFieldVal instanceof JTextComponent) { - newSettingsValue = Double.parseDouble(((JTextComponent) uiFieldVal).getText()); - } - break; - case "boolean": - if (uiFieldVal instanceof JCheckBox) { - newSettingsValue = ((JCheckBox) uiFieldVal).isSelected(); - } else if (uiFieldVal instanceof JTextComponent) { - newSettingsValue = Boolean.parseBoolean(((JTextComponent) uiFieldVal).getText()); - } - break; - default: - - if (java.lang.Enum.class.isAssignableFrom(settingsField.getType())) { - if (uiFieldVal instanceof ComboBox) { - ComboBox comboBox = (ComboBox) uiFieldVal; - CharSequence item = comboBox.getItem(); - newSettingsValue = Enum.valueOf((Class) settingsField.getType(), item.toString()); - } - } - break; - } - settingsField.set(settings, newSettingsValue); - } catch (Throwable e) { - if (verbose) new RuntimeException("Error processing " + settingsField, e).printStackTrace(); - } - } - } - - public void setProperties(@NotNull T settings) { - for (Field settingsField : settings.getClass().getDeclaredFields()) { - settingsField.setAccessible(true); - String fieldName = settingsField.getName(); - try { - Field uiField = this.getClass().getDeclaredField(fieldName); - Object settingsVal = settingsField.get(settings); - if(null == settingsVal) continue; - Object uiVal = uiField.get(this); - switch (settingsField.getType().getName()) { - case "java.lang.String": - if (uiVal instanceof JTextComponent) { - ((JTextComponent) uiVal).setText((String) settingsVal); - } else if (uiVal instanceof ComboBox) { - ((ComboBox) uiVal).setItem(settingsVal.toString()); - } - break; - case "int": - if (uiVal instanceof JTextComponent) { - ((JTextComponent) uiVal).setText(Integer.toString((Integer) settingsVal)); - } - break; - case "long": - if (uiVal instanceof JTextComponent) { - ((JTextComponent) uiVal).setText(Long.toString((Integer) settingsVal)); - } - break; - case "double": - if (uiVal instanceof JTextComponent) { - ((JTextComponent) uiVal).setText(Double.toString(((Double) settingsVal))); - } - break; - case "boolean": - if (uiVal instanceof JCheckBox) { - ((JCheckBox) uiVal).setSelected(((Boolean) settingsVal)); - } else if (uiVal instanceof JTextComponent) { - ((JTextComponent) uiVal).setText(Boolean.toString((Boolean) settingsVal)); - } - break; - default: - if (uiVal instanceof ComboBox) { - ((ComboBox) uiVal).setItem(settingsVal.toString()); - } - break; - } - } catch (Throwable e) { - if (verbose) new RuntimeException("Error processing " + settingsField, e).printStackTrace(); - } - } - } - - public JPanel getPanel() { - if (null == mainPanel) { - synchronized (this) { - if (null == mainPanel) { - mainPanel = buildMainPanel(); - } - } - } - return mainPanel; - } -} diff --git a/src/main/java/com/github/simiacryptus/aicoder/openai/ApiError.java b/src/main/java/com/github/simiacryptus/aicoder/openai/ApiError.java index 637ec47a..2d922498 100644 --- a/src/main/java/com/github/simiacryptus/aicoder/openai/ApiError.java +++ b/src/main/java/com/github/simiacryptus/aicoder/openai/ApiError.java @@ -1,14 +1,20 @@ package com.github.simiacryptus.aicoder.openai; public class ApiError { + @SuppressWarnings("unused") public String message; + @SuppressWarnings("unused") public String type; + @SuppressWarnings("unused") public String param; + @SuppressWarnings("unused") public Double code; + @SuppressWarnings("unused") public ApiError() { } + @SuppressWarnings("unused") public ApiError(String message, String type, String param, Double code) { this.message = message; this.type = type; diff --git a/src/main/java/com/github/simiacryptus/aicoder/openai/Choice.java b/src/main/java/com/github/simiacryptus/aicoder/openai/Choice.java index dbf21965..bdf7d9c9 100644 --- a/src/main/java/com/github/simiacryptus/aicoder/openai/Choice.java +++ b/src/main/java/com/github/simiacryptus/aicoder/openai/Choice.java @@ -2,13 +2,18 @@ public class Choice { public String text; + @SuppressWarnings("unused") public int index; + @SuppressWarnings("unused") public LogProbs logprobs; + @SuppressWarnings("unused") public String finish_reason; + @SuppressWarnings("unused") public Choice() { } + @SuppressWarnings("unused") public Choice(String text, int index, LogProbs logprobs, String finish_reason) { this.text = text; this.index = index; diff --git a/src/main/java/com/github/simiacryptus/aicoder/openai/CompletionRequest.java b/src/main/java/com/github/simiacryptus/aicoder/openai/CompletionRequest.java index 4ee31536..c0f62cd8 100644 --- a/src/main/java/com/github/simiacryptus/aicoder/openai/CompletionRequest.java +++ b/src/main/java/com/github/simiacryptus/aicoder/openai/CompletionRequest.java @@ -1,30 +1,55 @@ package com.github.simiacryptus.aicoder.openai; -import com.github.simiacryptus.aicoder.util.IndentedText; -import com.google.common.util.concurrent.ListenableFuture; -import com.intellij.openapi.project.Project; +import com.github.simiacryptus.aicoder.config.AppSettingsState; +import com.github.simiacryptus.aicoder.util.UITools; +import com.intellij.util.ui.FormBuilder; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; +import javax.swing.*; import java.util.ArrayList; import java.util.Arrays; -import java.util.Objects; - -import static com.github.simiacryptus.aicoder.util.StringTools.stripPrefix; -import static com.github.simiacryptus.aicoder.util.StringTools.stripUnbalancedTerminators; /** * The CompletionRequest class is used to create a request for completion of a given prompt. */ public class CompletionRequest { + + public static class CompletionRequestWithModel extends CompletionRequest { + + public String model; + + public CompletionRequestWithModel(String prompt, double temperature, int max_tokens, Integer logprobs, boolean echo, String model, CharSequence... stop) { + super(prompt, temperature, max_tokens, logprobs, echo, stop); + this.model = model; + } + + public CompletionRequestWithModel(CompletionRequestWithModel other) { + super(other); + this.model = other.model; + } + + public CompletionRequestWithModel(CompletionRequest other, String model) { + super(other); + this.model = model; + } + } public String prompt; - public String suffix = null; + public @Nullable String suffix = null; + @SuppressWarnings("unused") public double temperature; + @SuppressWarnings("unused") public int max_tokens; - public CharSequence[] stop; + public CharSequence @Nullable [] stop; + @SuppressWarnings("unused") public Integer logprobs; + @SuppressWarnings("unused") public boolean echo; + @SuppressWarnings("unused") + public CompletionRequest() { + } + public CompletionRequest(String prompt, double temperature, int max_tokens, Integer logprobs, boolean echo, CharSequence... stop) { this.prompt = prompt; this.temperature = temperature; @@ -33,46 +58,61 @@ public CompletionRequest(String prompt, double temperature, int max_tokens, Inte this.logprobs = logprobs; this.echo = echo; } - - @NotNull - public ListenableFuture complete(@Nullable Project project, CharSequence indent) { - return OpenAI_API.map(OpenAI_API.INSTANCE.complete(project, this), response -> response - .getFirstChoice() - .map(Objects::toString) - .map(String::trim) - .map(completion -> stripPrefix(completion, this.prompt.trim())) - .map(String::trim) - .map(completion -> stripUnbalancedTerminators(completion)) - .map(IndentedText::fromString) - .map(indentedText -> indentedText.withIndent(indent)) - .map(IndentedText::toString) - .map(indentedText -> indent + indentedText) - .orElse("")); + public CompletionRequest(CompletionRequest other) { + this.prompt = other.prompt; + this.temperature = other.temperature; + this.max_tokens = other.max_tokens; + this.stop = other.stop; + this.logprobs = other.logprobs; + this.echo = other.echo; } + public @NotNull CompletionRequest appendPrompt(CharSequence prompt) { this.prompt = this.prompt + prompt; return this; } - public @NotNull CompletionRequest addStops(@NotNull CharSequence... newStops) { + public @NotNull CompletionRequest addStops(@NotNull CharSequence @NotNull ... newStops) { ArrayList stops = new ArrayList<>(); for (CharSequence x : newStops) { - if (x != null) { - if (x.length() > 0) { - stops.add(x); - } + if (x.length() > 0) { + stops.add(x); } } if (!stops.isEmpty()) { - if(null != this.stop) Arrays.stream(this.stop).forEach(stops::add); + if (null != this.stop) Arrays.stream(this.stop).forEach(stops::add); this.stop = stops.stream().distinct().toArray(CharSequence[]::new); } return this; } - public CompletionRequest setSuffix(CharSequence suffix) { - this.suffix = suffix.toString(); + public @NotNull CompletionRequest setSuffix(@Nullable CharSequence suffix) { + this.suffix = null==suffix?null:suffix.toString(); return this; } + + public @NotNull CompletionRequestWithModel showModelEditDialog() { + FormBuilder formBuilder = FormBuilder.createFormBuilder(); + CompletionRequestWithModel withModel = new CompletionRequestWithModel(this, AppSettingsState.getInstance().model); + InteractiveRequest ui = new InteractiveRequest(withModel); + UITools.addFields(ui, formBuilder); + JPanel mainPanel = formBuilder.addComponentFillVertically(new JPanel(), 0).getPanel(); + UITools.writeUI(ui, withModel); + Object[] options = {"OK"}; + if (JOptionPane.showOptionDialog( + null, + mainPanel, + "OpenAI Completion Request", + JOptionPane.NO_OPTION, + JOptionPane.PLAIN_MESSAGE, + null, + options, + options[0]) == JOptionPane.OK_OPTION) { + UITools.readUI(ui, withModel); + return withModel; + } else { + return withModel; + } + } } diff --git a/src/main/java/com/github/simiacryptus/aicoder/openai/CompletionResponse.java b/src/main/java/com/github/simiacryptus/aicoder/openai/CompletionResponse.java index 449ebfee..71ae6452 100644 --- a/src/main/java/com/github/simiacryptus/aicoder/openai/CompletionResponse.java +++ b/src/main/java/com/github/simiacryptus/aicoder/openai/CompletionResponse.java @@ -6,13 +6,19 @@ import java.util.Optional; public class CompletionResponse { + @SuppressWarnings("unused") public String id; + @SuppressWarnings("unused") public String object; + @SuppressWarnings("unused") public int created; + @SuppressWarnings("unused") public String model; public Choice[] choices; + @SuppressWarnings("unused") public ApiError error; + @SuppressWarnings("unused") public Usage usage; public CompletionResponse() { diff --git a/src/main/java/com/github/simiacryptus/aicoder/openai/Engine.java b/src/main/java/com/github/simiacryptus/aicoder/openai/Engine.java index fe3b49b0..b68260ed 100644 --- a/src/main/java/com/github/simiacryptus/aicoder/openai/Engine.java +++ b/src/main/java/com/github/simiacryptus/aicoder/openai/Engine.java @@ -1,18 +1,28 @@ package com.github.simiacryptus.aicoder.openai; public class Engine { + @SuppressWarnings("unused") public String id; + @SuppressWarnings("unused") public boolean ready; + @SuppressWarnings("unused") public String owner; + @SuppressWarnings("unused") public String object; + @SuppressWarnings("unused") public Integer created; + @SuppressWarnings("unused") public String permissions; + @SuppressWarnings("unused") public Integer replicas; + @SuppressWarnings("unused") public Integer max_replicas; + @SuppressWarnings("unused") public Engine() { } + @SuppressWarnings("unused") public Engine(String id, boolean ready, String owner, String object, Integer created, String permissions, Integer replicas, Integer max_replicas) { this.id = id; this.ready = ready; diff --git a/src/main/java/com/github/simiacryptus/aicoder/openai/InteractiveRequest.java b/src/main/java/com/github/simiacryptus/aicoder/openai/InteractiveRequest.java new file mode 100644 index 00000000..d1a29e6a --- /dev/null +++ b/src/main/java/com/github/simiacryptus/aicoder/openai/InteractiveRequest.java @@ -0,0 +1,65 @@ +package com.github.simiacryptus.aicoder.openai; + +import com.github.simiacryptus.aicoder.config.AppSettingsState; +import com.github.simiacryptus.aicoder.config.Name; +import com.github.simiacryptus.aicoder.util.UITools; +import com.google.common.util.concurrent.FutureCallback; +import com.google.common.util.concurrent.Futures; +import com.google.common.util.concurrent.ListenableFuture; +import com.intellij.ui.components.JBTextArea; +import com.intellij.ui.components.JBTextField; +import org.jetbrains.annotations.NotNull; + +import javax.swing.*; +import java.awt.event.ActionEvent; +import java.util.Arrays; + +public class InteractiveRequest { + @SuppressWarnings("unused") + @Name("Prompt") + public final JBTextArea prompt = new JBTextArea(10, 40); + @SuppressWarnings("unused") + @Name("Suffix") + public final JBTextArea suffix = new JBTextArea(2, 40); + @SuppressWarnings("unused") + @Name("Model") + public final JComponent model = OpenAI_API.INSTANCE.getModelSelector(); + @SuppressWarnings("unused") + @Name("Temperature") + public final JBTextField temperature = new JBTextField(8); + @SuppressWarnings("unused") + @Name("Max Tokens") + public final JBTextField max_tokens = new JBTextField(8); + public final @NotNull JButton testRequest; + + public InteractiveRequest(@NotNull CompletionRequest parent) { + testRequest = new JButton(new AbstractAction("Test Request") { + @Override + public void actionPerformed(ActionEvent e) { + CompletionRequest.CompletionRequestWithModel withModel = new CompletionRequest.CompletionRequestWithModel(parent, AppSettingsState.getInstance().model); + UITools.readUI(InteractiveRequest.this, withModel); + ListenableFuture future = OpenAI_API.INSTANCE.complete(null, withModel, ""); + testRequest.setEnabled(false); + Futures.addCallback(future, new FutureCallback<>() { + @Override + public void onSuccess(@NotNull CharSequence result) { + testRequest.setEnabled(true); + String text = result.toString(); + int rows = Math.min(50, text.split("\n").length); + int columns = Math.min(200, Arrays.stream(text.split("\n")).mapToInt(String::length).max().getAsInt()); + JBTextArea area = new JBTextArea(rows, columns); + area.setText(text); + area.setEditable(false); + JOptionPane.showMessageDialog(null, area, "Test Output", JOptionPane.PLAIN_MESSAGE); + } + + @Override + public void onFailure(Throwable t) { + testRequest.setEnabled(true); + UITools.handle(t); + } + }, OpenAI_API.INSTANCE.pool); + } + }); + } +} diff --git a/src/main/java/com/github/simiacryptus/aicoder/openai/LogProbs.java b/src/main/java/com/github/simiacryptus/aicoder/openai/LogProbs.java index 4be2e717..b49c61fa 100644 --- a/src/main/java/com/github/simiacryptus/aicoder/openai/LogProbs.java +++ b/src/main/java/com/github/simiacryptus/aicoder/openai/LogProbs.java @@ -3,14 +3,20 @@ import com.fasterxml.jackson.databind.node.ObjectNode; public class LogProbs { + @SuppressWarnings("unused") public CharSequence[] tokens; + @SuppressWarnings("unused") public double[] token_logprobs; + @SuppressWarnings("unused") public ObjectNode[] top_logprobs; + @SuppressWarnings("unused") public int[] text_offset; + @SuppressWarnings("unused") public LogProbs() { } + @SuppressWarnings("unused") public LogProbs(CharSequence[] tokens, double[] token_logprobs, ObjectNode[] top_logprobs, int[] text_offset) { this.tokens = tokens; this.token_logprobs = token_logprobs; diff --git a/src/main/java/com/github/simiacryptus/aicoder/openai/OpenAI_API.java b/src/main/java/com/github/simiacryptus/aicoder/openai/OpenAI_API.java index ea82e63f..e2d551cf 100644 --- a/src/main/java/com/github/simiacryptus/aicoder/openai/OpenAI_API.java +++ b/src/main/java/com/github/simiacryptus/aicoder/openai/OpenAI_API.java @@ -1,11 +1,14 @@ package com.github.simiacryptus.aicoder.openai; import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.MapperFeature; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.SerializationFeature; import com.fasterxml.jackson.databind.node.ObjectNode; import com.github.simiacryptus.aicoder.config.AppSettingsState; +import com.github.simiacryptus.aicoder.util.IndentedText; +import com.github.simiacryptus.aicoder.util.StringTools; import com.github.simiacryptus.aicoder.util.UITools; import com.google.common.base.Function; import com.google.common.util.concurrent.*; @@ -17,6 +20,8 @@ import com.intellij.openapi.progress.Task; import com.intellij.openapi.progress.util.AbstractProgressIndicatorBase; import com.intellij.openapi.project.Project; +import com.intellij.openapi.ui.ComboBox; +import com.intellij.ui.components.JBTextField; import com.jetbrains.rd.util.LogLevel; import org.apache.http.HttpEntity; import org.apache.http.HttpResponse; @@ -29,8 +34,11 @@ import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; +import javax.swing.*; import java.io.IOException; +import java.util.Arrays; import java.util.Map; +import java.util.Objects; import java.util.concurrent.Executors; import java.util.function.Consumer; @@ -41,17 +49,60 @@ public final class OpenAI_API { private static final Logger log = Logger.getInstance(OpenAI_API.class); public static final OpenAI_API INSTANCE = new OpenAI_API(); - public final ListeningExecutorService pool; - private transient AppSettingsState settings = null; + public final @NotNull ListeningExecutorService pool; + private transient @Nullable AppSettingsState settings = null; - protected AppSettingsState getSettingsState() { + private transient ComboBox comboBox = null; + + @NotNull + public JComponent getModelSelector() { + if (null != comboBox) return comboBox; + AppSettingsState settings = AppSettingsState.getInstance(); + CharSequence apiKey = settings.apiKey; + if (apiKey.toString().trim().length() > 0) { + try { + comboBox = new ComboBox<>(new CharSequence[]{settings.model}); + onSuccess(INSTANCE.getEngines(), engines -> { + JsonNode data = engines.get("data"); + CharSequence[] items = new CharSequence[data.size()]; + for (int i = 0; i < data.size(); i++) { + items[i] = data.get(i).get("id").asText(); + } + Arrays.sort(items); + Arrays.stream(items).forEach(comboBox::addItem); + }); + return comboBox; + } catch (Throwable e) { + log.warn(e); + } + } + return new JBTextField(); + } + + @NotNull + public ListenableFuture complete(@Nullable Project project, @NotNull CompletionRequest request, CharSequence indent) { + return map(complete(project, request), response -> response + .getFirstChoice() + .map(Objects::toString) + .map(String::trim) + .map(completion -> stripPrefix(completion, request.prompt.trim())) + .map(String::trim) + .map(StringTools::stripUnbalancedTerminators) + .map(IndentedText::fromString) + .map(indentedText -> indentedText.withIndent(indent)) + .map(IndentedText::toString) + .map(indentedText -> indent + indentedText) + .orElse("")); + } + + private AppSettingsState getSettingsState() { if (null == this.settings) { this.settings = AppSettingsState.getInstance(); } return settings; } - protected OpenAI_API() { + private OpenAI_API() { this.pool = MoreExecutors.listeningDecorator(Executors.newSingleThreadExecutor()); } @@ -60,32 +111,56 @@ public ListenableFuture getEngines() { return pool.submit(() -> getMapper().readValue(get(getSettingsState().apiBase + "/engines"), ObjectNode.class)); } - protected String post(String url, @NotNull String body) throws IOException, InterruptedException { + private String post(String url, @NotNull String body) throws IOException, InterruptedException { return post(url, body, 3); } - public ListenableFuture complete(@Nullable Project project, @NotNull CompletionRequest completionRequest) { + private @NotNull ListenableFuture complete(@Nullable Project project, @NotNull CompletionRequest completionRequest) { AppSettingsState settings = getSettingsState(); - if (null != completionRequest.suffix) { - if (completionRequest.suffix.trim().length() == 0) { - completionRequest.setSuffix(null); + CompletionRequest.CompletionRequestWithModel withModel; + if (!(completionRequest instanceof CompletionRequest.CompletionRequestWithModel)) { + if (!AppSettingsState.getInstance().devActions) { + withModel = new CompletionRequest.CompletionRequestWithModel(completionRequest, AppSettingsState.getInstance().model); + } else { + withModel = completionRequest.showModelEditDialog(); + } + } else { + withModel = (CompletionRequest.CompletionRequestWithModel) completionRequest; + } + + if (null != withModel.suffix) { + if (withModel.suffix.trim().length() == 0) { + withModel.setSuffix(null); } else { - completionRequest.echo = false; + withModel.echo = false; } } - if (null != completionRequest.stop && completionRequest.stop.length == 0) { - completionRequest.stop = null; + if (null != withModel.stop && withModel.stop.length == 0) { + withModel.stop = null; } - if (completionRequest.prompt.length() > settings.maxPrompt) - throw new IllegalArgumentException("Prompt too long:" + completionRequest.prompt.length() + " chars"); + if (withModel.prompt.length() > settings.maxPrompt) + throw new IllegalArgumentException("Prompt too long:" + withModel.prompt.length() + " chars"); + return complete(project, new CompletionRequest(withModel), settings, withModel.model); + } + + @NotNull + private ListenableFuture complete(@Nullable Project project, @NotNull CompletionRequest completionRequest, @NotNull AppSettingsState settings, @NotNull final String model) { return OpenAI_API.map(moderateAsync(project, completionRequest.prompt), x -> { try { Task.WithResult task = new Task.WithResult<>(project, "OpenAI Text Completion", false) { @Override - protected CompletionResponse compute(@NotNull ProgressIndicator indicator) throws Exception { + protected @NotNull CompletionResponse compute(@NotNull ProgressIndicator indicator) throws Exception { try { + if (completionRequest.suffix == null) { + log(settings.apiLogLevel, String.format("Text Completion Request\nPrefix:\n\t%s\n", + completionRequest.prompt.replace("\n", "\n\t"))); + } else { + log(settings.apiLogLevel, String.format("Text Completion Request\nPrefix:\n\t%s\nSuffix:\n\t%s\n", + completionRequest.prompt.replace("\n", "\n\t"), + completionRequest.suffix.replace("\n", "\n\t"))); + } String request = getMapper().writeValueAsString(completionRequest); - String result = post(settings.apiBase + "/engines/" + settings.model + "/completions", request); + String result = post(settings.apiBase + "/engines/" + model + "/completions", request); JsonObject jsonObject = new Gson().fromJson(result, JsonObject.class); if (jsonObject.has("error")) { JsonObject errorObject = jsonObject.getAsJsonObject("error"); @@ -99,13 +174,10 @@ protected CompletionResponse compute(@NotNull ProgressIndicator indicator) throw } String completionResult = stripPrefix(completionResponse.getFirstChoice().orElse("").toString().trim(), completionRequest.prompt.trim()); if (completionRequest.suffix == null) { - log(settings.apiLogLevel, String.format("Text Completion Request\nPrefix:\n\t%s\nCompletion:\n\t%s", - completionRequest.prompt.replace("\n", "\n\t"), + log(settings.apiLogLevel, String.format("Text Completion Completion:\n\t%s", completionResult.replace("\n", "\n\t"))); } else { - log(settings.apiLogLevel, String.format("Text Completion Request\nPrefix:\n\t%s\nSuffix:\n\t%s\nCompletion:\n\t%s", - completionRequest.prompt.replace("\n", "\n\t"), - completionRequest.suffix.replace("\n", "\n\t"), + log(settings.apiLogLevel, String.format("Text Completion Completion:\n\t%s", completionResult.replace("\n", "\n\t"))); } return completionResponse; @@ -129,12 +201,12 @@ protected CompletionResponse compute(@NotNull ProgressIndicator indicator) throw } public static - ListenableFuture map(ListenableFuture moderateAsync, Function o) { + @NotNull ListenableFuture map(@NotNull ListenableFuture moderateAsync, @NotNull Function o) { return Futures.transform(moderateAsync, o, INSTANCE.pool); } - public static void onSuccess(ListenableFuture moderateAsync, Consumer o) { - Futures.addCallback(moderateAsync, new FutureCallback() { + public static void onSuccess(@NotNull ListenableFuture moderateAsync, @NotNull Consumer o) { + Futures.addCallback(moderateAsync, new FutureCallback<>() { @Override public void onSuccess(I result) { o.accept(result); @@ -147,7 +219,7 @@ public void onFailure(Throwable t) { }, INSTANCE.pool); } - private void log(LogLevel level, String msg) { + private void log(@NotNull LogLevel level, @NotNull String msg) { String message = msg.trim().replace("\n", "\n\t"); switch (level) { case Error: @@ -167,9 +239,9 @@ private void log(LogLevel level, String msg) { @NotNull private ListenableFuture moderateAsync(@Nullable Project project, @NotNull String text) { - Task.WithResult, Exception> task = new Task.WithResult, Exception>(project, "OpenAI Moderation", false) { + Task.WithResult, Exception> task = new Task.WithResult<>(project, "OpenAI Moderation", false) { @Override - protected ListenableFuture compute(@NotNull ProgressIndicator indicator) throws Exception { + protected @NotNull ListenableFuture compute(@NotNull ProgressIndicator indicator) throws Exception { return pool.submit(() -> { String body = null; try { @@ -222,7 +294,7 @@ protected ListenableFuture compute(@NotNull ProgressIndicator indicator) thro * @throws IOException If an IOException is thrown and the number of retries is exceeded. * @throws InterruptedException If the thread is interrupted while sleeping. */ - protected String post(String url, @NotNull String json, int retries) throws IOException, InterruptedException { + private String post(String url, @NotNull String json, int retries) throws IOException, InterruptedException { try { HttpClientBuilder client = HttpClientBuilder.create(); HttpPost request = new HttpPost(url); @@ -243,7 +315,8 @@ protected String post(String url, @NotNull String json, int retries) throws IOEx } } - protected @NotNull ObjectMapper getMapper() { + @NotNull + private ObjectMapper getMapper() { ObjectMapper mapper = new ObjectMapper(); mapper .enable(SerializationFeature.INDENT_OUTPUT) @@ -254,15 +327,15 @@ protected String post(String url, @NotNull String json, int retries) throws IOEx return mapper; } - protected void authorize(@NotNull HttpRequestBase request) throws IOException { + private void authorize(@NotNull HttpRequestBase request) throws IOException { AppSettingsState settingsState = getSettingsState(); String apiKey = settingsState.apiKey; - if (apiKey == null || apiKey.length() == 0) { + if (apiKey.length() == 0) { synchronized (settingsState) { apiKey = settingsState.apiKey; - if (apiKey == null || apiKey.length() == 0) { + if (apiKey.length() == 0) { apiKey = UITools.queryAPIKey(); - settingsState.apiKey = apiKey; + settingsState.apiKey = Objects.requireNonNull(apiKey); } } } diff --git a/src/main/java/com/github/simiacryptus/aicoder/openai/Response.java b/src/main/java/com/github/simiacryptus/aicoder/openai/Response.java index c316f194..96862660 100644 --- a/src/main/java/com/github/simiacryptus/aicoder/openai/Response.java +++ b/src/main/java/com/github/simiacryptus/aicoder/openai/Response.java @@ -1,12 +1,16 @@ package com.github.simiacryptus.aicoder.openai; public class Response { + @SuppressWarnings("unused") public String object; + @SuppressWarnings("unused") private Engine[] data; + @SuppressWarnings("unused") public Response() { } + @SuppressWarnings("unused") public Response(String object, Engine[] data) { this.object = object; this.data = data; diff --git a/src/main/java/com/github/simiacryptus/aicoder/openai/Usage.java b/src/main/java/com/github/simiacryptus/aicoder/openai/Usage.java index 13994645..ae08ccbf 100644 --- a/src/main/java/com/github/simiacryptus/aicoder/openai/Usage.java +++ b/src/main/java/com/github/simiacryptus/aicoder/openai/Usage.java @@ -1,10 +1,14 @@ package com.github.simiacryptus.aicoder.openai; public class Usage { + @SuppressWarnings("unused") public int prompt_tokens; + @SuppressWarnings("unused") public int completion_tokens; + @SuppressWarnings("unused") public int total_tokens; + @SuppressWarnings("unused") public Usage() { } } diff --git a/src/main/java/com/github/simiacryptus/aicoder/openai/translate/BaseTranslationRequest.java b/src/main/java/com/github/simiacryptus/aicoder/openai/translate/BaseTranslationRequest.java index ae52505d..bb8e6b28 100644 --- a/src/main/java/com/github/simiacryptus/aicoder/openai/translate/BaseTranslationRequest.java +++ b/src/main/java/com/github/simiacryptus/aicoder/openai/translate/BaseTranslationRequest.java @@ -1,6 +1,7 @@ package com.github.simiacryptus.aicoder.openai.translate; import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; import java.util.Collections; import java.util.HashMap; @@ -12,25 +13,25 @@ public abstract class BaseTranslationRequest private CharSequence outputTag; private CharSequence instruction; @NotNull - private Map inputAttr = new HashMap<>(); + private final Map inputAttr = new HashMap<>(); @NotNull - private Map outputAttr = new HashMap<>(); + private final Map outputAttr = new HashMap<>(); private CharSequence originalText; private double temperature; private int maxTokens; @Override - public String getInputTag() { + public @NotNull String getInputTag() { return inputTag.toString(); } @Override - public String getOutputTag() { + public @NotNull String getOutputTag() { return outputTag.toString(); } @Override - public CharSequence getInstruction() { + public @NotNull CharSequence getInstruction() { return instruction.toString(); } @@ -47,7 +48,7 @@ public Map getOutputAttr() { } @Override - public String getOriginalText() { + public @NotNull String getOriginalText() { return originalText.toString(); } @@ -62,43 +63,43 @@ public int getMaxTokens() { } @Override - public T setInputType(CharSequence inputTag) { + public @NotNull T setInputType(CharSequence inputTag) { this.inputTag = inputTag; return (T) this; } @Override - public T setOutputType(CharSequence outputTag) { + public @NotNull T setOutputType(CharSequence outputTag) { this.outputTag = outputTag; return (T) this; } @Override - public T setInstruction(CharSequence instruction) { + public @NotNull T setInstruction(CharSequence instruction) { this.instruction = instruction; return (T) this; } @Override - public T setInputText(CharSequence originalText) { + public @NotNull T setInputText(CharSequence originalText) { this.originalText = originalText; return (T) this; } @Override - public T setTemperature(double temperature) { + public @NotNull T setTemperature(double temperature) { this.temperature = temperature; return (T) this; } @Override - public T setMaxTokens(int maxTokens) { + public @NotNull T setMaxTokens(int maxTokens) { this.maxTokens = maxTokens; return (T) this; } @Override - public T setInputAttribute(CharSequence key, CharSequence value) { + public @NotNull T setInputAttribute(CharSequence key, @Nullable CharSequence value) { if(null == value || value.length()==0) { inputAttr.remove(key); } else { @@ -108,7 +109,7 @@ public T setInputAttribute(CharSequence key, CharSequence value) { } @Override - public T setOutputAttrute(CharSequence key, CharSequence value) { + public @NotNull T setOutputAttrute(CharSequence key, @Nullable CharSequence value) { if(null == value || value.length()==0) { outputAttr.remove(key); } else { diff --git a/src/main/java/com/github/simiacryptus/aicoder/openai/translate/TranslationRequest.java b/src/main/java/com/github/simiacryptus/aicoder/openai/translate/TranslationRequest.java index c25dc7cc..b476c868 100644 --- a/src/main/java/com/github/simiacryptus/aicoder/openai/translate/TranslationRequest.java +++ b/src/main/java/com/github/simiacryptus/aicoder/openai/translate/TranslationRequest.java @@ -8,20 +8,28 @@ public interface TranslationRequest { @NotNull CompletionRequest buildCompletionRequest(); + @SuppressWarnings("unused") String getInputTag(); + @SuppressWarnings("unused") String getOutputTag(); + @SuppressWarnings("unused") CharSequence getInstruction(); + @SuppressWarnings("unused") @NotNull Map getInputAttr(); + @SuppressWarnings("unused") @NotNull Map getOutputAttr(); + @SuppressWarnings("unused") String getOriginalText(); + @SuppressWarnings("unused") double getTemperature(); + @SuppressWarnings("unused") int getMaxTokens(); TranslationRequest setInputType(CharSequence inputTag); @@ -35,7 +43,9 @@ public interface TranslationRequest { TranslationRequest setInputText(CharSequence originalText); + @SuppressWarnings("unused") TranslationRequest setTemperature(double temperature); + @SuppressWarnings("unused") TranslationRequest setMaxTokens(int maxTokens); } diff --git a/src/main/java/com/github/simiacryptus/aicoder/openai/translate/TranslationRequestTemplate.java b/src/main/java/com/github/simiacryptus/aicoder/openai/translate/TranslationRequestTemplate.java index 8b970984..1f8c25ad 100644 --- a/src/main/java/com/github/simiacryptus/aicoder/openai/translate/TranslationRequestTemplate.java +++ b/src/main/java/com/github/simiacryptus/aicoder/openai/translate/TranslationRequestTemplate.java @@ -3,10 +3,9 @@ import com.github.simiacryptus.aicoder.config.AppSettingsState; import java.util.function.Function; -import java.util.function.Supplier; public enum TranslationRequestTemplate { - XML(config -> new TranslationRequest_XML(config)); + XML(TranslationRequest_XML::new); private final Function fn; TranslationRequestTemplate(Function fn) { diff --git a/src/main/java/com/github/simiacryptus/aicoder/openai/translate/TranslationRequest_XML.java b/src/main/java/com/github/simiacryptus/aicoder/openai/translate/TranslationRequest_XML.java index 58334ecc..b10f483e 100644 --- a/src/main/java/com/github/simiacryptus/aicoder/openai/translate/TranslationRequest_XML.java +++ b/src/main/java/com/github/simiacryptus/aicoder/openai/translate/TranslationRequest_XML.java @@ -8,7 +8,7 @@ public class TranslationRequest_XML extends BaseTranslationRequest { - public TranslationRequest_XML(AppSettingsState settings) { + public TranslationRequest_XML(@NotNull AppSettingsState settings) { setTemperature(settings.temperature); setMaxTokens(settings.maxTokens); } diff --git a/src/main/java/com/github/simiacryptus/aicoder/util/BlockComment.java b/src/main/java/com/github/simiacryptus/aicoder/util/BlockComment.java index 21b0cbce..7c376742 100644 --- a/src/main/java/com/github/simiacryptus/aicoder/util/BlockComment.java +++ b/src/main/java/com/github/simiacryptus/aicoder/util/BlockComment.java @@ -18,7 +18,7 @@ public Factory(String blockPrefix, String linePrefix, String blockSuffix) { } @Override - public BlockComment fromString(String text) { + public @NotNull BlockComment fromString(String text) { text = StringTools.stripSuffix(StringTools.trimSuffix(text.replace("\t", TAB_REPLACEMENT)), blockSuffix.trim()); @NotNull CharSequence indent = StringTools.getWhitespacePrefix(text.split(DELIMITER)); return new BlockComment(blockPrefix, linePrefix, blockSuffix, indent, Arrays.stream(text.split(DELIMITER)) @@ -30,7 +30,7 @@ public BlockComment fromString(String text) { } @Override - public boolean looksLike(String text) { + public boolean looksLike(@NotNull String text) { return text.trim().startsWith(blockPrefix) && text.trim().endsWith(blockSuffix); } } diff --git a/src/main/java/com/github/simiacryptus/aicoder/ComputerLanguage.java b/src/main/java/com/github/simiacryptus/aicoder/util/ComputerLanguage.java similarity index 83% rename from src/main/java/com/github/simiacryptus/aicoder/ComputerLanguage.java rename to src/main/java/com/github/simiacryptus/aicoder/util/ComputerLanguage.java index d0e988b4..ac931a98 100644 --- a/src/main/java/com/github/simiacryptus/aicoder/ComputerLanguage.java +++ b/src/main/java/com/github/simiacryptus/aicoder/util/ComputerLanguage.java @@ -1,12 +1,14 @@ -package com.github.simiacryptus.aicoder; +package com.github.simiacryptus.aicoder.util; -import com.github.simiacryptus.aicoder.util.BlockComment; -import com.github.simiacryptus.aicoder.util.LineComment; -import com.github.simiacryptus.aicoder.util.TextBlockFactory; +import com.intellij.openapi.actionSystem.AnActionEvent; +import com.intellij.openapi.actionSystem.CommonDataKeys; +import com.intellij.openapi.vfs.VirtualFile; +import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import java.util.Arrays; import java.util.List; +import java.util.Objects; public enum ComputerLanguage { Java(new Configuration() @@ -21,6 +23,18 @@ public enum ComputerLanguage { .setBlockComments(new BlockComment.Factory("/*", "", "*/")) .setDocComments(new BlockComment.Factory("/**", "*", "*/")) .setFileExtensions("cpp")), + SVG(new Configuration() + .setDocumentationStyle("SVG") + .setLineComments(new LineComment.Factory("")) + .setDocComments(new BlockComment.Factory("")) + .setFileExtensions("svg")), + OpenSCAD(new Configuration() + .setDocumentationStyle("OpenSCAD") + .setLineComments(new LineComment.Factory("//")) + .setBlockComments(new BlockComment.Factory("/*", "", "*/")) + .setDocComments(new BlockComment.Factory("/**", "*", "*/")) + .setFileExtensions("scad")), Bash(new Configuration() .setLineComments(new LineComment.Factory("#")) .setFileExtensions("sh")), @@ -141,7 +155,7 @@ public enum ComputerLanguage { .setLineComments(new LineComment.Factory("//")) .setBlockComments(new BlockComment.Factory("/*", "", "*/")) .setDocComments(new BlockComment.Factory("/**", "*", "*/")) - .setFileExtensions("kotlin", "kt")), + .setFileExtensions("kotlin", "kt", "kts")), Lisp(new Configuration() .setLineComments(new LineComment.Factory(";")) .setBlockComments(new BlockComment.Factory("/*", "", "*/")) @@ -265,13 +279,13 @@ public enum ComputerLanguage { .setLineComments(new LineComment.Factory("#")) .setFileExtensions("zsh")); - public final List extensions; + public final @NotNull List extensions; public final String docStyle; - public final TextBlockFactory lineComment; - public final TextBlockFactory blockComment; - public final TextBlockFactory docComment; + public final @Nullable TextBlockFactory lineComment; + public final @Nullable TextBlockFactory blockComment; + public final @Nullable TextBlockFactory docComment; - ComputerLanguage(Configuration configuration) { + ComputerLanguage(@NotNull Configuration configuration) { this.extensions = Arrays.asList(configuration.getFileExtensions()); this.docStyle = configuration.getDocumentationStyle(); this.lineComment = configuration.getLineComments(); @@ -284,31 +298,39 @@ public static ComputerLanguage findByExtension(CharSequence extension) { return Arrays.stream(values()).filter(x -> x.extensions.contains(extension)).findAny().orElse(null); } - public CharSequence getMultilineCommentSuffix() { + @Nullable + public static ComputerLanguage getComputerLanguage(@NotNull AnActionEvent e) { + VirtualFile file = e.getData(CommonDataKeys.VIRTUAL_FILE); + if (file == null) return null; + String extension = file.getExtension() != null ? file.getExtension().toLowerCase() : ""; + return findByExtension(extension); + } + + public @Nullable CharSequence getMultilineCommentSuffix() { if (docComment instanceof BlockComment.Factory) { return ((BlockComment.Factory) docComment).blockSuffix; } return null; } - public TextBlockFactory getCommentModel(String text) { - if (docComment.looksLike(text)) return docComment; - if (blockComment.looksLike(text)) return blockComment; + public @Nullable TextBlockFactory getCommentModel(String text) { + if (Objects.requireNonNull(docComment).looksLike(text)) return docComment; + if (Objects.requireNonNull(blockComment).looksLike(text)) return blockComment; return lineComment; } static class Configuration { private String documentationStyle = ""; private CharSequence[] fileExtensions = new CharSequence[]{}; - private TextBlockFactory lineComments = null; - private TextBlockFactory blockComments = null; - private TextBlockFactory docComments = null; + private @Nullable TextBlockFactory lineComments = null; + private @Nullable TextBlockFactory blockComments = null; + private @Nullable TextBlockFactory docComments = null; public String getDocumentationStyle() { return documentationStyle; } - public Configuration setDocumentationStyle(String documentationStyle) { + public @NotNull Configuration setDocumentationStyle(String documentationStyle) { this.documentationStyle = documentationStyle; return this; } @@ -317,36 +339,36 @@ public CharSequence[] getFileExtensions() { return fileExtensions; } - public Configuration setFileExtensions(CharSequence... fileExtensions) { + public @NotNull Configuration setFileExtensions(CharSequence... fileExtensions) { this.fileExtensions = fileExtensions; return this; } - public TextBlockFactory getLineComments() { + public @Nullable TextBlockFactory getLineComments() { return lineComments; } - public Configuration setLineComments(TextBlockFactory lineComments) { + public @NotNull Configuration setLineComments(TextBlockFactory lineComments) { this.lineComments = lineComments; return this; } - public TextBlockFactory getBlockComments() { + public @Nullable TextBlockFactory getBlockComments() { if (null == blockComments) return getLineComments(); return blockComments; } - public Configuration setBlockComments(TextBlockFactory blockComments) { + public @NotNull Configuration setBlockComments(TextBlockFactory blockComments) { this.blockComments = blockComments; return this; } - public TextBlockFactory getDocComments() { + public @Nullable TextBlockFactory getDocComments() { if (null == docComments) return getBlockComments(); return docComments; } - public Configuration setDocComments(TextBlockFactory docComments) { + public @NotNull Configuration setDocComments(TextBlockFactory docComments) { this.docComments = docComments; return this; } diff --git a/src/main/java/com/github/simiacryptus/aicoder/util/IndentedText.java b/src/main/java/com/github/simiacryptus/aicoder/util/IndentedText.java index 64ccbdb1..8927d5d2 100644 --- a/src/main/java/com/github/simiacryptus/aicoder/util/IndentedText.java +++ b/src/main/java/com/github/simiacryptus/aicoder/util/IndentedText.java @@ -23,12 +23,14 @@ public CharSequence getIndent() { return indent; } + @SuppressWarnings("unused") public static class Factory implements TextBlockFactory { @Override - public IndentedText fromString(String text) { + public @NotNull IndentedText fromString(String text) { return IndentedText.fromString(text); } + @SuppressWarnings("unused") @Override public boolean looksLike(String text) { return true; @@ -36,7 +38,7 @@ public boolean looksLike(String text) { } protected CharSequence indent; - protected CharSequence textBlock[]; + protected CharSequence[] textBlock; public IndentedText(CharSequence indent, CharSequence... textBlock) { this.indent = indent; diff --git a/src/main/java/com/github/simiacryptus/aicoder/util/LineComment.java b/src/main/java/com/github/simiacryptus/aicoder/util/LineComment.java index 21098d7c..3347775b 100644 --- a/src/main/java/com/github/simiacryptus/aicoder/util/LineComment.java +++ b/src/main/java/com/github/simiacryptus/aicoder/util/LineComment.java @@ -14,7 +14,7 @@ public Factory(String commentPrefix) { } @Override - public LineComment fromString(String text) { + public @NotNull LineComment fromString(String text) { text = text.replace("\t", TAB_REPLACEMENT); CharSequence indent = StringTools.getWhitespacePrefix(text.split(DELIMITER)); return new LineComment(commentPrefix, indent, Arrays.stream(text.split(DELIMITER)) @@ -25,7 +25,7 @@ public LineComment fromString(String text) { } @Override - public boolean looksLike(String text) { + public boolean looksLike(@NotNull String text) { return Arrays.stream(text.split(DELIMITER)).allMatch(x->x.trim().startsWith(commentPrefix)); } } diff --git a/src/main/java/com/github/simiacryptus/aicoder/util/StringTools.java b/src/main/java/com/github/simiacryptus/aicoder/util/StringTools.java index e0c09a24..5bb8ab00 100644 --- a/src/main/java/com/github/simiacryptus/aicoder/util/StringTools.java +++ b/src/main/java/com/github/simiacryptus/aicoder/util/StringTools.java @@ -18,7 +18,7 @@ public class StringTools { * @return The input string with unbalanced terminators removed. * @throws IllegalArgumentException If the input string is unbalanced. */ - public static CharSequence stripUnbalancedTerminators(CharSequence input) { + public static @NotNull CharSequence stripUnbalancedTerminators(@NotNull CharSequence input) { int openCount = 0; boolean inQuotes = false; StringBuilder output = new StringBuilder(); @@ -83,7 +83,7 @@ public static CharSequence stripUnbalancedTerminators(CharSequence input) { } } - public static String lineWrapping(CharSequence description, int width) { + public static @NotNull String lineWrapping(@NotNull CharSequence description, int width) { StringBuilder output = new StringBuilder(); String[] lines = description.toString().split("\n"); int lineLength = 0; @@ -105,7 +105,7 @@ public static String lineWrapping(CharSequence description, int width) { return output.toString(); } - private static String wrapSentence(CharSequence line, int width, AtomicInteger xPointer) { + private static @NotNull String wrapSentence(@NotNull CharSequence line, int width, @NotNull AtomicInteger xPointer) { StringBuilder sentenceBuffer = new StringBuilder(); String[] words = line.toString().split(" "); for (String word : words) { @@ -122,7 +122,7 @@ private static String wrapSentence(CharSequence line, int width, AtomicInteger x return sentenceBuffer.toString(); } - public static CharSequence toString(int[] ints) { + public static @NotNull CharSequence toString(int @NotNull [] ints) { char[] chars = new char[ints.length]; for (int i = 0; i < ints.length; i++) { chars[i] = (char) ints[i]; @@ -131,22 +131,22 @@ public static CharSequence toString(int[] ints) { } @NotNull - public static CharSequence getWhitespacePrefix(CharSequence... lines) { + public static CharSequence getWhitespacePrefix(CharSequence @NotNull ... lines) { return Arrays.stream(lines) - .map(l -> toString(l.chars().takeWhile(i -> Character.isWhitespace(i)).toArray())) + .map(l -> toString(l.chars().takeWhile(Character::isWhitespace).toArray())) .filter(x -> x.length()>0) - .min(Comparator.comparing(x -> x.length())).orElse(""); + .min(Comparator.comparing(CharSequence::length)).orElse(""); } @NotNull - public static String getWhitespaceSuffix(CharSequence... lines) { + public static String getWhitespaceSuffix(CharSequence @NotNull ... lines) { return reverse(Arrays.stream(lines) .map(StringTools::reverse) - .map(l -> toString(l.chars().takeWhile(i -> Character.isWhitespace(i)).toArray())) - .max(Comparator.comparing(x -> x.length())).orElse("")).toString(); + .map(l -> toString(l.chars().takeWhile(Character::isWhitespace).toArray())) + .max(Comparator.comparing(CharSequence::length)).orElse("")).toString(); } - private static CharSequence reverse(CharSequence l) { + private static @NotNull CharSequence reverse(@NotNull CharSequence l) { return new StringBuffer(l).reverse().toString(); } @@ -162,8 +162,8 @@ public static List trim(List items, int max, boolean return items; } - public static String transposeMarkdownTable(String table, boolean inputHeader, boolean outputHeader) { - String[][] cells = parseMarkdownTable(table, inputHeader); + public static @NotNull String transposeMarkdownTable(@NotNull String table, boolean inputHeader, boolean outputHeader) { + CharSequence[][] cells = parseMarkdownTable(table, inputHeader); StringBuilder transposedTable = new StringBuilder(); int columns = cells[0].length; int rows = cells.length; @@ -172,19 +172,19 @@ public static String transposeMarkdownTable(String table, boolean inputHeader, b transposedTable.append("|"); for (int row = 0; row < rows; row++) { String cellValue; - String[] rowCells = cells[row]; + CharSequence[] rowCells = cells[row]; if (outputHeader) { if (column < 1) { - cellValue = rowCells[column].trim(); + cellValue = rowCells[column].toString().trim(); } else if (column == 1) { cellValue = "---"; } else if ((column - 1) >= rowCells.length) { cellValue = ""; } else { - cellValue = rowCells[column - 1].trim(); + cellValue = rowCells[column - 1].toString().trim(); } } else { - cellValue = rowCells[column].trim(); + cellValue = rowCells[column].toString().trim(); } transposedTable.append(" ").append(cellValue).append(" |"); } @@ -193,17 +193,17 @@ public static String transposeMarkdownTable(String table, boolean inputHeader, b return transposedTable.toString(); } - private static String[][] parseMarkdownTable(String table, boolean removeHeader) { - ArrayList rows = new ArrayList(Arrays.stream(table.split("\n")).map(x -> Arrays.stream(x.split("\\|")).filter(cell -> cell.length() > 0).toArray(CharSequence[]::new)).collect(Collectors.toList())); + private static CharSequence[] @NotNull [] parseMarkdownTable(@NotNull String table, boolean removeHeader) { + ArrayList rows = new ArrayList<>(Arrays.stream(table.split("\n")).map(x -> Arrays.stream(x.split("\\|")).filter(cell -> cell.length() > 0).toArray(CharSequence[]::new)).collect(Collectors.toList())); if (removeHeader) { rows.remove(1); } return rows.stream() //.filter(x -> x.length == rows.get(0).length) - .toArray(String[][]::new); + .toArray(CharSequence[][]::new); } - public static CharSequence getPrefixForContext(String text) { + public static @NotNull CharSequence getPrefixForContext(@NotNull String text) { return getPrefixForContext(text, 512, ".", "\n", ",", ";"); } @@ -215,7 +215,7 @@ public static CharSequence getPrefixForContext(String text) { * @param delimiters The delimiters to split the text by. * @return The prefix for the given context. */ - public static CharSequence getPrefixForContext(String text, int idealLength, CharSequence... delimiters) { + public static @NotNull CharSequence getPrefixForContext(@NotNull String text, int idealLength, CharSequence... delimiters) { List candidates = Stream.of(delimiters).flatMap(d -> { StringBuilder sb = new StringBuilder(); String[] split = text.split(Pattern.quote(d.toString())); @@ -233,7 +233,7 @@ public static CharSequence getPrefixForContext(String text, int idealLength, Cha return winner.get(); } - public static CharSequence getSuffixForContext(String text) { + public static @NotNull CharSequence getSuffixForContext(@NotNull String text) { return getSuffixForContext(text, 512, ".", "\n", ",", ";"); } @@ -247,7 +247,7 @@ public static CharSequence getSuffixForContext(String text) { * @return The suffix for the given context. */ @NotNull - public static CharSequence getSuffixForContext(String text, int idealLength, CharSequence... delimiters) { + public static CharSequence getSuffixForContext(@NotNull String text, int idealLength, CharSequence... delimiters) { List candidates = Stream.of(delimiters).flatMap(d -> { StringBuilder sb = new StringBuilder(); String[] split = text.split(Pattern.quote(d.toString())); diff --git a/src/main/java/com/github/simiacryptus/aicoder/util/StyleUtil.java b/src/main/java/com/github/simiacryptus/aicoder/util/StyleUtil.java index 47a28eb5..fb7e4d8b 100644 --- a/src/main/java/com/github/simiacryptus/aicoder/util/StyleUtil.java +++ b/src/main/java/com/github/simiacryptus/aicoder/util/StyleUtil.java @@ -1,11 +1,11 @@ package com.github.simiacryptus.aicoder.util; -import com.github.simiacryptus.aicoder.ComputerLanguage; import com.github.simiacryptus.aicoder.config.AppSettingsState; import com.github.simiacryptus.aicoder.openai.CompletionRequest; import com.github.simiacryptus.aicoder.openai.OpenAI_API; import com.google.common.util.concurrent.ListenableFuture; import com.intellij.openapi.diagnostic.Logger; +import org.jetbrains.annotations.NotNull; import javax.swing.*; import java.util.Arrays; @@ -13,6 +13,7 @@ import java.util.Random; public class StyleUtil { + @SuppressWarnings("unused") private static final Logger log = Logger.getInstance(StyleUtil.class); /** @@ -124,7 +125,7 @@ public static void demoStyle(CharSequence style) { * @param language The language of the code snippet. * @param code The code snippet to be described. */ - public static void demoStyle(CharSequence style, ComputerLanguage language, String code) { + public static void demoStyle(CharSequence style, @NotNull ComputerLanguage language, @NotNull String code) { OpenAI_API.onSuccess(describeTest(style, language, code), description -> { CharSequence message = String.format("This code:\n %s\nwas described as:\n %s", code.replace("\n", "\n "), description.toString().replace("\n", "\n ")); JOptionPane.showMessageDialog(null, message, "Style Demo", JOptionPane.INFORMATION_MESSAGE); @@ -139,7 +140,7 @@ public static void demoStyle(CharSequence style, ComputerLanguage language, Stri * @param code The code. * @return A description of the test in the specified style and language. */ - public static ListenableFuture describeTest(CharSequence style, ComputerLanguage language, String code) { + public static @NotNull ListenableFuture describeTest(CharSequence style, @NotNull ComputerLanguage language, String code) { AppSettingsState settings = AppSettingsState.getInstance(); CompletionRequest completionRequest = settings.createTranslationRequest() .setInstruction(String.format("Explain this %s in %s (%s)", language.name(), settings.humanLanguage, style)) @@ -150,7 +151,7 @@ public static ListenableFuture describeTest(CharSequence style, Co .setOutputAttrute("type", "description") .setOutputAttrute("style", style) .buildCompletionRequest(); - ListenableFuture future = completionRequest.complete(null, ""); + ListenableFuture future = OpenAI_API.INSTANCE.complete(null, completionRequest, ""); return OpenAI_API.map(future, x->StringTools.lineWrapping(x.toString().trim(), 120)); } } diff --git a/src/main/java/com/github/simiacryptus/aicoder/util/TextBlock.java b/src/main/java/com/github/simiacryptus/aicoder/util/TextBlock.java index 7bbe3b0f..950d22ba 100644 --- a/src/main/java/com/github/simiacryptus/aicoder/util/TextBlock.java +++ b/src/main/java/com/github/simiacryptus/aicoder/util/TextBlock.java @@ -8,8 +8,8 @@ public interface TextBlock { - public static final CharSequence TAB_REPLACEMENT = " "; - public static final String DELIMITER = "\n"; + CharSequence TAB_REPLACEMENT = " "; + String DELIMITER = "\n"; CharSequence[] rawString(); @@ -19,7 +19,7 @@ default String getTextBlock() { @NotNull TextBlock withIndent(CharSequence indent); - default Stream stream() { + default @NotNull Stream stream() { return Arrays.stream(rawString()); } } diff --git a/src/main/java/com/github/simiacryptus/aicoder/util/TextBlockFactory.java b/src/main/java/com/github/simiacryptus/aicoder/util/TextBlockFactory.java index 9eb29abf..b9a18ac6 100644 --- a/src/main/java/com/github/simiacryptus/aicoder/util/TextBlockFactory.java +++ b/src/main/java/com/github/simiacryptus/aicoder/util/TextBlockFactory.java @@ -1,8 +1,11 @@ package com.github.simiacryptus.aicoder.util; +import org.jetbrains.annotations.NotNull; + public interface TextBlockFactory { T fromString(String text); - default CharSequence toString(T text) { + @SuppressWarnings("unused") + default CharSequence toString(@NotNull T text) { return text.toString(); } boolean looksLike(String text); diff --git a/src/main/java/com/github/simiacryptus/aicoder/util/TextReplacementAction.java b/src/main/java/com/github/simiacryptus/aicoder/util/TextReplacementAction.java index d3781557..fa414034 100644 --- a/src/main/java/com/github/simiacryptus/aicoder/util/TextReplacementAction.java +++ b/src/main/java/com/github/simiacryptus/aicoder/util/TextReplacementAction.java @@ -1,22 +1,5 @@ package com.github.simiacryptus.aicoder.util; -import com.github.simiacryptus.aicoder.openai.CompletionRequest; -import com.github.simiacryptus.aicoder.openai.ModerationException; -import com.intellij.openapi.actionSystem.AnAction; -import com.intellij.openapi.actionSystem.AnActionEvent; -import com.intellij.openapi.actionSystem.CommonDataKeys; -import com.intellij.openapi.editor.Caret; -import com.intellij.openapi.editor.CaretModel; -import com.intellij.openapi.editor.Editor; -import com.intellij.openapi.util.NlsActions; -import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; - -import javax.swing.*; -import java.io.IOException; - -import static com.github.simiacryptus.aicoder.util.UITools.replaceString; - /** * TextReplacementAction is an abstract class that extends the AnAction class. * It provides a static method create() that takes in a text, description, icon, and an ActionTextEditorFunction. @@ -25,53 +8,8 @@ * It then calls the edit() method, which is implemented by the subclasses, and replaces the selected text with the new text. * The ActionTextEditorFunction is a functional interface that takes in an AnActionEvent and a String and returns a String. */ -public class TextReplacementAction extends AnAction { - - private final ActionTextEditorFunction fn; - - private TextReplacementAction(@Nullable @NlsActions.ActionText CharSequence text, @Nullable @NlsActions.ActionDescription CharSequence description, @Nullable Icon icon, @NotNull ActionTextEditorFunction fn) { - super(text.toString(), description.toString(), icon); - this.fn = fn; - } - - public static @NotNull TextReplacementAction create(@Nullable @NlsActions.ActionText CharSequence text, @Nullable @NlsActions.ActionDescription CharSequence description, @Nullable Icon icon, @NotNull ActionTextEditorFunction fn) { - return new TextReplacementAction(text, description, icon, fn); - } - - @Override - public void actionPerformed(@NotNull final AnActionEvent e) { - final Editor editor = e.getRequiredData(CommonDataKeys.EDITOR); - final CaretModel caretModel = editor.getCaretModel(); - final Caret primaryCaret = caretModel.getPrimaryCaret(); - try { - int selectionStart = primaryCaret.getSelectionStart(); - int selectionEnd = primaryCaret.getSelectionEnd(); - String selectedText = primaryCaret.getSelectedText(); - CompletionRequest request = fn.apply(e, selectedText); - Caret caret = e.getData(CommonDataKeys.CARET); - CharSequence indent = UITools.getIndent(caret); - UITools.redoableRequest(request, indent, e, (CharSequence x) -> { - CharSequence newText = fn.postTransform(e, selectedText, x); - return replaceString(editor.getDocument(), selectionStart, selectionEnd, newText); - }); - } catch (ModerationException | IOException ex) { - UITools.handle(ex); - } - } - - public interface ActionTextEditorFunction { - CompletionRequest apply(AnActionEvent actionEvent, String input) throws IOException, ModerationException; +@SuppressWarnings("unused") +public class TextReplacementAction { - /** - * - * Override this method to post-transform the completion string. - * - * @param event The action event - * @param prompt The prompt string - * @param completion The completion string - * @return The transformed string - */ - default CharSequence postTransform(AnActionEvent event, CharSequence prompt, CharSequence completion) { return completion; } - } } diff --git a/src/main/java/com/github/simiacryptus/aicoder/util/UITools.java b/src/main/java/com/github/simiacryptus/aicoder/util/UITools.java index 32af4f43..5ff21911 100644 --- a/src/main/java/com/github/simiacryptus/aicoder/util/UITools.java +++ b/src/main/java/com/github/simiacryptus/aicoder/util/UITools.java @@ -1,6 +1,7 @@ package com.github.simiacryptus.aicoder.util; import com.github.simiacryptus.aicoder.config.AppSettingsState; +import com.github.simiacryptus.aicoder.config.Name; import com.github.simiacryptus.aicoder.openai.CompletionRequest; import com.github.simiacryptus.aicoder.openai.ModerationException; import com.github.simiacryptus.aicoder.openai.OpenAI_API; @@ -17,18 +18,33 @@ import com.intellij.openapi.progress.ProgressIndicator; import com.intellij.openapi.progress.ProgressManager; import com.intellij.openapi.project.Project; +import com.intellij.openapi.ui.ComboBox; import com.intellij.openapi.util.TextRange; +import com.intellij.ui.components.JBLabel; +import com.intellij.util.ui.FormBuilder; import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; import javax.swing.*; -import java.util.concurrent.ConcurrentLinkedDeque; +import javax.swing.text.JTextComponent; +import java.lang.reflect.Field; +import java.lang.reflect.Modifier; +import java.util.Arrays; +import java.util.Objects; +import java.util.Set; +import java.util.WeakHashMap; import java.util.function.Function; +import java.util.stream.Collectors; public class UITools { private static final Logger log = Logger.getInstance(UITools.class); - public static final ConcurrentLinkedDeque retry = new ConcurrentLinkedDeque<>(); + public static final WeakHashMap retry = new WeakHashMap<>(); + + public static void redoableRequest(@NotNull CompletionRequest request, CharSequence indent, @NotNull AnActionEvent event, @NotNull Function action) { + redoableRequest(request, indent, event, x->x, action); + } /** * This method is responsible for making a redoable request. @@ -39,33 +55,30 @@ public class UITools { * @param action The action to be taken when the request is completed. * @return A {@link Runnable} that can be used to redo the request. */ - public static void redoableRequest(CompletionRequest request, CharSequence indent, @NotNull AnActionEvent event, Function action) { + public static void redoableRequest(@NotNull CompletionRequest request, CharSequence indent, @NotNull AnActionEvent event, @NotNull Function transformCompletion, @NotNull Function action) { Editor editor = event.getData(CommonDataKeys.EDITOR); - Document document = editor.getDocument(); - //document.setReadOnly(true); + Document document = Objects.requireNonNull(editor).getDocument(); ProgressManager progressManager = ProgressManager.getInstance(); ProgressIndicator progressIndicator = progressManager.getProgressIndicator(); if(null != progressIndicator) { progressIndicator.setIndeterminate(true); progressIndicator.setText("Talking to OpenAI..."); } - ListenableFuture resultFuture = request.complete(event.getProject(), indent); - Futures.addCallback(resultFuture, new FutureCallback() { + ListenableFuture resultFuture = OpenAI_API.INSTANCE.complete(event.getProject(), request, indent); + Futures.addCallback(resultFuture, new FutureCallback<>() { @Override - public void onSuccess(CharSequence result) { - //document.setReadOnly(false); - if(null != progressIndicator) { + public void onSuccess(@NotNull CharSequence result) { + if (null != progressIndicator) { progressIndicator.cancel(); } WriteCommandAction.runWriteCommandAction(event.getProject(), () -> { - retry.add(getRetry(request, indent, event, action, action.apply(result.toString()))); + retry.put(document, getRetry(request, indent, event, action, action.apply(transformCompletion.apply(result.toString())))); }); } @Override public void onFailure(Throwable t) { - //document.setReadOnly(false); - if(null != progressIndicator) { + if (null != progressIndicator) { progressIndicator.cancel(); } handle(t); @@ -88,32 +101,29 @@ public void onFailure(Throwable t) { * @return a {@link Runnable} that will attempt to complete the given {@link CompletionRequest} */ @NotNull - private static Runnable getRetry(CompletionRequest request, CharSequence indent, AnActionEvent event, Function action, Runnable undo) { - Document document = event.getData(CommonDataKeys.EDITOR).getDocument(); - //document.setReadOnly(true); + private static Runnable getRetry(@NotNull CompletionRequest request, CharSequence indent, @NotNull AnActionEvent event, @NotNull Function action, @Nullable Runnable undo) { + Document document = Objects.requireNonNull(event.getData(CommonDataKeys.EDITOR)).getDocument(); ProgressIndicator progressIndicator = ProgressManager.getInstance().getProgressIndicator(); if(null != progressIndicator) { progressIndicator.setIndeterminate(true); } return () -> { - ListenableFuture retryFuture = request.complete(event.getProject(), indent); - Futures.addCallback(retryFuture, new FutureCallback() { + ListenableFuture retryFuture = OpenAI_API.INSTANCE.complete(event.getProject(), request, indent); + Futures.addCallback(retryFuture, new FutureCallback<>() { @Override - public void onSuccess(CharSequence result) { + public void onSuccess(@NotNull CharSequence result) { WriteCommandAction.runWriteCommandAction(event.getProject(), () -> { - //document.setReadOnly(false); - if(null != progressIndicator) { + if (null != progressIndicator) { progressIndicator.cancel(); } if (null != undo) undo.run(); - retry.add(getRetry(request, indent, event, action, action.apply(result.toString()))); + retry.put(document, getRetry(request, indent, event, action, action.apply(result.toString()))); }); } @Override public void onFailure(Throwable t) { - //document.setReadOnly(false); - if(null != progressIndicator) { + if (null != progressIndicator) { progressIndicator.cancel(); } handle(t); @@ -143,7 +153,7 @@ public static String getInstruction(String instruction) { * @param newText The new string to replace the old string. * @return A Runnable that can be used to undo the replacement. */ - public static Runnable replaceString(Document document, int startOffset, int endOffset, CharSequence newText) { + public static @NotNull Runnable replaceString(@NotNull Document document, int startOffset, int endOffset, @NotNull CharSequence newText) { CharSequence oldText = document.getText(new TextRange(startOffset, endOffset)); document.replaceString(startOffset, endOffset, newText); return () -> { @@ -161,7 +171,7 @@ public static Runnable replaceString(Document document, int startOffset, int end * @param newText The string to insert. * @return A Runnable that can be used to undo the insertion. */ - public static Runnable insertString(Document document, int startOffset, CharSequence newText) { + public static @NotNull Runnable insertString(@NotNull Document document, int startOffset, @NotNull CharSequence newText) { document.insertString(startOffset, newText); return () -> { if (!document.getText(new TextRange(startOffset, startOffset + newText.length())).equals(newText)) @@ -170,7 +180,8 @@ public static Runnable insertString(Document document, int startOffset, CharSequ }; } - public static Runnable deleteString(Document document, int startOffset, int endOffset) { + @SuppressWarnings("unused") + public static @NotNull Runnable deleteString(@NotNull Document document, int startOffset, int endOffset) { CharSequence oldText = document.getText(new TextRange(startOffset, endOffset)); document.deleteString(startOffset, endOffset); return () -> { @@ -178,12 +189,16 @@ public static Runnable deleteString(Document document, int startOffset, int endO }; } - public static CharSequence getIndent(Caret caret) { + public static CharSequence getIndent(@Nullable Caret caret) { if (null == caret) return ""; Document document = caret.getEditor().getDocument(); - return IndentedText.fromString(document.getText().split("\n")[document.getLineNumber(caret.getSelectionStart())]).getIndent(); + String documentText = document.getText(); + int lineNumber = document.getLineNumber(caret.getSelectionStart()); + String[] lines = documentText.split("\n"); + return IndentedText.fromString(lines[Math.min(Math.max(lineNumber, 0), lines.length-1)]).getIndent(); } + @SuppressWarnings("unused") public static boolean hasSelection(@NotNull AnActionEvent e) { Caret caret = e.getData(CommonDataKeys.CARET); return null != caret && caret.hasSelection(); @@ -194,7 +209,7 @@ public static void handle(@NotNull Throwable ex) { JOptionPane.showMessageDialog(null, ex.getMessage(), "Warning", JOptionPane.WARNING_MESSAGE); } - public static CharSequence getIndent(AnActionEvent event) { + public static CharSequence getIndent(@NotNull AnActionEvent event) { Caret caret = event.getData(CommonDataKeys.CARET); CharSequence indent; if (null == caret) { @@ -205,20 +220,159 @@ public static CharSequence getIndent(AnActionEvent event) { return indent; } - public static String queryAPIKey() { + public static @Nullable String queryAPIKey() { JPanel panel = new JPanel(); JLabel label = new JLabel("Enter OpenAI API Key:"); JPasswordField pass = new JPasswordField(100); panel.add(label); panel.add(pass); - CharSequence[] options = new CharSequence[]{"OK", "Cancel"}; - int option = JOptionPane.showOptionDialog(null, panel, "API Key", - JOptionPane.NO_OPTION, JOptionPane.PLAIN_MESSAGE, - null, options, options[1]); - if (option == 0) { + Object[] options = {"OK", "Cancel"}; + if (JOptionPane.showOptionDialog( + null, + panel, + "API Key", + JOptionPane.NO_OPTION, + JOptionPane.PLAIN_MESSAGE, + null, + options, + options[1]) == JOptionPane.OK_OPTION) { char[] password = pass.getPassword(); return new String(password); + } else { + return null; + } + } + + public static void readUI(@NotNull Object component, @NotNull T settings) { + Class componentClass = component.getClass(); + Set declaredUIFields = Arrays.stream(componentClass.getFields()).map(Field::getName).collect(Collectors.toSet()); + for (Field settingsField : settings.getClass().getFields()) { + settingsField.setAccessible(true); + String settingsFieldName = settingsField.getName(); + try { + Object newSettingsValue = null; + if(!declaredUIFields.contains(settingsFieldName)) continue; + Field uiField = componentClass.getDeclaredField(settingsFieldName); + Object uiFieldVal = uiField.get(component); + switch (settingsField.getType().getName()) { + case "java.lang.String": + if (uiFieldVal instanceof JTextComponent) { + newSettingsValue = ((JTextComponent) uiFieldVal).getText(); + } else if (uiFieldVal instanceof ComboBox) { + newSettingsValue = ((ComboBox) uiFieldVal).getItem(); + } + break; + case "int": + if (uiFieldVal instanceof JTextComponent) { + newSettingsValue = Integer.parseInt(((JTextComponent) uiFieldVal).getText()); + } + break; + case "long": + if (uiFieldVal instanceof JTextComponent) { + newSettingsValue = Long.parseLong(((JTextComponent) uiFieldVal).getText()); + } + break; + case "double": + if (uiFieldVal instanceof JTextComponent) { + newSettingsValue = Double.parseDouble(((JTextComponent) uiFieldVal).getText()); + } + break; + case "boolean": + if (uiFieldVal instanceof JCheckBox) { + newSettingsValue = ((JCheckBox) uiFieldVal).isSelected(); + } else if (uiFieldVal instanceof JTextComponent) { + newSettingsValue = Boolean.parseBoolean(((JTextComponent) uiFieldVal).getText()); + } + break; + default: + + if (Enum.class.isAssignableFrom(settingsField.getType())) { + if (uiFieldVal instanceof ComboBox) { + ComboBox comboBox = (ComboBox) uiFieldVal; + CharSequence item = comboBox.getItem(); + newSettingsValue = Enum.valueOf((Class) settingsField.getType(), item.toString()); + } + } + break; + } + settingsField.set(settings, newSettingsValue); + } catch (Throwable e) { + new RuntimeException("Error processing " + settingsField, e).printStackTrace(); + } + } + } + + public static void writeUI(@NotNull Object component, @NotNull T settings) { + Class componentClass = component.getClass(); + Set declaredUIFields = Arrays.stream(componentClass.getFields()).map(Field::getName).collect(Collectors.toSet()); + for (Field settingsField : settings.getClass().getFields()) { + String fieldName = settingsField.getName(); + try { + if(!declaredUIFields.contains(fieldName)) continue; + Field uiField = componentClass.getDeclaredField(fieldName); + Object settingsVal = settingsField.get(settings); + if(null == settingsVal) continue; + Object uiVal = uiField.get(component); + switch (settingsField.getType().getName()) { + case "java.lang.String": + if (uiVal instanceof JTextComponent) { + ((JTextComponent) uiVal).setText((String) settingsVal); + } else if (uiVal instanceof ComboBox) { + ((ComboBox) uiVal).setItem(settingsVal.toString()); + } + break; + case "int": + if (uiVal instanceof JTextComponent) { + ((JTextComponent) uiVal).setText(Integer.toString((Integer) settingsVal)); + } + break; + case "long": + if (uiVal instanceof JTextComponent) { + ((JTextComponent) uiVal).setText(Long.toString((Integer) settingsVal)); + } + break; + case "double": + if (uiVal instanceof JTextComponent) { + ((JTextComponent) uiVal).setText(Double.toString(((Double) settingsVal))); + } + break; + case "boolean": + if (uiVal instanceof JCheckBox) { + ((JCheckBox) uiVal).setSelected(((Boolean) settingsVal)); + } else if (uiVal instanceof JTextComponent) { + ((JTextComponent) uiVal).setText(Boolean.toString((Boolean) settingsVal)); + } + break; + default: + if (uiVal instanceof ComboBox) { + ((ComboBox) uiVal).setItem(settingsVal.toString()); + } + break; + } + } catch (Throwable e) { + new RuntimeException("Error processing " + settingsField, e).printStackTrace(); + } + } + } + + public static void addFields(@NotNull Object ui, @NotNull FormBuilder formBuilder) { + for (Field field : ui.getClass().getFields()) { + if(Modifier.isStatic(field.getModifiers())) continue; + try { + Name nameAnnotation = field.getDeclaredAnnotation(Name.class); + JComponent component = (JComponent) field.get(ui); + if (null == component) continue; + if (nameAnnotation != null) { + formBuilder.addLabeledComponent(new JBLabel(nameAnnotation.value() + ": "), component, 1, false); + } else { + formBuilder.addComponentToRightColumn(component, 1); + } + } catch (IllegalAccessException e) { + throw new RuntimeException(e); + } catch (Throwable e) { + log.warn("Error processing " + field.getName(), e); + } } - return null; } + } diff --git a/src/main/java/com/github/simiacryptus/aicoder/psi/PsiClassContext.java b/src/main/java/com/github/simiacryptus/aicoder/util/psi/PsiClassContext.java similarity index 96% rename from src/main/java/com/github/simiacryptus/aicoder/psi/PsiClassContext.java rename to src/main/java/com/github/simiacryptus/aicoder/util/psi/PsiClassContext.java index 049b81bc..15db4119 100644 --- a/src/main/java/com/github/simiacryptus/aicoder/psi/PsiClassContext.java +++ b/src/main/java/com/github/simiacryptus/aicoder/util/psi/PsiClassContext.java @@ -1,4 +1,4 @@ -package com.github.simiacryptus.aicoder.psi; +package com.github.simiacryptus.aicoder.util.psi; import com.github.simiacryptus.aicoder.util.StringTools; import com.intellij.openapi.util.TextRange; @@ -62,7 +62,7 @@ public void visitElement(@NotNull PsiElement element) { currentContext.children.add(new PsiClassContext(text.trim(), isPrior, isOverlap)); } else if (simpleName.equals("PsiCommentImpl") || simpleName.equals("PsiDocCommentImpl")) { if (within) { - currentContext.children.add(new PsiClassContext(indent + text.trim(), isPrior, isOverlap)); + currentContext.children.add(new PsiClassContext(indent + text.trim(), false, true)); } } else if (simpleName.equals("PsiMethodImpl") || simpleName.equals("PsiFieldImpl")) { String declaration = text; @@ -94,7 +94,7 @@ public void visitElement(@NotNull PsiElement element) { indent = prevIndent; currentContext = prevclassBuffer; if (!isOverlap) { - currentContext.children.add(new PsiClassContext("}", isPrior, isOverlap)); + currentContext.children.add(new PsiClassContext("}", isPrior, false)); } } else if (!isOverlap && Arrays.asList("PsiCodeBlockImpl", "PsiForStatementImpl").contains(simpleName)) { // Skip diff --git a/src/main/java/com/github/simiacryptus/aicoder/psi/PsiMarkdownContext.java b/src/main/java/com/github/simiacryptus/aicoder/util/psi/PsiMarkdownContext.java similarity index 94% rename from src/main/java/com/github/simiacryptus/aicoder/psi/PsiMarkdownContext.java rename to src/main/java/com/github/simiacryptus/aicoder/util/psi/PsiMarkdownContext.java index 3f111fbe..c4b77aea 100644 --- a/src/main/java/com/github/simiacryptus/aicoder/psi/PsiMarkdownContext.java +++ b/src/main/java/com/github/simiacryptus/aicoder/util/psi/PsiMarkdownContext.java @@ -1,4 +1,4 @@ -package com.github.simiacryptus.aicoder.psi; +package com.github.simiacryptus.aicoder.util.psi; import com.intellij.openapi.util.TextRange; import com.intellij.psi.PsiElement; @@ -27,7 +27,7 @@ public static PsiMarkdownContext getContext(@NotNull PsiFile psiFile, int select } public int getEnd() { - return Math.max(start + text.length(), children.stream().mapToInt(x -> x.getEnd()).max().orElse(0)); + return Math.max(start + text.length(), children.stream().mapToInt(PsiMarkdownContext::getEnd).max().orElse(0)); } public int headerLevel() { @@ -40,6 +40,7 @@ public int headerLevel() { final CharSequence indent = ""; @NotNull PsiMarkdownContext section = PsiMarkdownContext.this; + @SuppressWarnings("unused") @Override public void visitElement(@NotNull PsiElement element) { String text = element.getText(); diff --git a/src/main/java/com/github/simiacryptus/aicoder/psi/PsiUtil.java b/src/main/java/com/github/simiacryptus/aicoder/util/psi/PsiUtil.java similarity index 71% rename from src/main/java/com/github/simiacryptus/aicoder/psi/PsiUtil.java rename to src/main/java/com/github/simiacryptus/aicoder/util/psi/PsiUtil.java index 423f9981..fe077fed 100644 --- a/src/main/java/com/github/simiacryptus/aicoder/psi/PsiUtil.java +++ b/src/main/java/com/github/simiacryptus/aicoder/util/psi/PsiUtil.java @@ -1,16 +1,18 @@ -package com.github.simiacryptus.aicoder.psi; +package com.github.simiacryptus.aicoder.util.psi; import com.github.simiacryptus.aicoder.util.StringTools; +import com.intellij.openapi.actionSystem.AnActionEvent; +import com.intellij.openapi.actionSystem.CommonDataKeys; +import com.intellij.openapi.editor.Caret; import com.intellij.openapi.util.TextRange; import com.intellij.psi.PsiElement; import com.intellij.psi.PsiElementVisitor; import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.HashSet; -import java.util.List; +import java.util.*; import java.util.concurrent.atomic.AtomicReference; +import java.util.stream.Collectors; import java.util.stream.Stream; public class PsiUtil { @@ -28,13 +30,12 @@ public static PsiElement getLargestIntersectingComment(@NotNull PsiElement eleme * @param types The types of elements to search for. * @return The largest element that intersects with the given selection range. */ - public static PsiElement getLargestIntersecting(@NotNull PsiElement element, int selectionStart, int selectionEnd, CharSequence... types) { + public static PsiElement getLargestIntersecting(@NotNull PsiElement element, int selectionStart, int selectionEnd, CharSequence @NotNull ... types) { final AtomicReference largest = new AtomicReference<>(null); final AtomicReference visitor = new AtomicReference<>(); visitor.set(new PsiElementVisitor() { @Override public void visitElement(@NotNull PsiElement element) { - if (null == element) return; TextRange textRange = element.getTextRange(); boolean within = (textRange.getStartOffset() <= selectionStart && textRange.getEndOffset() + 1 >= selectionStart && textRange.getStartOffset() <= selectionEnd && textRange.getEndOffset() + 1 >= selectionEnd); CharSequence simpleName = element.getClass().getSimpleName(); @@ -50,13 +51,13 @@ public void visitElement(@NotNull PsiElement element) { element.accept(visitor.get()); return largest.get(); } - public static List getAll(@NotNull PsiElement element, CharSequence... types) { + + public static @NotNull List getAll(@NotNull PsiElement element, CharSequence @NotNull ... types) { final List elements = new ArrayList<>(); final AtomicReference visitor = new AtomicReference<>(); visitor.set(new PsiElementVisitor() { @Override public void visitElement(@NotNull PsiElement element) { - if (null == element) return; if (Arrays.asList(expand(types)).contains(element.getClass().getSimpleName())) { elements.add(element); } else { @@ -90,13 +91,12 @@ public static PsiElement getSmallestIntersecting(@NotNull PsiElement element, in * @param types The types of the elements to be retrieved. * @return The smallest intersecting entity from the given PsiElement. */ - public static PsiElement getSmallestIntersecting(@NotNull PsiElement element, int selectionStart, int selectionEnd, CharSequence... types) { + public static PsiElement getSmallestIntersecting(@NotNull PsiElement element, int selectionStart, int selectionEnd, CharSequence @NotNull ... types) { final AtomicReference largest = new AtomicReference<>(null); final AtomicReference visitor = new AtomicReference<>(); visitor.set(new PsiElementVisitor() { @Override public void visitElement(@NotNull PsiElement element) { - if (null == element) return; TextRange textRange = element.getTextRange(); boolean within = (textRange.getStartOffset() <= selectionStart && textRange.getEndOffset() + 1 >= selectionStart && textRange.getStartOffset() <= selectionEnd && textRange.getEndOffset() + 1 >= selectionEnd); CharSequence simpleName = element.getClass().getSimpleName(); @@ -114,15 +114,15 @@ public void visitElement(@NotNull PsiElement element) { return largest.get(); } - private static CharSequence[] expand(CharSequence[] types) { - return Arrays.stream(types).flatMap(x-> Stream.of(x, StringTools.stripSuffix(x, "Impl"))).distinct().toArray(CharSequence[]::new); + private static CharSequence @NotNull [] expand(CharSequence @NotNull [] types) { + return Arrays.stream(types).flatMap(x -> Stream.of(x, StringTools.stripSuffix(x, "Impl"))).distinct().toArray(CharSequence[]::new); } - public static PsiElement getFirstBlock(@NotNull PsiElement element, CharSequence blockType) { + public static @Nullable PsiElement getFirstBlock(@NotNull PsiElement element, CharSequence blockType) { PsiElement[] children = element.getChildren(); - if(null == children || 0 == children.length) return null; + if (0 == children.length) return null; PsiElement first = children[0]; - if(first.getClass().getSimpleName().equals(blockType)) return first; + if (first.getClass().getSimpleName().equals(blockType)) return first; return null; } @@ -143,8 +143,7 @@ public void visitElement(@NotNull PsiElement element) { } }); element.accept(visitor.get()); - PsiElement psiElement = largest.get(); - return psiElement; + return largest.get(); } /** @@ -155,6 +154,7 @@ public void visitElement(@NotNull PsiElement element) { * @return A {@link HashSet} of {@link String}s containing the simple names of all the {@link PsiElement}s contained * within the given {@link PsiElement}. */ + @SuppressWarnings("unused") public static @NotNull HashSet getAllElementNames(@NotNull PsiElement element) { HashSet set = new HashSet<>(); AtomicReference visitor = new AtomicReference<>(); @@ -180,21 +180,45 @@ private static void printTree(@NotNull PsiElement element, @NotNull StringBuilde for (int i = 0; i < level; i++) { builder.append(" "); } - builder.append(element.getClass().getSimpleName() + " " + element.getText().replaceAll("\n", "\\\\n")); + Class elementClass = element.getClass(); + String simpleName = getName(elementClass); + builder.append(simpleName + " " + element.getText().replaceAll("\n", "\\\\n")); builder.append("\n"); for (PsiElement child : element.getChildren()) { printTree(child, builder, level + 1); } } - public static PsiElement getLargestContainedEntity(PsiElement element, int selectionStart, int selectionEnd) { - if(null == element) return element; + @NotNull + private static String getName(Class elementClass) { + StringBuilder stringBuilder = new StringBuilder(); + Set interfaces = getInterfaces(elementClass); + while(elementClass != Object.class) { + if(stringBuilder.length()>0) stringBuilder.append("/"); + stringBuilder.append(elementClass.getSimpleName()); + elementClass = elementClass.getSuperclass(); + } + stringBuilder.append("[ "); + stringBuilder.append(interfaces.stream().sorted().collect(Collectors.joining(","))); + stringBuilder.append("]"); + return stringBuilder.toString(); + } + + @NotNull + private static Set getInterfaces(Class elementClass) { + HashSet strings = Arrays.stream(elementClass.getInterfaces()).map(Class::getSimpleName).collect(Collectors.toCollection(HashSet::new)); + if (elementClass.getSuperclass() != Object.class) strings.addAll(getInterfaces(elementClass.getSuperclass())); + return strings; + } + + public static PsiElement getLargestContainedEntity(@Nullable PsiElement element, int selectionStart, int selectionEnd) { + if (null == element) return null; TextRange textRange = element.getTextRange(); - if(textRange.getStartOffset() >= selectionStart && textRange.getEndOffset() <= selectionEnd) return element; + if (textRange.getStartOffset() >= selectionStart && textRange.getEndOffset() <= selectionEnd) return element; PsiElement largestContainedChild = null; for (PsiElement child : element.getChildren()) { PsiElement entity = getLargestContainedEntity(child, selectionStart, selectionEnd); - if(null != entity) { + if (null != entity) { if (largestContainedChild == null || largestContainedChild.getTextRange().getLength() < entity.getTextRange().getLength()) { largestContainedChild = entity; } @@ -203,4 +227,16 @@ public static PsiElement getLargestContainedEntity(PsiElement element, int selec return largestContainedChild; } + @Nullable + public static PsiElement getPsiFile(@NotNull AnActionEvent e) { + Caret caret = e.getData(CommonDataKeys.CARET); + if (null == caret) return null; + PsiElement psiFile = e.getData(CommonDataKeys.PSI_FILE); + if (null == psiFile) return null; + int selectionStart = caret.getSelectionStart(); + int selectionEnd = caret.getSelectionEnd(); + PsiElement largestContainedEntity = getLargestContainedEntity(psiFile, selectionStart, selectionEnd); + if (largestContainedEntity != null) psiFile = largestContainedEntity; + return psiFile; + } } diff --git a/src/main/resources/META-INF/plugin.xml b/src/main/resources/META-INF/plugin.xml index a0fb9d23..a1fb9945 100644 --- a/src/main/resources/META-INF/plugin.xml +++ b/src/main/resources/META-INF/plugin.xml @@ -18,15 +18,125 @@ SimiaCryptus Software - - + description="AI Coding Assistant Tools"> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +