From 025ea098a357aa26938af87f6fe6dcb1ec4fbefb Mon Sep 17 00:00:00 2001 From: Andrew Charneski Date: Mon, 4 Dec 2023 20:26:20 -0500 Subject: [PATCH] Goodbye Groovy... you were too funky to fit in the groove. --- build.gradle.kts | 22 +- .../actions/code/CommentsAction.groovy | 53 - .../actions/code/CustomEditAction.groovy | 86 -- .../actions/code/DescribeAction.groovy | 65 - .../aicoder/actions/code/DocAction.groovy | 99 -- .../actions/code/ImplementStubAction.groovy | 84 -- .../code/InsertImplementationAction.groovy | 148 --- .../aicoder/actions/code/PasteAction.groovy | 67 - .../actions/code/RecentCodeEditsAction.groovy | 42 - .../actions/code/RenameVariablesAction.groovy | 89 -- .../actions/generic/AnalogueFileAction.groovy | 133 -- .../actions/generic/AppendAction.groovy | 34 - .../actions/generic/CreateFileAction.groovy | 139 --- .../generic/ReplaceOptionsAction.groovy | 63 - .../MarkdownImplementActionGroup.groovy | 123 -- .../aicoder/actions/code/CommentsAction.kt | 47 + .../aicoder/actions/code/CustomEditAction.kt | 71 ++ .../aicoder/actions/code/DescribeAction.kt | 59 + .../aicoder/actions/code/DocAction.kt | 89 ++ .../actions/code/ImplementStubAction.kt | 80 ++ .../code/InsertImplementationAction.kt | 127 ++ .../aicoder/actions/code/PasteAction.kt | 55 + .../actions/code/RecentCodeEditsAction.kt | 43 + .../actions/code/RenameVariablesAction.kt | 78 ++ .../actions/dev/InternalCoderAction.kt | 2 - .../actions/generic/AnalogueFileAction.kt | 121 ++ .../aicoder/actions/generic/AppendAction.kt | 29 + .../actions/generic/CreateFileAction.kt | 105 ++ .../actions/generic/ReplaceOptionsAction.kt | 58 + .../markdown/MarkdownImplementActionGroup.kt | 81 ++ .../aicoder/config/ActionSettingsRegistry.kt | 55 +- .../simiacryptus/aicoder/config/MRUItems.kt | 4 +- .../aicoder/util/IdeaKotlinInterpreter.kt | 40 +- .../simiacryptus/aicoder/util/UITools.kt | 12 +- .../skyenet/heart/WeakGroovyInterpreter.kt | 72 -- .../skyenet/heart/WeakKotlinInterpreter.kt | 47 - .../actions/code/CommentsAction.groovy | 53 - .../actions/code/CustomEditAction.groovy | 86 -- .../actions/code/DescribeAction.groovy | 65 - .../aicoder/actions/code/DocAction.groovy | 99 -- .../actions/code/GenerateProjectAction.groovy | 545 -------- .../actions/code/ImplementStubAction.groovy | 84 -- .../code/InsertImplementationAction.groovy | 148 --- .../aicoder/actions/code/PasteAction.groovy | 67 - .../actions/code/QuestionAction.groovy | 67 - .../actions/code/RecentCodeEditsAction.groovy | 42 - .../actions/code/RenameVariablesAction.groovy | 89 -- .../actions/dev/GenerateProjectAction.groovy | 515 -------- .../actions/dev/GenerateStoryAction.groovy | 246 ---- .../actions/generic/AnalogueFileAction.groovy | 133 -- .../actions/generic/AppendAction.groovy | 34 - .../actions/generic/CreateFileAction.groovy | 139 --- .../generic/GenerateProjectAction.groovy | 533 -------- .../generic/GenerateStoryAction.groovy | 245 ---- .../generic/ReplaceOptionsAction.groovy | 63 - .../MarkdownImplementActionGroup.groovy | 123 -- .../aicoder/actions/BaseAction.kt | 44 + .../aicoder/actions/FileContextAction.kt | 83 ++ .../aicoder/actions/SelectionAction.kt | 201 +++ .../aicoder/actions/code/CommentsAction.kt | 47 + .../aicoder/actions/code/CustomEditAction.kt | 71 ++ .../aicoder/actions/code/DescribeAction.kt | 59 + .../aicoder/actions/code/DocAction.kt | 89 ++ .../actions/code/ImplementStubAction.kt | 80 ++ .../code/InsertImplementationAction.kt | 127 ++ .../aicoder/actions/code/PasteAction.kt | 55 + .../actions/code/RecentCodeEditsAction.kt | 43 + .../actions/code/RenameVariablesAction.kt | 78 ++ .../aicoder/actions/dev/AppServer.kt | 116 ++ .../actions/dev/InternalCoderAction.kt | 64 + .../aicoder/actions/dev/PrintTreeAction.kt | 30 + .../actions/generic/AnalogueFileAction.kt | 121 ++ .../aicoder/actions/generic/AppendAction.kt | 29 + .../aicoder/actions/generic/CodeChatAction.kt | 42 + .../actions/generic/CreateFileAction.kt | 105 ++ .../actions/generic/DictationAction.kt | 135 ++ .../aicoder/actions/generic/RedoLast.kt | 23 + .../actions/generic/ReplaceOptionsAction.kt | 58 + .../markdown/MarkdownImplementActionGroup.kt | 81 ++ .../actions/markdown/MarkdownListAction.kt | 108 ++ .../aicoder/config/ActionSettingsRegistry.kt | 209 ++++ .../aicoder/config/ActionTable.kt | 221 ++++ .../aicoder/config/AppSettingsComponent.kt | 124 ++ .../aicoder/config/AppSettingsConfigurable.kt | 68 + .../aicoder/config/AppSettingsState.kt | 115 ++ .../simiacryptus/aicoder/config/MRUItems.kt | 44 + .../simiacryptus/aicoder/config/Name.kt | 6 + .../simiacryptus/aicoder/ui/EditorMenu.kt | 13 + .../aicoder/ui/ModelSelectionWidgetFactory.kt | 111 ++ .../simiacryptus/aicoder/ui/ProjectMenu.kt | 11 + .../ui/TemperatureControlWidgetFactory.kt | 153 +++ .../aicoder/ui/TokenCountWidgetFactory.kt | 133 ++ .../simiacryptus/aicoder/util/BlockComment.kt | 54 + .../aicoder/util/ComputerLanguage.kt | 475 +++++++ .../aicoder/util/IdeaKotlinInterpreter.kt | 74 ++ .../aicoder/util/IdeaOpenAIClient.kt | 196 +++ .../simiacryptus/aicoder/util/LineComment.kt | 55 + .../aicoder/util/TextBlockFactory.kt | 12 + .../simiacryptus/aicoder/util/UITools.kt | 1102 +++++++++++++++++ .../aicoder/util/psi/PsiClassContext.kt | 146 +++ .../aicoder/util/psi/PsiTranslationTree.kt | 256 ++++ .../simiacryptus/aicoder/util/psi/PsiUtil.kt | 256 ++++ .../aicoder/util/psi/PsiVisitorBase.kt | 21 + 103 files changed, 6867 insertions(+), 4775 deletions(-) delete mode 100644 src/main/groovy/com/github/simiacryptus/aicoder/actions/code/CommentsAction.groovy delete mode 100644 src/main/groovy/com/github/simiacryptus/aicoder/actions/code/CustomEditAction.groovy delete mode 100644 src/main/groovy/com/github/simiacryptus/aicoder/actions/code/DescribeAction.groovy delete mode 100644 src/main/groovy/com/github/simiacryptus/aicoder/actions/code/DocAction.groovy delete mode 100644 src/main/groovy/com/github/simiacryptus/aicoder/actions/code/ImplementStubAction.groovy delete mode 100644 src/main/groovy/com/github/simiacryptus/aicoder/actions/code/InsertImplementationAction.groovy delete mode 100644 src/main/groovy/com/github/simiacryptus/aicoder/actions/code/PasteAction.groovy delete mode 100644 src/main/groovy/com/github/simiacryptus/aicoder/actions/code/RecentCodeEditsAction.groovy delete mode 100644 src/main/groovy/com/github/simiacryptus/aicoder/actions/code/RenameVariablesAction.groovy delete mode 100644 src/main/groovy/com/github/simiacryptus/aicoder/actions/generic/AnalogueFileAction.groovy delete mode 100644 src/main/groovy/com/github/simiacryptus/aicoder/actions/generic/AppendAction.groovy delete mode 100644 src/main/groovy/com/github/simiacryptus/aicoder/actions/generic/CreateFileAction.groovy delete mode 100644 src/main/groovy/com/github/simiacryptus/aicoder/actions/generic/ReplaceOptionsAction.groovy delete mode 100644 src/main/groovy/com/github/simiacryptus/aicoder/actions/markdown/MarkdownImplementActionGroup.groovy create mode 100644 src/main/kotlin/com/github/simiacryptus/aicoder/actions/code/CommentsAction.kt create mode 100644 src/main/kotlin/com/github/simiacryptus/aicoder/actions/code/CustomEditAction.kt create mode 100644 src/main/kotlin/com/github/simiacryptus/aicoder/actions/code/DescribeAction.kt create mode 100644 src/main/kotlin/com/github/simiacryptus/aicoder/actions/code/DocAction.kt create mode 100644 src/main/kotlin/com/github/simiacryptus/aicoder/actions/code/ImplementStubAction.kt create mode 100644 src/main/kotlin/com/github/simiacryptus/aicoder/actions/code/InsertImplementationAction.kt create mode 100644 src/main/kotlin/com/github/simiacryptus/aicoder/actions/code/PasteAction.kt create mode 100644 src/main/kotlin/com/github/simiacryptus/aicoder/actions/code/RecentCodeEditsAction.kt create mode 100644 src/main/kotlin/com/github/simiacryptus/aicoder/actions/code/RenameVariablesAction.kt create mode 100644 src/main/kotlin/com/github/simiacryptus/aicoder/actions/generic/AnalogueFileAction.kt create mode 100644 src/main/kotlin/com/github/simiacryptus/aicoder/actions/generic/AppendAction.kt create mode 100644 src/main/kotlin/com/github/simiacryptus/aicoder/actions/generic/CreateFileAction.kt create mode 100644 src/main/kotlin/com/github/simiacryptus/aicoder/actions/generic/ReplaceOptionsAction.kt create mode 100644 src/main/kotlin/com/github/simiacryptus/aicoder/actions/markdown/MarkdownImplementActionGroup.kt delete mode 100644 src/main/kotlin/com/simiacryptus/skyenet/heart/WeakGroovyInterpreter.kt delete mode 100644 src/main/kotlin/com/simiacryptus/skyenet/heart/WeakKotlinInterpreter.kt delete mode 100644 src/main/resources/sources/groovy/com/github/simiacryptus/aicoder/actions/code/CommentsAction.groovy delete mode 100644 src/main/resources/sources/groovy/com/github/simiacryptus/aicoder/actions/code/CustomEditAction.groovy delete mode 100644 src/main/resources/sources/groovy/com/github/simiacryptus/aicoder/actions/code/DescribeAction.groovy delete mode 100644 src/main/resources/sources/groovy/com/github/simiacryptus/aicoder/actions/code/DocAction.groovy delete mode 100644 src/main/resources/sources/groovy/com/github/simiacryptus/aicoder/actions/code/GenerateProjectAction.groovy delete mode 100644 src/main/resources/sources/groovy/com/github/simiacryptus/aicoder/actions/code/ImplementStubAction.groovy delete mode 100644 src/main/resources/sources/groovy/com/github/simiacryptus/aicoder/actions/code/InsertImplementationAction.groovy delete mode 100644 src/main/resources/sources/groovy/com/github/simiacryptus/aicoder/actions/code/PasteAction.groovy delete mode 100644 src/main/resources/sources/groovy/com/github/simiacryptus/aicoder/actions/code/QuestionAction.groovy delete mode 100644 src/main/resources/sources/groovy/com/github/simiacryptus/aicoder/actions/code/RecentCodeEditsAction.groovy delete mode 100644 src/main/resources/sources/groovy/com/github/simiacryptus/aicoder/actions/code/RenameVariablesAction.groovy delete mode 100644 src/main/resources/sources/groovy/com/github/simiacryptus/aicoder/actions/dev/GenerateProjectAction.groovy delete mode 100644 src/main/resources/sources/groovy/com/github/simiacryptus/aicoder/actions/dev/GenerateStoryAction.groovy delete mode 100644 src/main/resources/sources/groovy/com/github/simiacryptus/aicoder/actions/generic/AnalogueFileAction.groovy delete mode 100644 src/main/resources/sources/groovy/com/github/simiacryptus/aicoder/actions/generic/AppendAction.groovy delete mode 100644 src/main/resources/sources/groovy/com/github/simiacryptus/aicoder/actions/generic/CreateFileAction.groovy delete mode 100644 src/main/resources/sources/groovy/com/github/simiacryptus/aicoder/actions/generic/GenerateProjectAction.groovy delete mode 100644 src/main/resources/sources/groovy/com/github/simiacryptus/aicoder/actions/generic/GenerateStoryAction.groovy delete mode 100644 src/main/resources/sources/groovy/com/github/simiacryptus/aicoder/actions/generic/ReplaceOptionsAction.groovy delete mode 100644 src/main/resources/sources/groovy/com/github/simiacryptus/aicoder/actions/markdown/MarkdownImplementActionGroup.groovy create mode 100644 src/main/resources/sources/kt/com/github/simiacryptus/aicoder/actions/BaseAction.kt create mode 100644 src/main/resources/sources/kt/com/github/simiacryptus/aicoder/actions/FileContextAction.kt create mode 100644 src/main/resources/sources/kt/com/github/simiacryptus/aicoder/actions/SelectionAction.kt create mode 100644 src/main/resources/sources/kt/com/github/simiacryptus/aicoder/actions/code/CommentsAction.kt create mode 100644 src/main/resources/sources/kt/com/github/simiacryptus/aicoder/actions/code/CustomEditAction.kt create mode 100644 src/main/resources/sources/kt/com/github/simiacryptus/aicoder/actions/code/DescribeAction.kt create mode 100644 src/main/resources/sources/kt/com/github/simiacryptus/aicoder/actions/code/DocAction.kt create mode 100644 src/main/resources/sources/kt/com/github/simiacryptus/aicoder/actions/code/ImplementStubAction.kt create mode 100644 src/main/resources/sources/kt/com/github/simiacryptus/aicoder/actions/code/InsertImplementationAction.kt create mode 100644 src/main/resources/sources/kt/com/github/simiacryptus/aicoder/actions/code/PasteAction.kt create mode 100644 src/main/resources/sources/kt/com/github/simiacryptus/aicoder/actions/code/RecentCodeEditsAction.kt create mode 100644 src/main/resources/sources/kt/com/github/simiacryptus/aicoder/actions/code/RenameVariablesAction.kt create mode 100644 src/main/resources/sources/kt/com/github/simiacryptus/aicoder/actions/dev/AppServer.kt create mode 100644 src/main/resources/sources/kt/com/github/simiacryptus/aicoder/actions/dev/InternalCoderAction.kt create mode 100644 src/main/resources/sources/kt/com/github/simiacryptus/aicoder/actions/dev/PrintTreeAction.kt create mode 100644 src/main/resources/sources/kt/com/github/simiacryptus/aicoder/actions/generic/AnalogueFileAction.kt create mode 100644 src/main/resources/sources/kt/com/github/simiacryptus/aicoder/actions/generic/AppendAction.kt create mode 100644 src/main/resources/sources/kt/com/github/simiacryptus/aicoder/actions/generic/CodeChatAction.kt create mode 100644 src/main/resources/sources/kt/com/github/simiacryptus/aicoder/actions/generic/CreateFileAction.kt create mode 100644 src/main/resources/sources/kt/com/github/simiacryptus/aicoder/actions/generic/DictationAction.kt create mode 100644 src/main/resources/sources/kt/com/github/simiacryptus/aicoder/actions/generic/RedoLast.kt create mode 100644 src/main/resources/sources/kt/com/github/simiacryptus/aicoder/actions/generic/ReplaceOptionsAction.kt create mode 100644 src/main/resources/sources/kt/com/github/simiacryptus/aicoder/actions/markdown/MarkdownImplementActionGroup.kt create mode 100644 src/main/resources/sources/kt/com/github/simiacryptus/aicoder/actions/markdown/MarkdownListAction.kt create mode 100644 src/main/resources/sources/kt/com/github/simiacryptus/aicoder/config/ActionSettingsRegistry.kt create mode 100644 src/main/resources/sources/kt/com/github/simiacryptus/aicoder/config/ActionTable.kt create mode 100644 src/main/resources/sources/kt/com/github/simiacryptus/aicoder/config/AppSettingsComponent.kt create mode 100644 src/main/resources/sources/kt/com/github/simiacryptus/aicoder/config/AppSettingsConfigurable.kt create mode 100644 src/main/resources/sources/kt/com/github/simiacryptus/aicoder/config/AppSettingsState.kt create mode 100644 src/main/resources/sources/kt/com/github/simiacryptus/aicoder/config/MRUItems.kt create mode 100644 src/main/resources/sources/kt/com/github/simiacryptus/aicoder/config/Name.kt create mode 100644 src/main/resources/sources/kt/com/github/simiacryptus/aicoder/ui/EditorMenu.kt create mode 100644 src/main/resources/sources/kt/com/github/simiacryptus/aicoder/ui/ModelSelectionWidgetFactory.kt create mode 100644 src/main/resources/sources/kt/com/github/simiacryptus/aicoder/ui/ProjectMenu.kt create mode 100644 src/main/resources/sources/kt/com/github/simiacryptus/aicoder/ui/TemperatureControlWidgetFactory.kt create mode 100644 src/main/resources/sources/kt/com/github/simiacryptus/aicoder/ui/TokenCountWidgetFactory.kt create mode 100644 src/main/resources/sources/kt/com/github/simiacryptus/aicoder/util/BlockComment.kt create mode 100644 src/main/resources/sources/kt/com/github/simiacryptus/aicoder/util/ComputerLanguage.kt create mode 100644 src/main/resources/sources/kt/com/github/simiacryptus/aicoder/util/IdeaKotlinInterpreter.kt create mode 100644 src/main/resources/sources/kt/com/github/simiacryptus/aicoder/util/IdeaOpenAIClient.kt create mode 100644 src/main/resources/sources/kt/com/github/simiacryptus/aicoder/util/LineComment.kt create mode 100644 src/main/resources/sources/kt/com/github/simiacryptus/aicoder/util/TextBlockFactory.kt create mode 100644 src/main/resources/sources/kt/com/github/simiacryptus/aicoder/util/UITools.kt create mode 100644 src/main/resources/sources/kt/com/github/simiacryptus/aicoder/util/psi/PsiClassContext.kt create mode 100644 src/main/resources/sources/kt/com/github/simiacryptus/aicoder/util/psi/PsiTranslationTree.kt create mode 100644 src/main/resources/sources/kt/com/github/simiacryptus/aicoder/util/psi/PsiUtil.kt create mode 100644 src/main/resources/sources/kt/com/github/simiacryptus/aicoder/util/psi/PsiVisitorBase.kt diff --git a/build.gradle.kts b/build.gradle.kts index 0b98d439..2430912b 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -7,7 +7,6 @@ fun environment(key: String) = providers.environmentVariable(key).get() plugins { id("java") // Java support - id("groovy") id("org.jetbrains.kotlin.jvm") version "1.9.21" id("org.jetbrains.intellij") version "1.16.1" id("org.jetbrains.changelog") version "2.2.0" @@ -46,8 +45,6 @@ dependencies { // implementation(group = "com.simiacryptus.skyenet", name = "kotlin-hack", version = "1.0.42") // { isTransitive = false } - implementation("org.codehaus.groovy:groovy-all:3.0.13") - implementation(group = "org.apache.httpcomponents.client5", name = "httpclient5", version = "5.2.1") implementation(group = "org.eclipse.jetty", name = "jetty-server", version = jetty_version) implementation(group = "org.eclipse.jetty", name = "jetty-servlet", version = jetty_version) @@ -67,19 +64,15 @@ dependencies { } -tasks.register("copyGroovySourcesToResources") { - from("src/main/groovy") - into("src/main/resources/sources/groovy") +tasks.register("copySourcesToResources") { + from("src/main/kotlin") + into("src/main/resources/sources/kt") } tasks.named("processResources") { - dependsOn("copyGroovySourcesToResources") + dependsOn("copySourcesToResources") } - - - - kotlin { jvmToolchain(17) } @@ -88,16 +81,9 @@ tasks { compileKotlin { compilerOptions { javaParameters = true - version = "17" } } - compileGroovy { - dependsOn += compileKotlin - classpath += files(compileKotlin.get().destinationDirectory) - groovyOptions.isParameters = true - } - compileTestKotlin { compilerOptions { javaParameters = true diff --git a/src/main/groovy/com/github/simiacryptus/aicoder/actions/code/CommentsAction.groovy b/src/main/groovy/com/github/simiacryptus/aicoder/actions/code/CommentsAction.groovy deleted file mode 100644 index 9f80cc79..00000000 --- a/src/main/groovy/com/github/simiacryptus/aicoder/actions/code/CommentsAction.groovy +++ /dev/null @@ -1,53 +0,0 @@ -package com.github.simiacryptus.aicoder.actions.code - -import com.github.simiacryptus.aicoder.actions.SelectionAction -import com.github.simiacryptus.aicoder.config.AppSettingsState -import com.github.simiacryptus.aicoder.util.ComputerLanguage -import com.intellij.openapi.project.Project -import com.simiacryptus.jopenai.proxy.ChatProxy -import org.jetbrains.annotations.Nullable - -class CommentsAction extends SelectionAction { - - @Override - String getConfig(@Nullable Project project) { - return "" - } - - boolean isLanguageSupported(ComputerLanguage computerLanguage) { - if (computerLanguage == null) return false - return computerLanguage != ComputerLanguage.Text - } - - String processSelection(SelectionState state, String config) { - return new ChatProxy( - clazz: CommentsAction_VirtualAPI.class, - api: api, - temperature: AppSettingsState.instance.temperature, - model: AppSettingsState.instance.defaultChatModel(), - deserializerRetries: 5, - ).create().editCode( - state.selectedText, - "Add comments to each line explaining the code", - state.language.toString(), - AppSettingsState.instance.humanLanguage - ).code ?: "" - } - - interface CommentsAction_VirtualAPI { - CommentsAction_ConvertedText editCode( - String code, - String operations, - String computerLanguage, - String humanLanguage - ) - - class CommentsAction_ConvertedText { - public String code - public String language - - def ConvertedText() {} - } - } - -} diff --git a/src/main/groovy/com/github/simiacryptus/aicoder/actions/code/CustomEditAction.groovy b/src/main/groovy/com/github/simiacryptus/aicoder/actions/code/CustomEditAction.groovy deleted file mode 100644 index 354ec208..00000000 --- a/src/main/groovy/com/github/simiacryptus/aicoder/actions/code/CustomEditAction.groovy +++ /dev/null @@ -1,86 +0,0 @@ -package com.github.simiacryptus.aicoder.actions.code - -import com.github.simiacryptus.aicoder.actions.SelectionAction -import com.github.simiacryptus.aicoder.config.AppSettingsState -import com.github.simiacryptus.aicoder.util.UITools -import com.intellij.openapi.project.Project -import com.simiacryptus.jopenai.proxy.ChatProxy -import org.jetbrains.annotations.Nullable - -import javax.swing.* - -class CustomEditAction extends SelectionAction { - - interface VirtualAPI { - EditedText editCode( - String code, - String operation, - String computerLanguage, - String humanLanguage - ) - - class EditedText { - public String code = null - public String language = null - - EditedText() {} - - EditedText(String code, String language) { - this.code = code - this.language = language - } - - } - } - - def getProxy() { - def chatProxy = new ChatProxy( - clazz: VirtualAPI.class, - api: api, - temperature: AppSettingsState.instance.temperature, - model: AppSettingsState.instance.defaultChatModel(), - ) - chatProxy.addExample( - new VirtualAPI.EditedText( - """ - // Print Hello, World! to the console - println("Hello, World!") - """.stripIndent(), - "java" - ) - ) { - it.editCode( - """println("Hello, World!")""", - "Add code comments", - "java", - "English" - ) - } - return chatProxy.create() - } - - @Override - String getConfig(@Nullable Project project) { - return UITools.showInputDialog(null, "Instruction:", "Edit Code", JOptionPane.QUESTION_MESSAGE - //, AppSettingsState.instance.getRecentCommands("customEdits").mostRecentHistory - ) - } - - - @Override - String processSelection(SelectionState state, String instruction) { - if (null == instruction) return (state.selectedText ?: "") - if (instruction.isBlank()) return state.selectedText ?: "" - def settings = AppSettingsState.instance - def outputHumanLanguage = AppSettingsState.instance.humanLanguage - settings.getRecentCommands("customEdits").addInstructionToHistory(instruction) - return proxy.editCode( - state.selectedText, - instruction.toString(), - state.language.name(), - outputHumanLanguage - ).code ?: state.selectedText ?: "" - } - - -} \ No newline at end of file diff --git a/src/main/groovy/com/github/simiacryptus/aicoder/actions/code/DescribeAction.groovy b/src/main/groovy/com/github/simiacryptus/aicoder/actions/code/DescribeAction.groovy deleted file mode 100644 index b07fa10b..00000000 --- a/src/main/groovy/com/github/simiacryptus/aicoder/actions/code/DescribeAction.groovy +++ /dev/null @@ -1,65 +0,0 @@ -package com.github.simiacryptus.aicoder.actions.code - -import com.github.simiacryptus.aicoder.actions.SelectionAction -import com.github.simiacryptus.aicoder.config.AppSettingsState -import com.github.simiacryptus.aicoder.util.IndentedText -import com.github.simiacryptus.aicoder.util.TextBlockFactory -import com.intellij.openapi.project.Project -import com.simiacryptus.jopenai.proxy.ChatProxy -import com.simiacryptus.jopenai.util.StringUtil -import org.jetbrains.annotations.Nullable - -class DescribeAction extends SelectionAction { - - interface DescribeAction_VirtualAPI { - DescribeAction_ConvertedText describeCode( - String code, - String computerLanguage, - String humanLanguage - ) - - class DescribeAction_ConvertedText { - public String text = null - public String language = null - - DescribeAction_ConvertedText() { - } - } - } - - def getProxy() { - return new ChatProxy( - clazz: DescribeAction_VirtualAPI.class, - api: api, - temperature: AppSettingsState.instance.temperature, - model: AppSettingsState.instance.defaultChatModel(), - deserializerRetries: 5 - ).create() - } - @Override - String getConfig(@Nullable Project project) { - return "" - } - - - @Override - String processSelection(SelectionState state, String config) { - def description = proxy.describeCode( - IndentedText.fromString(state.selectedText).textBlock.toString().trim(), - state.language?.name() ?: "", - AppSettingsState.instance.humanLanguage, - ).text ?: "" - def wrapping = StringUtil.lineWrapping(description.trim(), 120) - def numberOfLines = wrapping.trim().split("\n").reverse().dropWhile { it.isEmpty() }.size() - TextBlockFactory commentStyle = null - if (numberOfLines == 1) { - state.language?.lineComment - } else { - state.language?.blockComment - } - return """ - ${state.indent}${commentStyle?.fromString(wrapping)?.withIndent(state.indent) ?: wrapping} - ${state.indent}${state.selectedText} - """ - } -} \ No newline at end of file diff --git a/src/main/groovy/com/github/simiacryptus/aicoder/actions/code/DocAction.groovy b/src/main/groovy/com/github/simiacryptus/aicoder/actions/code/DocAction.groovy deleted file mode 100644 index cfd7dbc3..00000000 --- a/src/main/groovy/com/github/simiacryptus/aicoder/actions/code/DocAction.groovy +++ /dev/null @@ -1,99 +0,0 @@ -package com.github.simiacryptus.aicoder.actions.code - -import com.github.simiacryptus.aicoder.actions.SelectionAction -import com.github.simiacryptus.aicoder.config.AppSettingsState -import com.github.simiacryptus.aicoder.util.ComputerLanguage -import com.github.simiacryptus.aicoder.util.IndentedText -import com.github.simiacryptus.aicoder.util.psi.PsiUtil -import com.intellij.openapi.project.Project -import com.simiacryptus.jopenai.proxy.ChatProxy -import kotlin.Pair -import org.jetbrains.annotations.Nullable - -class DocAction extends SelectionAction { - - interface DocAction_VirtualAPI { - DocAction_ConvertedText processCode( - String code, - String operation, - String computerLanguage, - String humanLanguage - ) - - class DocAction_ConvertedText { - public String text - public String language - - def ConvertedText() {} - } - } - - DocAction_VirtualAPI getProxy() { - ChatProxy chatProxy = new ChatProxy( - clazz: DocAction_VirtualAPI, - api: api, - model: AppSettingsState.instance.defaultChatModel(), - temperature: AppSettingsState.instance.temperature, - deserializerRetries: 5 - ) - chatProxy.addExample( - new DocAction_VirtualAPI.DocAction_ConvertedText( - text: ''' - /** - * Prints "Hello, world!" to the console - */ - '''.trim(), - language: "English" - ) - ) { - (DocAction_VirtualAPI x) -> - x.processCode( - ''' - fun hello() { - println("Hello, world!") - } - '''.trim(), - "Write detailed KDoc prefix for code block", - "Kotlin", - "English" - ) - } - return chatProxy.create() as DocAction_VirtualAPI - } - - @Override - String getConfig(@Nullable Project project) { - return "" - } - - - @Override - String processSelection(SelectionState state, String config) { - CharSequence code = state.selectedText - IndentedText indentedInput = IndentedText.fromString(code.toString()) - String docString = proxy.processCode( - indentedInput.textBlock.toString(), - "Write detailed " + (state.language?.docStyle ?: "documentation") + " prefix for code block", - state.language.name(), - AppSettingsState.instance.humanLanguage - ).text ?: "" - return docString + code - } - - @Override - boolean isLanguageSupported(ComputerLanguage computerLanguage) { - if (computerLanguage == ComputerLanguage.Text) return false - if (computerLanguage?.docStyle == null) return false - if (computerLanguage?.docStyle?.isBlank()) return false - return true - } - - @Override - Pair editSelection(EditorState state, int start, int end) { - if (null == state.psiFile) return super.editSelection(state, start, end) - def codeBlock = PsiUtil.getCodeElement(state.psiFile, start, end) - if (null == codeBlock) return super.editSelection(state, start, end) - def textRange = codeBlock.textRange - return new Pair<>(textRange.startOffset, textRange.endOffset) - } -} diff --git a/src/main/groovy/com/github/simiacryptus/aicoder/actions/code/ImplementStubAction.groovy b/src/main/groovy/com/github/simiacryptus/aicoder/actions/code/ImplementStubAction.groovy deleted file mode 100644 index 829355df..00000000 --- a/src/main/groovy/com/github/simiacryptus/aicoder/actions/code/ImplementStubAction.groovy +++ /dev/null @@ -1,84 +0,0 @@ -package com.github.simiacryptus.aicoder.actions.code - -import com.github.simiacryptus.aicoder.actions.SelectionAction -import com.github.simiacryptus.aicoder.config.AppSettingsState -import com.github.simiacryptus.aicoder.util.ComputerLanguage -import com.github.simiacryptus.aicoder.util.psi.PsiUtil -import com.intellij.openapi.project.Project -import com.simiacryptus.jopenai.proxy.ChatProxy -import com.simiacryptus.jopenai.util.StringUtil -import kotlin.Pair -import org.jetbrains.annotations.Nullable - -class ImplementStubAction extends SelectionAction { - - static interface VirtualAPI { - ConvertedText editCode( - String code, - String operation, - String computerLanguage, - String humanLanguage - ) - - static class ConvertedText { - public String code - public String language - - ConvertedText() {} - } - } - - def getProxy() { - return new ChatProxy( - clazz: VirtualAPI.class, - api: api, - model: AppSettingsState.instance.defaultChatModel(), - temperature: AppSettingsState.instance.temperature, - deserializerRetries: 5, - ).create() - } - - boolean isLanguageSupported(ComputerLanguage computerLanguage) { - if (computerLanguage == null) return false - return computerLanguage != ComputerLanguage.Text - } - - Pair defaultSelection(EditorState editorState, int offset) { - def codeRanges = editorState.contextRanges.findAll { PsiUtil.matchesType(it.name, PsiUtil.ELEMENTS_CODE) } - if (codeRanges.isEmpty()) return editorState.line - return codeRanges.min { it.length() }.range() - } - @Override - String getConfig(@Nullable Project project) { - return "" - } - - - String processSelection(SelectionState state, String config) { - def code = state.selectedText ?: "" - def settings = AppSettingsState.instance - def outputHumanLanguage = settings.humanLanguage - def computerLanguage = state.language - - def codeContext = state.contextRanges.findAll { - PsiUtil.matchesType( - it.name, - PsiUtil.ELEMENTS_CODE - ) - } - def smallestIntersectingMethod = "" - if(!codeContext.isEmpty()) smallestIntersectingMethod = codeContext.min { it.length() }.subString(state.entireDocument) - - def declaration = code - declaration = StringUtil.stripSuffix(declaration.toString().trim(), smallestIntersectingMethod) - declaration = declaration.toString().trim() - - return proxy.editCode( - declaration, - "Implement Stub", - computerLanguage.name().toLowerCase(Locale.ROOT), - outputHumanLanguage - ).code ?: "" - } - -} \ No newline at end of file diff --git a/src/main/groovy/com/github/simiacryptus/aicoder/actions/code/InsertImplementationAction.groovy b/src/main/groovy/com/github/simiacryptus/aicoder/actions/code/InsertImplementationAction.groovy deleted file mode 100644 index 77175013..00000000 --- a/src/main/groovy/com/github/simiacryptus/aicoder/actions/code/InsertImplementationAction.groovy +++ /dev/null @@ -1,148 +0,0 @@ -package com.github.simiacryptus.aicoder.actions.code - -import com.github.simiacryptus.aicoder.actions.SelectionAction -import com.github.simiacryptus.aicoder.config.AppSettingsState -import com.github.simiacryptus.aicoder.util.ComputerLanguage -import com.github.simiacryptus.aicoder.util.UITools -import com.github.simiacryptus.aicoder.util.psi.PsiClassContext -import com.github.simiacryptus.aicoder.util.psi.PsiUtil -import com.intellij.openapi.project.Project -import com.simiacryptus.jopenai.proxy.ChatProxy -import kotlin.Pair -import org.jetbrains.annotations.NotNull -import org.jetbrains.annotations.Nullable - -import static com.intellij.openapi.application.ActionsKt.runReadAction - -class InsertImplementationAction extends SelectionAction { - - interface VirtualAPI { - ConvertedText implementCode( - String specification, - String prefix, - String computerLanguage, - String humanLanguage - ) - - class ConvertedText { - public String code - public String language - - ConvertedText() {} - } - } - - def getProxy() { - return new ChatProxy( - clazz: VirtualAPI.class, - api: api, - model: AppSettingsState.instance.defaultChatModel(), - temperature: AppSettingsState.instance.temperature, - deserializerRetries: 5, - ).create() - } - - @Override - String getConfig(@Nullable Project project) { - return "" - } - - @Override - Pair defaultSelection(@NotNull EditorState editorState, int offset) { - def foundItem = editorState.contextRanges.findAll { - PsiUtil.matchesType( - it.name, - PsiUtil.ELEMENTS_COMMENTS - ) - }.min({ it.length() }) - return foundItem?.range() ?: editorState.line - } - - @Override - Pair editSelection(@NotNull EditorState state, int start, int end) { - def foundItem = state.contextRanges.findAll { - PsiUtil.matchesType( - it.name, - PsiUtil.ELEMENTS_COMMENTS - ) - }.min({ it.length() }) - return foundItem?.range() ?: new Pair<>(start, end) - } - - @Override - String processSelection(SelectionState state, String config) { - def humanLanguage = AppSettingsState.instance.humanLanguage - def computerLanguage = state.language - def psiClassContextActionParams = getPsiClassContextActionParams(state) - def selectedText = state.selectedText ?: "" - - def comment = psiClassContextActionParams.largestIntersectingComment - def instruct = (null == comment) ? selectedText : comment.subString(state.entireDocument ?: "").trim() - if (selectedText.split(" ").reverse().dropWhile { it.isEmpty() }.reverse().length > 4) { - instruct = selectedText.trim() - } - def specification = Objects.requireNonNull(computerLanguage.getCommentModel(instruct)) - .fromString(instruct).stream() - .map { obj -> obj.toString() } - .map { obj -> obj.trim() } - .filter { x -> !x.isEmpty() } - .reduce { a, b -> "$a $b" }.get() - if(null != state.psiFile) { - def code = UITools.run(state.project, "Insert Implementation", true, true, { - def psiClassContext = runReadAction { - PsiClassContext.getContext( - state.psiFile, - psiClassContextActionParams.selectionStart, - psiClassContextActionParams.selectionEnd, - computerLanguage - ).toString() - } - proxy.implementCode( - specification, - psiClassContext, - computerLanguage.name(), - humanLanguage, - ).code - }) - if(null != code) return selectedText + "\n${state.indent}" + code - } else { - def code = proxy.implementCode( - specification, - "", - computerLanguage.name(), - humanLanguage, - ).code - if(null != code) return selectedText + "\n${state.indent}" + code - } - return selectedText - } - - static class PsiClassContextActionParams { - int selectionStart - int selectionEnd - SelectionAction.ContextRange largestIntersectingComment - - PsiClassContextActionParams(int selectionStart, int selectionEnd, SelectionAction.ContextRange largestIntersectingComment) { - this.selectionStart = selectionStart - this.selectionEnd = selectionEnd - this.largestIntersectingComment = largestIntersectingComment - } - } - - static PsiClassContextActionParams getPsiClassContextActionParams(SelectionState state) { - int selectionStart = state.selectionOffset - return new PsiClassContextActionParams( - selectionStart, - selectionStart + (state.selectionLength ?: 0), - state.contextRanges.find { PsiUtil.matchesType(it.name, PsiUtil.ELEMENTS_COMMENTS) } - ) - } - - @Override - boolean isLanguageSupported(@Nullable ComputerLanguage computerLanguage) { - if (computerLanguage == null) return false - if (computerLanguage == ComputerLanguage.Text) return false - if (computerLanguage == ComputerLanguage.Markdown) return false - return super.isLanguageSupported(computerLanguage) - } -} \ No newline at end of file diff --git a/src/main/groovy/com/github/simiacryptus/aicoder/actions/code/PasteAction.groovy b/src/main/groovy/com/github/simiacryptus/aicoder/actions/code/PasteAction.groovy deleted file mode 100644 index 564b5f54..00000000 --- a/src/main/groovy/com/github/simiacryptus/aicoder/actions/code/PasteAction.groovy +++ /dev/null @@ -1,67 +0,0 @@ -package com.github.simiacryptus.aicoder.actions.code - -import com.github.simiacryptus.aicoder.actions.SelectionAction -import com.github.simiacryptus.aicoder.config.AppSettingsState -import com.github.simiacryptus.aicoder.util.ComputerLanguage -import com.intellij.openapi.actionSystem.AnActionEvent -import com.intellij.openapi.project.Project -import com.simiacryptus.jopenai.proxy.ChatProxy -import org.jetbrains.annotations.Nullable - -import java.awt.* -import java.awt.datatransfer.DataFlavor - -class PasteAction extends SelectionAction { - PasteAction() { - super(false) - } - - interface VirtualAPI { - ConvertedText convert(String text, String from_language, String to_language) - - class ConvertedText { - public String code - public String language - - ConvertedText() {} - } - } - @Override - String getConfig(@Nullable Project project) { - return "" - } - - - @Override - String processSelection(SelectionState state, String config) { - return new ChatProxy( - clazz: VirtualAPI.class, - api: api, - model: AppSettingsState.instance.defaultChatModel(), - temperature: AppSettingsState.instance.temperature, - deserializerRetries: 5, - ).create().convert( - getClipboard().toString().trim(), - "autodetect", - state.language.name() - ).code ?: "" - } - - @Override - boolean isLanguageSupported(ComputerLanguage computerLanguage) { - if (computerLanguage == null) return false - return computerLanguage != ComputerLanguage.Text - } - - @Override - boolean isEnabled(AnActionEvent event) { - if (getClipboard() == null) return false - return super.isEnabled(event) - } - - private Object getClipboard() { - def contents = Toolkit.getDefaultToolkit().systemClipboard.getContents(null) - if (contents?.isDataFlavorSupported(DataFlavor.stringFlavor) == true) contents?.getTransferData(DataFlavor.stringFlavor) - else null - } -} \ No newline at end of file diff --git a/src/main/groovy/com/github/simiacryptus/aicoder/actions/code/RecentCodeEditsAction.groovy b/src/main/groovy/com/github/simiacryptus/aicoder/actions/code/RecentCodeEditsAction.groovy deleted file mode 100644 index 65522faa..00000000 --- a/src/main/groovy/com/github/simiacryptus/aicoder/actions/code/RecentCodeEditsAction.groovy +++ /dev/null @@ -1,42 +0,0 @@ -package com.github.simiacryptus.aicoder.actions.code - -import com.github.simiacryptus.aicoder.config.AppSettingsState -import com.github.simiacryptus.aicoder.util.ComputerLanguage -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.project.Project -import org.jetbrains.annotations.Nullable - -class RecentCodeEditsAction extends ActionGroup { - void update(AnActionEvent e) { - e.presentation.setEnabledAndVisible(isEnabled(e)) - super.update(e) - } - - AnAction[] getChildren(AnActionEvent e) { - if (null == e) return [] - def children = [] - for (instruction in AppSettingsState.instance.getRecentCommands("customEdits").mostUsedHistory.keySet()) { - def id = children.size() + 1 - def text = id < 10 ? "_${id}: ${instruction}" : "${id}: ${instruction}" - def element = new CustomEditAction() { - @Override String getConfig(@Nullable Project project) { - return instruction - } - } - element.templatePresentation.text = text - element.templatePresentation.description = instruction - element.templatePresentation.icon = null - children.add(element) - } - return children as AnAction[] - } - - static boolean isEnabled(AnActionEvent e) { - if (!UITools.hasSelection(e)) return false - def computerLanguage = ComputerLanguage.getComputerLanguage(e) - return computerLanguage != ComputerLanguage.Text - } -} \ No newline at end of file diff --git a/src/main/groovy/com/github/simiacryptus/aicoder/actions/code/RenameVariablesAction.groovy b/src/main/groovy/com/github/simiacryptus/aicoder/actions/code/RenameVariablesAction.groovy deleted file mode 100644 index 94741f60..00000000 --- a/src/main/groovy/com/github/simiacryptus/aicoder/actions/code/RenameVariablesAction.groovy +++ /dev/null @@ -1,89 +0,0 @@ -package com.github.simiacryptus.aicoder.actions.code - -import com.github.simiacryptus.aicoder.actions.SelectionAction -import com.github.simiacryptus.aicoder.config.AppSettingsState -import com.github.simiacryptus.aicoder.util.ComputerLanguage -import com.github.simiacryptus.aicoder.util.UITools -import com.intellij.openapi.actionSystem.AnActionEvent -import com.intellij.openapi.project.Project -import com.simiacryptus.jopenai.proxy.ChatProxy -import org.jetbrains.annotations.Nullable - -class RenameVariablesAction extends SelectionAction { - - interface RenameAPI { - SuggestionResponse suggestRenames( - String code, - String computerLanguage, - String humanLanguage - ) - - class SuggestionResponse { - public List suggestions = [] - - SuggestionResponse() {} - } - - class Suggestion { - public String originalName = null - public String suggestedName = null - - Suggestion() {} - } - } - - def getProxy() { - return new ChatProxy( - clazz: RenameAPI.class, - api: api, - model: AppSettingsState.instance.defaultChatModel(), - temperature: AppSettingsState.instance.temperature, - deserializerRetries: 5, - ).create() - } - - @Override - String getConfig(@Nullable Project project) { - return "" - } - - - @Override - String processSelection(AnActionEvent event, SelectionState state, String config) { - def renameSuggestions = UITools.run(event == null ? null : event.project, templateText, true, true, { - return proxy - .suggestRenames( - state.selectedText, - state.language?.name(), - AppSettingsState.instance.humanLanguage - ) - .suggestions - .findAll { it.originalName != null && it.suggestedName != null } - .collectEntries { [(it.originalName): it.suggestedName] } - }) - def selectedSuggestions = choose(renameSuggestions) - return UITools.run(event == null ? null : event.project, templateText, true, true, { - def selectedText = state.selectedText - def filter = renameSuggestions.findAll { x -> selectedSuggestions.contains(x.key) } - def txt = selectedText - for (entry in filter) { - txt = txt.replace(entry.key, entry.value) - } - return txt - }) - - } - - def choose(Map renameSuggestions) { - return UITools.showCheckboxDialog( - "Select which items to rename", - renameSuggestions.keySet().toArray(String[]::new), - renameSuggestions.collect { kv -> "${kv.key} -> ${kv.value}".toString() }.toArray(String[]::new) - ) - } - - boolean isLanguageSupported(ComputerLanguage computerLanguage) { - return computerLanguage != ComputerLanguage.Text - } - -} \ No newline at end of file diff --git a/src/main/groovy/com/github/simiacryptus/aicoder/actions/generic/AnalogueFileAction.groovy b/src/main/groovy/com/github/simiacryptus/aicoder/actions/generic/AnalogueFileAction.groovy deleted file mode 100644 index 005235de..00000000 --- a/src/main/groovy/com/github/simiacryptus/aicoder/actions/generic/AnalogueFileAction.groovy +++ /dev/null @@ -1,133 +0,0 @@ -package com.github.simiacryptus.aicoder.actions.generic - -import com.github.simiacryptus.aicoder.actions.FileContextAction -import com.github.simiacryptus.aicoder.config.AppSettingsState -import com.github.simiacryptus.aicoder.config.Name -import com.github.simiacryptus.aicoder.util.UITools -import com.intellij.openapi.project.Project -import com.simiacryptus.jopenai.ApiModel.ChatMessage -import com.simiacryptus.jopenai.ApiModel.ChatRequest -import org.apache.commons.io.FileUtils -import org.apache.commons.io.IOUtils - -import javax.swing.* -import java.nio.file.Path - -class AnalogueFileAction extends FileContextAction { - - AnalogueFileAction() { - super(true, false) - } - - - private static class ProjectFile { - public String path = "" - public String code = "" - - ProjectFile() { - } - } - - @SuppressWarnings("UNUSED") - static class SettingsUI { - @Name("Directive") - public JTextArea directive = new JTextArea( - /* text = */ """ - Create test cases - """.stripIndent().trim(), - /* rows = */ 3, - /* columns = */ 120 - ) - } - - static class Settings { - public String directive = "" - - Settings() { - } - } - - @Override - Settings getConfig(Project project) { - return UITools.showDialog( - project, - SettingsUI.class, - Settings.class, - "Create Analogue File", - {} - ) - } - - @Override - File[] processSelection(SelectionState state, Settings config) { - ProjectFile analogue = generateFile( - new ProjectFile( - path: state.projectRoot.toPath().relativize(state.selectedFile.toPath()), - code: IOUtils.toString(new FileInputStream(state.selectedFile), "UTF-8") - ), - config?.directive ?: "" - ) - Path outputPath = state.projectRoot.toPath().resolve(analogue.path) - if (outputPath.toFile().exists()) { - String extension = outputPath.toString().split("\\.").last() - String name = outputPath.toString().split("\\.").dropRight(1).join(".") - int fileIndex = (1..Integer.MAX_VALUE).find { - !new File(state.projectRoot, "$name.$it.$extension").exists() - } - outputPath = state.projectRoot.toPath().resolve("$name.$fileIndex.$extension") - } - outputPath.parent.toFile().mkdirs() - FileUtils.write(outputPath.toFile(), analogue.code, "UTF-8") - Thread.sleep(100) - return [outputPath.toFile()] - - } - - private ProjectFile generateFile(ProjectFile baseFile, String directive) { - def chatRequest = new ChatRequest() - def model = AppSettingsState.instance.defaultChatModel() - chatRequest.model = model.modelName - chatRequest.temperature = AppSettingsState.instance.temperature - chatRequest.messages = [ - new ChatMessage( - Role.system, """ - You will combine natural language instructions with a user provided code example to create a new file. - Provide a new filename and the code to be written to the file. - Paths should be relative to the project root and should not exist. - Output the file path using the a line with the format "File: ". - Output the file code directly after the header line with no additional decoration. - """.stripIndent(), null - ), - new ChatMessage( - Role.owner, """ - Create a new file based on the following directive: $directive - - The file should be based on `${baseFile.path}` which contains the following code: - - ``` - ${baseFile.code} - ``` - """.stripIndent(), null - ) - ] - String response = api.chat( - chatRequest, - AppSettingsState.instance.defaultChatModel() - ).choices?.first()?.message?.content?.trim() - String outputPath = baseFile.path - String header = response.split("\n").first() - String body = response.split("\n").drop(1).join("\n").trim() - if (body.contains("```")) { - body = body.split("```.*").drop(1).first().trim() - } - def pathPattern = ~"""File(?:name)?: ['`"]?([^'`"]+)['`"]?""" - def matcher = pathPattern.matcher(header) - if (matcher.find()) { - outputPath = matcher.group(1).trim() - } - return new ProjectFile( - path: outputPath, - code: body - ) - } -} \ No newline at end of file diff --git a/src/main/groovy/com/github/simiacryptus/aicoder/actions/generic/AppendAction.groovy b/src/main/groovy/com/github/simiacryptus/aicoder/actions/generic/AppendAction.groovy deleted file mode 100644 index 3465d2dc..00000000 --- a/src/main/groovy/com/github/simiacryptus/aicoder/actions/generic/AppendAction.groovy +++ /dev/null @@ -1,34 +0,0 @@ -package com.github.simiacryptus.aicoder.actions.generic - -import com.github.simiacryptus.aicoder.actions.SelectionAction -import com.github.simiacryptus.aicoder.config.AppSettingsState -import com.intellij.openapi.project.Project -import org.jetbrains.annotations.Nullable - -class AppendAction extends SelectionAction { - @Override - java.lang.String getConfig(@Nullable Project project) { - return "" - } - - @Override - String processSelection(SelectionState state, String config) { - def settings = AppSettingsState.instance - def request = settings.createChatRequest() - request.temperature = AppSettingsState.instance.temperature - request.messages = [ - new com.simiacryptus.jopenai.ApiModel.ChatMessage( - com.simiacryptus.jopenai.ApiModel.Role.system, - "Append text to the end of the user's prompt", null - ), - new com.simiacryptus.jopenai.ApiModel.ChatMessage( - com.simiacryptus.jopenai.ApiModel.Role.user, - state.selectedText.toString(), null - ) - ] - def chatResponse = api.chat(request, AppSettingsState.instance.defaultChatModel()) - def b4 = state.selectedText ?: "" - def str = (chatResponse.choices[0].message?.content ?: "") - return b4 + (str.startsWith(b4) ? str.substring(b4.length) : str) - } -} \ No newline at end of file diff --git a/src/main/groovy/com/github/simiacryptus/aicoder/actions/generic/CreateFileAction.groovy b/src/main/groovy/com/github/simiacryptus/aicoder/actions/generic/CreateFileAction.groovy deleted file mode 100644 index b0eb385b..00000000 --- a/src/main/groovy/com/github/simiacryptus/aicoder/actions/generic/CreateFileAction.groovy +++ /dev/null @@ -1,139 +0,0 @@ -package com.github.simiacryptus.aicoder.actions.generic - -import com.github.simiacryptus.aicoder.actions.FileContextAction -import com.github.simiacryptus.aicoder.config.AppSettingsState -import com.github.simiacryptus.aicoder.config.Name -import com.github.simiacryptus.aicoder.util.UITools -import com.intellij.openapi.project.Project -import com.simiacryptus.jopenai.ApiModel.ChatMessage -import com.simiacryptus.jopenai.ApiModel.ChatRequest - -import javax.swing.* - -class CreateFileAction extends FileContextAction { - - CreateFileAction() { - super(false, true) - } - - static class ProjectFile { - public String path = "" - public String code = "" - - ProjectFile() { - } - } - - static class SettingsUI { - @Name("Directive") - public JTextArea directive = new JTextArea( - /* text = */ """ - Create a default log4j configuration file - """.stripIndent().trim(), - /* rows = */ 3, - /* columns = */ 120 - ) - - SettingsUI() { - } - } - - static class Settings { - public String directive = "" - - Settings() { - } - } - - @Override - File[] processSelection( - SelectionState state, - Settings config - ) { - def projectRoot = state.projectRoot.toPath() - def inputPath = projectRoot.relativize(state.selectedFile.toPath()).toString() - def pathSegments = inputPath.split("/").toList() - def updirSegments = pathSegments.takeWhile { it == ".." }.toList() - def moduleRoot = projectRoot.resolve(pathSegments.take(updirSegments.size() * 2).join("/")) - def filePath = pathSegments.drop(updirSegments.size() * 2).join("/") - - def generatedFile = generateFile(filePath, config?.directive ?: "") - - def path = generatedFile.path - def outputPath = moduleRoot.resolve(path) - if (outputPath.toFile().exists()) { - def extension = path.split(".").last() - def name = path.split(".").init().join(".") - def fileIndex = (1..Integer.MAX_VALUE).find { - !new File("$name.$it.$extension").exists() - } - path = "$name.$fileIndex.$extension" - outputPath = projectRoot.resolve(path) - } - outputPath.parent.toFile().mkdirs() - outputPath.toFile().text = generatedFile.code - Thread.sleep(100) - - return [outputPath.toFile()] as File[] - } - - private ProjectFile generateFile( - String basePath, - String directive - ) { - def chatRequest = new ChatRequest() - def model = AppSettingsState.instance.defaultChatModel() - chatRequest.model = model.modelName - chatRequest.temperature = AppSettingsState.instance.temperature - chatRequest.messages = [ - //language=TEXT - new ChatMessage( - Role.system, """ - You will interpret natural language requirements to create a new file. - Provide a new filename and the code to be written to the file. - Paths should be relative to the project root and should not exist. - Output the file path using the a line with the format "File: ". - Output the file code directly after the header line with no additional decoration. - """.stripIndent(), null - ), - //language=TEXT - new ChatMessage( - Role.owner, """ - Create a new file based on the following directive: $directive - - The file location should be based on the selected path `${basePath}` - """.stripIndent(), null - ) - ] - def response = api.chat( - chatRequest, - AppSettingsState.instance.defaultChatModel() - ).choices?.first()?.message?.content?.trim() - def outputPath = basePath - def header = response.split("\n").first() - def body = response.split("\n").drop(1).join("\n").trim() - if (body.startsWith("```")) { - // Remove beginning ``` (optionally ```language) and ending ``` - body = body.split("\n").drop(1).init().join("\n").trim() - } - def pathPattern = ~"""File(?:name)?: ['`"]?([^'`"]+)['`"]?""" - if (header =~ pathPattern) { - def match = (header =~ pathPattern)[0] - outputPath = match[1].toString() - } - return new ProjectFile( - path: outputPath, - code: body - ) - } - - @Override - Settings getConfig(Project project) { - return UITools.showDialog( - project, - SettingsUI, - Settings, - "Create File from Requirements", {} - ) - } -} \ No newline at end of file diff --git a/src/main/groovy/com/github/simiacryptus/aicoder/actions/generic/ReplaceOptionsAction.groovy b/src/main/groovy/com/github/simiacryptus/aicoder/actions/generic/ReplaceOptionsAction.groovy deleted file mode 100644 index 1acbbf59..00000000 --- a/src/main/groovy/com/github/simiacryptus/aicoder/actions/generic/ReplaceOptionsAction.groovy +++ /dev/null @@ -1,63 +0,0 @@ -package com.github.simiacryptus.aicoder.actions.generic - -import com.github.simiacryptus.aicoder.actions.SelectionAction -import com.github.simiacryptus.aicoder.config.AppSettingsState -import com.github.simiacryptus.aicoder.util.UITools -import com.intellij.openapi.actionSystem.AnActionEvent -import com.intellij.openapi.project.Project -import com.simiacryptus.jopenai.proxy.ChatProxy -import com.simiacryptus.jopenai.util.StringUtil -import org.jetbrains.annotations.NotNull -import org.jetbrains.annotations.Nullable - -import static java.lang.Math.* - -class ReplaceOptionsAction extends SelectionAction { - interface VirtualAPI { - Suggestions suggestText(String template, List examples) - - class Suggestions { - public List choices = null - - Suggestions() {} - } - } - - def getProxy() { - return new ChatProxy( - clazz: VirtualAPI.class, - api: api, - model: AppSettingsState.instance.defaultChatModel(), - temperature: AppSettingsState.instance.temperature, - deserializerRetries: 5, - ).create() - } - @Override - String getConfig(@Nullable Project project) { - return "" - } - - @Override - String processSelection(@Nullable AnActionEvent event, @NotNull SelectionState state, @Nullable String config) { - List choices = UITools.run(event==null?null:event.project, templateText, true, true, { - String selectedText = state.selectedText - int idealLength = pow(2, 2 + ceil(log(selectedText.length()))).intValue() - int selectionStart = state.selectionOffset - String allBefore = state.entireDocument?.substring(0, selectionStart) ?: "" - int selectionEnd = state.selectionOffset + (state.selectionLength ?: 0) - String allAfter = state.entireDocument?.substring(selectionEnd, state.entireDocument.length()) ?: "" - String before = StringUtil.getSuffixForContext(allBefore, idealLength).replaceAll("\n", " ") - String after = StringUtil.getPrefixForContext(allAfter, idealLength).replaceAll("\n", " ") - return proxy.suggestText( - "$before _____ $after", - [selectedText] - ).choices - }) - return choose(choices) - } - - def choose(List choices) { - return UITools.showRadioButtonDialog("Select an option to fill in the blank:", choices.toArray(CharSequence[]::new))?.toString() ?: "" - } - -} \ No newline at end of file diff --git a/src/main/groovy/com/github/simiacryptus/aicoder/actions/markdown/MarkdownImplementActionGroup.groovy b/src/main/groovy/com/github/simiacryptus/aicoder/actions/markdown/MarkdownImplementActionGroup.groovy deleted file mode 100644 index a798a177..00000000 --- a/src/main/groovy/com/github/simiacryptus/aicoder/actions/markdown/MarkdownImplementActionGroup.groovy +++ /dev/null @@ -1,123 +0,0 @@ -package com.github.simiacryptus.aicoder.actions.markdown - -import com.github.simiacryptus.aicoder.actions.SelectionAction -import com.github.simiacryptus.aicoder.config.AppSettingsState -import com.github.simiacryptus.aicoder.util.ComputerLanguage -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.project.Project -import com.simiacryptus.jopenai.proxy.ChatProxy - -class MarkdownImplementActionGroup extends ActionGroup { - List markdownLanguages = [ - "sql", - "java", - "asp", - "c", - "clojure", - "coffee", - "cpp", - "csharp", - "css", - "bash", - "go", - "java", - "javascript", - "less", - "make", - "matlab", - "objectivec", - "pascal", - "PHP", - "Perl", - "python", - "rust", - "scss", - "sql", - "svg", - "swift", - "ruby", - "smalltalk", - "vhdl" - ] - - void update(AnActionEvent e) { - e.presentation.setEnabledAndVisible(isEnabled(e)) - super.update(e) - } - - static boolean isEnabled(AnActionEvent e) { - def computerLanguage = ComputerLanguage.getComputerLanguage(e) - if (null == computerLanguage) return false - if (ComputerLanguage.Markdown != computerLanguage) return false - return UITools.hasSelection(e) - } - - AnAction[] getChildren(AnActionEvent e) { - if (null == e) return [] - def computerLanguage = ComputerLanguage.getComputerLanguage(e) - if (null == computerLanguage) return [] - ArrayList actions = [] - for (language in markdownLanguages) { - actions.add(new MarkdownImplementAction(language)) - } - return actions.toArray(AnAction[]::new) - } - - - static class MarkdownImplementAction extends SelectionAction { - String language - - MarkdownImplementAction(String language) { - super(true) - this.language = language - this.templatePresentation.text = language - this.templatePresentation.description = language - } - - interface ConversionAPI { - ConvertedText implement(String text, String humanLanguage, String computerLanguage) - - class ConvertedText { - public String code - public String language - - ConvertedText() { - } - } - } - - def getProxy() { - return new ChatProxy( - clazz: ConversionAPI.class, - api: api, - model: AppSettingsState.instance.defaultChatModel(), - temperature: AppSettingsState.instance.temperature, - deserializerRetries: 5, - ).create() - } - - @Override - java.lang.String getConfig(Project project) { - return "" - } - - - String processSelection(SelectionState state, String config) { - def code = proxy.implement(state.selectedText ?: "", "autodetect", language).code ?: "" - return """ - | - |```$language - |$code - |``` - | - |""".stripMargin() - } - - boolean isLanguageSupported(ComputerLanguage computerLanguage) { - return ComputerLanguage.Markdown == computerLanguage - } - } -} \ No newline at end of file diff --git a/src/main/kotlin/com/github/simiacryptus/aicoder/actions/code/CommentsAction.kt b/src/main/kotlin/com/github/simiacryptus/aicoder/actions/code/CommentsAction.kt new file mode 100644 index 00000000..1793a0a2 --- /dev/null +++ b/src/main/kotlin/com/github/simiacryptus/aicoder/actions/code/CommentsAction.kt @@ -0,0 +1,47 @@ +package com.github.simiacryptus.aicoder.actions.code + +import com.github.simiacryptus.aicoder.actions.SelectionAction +import com.github.simiacryptus.aicoder.config.AppSettingsState +import com.github.simiacryptus.aicoder.util.ComputerLanguage +import com.intellij.openapi.project.Project +import com.simiacryptus.jopenai.proxy.ChatProxy + +class CommentsAction : SelectionAction() { + + override fun getConfig(project: Project?): String { + return "" + } + + override fun isLanguageSupported(computerLanguage: ComputerLanguage?): Boolean { + return computerLanguage != null && computerLanguage != ComputerLanguage.Text + } + + override fun processSelection(state: SelectionState, config: String?): String { + return ChatProxy( + clazz = CommentsAction_VirtualAPI::class.java, + api = api, + temperature = AppSettingsState.instance.temperature, + model = AppSettingsState.instance.defaultChatModel(), + deserializerRetries = 5 + ).create().editCode( + state.selectedText ?: "", + "Add comments to each line explaining the code", + state.language.toString(), + AppSettingsState.instance.humanLanguage + ).code ?: "" + } + + interface CommentsAction_VirtualAPI { + fun editCode( + code: String, + operations: String, + computerLanguage: String, + humanLanguage: String + ): CommentsAction_ConvertedText + + class CommentsAction_ConvertedText { + var code: String? = null + var language: String? = null + } + } +} \ No newline at end of file diff --git a/src/main/kotlin/com/github/simiacryptus/aicoder/actions/code/CustomEditAction.kt b/src/main/kotlin/com/github/simiacryptus/aicoder/actions/code/CustomEditAction.kt new file mode 100644 index 00000000..53010d4b --- /dev/null +++ b/src/main/kotlin/com/github/simiacryptus/aicoder/actions/code/CustomEditAction.kt @@ -0,0 +1,71 @@ +package com.github.simiacryptus.aicoder.actions.code + +import com.github.simiacryptus.aicoder.actions.SelectionAction +import com.github.simiacryptus.aicoder.config.AppSettingsState +import com.github.simiacryptus.aicoder.util.UITools +import com.intellij.openapi.project.Project +import com.simiacryptus.jopenai.proxy.ChatProxy +import javax.swing.JOptionPane + +open class CustomEditAction : SelectionAction() { + + interface VirtualAPI { + fun editCode( + code: String, + operation: String, + computerLanguage: String, + humanLanguage: String + ): EditedText + + data class EditedText( + var code: String? = null, + var language: String? = null + ) + } + + val proxy: VirtualAPI get() { + val chatProxy = ChatProxy( + clazz = VirtualAPI::class.java, + api = api, + temperature = AppSettingsState.instance.temperature, + model = AppSettingsState.instance.defaultChatModel(), + ) + chatProxy.addExample( + VirtualAPI.EditedText( + """ + // Print Hello, World! to the console + println("Hello, World!") + """.trimIndent(), + "java" + ) + ) { + it.editCode( + """println("Hello, World!")""", + "Add code comments", + "java", + "English" + ) + } + return chatProxy.create() + } + + override fun getConfig(project: Project?): String { + return UITools.showInputDialog( + null, "Instruction:", "Edit Code", JOptionPane.QUESTION_MESSAGE + //, AppSettingsState.instance.getRecentCommands("customEdits").mostRecentHistory + ) as String? ?: "" + } + + override fun processSelection(state: SelectionState, instruction: String?): String { + if (instruction == null || instruction.isBlank()) return state.selectedText ?: "" + val settings = AppSettingsState.instance + val outputHumanLanguage = AppSettingsState.instance.humanLanguage + settings.getRecentCommands("customEdits").addInstructionToHistory(instruction) + return proxy.editCode( + state.selectedText ?: "", + instruction ?: "", + state.language?.name ?: "", + outputHumanLanguage + ).code ?: state.selectedText ?: "" + } +} \ No newline at end of file diff --git a/src/main/kotlin/com/github/simiacryptus/aicoder/actions/code/DescribeAction.kt b/src/main/kotlin/com/github/simiacryptus/aicoder/actions/code/DescribeAction.kt new file mode 100644 index 00000000..65e8ce1b --- /dev/null +++ b/src/main/kotlin/com/github/simiacryptus/aicoder/actions/code/DescribeAction.kt @@ -0,0 +1,59 @@ +package com.github.simiacryptus.aicoder.actions.code + +import com.github.simiacryptus.aicoder.actions.SelectionAction +import com.github.simiacryptus.aicoder.config.AppSettingsState +import com.github.simiacryptus.aicoder.util.IndentedText +import com.intellij.openapi.project.Project +import com.simiacryptus.jopenai.proxy.ChatProxy +import com.simiacryptus.jopenai.util.StringUtil + +class DescribeAction : SelectionAction() { + + interface DescribeAction_VirtualAPI { + fun describeCode( + code: String, + computerLanguage: String, + humanLanguage: String + ): DescribeAction_ConvertedText + + class DescribeAction_ConvertedText { + var text: String? = null + var language: String? = null + } + } + + private val proxy: DescribeAction_VirtualAPI + get() = ChatProxy( + clazz = DescribeAction_VirtualAPI::class.java, + api = api, + temperature = AppSettingsState.instance.temperature, + model = AppSettingsState.instance.defaultChatModel(), + deserializerRetries = 5 + ).create() + + override fun getConfig(project: Project?): String { + return "" + } + + override fun processSelection(state: SelectionState, config: String?): String { + val description = proxy.describeCode( + IndentedText.fromString(state.selectedText).textBlock.toString().trim(), + state.language?.name ?: "", + AppSettingsState.instance.humanLanguage + ).text ?: "" + val wrapping = StringUtil.lineWrapping(description.trim(), 120) + val numberOfLines = wrapping.trim().split("\n").reversed().dropWhile { it.isEmpty() }.size + val commentStyle = if (numberOfLines == 1) { + state.language?.lineComment + } else { + state.language?.blockComment + } + return buildString { + append(state.indent) + append(commentStyle?.fromString(wrapping)?.withIndent(state.indent) ?: wrapping) + append("\n") + append(state.indent) + append(state.selectedText) + } + } +} \ No newline at end of file diff --git a/src/main/kotlin/com/github/simiacryptus/aicoder/actions/code/DocAction.kt b/src/main/kotlin/com/github/simiacryptus/aicoder/actions/code/DocAction.kt new file mode 100644 index 00000000..568684dc --- /dev/null +++ b/src/main/kotlin/com/github/simiacryptus/aicoder/actions/code/DocAction.kt @@ -0,0 +1,89 @@ +package com.github.simiacryptus.aicoder.actions.code + +import com.github.simiacryptus.aicoder.actions.SelectionAction +import com.github.simiacryptus.aicoder.config.AppSettingsState +import com.github.simiacryptus.aicoder.util.ComputerLanguage +import com.github.simiacryptus.aicoder.util.IndentedText +import com.github.simiacryptus.aicoder.util.psi.PsiUtil +import com.intellij.openapi.project.Project +import com.simiacryptus.jopenai.proxy.ChatProxy + +class DocAction : SelectionAction() { + + interface DocAction_VirtualAPI { + fun processCode( + code: String, + operation: String, + computerLanguage: String, + humanLanguage: String + ): DocAction_ConvertedText + + class DocAction_ConvertedText { + var text: String? = null + var language: String? = null + } + } + + private val proxy: DocAction_VirtualAPI by lazy { + val chatProxy = ChatProxy( + clazz = DocAction_VirtualAPI::class.java, + api = api, + model = AppSettingsState.instance.defaultChatModel(), + temperature = AppSettingsState.instance.temperature, + deserializerRetries = 5 + ) + chatProxy.addExample( + DocAction_VirtualAPI.DocAction_ConvertedText().apply { + text = """ + /** + * Prints "Hello, world!" to the console + */ + """.trimIndent() + language = "English" + } + ) { + it.processCode( + """ + fun hello() { + println("Hello, world!") + } + """.trimIndent(), + "Write detailed KDoc prefix for code block", + "Kotlin", + "English" + ) + } + chatProxy.create() + } + + override fun getConfig(project: Project?): String { + return "" + } + + override fun processSelection(state: SelectionState, config: String?): String { + val code = state.selectedText + val indentedInput = IndentedText.fromString(code.toString()) + val docString = proxy.processCode( + indentedInput.textBlock.toString(), + "Write detailed " + (state.language?.docStyle ?: "documentation") + " prefix for code block", + state.language?.name ?: "", + AppSettingsState.instance.humanLanguage + ).text ?: "" + return docString + code + } + + override fun isLanguageSupported(computerLanguage: ComputerLanguage?): Boolean { + if (computerLanguage == ComputerLanguage.Text) return false + if (computerLanguage?.docStyle == null) return false + if (computerLanguage.docStyle.isBlank()) return false + return true + } + + override fun editSelection(state: EditorState, start: Int, end: Int): Pair { + if (state.psiFile == null) return super.editSelection(state, start, end) + val codeBlock = PsiUtil.getCodeElement(state.psiFile, start, end) + if (codeBlock == null) return super.editSelection(state, start, end) + val textRange = codeBlock.textRange + return Pair(textRange.startOffset, textRange.endOffset) + } +} \ No newline at end of file diff --git a/src/main/kotlin/com/github/simiacryptus/aicoder/actions/code/ImplementStubAction.kt b/src/main/kotlin/com/github/simiacryptus/aicoder/actions/code/ImplementStubAction.kt new file mode 100644 index 00000000..5fca34ce --- /dev/null +++ b/src/main/kotlin/com/github/simiacryptus/aicoder/actions/code/ImplementStubAction.kt @@ -0,0 +1,80 @@ +package com.github.simiacryptus.aicoder.actions.code + +import com.github.simiacryptus.aicoder.actions.SelectionAction +import com.github.simiacryptus.aicoder.config.AppSettingsState +import com.github.simiacryptus.aicoder.util.ComputerLanguage +import com.github.simiacryptus.aicoder.util.psi.PsiUtil +import com.intellij.openapi.project.Project +import com.simiacryptus.jopenai.proxy.ChatProxy +import com.simiacryptus.jopenai.util.StringUtil +import java.util.* + +class ImplementStubAction : SelectionAction() { + + interface VirtualAPI { + fun editCode( + code: String, + operation: String, + computerLanguage: String, + humanLanguage: String + ): ConvertedText + + class ConvertedText { + var code: String? = null + var language: String? = null + } + } + + private fun getProxy(): VirtualAPI { + return ChatProxy( + clazz = VirtualAPI::class.java, + api = api, + model = AppSettingsState.instance.defaultChatModel(), + temperature = AppSettingsState.instance.temperature, + deserializerRetries = 5 + ).create() + } + + override fun isLanguageSupported(computerLanguage: ComputerLanguage?): Boolean { + if (computerLanguage == null) return false + return computerLanguage != ComputerLanguage.Text + } + + override fun defaultSelection(editorState: EditorState, offset: Int): Pair { + val codeRanges = editorState.contextRanges.filter { PsiUtil.matchesType(it.name, PsiUtil.ELEMENTS_CODE) } + if (codeRanges.isEmpty()) return editorState.line + return codeRanges.minByOrNull { it.length() }?.range() ?: editorState.line + } + + override fun getConfig(project: Project?): String { + return "" + } + + override fun processSelection(state: SelectionState, config: String?): String { + val code = state.selectedText ?: "" + val settings = AppSettingsState.instance + val outputHumanLanguage = settings.humanLanguage + val computerLanguage = state.language + + val codeContext = state.contextRanges.filter { + PsiUtil.matchesType( + it.name, + PsiUtil.ELEMENTS_CODE + ) + } + var smallestIntersectingMethod = "" + if (codeContext.isNotEmpty()) smallestIntersectingMethod = codeContext.minByOrNull { it.length() }?.subString(state.entireDocument ?: "") ?: "" + + var declaration = code + declaration = StringUtil.stripSuffix(declaration.trim(), smallestIntersectingMethod) + declaration = declaration.trim() + + return getProxy().editCode( + declaration, + "Implement Stub", + computerLanguage?.name?.lowercase(Locale.ROOT) ?: "", + outputHumanLanguage + ).code ?: "" + } + +} \ No newline at end of file diff --git a/src/main/kotlin/com/github/simiacryptus/aicoder/actions/code/InsertImplementationAction.kt b/src/main/kotlin/com/github/simiacryptus/aicoder/actions/code/InsertImplementationAction.kt new file mode 100644 index 00000000..8dec68de --- /dev/null +++ b/src/main/kotlin/com/github/simiacryptus/aicoder/actions/code/InsertImplementationAction.kt @@ -0,0 +1,127 @@ +package com.github.simiacryptus.aicoder.actions.code + +import com.github.simiacryptus.aicoder.actions.SelectionAction +import com.github.simiacryptus.aicoder.config.AppSettingsState +import com.github.simiacryptus.aicoder.util.ComputerLanguage +import com.github.simiacryptus.aicoder.util.TextBlock +import com.github.simiacryptus.aicoder.util.UITools +import com.github.simiacryptus.aicoder.util.psi.PsiClassContext +import com.github.simiacryptus.aicoder.util.psi.PsiUtil +import com.intellij.openapi.application.runReadAction +import com.intellij.openapi.project.Project +import com.simiacryptus.jopenai.proxy.ChatProxy + +class InsertImplementationAction : SelectionAction() { + + interface VirtualAPI { + fun implementCode( + specification: String, + prefix: String, + computerLanguage: String, + humanLanguage: String + ): ConvertedText + + class ConvertedText { + var code: String? = null + var language: String? = null + } + } + + private fun getProxy(): VirtualAPI { + return ChatProxy( + clazz = VirtualAPI::class.java, + api = api, + model = AppSettingsState.instance.defaultChatModel(), + temperature = AppSettingsState.instance.temperature, + deserializerRetries = 5 + ).create() + } + + override fun getConfig(project: Project?): String { + return "" + } + + override fun defaultSelection(editorState: EditorState, offset: Int): Pair { + val foundItem = editorState.contextRanges.filter { + PsiUtil.matchesType( + it.name, + PsiUtil.ELEMENTS_COMMENTS + ) + }.minByOrNull { it.length() } + return foundItem?.range() ?: editorState.line + } + + override fun editSelection(state: EditorState, start: Int, end: Int): Pair { + val foundItem = state.contextRanges.filter { + PsiUtil.matchesType( + it.name, + PsiUtil.ELEMENTS_COMMENTS + ) + }.minByOrNull { it.length() } + return foundItem?.range() ?: Pair(start, end) + } + + override fun processSelection(state: SelectionState, config: String?): String { + val humanLanguage = AppSettingsState.instance.humanLanguage + val computerLanguage = state.language + val psiClassContextActionParams = getPsiClassContextActionParams(state) + val selectedText = state.selectedText ?: "" + + val comment = psiClassContextActionParams.largestIntersectingComment + var instruct = comment?.subString(state.entireDocument ?: "")?.trim() ?: selectedText + if (selectedText.split(" ").dropWhile { it.isEmpty() }.size > 4) { + instruct = selectedText.trim() + } + val fromString: TextBlock? = computerLanguage?.getCommentModel(instruct)?.fromString(instruct) + val specification = fromString?.rawString()?.map { it.toString().trim() } + ?.filter { it.isNotEmpty() }?.reduce { a, b -> "$a $b" } ?: return selectedText + val code = if (state.psiFile != null) { + UITools.run(state.project, "Insert Implementation", true, true) { + val psiClassContext = runReadAction { + PsiClassContext.getContext( + state.psiFile, + psiClassContextActionParams.selectionStart, + psiClassContextActionParams.selectionEnd, + computerLanguage + ).toString() + } + getProxy().implementCode( + specification, + psiClassContext, + computerLanguage.name, + humanLanguage + ).code + } + } else { + getProxy().implementCode( + specification, + "", + computerLanguage.name, + humanLanguage + ).code + } + return if (code != null) "$selectedText\n${state.indent}$code" else selectedText + } + + private fun getPsiClassContextActionParams(state: SelectionState): PsiClassContextActionParams { + val selectionStart = state.selectionOffset + return PsiClassContextActionParams( + selectionStart, + selectionStart + (state.selectionLength ?: 0), + state.contextRanges.find { PsiUtil.matchesType(it.name, PsiUtil.ELEMENTS_COMMENTS) } + ) + } + + override fun isLanguageSupported(computerLanguage: ComputerLanguage?): Boolean { + if (computerLanguage == null || computerLanguage == ComputerLanguage.Text || computerLanguage == ComputerLanguage.Markdown) { + return false + } + return super.isLanguageSupported(computerLanguage) + } + + private class PsiClassContextActionParams( + val selectionStart: Int, + val selectionEnd: Int, + val largestIntersectingComment: SelectionAction.ContextRange? + ) +} \ No newline at end of file diff --git a/src/main/kotlin/com/github/simiacryptus/aicoder/actions/code/PasteAction.kt b/src/main/kotlin/com/github/simiacryptus/aicoder/actions/code/PasteAction.kt new file mode 100644 index 00000000..4ba3108b --- /dev/null +++ b/src/main/kotlin/com/github/simiacryptus/aicoder/actions/code/PasteAction.kt @@ -0,0 +1,55 @@ +package com.github.simiacryptus.aicoder.actions.code + +import com.github.simiacryptus.aicoder.actions.SelectionAction +import com.github.simiacryptus.aicoder.config.AppSettingsState +import com.github.simiacryptus.aicoder.util.ComputerLanguage +import com.intellij.openapi.actionSystem.AnActionEvent +import com.intellij.openapi.project.Project +import com.simiacryptus.jopenai.proxy.ChatProxy +import java.awt.Toolkit +import java.awt.datatransfer.DataFlavor + +class PasteAction : SelectionAction(false) { + + interface VirtualAPI { + fun convert(text: String, from_language: String, to_language: String): ConvertedText + + class ConvertedText { + var code: String? = null + var language: String? = null + } + } + + override fun getConfig(project: Project?): String { + return "" + } + + override fun processSelection(state: SelectionState, config: String?): String { + return ChatProxy( + VirtualAPI::class.java, + api, + AppSettingsState.instance.defaultChatModel(), + AppSettingsState.instance.temperature, + ).create().convert( + getClipboard().toString().trim(), + "autodetect", + state.language?.name ?: "" + ).code ?: "" + } + + override fun isLanguageSupported(computerLanguage: ComputerLanguage?): Boolean { + if (computerLanguage == null) return false + return computerLanguage != ComputerLanguage.Text + } + + override fun isEnabled(event: AnActionEvent): Boolean { + if (getClipboard() == null) return false + return super.isEnabled(event) + } + + private fun getClipboard(): Any? { + val contents = Toolkit.getDefaultToolkit().systemClipboard.getContents(null) + return if (contents?.isDataFlavorSupported(DataFlavor.stringFlavor) == true) contents.getTransferData(DataFlavor.stringFlavor) + else null + } +} \ No newline at end of file diff --git a/src/main/kotlin/com/github/simiacryptus/aicoder/actions/code/RecentCodeEditsAction.kt b/src/main/kotlin/com/github/simiacryptus/aicoder/actions/code/RecentCodeEditsAction.kt new file mode 100644 index 00000000..b4a17b41 --- /dev/null +++ b/src/main/kotlin/com/github/simiacryptus/aicoder/actions/code/RecentCodeEditsAction.kt @@ -0,0 +1,43 @@ +package com.github.simiacryptus.aicoder.actions.code + +import com.github.simiacryptus.aicoder.config.AppSettingsState +import com.github.simiacryptus.aicoder.util.ComputerLanguage +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.project.Project + +class RecentCodeEditsAction : ActionGroup() { + override fun update(e: AnActionEvent) { + e.presentation.isEnabledAndVisible = isEnabled(e) + super.update(e) + } + + override fun getChildren(e: AnActionEvent?): Array { + if (e == null) return emptyArray() + val children = mutableListOf() + for ((instruction, _) in AppSettingsState.instance.getRecentCommands("customEdits").mostUsedHistory) { + val id = children.size + 1 + val text = if (id < 10) "_$id: $instruction" else "$id: $instruction" + val element = object : CustomEditAction() { + override fun getConfig(project: Project?): String { + return instruction + } + } + element.templatePresentation.text = text + element.templatePresentation.description = instruction + element.templatePresentation.icon = null + children.add(element) + } + return children.toTypedArray() + } + + companion object { + fun isEnabled(e: AnActionEvent): Boolean { + if (!UITools.hasSelection(e)) return false + val computerLanguage = ComputerLanguage.getComputerLanguage(e) + return computerLanguage != ComputerLanguage.Text + } + } +} \ No newline at end of file diff --git a/src/main/kotlin/com/github/simiacryptus/aicoder/actions/code/RenameVariablesAction.kt b/src/main/kotlin/com/github/simiacryptus/aicoder/actions/code/RenameVariablesAction.kt new file mode 100644 index 00000000..e393e513 --- /dev/null +++ b/src/main/kotlin/com/github/simiacryptus/aicoder/actions/code/RenameVariablesAction.kt @@ -0,0 +1,78 @@ +package com.github.simiacryptus.aicoder.actions.code + +import com.github.simiacryptus.aicoder.actions.SelectionAction +import com.github.simiacryptus.aicoder.config.AppSettingsState +import com.github.simiacryptus.aicoder.util.ComputerLanguage +import com.github.simiacryptus.aicoder.util.UITools +import com.intellij.openapi.actionSystem.AnActionEvent +import com.intellij.openapi.project.Project +import com.simiacryptus.jopenai.proxy.ChatProxy + +class RenameVariablesAction : SelectionAction() { + + interface RenameAPI { + fun suggestRenames( + code: String, + computerLanguage: String, + humanLanguage: String + ): SuggestionResponse + + class SuggestionResponse { + var suggestions: MutableList = mutableListOf() + + class Suggestion { + var originalName: String? = null + var suggestedName: String? = null + } + } + } + + val proxy: RenameAPI get() { + return ChatProxy( + clazz = RenameAPI::class.java, + api = api, + model = AppSettingsState.instance.defaultChatModel(), + temperature = AppSettingsState.instance.temperature, + deserializerRetries = 5 + ).create() + } + + override fun getConfig(project: Project?): String { + return "" + } + + override fun processSelection(event: AnActionEvent?, state: SelectionState, config: String?): String { + val renameSuggestions = UITools.run(event?.project, templateText, true, true) { + proxy + .suggestRenames( + state.selectedText ?: "", + state.language?.name ?: "", + AppSettingsState.instance.humanLanguage + ) + .suggestions + .filter { it.originalName != null && it.suggestedName != null } + .associate { it.originalName!! to it.suggestedName!! } + } + val selectedSuggestions = choose(renameSuggestions) + return UITools.run(event?.project, templateText, true, true) { + var selectedText = state.selectedText + val filter = renameSuggestions.filter { it.key in selectedSuggestions } + filter.forEach { (key, value) -> + selectedText = selectedText?.replace(key, value) + } + selectedText ?: "" + } + } + + private fun choose(renameSuggestions: Map): Set { + return UITools.showCheckboxDialog( + "Select which items to rename", + renameSuggestions.keys.toTypedArray(), + renameSuggestions.map { (key, value) -> "$key -> $value" }.toTypedArray() + ).toSet() + } + + override fun isLanguageSupported(computerLanguage: ComputerLanguage?): Boolean { + return computerLanguage != ComputerLanguage.Text + } +} \ No newline at end of file diff --git a/src/main/kotlin/com/github/simiacryptus/aicoder/actions/dev/InternalCoderAction.kt b/src/main/kotlin/com/github/simiacryptus/aicoder/actions/dev/InternalCoderAction.kt index 6ff079fe..069e3adf 100644 --- a/src/main/kotlin/com/github/simiacryptus/aicoder/actions/dev/InternalCoderAction.kt +++ b/src/main/kotlin/com/github/simiacryptus/aicoder/actions/dev/InternalCoderAction.kt @@ -20,7 +20,6 @@ class InternalCoderAction : BaseAction() { "event" to e, ).toMutableMap() - // TODO: Set this up at startup and lock ApplicationServices ApplicationServices.clientManager = object : ClientManager() { override fun createClient(session: Session, user: User?, dataStorage: StorageInterface?) = @@ -28,7 +27,6 @@ class InternalCoderAction : BaseAction() { } IdeaKotlinInterpreter.project = e.getData(CommonDataKeys.PROJECT) ?: throw IllegalStateException("No project") - e.getData(CommonDataKeys.EDITOR)?.apply { symbols["editor"] = this } e.getData(CommonDataKeys.PSI_FILE)?.apply { symbols["file"] = this } e.getData(CommonDataKeys.PSI_ELEMENT)?.apply { symbols["element"] = this } diff --git a/src/main/kotlin/com/github/simiacryptus/aicoder/actions/generic/AnalogueFileAction.kt b/src/main/kotlin/com/github/simiacryptus/aicoder/actions/generic/AnalogueFileAction.kt new file mode 100644 index 00000000..f44665a3 --- /dev/null +++ b/src/main/kotlin/com/github/simiacryptus/aicoder/actions/generic/AnalogueFileAction.kt @@ -0,0 +1,121 @@ +package com.github.simiacryptus.aicoder.actions.generic + +import com.github.simiacryptus.aicoder.actions.FileContextAction +import com.github.simiacryptus.aicoder.config.AppSettingsState +import com.github.simiacryptus.aicoder.config.Name +import com.github.simiacryptus.aicoder.util.UITools +import com.intellij.openapi.project.Project +import com.simiacryptus.jopenai.ApiModel +import com.simiacryptus.jopenai.ApiModel.ChatMessage +import com.simiacryptus.jopenai.ApiModel.Role +import com.simiacryptus.jopenai.ClientUtil.toContentList +import org.apache.commons.io.FileUtils +import org.apache.commons.io.IOUtils +import java.io.File +import java.io.FileInputStream +import javax.swing.JTextArea + +class AnalogueFileAction : FileContextAction() { + + data class ProjectFile( + val path: String = "", + val code: String = "" + ) + + class SettingsUI { + @Name("Directive") + var directive: JTextArea = JTextArea( + """ + Create test cases + """.trimIndent(), + 3, + 120 + ) + } + + class Settings ( + var directive: String = "" + ) + + override fun getConfig(project: Project?): Settings? { + return UITools.showDialog( + project, + SettingsUI::class.java, + Settings::class.java, + "Create Analogue File" + ) + } + + override fun processSelection(state: SelectionState, config: Settings?): Array { + val analogue = generateFile( + ProjectFile( + path = state.projectRoot.toPath().relativize(state.selectedFile.toPath()).toString(), + code = IOUtils.toString(FileInputStream(state.selectedFile), "UTF-8") + ), + config?.directive ?: "" + ) + var outputPath = state.projectRoot.toPath().resolve(analogue.path) + if (outputPath.toFile().exists()) { + val extension = outputPath.toString().split(".").last() + val name = outputPath.toString().split(".").dropLast(1).joinToString(".") + val fileIndex = (1..Int.MAX_VALUE).find { + !File(state.projectRoot, "$name.$it.$extension").exists() + } + outputPath = state.projectRoot.toPath().resolve("$name.$fileIndex.$extension") + } + outputPath.parent.toFile().mkdirs() + FileUtils.write(outputPath.toFile(), analogue.code, "UTF-8") + Thread.sleep(100) + return arrayOf(outputPath.toFile()) + } + + private fun generateFile(baseFile: ProjectFile, directive: String): ProjectFile { + val model = AppSettingsState.instance.defaultChatModel() + val chatRequest = ApiModel.ChatRequest( + model = model.modelName, + temperature = AppSettingsState.instance.temperature, + messages = listOf( + ChatMessage( + Role.system, """ + You will combine natural language instructions with a user provided code example to create a new file. + Provide a new filename and the code to be written to the file. + Paths should be relative to the project root and should not exist. + Output the file path using the a line with the format "File: ". + Output the file code directly after the header line with no additional decoration. + """.trimIndent().toContentList(), null + ), + ChatMessage( + Role.user, """ + Create a new file based on the following directive: $directive + + The file should be based on `${baseFile.path}` which contains the following code: + + ``` + ${baseFile.code} + ``` + """.trimIndent().toContentList(), null + ) + + ) + ) + val response = api.chat( + chatRequest, + AppSettingsState.instance.defaultChatModel() + ).choices.first().message?.content?.trim() + var outputPath = baseFile.path + val header = response?.split("\n")?.first() + var body = response?.split("\n")?.drop(1)?.joinToString("\n")?.trim() + if (body?.contains("```") == true) { + body = body.split("```.*".toRegex()).drop(1).firstOrNull()?.trim() ?: body + } + val pathPattern = "File(?:name)?: ['\"]?([^'\"]+)['\"]?".toRegex() + val matcher = pathPattern.find(header ?: "") + if (matcher != null) { + outputPath = matcher.groupValues[1].trim() + } + return ProjectFile( + path = outputPath, + code = body ?: "" + ) + } +} diff --git a/src/main/kotlin/com/github/simiacryptus/aicoder/actions/generic/AppendAction.kt b/src/main/kotlin/com/github/simiacryptus/aicoder/actions/generic/AppendAction.kt new file mode 100644 index 00000000..7899c1fd --- /dev/null +++ b/src/main/kotlin/com/github/simiacryptus/aicoder/actions/generic/AppendAction.kt @@ -0,0 +1,29 @@ + package com.github.simiacryptus.aicoder.actions.generic + +import com.github.simiacryptus.aicoder.actions.SelectionAction +import com.github.simiacryptus.aicoder.config.AppSettingsState +import com.intellij.openapi.project.Project +import com.simiacryptus.jopenai.ApiModel.ChatMessage +import com.simiacryptus.jopenai.ApiModel.Role +import com.simiacryptus.jopenai.ClientUtil.toContentList + + class AppendAction : SelectionAction() { + override fun getConfig(project: Project?): String { + return "" + } + + override fun processSelection(state: SelectionState, config: String?): String { + val settings = AppSettingsState.instance + val request = settings.createChatRequest().copy( + temperature = settings.temperature, + messages = listOf( + ChatMessage(Role.system, "Append text to the end of the user's prompt".toContentList(), null), + ChatMessage(Role.user, state.selectedText.toString().toContentList(), null) + ), + ) + val chatResponse = api.chat(request, settings.defaultChatModel()) + val b4 = state.selectedText ?: "" + val str = chatResponse.choices[0].message?.content ?: "" + return b4 + if (str.startsWith(b4)) str.substring(b4.length) else str + } +} \ No newline at end of file diff --git a/src/main/kotlin/com/github/simiacryptus/aicoder/actions/generic/CreateFileAction.kt b/src/main/kotlin/com/github/simiacryptus/aicoder/actions/generic/CreateFileAction.kt new file mode 100644 index 00000000..dbf176bb --- /dev/null +++ b/src/main/kotlin/com/github/simiacryptus/aicoder/actions/generic/CreateFileAction.kt @@ -0,0 +1,105 @@ +package com.github.simiacryptus.aicoder.actions.generic + +import com.github.simiacryptus.aicoder.actions.FileContextAction +import com.github.simiacryptus.aicoder.config.AppSettingsState +import com.github.simiacryptus.aicoder.config.Name +import com.simiacryptus.jopenai.ApiModel.* +import com.simiacryptus.jopenai.ClientUtil.toContentList +import java.io.File +import javax.swing.JTextArea + +class CreateFileAction : FileContextAction(false, true) { + + class ProjectFile(var path: String = "", var code: String = "") + + class SettingsUI { + @Name("Directive") + var directive: JTextArea = JTextArea( + """ + Create a default log4j configuration file + """.trimIndent(), 3, 120 + ) + } + + class Settings(var directive: String = "") + + override fun processSelection( + state: SelectionState, + config: Settings? + ): Array { + val projectRoot = state.projectRoot.toPath() + val inputPath = projectRoot.relativize(state.selectedFile.toPath()).toString() + val pathSegments = inputPath.split("/").toList() + val updirSegments = pathSegments.takeWhile { it == ".." } + val moduleRoot = projectRoot.resolve(pathSegments.take(updirSegments.size * 2).joinToString("/")) + val filePath = pathSegments.drop(updirSegments.size * 2).joinToString("/") + + val generatedFile = generateFile(filePath, config?.directive ?: "") + + var path = generatedFile.path + var outputPath = moduleRoot.resolve(path) + if (outputPath.toFile().exists()) { + val extension = path.substringAfterLast(".") + val name = path.substringBeforeLast(".") + val fileIndex = (1..Int.MAX_VALUE).find { + !File("$name.$it.$extension").exists() + } + path = "$name.$fileIndex.$extension" + outputPath = projectRoot.resolve(path) + } + outputPath.parent.toFile().mkdirs() + outputPath.toFile().writeText(generatedFile.code) + Thread.sleep(100) + + return arrayOf(outputPath.toFile()) + } + + private fun generateFile( + basePath: String, + directive: String + ): ProjectFile { + val model = AppSettingsState.instance.defaultChatModel() + val chatRequest = ChatRequest( + model = model.modelName, + temperature = AppSettingsState.instance.temperature, + messages = listOf( + ChatMessage( + Role.system, """ + You will interpret natural language requirements to create a new file. + Provide a new filename and the code to be written to the file. + Paths should be relative to the project root and should not exist. + Output the file path using the a line with the format "File: ". + Output the file code directly after the header line with no additional decoration. + """.trimIndent().toContentList(), null + ), + ChatMessage( + Role.user, """ + Create a new file based on the following directive: $directive + + The file location should be based on the selected path `$basePath` + """.trimIndent().toContentList(), null + ) + ) + ) + val response = api.chat( + chatRequest, + AppSettingsState.instance.defaultChatModel() + ).choices?.first()?.message?.content?.trim() ?: "" + var outputPath = basePath + val header = response.lines().first() + var body = response.lines().drop(1).joinToString("\n").trim() + if (body.startsWith("```")) { + // Remove beginning ``` (optionally ```language) and ending ``` + body = body.split("\n").drop(1).joinToString("\n").trim() + } + val pathPattern = """File(?:name)?: ['`"]?([^'`"]+)['`"]?""".toRegex() + if (pathPattern.matches(header)) { + val match = pathPattern.matchEntire(header)!! + outputPath = match.groupValues[1] + } + return ProjectFile( + path = outputPath, + code = body + ) + } +} \ No newline at end of file diff --git a/src/main/kotlin/com/github/simiacryptus/aicoder/actions/generic/ReplaceOptionsAction.kt b/src/main/kotlin/com/github/simiacryptus/aicoder/actions/generic/ReplaceOptionsAction.kt new file mode 100644 index 00000000..eaa4a8f4 --- /dev/null +++ b/src/main/kotlin/com/github/simiacryptus/aicoder/actions/generic/ReplaceOptionsAction.kt @@ -0,0 +1,58 @@ +package com.github.simiacryptus.aicoder.actions.generic + +import com.github.simiacryptus.aicoder.actions.SelectionAction +import com.github.simiacryptus.aicoder.config.AppSettingsState +import com.github.simiacryptus.aicoder.util.UITools +import com.intellij.openapi.actionSystem.AnActionEvent +import com.intellij.openapi.project.Project +import com.simiacryptus.jopenai.proxy.ChatProxy +import com.simiacryptus.jopenai.util.StringUtil +import kotlin.math.ceil +import kotlin.math.ln +import kotlin.math.pow + +class ReplaceOptionsAction : SelectionAction() { + interface VirtualAPI { + fun suggestText(template: String, examples: List): Suggestions + + class Suggestions { + var choices: List? = null + } + } + + val proxy: VirtualAPI get() { + return ChatProxy( + clazz = VirtualAPI::class.java, + api = api, + model = AppSettingsState.instance.defaultChatModel(), + temperature = AppSettingsState.instance.temperature, + deserializerRetries = 5 + ).create() + } + + override fun getConfig(project: Project?): String { + return "" + } + + override fun processSelection(event: AnActionEvent?, state: SelectionState, config: String?): String { + val choices = UITools.run(event?.project, templateText, true, true) { + val selectedText = state.selectedText + val idealLength = 2.0.pow(2 + ceil(ln(selectedText?.length?.toDouble() ?: 1.0))).toInt() + val selectionStart = state.selectionOffset + val allBefore = state.entireDocument?.substring(0, selectionStart) ?: "" + val selectionEnd = state.selectionOffset + (state.selectionLength ?: 0) + val allAfter = state.entireDocument?.substring(selectionEnd, state.entireDocument.length) ?: "" + val before = StringUtil.getSuffixForContext(allBefore, idealLength).toString().replace('\n', ' ') + val after = StringUtil.getPrefixForContext(allAfter, idealLength).toString().replace('\n', ' ') + proxy.suggestText( + "$before _____ $after", + listOf(selectedText.toString()) + ).choices + } + return choose(choices ?: listOf()) + } + + private fun choose(choices: List): String { + return UITools.showRadioButtonDialog("Select an option to fill in the blank:", *choices.toTypedArray())?.toString() ?: "" + } +} \ No newline at end of file diff --git a/src/main/kotlin/com/github/simiacryptus/aicoder/actions/markdown/MarkdownImplementActionGroup.kt b/src/main/kotlin/com/github/simiacryptus/aicoder/actions/markdown/MarkdownImplementActionGroup.kt new file mode 100644 index 00000000..5ca450b4 --- /dev/null +++ b/src/main/kotlin/com/github/simiacryptus/aicoder/actions/markdown/MarkdownImplementActionGroup.kt @@ -0,0 +1,81 @@ +package com.github.simiacryptus.aicoder.actions.markdown + +import com.github.simiacryptus.aicoder.actions.SelectionAction +import com.github.simiacryptus.aicoder.config.AppSettingsState +import com.github.simiacryptus.aicoder.util.ComputerLanguage +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.project.Project +import com.simiacryptus.jopenai.proxy.ChatProxy + +class MarkdownImplementActionGroup : ActionGroup() { + private val markdownLanguages = listOf( + "sql", "java", "asp", "c", "clojure", "coffee", "cpp", "csharp", "css", "bash", "go", "java", "javascript", + "less", "make", "matlab", "objectivec", "pascal", "PHP", "Perl", "python", "rust", "scss", "sql", "svg", + "swift", "ruby", "smalltalk", "vhdl" + ) + + override fun update(e: AnActionEvent) { + e.presentation.isEnabledAndVisible = isEnabled(e) + super.update(e) + } + + companion object { + fun isEnabled(e: AnActionEvent): Boolean { + val computerLanguage = ComputerLanguage.getComputerLanguage(e) ?: return false + if (ComputerLanguage.Markdown != computerLanguage) return false + return UITools.hasSelection(e) + } + } + + override fun getChildren(e: AnActionEvent?): Array { + if (e == null) return emptyArray() + val computerLanguage = ComputerLanguage.getComputerLanguage(e) ?: return emptyArray() + val actions = markdownLanguages.map { language -> MarkdownImplementAction(language) } + return actions.toTypedArray() + } + + class MarkdownImplementAction(private val language: String) : SelectionAction(true) { + init { + templatePresentation.text = language + templatePresentation.description = language + } + + interface ConversionAPI { + fun implement(text: String, humanLanguage: String, computerLanguage: String): ConvertedText + + class ConvertedText { + var code: String? = null + var language: String? = null + } + } + + private fun getProxy(): ConversionAPI { + return ChatProxy( + clazz = ConversionAPI::class.java, + api = api, + model = AppSettingsState.instance.defaultChatModel(), + temperature = AppSettingsState.instance.temperature, + deserializerRetries = 5 + ).create() + } + + override fun getConfig(project: Project?): String { + return "" + } + + override fun processSelection(state: SelectionState, config: String?): String { + val code = getProxy().implement(state.selectedText ?: "", "autodetect", language).code ?: "" + return """ + | + | + |```$language + |$code + |``` + | + |""".trimMargin() + } + } +} \ No newline at end of file diff --git a/src/main/kotlin/com/github/simiacryptus/aicoder/config/ActionSettingsRegistry.kt b/src/main/kotlin/com/github/simiacryptus/aicoder/config/ActionSettingsRegistry.kt index f42cb0b4..8ef7c92e 100644 --- a/src/main/kotlin/com/github/simiacryptus/aicoder/config/ActionSettingsRegistry.kt +++ b/src/main/kotlin/com/github/simiacryptus/aicoder/config/ActionSettingsRegistry.kt @@ -2,9 +2,9 @@ import com.fasterxml.jackson.annotation.JsonIgnore import com.github.simiacryptus.aicoder.ui.EditorMenu +import com.github.simiacryptus.aicoder.util.IdeaKotlinInterpreter import com.github.simiacryptus.aicoder.util.UITools import com.intellij.openapi.actionSystem.AnAction -import groovy.lang.GroovyClassLoader import java.io.File import java.util.stream.Collectors @@ -16,8 +16,8 @@ class ActionSettingsRegistry { fun edit(superChildren: Array): Array { val children = superChildren.toList().toMutableList() children.toTypedArray().forEach { - val language = "groovy" - val code = load(it.javaClass, language) + val language = "kt" + val code: String? = load(it.javaClass, language) if (null != code) { try { val actionConfig = this.getActionConfig(it) @@ -49,8 +49,8 @@ class ActionSettingsRegistry { children.remove(it) } } else { - val localCode = actionConfig.file.readText() - if (localCode != code) { + val localCode = actionConfig.file.readText().drop(1) + if (!localCode.equals(code)) { val element = actionConfig.buildAction(localCode) children.remove(it) children.add(element) @@ -93,23 +93,32 @@ class ActionSettingsRegistry { fun buildAction( code: String - ): AnAction { + ): AnAction = try { val newClassName = this.className + "_" + Integer.toHexString(code.hashCode()) - try { - return with( - actionCache.getOrPut("$packageName.$newClassName") { - (GroovyClassLoader(ActionSettingsRegistry::class.java.classLoader).parseClass( - code.replace( - ("""(? { + try { + val kotlinInterpreter = IdeaKotlinInterpreter(mapOf()) + val scriptEngine = kotlinInterpreter.scriptEngine + val eval = scriptEngine.eval(code) + return eval as Class<*> } catch (e: Throwable) { throw DynamicActionException(e, "Error in Action " + displayText, file, this) } @@ -182,8 +191,10 @@ class ActionSettingsRegistry { private fun load(actionPackage: String, actionName: String, language: String) = load("/sources/${language}/$actionPackage/$actionName.$language") - private fun load(path: String) = - EditorMenu::class.java.getResourceAsStream(path)?.readAllBytes()?.toString(Charsets.UTF_8) + private fun load(path: String): String? { + val bytes = EditorMenu::class.java.getResourceAsStream(path)?.readAllBytes() + return bytes?.toString(Charsets.UTF_8)?.drop(1) // XXX Why? '\uFEFF' is first byte + } fun load(clazz: Class, language: String) = load(clazz.`package`.name.replace('.', '/'), clazz.simpleName, language) diff --git a/src/main/kotlin/com/github/simiacryptus/aicoder/config/MRUItems.kt b/src/main/kotlin/com/github/simiacryptus/aicoder/config/MRUItems.kt index 6a78777d..eb7b134a 100644 --- a/src/main/kotlin/com/github/simiacryptus/aicoder/config/MRUItems.kt +++ b/src/main/kotlin/com/github/simiacryptus/aicoder/config/MRUItems.kt @@ -1,6 +1,6 @@ package com.github.simiacryptus.aicoder.config -import java.util.Map +import java.util.Map.Entry.comparingByValue import java.util.stream.Collectors class MRUItems { @@ -26,7 +26,7 @@ class MRUItems { if (mostUsedHistory.size > historyLimit) { val retain = mostUsedHistory.entries.stream() - .sorted(Map.Entry.comparingByValue().reversed()) + .sorted(comparingByValue().reversed()) .limit(historyLimit.toLong()) .map { (key, _) -> key }.collect( Collectors.toList() diff --git a/src/main/kotlin/com/github/simiacryptus/aicoder/util/IdeaKotlinInterpreter.kt b/src/main/kotlin/com/github/simiacryptus/aicoder/util/IdeaKotlinInterpreter.kt index 9716f408..069b651b 100644 --- a/src/main/kotlin/com/github/simiacryptus/aicoder/util/IdeaKotlinInterpreter.kt +++ b/src/main/kotlin/com/github/simiacryptus/aicoder/util/IdeaKotlinInterpreter.kt @@ -5,19 +5,49 @@ import com.intellij.openapi.application.runReadAction import com.intellij.openapi.project.Project import com.intellij.psi.PsiFileFactory import com.simiacryptus.skyenet.kotlin.KotlinInterpreter -import org.jetbrains.kotlin.jsr223.KotlinJsr223StandardScriptEngineFactory4Idea +import org.jetbrains.kotlin.cli.common.repl.KotlinJsr223JvmScriptEngineFactoryBase +import org.jetbrains.kotlin.cli.common.repl.ScriptArgsWithTypes +import org.jetbrains.kotlin.jsr223.KotlinJsr223JvmScriptEngine4Idea import org.jetbrains.kotlin.resolve.AnalyzingUtils import org.slf4j.LoggerFactory -import java.util.Map +import javax.script.ScriptEngine +import kotlin.script.experimental.jvm.util.KotlinJars.kotlinScriptStandardJars +import kotlin.script.experimental.jvm.util.scriptCompilationClasspathFromContextOrStdlib -class IdeaKotlinInterpreter(defs: Map) : KotlinInterpreter(defs) { +class IdeaKotlinInterpreter(defs: Map) : KotlinInterpreter(defs) { companion object { private val log = LoggerFactory.getLogger(IdeaKotlinInterpreter::class.java) var project: Project? = null } - override val scriptEngine: javax.script.ScriptEngine - get() = KotlinJsr223StandardScriptEngineFactory4Idea().scriptEngine + + override val scriptEngine: ScriptEngine + get() { + val factory = object : KotlinJsr223JvmScriptEngineFactoryBase() { + override fun getScriptEngine(): ScriptEngine = KotlinJsr223JvmScriptEngine4Idea( + factory = this, + templateClasspath = scriptCompilationClasspathFromContextOrStdlib( + keyNames = arrayOf(), + classLoader = KotlinInterpreter::class.java.classLoader!!, + wholeClasspath = true, + ) + kotlinScriptStandardJars, + templateClassName = "kotlin.script.templates.standard.SimpleScriptTemplate", + getScriptArgs = { context, kClasses -> + ScriptArgsWithTypes( + scriptArgs = arrayOf( +// context.getBindings(ScriptContext.ENGINE_SCOPE) + ), + scriptArgsTypes = arrayOf( +// Bindings::class + )) }, + scriptArgsTypes = arrayOf( + //Reflection.getOrCreateKotlinClass(MutableMap::class.java) + ) + ) + } + return factory.scriptEngine + } + override fun validate(code: String) = try { val messageCollector = MessageCollectorImpl(code) val psiFileFactory = PsiFileFactory.getInstance(project!!) diff --git a/src/main/kotlin/com/github/simiacryptus/aicoder/util/UITools.kt b/src/main/kotlin/com/github/simiacryptus/aicoder/util/UITools.kt index 84e82a2f..e4c615dd 100644 --- a/src/main/kotlin/com/github/simiacryptus/aicoder/util/UITools.kt +++ b/src/main/kotlin/com/github/simiacryptus/aicoder/util/UITools.kt @@ -33,7 +33,6 @@ import com.intellij.util.ui.FormBuilder import com.simiacryptus.jopenai.OpenAIClient import com.simiacryptus.jopenai.exceptions.ModerationException import com.simiacryptus.jopenai.util.StringUtil -import groovy.lang.GroovyRuntimeException import org.jdesktop.swingx.JXButton import org.slf4j.LoggerFactory import java.awt.* @@ -51,6 +50,7 @@ import java.util.concurrent.* import java.util.concurrent.atomic.AtomicBoolean import java.util.concurrent.atomic.AtomicReference import java.util.function.Supplier +import javax.script.ScriptException import javax.swing.* import javax.swing.text.JTextComponent import kotlin.math.max @@ -897,16 +897,16 @@ object UITools { modal = true ) log.info("showOptionDialog = $showOptionDialog") - } else if (e.matches { GroovyRuntimeException::class.java.isAssignableFrom(it.javaClass) }) { - val groovyRuntimeException = - e.get { GroovyRuntimeException::class.java.isAssignableFrom(it.javaClass) } as GroovyRuntimeException? + } else if (e.matches { ScriptException::class.java.isAssignableFrom(it.javaClass) }) { + val scriptException = + e.get { ScriptException::class.java.isAssignableFrom(it.javaClass) } as ScriptException? val dynamicActionException = e.get { ActionSettingsRegistry.DynamicActionException::class.java.isAssignableFrom(it.javaClass) } as ActionSettingsRegistry.DynamicActionException? val formBuilder = FormBuilder.createFormBuilder() formBuilder.addLabeledComponent( "Error", - JLabel("An error occurred while executing the groovy action.") + JLabel("An error occurred while executing the dynamic action.") ) val bugReportTextArea = JBTextArea() @@ -916,7 +916,7 @@ object UITools { bugReportTextArea.text = """ |Action Name: ${dynamicActionException?.actionSetting?.displayText} |Action ID: ${dynamicActionException?.actionSetting?.id} - |Groovy Error: ${groovyRuntimeException?.message} + |Script Error: ${scriptException?.message} | |Error Details: |``` diff --git a/src/main/kotlin/com/simiacryptus/skyenet/heart/WeakGroovyInterpreter.kt b/src/main/kotlin/com/simiacryptus/skyenet/heart/WeakGroovyInterpreter.kt deleted file mode 100644 index 86750e2f..00000000 --- a/src/main/kotlin/com/simiacryptus/skyenet/heart/WeakGroovyInterpreter.kt +++ /dev/null @@ -1,72 +0,0 @@ -@file:Suppress("PLATFORM_CLASS_MAPPED_TO_KOTLIN") - -package com.simiacryptus.skyenet.heart - -import com.simiacryptus.skyenet.core.Interpreter -import java.lang.reflect.Method - -@Suppress("unused") -open class WeakGroovyInterpreter(val defs: java.util.Map) : Interpreter { - override fun symbols() = defs as Map - - private val shell: Any - private val parseMethod: Method - private val runMethod: Method - private val setVariableMethod: Method - - init { - val groovyClassLoader = WeakGroovyInterpreter::class.java.classLoader - val compilerConfigurationClass = - groovyClassLoader.loadClass("org.codehaus.groovy.control.CompilerConfiguration") - val groovyShellClass = groovyClassLoader.loadClass("groovy.lang.GroovyShell") - val scriptClass = groovyClassLoader.loadClass("groovy.lang.Script") - - parseMethod = groovyShellClass.getMethod("parse", String::class.java) - runMethod = scriptClass.getMethod("run") - setVariableMethod = groovyShellClass.getMethod("setVariable", String::class.java, Any::class.java) - - val compilerConfiguration = compilerConfigurationClass.getDeclaredConstructor() - .newInstance() //as org.codehaus.groovy.control.CompilerConfiguration - compilerConfiguration.javaClass.getMethod("setParameters", Boolean::class.java) - .invoke(compilerConfiguration, true) - shell = groovyShellClass.getDeclaredConstructor(compilerConfigurationClass).newInstance(compilerConfiguration) - - defs.entrySet().forEach { (key, value) -> - setVariableMethod.invoke(shell, key, value) - } - } - - override fun getLanguage(): String { - return "groovy" - } - - override fun run(code: String): Any? { - val wrapExecution = wrapExecution { - try { - val script = parseMethod.invoke(shell, wrapCode(code)) - runMethod.invoke(script) - } catch (e: Exception) { - if (e.cause?.javaClass?.name == "org.codehaus.groovy.control.CompilationFailedException") { - throw e.cause as Exception - } else { - throw e - } - } - } - return wrapExecution - } - - override fun validate(code: String): Exception? { - return try { - parseMethod.invoke(shell, wrapCode(code)) - null - } catch (e: Exception) { - if (e.cause?.javaClass?.name == "org.codehaus.groovy.control.CompilationFailedException") { - e.cause as Exception - } else { - null - } - } - } -} - diff --git a/src/main/kotlin/com/simiacryptus/skyenet/heart/WeakKotlinInterpreter.kt b/src/main/kotlin/com/simiacryptus/skyenet/heart/WeakKotlinInterpreter.kt deleted file mode 100644 index 10249ae1..00000000 --- a/src/main/kotlin/com/simiacryptus/skyenet/heart/WeakKotlinInterpreter.kt +++ /dev/null @@ -1,47 +0,0 @@ -package com.simiacryptus.skyenet.heart - -import com.simiacryptus.skyenet.core.Interpreter -import java.lang.reflect.Method - -@Suppress("unused") -open class WeakKotlinInterpreter( - val defs: Map = mapOf(), -) : Interpreter { - override fun symbols() = defs as Map - - private val engine: Any - - init { - // Use WeakKotlinInterpreter classloader to load Kotlin classes - val groovyClassLoader = WeakKotlinInterpreter::class.java.classLoader - val factoryClass = - groovyClassLoader.loadClass("org.jetbrains.kotlin.script.jsr223.KotlinJsr223JvmLocalScriptEngineFactory") - val factory = factoryClass.getDeclaredConstructor().newInstance() - val getScriptEngineMethod: Method = factoryClass.getMethod("getScriptEngine") - engine = getScriptEngineMethod.invoke(factory) - defs.forEach { (key, value) -> - val putMethod: Method = engine.javaClass.getMethod("put", String::class.java, Any::class.java) - putMethod.invoke(engine, key, value) - } - } - - override fun getLanguage(): String { - return "Kotlin" - } - - override fun run(code: String): Any? { - val evalMethod: Method = engine.javaClass.getMethod("eval", String::class.java) - return wrapExecution { evalMethod.invoke(engine, wrapCode(code)) } - } - - override fun validate(code: String): Exception? { - return try { - val compileMethod: Method = - engine.javaClass.getMethod("compile", String::class.java, Class.forName("javax.script.ScriptContext")) - compileMethod.invoke(engine, wrapCode(code), engine.javaClass.getMethod("getContext").invoke(engine)) - null - } catch (e: Exception) { - e - } - } -} diff --git a/src/main/resources/sources/groovy/com/github/simiacryptus/aicoder/actions/code/CommentsAction.groovy b/src/main/resources/sources/groovy/com/github/simiacryptus/aicoder/actions/code/CommentsAction.groovy deleted file mode 100644 index 9f80cc79..00000000 --- a/src/main/resources/sources/groovy/com/github/simiacryptus/aicoder/actions/code/CommentsAction.groovy +++ /dev/null @@ -1,53 +0,0 @@ -package com.github.simiacryptus.aicoder.actions.code - -import com.github.simiacryptus.aicoder.actions.SelectionAction -import com.github.simiacryptus.aicoder.config.AppSettingsState -import com.github.simiacryptus.aicoder.util.ComputerLanguage -import com.intellij.openapi.project.Project -import com.simiacryptus.jopenai.proxy.ChatProxy -import org.jetbrains.annotations.Nullable - -class CommentsAction extends SelectionAction { - - @Override - String getConfig(@Nullable Project project) { - return "" - } - - boolean isLanguageSupported(ComputerLanguage computerLanguage) { - if (computerLanguage == null) return false - return computerLanguage != ComputerLanguage.Text - } - - String processSelection(SelectionState state, String config) { - return new ChatProxy( - clazz: CommentsAction_VirtualAPI.class, - api: api, - temperature: AppSettingsState.instance.temperature, - model: AppSettingsState.instance.defaultChatModel(), - deserializerRetries: 5, - ).create().editCode( - state.selectedText, - "Add comments to each line explaining the code", - state.language.toString(), - AppSettingsState.instance.humanLanguage - ).code ?: "" - } - - interface CommentsAction_VirtualAPI { - CommentsAction_ConvertedText editCode( - String code, - String operations, - String computerLanguage, - String humanLanguage - ) - - class CommentsAction_ConvertedText { - public String code - public String language - - def ConvertedText() {} - } - } - -} diff --git a/src/main/resources/sources/groovy/com/github/simiacryptus/aicoder/actions/code/CustomEditAction.groovy b/src/main/resources/sources/groovy/com/github/simiacryptus/aicoder/actions/code/CustomEditAction.groovy deleted file mode 100644 index 354ec208..00000000 --- a/src/main/resources/sources/groovy/com/github/simiacryptus/aicoder/actions/code/CustomEditAction.groovy +++ /dev/null @@ -1,86 +0,0 @@ -package com.github.simiacryptus.aicoder.actions.code - -import com.github.simiacryptus.aicoder.actions.SelectionAction -import com.github.simiacryptus.aicoder.config.AppSettingsState -import com.github.simiacryptus.aicoder.util.UITools -import com.intellij.openapi.project.Project -import com.simiacryptus.jopenai.proxy.ChatProxy -import org.jetbrains.annotations.Nullable - -import javax.swing.* - -class CustomEditAction extends SelectionAction { - - interface VirtualAPI { - EditedText editCode( - String code, - String operation, - String computerLanguage, - String humanLanguage - ) - - class EditedText { - public String code = null - public String language = null - - EditedText() {} - - EditedText(String code, String language) { - this.code = code - this.language = language - } - - } - } - - def getProxy() { - def chatProxy = new ChatProxy( - clazz: VirtualAPI.class, - api: api, - temperature: AppSettingsState.instance.temperature, - model: AppSettingsState.instance.defaultChatModel(), - ) - chatProxy.addExample( - new VirtualAPI.EditedText( - """ - // Print Hello, World! to the console - println("Hello, World!") - """.stripIndent(), - "java" - ) - ) { - it.editCode( - """println("Hello, World!")""", - "Add code comments", - "java", - "English" - ) - } - return chatProxy.create() - } - - @Override - String getConfig(@Nullable Project project) { - return UITools.showInputDialog(null, "Instruction:", "Edit Code", JOptionPane.QUESTION_MESSAGE - //, AppSettingsState.instance.getRecentCommands("customEdits").mostRecentHistory - ) - } - - - @Override - String processSelection(SelectionState state, String instruction) { - if (null == instruction) return (state.selectedText ?: "") - if (instruction.isBlank()) return state.selectedText ?: "" - def settings = AppSettingsState.instance - def outputHumanLanguage = AppSettingsState.instance.humanLanguage - settings.getRecentCommands("customEdits").addInstructionToHistory(instruction) - return proxy.editCode( - state.selectedText, - instruction.toString(), - state.language.name(), - outputHumanLanguage - ).code ?: state.selectedText ?: "" - } - - -} \ No newline at end of file diff --git a/src/main/resources/sources/groovy/com/github/simiacryptus/aicoder/actions/code/DescribeAction.groovy b/src/main/resources/sources/groovy/com/github/simiacryptus/aicoder/actions/code/DescribeAction.groovy deleted file mode 100644 index b07fa10b..00000000 --- a/src/main/resources/sources/groovy/com/github/simiacryptus/aicoder/actions/code/DescribeAction.groovy +++ /dev/null @@ -1,65 +0,0 @@ -package com.github.simiacryptus.aicoder.actions.code - -import com.github.simiacryptus.aicoder.actions.SelectionAction -import com.github.simiacryptus.aicoder.config.AppSettingsState -import com.github.simiacryptus.aicoder.util.IndentedText -import com.github.simiacryptus.aicoder.util.TextBlockFactory -import com.intellij.openapi.project.Project -import com.simiacryptus.jopenai.proxy.ChatProxy -import com.simiacryptus.jopenai.util.StringUtil -import org.jetbrains.annotations.Nullable - -class DescribeAction extends SelectionAction { - - interface DescribeAction_VirtualAPI { - DescribeAction_ConvertedText describeCode( - String code, - String computerLanguage, - String humanLanguage - ) - - class DescribeAction_ConvertedText { - public String text = null - public String language = null - - DescribeAction_ConvertedText() { - } - } - } - - def getProxy() { - return new ChatProxy( - clazz: DescribeAction_VirtualAPI.class, - api: api, - temperature: AppSettingsState.instance.temperature, - model: AppSettingsState.instance.defaultChatModel(), - deserializerRetries: 5 - ).create() - } - @Override - String getConfig(@Nullable Project project) { - return "" - } - - - @Override - String processSelection(SelectionState state, String config) { - def description = proxy.describeCode( - IndentedText.fromString(state.selectedText).textBlock.toString().trim(), - state.language?.name() ?: "", - AppSettingsState.instance.humanLanguage, - ).text ?: "" - def wrapping = StringUtil.lineWrapping(description.trim(), 120) - def numberOfLines = wrapping.trim().split("\n").reverse().dropWhile { it.isEmpty() }.size() - TextBlockFactory commentStyle = null - if (numberOfLines == 1) { - state.language?.lineComment - } else { - state.language?.blockComment - } - return """ - ${state.indent}${commentStyle?.fromString(wrapping)?.withIndent(state.indent) ?: wrapping} - ${state.indent}${state.selectedText} - """ - } -} \ No newline at end of file diff --git a/src/main/resources/sources/groovy/com/github/simiacryptus/aicoder/actions/code/DocAction.groovy b/src/main/resources/sources/groovy/com/github/simiacryptus/aicoder/actions/code/DocAction.groovy deleted file mode 100644 index cfd7dbc3..00000000 --- a/src/main/resources/sources/groovy/com/github/simiacryptus/aicoder/actions/code/DocAction.groovy +++ /dev/null @@ -1,99 +0,0 @@ -package com.github.simiacryptus.aicoder.actions.code - -import com.github.simiacryptus.aicoder.actions.SelectionAction -import com.github.simiacryptus.aicoder.config.AppSettingsState -import com.github.simiacryptus.aicoder.util.ComputerLanguage -import com.github.simiacryptus.aicoder.util.IndentedText -import com.github.simiacryptus.aicoder.util.psi.PsiUtil -import com.intellij.openapi.project.Project -import com.simiacryptus.jopenai.proxy.ChatProxy -import kotlin.Pair -import org.jetbrains.annotations.Nullable - -class DocAction extends SelectionAction { - - interface DocAction_VirtualAPI { - DocAction_ConvertedText processCode( - String code, - String operation, - String computerLanguage, - String humanLanguage - ) - - class DocAction_ConvertedText { - public String text - public String language - - def ConvertedText() {} - } - } - - DocAction_VirtualAPI getProxy() { - ChatProxy chatProxy = new ChatProxy( - clazz: DocAction_VirtualAPI, - api: api, - model: AppSettingsState.instance.defaultChatModel(), - temperature: AppSettingsState.instance.temperature, - deserializerRetries: 5 - ) - chatProxy.addExample( - new DocAction_VirtualAPI.DocAction_ConvertedText( - text: ''' - /** - * Prints "Hello, world!" to the console - */ - '''.trim(), - language: "English" - ) - ) { - (DocAction_VirtualAPI x) -> - x.processCode( - ''' - fun hello() { - println("Hello, world!") - } - '''.trim(), - "Write detailed KDoc prefix for code block", - "Kotlin", - "English" - ) - } - return chatProxy.create() as DocAction_VirtualAPI - } - - @Override - String getConfig(@Nullable Project project) { - return "" - } - - - @Override - String processSelection(SelectionState state, String config) { - CharSequence code = state.selectedText - IndentedText indentedInput = IndentedText.fromString(code.toString()) - String docString = proxy.processCode( - indentedInput.textBlock.toString(), - "Write detailed " + (state.language?.docStyle ?: "documentation") + " prefix for code block", - state.language.name(), - AppSettingsState.instance.humanLanguage - ).text ?: "" - return docString + code - } - - @Override - boolean isLanguageSupported(ComputerLanguage computerLanguage) { - if (computerLanguage == ComputerLanguage.Text) return false - if (computerLanguage?.docStyle == null) return false - if (computerLanguage?.docStyle?.isBlank()) return false - return true - } - - @Override - Pair editSelection(EditorState state, int start, int end) { - if (null == state.psiFile) return super.editSelection(state, start, end) - def codeBlock = PsiUtil.getCodeElement(state.psiFile, start, end) - if (null == codeBlock) return super.editSelection(state, start, end) - def textRange = codeBlock.textRange - return new Pair<>(textRange.startOffset, textRange.endOffset) - } -} diff --git a/src/main/resources/sources/groovy/com/github/simiacryptus/aicoder/actions/code/GenerateProjectAction.groovy b/src/main/resources/sources/groovy/com/github/simiacryptus/aicoder/actions/code/GenerateProjectAction.groovy deleted file mode 100644 index 49c7835f..00000000 --- a/src/main/resources/sources/groovy/com/github/simiacryptus/aicoder/actions/code/GenerateProjectAction.groovy +++ /dev/null @@ -1,545 +0,0 @@ -package com.github.simiacryptus.aicoder.actions.code - -import com.github.simiacryptus.aicoder.actions.FileContextAction -import com.github.simiacryptus.aicoder.config.AppSettingsState -import com.github.simiacryptus.aicoder.config.Name -import com.github.simiacryptus.aicoder.util.UITools -import com.intellij.openapi.project.Project -import com.simiacryptus.openai.proxy.ChatProxy -import com.simiacryptus.openai.proxy.ValidatedObject -import kotlin.Pair -import org.slf4j.Logger -import org.slf4j.LoggerFactory - -import javax.swing.* -import java.util.concurrent.Callable -import java.util.concurrent.ConcurrentHashMap -import java.util.concurrent.ExecutorService -import java.util.concurrent.Executors -import java.util.concurrent.Future -import java.util.concurrent.atomic.AtomicInteger -import java.util.zip.ZipEntry -import java.util.zip.ZipOutputStream - -class GenerateProjectAction extends FileContextAction { - - GenerateProjectAction() { - super(false, true) - } - - static String trimStart(String s1, List prefixChars) { - String s = s1 - while (s.length() > 0 && prefixChars.contains(s.charAt(0))) { - s = s.substring(1) - } - return s - } - - static String trimEnd(String string, String suffix) { - if (string.endsWith(suffix)) { - return string.substring(0, string.length() - suffix.length()) - } else { - return string - } - } - - interface SoftwareProjectAI { - - Project newProject(String description) - - class Project implements ValidatedObject { - public String name = '' - public String description = '' - public String language = '' - public List features = [] - public List libraries = [] - public List buildTools = [] - - Project() {} - boolean validate() { - return true - } - } - - ProjectStatements getProjectStatements(String description, Project project) - - class ProjectStatements implements ValidatedObject { - public List assumptions = [] - public List designPatterns = [] - public List requirements = [] - public List risks = [] - - ProjectStatements() {} - - boolean validate() { - return true - } - } - - ProjectDesign buildProjectDesign(Project project, ProjectStatements requirements) - - class ProjectDesign implements ValidatedObject { - public List components = [] - public List documents = [] - public List tests = [] - - ProjectDesign() {} - - boolean validate() { - return true - } - } - - class ComponentDetails implements ValidatedObject { - public String name = '' - public String description = '' - public List features = [] - - ComponentDetails() {} - boolean validate() { - return true - } - } - - class TestDetails implements ValidatedObject { - public String name = '' - public List steps = [] - public List expectations = [] - - TestDetails() {} - boolean validate() { - return true - } - } - - class DocumentationDetails implements ValidatedObject { - public String name = '' - public String description = '' - public List sections = [] - - DocumentationDetails() {} - - boolean validate() { - return true - } - } - - CodeSpecificationList buildProjectFileSpecifications(Project project, ProjectStatements requirements, ProjectDesign design, boolean recursive) - - class CodeSpecificationList implements ValidatedObject { - public List files = [] - - CodeSpecificationList() {} - boolean validate() { - return true - } - } - - CodeSpecificationList buildComponentFileSpecifications(Project project, ProjectStatements requirements, ComponentDetails design) - - TestSpecificationList buildTestFileSpecifications(Project project, ProjectStatements requirements, TestDetails design, boolean recursive) - - class TestSpecificationList implements ValidatedObject { - public List files = [] - - TestSpecificationList() {} - boolean validate() { - return true - } - } - - DocumentSpecificationList buildDocumentationFileSpecifications(Project project, ProjectStatements requirements, DocumentationDetails design, boolean recursive) - - class DocumentSpecificationList implements ValidatedObject { - public List files = [] - - DocumentSpecificationList() {} - boolean validate() { - return true - } - } - - class CodeSpecification implements ValidatedObject { - public String description = '' - public List requires = [] - public List publicProperties = [] - public List publicMethodSignatures = [] - public String language = '' - public FilePath location = new FilePath() - - CodeSpecification() {} - boolean validate() { - return true - } - } - - class DocumentSpecification implements ValidatedObject { - public String description = '' - public List requires = [] - public List sections = [] - public String language = '' - public FilePath location = new FilePath() - - DocumentSpecification() {} - boolean validate() { - return true - } - } - - class TestSpecification implements ValidatedObject { - public String description = '' - public List requires = [] - public List steps = [] - public List expectations = [] - public String language = '' - public FilePath location = new FilePath() - - TestSpecification() {} - boolean validate() { - return true - } - } - - class FilePath implements ValidatedObject { - public String file = '' - - FilePath() {} - - boolean validate() { - return file?.isBlank() == false - } - } - - SourceCode implementComponentSpecification(Project project, ComponentDetails component, List imports, CodeSpecification specification) - - SourceCode implementTestSpecification(Project project, TestSpecification specification, TestDetails test, List imports, TestSpecification specificationAgain) - - SourceCode implementDocumentationSpecification(Project project, DocumentSpecification specification, DocumentationDetails documentation, List imports, DocumentSpecification specificationAgain) - - class SourceCode implements ValidatedObject { - public String language = '' - public String code = '' - - SourceCode() {} - boolean validate() { - return true - } - } - } - - - Map parallelImplement( - SoftwareProjectAI api, - Project project, - Map> components = [:], - Map> documents = [:], - Map> tests = [:], - int drafts, - int threads - ) { - Map> alternates = parallelImplementWithAlternates( - api, - project, - components, - documents, - tests, - drafts, - threads, - { -> return 0.0 } - ) - alternates.collectEntries { [(it.key): it.value.max { it.code?.length() ?: 0 }] } - } - - void write( - File zipArchiveFile, - Map implementations - ) { - new ZipOutputStream(zipArchiveFile.newOutputStream()).with { zip -> - implementations.each { file, sourceCodes -> - zip.putNextEntry(new ZipEntry(file.toString())) - zip.write(sourceCodes.code.bytes) - zip.closeEntry() - } - } - } - - private static final ExecutorService threadPool = Executors.newCachedThreadPool() - - Map> parallelImplementWithAlternates( - SoftwareProjectAI projectAI, - Project project, - Map> components, - Map> documents, - Map> tests, - int drafts, - int threads, - Closure progress - ) { - return (components.collectMany { entry -> buildComponentDetails(project, entry.key, entry.value) } + - documents.collectMany { entry -> buildDocumentDetails(project, entry.key, entry.value) } + - tests.collectMany { entry -> buildTestDetails(project, entry.key, entry.value) }).collect { - try { - Optional.ofNullable(it.get()) - } catch (Throwable ignored) { - Optional.> empty() - } - }.findAll { !it.empty }.collect { it.get() } - .groupBy { it.first }.collectEntries { Map.Entry>> entry -> - [(entry.key): entry.value.sort { Pair p -> p.second.code.length() }] - } - } - - AtomicInteger currentDraft = new AtomicInteger(0) - ConcurrentHashMap>>> fileImplCache = new ConcurrentHashMap>>>() - - static def normalizeFileName(String it) { - trimStart(it, ['/', '.']) - } - - def buildComponentDetails(SoftwareProjectAI.Project project, SoftwareProjectAI.ComponentDetails component, List files, int drafts) { - files.collectMany { SoftwareProjectAI.CodeSpecification file -> - //buildCodeSpec(component, files, file) - if (file.location == null) { - return new ArrayList>>() - } - - def key = normalizeFileName(file.location.file) - if(!fileImplCache.containsKey(key)) { - def value = (0.. - threadPool.submit(new Callable>() { - @Override - Pair call() throws Exception { - SoftwareProjectAI.SourceCode implement = projectAI.implementComponentSpecification( - project, - component, - files.findAll { file.requires?.contains(it.location) ?: false }, - new SoftwareProjectAI.CodeSpecification( - description: file.description, - requires: [], - publicProperties: file.publicProperties, - publicMethodSignatures: file.publicMethodSignatures, - language: file.language, - location: file.location - ) - ) -// def progressVal = currentDraft.incrementAndGet().toDouble() / totalDrafts -// progress(progressVal) -// log.info("Progress: $progressVal") - return new Pair(file.location, implement) - } - }) - } - fileImplCache.put(key, value) - } - return fileImplCache.get(key) - - } - } - - def buildDocumentDetails(SoftwareProjectAI.Project project, SoftwareProjectAI.DocumentationDetails documentation, List files, int drafts) { - files.collectMany { SoftwareProjectAI.DocumentSpecification file -> - //buildDocumentSpec(documentation, files, file) - if (file.location == null) { - return new ArrayList>>() - } - def key = normalizeFileName(file.location.file) - if(!fileImplCache.containsKey(key)) { - def value = (0.. - threadPool.submit(new Callable>() { - @Override - Pair call() throws Exception { - def implement = projectAI.implementDocumentationSpecification( - project, - new SoftwareProjectAI.DocumentSpecification( - description: file.description, - requires: [], - sections: file.sections, - language: file.language, - location: file.location - ), - documentation, - files.findAll { file.requires?.contains(it.location) ?: false }, - new SoftwareProjectAI.DocumentSpecification( - description: file.description, - requires: [], - sections: file.sections, - language: file.language, - location: file.location - ) - ) -// def progressVal = currentDraft.incrementAndGet().toDouble() / totalDrafts -// progress(progressVal) -// log.info("Progress: $progressVal") - return new Pair(file.location, implement) - } - }) - } - fileImplCache.put(key, value) - } - return fileImplCache.get(key) - } - } - - def buildTestDetails(SoftwareProjectAI.Project project, SoftwareProjectAI.TestDetails test, List files, int drafts) { - files.collectMany { SoftwareProjectAI.TestSpecification file -> - //buildTestSpec(test, files, file) - if (file.location == null) { - return new ArrayList>>() - } - - def key = normalizeFileName(file.location.file) - if(!fileImplCache.containsKey(key)) { - def value = (0.. - threadPool.submit(new Callable>() { - @Override - Pair call() throws Exception { - def implement = projectAI.implementTestSpecification( - project, - new SoftwareProjectAI.TestSpecification( - description: file.description, - requires: [], - steps: file.steps, - expectations: file.expectations, - language: file.language, - location: file.location - ), - test, - files.findAll { file.requires?.contains(it.location) ?: false }, - new SoftwareProjectAI.TestSpecification( - description: file.description, - requires: [], - steps: file.steps, - expectations: file.expectations, - language: file.language, - location: file.location - ) - ) -// def progressVal = currentDraft.incrementAndGet().toDouble() / totalDrafts -// progress(progressVal) -// log.info("Progress: $progressVal") - return new Pair(file.location, implement) - } - }) - } - fileImplCache.put(key, value) - } - return fileImplCache.get(key) - } - } - - @SuppressWarnings("UNUSED") - static class SettingsUI { - @Name("Project Description") - public JTextArea description = new JTextArea() - - @Name("Drafts Per File") - public JTextField drafts = new JTextField("2") - public JCheckBox saveAlternates = new JCheckBox("Save Alternates") - - SettingsUI() { - description.setLineWrap(true) - description.setWrapStyleWord(true) - } - } - - static class Settings { - public String description = "" - public int drafts = 2 - public boolean saveAlternates = false - - Settings() {} - } - - @Override - Settings getConfig(Project project) { - return UITools.showDialog(project, SettingsUI.class, Settings.class, "Project Settings", { config -> }) - } - - SoftwareProjectAI projectAI = null - - @Override - File[] processSelection(SelectionState state, Settings config) { - projectAI = new ChatProxy( - clazz: SoftwareProjectAI.class, - api: api, - model: AppSettingsState.instance.defaultChatModel(), - temperature: AppSettingsState.instance.temperature, - deserializerRetries: 2, - ).create() - if (config == null) return new File[0] - - - SoftwareProjectAI.Project project = projectAI.newProject(config.description.trim()) - def projectStatements = projectAI.getProjectStatements(config.description, project) - def buildProjectDesign = projectAI.buildProjectDesign(project, projectStatements) - - def components = buildProjectDesign.components., SoftwareProjectAI.ComponentDetails> collectEntries { - [(it): projectAI.buildComponentFileSpecifications( - project, - projectStatements, - it - ).files] - } - - def documents = buildProjectDesign.documents., SoftwareProjectAI.DocumentationDetails> collectEntries { - [(it): projectAI.buildDocumentationFileSpecifications( - project, - projectStatements, - it, - false - ).files] - } - - def tests = buildProjectDesign.tests., SoftwareProjectAI.TestDetails> collectEntries { - [(it): projectAI.buildTestFileSpecifications( - project, - projectStatements, - it, - false - ).files] - } - def outputDir = new File(state.selectedFile.canonicalPath) - - def entries = ( - components.collectMany { entry -> buildComponentDetails(project, entry.key, entry.value, config.drafts) } - + documents.collectMany { entry -> buildDocumentDetails(project, entry.key, entry.value, config.drafts) } - + tests.collectMany { entry -> buildTestDetails(project, entry.key, entry.value, config.drafts) }) - .collect { - try { - Optional.ofNullable(it.get()) - } catch (Throwable ignored) { - Optional.> empty() - } - }.findAll { !it.empty }.collect { it.get() } - .groupBy { it.first }., SoftwareProjectAI.FilePath, List>> collectEntries { - entry -> [(entry.key): entry.value.collect { it.second}.sort { a, b -> a.code.length() <=> b.code.length()}] - } - - - def generatedFiles = [] - entries.each { file, sourceCode -> - def relative = trimStart(trimEnd(file.file, '/'), ['/', '.']) ?: "" - if (new File(relative).isAbsolute()) { - log.warn("Invalid path: $relative") - } else { - def outFile = new File(outputDir, relative) - outFile.parentFile.mkdirs() - def best = sourceCode.max { it.code.length() } - outFile.text = best.code - log.debug("Wrote ${outFile.canonicalPath} (Resolved from $relative)") - generatedFiles << outFile - if (config.saveAlternates) - sourceCode.findAll { it != best }.eachWithIndex { alternate, index -> - def outFileAlternate = new File(outputDir, relative + ".${index + 1}") - outFileAlternate.parentFile.mkdirs() - outFileAlternate.text = alternate.code - log.debug("Wrote ${outFileAlternate.canonicalPath} (Resolved from $relative)") - generatedFiles << outFileAlternate - } - } - } - return generatedFiles as File[] - } - - -} \ No newline at end of file diff --git a/src/main/resources/sources/groovy/com/github/simiacryptus/aicoder/actions/code/ImplementStubAction.groovy b/src/main/resources/sources/groovy/com/github/simiacryptus/aicoder/actions/code/ImplementStubAction.groovy deleted file mode 100644 index 829355df..00000000 --- a/src/main/resources/sources/groovy/com/github/simiacryptus/aicoder/actions/code/ImplementStubAction.groovy +++ /dev/null @@ -1,84 +0,0 @@ -package com.github.simiacryptus.aicoder.actions.code - -import com.github.simiacryptus.aicoder.actions.SelectionAction -import com.github.simiacryptus.aicoder.config.AppSettingsState -import com.github.simiacryptus.aicoder.util.ComputerLanguage -import com.github.simiacryptus.aicoder.util.psi.PsiUtil -import com.intellij.openapi.project.Project -import com.simiacryptus.jopenai.proxy.ChatProxy -import com.simiacryptus.jopenai.util.StringUtil -import kotlin.Pair -import org.jetbrains.annotations.Nullable - -class ImplementStubAction extends SelectionAction { - - static interface VirtualAPI { - ConvertedText editCode( - String code, - String operation, - String computerLanguage, - String humanLanguage - ) - - static class ConvertedText { - public String code - public String language - - ConvertedText() {} - } - } - - def getProxy() { - return new ChatProxy( - clazz: VirtualAPI.class, - api: api, - model: AppSettingsState.instance.defaultChatModel(), - temperature: AppSettingsState.instance.temperature, - deserializerRetries: 5, - ).create() - } - - boolean isLanguageSupported(ComputerLanguage computerLanguage) { - if (computerLanguage == null) return false - return computerLanguage != ComputerLanguage.Text - } - - Pair defaultSelection(EditorState editorState, int offset) { - def codeRanges = editorState.contextRanges.findAll { PsiUtil.matchesType(it.name, PsiUtil.ELEMENTS_CODE) } - if (codeRanges.isEmpty()) return editorState.line - return codeRanges.min { it.length() }.range() - } - @Override - String getConfig(@Nullable Project project) { - return "" - } - - - String processSelection(SelectionState state, String config) { - def code = state.selectedText ?: "" - def settings = AppSettingsState.instance - def outputHumanLanguage = settings.humanLanguage - def computerLanguage = state.language - - def codeContext = state.contextRanges.findAll { - PsiUtil.matchesType( - it.name, - PsiUtil.ELEMENTS_CODE - ) - } - def smallestIntersectingMethod = "" - if(!codeContext.isEmpty()) smallestIntersectingMethod = codeContext.min { it.length() }.subString(state.entireDocument) - - def declaration = code - declaration = StringUtil.stripSuffix(declaration.toString().trim(), smallestIntersectingMethod) - declaration = declaration.toString().trim() - - return proxy.editCode( - declaration, - "Implement Stub", - computerLanguage.name().toLowerCase(Locale.ROOT), - outputHumanLanguage - ).code ?: "" - } - -} \ No newline at end of file diff --git a/src/main/resources/sources/groovy/com/github/simiacryptus/aicoder/actions/code/InsertImplementationAction.groovy b/src/main/resources/sources/groovy/com/github/simiacryptus/aicoder/actions/code/InsertImplementationAction.groovy deleted file mode 100644 index 77175013..00000000 --- a/src/main/resources/sources/groovy/com/github/simiacryptus/aicoder/actions/code/InsertImplementationAction.groovy +++ /dev/null @@ -1,148 +0,0 @@ -package com.github.simiacryptus.aicoder.actions.code - -import com.github.simiacryptus.aicoder.actions.SelectionAction -import com.github.simiacryptus.aicoder.config.AppSettingsState -import com.github.simiacryptus.aicoder.util.ComputerLanguage -import com.github.simiacryptus.aicoder.util.UITools -import com.github.simiacryptus.aicoder.util.psi.PsiClassContext -import com.github.simiacryptus.aicoder.util.psi.PsiUtil -import com.intellij.openapi.project.Project -import com.simiacryptus.jopenai.proxy.ChatProxy -import kotlin.Pair -import org.jetbrains.annotations.NotNull -import org.jetbrains.annotations.Nullable - -import static com.intellij.openapi.application.ActionsKt.runReadAction - -class InsertImplementationAction extends SelectionAction { - - interface VirtualAPI { - ConvertedText implementCode( - String specification, - String prefix, - String computerLanguage, - String humanLanguage - ) - - class ConvertedText { - public String code - public String language - - ConvertedText() {} - } - } - - def getProxy() { - return new ChatProxy( - clazz: VirtualAPI.class, - api: api, - model: AppSettingsState.instance.defaultChatModel(), - temperature: AppSettingsState.instance.temperature, - deserializerRetries: 5, - ).create() - } - - @Override - String getConfig(@Nullable Project project) { - return "" - } - - @Override - Pair defaultSelection(@NotNull EditorState editorState, int offset) { - def foundItem = editorState.contextRanges.findAll { - PsiUtil.matchesType( - it.name, - PsiUtil.ELEMENTS_COMMENTS - ) - }.min({ it.length() }) - return foundItem?.range() ?: editorState.line - } - - @Override - Pair editSelection(@NotNull EditorState state, int start, int end) { - def foundItem = state.contextRanges.findAll { - PsiUtil.matchesType( - it.name, - PsiUtil.ELEMENTS_COMMENTS - ) - }.min({ it.length() }) - return foundItem?.range() ?: new Pair<>(start, end) - } - - @Override - String processSelection(SelectionState state, String config) { - def humanLanguage = AppSettingsState.instance.humanLanguage - def computerLanguage = state.language - def psiClassContextActionParams = getPsiClassContextActionParams(state) - def selectedText = state.selectedText ?: "" - - def comment = psiClassContextActionParams.largestIntersectingComment - def instruct = (null == comment) ? selectedText : comment.subString(state.entireDocument ?: "").trim() - if (selectedText.split(" ").reverse().dropWhile { it.isEmpty() }.reverse().length > 4) { - instruct = selectedText.trim() - } - def specification = Objects.requireNonNull(computerLanguage.getCommentModel(instruct)) - .fromString(instruct).stream() - .map { obj -> obj.toString() } - .map { obj -> obj.trim() } - .filter { x -> !x.isEmpty() } - .reduce { a, b -> "$a $b" }.get() - if(null != state.psiFile) { - def code = UITools.run(state.project, "Insert Implementation", true, true, { - def psiClassContext = runReadAction { - PsiClassContext.getContext( - state.psiFile, - psiClassContextActionParams.selectionStart, - psiClassContextActionParams.selectionEnd, - computerLanguage - ).toString() - } - proxy.implementCode( - specification, - psiClassContext, - computerLanguage.name(), - humanLanguage, - ).code - }) - if(null != code) return selectedText + "\n${state.indent}" + code - } else { - def code = proxy.implementCode( - specification, - "", - computerLanguage.name(), - humanLanguage, - ).code - if(null != code) return selectedText + "\n${state.indent}" + code - } - return selectedText - } - - static class PsiClassContextActionParams { - int selectionStart - int selectionEnd - SelectionAction.ContextRange largestIntersectingComment - - PsiClassContextActionParams(int selectionStart, int selectionEnd, SelectionAction.ContextRange largestIntersectingComment) { - this.selectionStart = selectionStart - this.selectionEnd = selectionEnd - this.largestIntersectingComment = largestIntersectingComment - } - } - - static PsiClassContextActionParams getPsiClassContextActionParams(SelectionState state) { - int selectionStart = state.selectionOffset - return new PsiClassContextActionParams( - selectionStart, - selectionStart + (state.selectionLength ?: 0), - state.contextRanges.find { PsiUtil.matchesType(it.name, PsiUtil.ELEMENTS_COMMENTS) } - ) - } - - @Override - boolean isLanguageSupported(@Nullable ComputerLanguage computerLanguage) { - if (computerLanguage == null) return false - if (computerLanguage == ComputerLanguage.Text) return false - if (computerLanguage == ComputerLanguage.Markdown) return false - return super.isLanguageSupported(computerLanguage) - } -} \ No newline at end of file diff --git a/src/main/resources/sources/groovy/com/github/simiacryptus/aicoder/actions/code/PasteAction.groovy b/src/main/resources/sources/groovy/com/github/simiacryptus/aicoder/actions/code/PasteAction.groovy deleted file mode 100644 index 564b5f54..00000000 --- a/src/main/resources/sources/groovy/com/github/simiacryptus/aicoder/actions/code/PasteAction.groovy +++ /dev/null @@ -1,67 +0,0 @@ -package com.github.simiacryptus.aicoder.actions.code - -import com.github.simiacryptus.aicoder.actions.SelectionAction -import com.github.simiacryptus.aicoder.config.AppSettingsState -import com.github.simiacryptus.aicoder.util.ComputerLanguage -import com.intellij.openapi.actionSystem.AnActionEvent -import com.intellij.openapi.project.Project -import com.simiacryptus.jopenai.proxy.ChatProxy -import org.jetbrains.annotations.Nullable - -import java.awt.* -import java.awt.datatransfer.DataFlavor - -class PasteAction extends SelectionAction { - PasteAction() { - super(false) - } - - interface VirtualAPI { - ConvertedText convert(String text, String from_language, String to_language) - - class ConvertedText { - public String code - public String language - - ConvertedText() {} - } - } - @Override - String getConfig(@Nullable Project project) { - return "" - } - - - @Override - String processSelection(SelectionState state, String config) { - return new ChatProxy( - clazz: VirtualAPI.class, - api: api, - model: AppSettingsState.instance.defaultChatModel(), - temperature: AppSettingsState.instance.temperature, - deserializerRetries: 5, - ).create().convert( - getClipboard().toString().trim(), - "autodetect", - state.language.name() - ).code ?: "" - } - - @Override - boolean isLanguageSupported(ComputerLanguage computerLanguage) { - if (computerLanguage == null) return false - return computerLanguage != ComputerLanguage.Text - } - - @Override - boolean isEnabled(AnActionEvent event) { - if (getClipboard() == null) return false - return super.isEnabled(event) - } - - private Object getClipboard() { - def contents = Toolkit.getDefaultToolkit().systemClipboard.getContents(null) - if (contents?.isDataFlavorSupported(DataFlavor.stringFlavor) == true) contents?.getTransferData(DataFlavor.stringFlavor) - else null - } -} \ No newline at end of file diff --git a/src/main/resources/sources/groovy/com/github/simiacryptus/aicoder/actions/code/QuestionAction.groovy b/src/main/resources/sources/groovy/com/github/simiacryptus/aicoder/actions/code/QuestionAction.groovy deleted file mode 100644 index aa5ec213..00000000 --- a/src/main/resources/sources/groovy/com/github/simiacryptus/aicoder/actions/code/QuestionAction.groovy +++ /dev/null @@ -1,67 +0,0 @@ -package com.github.simiacryptus.aicoder.actions.code - -import com.github.simiacryptus.aicoder.actions.SelectionAction -import com.github.simiacryptus.aicoder.config.AppSettingsState -import com.github.simiacryptus.aicoder.util.ComputerLanguage -import com.intellij.openapi.project.Project -import com.simiacryptus.openai.proxy.ChatProxy -import org.jetbrains.annotations.Nullable - -import javax.swing.JOptionPane - -class QuestionAction extends SelectionAction { - - interface VirtualAPI { - Answer questionCode( - String code, - String question - ) - - class Answer { - public String text = null - - Answer() {} - } - } - - def getProxy() { - return new ChatProxy( - clazz: VirtualAPI.class, - api: api, - model: AppSettingsState.instance.defaultChatModel(), - temperature: AppSettingsState.instance.temperature, - deserializerRetries: 5, - ).create() - } - - @Override - String getConfig(@Nullable Project project) { - return JOptionPane.showInputDialog(null, "Question:", "Question", JOptionPane.QUESTION_MESSAGE) - } - - - @Override - String processSelection(SelectionState state, String question) { - - if (question.isBlank()) return "" - - def newText = proxy.questionCode( - state.selectedText ?: "", - question, - ).text ?: "" - - def answer = """ - |Question: $question - |Answer: ${newText.trim()} - |""".stripMargin().trim() - - def blockComment = state.language?.blockComment - def fromString = blockComment?.fromString(answer) - return "${state.indent}${fromString?.withIndent(state.indent) ?: ""}\n${state.indent}" + state.selectedText - } - - @Override - boolean isLanguageSupported(ComputerLanguage computerLanguage) { - return computerLanguage != ComputerLanguage.Text - } -} \ No newline at end of file diff --git a/src/main/resources/sources/groovy/com/github/simiacryptus/aicoder/actions/code/RecentCodeEditsAction.groovy b/src/main/resources/sources/groovy/com/github/simiacryptus/aicoder/actions/code/RecentCodeEditsAction.groovy deleted file mode 100644 index 65522faa..00000000 --- a/src/main/resources/sources/groovy/com/github/simiacryptus/aicoder/actions/code/RecentCodeEditsAction.groovy +++ /dev/null @@ -1,42 +0,0 @@ -package com.github.simiacryptus.aicoder.actions.code - -import com.github.simiacryptus.aicoder.config.AppSettingsState -import com.github.simiacryptus.aicoder.util.ComputerLanguage -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.project.Project -import org.jetbrains.annotations.Nullable - -class RecentCodeEditsAction extends ActionGroup { - void update(AnActionEvent e) { - e.presentation.setEnabledAndVisible(isEnabled(e)) - super.update(e) - } - - AnAction[] getChildren(AnActionEvent e) { - if (null == e) return [] - def children = [] - for (instruction in AppSettingsState.instance.getRecentCommands("customEdits").mostUsedHistory.keySet()) { - def id = children.size() + 1 - def text = id < 10 ? "_${id}: ${instruction}" : "${id}: ${instruction}" - def element = new CustomEditAction() { - @Override String getConfig(@Nullable Project project) { - return instruction - } - } - element.templatePresentation.text = text - element.templatePresentation.description = instruction - element.templatePresentation.icon = null - children.add(element) - } - return children as AnAction[] - } - - static boolean isEnabled(AnActionEvent e) { - if (!UITools.hasSelection(e)) return false - def computerLanguage = ComputerLanguage.getComputerLanguage(e) - return computerLanguage != ComputerLanguage.Text - } -} \ No newline at end of file diff --git a/src/main/resources/sources/groovy/com/github/simiacryptus/aicoder/actions/code/RenameVariablesAction.groovy b/src/main/resources/sources/groovy/com/github/simiacryptus/aicoder/actions/code/RenameVariablesAction.groovy deleted file mode 100644 index 94741f60..00000000 --- a/src/main/resources/sources/groovy/com/github/simiacryptus/aicoder/actions/code/RenameVariablesAction.groovy +++ /dev/null @@ -1,89 +0,0 @@ -package com.github.simiacryptus.aicoder.actions.code - -import com.github.simiacryptus.aicoder.actions.SelectionAction -import com.github.simiacryptus.aicoder.config.AppSettingsState -import com.github.simiacryptus.aicoder.util.ComputerLanguage -import com.github.simiacryptus.aicoder.util.UITools -import com.intellij.openapi.actionSystem.AnActionEvent -import com.intellij.openapi.project.Project -import com.simiacryptus.jopenai.proxy.ChatProxy -import org.jetbrains.annotations.Nullable - -class RenameVariablesAction extends SelectionAction { - - interface RenameAPI { - SuggestionResponse suggestRenames( - String code, - String computerLanguage, - String humanLanguage - ) - - class SuggestionResponse { - public List suggestions = [] - - SuggestionResponse() {} - } - - class Suggestion { - public String originalName = null - public String suggestedName = null - - Suggestion() {} - } - } - - def getProxy() { - return new ChatProxy( - clazz: RenameAPI.class, - api: api, - model: AppSettingsState.instance.defaultChatModel(), - temperature: AppSettingsState.instance.temperature, - deserializerRetries: 5, - ).create() - } - - @Override - String getConfig(@Nullable Project project) { - return "" - } - - - @Override - String processSelection(AnActionEvent event, SelectionState state, String config) { - def renameSuggestions = UITools.run(event == null ? null : event.project, templateText, true, true, { - return proxy - .suggestRenames( - state.selectedText, - state.language?.name(), - AppSettingsState.instance.humanLanguage - ) - .suggestions - .findAll { it.originalName != null && it.suggestedName != null } - .collectEntries { [(it.originalName): it.suggestedName] } - }) - def selectedSuggestions = choose(renameSuggestions) - return UITools.run(event == null ? null : event.project, templateText, true, true, { - def selectedText = state.selectedText - def filter = renameSuggestions.findAll { x -> selectedSuggestions.contains(x.key) } - def txt = selectedText - for (entry in filter) { - txt = txt.replace(entry.key, entry.value) - } - return txt - }) - - } - - def choose(Map renameSuggestions) { - return UITools.showCheckboxDialog( - "Select which items to rename", - renameSuggestions.keySet().toArray(String[]::new), - renameSuggestions.collect { kv -> "${kv.key} -> ${kv.value}".toString() }.toArray(String[]::new) - ) - } - - boolean isLanguageSupported(ComputerLanguage computerLanguage) { - return computerLanguage != ComputerLanguage.Text - } - -} \ No newline at end of file diff --git a/src/main/resources/sources/groovy/com/github/simiacryptus/aicoder/actions/dev/GenerateProjectAction.groovy b/src/main/resources/sources/groovy/com/github/simiacryptus/aicoder/actions/dev/GenerateProjectAction.groovy deleted file mode 100644 index a866676c..00000000 --- a/src/main/resources/sources/groovy/com/github/simiacryptus/aicoder/actions/dev/GenerateProjectAction.groovy +++ /dev/null @@ -1,515 +0,0 @@ -package com.github.simiacryptus.aicoder.actions.dev - -import com.github.simiacryptus.aicoder.actions.FileContextAction -import com.github.simiacryptus.aicoder.config.AppSettingsState -import com.github.simiacryptus.aicoder.config.Name -import com.github.simiacryptus.aicoder.util.UITools -import com.intellij.openapi.actionSystem.AnActionEvent -import com.intellij.openapi.project.Project -import com.simiacryptus.openai.proxy.ChatProxy -import com.simiacryptus.openai.proxy.ValidatedObject -import groovy.transform.Canonical -import groovy.transform.EqualsAndHashCode -import groovy.transform.ToString -import kotlin.Pair -import org.slf4j.Logger -import org.slf4j.LoggerFactory - -import javax.swing.* -import java.util.concurrent.Callable -import java.util.concurrent.ConcurrentHashMap -import java.util.concurrent.Executors -import java.util.concurrent.Future -import java.util.concurrent.atomic.AtomicInteger -import java.util.zip.ZipEntry -import java.util.zip.ZipOutputStream - -class GenerateProjectAction extends FileContextAction { - static Logger logger = LoggerFactory.getLogger(GenerateProjectAction.class) - - - GenerateProjectAction() { - super(false,true) - setDevAction(true) - } - - static String trimStart(String s1, char[] prefixChars) { - String s = s1 - while (s.length() > 0 && Arrays.asList(prefixChars).contains(s.charAt(0))) { - s = s.substring(1) - } - return s - } - - interface SoftwareProjectAI { - - Project newProject(String description) - - class Project implements ValidatedObject { - String name = '' - String description = '' - String language = '' - List features = [] - List libraries = [] - List buildTools = [] - - boolean validate() { - return true - } - } - - ProjectStatements getProjectStatements(String description, Project project) - - class ProjectStatements implements ValidatedObject { - List assumptions = [] - List designPatterns = [] - List requirements = [] - List risks = [] - - boolean validate() { - return true - } - } - - ProjectDesign buildProjectDesign(Project project, ProjectStatements requirements) - - class ProjectDesign implements ValidatedObject { - List components = [] - List documents = [] - List tests = [] - - boolean validate() { - return true - } - } - - class ComponentDetails implements ValidatedObject { - String name = '' - String description = '' - List features = [] - - boolean validate() { - return true - } - } - - class TestDetails implements ValidatedObject { - String name = '' - List steps = [] - List expectations = [] - - boolean validate() { - return true - } - } - - class DocumentationDetails implements ValidatedObject { - String name = '' - String description = '' - List sections = [] - - boolean validate() { - return true - } - } - - List buildProjectFileSpecifications(Project project, ProjectStatements requirements, ProjectDesign design, boolean recursive) - - List buildComponentFileSpecifications(Project project, ProjectStatements requirements, ComponentDetails design) - - List buildTestFileSpecifications(Project project, ProjectStatements requirements, TestDetails design, boolean recursive) - - List buildDocumentationFileSpecifications(Project project, ProjectStatements requirements, DocumentationDetails design, boolean recursive) - - class CodeSpecification implements ValidatedObject { - String description = '' - List requires = [] - List publicProperties = [] - List publicMethodSignatures = [] - String language = '' - FilePath location = new FilePath() - - boolean validate() { - return true - } - } - - class DocumentSpecification implements ValidatedObject { - String description = '' - List requires = [] - List sections = [] - String language = '' - FilePath location = new FilePath() - - boolean validate() { - return true - } - } - - class TestSpecification implements ValidatedObject { - String description = '' - List requires = [] - List steps = [] - List expectations = [] - String language = '' - FilePath location = new FilePath() - - boolean validate() { - return true - } - } - - @ToString(includeNames = true) - @EqualsAndHashCode() - @Canonical - class FilePath implements ValidatedObject { - String file = '' - - boolean validate() { - return file?.isBlank() == false - } - } - - SourceCode implementComponentSpecification(Project project, ComponentDetails component, List imports, CodeSpecification specification) - - SourceCode implementTestSpecification(Project project, TestSpecification specification, TestDetails test, List imports, TestSpecification specificationAgain) - - SourceCode implementDocumentationSpecification(Project project, DocumentSpecification specification, DocumentationDetails documentation, List imports, DocumentSpecification specificationAgain) - - @ToString(includeNames = true) - @EqualsAndHashCode() - @Canonical - class SourceCode implements ValidatedObject { - String language = '' - String code = '' - - boolean validate() { - return true - } - } - } - - - Map parallelImplement( - SoftwareProjectAI api, - Project project, - Map> components = [:], - Map> documents = [:], - Map> tests = [:], - int drafts, - int threads - ) { - Map> alternates = parallelImplementWithAlternates( - api, - project, - components, - documents, - tests, - drafts, - threads, - { -> return 0.0 } - ) - alternates.collectEntries { [(it.key): it.value.max { it.code?.length() ?: 0 }] } - } - - void write( - File zipArchiveFile, - Map implementations - ) { - new ZipOutputStream(zipArchiveFile.newOutputStream()).with { zip -> - implementations.each { file, sourceCodes -> - zip.putNextEntry(new ZipEntry(file.toString())) - zip.write(sourceCodes.code.bytes) - zip.closeEntry() - } - } - } - - Map> parallelImplementWithAlternates( - SoftwareProjectAI projectAI, - Project project, - Map> components, - Map> documents, - Map> tests, - int drafts, - int threads, - Closure progress - ) { - def threadPool = Executors.newFixedThreadPool(threads) - try { - def totalDrafts = ( - components.values.toList().flatten().size() + - tests.values.toList().flatten().size() + - documents.values.toList().flatten().size() - ) * drafts - - - return components.collectMany { entry -> - buildComponentDetails(entry.key, entry.value) + buildDocumentDetails(entry.key, entry.value) + buildTestDetails(entry.key, entry.value) - }.collect { - try { - Optional.ofNullable(it.get()) - } catch (Throwable ignored) { - Optional.> empty() - } - }.findAll { !it.empty }.collect { it.get() } - .groupBy { it.first }.collectEntries { Map.Entry>> entry -> - [(entry.key): entry.value.sort { Pair p -> p.second.code.length() }] - } - } finally { - threadPool.shutdown() - } - } - - AtomicInteger currentDraft = new AtomicInteger(0) - ConcurrentHashMap>>> fileImplCache = new ConcurrentHashMap>>>() - - def normalizeFileName(String it) { - trimStart(it, ['/', '.']) ?: "" - } - - - def buildComponentDetails(SoftwareProjectAI.ComponentDetails component, List files) { - files.collectMany { SoftwareProjectAI.CodeSpecification file -> - //buildCodeSpec(component, files, file) - if (file.location == null) { - return new ArrayList>>() - } - fileImplCache.getOrPut(normalizeFileName(file.location.file)) { - (0.. - threadPool.submit(new Callable>() { - @Override - Pair call() throws Exception { - SoftwareProjectAI.SourceCode implement = api.implementComponentSpecification( - project, - component, - files.findAll { file.requires?.contains(it.location) ?: false }, - new SoftwareProjectAI.CodeSpecification( - description: file.description, - requires: [], - publicProperties: file.publicProperties, - publicMethodSignatures: file.publicMethodSignatures, - language: file.language, - location: file.location - ) - ) - def progressVal = currentDraft.incrementAndGet().toDouble() / totalDrafts - progress(progressVal) - logger.info("Progress: $progressVal") - return new Pair(file.location, implement) - } - }) - } - } - - } - } - - def buildDocumentDetails(SoftwareProjectAI.DocumentationDetails documentation, List files) { - files.collectMany { SoftwareProjectAI.DocumentSpecification file -> - //buildDocumentSpec(documentation, files, file) - if (file.location == null) { - return new ArrayList>>() - } - fileImplCache.getOrPut(normalizeFileName(file.location.file)) { - (0.. - threadPool.submit(new Callable>() { - - @Override - Pair call() throws Exception { - def implement = projectAI.implementDocumentationSpecification( - project, - new SoftwareProjectAI.DocumentSpecification( - description: file.description, - requires: [], - sections: file.sections, - language: file.language, - location: file.location - ), - documentation, - files.findAll { file.requires?.contains(it.location) ?: false }, - new SoftwareProjectAI.DocumentSpecification( - description: file.description, - requires: [], - sections: file.sections, - language: file.language, - location: file.location - ) - ) - def progressVal = currentDraft.incrementAndGet().toDouble() / totalDrafts - progress(progressVal) - logger.info("Progress: $progressVal") - return new Pair(file.location, implement) - } - }) - } - } - } - } - - - def buildTestDetails(SoftwareProjectAI.TestDetails test, List files) { - files.collectMany { SoftwareProjectAI.TestSpecification file -> - //buildTestSpec(test, files, file) - if (file.location == null) { - return new ArrayList>>() - } - fileImplCache.getOrPut(normalizeFileName(file.location.file)) { - def futures = (0.. - def future = threadPool.submit(new Callable>() { - @Override - Pair call() throws Exception { - def implement = api.implementTestSpecification( - project, - new SoftwareProjectAI.TestSpecification( - description: file.description, - requires: [], - steps: file.steps, - expectations: file.expectations, - language: file.language, - location: file.location - ), - test, - files.findAll { file.requires?.contains(it.location) ?: false }, - new SoftwareProjectAI.TestSpecification( - description: file.description, - requires: [], - steps: file.steps, - expectations: file.expectations, - language: file.language, - location: file.location - ) - ) - def progressVal = currentDraft.incrementAndGet().toDouble() / totalDrafts - progress(progressVal) - logger.info("Progress: $progressVal") - return new Pair(file.location, implement) - } - }) - future - } - futures - } - } - } - - @SuppressWarnings("UNUSED") - class SettingsUI { - @Name("Project Description") - JTextArea description = new JTextArea() - - @Name("Drafts Per File") - JTextField drafts = new JTextField("2") - JCheckBox saveAlternates = new JCheckBox("Save Alternates") - } - - static class Settings { - String description = "" - int drafts = 2 - boolean saveAlternates = false - } - - @Override - Settings getConfig(Project project) { - return UITools.showDialog(project, SettingsUI.class, Settings.class) - } - - @Override - File[] processSelection(SelectionState state, Settings config) { - if (config == null) return new File[0] - - SoftwareProjectAI projectAI = new ChatProxy( - clazz: SoftwareProjectAI.class, - api: api, - model: AppSettingsState.instance.defaultChatModel(), - temperature: AppSettingsState.instance.temperature, - deserializerRetries: 2, - ).create() - - SoftwareProjectAI.Project newProject = projectAI.newProject(config.description.trim()) - - def projectStatements = projectAI.getProjectStatements(config.description, newProject) - def buildProjectDesign = projectAI.buildProjectDesign(newProject, projectStatements) - - def components = buildProjectDesign.components.,SoftwareProjectAI.ComponentDetails>collectEntries { SoftwareProjectAI.ComponentDetails details -> - List specifications = projectAI.buildComponentFileSpecifications( - newProject, - projectStatements, - details - ) - [details: specifications] - } - - def documents = buildProjectDesign.documents.,SoftwareProjectAI.DocumentationDetails>collectEntries { - [(it): projectAI.buildDocumentationFileSpecifications( - newProject, - projectStatements, - it, - false - )] - } - - def tests = buildProjectDesign.tests.,SoftwareProjectAI.TestDetails>collectEntries { - [(it): projectAI.buildTestFileSpecifications( - newProject, - projectStatements, - it, - false - )] - } - def outputDir = new File(state.selectedFile.canonicalPath) - - def threadPool = Executors.newFixedThreadPool(4) - Map> entries - try { -// def totalDrafts = ( -// components.values.toList().flatten().size() + -// tests.values.toList().flatten().size() + -// documents.values.toList().flatten().size() -// ) * drafts - - def groupBy = components.collectMany { entry -> - buildComponentDetails(entry.key, entry.value) + buildDocumentDetails(entry.key, entry.value) + buildTestDetails(entry.key, entry.value) - }.collect { - try { - Optional.ofNullable(it.get()) - } catch (Throwable ignored) { - Optional.> empty() - } - }.findAll { !it.empty }.collect { it.get() } - .groupBy { it.first } - entries = groupBy., SoftwareProjectAI.FilePath, List>> collectEntries { Map.Entry>> entry -> - [(entry.key): entry.value.sort { Pair p -> p.second.code.length() }] - } - } finally { - threadPool.shutdown() - } - - - def generatedFiles = [] - entries.each { file, sourceCode -> - def relative = trimStart(file.file?.trimEnd('/'), ['/', '.']) ?: "" - if (new File(relative).isAbsolute()) { - logger.warn("Invalid path: $relative") - } else { - def outFile = new File(outputDir, relative) - outFile.parentFile.mkdirs() - def best = sourceCode.max { it.code?.length() ?: 0 } - outFile.text = best.code ?: "" - logger.debug("Wrote ${outFile.canonicalPath} (Resolved from $relative)") - generatedFiles << outFile - if (config.saveAlternates) - sourceCode.findAll { it != best }.eachWithIndex { alternate, index -> - def outFileAlternate = new File(outputDir, relative + ".${index + 1}") - outFileAlternate.parentFile.mkdirs() - outFileAlternate.text = alternate.code ?: "" - logger.debug("Wrote ${outFileAlternate.canonicalPath} (Resolved from $relative)") - generatedFiles << outFileAlternate - } - } - } - return generatedFiles as File[] - } - - -} \ No newline at end of file diff --git a/src/main/resources/sources/groovy/com/github/simiacryptus/aicoder/actions/dev/GenerateStoryAction.groovy b/src/main/resources/sources/groovy/com/github/simiacryptus/aicoder/actions/dev/GenerateStoryAction.groovy deleted file mode 100644 index 6cdda634..00000000 --- a/src/main/resources/sources/groovy/com/github/simiacryptus/aicoder/actions/dev/GenerateStoryAction.groovy +++ /dev/null @@ -1,246 +0,0 @@ -package com.github.simiacryptus.aicoder.actions.dev - -import com.github.simiacryptus.aicoder.actions.FileContextAction -import com.github.simiacryptus.aicoder.config.AppSettingsState -import com.github.simiacryptus.aicoder.config.Name -import com.github.simiacryptus.aicoder.util.UITools -import com.intellij.openapi.project.Project -import com.simiacryptus.openai.proxy.ChatProxy -import com.simiacryptus.util.describe.Description -import org.apache.commons.io.FileUtils - -import javax.swing.* - -class GenerateStoryAction extends FileContextAction { - - GenerateStoryAction() { - super(false, true) - setDevAction(true) - } - - interface GenerateStoryAction_VirtualAPI { - public class Idea { - public String title = "" - public String description = "" - - public Idea() {} - - public Idea(String title, String description) { - this.title = title - this.description = description - } - } - - StoryTemplate generatePlot(Idea idea, int numberOfCharacters, int numberOfSettings) - - public class Character { - public String name = "" - public int age = 0 - public String bio = "" - public String role = "" - public String development = "" - - public Character() {} - } - - public class Setting { - public String location = "" - public String timePeriod = "" - public String description = "" - public String significance = "" - - public Setting() {} - } - - public class StoryTemplate { - public List characters = [] - public List settings = [] - @Description("Date and time of the start of the story (e.g. \"2021-01-01 12:00:00\")") - public String startDateTime = "" - @Description("Date and time of the end of the story (e.g. \"2021-01-01 12:00:00\")") - public String endDateTime = "" - @Description("Genre of the story (e.g. \"fantasy\", \"sci-fi\", \"romance\")") - public String genre = "" - @Description("Tone of the story (e.g. \"serious\", \"funny\", \"sad\")") - public String tone = "" - @Description("Mood of the story (e.g. \"happy\", \"sad\", \"angry\")") - public String mood = "" - @Description("Theme of the story (e.g. \"love\", \"death\", \"revenge\")") - public String theme = "" - - public StoryTemplate() {} - } - - public class StoryEvents { - public List storyEvents = [] - - public StoryEvents() {} - } - - public class StoryEvent { - public String who = "" - public String what = "" - public String where = "" - @Description("Date and time of the event (e.g. \"2021-01-01 12:00:00\")") - public String when = "" - public String why = "" - public String how = "" - public String result = "" - public String punchline = "" - public List origins = [] - - public StoryEvent() {} - } - - public class StoryObjectOrigin { - @Description("Actor or object name") - public String name = "" - @Description("Where did this object come from? e.g. \"Jackie was introduced in the previous chapter.\" or \"the book was found on a bookshelf\"") - public String origin = "" - - public StoryObjectOrigin() {} - } - - StoryEvents generatePlotPoints(Idea idea, StoryTemplate story) - - StoryEvents expandEvents( - @Description("Overall story for context") - StoryTemplate context, - @Description("Previous events (for continuity)") - StoryEvents previous, - @Description("Next events; Expanded events will lead to these events") - StoryEvents next, - @Description("Current event to expand") - StoryEvent event, - @Description("The number of events to generate") - int count - ) - - ScreenplaySegment writeScreenplaySegment( - StoryTemplate story, - StoryEvent event, - ScreenplaySegment prevoiousSegment, - int segmentItemCount - ) - - public class ScreenplaySegment { - public String settingStart = "" - public List items = [] - public String settingEnd = "" - - public ScreenplaySegment() {} - } - - public class ScreenplayItem { - public String actor = "" - public String type = "" - public String text = "" - - public ScreenplayItem() {} - } - - Page writeStoryPage( - String style, - ScreenplaySegment scene, - Page previousPage, - int pageWordCount - ) - - public class Page { - public int pageNumber - @Description("Full page text") - public String text = "" - - public Page() {} - } - } - - - @SuppressWarnings("UNUSED") - static class SettingsUI { - @Name("Title") - public JTextField title = new JTextField("How to write a book") - - @Name("Description") - public JTextArea description = new JTextArea( - """ - |A software developer teaches a computer how to teach another computer how to write a book. - |They then teach another computer to use that computer to publish and sell books online. - |Chaos ensues. Society collapses. The world ends. - |""".stripMargin().trim() - ) - - @Name("Title") - public JTextField writingStyle = new JTextField("First Person Narrative, Present Tense, 8th Grade Reading Level, Funny") - } - - static class Settings { - public String title = "" - public String description = "" - public String writingStyle = "" - - public Settings() {} - } - - @Override - Settings getConfig(Project project) { - return UITools.showDialog(project, SettingsUI.class, Settings.class, "Generate Story", {}) - } - - @Override - File[] processSelection(SelectionState state, Settings config) { - List outputFiles = [] - - if (config) { - File selectedFolder = state.selectedFile - - def proxy = new ChatProxy( - clazz: GenerateStoryAction_VirtualAPI.class, - api: api, - model: AppSettingsState.instance.defaultChatModel(), - temperature: AppSettingsState.instance.temperature, - deserializerRetries: 2, - ).create() - - - def idea = new GenerateStoryAction_VirtualAPI.Idea(config.title, config.description) - def storyTemplate = proxy.generatePlot(idea, 5, 5) - def storyEvents = proxy.generatePlotPoints(idea, storyTemplate) - List segments = [] - GenerateStoryAction_VirtualAPI.ScreenplaySegment previousSegment = null - storyEvents.storyEvents.each { event -> - segments << proxy.writeScreenplaySegment(storyTemplate, event, previousSegment, 5) - previousSegment = segments.last() - } - File screenplayFile = new File(new File(selectedFolder.path), config.title + "_screenplay.md") - screenplayFile.parentFile.mkdirs() - def fileContents = segments.collect { - it.items.collect { - """ - | - |**${it.actor}**: ${it.text} - | - """.stripMargin() - }.join("\n") - }.join("\n\n") - FileUtils.write(screenplayFile, fileContents, "UTF-8") - outputFiles << screenplayFile - List pages = [] - GenerateStoryAction_VirtualAPI.Page previousPage = null - segments.each { segment -> - try { - pages << proxy.writeStoryPage(config.writingStyle, segment, previousPage, 2) - previousPage = pages.last() - } catch (Exception e) { - log.warn("Failed to write page: ${e.message}", e) - } - } - File storyFile = new File(new File(selectedFolder.path), config.title + ".md") - FileUtils.write(storyFile, pages.collect { it.text }.join("\n\n"), "UTF-8") - outputFiles << storyFile - } - - return outputFiles.toArray() - } - -} \ No newline at end of file diff --git a/src/main/resources/sources/groovy/com/github/simiacryptus/aicoder/actions/generic/AnalogueFileAction.groovy b/src/main/resources/sources/groovy/com/github/simiacryptus/aicoder/actions/generic/AnalogueFileAction.groovy deleted file mode 100644 index 005235de..00000000 --- a/src/main/resources/sources/groovy/com/github/simiacryptus/aicoder/actions/generic/AnalogueFileAction.groovy +++ /dev/null @@ -1,133 +0,0 @@ -package com.github.simiacryptus.aicoder.actions.generic - -import com.github.simiacryptus.aicoder.actions.FileContextAction -import com.github.simiacryptus.aicoder.config.AppSettingsState -import com.github.simiacryptus.aicoder.config.Name -import com.github.simiacryptus.aicoder.util.UITools -import com.intellij.openapi.project.Project -import com.simiacryptus.jopenai.ApiModel.ChatMessage -import com.simiacryptus.jopenai.ApiModel.ChatRequest -import org.apache.commons.io.FileUtils -import org.apache.commons.io.IOUtils - -import javax.swing.* -import java.nio.file.Path - -class AnalogueFileAction extends FileContextAction { - - AnalogueFileAction() { - super(true, false) - } - - - private static class ProjectFile { - public String path = "" - public String code = "" - - ProjectFile() { - } - } - - @SuppressWarnings("UNUSED") - static class SettingsUI { - @Name("Directive") - public JTextArea directive = new JTextArea( - /* text = */ """ - Create test cases - """.stripIndent().trim(), - /* rows = */ 3, - /* columns = */ 120 - ) - } - - static class Settings { - public String directive = "" - - Settings() { - } - } - - @Override - Settings getConfig(Project project) { - return UITools.showDialog( - project, - SettingsUI.class, - Settings.class, - "Create Analogue File", - {} - ) - } - - @Override - File[] processSelection(SelectionState state, Settings config) { - ProjectFile analogue = generateFile( - new ProjectFile( - path: state.projectRoot.toPath().relativize(state.selectedFile.toPath()), - code: IOUtils.toString(new FileInputStream(state.selectedFile), "UTF-8") - ), - config?.directive ?: "" - ) - Path outputPath = state.projectRoot.toPath().resolve(analogue.path) - if (outputPath.toFile().exists()) { - String extension = outputPath.toString().split("\\.").last() - String name = outputPath.toString().split("\\.").dropRight(1).join(".") - int fileIndex = (1..Integer.MAX_VALUE).find { - !new File(state.projectRoot, "$name.$it.$extension").exists() - } - outputPath = state.projectRoot.toPath().resolve("$name.$fileIndex.$extension") - } - outputPath.parent.toFile().mkdirs() - FileUtils.write(outputPath.toFile(), analogue.code, "UTF-8") - Thread.sleep(100) - return [outputPath.toFile()] - - } - - private ProjectFile generateFile(ProjectFile baseFile, String directive) { - def chatRequest = new ChatRequest() - def model = AppSettingsState.instance.defaultChatModel() - chatRequest.model = model.modelName - chatRequest.temperature = AppSettingsState.instance.temperature - chatRequest.messages = [ - new ChatMessage( - Role.system, """ - You will combine natural language instructions with a user provided code example to create a new file. - Provide a new filename and the code to be written to the file. - Paths should be relative to the project root and should not exist. - Output the file path using the a line with the format "File: ". - Output the file code directly after the header line with no additional decoration. - """.stripIndent(), null - ), - new ChatMessage( - Role.owner, """ - Create a new file based on the following directive: $directive - - The file should be based on `${baseFile.path}` which contains the following code: - - ``` - ${baseFile.code} - ``` - """.stripIndent(), null - ) - ] - String response = api.chat( - chatRequest, - AppSettingsState.instance.defaultChatModel() - ).choices?.first()?.message?.content?.trim() - String outputPath = baseFile.path - String header = response.split("\n").first() - String body = response.split("\n").drop(1).join("\n").trim() - if (body.contains("```")) { - body = body.split("```.*").drop(1).first().trim() - } - def pathPattern = ~"""File(?:name)?: ['`"]?([^'`"]+)['`"]?""" - def matcher = pathPattern.matcher(header) - if (matcher.find()) { - outputPath = matcher.group(1).trim() - } - return new ProjectFile( - path: outputPath, - code: body - ) - } -} \ No newline at end of file diff --git a/src/main/resources/sources/groovy/com/github/simiacryptus/aicoder/actions/generic/AppendAction.groovy b/src/main/resources/sources/groovy/com/github/simiacryptus/aicoder/actions/generic/AppendAction.groovy deleted file mode 100644 index 3465d2dc..00000000 --- a/src/main/resources/sources/groovy/com/github/simiacryptus/aicoder/actions/generic/AppendAction.groovy +++ /dev/null @@ -1,34 +0,0 @@ -package com.github.simiacryptus.aicoder.actions.generic - -import com.github.simiacryptus.aicoder.actions.SelectionAction -import com.github.simiacryptus.aicoder.config.AppSettingsState -import com.intellij.openapi.project.Project -import org.jetbrains.annotations.Nullable - -class AppendAction extends SelectionAction { - @Override - java.lang.String getConfig(@Nullable Project project) { - return "" - } - - @Override - String processSelection(SelectionState state, String config) { - def settings = AppSettingsState.instance - def request = settings.createChatRequest() - request.temperature = AppSettingsState.instance.temperature - request.messages = [ - new com.simiacryptus.jopenai.ApiModel.ChatMessage( - com.simiacryptus.jopenai.ApiModel.Role.system, - "Append text to the end of the user's prompt", null - ), - new com.simiacryptus.jopenai.ApiModel.ChatMessage( - com.simiacryptus.jopenai.ApiModel.Role.user, - state.selectedText.toString(), null - ) - ] - def chatResponse = api.chat(request, AppSettingsState.instance.defaultChatModel()) - def b4 = state.selectedText ?: "" - def str = (chatResponse.choices[0].message?.content ?: "") - return b4 + (str.startsWith(b4) ? str.substring(b4.length) : str) - } -} \ No newline at end of file diff --git a/src/main/resources/sources/groovy/com/github/simiacryptus/aicoder/actions/generic/CreateFileAction.groovy b/src/main/resources/sources/groovy/com/github/simiacryptus/aicoder/actions/generic/CreateFileAction.groovy deleted file mode 100644 index b0eb385b..00000000 --- a/src/main/resources/sources/groovy/com/github/simiacryptus/aicoder/actions/generic/CreateFileAction.groovy +++ /dev/null @@ -1,139 +0,0 @@ -package com.github.simiacryptus.aicoder.actions.generic - -import com.github.simiacryptus.aicoder.actions.FileContextAction -import com.github.simiacryptus.aicoder.config.AppSettingsState -import com.github.simiacryptus.aicoder.config.Name -import com.github.simiacryptus.aicoder.util.UITools -import com.intellij.openapi.project.Project -import com.simiacryptus.jopenai.ApiModel.ChatMessage -import com.simiacryptus.jopenai.ApiModel.ChatRequest - -import javax.swing.* - -class CreateFileAction extends FileContextAction { - - CreateFileAction() { - super(false, true) - } - - static class ProjectFile { - public String path = "" - public String code = "" - - ProjectFile() { - } - } - - static class SettingsUI { - @Name("Directive") - public JTextArea directive = new JTextArea( - /* text = */ """ - Create a default log4j configuration file - """.stripIndent().trim(), - /* rows = */ 3, - /* columns = */ 120 - ) - - SettingsUI() { - } - } - - static class Settings { - public String directive = "" - - Settings() { - } - } - - @Override - File[] processSelection( - SelectionState state, - Settings config - ) { - def projectRoot = state.projectRoot.toPath() - def inputPath = projectRoot.relativize(state.selectedFile.toPath()).toString() - def pathSegments = inputPath.split("/").toList() - def updirSegments = pathSegments.takeWhile { it == ".." }.toList() - def moduleRoot = projectRoot.resolve(pathSegments.take(updirSegments.size() * 2).join("/")) - def filePath = pathSegments.drop(updirSegments.size() * 2).join("/") - - def generatedFile = generateFile(filePath, config?.directive ?: "") - - def path = generatedFile.path - def outputPath = moduleRoot.resolve(path) - if (outputPath.toFile().exists()) { - def extension = path.split(".").last() - def name = path.split(".").init().join(".") - def fileIndex = (1..Integer.MAX_VALUE).find { - !new File("$name.$it.$extension").exists() - } - path = "$name.$fileIndex.$extension" - outputPath = projectRoot.resolve(path) - } - outputPath.parent.toFile().mkdirs() - outputPath.toFile().text = generatedFile.code - Thread.sleep(100) - - return [outputPath.toFile()] as File[] - } - - private ProjectFile generateFile( - String basePath, - String directive - ) { - def chatRequest = new ChatRequest() - def model = AppSettingsState.instance.defaultChatModel() - chatRequest.model = model.modelName - chatRequest.temperature = AppSettingsState.instance.temperature - chatRequest.messages = [ - //language=TEXT - new ChatMessage( - Role.system, """ - You will interpret natural language requirements to create a new file. - Provide a new filename and the code to be written to the file. - Paths should be relative to the project root and should not exist. - Output the file path using the a line with the format "File: ". - Output the file code directly after the header line with no additional decoration. - """.stripIndent(), null - ), - //language=TEXT - new ChatMessage( - Role.owner, """ - Create a new file based on the following directive: $directive - - The file location should be based on the selected path `${basePath}` - """.stripIndent(), null - ) - ] - def response = api.chat( - chatRequest, - AppSettingsState.instance.defaultChatModel() - ).choices?.first()?.message?.content?.trim() - def outputPath = basePath - def header = response.split("\n").first() - def body = response.split("\n").drop(1).join("\n").trim() - if (body.startsWith("```")) { - // Remove beginning ``` (optionally ```language) and ending ``` - body = body.split("\n").drop(1).init().join("\n").trim() - } - def pathPattern = ~"""File(?:name)?: ['`"]?([^'`"]+)['`"]?""" - if (header =~ pathPattern) { - def match = (header =~ pathPattern)[0] - outputPath = match[1].toString() - } - return new ProjectFile( - path: outputPath, - code: body - ) - } - - @Override - Settings getConfig(Project project) { - return UITools.showDialog( - project, - SettingsUI, - Settings, - "Create File from Requirements", {} - ) - } -} \ No newline at end of file diff --git a/src/main/resources/sources/groovy/com/github/simiacryptus/aicoder/actions/generic/GenerateProjectAction.groovy b/src/main/resources/sources/groovy/com/github/simiacryptus/aicoder/actions/generic/GenerateProjectAction.groovy deleted file mode 100644 index 8ab50bd9..00000000 --- a/src/main/resources/sources/groovy/com/github/simiacryptus/aicoder/actions/generic/GenerateProjectAction.groovy +++ /dev/null @@ -1,533 +0,0 @@ -package com.github.simiacryptus.aicoder.actions.generic - -import com.github.simiacryptus.aicoder.actions.FileContextAction -import com.github.simiacryptus.aicoder.config.AppSettingsState -import com.github.simiacryptus.aicoder.config.Name -import com.github.simiacryptus.aicoder.util.UITools -import com.intellij.openapi.project.Project -import com.simiacryptus.openai.proxy.ChatProxy -import com.simiacryptus.openai.proxy.ValidatedObject -import kotlin.Pair -import org.slf4j.Logger -import org.slf4j.LoggerFactory - -import javax.swing.* -import java.util.concurrent.Callable -import java.util.concurrent.ConcurrentHashMap -import java.util.concurrent.ExecutorService -import java.util.concurrent.Executors -import java.util.concurrent.Future -import java.util.concurrent.atomic.AtomicInteger -import java.util.zip.ZipEntry -import java.util.zip.ZipOutputStream - -public class GenerateProjectAction extends FileContextAction { - static Logger logger = LoggerFactory.getLogger(GenerateProjectAction.class) - - - - public GenerateProjectAction() { - super(false, true) - } - - static String trimStart(String s1, List prefixChars) { - String s = s1 - while (s.length() > 0 && prefixChars.contains(s.charAt(0))) { - s = s.substring(1) - } - return s - } - - static String trimEnd(String string, String suffix) { - if (string.endsWith(suffix)) { - return string.substring(0, string.length() - suffix.length()) - } else { - return string - } - } - - interface SoftwareProjectAI { - - Project newProject(String description) - - public class Project implements ValidatedObject { - public String name = '' - public String description = '' - public String language = '' - public List features = [] - public List libraries = [] - public List buildTools = [] - public Project() {} - boolean validate() { - return true - } - } - - ProjectStatements getProjectStatements(String description, Project project) - - public class ProjectStatements implements ValidatedObject { - public List assumptions = [] - public List designPatterns = [] - public List requirements = [] - public List risks = [] - public ProjectStatements() {} - - boolean validate() { - return true - } - } - - ProjectDesign buildProjectDesign(Project project, ProjectStatements requirements) - - public class ProjectDesign implements ValidatedObject { - public List components = [] - public List documents = [] - public List tests = [] - public ProjectDesign() {} - - boolean validate() { - return true - } - } - - public class ComponentDetails implements ValidatedObject { - public String name = '' - public String description = '' - public List features = [] - public ComponentDetails() {} - boolean validate() { - return true - } - } - - public class TestDetails implements ValidatedObject { - public String name = '' - public List steps = [] - public List expectations = [] - public TestDetails() {} - boolean validate() { - return true - } - } - - public class DocumentationDetails implements ValidatedObject { - public String name = '' - public String description = '' - public List sections = [] - public DocumentationDetails() {} - - boolean validate() { - return true - } - } - - CodeSpecificationList buildProjectFileSpecifications(Project project, ProjectStatements requirements, ProjectDesign design, boolean recursive) - - public class CodeSpecificationList implements ValidatedObject { - public List files = [] - public CodeSpecificationList() {} - boolean validate() { - return true - } - } - - CodeSpecificationList buildComponentFileSpecifications(Project project, ProjectStatements requirements, ComponentDetails design) - - TestSpecificationList buildTestFileSpecifications(Project project, ProjectStatements requirements, TestDetails design, boolean recursive) - - public class TestSpecificationList implements ValidatedObject { - public List files = [] - public TestSpecificationList() {} - boolean validate() { - return true - } - } - - DocumentSpecificationList buildDocumentationFileSpecifications(Project project, ProjectStatements requirements, DocumentationDetails design, boolean recursive) - - public class DocumentSpecificationList implements ValidatedObject { - public List files = [] - public DocumentSpecificationList() {} - boolean validate() { - return true - } - } - - public class CodeSpecification implements ValidatedObject { - public String description = '' - public List requires = [] - public List publicProperties = [] - public List publicMethodSignatures = [] - public String language = '' - public FilePath location = new FilePath() - public CodeSpecification() {} - boolean validate() { - return true - } - } - - public class DocumentSpecification implements ValidatedObject { - public String description = '' - public List requires = [] - public List sections = [] - public String language = '' - public FilePath location = new FilePath() - public DocumentSpecification() {} - boolean validate() { - return true - } - } - - public class TestSpecification implements ValidatedObject { - public String description = '' - public List requires = [] - public List steps = [] - public List expectations = [] - public String language = '' - public FilePath location = new FilePath() - public TestSpecification() {} - boolean validate() { - return true - } - } - - public class FilePath implements ValidatedObject { - public String file = '' - public FilePath() {} - - boolean validate() { - return file?.isBlank() == false - } - } - - SourceCode implementComponentSpecification(Project project, ComponentDetails component, List imports, CodeSpecification specification) - - SourceCode implementTestSpecification(Project project, TestSpecification specification, TestDetails test, List imports, TestSpecification specificationAgain) - - SourceCode implementDocumentationSpecification(Project project, DocumentSpecification specification, DocumentationDetails documentation, List imports, DocumentSpecification specificationAgain) - - public class SourceCode implements ValidatedObject { - public String language = '' - public String code = '' - public SourceCode() {} - boolean validate() { - return true - } - } - } - - - Map parallelImplement( - SoftwareProjectAI api, - Project project, - Map> components = [:], - Map> documents = [:], - Map> tests = [:], - int drafts, - int threads - ) { - Map> alternates = parallelImplementWithAlternates( - api, - project, - components, - documents, - tests, - drafts, - threads, - { -> return 0.0 } - ) - alternates.collectEntries { [(it.key): it.value.max { it.code?.length() ?: 0 }] } - } - - void write( - File zipArchiveFile, - Map implementations - ) { - new ZipOutputStream(zipArchiveFile.newOutputStream()).with { zip -> - implementations.each { file, sourceCodes -> - zip.putNextEntry(new ZipEntry(file.toString())) - zip.write(sourceCodes.code.bytes) - zip.closeEntry() - } - } - } - - private static final ExecutorService threadPool = Executors.newCachedThreadPool() - - Map> parallelImplementWithAlternates( - SoftwareProjectAI projectAI, - Project project, - Map> components, - Map> documents, - Map> tests, - int drafts, - int threads, - Closure progress - ) { - return (components.collectMany { entry -> buildComponentDetails(project, entry.key, entry.value) } + - documents.collectMany { entry -> buildDocumentDetails(project, entry.key, entry.value) } + - tests.collectMany { entry -> buildTestDetails(project, entry.key, entry.value) }).collect { - try { - Optional.ofNullable(it.get()) - } catch (Throwable ignored) { - Optional.> empty() - } - }.findAll { !it.empty }.collect { it.get() } - .groupBy { it.first }.collectEntries { Map.Entry>> entry -> - [(entry.key): entry.value.sort { Pair p -> p.second.code.length() }] - } - } - - AtomicInteger currentDraft = new AtomicInteger(0) - ConcurrentHashMap>>> fileImplCache = new ConcurrentHashMap>>>() - - static def normalizeFileName(String it) { - trimStart(it, ['/', '.']) - } - - def buildComponentDetails(SoftwareProjectAI.Project project, SoftwareProjectAI.ComponentDetails component, List files, int drafts) { - files.collectMany { SoftwareProjectAI.CodeSpecification file -> - //buildCodeSpec(component, files, file) - if (file.location == null) { - return new ArrayList>>() - } - - def key = normalizeFileName(file.location.file) - if(!fileImplCache.containsKey(key)) { - def value = (0.. - threadPool.submit(new Callable>() { - @Override - Pair call() throws Exception { - SoftwareProjectAI.SourceCode implement = projectAI.implementComponentSpecification( - project, - component, - files.findAll { file.requires?.contains(it.location) ?: false }, - new SoftwareProjectAI.CodeSpecification( - description: file.description, - requires: [], - publicProperties: file.publicProperties, - publicMethodSignatures: file.publicMethodSignatures, - language: file.language, - location: file.location - ) - ) -// def progressVal = currentDraft.incrementAndGet().toDouble() / totalDrafts -// progress(progressVal) -// logger.info("Progress: $progressVal") - return new Pair(file.location, implement) - } - }) - } - fileImplCache.put(key, value) - } - return fileImplCache.get(key) - - } - } - - def buildDocumentDetails(SoftwareProjectAI.Project project, SoftwareProjectAI.DocumentationDetails documentation, List files, int drafts) { - files.collectMany { SoftwareProjectAI.DocumentSpecification file -> - //buildDocumentSpec(documentation, files, file) - if (file.location == null) { - return new ArrayList>>() - } - def key = normalizeFileName(file.location.file) - if(!fileImplCache.containsKey(key)) { - def value = (0.. - threadPool.submit(new Callable>() { - @Override - Pair call() throws Exception { - def implement = projectAI.implementDocumentationSpecification( - project, - new SoftwareProjectAI.DocumentSpecification( - description: file.description, - requires: [], - sections: file.sections, - language: file.language, - location: file.location - ), - documentation, - files.findAll { file.requires?.contains(it.location) ?: false }, - new SoftwareProjectAI.DocumentSpecification( - description: file.description, - requires: [], - sections: file.sections, - language: file.language, - location: file.location - ) - ) -// def progressVal = currentDraft.incrementAndGet().toDouble() / totalDrafts -// progress(progressVal) -// logger.info("Progress: $progressVal") - return new Pair(file.location, implement) - } - }) - } - fileImplCache.put(key, value) - } - return fileImplCache.get(key) - } - } - - def buildTestDetails(SoftwareProjectAI.Project project, SoftwareProjectAI.TestDetails test, List files, int drafts) { - files.collectMany { SoftwareProjectAI.TestSpecification file -> - //buildTestSpec(test, files, file) - if (file.location == null) { - return new ArrayList>>() - } - - def key = normalizeFileName(file.location.file) - if(!fileImplCache.containsKey(key)) { - def value = (0.. - threadPool.submit(new Callable>() { - @Override - Pair call() throws Exception { - def implement = projectAI.implementTestSpecification( - project, - new SoftwareProjectAI.TestSpecification( - description: file.description, - requires: [], - steps: file.steps, - expectations: file.expectations, - language: file.language, - location: file.location - ), - test, - files.findAll { file.requires?.contains(it.location) ?: false }, - new SoftwareProjectAI.TestSpecification( - description: file.description, - requires: [], - steps: file.steps, - expectations: file.expectations, - language: file.language, - location: file.location - ) - ) -// def progressVal = currentDraft.incrementAndGet().toDouble() / totalDrafts -// progress(progressVal) -// logger.info("Progress: $progressVal") - return new Pair(file.location, implement) - } - }) - } - fileImplCache.put(key, value) - } - return fileImplCache.get(key) - } - } - - @SuppressWarnings("UNUSED") - public static class SettingsUI { - @Name("Project Description") - public JTextArea description = new JTextArea() - - @Name("Drafts Per File") - public JTextField drafts = new JTextField("2") - public JCheckBox saveAlternates = new JCheckBox("Save Alternates") - - public SettingsUI() { - description.setLineWrap(true) - description.setWrapStyleWord(true) - } - } - - public static class Settings { - public String description = "" - public int drafts = 2 - public boolean saveAlternates = false - - public Settings() {} - } - - @Override - Settings getConfig(Project project) { - return UITools.showDialog(project, SettingsUI.class, Settings.class, "Project Settings", { config -> }) - } - - SoftwareProjectAI projectAI = new ChatProxy( - clazz: SoftwareProjectAI.class, - api: api, - model: AppSettingsState.instance.defaultChatModel(), - temperature: AppSettingsState.instance.temperature, - deserializerRetries: 2, - ).create() - - @Override - File[] processSelection(SelectionState state, Settings config) { - if (config == null) return new File[0] - - - SoftwareProjectAI.Project project = projectAI.newProject(config.description.trim()) - def projectStatements = projectAI.getProjectStatements(config.description, project) - def buildProjectDesign = projectAI.buildProjectDesign(project, projectStatements) - - def components = buildProjectDesign.components., SoftwareProjectAI.ComponentDetails> collectEntries { - [(it): projectAI.buildComponentFileSpecifications( - project, - projectStatements, - it - ).files] - } - - def documents = buildProjectDesign.documents., SoftwareProjectAI.DocumentationDetails> collectEntries { - [(it): projectAI.buildDocumentationFileSpecifications( - project, - projectStatements, - it, - false - ).files] - } - - def tests = buildProjectDesign.tests., SoftwareProjectAI.TestDetails> collectEntries { - [(it): projectAI.buildTestFileSpecifications( - project, - projectStatements, - it, - false - ).files] - } - def outputDir = new File(state.selectedFile.canonicalPath) - - def entries = ( - components.collectMany { entry -> buildComponentDetails(project, entry.key, entry.value, config.drafts) } - + documents.collectMany { entry -> buildDocumentDetails(project, entry.key, entry.value, config.drafts) } - + tests.collectMany { entry -> buildTestDetails(project, entry.key, entry.value, config.drafts) }) - .collect { - try { - Optional.ofNullable(it.get()) - } catch (Throwable ignored) { - Optional.> empty() - } - }.findAll { !it.empty }.collect { it.get() } - .groupBy { it.first }., SoftwareProjectAI.FilePath, List>> collectEntries { - entry -> [(entry.key): entry.value.collect { it.second}.sort { a, b -> a.code.length() <=> b.code.length()}] - } - - - def generatedFiles = [] - entries.each { file, sourceCode -> - def relative = trimStart(trimEnd(file.file, '/'), ['/', '.']) ?: "" - if (new File(relative).isAbsolute()) { - logger.warn("Invalid path: $relative") - } else { - def outFile = new File(outputDir, relative) - outFile.parentFile.mkdirs() - def best = sourceCode.max { it.code.length() } - outFile.text = best.code - logger.debug("Wrote ${outFile.canonicalPath} (Resolved from $relative)") - generatedFiles << outFile - if (config.saveAlternates) - sourceCode.findAll { it != best }.eachWithIndex { alternate, index -> - def outFileAlternate = new File(outputDir, relative + ".${index + 1}") - outFileAlternate.parentFile.mkdirs() - outFileAlternate.text = alternate.code - logger.debug("Wrote ${outFileAlternate.canonicalPath} (Resolved from $relative)") - generatedFiles << outFileAlternate - } - } - } - return generatedFiles as File[] - } - - -} \ No newline at end of file diff --git a/src/main/resources/sources/groovy/com/github/simiacryptus/aicoder/actions/generic/GenerateStoryAction.groovy b/src/main/resources/sources/groovy/com/github/simiacryptus/aicoder/actions/generic/GenerateStoryAction.groovy deleted file mode 100644 index 78f77b98..00000000 --- a/src/main/resources/sources/groovy/com/github/simiacryptus/aicoder/actions/generic/GenerateStoryAction.groovy +++ /dev/null @@ -1,245 +0,0 @@ -package com.github.simiacryptus.aicoder.actions.generic - -import com.github.simiacryptus.aicoder.actions.FileContextAction -import com.github.simiacryptus.aicoder.config.AppSettingsState -import com.github.simiacryptus.aicoder.config.Name -import com.github.simiacryptus.aicoder.util.UITools -import com.intellij.openapi.project.Project -import com.simiacryptus.openai.proxy.ChatProxy -import com.simiacryptus.util.describe.Description -import org.apache.commons.io.FileUtils - -import javax.swing.* - -class GenerateStoryAction extends FileContextAction { - - GenerateStoryAction() { - super(false, true) - } - - interface AuthorAPI { - class Idea { - public String title = "" - public String description = "" - - Idea() {} - - Idea(String title, String description) { - this.title = title - this.description = description - } - } - - StoryTemplate generatePlot(Idea idea, int numberOfCharacters, int numberOfSettings) - - class Character { - public String name = "" - public int age = 0 - public String bio = "" - public String role = "" - public String development = "" - - Character() {} - } - - class Setting { - public String location = "" - public String timePeriod = "" - public String description = "" - public String significance = "" - - Setting() {} - } - - class StoryTemplate { - public List characters = [] - public List settings = [] - @Description("Date and time of the start of the story (e.g. \"2021-01-01 12:00:00\")") - public String startDateTime = "" - @Description("Date and time of the end of the story (e.g. \"2021-01-01 12:00:00\")") - public String endDateTime = "" - @Description("Genre of the story (e.g. \"fantasy\", \"sci-fi\", \"romance\")") - public String genre = "" - @Description("Tone of the story (e.g. \"serious\", \"funny\", \"sad\")") - public String tone = "" - @Description("Mood of the story (e.g. \"happy\", \"sad\", \"angry\")") - public String mood = "" - @Description("Theme of the story (e.g. \"love\", \"death\", \"revenge\")") - public String theme = "" - - StoryTemplate() {} - } - - class StoryEvents { - public List storyEvents = [] - - StoryEvents() {} - } - - class StoryEvent { - public String who = "" - public String what = "" - public String where = "" - @Description("Date and time of the event (e.g. \"2021-01-01 12:00:00\")") - public String when = "" - public String why = "" - public String how = "" - public String result = "" - public String punchline = "" - public List origins = [] - - StoryEvent() {} - } - - class StoryObjectOrigin { - @Description("Actor or object name") - public String name = "" - @Description("Where did this object come from? e.g. \"Jackie was introduced in the previous chapter.\" or \"the book was found on a bookshelf\"") - public String origin = "" - - StoryObjectOrigin() {} - } - - StoryEvents generatePlotPoints(Idea idea, StoryTemplate story) - - StoryEvents expandEvents( - @Description("Overall story for context") - StoryTemplate context, - @Description("Previous events (for continuity)") - StoryEvents previous, - @Description("Next events; Expanded events will lead to these events") - StoryEvents next, - @Description("Current event to expand") - StoryEvent event, - @Description("The number of events to generate") - int count - ) - - ScreenplaySegment writeScreenplaySegment( - StoryTemplate story, - StoryEvent event, - ScreenplaySegment prevoiousSegment, - int segmentItemCount - ) - - class ScreenplaySegment { - public String settingStart = "" - public List items = [] - public String settingEnd = "" - - ScreenplaySegment() {} - } - - class ScreenplayItem { - public String actor = "" - public String type = "" - public String text = "" - - ScreenplayItem() {} - } - - Page writeStoryPage( - String style, - ScreenplaySegment scene, - Page previousPage, - int pageWordCount - ) - - class Page { - public int pageNumber - @Description("Full page text") - public String text = "" - - Page() {} - } - } - - - @SuppressWarnings("UNUSED") - static class SettingsUI { - @Name("Title") - public JTextField title = new JTextField("How to write a book") - - @Name("Description") - public JTextArea description = new JTextArea( - """ - |A software developer teaches a computer how to teach another computer how to write a book. - |They then teach another computer to use that computer to publish and sell books online. - |Chaos ensues. Society collapses. The world ends. - |""".stripMargin().trim() - ) - - @Name("Title") - public JTextField writingStyle = new JTextField("First Person Narrative, Present Tense, 8th Grade Reading Level, Funny") - } - - static class Settings { - public String title = "" - public String description = "" - public String writingStyle = "" - - Settings() {} - } - - @Override - Settings getConfig(Project project) { - return UITools.showDialog(project, SettingsUI.class, Settings.class, "Generate Story", {}) - } - - AuthorAPI proxy = null - - @Override - File[] processSelection(SelectionState state, Settings config) { - proxy = new ChatProxy( - clazz: AuthorAPI.class, - api: api, - model: AppSettingsState.instance.defaultChatModel(), - temperature: AppSettingsState.instance.temperature, - deserializerRetries: 2, - ).create() - - List outputFiles = [] - - if (config) { - File selectedFolder = state.selectedFile - def idea = new AuthorAPI.Idea(config.title, config.description) - def storyTemplate = proxy.generatePlot(idea, 5, 5) - def storyEvents = proxy.generatePlotPoints(idea, storyTemplate) - List segments = [] - AuthorAPI.ScreenplaySegment previousSegment = null - storyEvents.storyEvents.each { event -> - segments << proxy.writeScreenplaySegment(storyTemplate, event, previousSegment, 5) - previousSegment = segments.last() - } - File screenplayFile = new File(new File(selectedFolder.path), config.title + "_screenplay.md") - screenplayFile.parentFile.mkdirs() - def fileContents = segments.collect { - it.items.collect { - """ - | - |**${it.actor}**: ${it.text} - | - """.stripMargin() - }.join("\n") - }.join("\n\n") - FileUtils.write(screenplayFile, fileContents, "UTF-8") - outputFiles << screenplayFile - List pages = [] - AuthorAPI.Page previousPage = null - segments.each { segment -> - try { - pages << proxy.writeStoryPage(config.writingStyle, segment, previousPage, 2) - previousPage = pages.last() - } catch (Exception e) { - UITools.error(log,"Failed to write page: ${e.message}", e) - } - } - File storyFile = new File(new File(selectedFolder.path), config.title + ".md") - FileUtils.write(storyFile, pages.collect { it.text }.join("\n\n"), "UTF-8") - outputFiles << storyFile - } - - return outputFiles.toArray(new File[0]) - } - -} \ No newline at end of file diff --git a/src/main/resources/sources/groovy/com/github/simiacryptus/aicoder/actions/generic/ReplaceOptionsAction.groovy b/src/main/resources/sources/groovy/com/github/simiacryptus/aicoder/actions/generic/ReplaceOptionsAction.groovy deleted file mode 100644 index 1acbbf59..00000000 --- a/src/main/resources/sources/groovy/com/github/simiacryptus/aicoder/actions/generic/ReplaceOptionsAction.groovy +++ /dev/null @@ -1,63 +0,0 @@ -package com.github.simiacryptus.aicoder.actions.generic - -import com.github.simiacryptus.aicoder.actions.SelectionAction -import com.github.simiacryptus.aicoder.config.AppSettingsState -import com.github.simiacryptus.aicoder.util.UITools -import com.intellij.openapi.actionSystem.AnActionEvent -import com.intellij.openapi.project.Project -import com.simiacryptus.jopenai.proxy.ChatProxy -import com.simiacryptus.jopenai.util.StringUtil -import org.jetbrains.annotations.NotNull -import org.jetbrains.annotations.Nullable - -import static java.lang.Math.* - -class ReplaceOptionsAction extends SelectionAction { - interface VirtualAPI { - Suggestions suggestText(String template, List examples) - - class Suggestions { - public List choices = null - - Suggestions() {} - } - } - - def getProxy() { - return new ChatProxy( - clazz: VirtualAPI.class, - api: api, - model: AppSettingsState.instance.defaultChatModel(), - temperature: AppSettingsState.instance.temperature, - deserializerRetries: 5, - ).create() - } - @Override - String getConfig(@Nullable Project project) { - return "" - } - - @Override - String processSelection(@Nullable AnActionEvent event, @NotNull SelectionState state, @Nullable String config) { - List choices = UITools.run(event==null?null:event.project, templateText, true, true, { - String selectedText = state.selectedText - int idealLength = pow(2, 2 + ceil(log(selectedText.length()))).intValue() - int selectionStart = state.selectionOffset - String allBefore = state.entireDocument?.substring(0, selectionStart) ?: "" - int selectionEnd = state.selectionOffset + (state.selectionLength ?: 0) - String allAfter = state.entireDocument?.substring(selectionEnd, state.entireDocument.length()) ?: "" - String before = StringUtil.getSuffixForContext(allBefore, idealLength).replaceAll("\n", " ") - String after = StringUtil.getPrefixForContext(allAfter, idealLength).replaceAll("\n", " ") - return proxy.suggestText( - "$before _____ $after", - [selectedText] - ).choices - }) - return choose(choices) - } - - def choose(List choices) { - return UITools.showRadioButtonDialog("Select an option to fill in the blank:", choices.toArray(CharSequence[]::new))?.toString() ?: "" - } - -} \ No newline at end of file diff --git a/src/main/resources/sources/groovy/com/github/simiacryptus/aicoder/actions/markdown/MarkdownImplementActionGroup.groovy b/src/main/resources/sources/groovy/com/github/simiacryptus/aicoder/actions/markdown/MarkdownImplementActionGroup.groovy deleted file mode 100644 index a798a177..00000000 --- a/src/main/resources/sources/groovy/com/github/simiacryptus/aicoder/actions/markdown/MarkdownImplementActionGroup.groovy +++ /dev/null @@ -1,123 +0,0 @@ -package com.github.simiacryptus.aicoder.actions.markdown - -import com.github.simiacryptus.aicoder.actions.SelectionAction -import com.github.simiacryptus.aicoder.config.AppSettingsState -import com.github.simiacryptus.aicoder.util.ComputerLanguage -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.project.Project -import com.simiacryptus.jopenai.proxy.ChatProxy - -class MarkdownImplementActionGroup extends ActionGroup { - List markdownLanguages = [ - "sql", - "java", - "asp", - "c", - "clojure", - "coffee", - "cpp", - "csharp", - "css", - "bash", - "go", - "java", - "javascript", - "less", - "make", - "matlab", - "objectivec", - "pascal", - "PHP", - "Perl", - "python", - "rust", - "scss", - "sql", - "svg", - "swift", - "ruby", - "smalltalk", - "vhdl" - ] - - void update(AnActionEvent e) { - e.presentation.setEnabledAndVisible(isEnabled(e)) - super.update(e) - } - - static boolean isEnabled(AnActionEvent e) { - def computerLanguage = ComputerLanguage.getComputerLanguage(e) - if (null == computerLanguage) return false - if (ComputerLanguage.Markdown != computerLanguage) return false - return UITools.hasSelection(e) - } - - AnAction[] getChildren(AnActionEvent e) { - if (null == e) return [] - def computerLanguage = ComputerLanguage.getComputerLanguage(e) - if (null == computerLanguage) return [] - ArrayList actions = [] - for (language in markdownLanguages) { - actions.add(new MarkdownImplementAction(language)) - } - return actions.toArray(AnAction[]::new) - } - - - static class MarkdownImplementAction extends SelectionAction { - String language - - MarkdownImplementAction(String language) { - super(true) - this.language = language - this.templatePresentation.text = language - this.templatePresentation.description = language - } - - interface ConversionAPI { - ConvertedText implement(String text, String humanLanguage, String computerLanguage) - - class ConvertedText { - public String code - public String language - - ConvertedText() { - } - } - } - - def getProxy() { - return new ChatProxy( - clazz: ConversionAPI.class, - api: api, - model: AppSettingsState.instance.defaultChatModel(), - temperature: AppSettingsState.instance.temperature, - deserializerRetries: 5, - ).create() - } - - @Override - java.lang.String getConfig(Project project) { - return "" - } - - - String processSelection(SelectionState state, String config) { - def code = proxy.implement(state.selectedText ?: "", "autodetect", language).code ?: "" - return """ - | - |```$language - |$code - |``` - | - |""".stripMargin() - } - - boolean isLanguageSupported(ComputerLanguage computerLanguage) { - return ComputerLanguage.Markdown == computerLanguage - } - } -} \ No newline at end of file diff --git a/src/main/resources/sources/kt/com/github/simiacryptus/aicoder/actions/BaseAction.kt b/src/main/resources/sources/kt/com/github/simiacryptus/aicoder/actions/BaseAction.kt new file mode 100644 index 00000000..97e40b5c --- /dev/null +++ b/src/main/resources/sources/kt/com/github/simiacryptus/aicoder/actions/BaseAction.kt @@ -0,0 +1,44 @@ +package com.github.simiacryptus.aicoder.actions + +import com.github.simiacryptus.aicoder.util.IdeaOpenAIClient +import com.github.simiacryptus.aicoder.util.UITools +import com.intellij.openapi.actionSystem.AnAction +import com.intellij.openapi.actionSystem.AnActionEvent +import com.simiacryptus.jopenai.OpenAIClient +import org.slf4j.LoggerFactory +import javax.swing.Icon + +abstract class BaseAction( + name: String? = null, + description: String? = null, + icon: Icon? = null, +) : AnAction(name, description, icon) { + + private val log by lazy { LoggerFactory.getLogger(javaClass) } + //override fun getActionUpdateThread(): ActionUpdateThread = ActionUpdateThread.BGT + + val api: OpenAIClient + get() = IdeaOpenAIClient.api + + final override fun update(event: AnActionEvent) { + event.presentation.isEnabledAndVisible = isEnabled(event) + super.update(event) + } + + abstract fun handle(e: AnActionEvent) + + + final override fun actionPerformed(e: AnActionEvent) { + UITools.logAction(""" + |Action: ${javaClass.simpleName} + """.trimMargin().trim()) + IdeaOpenAIClient.lastEvent = e + try { + handle(e) + } catch (e: Throwable) { + UITools.error(log, "Error in Action ${javaClass.simpleName}", e) + } + } + + open fun isEnabled(event: AnActionEvent): Boolean = true +} diff --git a/src/main/resources/sources/kt/com/github/simiacryptus/aicoder/actions/FileContextAction.kt b/src/main/resources/sources/kt/com/github/simiacryptus/aicoder/actions/FileContextAction.kt new file mode 100644 index 00000000..d2202ba5 --- /dev/null +++ b/src/main/resources/sources/kt/com/github/simiacryptus/aicoder/actions/FileContextAction.kt @@ -0,0 +1,83 @@ +package com.github.simiacryptus.aicoder.actions + +import com.github.simiacryptus.aicoder.config.AppSettingsState +import com.github.simiacryptus.aicoder.util.UITools +import com.intellij.openapi.actionSystem.AnActionEvent +import com.intellij.openapi.fileEditor.FileEditorManager +import com.intellij.openapi.project.Project +import com.intellij.openapi.vfs.LocalFileSystem +import com.intellij.openapi.vfs.VirtualFile +import org.slf4j.LoggerFactory +import java.io.File + +abstract class FileContextAction( + private val supportsFiles: Boolean = true, + private val supportsFolders: Boolean = true, +) : BaseAction() { + + data class SelectionState( + val selectedFile: File, + val projectRoot: File, + ) + + abstract fun processSelection(state: SelectionState, config: T?): Array + + final override fun handle(e: AnActionEvent) { + val config = getConfig(e.project) + val virtualFile = UITools.getSelectedFile(e) ?: UITools.getSelectedFolder(e) ?: return + val project = e.project ?: return + val projectRoot = File(project.basePath!!).toPath() + Thread { + try { + UITools.redoableTask(e) { + UITools.run(e.project, templateText!!, true) { + val newFiles = try { + processSelection( + SelectionState( + selectedFile = virtualFile.toNioPath().toFile(), + projectRoot = projectRoot.toFile(), + ), config + ) + } finally { + if (it.isCanceled) throw InterruptedException() + } + UITools.writeableFn(e) { + val files = newFiles.map { file -> + val localFileSystem = LocalFileSystem.getInstance() + localFileSystem.findFileByIoFile(file.parentFile)?.refresh(false, true) + val generatedFile = localFileSystem.findFileByIoFile(file) + if (generatedFile == null) { + log.warn("Generated file not found: ${file.path}") + } else { + generatedFile.refresh(false, false) + FileEditorManager.getInstance(project).openFile(generatedFile, true) + } + generatedFile + }.filter { it != null }.toTypedArray() + Runnable { + files.forEach { it?.delete(this@FileContextAction) } + } + } + } + } + } catch (e: Throwable) { + UITools.error(log, "Error in ${javaClass.simpleName}", e) + } + }.start() + } + + open fun getConfig(project: Project?): T? = null + + private var isDevAction = false + override fun isEnabled(event: AnActionEvent): Boolean { + if (!super.isEnabled(event)) return false + if (isDevAction && !AppSettingsState.instance.devActions) return false + val virtualFile = UITools.getSelectedFile(event) ?: UITools.getSelectedFolder(event) ?: return false + return if (virtualFile.isDirectory) supportsFolders else supportsFiles + } + + companion object { + private val log = LoggerFactory.getLogger(FileContextAction::class.java) + } + +} \ No newline at end of file diff --git a/src/main/resources/sources/kt/com/github/simiacryptus/aicoder/actions/SelectionAction.kt b/src/main/resources/sources/kt/com/github/simiacryptus/aicoder/actions/SelectionAction.kt new file mode 100644 index 00000000..5b51e94a --- /dev/null +++ b/src/main/resources/sources/kt/com/github/simiacryptus/aicoder/actions/SelectionAction.kt @@ -0,0 +1,201 @@ +package com.github.simiacryptus.aicoder.actions + +import com.github.simiacryptus.aicoder.util.ComputerLanguage +import com.github.simiacryptus.aicoder.util.UITools +import com.intellij.openapi.actionSystem.AnActionEvent +import com.intellij.openapi.actionSystem.CommonDataKeys +import com.intellij.openapi.command.WriteCommandAction +import com.intellij.openapi.editor.Editor +import com.intellij.openapi.editor.RangeMarker +import com.intellij.openapi.fileEditor.FileDocumentManager +import com.intellij.openapi.project.Project +import com.intellij.openapi.util.NlsSafe +import com.intellij.psi.PsiElement +import com.intellij.psi.PsiFile +import com.intellij.psi.PsiManager +import com.intellij.psi.PsiRecursiveElementVisitor + +abstract class SelectionAction( + private val requiresSelection: Boolean = true +) : BaseAction() { + + open fun getConfig(project: Project?): T? = null + + private fun retarget( + editorState: EditorState, + selectedText: @NlsSafe String?, + selectionStart: Int, + selectionEnd: Int + ): Pair? { + if (selectedText.isNullOrEmpty()) { + var (start, end) = defaultSelection(editorState, selectionStart) + if (start >= end && requiresSelection) return null + start = start.coerceAtLeast(0) + end = end.coerceAtLeast(start).coerceAtMost(editorState.text.length - 1) + return Pair(start, end) + } else { + var (start, end) = editSelection(editorState, selectionStart, selectionEnd) + if (start >= end && requiresSelection) return null + start = start.coerceAtLeast(0) + end = end.coerceAtLeast(start).coerceAtMost(editorState.text.length - 1) + return Pair(start, end) + } + } + + final override fun handle(e: AnActionEvent) { + val editor = e.getData(CommonDataKeys.EDITOR) ?: return + val config = getConfig(e.project) + val indent = UITools.getIndent(e) + val caretModel = editor.caretModel + val primaryCaret = caretModel.primaryCaret + var selectionStart = primaryCaret.selectionStart + var selectionEnd = primaryCaret.selectionEnd + var selectedText = primaryCaret.selectedText + val editorState = editorState(editor) + val (start, end) = retarget(editorState, selectedText, selectionStart, selectionEnd) ?: return + selectedText = editorState.text.substring(start, end) + selectionEnd = end + selectionStart = start + + UITools.redoableTask(e) { + val document = e.getData(CommonDataKeys.EDITOR)?.document + var rangeMarker: RangeMarker? = null + WriteCommandAction.runWriteCommandAction(e.project) { + rangeMarker = document?.createGuardedBlock(selectionStart, selectionEnd) + } + + val newText = try { + processSelection( + event = e, + SelectionState( + selectedText = selectedText, + selectionOffset = selectionStart, + selectionLength = selectionEnd - selectionStart, + entireDocument = editor.document.text, + language = ComputerLanguage.getComputerLanguage(e), + indent = indent, + contextRanges = editorState.contextRanges, + psiFile = editorState.psiFile, + project = e.project + ), + config = config + ) + } finally { + if(null != rangeMarker) + WriteCommandAction.runWriteCommandAction(e.project) { + document?.removeGuardedBlock(rangeMarker!!) + } + } + UITools.writeableFn(e) { + UITools.replaceString(editor.document, selectionStart, selectionEnd, newText) + } + } + } + + data class EditorState( + val text: @NlsSafe String, + val cursorOffset: Int, + val line: Pair, + val psiFile: PsiFile?, + val contextRanges: Array = arrayOf(), + ) + + data class ContextRange( + val name: String, + val start: Int, + val end: Int + ) { + fun length() = end - start + fun range() = Pair(start, end) + + fun subString(text: String) = text.substring(start, end) + } + + private fun editorState(editor: Editor): EditorState { + val document = editor.document + val lineNumber = document.getLineNumber(editor.caretModel.offset) + val virtualFile = FileDocumentManager.getInstance().getFile(editor.document) + val psiFile = if (virtualFile == null) { + null + } else { + PsiManager.getInstance(editor.project!!).findFile(virtualFile) + } + return EditorState( + text = document.text, + cursorOffset = editor.caretModel.offset, + line = Pair(document.getLineStartOffset(lineNumber), document.getLineEndOffset(lineNumber)), + psiFile = psiFile, + contextRanges = contextRanges(psiFile, editor) + ) + } + + private fun contextRanges( + psiFile: PsiFile?, + editor: Editor + ): Array { + val contextRanges = mutableListOf() + psiFile?.acceptChildren(object : PsiRecursiveElementVisitor() { + override fun visitElement(element: PsiElement) { + val start = element.textRange.startOffset + val end = element.textRange.endOffset + if (start <= editor.caretModel.offset && end >= editor.caretModel.offset) { + contextRanges.add(ContextRange(element.javaClass.simpleName, start, end)) + } + super.visitElement(element) + } + }) + return contextRanges.toTypedArray() + } + + + override fun isEnabled(event: AnActionEvent): Boolean { + if (!super.isEnabled(event)) return false + val editor = event.getData(CommonDataKeys.EDITOR) ?: return false + if (requiresSelection) { + if (editor.caretModel.primaryCaret.selectedText.isNullOrEmpty()) { + val editorState = editorState(editor) + val (start, end) = defaultSelection(editorState, editorState.cursorOffset) + if (start >= end) return false + } + } + val computerLanguage = ComputerLanguage.getComputerLanguage(event) + return isLanguageSupported(computerLanguage) + } + + data class SelectionState( + val selectedText: String? = null, + val selectionOffset: Int = 0, + val selectionLength: Int? = null, + val entireDocument: String? = null, + val language: ComputerLanguage? = null, + val indent: CharSequence? = null, + val contextRanges: Array = arrayOf(), + val psiFile: PsiFile?, + val project: Project? + ) + + open fun isLanguageSupported(computerLanguage: ComputerLanguage?): Boolean { + computerLanguage ?: return false + return true + } + + open fun defaultSelection(editorState: EditorState, offset: Int) = editorState.line + + open fun editSelection(state: EditorState, start: Int, end: Int) = Pair(start, end) + + + open fun processSelection( + event: AnActionEvent?, + selectionState: SelectionState, + config: T? + ): String { + return UITools.run(event?.project, templateText ?: "", true) { + processSelection(selectionState, config) + } + } + + open fun processSelection(state: SelectionState, config: T?): String { + throw NotImplementedError() + } + +} diff --git a/src/main/resources/sources/kt/com/github/simiacryptus/aicoder/actions/code/CommentsAction.kt b/src/main/resources/sources/kt/com/github/simiacryptus/aicoder/actions/code/CommentsAction.kt new file mode 100644 index 00000000..1793a0a2 --- /dev/null +++ b/src/main/resources/sources/kt/com/github/simiacryptus/aicoder/actions/code/CommentsAction.kt @@ -0,0 +1,47 @@ +package com.github.simiacryptus.aicoder.actions.code + +import com.github.simiacryptus.aicoder.actions.SelectionAction +import com.github.simiacryptus.aicoder.config.AppSettingsState +import com.github.simiacryptus.aicoder.util.ComputerLanguage +import com.intellij.openapi.project.Project +import com.simiacryptus.jopenai.proxy.ChatProxy + +class CommentsAction : SelectionAction() { + + override fun getConfig(project: Project?): String { + return "" + } + + override fun isLanguageSupported(computerLanguage: ComputerLanguage?): Boolean { + return computerLanguage != null && computerLanguage != ComputerLanguage.Text + } + + override fun processSelection(state: SelectionState, config: String?): String { + return ChatProxy( + clazz = CommentsAction_VirtualAPI::class.java, + api = api, + temperature = AppSettingsState.instance.temperature, + model = AppSettingsState.instance.defaultChatModel(), + deserializerRetries = 5 + ).create().editCode( + state.selectedText ?: "", + "Add comments to each line explaining the code", + state.language.toString(), + AppSettingsState.instance.humanLanguage + ).code ?: "" + } + + interface CommentsAction_VirtualAPI { + fun editCode( + code: String, + operations: String, + computerLanguage: String, + humanLanguage: String + ): CommentsAction_ConvertedText + + class CommentsAction_ConvertedText { + var code: String? = null + var language: String? = null + } + } +} \ No newline at end of file diff --git a/src/main/resources/sources/kt/com/github/simiacryptus/aicoder/actions/code/CustomEditAction.kt b/src/main/resources/sources/kt/com/github/simiacryptus/aicoder/actions/code/CustomEditAction.kt new file mode 100644 index 00000000..53010d4b --- /dev/null +++ b/src/main/resources/sources/kt/com/github/simiacryptus/aicoder/actions/code/CustomEditAction.kt @@ -0,0 +1,71 @@ +package com.github.simiacryptus.aicoder.actions.code + +import com.github.simiacryptus.aicoder.actions.SelectionAction +import com.github.simiacryptus.aicoder.config.AppSettingsState +import com.github.simiacryptus.aicoder.util.UITools +import com.intellij.openapi.project.Project +import com.simiacryptus.jopenai.proxy.ChatProxy +import javax.swing.JOptionPane + +open class CustomEditAction : SelectionAction() { + + interface VirtualAPI { + fun editCode( + code: String, + operation: String, + computerLanguage: String, + humanLanguage: String + ): EditedText + + data class EditedText( + var code: String? = null, + var language: String? = null + ) + } + + val proxy: VirtualAPI get() { + val chatProxy = ChatProxy( + clazz = VirtualAPI::class.java, + api = api, + temperature = AppSettingsState.instance.temperature, + model = AppSettingsState.instance.defaultChatModel(), + ) + chatProxy.addExample( + VirtualAPI.EditedText( + """ + // Print Hello, World! to the console + println("Hello, World!") + """.trimIndent(), + "java" + ) + ) { + it.editCode( + """println("Hello, World!")""", + "Add code comments", + "java", + "English" + ) + } + return chatProxy.create() + } + + override fun getConfig(project: Project?): String { + return UITools.showInputDialog( + null, "Instruction:", "Edit Code", JOptionPane.QUESTION_MESSAGE + //, AppSettingsState.instance.getRecentCommands("customEdits").mostRecentHistory + ) as String? ?: "" + } + + override fun processSelection(state: SelectionState, instruction: String?): String { + if (instruction == null || instruction.isBlank()) return state.selectedText ?: "" + val settings = AppSettingsState.instance + val outputHumanLanguage = AppSettingsState.instance.humanLanguage + settings.getRecentCommands("customEdits").addInstructionToHistory(instruction) + return proxy.editCode( + state.selectedText ?: "", + instruction ?: "", + state.language?.name ?: "", + outputHumanLanguage + ).code ?: state.selectedText ?: "" + } +} \ No newline at end of file diff --git a/src/main/resources/sources/kt/com/github/simiacryptus/aicoder/actions/code/DescribeAction.kt b/src/main/resources/sources/kt/com/github/simiacryptus/aicoder/actions/code/DescribeAction.kt new file mode 100644 index 00000000..65e8ce1b --- /dev/null +++ b/src/main/resources/sources/kt/com/github/simiacryptus/aicoder/actions/code/DescribeAction.kt @@ -0,0 +1,59 @@ +package com.github.simiacryptus.aicoder.actions.code + +import com.github.simiacryptus.aicoder.actions.SelectionAction +import com.github.simiacryptus.aicoder.config.AppSettingsState +import com.github.simiacryptus.aicoder.util.IndentedText +import com.intellij.openapi.project.Project +import com.simiacryptus.jopenai.proxy.ChatProxy +import com.simiacryptus.jopenai.util.StringUtil + +class DescribeAction : SelectionAction() { + + interface DescribeAction_VirtualAPI { + fun describeCode( + code: String, + computerLanguage: String, + humanLanguage: String + ): DescribeAction_ConvertedText + + class DescribeAction_ConvertedText { + var text: String? = null + var language: String? = null + } + } + + private val proxy: DescribeAction_VirtualAPI + get() = ChatProxy( + clazz = DescribeAction_VirtualAPI::class.java, + api = api, + temperature = AppSettingsState.instance.temperature, + model = AppSettingsState.instance.defaultChatModel(), + deserializerRetries = 5 + ).create() + + override fun getConfig(project: Project?): String { + return "" + } + + override fun processSelection(state: SelectionState, config: String?): String { + val description = proxy.describeCode( + IndentedText.fromString(state.selectedText).textBlock.toString().trim(), + state.language?.name ?: "", + AppSettingsState.instance.humanLanguage + ).text ?: "" + val wrapping = StringUtil.lineWrapping(description.trim(), 120) + val numberOfLines = wrapping.trim().split("\n").reversed().dropWhile { it.isEmpty() }.size + val commentStyle = if (numberOfLines == 1) { + state.language?.lineComment + } else { + state.language?.blockComment + } + return buildString { + append(state.indent) + append(commentStyle?.fromString(wrapping)?.withIndent(state.indent) ?: wrapping) + append("\n") + append(state.indent) + append(state.selectedText) + } + } +} \ No newline at end of file diff --git a/src/main/resources/sources/kt/com/github/simiacryptus/aicoder/actions/code/DocAction.kt b/src/main/resources/sources/kt/com/github/simiacryptus/aicoder/actions/code/DocAction.kt new file mode 100644 index 00000000..568684dc --- /dev/null +++ b/src/main/resources/sources/kt/com/github/simiacryptus/aicoder/actions/code/DocAction.kt @@ -0,0 +1,89 @@ +package com.github.simiacryptus.aicoder.actions.code + +import com.github.simiacryptus.aicoder.actions.SelectionAction +import com.github.simiacryptus.aicoder.config.AppSettingsState +import com.github.simiacryptus.aicoder.util.ComputerLanguage +import com.github.simiacryptus.aicoder.util.IndentedText +import com.github.simiacryptus.aicoder.util.psi.PsiUtil +import com.intellij.openapi.project.Project +import com.simiacryptus.jopenai.proxy.ChatProxy + +class DocAction : SelectionAction() { + + interface DocAction_VirtualAPI { + fun processCode( + code: String, + operation: String, + computerLanguage: String, + humanLanguage: String + ): DocAction_ConvertedText + + class DocAction_ConvertedText { + var text: String? = null + var language: String? = null + } + } + + private val proxy: DocAction_VirtualAPI by lazy { + val chatProxy = ChatProxy( + clazz = DocAction_VirtualAPI::class.java, + api = api, + model = AppSettingsState.instance.defaultChatModel(), + temperature = AppSettingsState.instance.temperature, + deserializerRetries = 5 + ) + chatProxy.addExample( + DocAction_VirtualAPI.DocAction_ConvertedText().apply { + text = """ + /** + * Prints "Hello, world!" to the console + */ + """.trimIndent() + language = "English" + } + ) { + it.processCode( + """ + fun hello() { + println("Hello, world!") + } + """.trimIndent(), + "Write detailed KDoc prefix for code block", + "Kotlin", + "English" + ) + } + chatProxy.create() + } + + override fun getConfig(project: Project?): String { + return "" + } + + override fun processSelection(state: SelectionState, config: String?): String { + val code = state.selectedText + val indentedInput = IndentedText.fromString(code.toString()) + val docString = proxy.processCode( + indentedInput.textBlock.toString(), + "Write detailed " + (state.language?.docStyle ?: "documentation") + " prefix for code block", + state.language?.name ?: "", + AppSettingsState.instance.humanLanguage + ).text ?: "" + return docString + code + } + + override fun isLanguageSupported(computerLanguage: ComputerLanguage?): Boolean { + if (computerLanguage == ComputerLanguage.Text) return false + if (computerLanguage?.docStyle == null) return false + if (computerLanguage.docStyle.isBlank()) return false + return true + } + + override fun editSelection(state: EditorState, start: Int, end: Int): Pair { + if (state.psiFile == null) return super.editSelection(state, start, end) + val codeBlock = PsiUtil.getCodeElement(state.psiFile, start, end) + if (codeBlock == null) return super.editSelection(state, start, end) + val textRange = codeBlock.textRange + return Pair(textRange.startOffset, textRange.endOffset) + } +} \ No newline at end of file diff --git a/src/main/resources/sources/kt/com/github/simiacryptus/aicoder/actions/code/ImplementStubAction.kt b/src/main/resources/sources/kt/com/github/simiacryptus/aicoder/actions/code/ImplementStubAction.kt new file mode 100644 index 00000000..5fca34ce --- /dev/null +++ b/src/main/resources/sources/kt/com/github/simiacryptus/aicoder/actions/code/ImplementStubAction.kt @@ -0,0 +1,80 @@ +package com.github.simiacryptus.aicoder.actions.code + +import com.github.simiacryptus.aicoder.actions.SelectionAction +import com.github.simiacryptus.aicoder.config.AppSettingsState +import com.github.simiacryptus.aicoder.util.ComputerLanguage +import com.github.simiacryptus.aicoder.util.psi.PsiUtil +import com.intellij.openapi.project.Project +import com.simiacryptus.jopenai.proxy.ChatProxy +import com.simiacryptus.jopenai.util.StringUtil +import java.util.* + +class ImplementStubAction : SelectionAction() { + + interface VirtualAPI { + fun editCode( + code: String, + operation: String, + computerLanguage: String, + humanLanguage: String + ): ConvertedText + + class ConvertedText { + var code: String? = null + var language: String? = null + } + } + + private fun getProxy(): VirtualAPI { + return ChatProxy( + clazz = VirtualAPI::class.java, + api = api, + model = AppSettingsState.instance.defaultChatModel(), + temperature = AppSettingsState.instance.temperature, + deserializerRetries = 5 + ).create() + } + + override fun isLanguageSupported(computerLanguage: ComputerLanguage?): Boolean { + if (computerLanguage == null) return false + return computerLanguage != ComputerLanguage.Text + } + + override fun defaultSelection(editorState: EditorState, offset: Int): Pair { + val codeRanges = editorState.contextRanges.filter { PsiUtil.matchesType(it.name, PsiUtil.ELEMENTS_CODE) } + if (codeRanges.isEmpty()) return editorState.line + return codeRanges.minByOrNull { it.length() }?.range() ?: editorState.line + } + + override fun getConfig(project: Project?): String { + return "" + } + + override fun processSelection(state: SelectionState, config: String?): String { + val code = state.selectedText ?: "" + val settings = AppSettingsState.instance + val outputHumanLanguage = settings.humanLanguage + val computerLanguage = state.language + + val codeContext = state.contextRanges.filter { + PsiUtil.matchesType( + it.name, + PsiUtil.ELEMENTS_CODE + ) + } + var smallestIntersectingMethod = "" + if (codeContext.isNotEmpty()) smallestIntersectingMethod = codeContext.minByOrNull { it.length() }?.subString(state.entireDocument ?: "") ?: "" + + var declaration = code + declaration = StringUtil.stripSuffix(declaration.trim(), smallestIntersectingMethod) + declaration = declaration.trim() + + return getProxy().editCode( + declaration, + "Implement Stub", + computerLanguage?.name?.lowercase(Locale.ROOT) ?: "", + outputHumanLanguage + ).code ?: "" + } + +} \ No newline at end of file diff --git a/src/main/resources/sources/kt/com/github/simiacryptus/aicoder/actions/code/InsertImplementationAction.kt b/src/main/resources/sources/kt/com/github/simiacryptus/aicoder/actions/code/InsertImplementationAction.kt new file mode 100644 index 00000000..8dec68de --- /dev/null +++ b/src/main/resources/sources/kt/com/github/simiacryptus/aicoder/actions/code/InsertImplementationAction.kt @@ -0,0 +1,127 @@ +package com.github.simiacryptus.aicoder.actions.code + +import com.github.simiacryptus.aicoder.actions.SelectionAction +import com.github.simiacryptus.aicoder.config.AppSettingsState +import com.github.simiacryptus.aicoder.util.ComputerLanguage +import com.github.simiacryptus.aicoder.util.TextBlock +import com.github.simiacryptus.aicoder.util.UITools +import com.github.simiacryptus.aicoder.util.psi.PsiClassContext +import com.github.simiacryptus.aicoder.util.psi.PsiUtil +import com.intellij.openapi.application.runReadAction +import com.intellij.openapi.project.Project +import com.simiacryptus.jopenai.proxy.ChatProxy + +class InsertImplementationAction : SelectionAction() { + + interface VirtualAPI { + fun implementCode( + specification: String, + prefix: String, + computerLanguage: String, + humanLanguage: String + ): ConvertedText + + class ConvertedText { + var code: String? = null + var language: String? = null + } + } + + private fun getProxy(): VirtualAPI { + return ChatProxy( + clazz = VirtualAPI::class.java, + api = api, + model = AppSettingsState.instance.defaultChatModel(), + temperature = AppSettingsState.instance.temperature, + deserializerRetries = 5 + ).create() + } + + override fun getConfig(project: Project?): String { + return "" + } + + override fun defaultSelection(editorState: EditorState, offset: Int): Pair { + val foundItem = editorState.contextRanges.filter { + PsiUtil.matchesType( + it.name, + PsiUtil.ELEMENTS_COMMENTS + ) + }.minByOrNull { it.length() } + return foundItem?.range() ?: editorState.line + } + + override fun editSelection(state: EditorState, start: Int, end: Int): Pair { + val foundItem = state.contextRanges.filter { + PsiUtil.matchesType( + it.name, + PsiUtil.ELEMENTS_COMMENTS + ) + }.minByOrNull { it.length() } + return foundItem?.range() ?: Pair(start, end) + } + + override fun processSelection(state: SelectionState, config: String?): String { + val humanLanguage = AppSettingsState.instance.humanLanguage + val computerLanguage = state.language + val psiClassContextActionParams = getPsiClassContextActionParams(state) + val selectedText = state.selectedText ?: "" + + val comment = psiClassContextActionParams.largestIntersectingComment + var instruct = comment?.subString(state.entireDocument ?: "")?.trim() ?: selectedText + if (selectedText.split(" ").dropWhile { it.isEmpty() }.size > 4) { + instruct = selectedText.trim() + } + val fromString: TextBlock? = computerLanguage?.getCommentModel(instruct)?.fromString(instruct) + val specification = fromString?.rawString()?.map { it.toString().trim() } + ?.filter { it.isNotEmpty() }?.reduce { a, b -> "$a $b" } ?: return selectedText + val code = if (state.psiFile != null) { + UITools.run(state.project, "Insert Implementation", true, true) { + val psiClassContext = runReadAction { + PsiClassContext.getContext( + state.psiFile, + psiClassContextActionParams.selectionStart, + psiClassContextActionParams.selectionEnd, + computerLanguage + ).toString() + } + getProxy().implementCode( + specification, + psiClassContext, + computerLanguage.name, + humanLanguage + ).code + } + } else { + getProxy().implementCode( + specification, + "", + computerLanguage.name, + humanLanguage + ).code + } + return if (code != null) "$selectedText\n${state.indent}$code" else selectedText + } + + private fun getPsiClassContextActionParams(state: SelectionState): PsiClassContextActionParams { + val selectionStart = state.selectionOffset + return PsiClassContextActionParams( + selectionStart, + selectionStart + (state.selectionLength ?: 0), + state.contextRanges.find { PsiUtil.matchesType(it.name, PsiUtil.ELEMENTS_COMMENTS) } + ) + } + + override fun isLanguageSupported(computerLanguage: ComputerLanguage?): Boolean { + if (computerLanguage == null || computerLanguage == ComputerLanguage.Text || computerLanguage == ComputerLanguage.Markdown) { + return false + } + return super.isLanguageSupported(computerLanguage) + } + + private class PsiClassContextActionParams( + val selectionStart: Int, + val selectionEnd: Int, + val largestIntersectingComment: SelectionAction.ContextRange? + ) +} \ No newline at end of file diff --git a/src/main/resources/sources/kt/com/github/simiacryptus/aicoder/actions/code/PasteAction.kt b/src/main/resources/sources/kt/com/github/simiacryptus/aicoder/actions/code/PasteAction.kt new file mode 100644 index 00000000..4ba3108b --- /dev/null +++ b/src/main/resources/sources/kt/com/github/simiacryptus/aicoder/actions/code/PasteAction.kt @@ -0,0 +1,55 @@ +package com.github.simiacryptus.aicoder.actions.code + +import com.github.simiacryptus.aicoder.actions.SelectionAction +import com.github.simiacryptus.aicoder.config.AppSettingsState +import com.github.simiacryptus.aicoder.util.ComputerLanguage +import com.intellij.openapi.actionSystem.AnActionEvent +import com.intellij.openapi.project.Project +import com.simiacryptus.jopenai.proxy.ChatProxy +import java.awt.Toolkit +import java.awt.datatransfer.DataFlavor + +class PasteAction : SelectionAction(false) { + + interface VirtualAPI { + fun convert(text: String, from_language: String, to_language: String): ConvertedText + + class ConvertedText { + var code: String? = null + var language: String? = null + } + } + + override fun getConfig(project: Project?): String { + return "" + } + + override fun processSelection(state: SelectionState, config: String?): String { + return ChatProxy( + VirtualAPI::class.java, + api, + AppSettingsState.instance.defaultChatModel(), + AppSettingsState.instance.temperature, + ).create().convert( + getClipboard().toString().trim(), + "autodetect", + state.language?.name ?: "" + ).code ?: "" + } + + override fun isLanguageSupported(computerLanguage: ComputerLanguage?): Boolean { + if (computerLanguage == null) return false + return computerLanguage != ComputerLanguage.Text + } + + override fun isEnabled(event: AnActionEvent): Boolean { + if (getClipboard() == null) return false + return super.isEnabled(event) + } + + private fun getClipboard(): Any? { + val contents = Toolkit.getDefaultToolkit().systemClipboard.getContents(null) + return if (contents?.isDataFlavorSupported(DataFlavor.stringFlavor) == true) contents.getTransferData(DataFlavor.stringFlavor) + else null + } +} \ No newline at end of file diff --git a/src/main/resources/sources/kt/com/github/simiacryptus/aicoder/actions/code/RecentCodeEditsAction.kt b/src/main/resources/sources/kt/com/github/simiacryptus/aicoder/actions/code/RecentCodeEditsAction.kt new file mode 100644 index 00000000..b4a17b41 --- /dev/null +++ b/src/main/resources/sources/kt/com/github/simiacryptus/aicoder/actions/code/RecentCodeEditsAction.kt @@ -0,0 +1,43 @@ +package com.github.simiacryptus.aicoder.actions.code + +import com.github.simiacryptus.aicoder.config.AppSettingsState +import com.github.simiacryptus.aicoder.util.ComputerLanguage +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.project.Project + +class RecentCodeEditsAction : ActionGroup() { + override fun update(e: AnActionEvent) { + e.presentation.isEnabledAndVisible = isEnabled(e) + super.update(e) + } + + override fun getChildren(e: AnActionEvent?): Array { + if (e == null) return emptyArray() + val children = mutableListOf() + for ((instruction, _) in AppSettingsState.instance.getRecentCommands("customEdits").mostUsedHistory) { + val id = children.size + 1 + val text = if (id < 10) "_$id: $instruction" else "$id: $instruction" + val element = object : CustomEditAction() { + override fun getConfig(project: Project?): String { + return instruction + } + } + element.templatePresentation.text = text + element.templatePresentation.description = instruction + element.templatePresentation.icon = null + children.add(element) + } + return children.toTypedArray() + } + + companion object { + fun isEnabled(e: AnActionEvent): Boolean { + if (!UITools.hasSelection(e)) return false + val computerLanguage = ComputerLanguage.getComputerLanguage(e) + return computerLanguage != ComputerLanguage.Text + } + } +} \ No newline at end of file diff --git a/src/main/resources/sources/kt/com/github/simiacryptus/aicoder/actions/code/RenameVariablesAction.kt b/src/main/resources/sources/kt/com/github/simiacryptus/aicoder/actions/code/RenameVariablesAction.kt new file mode 100644 index 00000000..e393e513 --- /dev/null +++ b/src/main/resources/sources/kt/com/github/simiacryptus/aicoder/actions/code/RenameVariablesAction.kt @@ -0,0 +1,78 @@ +package com.github.simiacryptus.aicoder.actions.code + +import com.github.simiacryptus.aicoder.actions.SelectionAction +import com.github.simiacryptus.aicoder.config.AppSettingsState +import com.github.simiacryptus.aicoder.util.ComputerLanguage +import com.github.simiacryptus.aicoder.util.UITools +import com.intellij.openapi.actionSystem.AnActionEvent +import com.intellij.openapi.project.Project +import com.simiacryptus.jopenai.proxy.ChatProxy + +class RenameVariablesAction : SelectionAction() { + + interface RenameAPI { + fun suggestRenames( + code: String, + computerLanguage: String, + humanLanguage: String + ): SuggestionResponse + + class SuggestionResponse { + var suggestions: MutableList = mutableListOf() + + class Suggestion { + var originalName: String? = null + var suggestedName: String? = null + } + } + } + + val proxy: RenameAPI get() { + return ChatProxy( + clazz = RenameAPI::class.java, + api = api, + model = AppSettingsState.instance.defaultChatModel(), + temperature = AppSettingsState.instance.temperature, + deserializerRetries = 5 + ).create() + } + + override fun getConfig(project: Project?): String { + return "" + } + + override fun processSelection(event: AnActionEvent?, state: SelectionState, config: String?): String { + val renameSuggestions = UITools.run(event?.project, templateText, true, true) { + proxy + .suggestRenames( + state.selectedText ?: "", + state.language?.name ?: "", + AppSettingsState.instance.humanLanguage + ) + .suggestions + .filter { it.originalName != null && it.suggestedName != null } + .associate { it.originalName!! to it.suggestedName!! } + } + val selectedSuggestions = choose(renameSuggestions) + return UITools.run(event?.project, templateText, true, true) { + var selectedText = state.selectedText + val filter = renameSuggestions.filter { it.key in selectedSuggestions } + filter.forEach { (key, value) -> + selectedText = selectedText?.replace(key, value) + } + selectedText ?: "" + } + } + + private fun choose(renameSuggestions: Map): Set { + return UITools.showCheckboxDialog( + "Select which items to rename", + renameSuggestions.keys.toTypedArray(), + renameSuggestions.map { (key, value) -> "$key -> $value" }.toTypedArray() + ).toSet() + } + + override fun isLanguageSupported(computerLanguage: ComputerLanguage?): Boolean { + return computerLanguage != ComputerLanguage.Text + } +} \ No newline at end of file diff --git a/src/main/resources/sources/kt/com/github/simiacryptus/aicoder/actions/dev/AppServer.kt b/src/main/resources/sources/kt/com/github/simiacryptus/aicoder/actions/dev/AppServer.kt new file mode 100644 index 00000000..df37403b --- /dev/null +++ b/src/main/resources/sources/kt/com/github/simiacryptus/aicoder/actions/dev/AppServer.kt @@ -0,0 +1,116 @@ +package com.github.simiacryptus.aicoder.actions.dev + +import com.github.simiacryptus.aicoder.config.AppSettingsState +import com.github.simiacryptus.aicoder.util.UITools +import com.intellij.openapi.progress.ProgressIndicator +import com.intellij.openapi.project.Project +import com.simiacryptus.skyenet.webui.chat.ChatServer +import org.eclipse.jetty.server.Server +import org.eclipse.jetty.server.handler.ContextHandlerCollection +import org.eclipse.jetty.webapp.WebAppContext +import org.eclipse.jetty.websocket.server.config.JettyWebSocketServletContainerInitializer +import org.slf4j.LoggerFactory +import java.net.InetSocketAddress + +class AppServer( + private val localName: String, + private val port: Int, + project: Project? +) { + + private val log by lazy { LoggerFactory.getLogger(javaClass) } + + private var domainName: String = "http://$localName:$port" + + private val contexts by lazy { + val contexts = ContextHandlerCollection() + contexts.handlers = handlers.toTypedArray() + contexts + } + + @Synchronized + fun addApp(path: String, socketServer: ChatServer) { + try { + synchronized(serverLock) { + if (server.isRunning) server.stop() // Stop the server + handlers += newWebAppContext(socketServer, path) + contexts.handlers = handlers.toTypedArray() + server.handler = contexts + server.start() // Start the server again to reflect the new context + } + } catch (e: Exception) { + log.error("Error while restarting the server with new context", e) + } + } + + private fun newWebAppContext(server: ChatServer, path: String): WebAppContext { + val context = WebAppContext() + JettyWebSocketServletContainerInitializer.configure(context, null) + context.baseResource = server.baseResource + context.classLoader = AppServer::class.java.classLoader + context.contextPath = path + context.welcomeFiles = arrayOf("index.html") + server.configure(context, baseUrl = "$domainName/$path") + return context + } + + private val handlers = arrayOf().toMutableList() + + val server by lazy { + val server = Server(InetSocketAddress(localName, port)) + server.handler = contexts + server + } + + private val serverLock = Object() + private val progressThread = Thread { + try { + UITools.run( + project, "Running CodeChat Server on $port", false + ) { + while (isRunning(it)) { + Thread.sleep(1000) + } + synchronized(serverLock) { + if (it.isCanceled) { + log.info("Server cancelled") + server.stop() + } else { + log.info("Server stopped") + } + } + } + } finally { + log.info("Stopping Server") + server.stop() + } + } + + private fun isRunning(it: ProgressIndicator) = synchronized(serverLock) { !it.isCanceled && server.isRunning } + fun start() { + server.start() + progressThread.start() + } + + companion object { + @Transient + private var server: AppServer? = null + fun getServer(project: Project?): AppServer { + if (null == server || !server!!.server.isRunning) { + server = AppServer( + AppSettingsState.instance.listeningEndpoint, + AppSettingsState.instance.listeningPort, + project + ) + server!!.start() + } + return server!! + } + + fun stop() { + server?.server?.stop() + server = null + } + } + +} \ No newline at end of file diff --git a/src/main/resources/sources/kt/com/github/simiacryptus/aicoder/actions/dev/InternalCoderAction.kt b/src/main/resources/sources/kt/com/github/simiacryptus/aicoder/actions/dev/InternalCoderAction.kt new file mode 100644 index 00000000..069e3adf --- /dev/null +++ b/src/main/resources/sources/kt/com/github/simiacryptus/aicoder/actions/dev/InternalCoderAction.kt @@ -0,0 +1,64 @@ +package com.github.simiacryptus.aicoder.actions.dev + +import com.github.simiacryptus.aicoder.actions.BaseAction +import com.github.simiacryptus.aicoder.config.AppSettingsState +import com.github.simiacryptus.aicoder.util.IdeaKotlinInterpreter +import com.intellij.openapi.actionSystem.AnActionEvent +import com.intellij.openapi.actionSystem.CommonDataKeys +import com.simiacryptus.skyenet.apps.coding.CodingApp +import com.simiacryptus.skyenet.core.platform.* +import org.slf4j.LoggerFactory +import java.awt.Desktop +import java.util.* + +class InternalCoderAction : BaseAction() { + + override fun handle(e: AnActionEvent) { + val server = AppServer.getServer(e.project) + val uuid = UUID.randomUUID().toString() + val symbols: MutableMap = mapOf( + "event" to e, + ).toMutableMap() + + // TODO: Set this up at startup and lock ApplicationServices + ApplicationServices.clientManager = object : ClientManager() { + override fun createClient(session: Session, user: User?, dataStorage: StorageInterface?) = + this@InternalCoderAction.api + } + IdeaKotlinInterpreter.project = e.getData(CommonDataKeys.PROJECT) ?: throw IllegalStateException("No project") + + e.getData(CommonDataKeys.EDITOR)?.apply { symbols["editor"] = this } + e.getData(CommonDataKeys.PSI_FILE)?.apply { symbols["file"] = this } + e.getData(CommonDataKeys.PSI_ELEMENT)?.apply { symbols["element"] = this } + e.getData(CommonDataKeys.VIRTUAL_FILE)?.apply { symbols["virtualFile"] = this } + IdeaKotlinInterpreter.project?.apply { symbols["project"] = this } + e.getData(CommonDataKeys.SYMBOLS)?.apply { symbols["symbols"] = this } + e.getData(CommonDataKeys.CARET)?.apply { symbols["psiElement"] = this } + e.getData(CommonDataKeys.CARET)?.apply { symbols["psiElement"] = this } + server.addApp( + "/$uuid", CodingApp( + "IntelliJ Internal Coding Agent", + IdeaKotlinInterpreter::class, + symbols + ) + ) + Thread { + Thread.sleep(500) + try { + Desktop.getDesktop().browse(server.server.uri.resolve("/$uuid/index.html")) + } catch (e: Throwable) { + log.warn("Error opening browser", e) + } + }.start() + } + + override fun isEnabled(event: AnActionEvent) = when { + !AppSettingsState.instance.devActions -> false + else -> true + } + + companion object { + private val log = LoggerFactory.getLogger(InternalCoderAction::class.java) + + } +} diff --git a/src/main/resources/sources/kt/com/github/simiacryptus/aicoder/actions/dev/PrintTreeAction.kt b/src/main/resources/sources/kt/com/github/simiacryptus/aicoder/actions/dev/PrintTreeAction.kt new file mode 100644 index 00000000..78e0b8a2 --- /dev/null +++ b/src/main/resources/sources/kt/com/github/simiacryptus/aicoder/actions/dev/PrintTreeAction.kt @@ -0,0 +1,30 @@ +package com.github.simiacryptus.aicoder.actions.dev + +import com.github.simiacryptus.aicoder.actions.BaseAction +import com.github.simiacryptus.aicoder.config.AppSettingsState +import com.github.simiacryptus.aicoder.util.psi.PsiUtil +import com.intellij.openapi.actionSystem.AnActionEvent +import org.slf4j.LoggerFactory + +/** + * The PrintTreeAction class is an IntelliJ action that enables developers to print the tree structure of a PsiFile. + * To use this action, first make sure that the "devActions" setting is enabled. + * Then, open the file you want to print the tree structure of. + * Finally, select the "PrintTreeAction" action from the editor context menu. + * This will print the tree structure of the file to the log. + */ +class PrintTreeAction : BaseAction() { + + + override fun handle(e: AnActionEvent) { + log.warn(PsiUtil.printTree(PsiUtil.getLargestContainedEntity(e)!!)) + } + + override fun isEnabled(event: AnActionEvent): Boolean { + return AppSettingsState.instance.devActions + } + + companion object { + private val log = LoggerFactory.getLogger(PrintTreeAction::class.java) + } +} diff --git a/src/main/resources/sources/kt/com/github/simiacryptus/aicoder/actions/generic/AnalogueFileAction.kt b/src/main/resources/sources/kt/com/github/simiacryptus/aicoder/actions/generic/AnalogueFileAction.kt new file mode 100644 index 00000000..f44665a3 --- /dev/null +++ b/src/main/resources/sources/kt/com/github/simiacryptus/aicoder/actions/generic/AnalogueFileAction.kt @@ -0,0 +1,121 @@ +package com.github.simiacryptus.aicoder.actions.generic + +import com.github.simiacryptus.aicoder.actions.FileContextAction +import com.github.simiacryptus.aicoder.config.AppSettingsState +import com.github.simiacryptus.aicoder.config.Name +import com.github.simiacryptus.aicoder.util.UITools +import com.intellij.openapi.project.Project +import com.simiacryptus.jopenai.ApiModel +import com.simiacryptus.jopenai.ApiModel.ChatMessage +import com.simiacryptus.jopenai.ApiModel.Role +import com.simiacryptus.jopenai.ClientUtil.toContentList +import org.apache.commons.io.FileUtils +import org.apache.commons.io.IOUtils +import java.io.File +import java.io.FileInputStream +import javax.swing.JTextArea + +class AnalogueFileAction : FileContextAction() { + + data class ProjectFile( + val path: String = "", + val code: String = "" + ) + + class SettingsUI { + @Name("Directive") + var directive: JTextArea = JTextArea( + """ + Create test cases + """.trimIndent(), + 3, + 120 + ) + } + + class Settings ( + var directive: String = "" + ) + + override fun getConfig(project: Project?): Settings? { + return UITools.showDialog( + project, + SettingsUI::class.java, + Settings::class.java, + "Create Analogue File" + ) + } + + override fun processSelection(state: SelectionState, config: Settings?): Array { + val analogue = generateFile( + ProjectFile( + path = state.projectRoot.toPath().relativize(state.selectedFile.toPath()).toString(), + code = IOUtils.toString(FileInputStream(state.selectedFile), "UTF-8") + ), + config?.directive ?: "" + ) + var outputPath = state.projectRoot.toPath().resolve(analogue.path) + if (outputPath.toFile().exists()) { + val extension = outputPath.toString().split(".").last() + val name = outputPath.toString().split(".").dropLast(1).joinToString(".") + val fileIndex = (1..Int.MAX_VALUE).find { + !File(state.projectRoot, "$name.$it.$extension").exists() + } + outputPath = state.projectRoot.toPath().resolve("$name.$fileIndex.$extension") + } + outputPath.parent.toFile().mkdirs() + FileUtils.write(outputPath.toFile(), analogue.code, "UTF-8") + Thread.sleep(100) + return arrayOf(outputPath.toFile()) + } + + private fun generateFile(baseFile: ProjectFile, directive: String): ProjectFile { + val model = AppSettingsState.instance.defaultChatModel() + val chatRequest = ApiModel.ChatRequest( + model = model.modelName, + temperature = AppSettingsState.instance.temperature, + messages = listOf( + ChatMessage( + Role.system, """ + You will combine natural language instructions with a user provided code example to create a new file. + Provide a new filename and the code to be written to the file. + Paths should be relative to the project root and should not exist. + Output the file path using the a line with the format "File: ". + Output the file code directly after the header line with no additional decoration. + """.trimIndent().toContentList(), null + ), + ChatMessage( + Role.user, """ + Create a new file based on the following directive: $directive + + The file should be based on `${baseFile.path}` which contains the following code: + + ``` + ${baseFile.code} + ``` + """.trimIndent().toContentList(), null + ) + + ) + ) + val response = api.chat( + chatRequest, + AppSettingsState.instance.defaultChatModel() + ).choices.first().message?.content?.trim() + var outputPath = baseFile.path + val header = response?.split("\n")?.first() + var body = response?.split("\n")?.drop(1)?.joinToString("\n")?.trim() + if (body?.contains("```") == true) { + body = body.split("```.*".toRegex()).drop(1).firstOrNull()?.trim() ?: body + } + val pathPattern = "File(?:name)?: ['\"]?([^'\"]+)['\"]?".toRegex() + val matcher = pathPattern.find(header ?: "") + if (matcher != null) { + outputPath = matcher.groupValues[1].trim() + } + return ProjectFile( + path = outputPath, + code = body ?: "" + ) + } +} diff --git a/src/main/resources/sources/kt/com/github/simiacryptus/aicoder/actions/generic/AppendAction.kt b/src/main/resources/sources/kt/com/github/simiacryptus/aicoder/actions/generic/AppendAction.kt new file mode 100644 index 00000000..7899c1fd --- /dev/null +++ b/src/main/resources/sources/kt/com/github/simiacryptus/aicoder/actions/generic/AppendAction.kt @@ -0,0 +1,29 @@ + package com.github.simiacryptus.aicoder.actions.generic + +import com.github.simiacryptus.aicoder.actions.SelectionAction +import com.github.simiacryptus.aicoder.config.AppSettingsState +import com.intellij.openapi.project.Project +import com.simiacryptus.jopenai.ApiModel.ChatMessage +import com.simiacryptus.jopenai.ApiModel.Role +import com.simiacryptus.jopenai.ClientUtil.toContentList + + class AppendAction : SelectionAction() { + override fun getConfig(project: Project?): String { + return "" + } + + override fun processSelection(state: SelectionState, config: String?): String { + val settings = AppSettingsState.instance + val request = settings.createChatRequest().copy( + temperature = settings.temperature, + messages = listOf( + ChatMessage(Role.system, "Append text to the end of the user's prompt".toContentList(), null), + ChatMessage(Role.user, state.selectedText.toString().toContentList(), null) + ), + ) + val chatResponse = api.chat(request, settings.defaultChatModel()) + val b4 = state.selectedText ?: "" + val str = chatResponse.choices[0].message?.content ?: "" + return b4 + if (str.startsWith(b4)) str.substring(b4.length) else str + } +} \ No newline at end of file diff --git a/src/main/resources/sources/kt/com/github/simiacryptus/aicoder/actions/generic/CodeChatAction.kt b/src/main/resources/sources/kt/com/github/simiacryptus/aicoder/actions/generic/CodeChatAction.kt new file mode 100644 index 00000000..7c11ba01 --- /dev/null +++ b/src/main/resources/sources/kt/com/github/simiacryptus/aicoder/actions/generic/CodeChatAction.kt @@ -0,0 +1,42 @@ +package com.github.simiacryptus.aicoder.actions.generic + +import com.github.simiacryptus.aicoder.actions.BaseAction +import com.github.simiacryptus.aicoder.actions.dev.AppServer +import com.github.simiacryptus.aicoder.config.AppSettingsState +import com.github.simiacryptus.aicoder.util.ComputerLanguage +import com.intellij.openapi.actionSystem.AnActionEvent +import com.intellij.openapi.actionSystem.CommonDataKeys +import com.simiacryptus.skyenet.webui.chat.CodeChatServer +import org.slf4j.LoggerFactory +import java.awt.Desktop +import java.util.* + +class CodeChatAction : BaseAction() { + + override fun handle(e: AnActionEvent) { + val editor = e.getData(CommonDataKeys.EDITOR) ?: return + val caretModel = editor.caretModel + val primaryCaret = caretModel.primaryCaret + val selectedText = primaryCaret.selectedText ?: editor.document.text + val language = ComputerLanguage.getComputerLanguage(e)?.name ?: return + val server = AppServer.getServer(e.project) + val uuid = UUID.randomUUID().toString() + server.addApp("/$uuid", CodeChatServer(language, selectedText, api = api, model = AppSettingsState.instance.defaultChatModel())) + Thread { + Thread.sleep(500) + try { + Desktop.getDesktop().browse(server.server.uri.resolve("/$uuid/index.html")) + } catch (e: Throwable) { + log.warn("Error opening browser", e) + } + }.start() + } + + override fun isEnabled(event: AnActionEvent): Boolean { + return true + } + + companion object { + private val log = LoggerFactory.getLogger(CodeChatAction::class.java) + } +} diff --git a/src/main/resources/sources/kt/com/github/simiacryptus/aicoder/actions/generic/CreateFileAction.kt b/src/main/resources/sources/kt/com/github/simiacryptus/aicoder/actions/generic/CreateFileAction.kt new file mode 100644 index 00000000..dbf176bb --- /dev/null +++ b/src/main/resources/sources/kt/com/github/simiacryptus/aicoder/actions/generic/CreateFileAction.kt @@ -0,0 +1,105 @@ +package com.github.simiacryptus.aicoder.actions.generic + +import com.github.simiacryptus.aicoder.actions.FileContextAction +import com.github.simiacryptus.aicoder.config.AppSettingsState +import com.github.simiacryptus.aicoder.config.Name +import com.simiacryptus.jopenai.ApiModel.* +import com.simiacryptus.jopenai.ClientUtil.toContentList +import java.io.File +import javax.swing.JTextArea + +class CreateFileAction : FileContextAction(false, true) { + + class ProjectFile(var path: String = "", var code: String = "") + + class SettingsUI { + @Name("Directive") + var directive: JTextArea = JTextArea( + """ + Create a default log4j configuration file + """.trimIndent(), 3, 120 + ) + } + + class Settings(var directive: String = "") + + override fun processSelection( + state: SelectionState, + config: Settings? + ): Array { + val projectRoot = state.projectRoot.toPath() + val inputPath = projectRoot.relativize(state.selectedFile.toPath()).toString() + val pathSegments = inputPath.split("/").toList() + val updirSegments = pathSegments.takeWhile { it == ".." } + val moduleRoot = projectRoot.resolve(pathSegments.take(updirSegments.size * 2).joinToString("/")) + val filePath = pathSegments.drop(updirSegments.size * 2).joinToString("/") + + val generatedFile = generateFile(filePath, config?.directive ?: "") + + var path = generatedFile.path + var outputPath = moduleRoot.resolve(path) + if (outputPath.toFile().exists()) { + val extension = path.substringAfterLast(".") + val name = path.substringBeforeLast(".") + val fileIndex = (1..Int.MAX_VALUE).find { + !File("$name.$it.$extension").exists() + } + path = "$name.$fileIndex.$extension" + outputPath = projectRoot.resolve(path) + } + outputPath.parent.toFile().mkdirs() + outputPath.toFile().writeText(generatedFile.code) + Thread.sleep(100) + + return arrayOf(outputPath.toFile()) + } + + private fun generateFile( + basePath: String, + directive: String + ): ProjectFile { + val model = AppSettingsState.instance.defaultChatModel() + val chatRequest = ChatRequest( + model = model.modelName, + temperature = AppSettingsState.instance.temperature, + messages = listOf( + ChatMessage( + Role.system, """ + You will interpret natural language requirements to create a new file. + Provide a new filename and the code to be written to the file. + Paths should be relative to the project root and should not exist. + Output the file path using the a line with the format "File: ". + Output the file code directly after the header line with no additional decoration. + """.trimIndent().toContentList(), null + ), + ChatMessage( + Role.user, """ + Create a new file based on the following directive: $directive + + The file location should be based on the selected path `$basePath` + """.trimIndent().toContentList(), null + ) + ) + ) + val response = api.chat( + chatRequest, + AppSettingsState.instance.defaultChatModel() + ).choices?.first()?.message?.content?.trim() ?: "" + var outputPath = basePath + val header = response.lines().first() + var body = response.lines().drop(1).joinToString("\n").trim() + if (body.startsWith("```")) { + // Remove beginning ``` (optionally ```language) and ending ``` + body = body.split("\n").drop(1).joinToString("\n").trim() + } + val pathPattern = """File(?:name)?: ['`"]?([^'`"]+)['`"]?""".toRegex() + if (pathPattern.matches(header)) { + val match = pathPattern.matchEntire(header)!! + outputPath = match.groupValues[1] + } + return ProjectFile( + path = outputPath, + code = body + ) + } +} \ No newline at end of file diff --git a/src/main/resources/sources/kt/com/github/simiacryptus/aicoder/actions/generic/DictationAction.kt b/src/main/resources/sources/kt/com/github/simiacryptus/aicoder/actions/generic/DictationAction.kt new file mode 100644 index 00000000..fe068ddf --- /dev/null +++ b/src/main/resources/sources/kt/com/github/simiacryptus/aicoder/actions/generic/DictationAction.kt @@ -0,0 +1,135 @@ +package com.github.simiacryptus.aicoder.actions.generic + +import com.github.simiacryptus.aicoder.actions.BaseAction +import com.github.simiacryptus.aicoder.util.UITools +import com.intellij.openapi.actionSystem.AnActionEvent +import com.intellij.openapi.actionSystem.CommonDataKeys +import com.intellij.openapi.actionSystem.PlatformDataKeys +import com.intellij.openapi.command.WriteCommandAction +import com.simiacryptus.jopenai.audio.AudioRecorder +import com.simiacryptus.jopenai.audio.LookbackLoudnessWindowBuffer +import org.slf4j.LoggerFactory +import java.util.* +import java.util.concurrent.ConcurrentLinkedDeque +import java.util.concurrent.Executors +import java.util.concurrent.Future +import java.util.concurrent.TimeUnit +import java.util.concurrent.atomic.AtomicInteger +import javax.sound.sampled.AudioSystem +import javax.sound.sampled.TargetDataLine +import javax.swing.JFrame +import javax.swing.JLabel + +class DictationAction : BaseAction() { + override fun handle(e: AnActionEvent) { + val continueFn = statusDialog(e)::isVisible + + val rawBuffer = ConcurrentLinkedDeque() + Thread({ + try { + log.warn("Recording thread started") + AudioRecorder(rawBuffer, 0.05, continueFn).run() + log.warn("Recording thread complete") + } catch (e: Throwable) { + UITools.error(log,"Error", e) + } + }, "dication-audio-recorder").start() + + val wavBuffer = ConcurrentLinkedDeque() + Thread({ + log.warn("Audio processing thread started") + try { + LookbackLoudnessWindowBuffer(rawBuffer, wavBuffer, continueFn).run() + } catch (e: Throwable) { + UITools.error(log,"Error", e) + } + log.warn("Audio processing thread complete") + }, "dictation-audio-processor").start() + + val caretModel = (e.getData(CommonDataKeys.EDITOR) ?: return).caretModel + val primaryCaret = caretModel.primaryCaret + val dictationPump = if (primaryCaret.hasSelection()) { + DictationPump(e, wavBuffer, continueFn, primaryCaret.selectionEnd, primaryCaret.selectedText ?: "") + } else { + DictationPump(e, wavBuffer, continueFn, caretModel.offset) + } + Thread({ + log.warn("Speech-To-Text thread started") + try { + dictationPump.run() + } catch (e: Throwable) { + UITools.error(log,"Error", e) + } + log.warn("Speech-To-Text thread complete") + }, "dictation-api-processor").start() + } + + private inner class DictationPump( + val event: AnActionEvent, + private val audioBuffer: Deque, + val continueFn: () -> Boolean, + offsetStart: Int, + var prompt: String = "" + ) { + + private val offset: AtomicInteger = AtomicInteger(offsetStart) + + fun run() { + while (this.continueFn() || audioBuffer.isNotEmpty()) { + val recordAudio = audioBuffer.poll() + if (null == recordAudio) { + Thread.sleep(1) + } else { + log.warn("Speech-To-Text Starting...") + var text = api.transcription(recordAudio, prompt) + if (prompt.isNotEmpty()) text = " $text" + val newPrompt = (prompt + text).split(" ").takeLast(32).joinToString(" ") + log.warn( + """Speech-To-Text Complete + | Prompt: $prompt + | Result: $text""".trimMargin() + ) + prompt = newPrompt + WriteCommandAction.runWriteCommandAction(event.project) { + val editor = event.getData(CommonDataKeys.EDITOR) ?: return@runWriteCommandAction + editor.document.insertString(offset.getAndAdd(text.length), text) + } + } + } + } + } + + + private fun statusDialog(e1: AnActionEvent): JFrame { + val dialog = JFrame("Dictation") + val jLabel = JLabel("Close this window to stop recording and dictation") + jLabel.font = jLabel.font.deriveFont(48f) + dialog.add(jLabel) + dialog.pack() + dialog.location = e1.getData(PlatformDataKeys.CONTEXT_COMPONENT)?.locationOnScreen!! + dialog.isAlwaysOnTop = true + dialog.isVisible = true + return dialog + } + + override fun isEnabled(event: AnActionEvent): Boolean { + return try { + null != targetDataLine.get(50, TimeUnit.MILLISECONDS) + } catch (e: Exception) { + false + } + } + + companion object { + private val log = LoggerFactory.getLogger(DictationAction::class.java) + + private val pool = Executors.newFixedThreadPool(1) + + val targetDataLine: Future by lazy { + pool.submit { + AudioSystem.getTargetDataLine(AudioRecorder.audioFormat) + } + } + } + +} diff --git a/src/main/resources/sources/kt/com/github/simiacryptus/aicoder/actions/generic/RedoLast.kt b/src/main/resources/sources/kt/com/github/simiacryptus/aicoder/actions/generic/RedoLast.kt new file mode 100644 index 00000000..5a03da8e --- /dev/null +++ b/src/main/resources/sources/kt/com/github/simiacryptus/aicoder/actions/generic/RedoLast.kt @@ -0,0 +1,23 @@ +package com.github.simiacryptus.aicoder.actions.generic + +import com.github.simiacryptus.aicoder.actions.BaseAction +import com.github.simiacryptus.aicoder.util.UITools.retry +import com.intellij.openapi.actionSystem.AnActionEvent +import com.intellij.openapi.actionSystem.CommonDataKeys + +/** + * The RedoLast action is an IntelliJ action that allows users to redo the last AI Coder action they performed in the editor. + * To use this action, open the editor and select the RedoLast action from the editor context menu. + * This will redo the last action that was performed in the editor. + */ +class RedoLast : BaseAction() { + + override fun handle(e: AnActionEvent) { + retry[e.getRequiredData(CommonDataKeys.EDITOR).document]!!.run() + } + + override fun isEnabled(event: AnActionEvent): Boolean { + return null != retry[event.getRequiredData(CommonDataKeys.EDITOR).document] + } + +} diff --git a/src/main/resources/sources/kt/com/github/simiacryptus/aicoder/actions/generic/ReplaceOptionsAction.kt b/src/main/resources/sources/kt/com/github/simiacryptus/aicoder/actions/generic/ReplaceOptionsAction.kt new file mode 100644 index 00000000..eaa4a8f4 --- /dev/null +++ b/src/main/resources/sources/kt/com/github/simiacryptus/aicoder/actions/generic/ReplaceOptionsAction.kt @@ -0,0 +1,58 @@ +package com.github.simiacryptus.aicoder.actions.generic + +import com.github.simiacryptus.aicoder.actions.SelectionAction +import com.github.simiacryptus.aicoder.config.AppSettingsState +import com.github.simiacryptus.aicoder.util.UITools +import com.intellij.openapi.actionSystem.AnActionEvent +import com.intellij.openapi.project.Project +import com.simiacryptus.jopenai.proxy.ChatProxy +import com.simiacryptus.jopenai.util.StringUtil +import kotlin.math.ceil +import kotlin.math.ln +import kotlin.math.pow + +class ReplaceOptionsAction : SelectionAction() { + interface VirtualAPI { + fun suggestText(template: String, examples: List): Suggestions + + class Suggestions { + var choices: List? = null + } + } + + val proxy: VirtualAPI get() { + return ChatProxy( + clazz = VirtualAPI::class.java, + api = api, + model = AppSettingsState.instance.defaultChatModel(), + temperature = AppSettingsState.instance.temperature, + deserializerRetries = 5 + ).create() + } + + override fun getConfig(project: Project?): String { + return "" + } + + override fun processSelection(event: AnActionEvent?, state: SelectionState, config: String?): String { + val choices = UITools.run(event?.project, templateText, true, true) { + val selectedText = state.selectedText + val idealLength = 2.0.pow(2 + ceil(ln(selectedText?.length?.toDouble() ?: 1.0))).toInt() + val selectionStart = state.selectionOffset + val allBefore = state.entireDocument?.substring(0, selectionStart) ?: "" + val selectionEnd = state.selectionOffset + (state.selectionLength ?: 0) + val allAfter = state.entireDocument?.substring(selectionEnd, state.entireDocument.length) ?: "" + val before = StringUtil.getSuffixForContext(allBefore, idealLength).toString().replace('\n', ' ') + val after = StringUtil.getPrefixForContext(allAfter, idealLength).toString().replace('\n', ' ') + proxy.suggestText( + "$before _____ $after", + listOf(selectedText.toString()) + ).choices + } + return choose(choices ?: listOf()) + } + + private fun choose(choices: List): String { + return UITools.showRadioButtonDialog("Select an option to fill in the blank:", *choices.toTypedArray())?.toString() ?: "" + } +} \ No newline at end of file diff --git a/src/main/resources/sources/kt/com/github/simiacryptus/aicoder/actions/markdown/MarkdownImplementActionGroup.kt b/src/main/resources/sources/kt/com/github/simiacryptus/aicoder/actions/markdown/MarkdownImplementActionGroup.kt new file mode 100644 index 00000000..5ca450b4 --- /dev/null +++ b/src/main/resources/sources/kt/com/github/simiacryptus/aicoder/actions/markdown/MarkdownImplementActionGroup.kt @@ -0,0 +1,81 @@ +package com.github.simiacryptus.aicoder.actions.markdown + +import com.github.simiacryptus.aicoder.actions.SelectionAction +import com.github.simiacryptus.aicoder.config.AppSettingsState +import com.github.simiacryptus.aicoder.util.ComputerLanguage +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.project.Project +import com.simiacryptus.jopenai.proxy.ChatProxy + +class MarkdownImplementActionGroup : ActionGroup() { + private val markdownLanguages = listOf( + "sql", "java", "asp", "c", "clojure", "coffee", "cpp", "csharp", "css", "bash", "go", "java", "javascript", + "less", "make", "matlab", "objectivec", "pascal", "PHP", "Perl", "python", "rust", "scss", "sql", "svg", + "swift", "ruby", "smalltalk", "vhdl" + ) + + override fun update(e: AnActionEvent) { + e.presentation.isEnabledAndVisible = isEnabled(e) + super.update(e) + } + + companion object { + fun isEnabled(e: AnActionEvent): Boolean { + val computerLanguage = ComputerLanguage.getComputerLanguage(e) ?: return false + if (ComputerLanguage.Markdown != computerLanguage) return false + return UITools.hasSelection(e) + } + } + + override fun getChildren(e: AnActionEvent?): Array { + if (e == null) return emptyArray() + val computerLanguage = ComputerLanguage.getComputerLanguage(e) ?: return emptyArray() + val actions = markdownLanguages.map { language -> MarkdownImplementAction(language) } + return actions.toTypedArray() + } + + class MarkdownImplementAction(private val language: String) : SelectionAction(true) { + init { + templatePresentation.text = language + templatePresentation.description = language + } + + interface ConversionAPI { + fun implement(text: String, humanLanguage: String, computerLanguage: String): ConvertedText + + class ConvertedText { + var code: String? = null + var language: String? = null + } + } + + private fun getProxy(): ConversionAPI { + return ChatProxy( + clazz = ConversionAPI::class.java, + api = api, + model = AppSettingsState.instance.defaultChatModel(), + temperature = AppSettingsState.instance.temperature, + deserializerRetries = 5 + ).create() + } + + override fun getConfig(project: Project?): String { + return "" + } + + override fun processSelection(state: SelectionState, config: String?): String { + val code = getProxy().implement(state.selectedText ?: "", "autodetect", language).code ?: "" + return """ + | + | + |```$language + |$code + |``` + | + |""".trimMargin() + } + } +} \ No newline at end of file diff --git a/src/main/resources/sources/kt/com/github/simiacryptus/aicoder/actions/markdown/MarkdownListAction.kt b/src/main/resources/sources/kt/com/github/simiacryptus/aicoder/actions/markdown/MarkdownListAction.kt new file mode 100644 index 00000000..48d95bf5 --- /dev/null +++ b/src/main/resources/sources/kt/com/github/simiacryptus/aicoder/actions/markdown/MarkdownListAction.kt @@ -0,0 +1,108 @@ +package com.github.simiacryptus.aicoder.actions.markdown + +import com.github.simiacryptus.aicoder.actions.BaseAction +import com.github.simiacryptus.aicoder.config.AppSettingsState +import com.github.simiacryptus.aicoder.util.ComputerLanguage +import com.github.simiacryptus.aicoder.util.UITools +import com.github.simiacryptus.aicoder.util.UITools.getIndent +import com.github.simiacryptus.aicoder.util.UITools.insertString +import com.github.simiacryptus.aicoder.util.psi.PsiUtil.getAll +import com.github.simiacryptus.aicoder.util.psi.PsiUtil.getSmallestIntersecting +import com.intellij.openapi.actionSystem.AnActionEvent +import com.intellij.openapi.actionSystem.CommonDataKeys +import com.intellij.openapi.application.ApplicationManager +import com.simiacryptus.jopenai.proxy.ChatProxy +import com.simiacryptus.jopenai.util.StringUtil + +class MarkdownListAction : BaseAction() { + + interface ListAPI { + fun newListItems( + items: List?, + count: Int, + ): Items + + data class Items( + val items: List? = null, + ) + } + + val proxy: ListAPI + get() { + val chatProxy = ChatProxy( + clazz = ListAPI::class.java, + api = api, + model = AppSettingsState.instance.defaultChatModel(), + deserializerRetries = 5, + ) + chatProxy.addExample( + returnValue = ListAPI.Items( + items = listOf("Item 4", "Item 5", "Item 6") + ) + ) { + it.newListItems( + items = listOf("Item 1", "Item 2", "Item 3"), + count = 6 + ) + } + return chatProxy.create() + } + + override fun handle(e: AnActionEvent) { + val caret = e.getData(CommonDataKeys.CARET) ?: return + val psiFile = e.getData(CommonDataKeys.PSI_FILE) ?: return + val list = + getSmallestIntersecting(psiFile, caret.selectionStart, caret.selectionEnd, "MarkdownListImpl") ?: return + val items = StringUtil.trim( + getAll(list, "MarkdownListItemImpl") + .map { + val all = getAll(it, "MarkdownParagraphImpl") + if (all.isEmpty()) it.text else all[0].text + }.toList(), 10, false + ) + val indent = getIndent(caret) + val endOffset = list.textRange.endOffset + val bulletTypes = listOf("- [ ] ", "- ", "* ") + val document = (e.getData(CommonDataKeys.EDITOR) ?: return).document + val rawItems = items.map(CharSequence::trim).map { + val bulletType = bulletTypes.find(it::startsWith) + if (null != bulletType) StringUtil.stripPrefix(it, bulletType).toString() + else it.toString() + } + + UITools.redoableTask(e) { + var newItems: List? = null + UITools.run( + e.project, "Generating New Items", true + ) { + newItems = proxy.newListItems( + rawItems, + (items.size * 2) + ).items + } + var newList = "" + ApplicationManager.getApplication().runReadAction { + val strippedList = list.text.split("\n") + .map(String::trim).filter(String::isNotEmpty) + .joinToString("\n") + val bulletString = bulletTypes.find(strippedList::startsWith) ?: "1. " + newList = newItems?.joinToString("\n") { indent.toString() + bulletString + it } ?: "" + } + UITools.writeableFn(e) { + insertString(document, endOffset, "\n" + newList) + } + } + } + + override fun isEnabled(event: AnActionEvent): Boolean { + val computerLanguage = ComputerLanguage.getComputerLanguage(event) ?: return false + if (ComputerLanguage.Markdown != computerLanguage) return false + val caret = event.getData(CommonDataKeys.CARET) ?: return false + val psiFile = event.getData(CommonDataKeys.PSI_FILE) ?: return false + getSmallestIntersecting(psiFile, caret.selectionStart, caret.selectionEnd, "MarkdownListImpl") ?: return false + return true + } +} + + + diff --git a/src/main/resources/sources/kt/com/github/simiacryptus/aicoder/config/ActionSettingsRegistry.kt b/src/main/resources/sources/kt/com/github/simiacryptus/aicoder/config/ActionSettingsRegistry.kt new file mode 100644 index 00000000..3db4fc71 --- /dev/null +++ b/src/main/resources/sources/kt/com/github/simiacryptus/aicoder/config/ActionSettingsRegistry.kt @@ -0,0 +1,209 @@ +package com.github.simiacryptus.aicoder.config + +import com.fasterxml.jackson.annotation.JsonIgnore +import com.github.simiacryptus.aicoder.ui.EditorMenu +import com.github.simiacryptus.aicoder.util.IdeaKotlinInterpreter +import com.github.simiacryptus.aicoder.util.UITools +import com.intellij.openapi.actionSystem.AnAction +import java.io.File +import java.util.stream.Collectors + +class ActionSettingsRegistry { + + val actionSettings: MutableMap = HashMap() + private val version = 2.0005 + + fun edit(superChildren: Array): Array { + val children = superChildren.toList().toMutableList() + children.toTypedArray().forEach { + val language = "kt" + val code: String? = load(it.javaClass, language) + if (null != code) { + try { + val actionConfig = this.getActionConfig(it) + actionConfig.language = language + actionConfig.isDynamic = false + with(it) { + templatePresentation.text = actionConfig.displayText + templatePresentation.description = actionConfig.displayText + } + if (!actionConfig.enabled) { + children.remove(it) + } else if (!actionConfig.file.exists() + || actionConfig.file.readText().isBlank() + || (actionConfig.version ?: 0.0) < version + ) { + actionConfig.file.writeText(code) + actionConfig.version = version + } else if (!(actionConfig.isDynamic || (actionConfig.version ?: 0.0) >= version)) { + val canLoad = try { + ActionSettingsRegistry::class.java.classLoader.loadClass(actionConfig.id) + true + } catch (e: Throwable) { + false + } + if (canLoad) { + actionConfig.file.writeText(code) + actionConfig.version = version + } else { + children.remove(it) + } + } else { + val localCode = actionConfig.file.readText().drop(1) + if (true || !localCode.equals(code)) { // HACK to test compile + val element = actionConfig.buildAction(localCode) + children.remove(it) + children.add(element) + } + } + actionConfig.version = version + } catch (e: Throwable) { + UITools.error(log, "Error loading ${it.javaClass}", e) + } + } + } + this.getDynamicActions().forEach { + try { + if (!it.file.exists()) return@forEach + if (!it.enabled) return@forEach + val element = it.buildAction(it.file.readText()) + children.add(element) + } catch (e: Throwable) { + UITools.error(log, "Error loading dynamic action", e) + } + } + return children.toTypedArray() + } + + class DynamicActionException( + cause: Throwable, + msg: String, + val file: File, + val actionSetting: ActionSettings + ) : Exception(msg, cause) + + data class ActionSettings( + val id: String, // Static property + var enabled: Boolean = true, // User settable + var displayText: String? = null, // User settable + var version: Double? = null, // System property + var isDynamic: Boolean = false, // Static property + var language: String? = null, // Static property + ) { + + fun buildAction( + code: String + ): AnAction = try { + val newClassName = this.className + "_" + Integer.toHexString(code.hashCode()) + with( + actionCache.getOrPut("$packageName.$newClassName") { + (compile( + code.replace( + ("""(? { + try { + val kotlinInterpreter = IdeaKotlinInterpreter(mapOf()) + val scriptEngine = kotlinInterpreter.scriptEngine + val eval = scriptEngine.eval(code) + throw Exception("Not implemented") + } catch (e: Throwable) { + throw DynamicActionException(e, "Error in Action " + displayText, file, this) + } + } + + private val packageName: String get() = id.substringBeforeLast('.') + val className: String get() = id.substringAfterLast('.') + + val file: File + get() { + val file = File(configDir(), "aicoder/actions/${packageName.replace('.', '/')}/$className.$language") + file.parentFile.mkdirs() + return file + } + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as ActionSettings + + if (id != other.id) return false + if (enabled != other.enabled) return false + if (displayText != other.displayText) return false + if (isDynamic != other.isDynamic) return false + return language == other.language + } + + override fun hashCode(): Int { + var result = id.hashCode() + result = 31 * result + enabled.hashCode() + result = 31 * result + (displayText?.hashCode() ?: 0) + result = 31 * result + isDynamic.hashCode() + result = 31 * result + (language?.hashCode() ?: 0) + return result + } + + } + + private fun getActionConfig(action: AnAction): ActionSettings { + return actionSettings.getOrPut(action.javaClass.name) { + val actionConfig = ActionSettings(action.javaClass.name) + actionConfig.displayText = action.templatePresentation.text + actionConfig + } + } + + @JsonIgnore + fun getDynamicActions(): List { + return actionSettings.entries.stream().filter { it.value.isDynamic && it.value.enabled }.map { it.value } + .collect(Collectors.toList()) + } + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + other as ActionSettingsRegistry + return actionSettings == other.actionSettings + } + + override fun hashCode(): Int { + return actionSettings.hashCode() + } + + companion object { + + private val log = org.slf4j.LoggerFactory.getLogger(ActionSettingsRegistry::class.java) + + val actionCache = HashMap() + private fun load(actionPackage: String, actionName: String, language: String) = + load("/sources/${language}/$actionPackage/$actionName.$language") + + private fun load(path: String): String? { + val bytes = EditorMenu::class.java.getResourceAsStream(path)?.readAllBytes() + return bytes?.toString(Charsets.UTF_8)?.drop(1) // XXX Why? '\uFEFF' is first byte + } + + fun load(clazz: Class, language: String) = + load(clazz.`package`.name.replace('.', '/'), clazz.simpleName, language) + + fun configDir(): File { + var baseDir = System.getProperty("idea.config.path") + if (baseDir == null) baseDir = System.getProperty("user.home") + return File(baseDir) + } + } + +} diff --git a/src/main/resources/sources/kt/com/github/simiacryptus/aicoder/config/ActionTable.kt b/src/main/resources/sources/kt/com/github/simiacryptus/aicoder/config/ActionTable.kt new file mode 100644 index 00000000..76f8d6d6 --- /dev/null +++ b/src/main/resources/sources/kt/com/github/simiacryptus/aicoder/config/ActionTable.kt @@ -0,0 +1,221 @@ +package com.github.simiacryptus.aicoder.config + +import com.intellij.openapi.application.ApplicationManager +import com.intellij.openapi.fileEditor.FileEditorManager +import com.intellij.openapi.project.Project +import com.intellij.openapi.ui.VerticalFlowLayout +import com.intellij.openapi.vfs.LocalFileSystem +import com.intellij.ui.BooleanTableCellEditor +import com.intellij.ui.BooleanTableCellRenderer +import com.intellij.ui.components.JBScrollPane +import com.intellij.ui.components.panels.HorizontalLayout +import com.intellij.ui.table.JBTable +import org.jdesktop.swingx.JXTable +import org.slf4j.LoggerFactory +import java.awt.BorderLayout +import java.awt.event.ActionEvent +import java.util.* +import javax.swing.* +import javax.swing.table.AbstractTableModel +import javax.swing.table.DefaultTableCellRenderer + +class ActionTable( + val actionSettings: MutableList +) : JPanel(BorderLayout()) { + + fun read(registry: ActionSettingsRegistry) { + registry.actionSettings.clear() + rowData.map { row -> + val copy = (actionSettings.find { it.id == row[2] })!!.copy( + enabled = ((row[0] as String) == "true"), + displayText = row[1] as String + ) + registry.actionSettings.put(copy.id, copy) + } + } + + fun write(registry: ActionSettingsRegistry) { + registry.actionSettings.values.forEach { actionSetting -> + val row = rowData.find { it[2] == actionSetting.id } + row?.let { + actionSetting.enabled = (it[0] as String) == "true" + actionSetting.displayText = it[1] as String + } + } + } + + + private val buttonPanel = JPanel() + val columnNames = arrayOf("Enabled", "Display Text", "ID") + + val rowData = actionSettings.map { + listOf(it.enabled.toString(), it.displayText, it.id).toMutableList() + }.toMutableList() + + val dataModel = object : AbstractTableModel() { + override fun getColumnName(column: Int): String { + return columnNames.get(column).toString() + } + + override fun getRowCount(): Int { + return rowData.size + } + + override fun getColumnCount(): Int { + return columnNames.size + } + + override fun getValueAt(row: Int, col: Int): Any { + return rowData[row][col]!! + } + + override fun isCellEditable(row: Int, column: Int): Boolean { + return true + } + + override fun setValueAt(value: Any, row: Int, col: Int) { + rowData[row][col] = value.toString() + fireTableCellUpdated(row, col) + } + + } + + val jtable = JBTable(dataModel) + + private val scrollpane = JBScrollPane(jtable) + + private val cloneButton = JButton(object : AbstractAction("Clone") { + override fun actionPerformed(e: ActionEvent?) { + + if (jtable.selectedRows.size != 1) { + JOptionPane.showMessageDialog(null, "Please select a single row to clone") + return + } + + val selectedRowIndex = jtable.selectedRow + val selectedSettings = actionSettings.find { + it.id == dataModel.getValueAt(selectedRowIndex, 2) + } + + val panel = JPanel(VerticalFlowLayout(VerticalFlowLayout.TOP)) + val classnameField = JTextField(100) + classnameField.text = dataModel.getValueAt(selectedRowIndex, 2).toString() + panel.add(with(JPanel(HorizontalLayout(2))) { + add(JLabel("New class name:")) + add(classnameField) + this + }) + val displayField = JTextField(100) + displayField.text = dataModel.getValueAt(selectedRowIndex, 1).toString() + panel.add(with(JPanel(HorizontalLayout(2))) { + add(JLabel("New description:")) + add(displayField) + this + }) + val options = arrayOf("OK", "Cancel") + if (JOptionPane.showOptionDialog( + null, + panel, + "API Key", + JOptionPane.NO_OPTION, + JOptionPane.PLAIN_MESSAGE, + null, + options, + options[1] + ) == JOptionPane.OK_OPTION + ) { + if ((0 until dataModel.rowCount).toList().any { dataModel.getValueAt(it, 2) == classnameField.text }) { + JOptionPane.showMessageDialog(null, "Class name already exists") + } else { + val newRow = mutableListOf() + newRow.add("true") + newRow.add(displayField.text) + newRow.add(classnameField.text) + val newSettings = selectedSettings!!.copy( + id = classnameField.text, + displayText = displayField.text, + enabled = true, + isDynamic = true + ) + newSettings.file.writeText( + selectedSettings.file.readText().replace( + ("""(? { + com.intellij.openapi.project.ProjectManager.getInstance().openProjects.firstOrNull() + } + if (it.exists()) { + ApplicationManager.getApplication().invokeLater { + val virtualFile = LocalFileSystem.getInstance().refreshAndFindFileByIoFile(it) + FileEditorManager.getInstance(project!!).openFile(virtualFile!!, true) + } + } else { + log.warn("File not found: ${it.absolutePath}") + } + } + } + })) + + private val removeButton = JButton(object : AbstractAction("Remove") { + override fun actionPerformed(e: ActionEvent?) { + if (jtable.selectedRows.size != 1) { + JOptionPane.showMessageDialog(null, "Please select a single row to clone") + return + } + val selectedRow = jtable.selectedRow + val selectedSettings = actionSettings.find { + it.id == dataModel.getValueAt(selectedRow, 2) + } + if (selectedSettings?.isDynamic != true) { + JOptionPane.showMessageDialog(null, "Cannot remove non-dynamic action") + return + } + rowData.removeIf { + it[2] == selectedSettings.id + } + this@ActionTable.parent.invalidate() + } + }) + + init { + jtable.columnModel.getColumn(0).cellRenderer = BooleanTableCellRenderer() + jtable.columnModel.getColumn(1).cellRenderer = DefaultTableCellRenderer() + jtable.columnModel.getColumn(2).cellRenderer = DefaultTableCellRenderer() + + jtable.columnModel.getColumn(0).cellEditor = BooleanTableCellEditor() + jtable.columnModel.getColumn(1).cellEditor = JXTable.GenericEditor() + jtable.columnModel.getColumn(2).cellEditor = object : JXTable.GenericEditor() { + override fun isCellEditable(anEvent: EventObject?) = false + } + + jtable.columnModel.getColumn(0).headerRenderer = DefaultTableCellRenderer() + jtable.columnModel.getColumn(1).headerRenderer = DefaultTableCellRenderer() + jtable.columnModel.getColumn(2).headerRenderer = DefaultTableCellRenderer() + + jtable.tableHeader.defaultRenderer = DefaultTableCellRenderer() + + add(scrollpane, BorderLayout.CENTER) + buttonPanel.add(cloneButton) + buttonPanel.add(editButton) + buttonPanel.add(removeButton) + add(buttonPanel, BorderLayout.SOUTH) + } + companion object { + private val log = LoggerFactory.getLogger(ActionTable::class.java) + } +} diff --git a/src/main/resources/sources/kt/com/github/simiacryptus/aicoder/config/AppSettingsComponent.kt b/src/main/resources/sources/kt/com/github/simiacryptus/aicoder/config/AppSettingsComponent.kt new file mode 100644 index 00000000..b9d958c1 --- /dev/null +++ b/src/main/resources/sources/kt/com/github/simiacryptus/aicoder/config/AppSettingsComponent.kt @@ -0,0 +1,124 @@ +@file:Suppress("unused") + +package com.github.simiacryptus.aicoder.config + +import com.intellij.openapi.application.ApplicationManager +import com.intellij.openapi.fileEditor.FileEditorManager +import com.intellij.openapi.project.Project +import com.intellij.openapi.ui.ComboBox +import com.intellij.openapi.vfs.LocalFileSystem +import com.intellij.ui.components.JBCheckBox +import com.intellij.ui.components.JBPasswordField +import com.intellij.ui.components.JBTextField +import com.simiacryptus.jopenai.ClientUtil +import com.simiacryptus.jopenai.models.ChatModels + +import org.slf4j.LoggerFactory +import java.awt.event.ActionEvent +import javax.swing.AbstractAction +import javax.swing.JButton +import javax.swing.JComponent + +class AppSettingsComponent { + + @Name("Token Counter") + val tokenCounter = JBTextField() + + @Suppress("unused") + val clearCounter = JButton(object : AbstractAction("Clear Token Counter") { + override fun actionPerformed(e: ActionEvent) { + tokenCounter.text = "0" + } + }) + + @Suppress("unused") + @Name("Human Language") + val humanLanguage = JBTextField() + + @Suppress("unused") + @Name("Listening Port") + val listeningPort = JBTextField() + + @Suppress("unused") + @Name("Listening Endpoint") + val listeningEndpoint = JBTextField() + + @Suppress("unused") + @Name("Suppress Errors") + val suppressErrors = JBCheckBox() + + @Suppress("unused") + @Name("Model") + val modelName = ComboBox() + + @Suppress("unused") + @Name("Enable API Log") + val apiLog = JBCheckBox() + + @Suppress("unused") + val openApiLog = JButton(object : AbstractAction("Open API Log") { + override fun actionPerformed(e: ActionEvent) { + ClientUtil.auxiliaryLog?.let { + val project = ApplicationManager.getApplication().runReadAction { + com.intellij.openapi.project.ProjectManager.getInstance().openProjects.firstOrNull() + } + if (it.exists()) { + ApplicationManager.getApplication().invokeLater { + val virtualFile = LocalFileSystem.getInstance().refreshAndFindFileByIoFile(it) + FileEditorManager.getInstance(project!!).openFile(virtualFile!!, true) + } + } else { + log.warn("Log file not found: ${it.absolutePath}") + } + } + } + }) + + + @Suppress("unused") + @Name("Developer Tools") + val devActions = JBCheckBox() + + @Suppress("unused") + @Name("Edit API Requests") + val editRequests = JBCheckBox() + + @Suppress("unused") + @Name("Temperature") + val temperature = JBTextField() + + @Name("API Key") + val apiKey = JBPasswordField() + + @Suppress("unused") + @Name("API Base") + val apiBase = JBTextField() + + @Name("File Actions") + var fileActions = ActionTable(AppSettingsState.instance.fileActions.actionSettings.values.map { it.copy() } + .toTypedArray().toMutableList()) + + @Name("Editor Actions") + var editorActions = ActionTable(AppSettingsState.instance.editorActions.actionSettings.values.map { it.copy() } + .toTypedArray().toMutableList()) + + init { + tokenCounter.isEditable = false + this.modelName.addItem(ChatModels.GPT35Turbo.modelName) + this.modelName.addItem(ChatModels.GPT4.modelName) + this.modelName.addItem(ChatModels.GPT4Turbo.modelName) + } + + val preferredFocusedComponent: JComponent + get() = apiKey + + class ActionChangedListener { + fun actionChanged() { + } + } + + companion object { + private val log = LoggerFactory.getLogger(AppSettingsComponent::class.java) + //val ACTIONS_TOPIC = Topic.create("Actions", ActionChangedListener::class.java) + } +} diff --git a/src/main/resources/sources/kt/com/github/simiacryptus/aicoder/config/AppSettingsConfigurable.kt b/src/main/resources/sources/kt/com/github/simiacryptus/aicoder/config/AppSettingsConfigurable.kt new file mode 100644 index 00000000..5c0e4e5d --- /dev/null +++ b/src/main/resources/sources/kt/com/github/simiacryptus/aicoder/config/AppSettingsConfigurable.kt @@ -0,0 +1,68 @@ +package com.github.simiacryptus.aicoder.config + +import com.github.simiacryptus.aicoder.util.UITools +import com.intellij.openapi.options.Configurable +import java.util.* +import javax.swing.JComponent +import javax.swing.JPanel + +class AppSettingsConfigurable : Configurable { + private var settingsComponent: AppSettingsComponent? = null + + @Volatile + private var mainPanel: JPanel? = null + override fun getDisplayName(): String { + return "AICoder Settings" + } + + override fun getPreferredFocusedComponent(): JComponent? { + return Objects.requireNonNull(settingsComponent)?.preferredFocusedComponent + } + + override fun createComponent(): JComponent? { + if (null == mainPanel) { + synchronized(this) { + if (null == mainPanel) { + settingsComponent = AppSettingsComponent() + reset() + mainPanel = UITools.build(settingsComponent!!, false) + } + } + } + return mainPanel + } + + + override fun isModified(): Boolean { + val buffer = AppSettingsState() + if (settingsComponent != null) { + UITools.readKotlinUI(settingsComponent!!, buffer) + settingsComponent?.editorActions?.read(buffer.editorActions) + settingsComponent?.fileActions?.read(buffer.fileActions) + } + return buffer != AppSettingsState.instance + } + + override fun apply() { + if (settingsComponent != null) { + UITools.readKotlinUI(settingsComponent!!, AppSettingsState.instance) + settingsComponent?.editorActions?.read(AppSettingsState.instance.editorActions) + settingsComponent?.fileActions?.read(AppSettingsState.instance.fileActions) + } + } + + override fun reset() { + if (settingsComponent != null) { + UITools.writeKotlinUI(settingsComponent!!, AppSettingsState.instance) + settingsComponent?.editorActions?.write(AppSettingsState.instance.editorActions) + settingsComponent?.fileActions?.write(AppSettingsState.instance.fileActions) + } + } + + override fun disposeUIResources() { + settingsComponent = null + } +} + + + diff --git a/src/main/resources/sources/kt/com/github/simiacryptus/aicoder/config/AppSettingsState.kt b/src/main/resources/sources/kt/com/github/simiacryptus/aicoder/config/AppSettingsState.kt new file mode 100644 index 00000000..2010759a --- /dev/null +++ b/src/main/resources/sources/kt/com/github/simiacryptus/aicoder/config/AppSettingsState.kt @@ -0,0 +1,115 @@ +package com.github.simiacryptus.aicoder.config + +import com.fasterxml.jackson.annotation.JsonIgnore +import com.intellij.openapi.application.ApplicationManager +import com.intellij.openapi.components.PersistentStateComponent +import com.intellij.openapi.components.State +import com.intellij.openapi.components.Storage +import com.intellij.util.xmlb.XmlSerializerUtil +import com.simiacryptus.jopenai.ApiModel.ChatRequest +import com.simiacryptus.jopenai.models.ChatModels +import com.simiacryptus.jopenai.models.OpenAIModel +import com.simiacryptus.jopenai.models.OpenAITextModel +import com.simiacryptus.jopenai.util.JsonUtil + +class SimpleEnvelope(var value: String? = null) + +@State(name = "org.intellij.sdk.settings.AppSettingsState", storages = [Storage("SdkSettingsPlugin.xml")]) +class AppSettingsState : PersistentStateComponent { + var listeningPort: Int = 8081 + var listeningEndpoint: String = "localhost" + var modalTasks: Boolean = false + var suppressErrors: Boolean = false + var apiLog: Boolean = false + var apiBase = "https://api.openai.com/v1" + var apiKey = "" + var temperature = 0.1 + var modelName : String = ChatModels.GPT35Turbo.modelName + var tokenCounter = 0 + var humanLanguage = "English" + var devActions = false + var editRequests = false + var apiThreads = 4 + val editorActions = ActionSettingsRegistry() + val fileActions = ActionSettingsRegistry() + + private val recentCommands = mutableMapOf() + + fun createChatRequest(): ChatRequest { + return createChatRequest(defaultChatModel()) + } + + fun defaultChatModel(): OpenAITextModel = ChatModels.entries.first { it.modelName == modelName } + + private fun createChatRequest(model: OpenAIModel): ChatRequest = ChatRequest( + model = model.modelName, + temperature = temperature + ) + + @JsonIgnore + override fun getState(): SimpleEnvelope { + return SimpleEnvelope(JsonUtil.toJson(this)) + } + + fun getRecentCommands(id:String) = recentCommands.computeIfAbsent(id) { MRUItems() } + + override fun loadState(state: SimpleEnvelope) { + state.value ?: return + val fromJson = JsonUtil.fromJson(state.value!!, AppSettingsState::class.java) + XmlSerializerUtil.copyBean(fromJson, this) + + recentCommands.clear(); recentCommands.putAll(fromJson.recentCommands) + editorActions.actionSettings.clear(); editorActions.actionSettings.putAll(fromJson.editorActions.actionSettings) + fileActions.actionSettings.clear(); fileActions.actionSettings.putAll(fromJson.fileActions.actionSettings) + } + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as AppSettingsState + + if (listeningPort != other.listeningPort) return false + if (listeningEndpoint != other.listeningEndpoint) return false + if (modalTasks != other.modalTasks) return false + if (suppressErrors != other.suppressErrors) return false + if (apiLog != other.apiLog) return false + if (apiBase != other.apiBase) return false + if (apiKey != other.apiKey) return false + if (temperature != other.temperature) return false + if (modelName != other.modelName) return false + if (tokenCounter != other.tokenCounter) return false + if (humanLanguage != other.humanLanguage) return false + if (devActions != other.devActions) return false + if (editRequests != other.editRequests) return false + if (apiThreads != other.apiThreads) return false + + return true + } + + override fun hashCode(): Int { + var result = listeningPort + result = 31 * result + listeningEndpoint.hashCode() + result = 31 * result + modalTasks.hashCode() + result = 31 * result + suppressErrors.hashCode() + result = 31 * result + apiLog.hashCode() + result = 31 * result + apiBase.hashCode() + result = 31 * result + apiKey.hashCode() + result = 31 * result + temperature.hashCode() + result = 31 * result + modelName.hashCode() + result = 31 * result + tokenCounter + result = 31 * result + humanLanguage.hashCode() + result = 31 * result + devActions.hashCode() + result = 31 * result + editRequests.hashCode() + result = 31 * result + apiThreads + return result + } + + companion object { + @JvmStatic + val instance: AppSettingsState by lazy { + val application = ApplicationManager.getApplication() + if (null == application) AppSettingsState() else application.getService(AppSettingsState::class.java) + } + } +} diff --git a/src/main/resources/sources/kt/com/github/simiacryptus/aicoder/config/MRUItems.kt b/src/main/resources/sources/kt/com/github/simiacryptus/aicoder/config/MRUItems.kt new file mode 100644 index 00000000..eb7b134a --- /dev/null +++ b/src/main/resources/sources/kt/com/github/simiacryptus/aicoder/config/MRUItems.kt @@ -0,0 +1,44 @@ +package com.github.simiacryptus.aicoder.config + +import java.util.Map.Entry.comparingByValue +import java.util.stream.Collectors + +class MRUItems { + val mostUsedHistory: MutableMap = HashMap() + private val mostRecentHistory: MutableList = ArrayList() + private var historyLimit = 10 + fun addInstructionToHistory(instruction: CharSequence) { + synchronized(mostRecentHistory) { + if(mostRecentHistory.contains(instruction.toString())) { + mostRecentHistory.remove(instruction.toString()) + } + mostRecentHistory.add(instruction.toString()) + while (mostRecentHistory.size > historyLimit) { + mostRecentHistory.removeAt(0) + } + } + synchronized(mostUsedHistory) { + mostUsedHistory.put( + instruction.toString(), + (mostUsedHistory[instruction] ?: 0) + 1 + ) + } + + if (mostUsedHistory.size > historyLimit) { + val retain = mostUsedHistory.entries.stream() + .sorted(comparingByValue().reversed()) + .limit(historyLimit.toLong()) + .map { (key, _) -> key }.collect( + Collectors.toList() + ) + val toRemove = HashSet(mostUsedHistory.keys) + toRemove.removeAll(retain.toSet()) + toRemove.removeAll(mostRecentHistory.toSet()) + toRemove.forEach { key: CharSequence? -> + mostUsedHistory.remove(key) + mostRecentHistory.remove(key.toString()) + } + } + } + +} \ No newline at end of file diff --git a/src/main/resources/sources/kt/com/github/simiacryptus/aicoder/config/Name.kt b/src/main/resources/sources/kt/com/github/simiacryptus/aicoder/config/Name.kt new file mode 100644 index 00000000..803eaf6d --- /dev/null +++ b/src/main/resources/sources/kt/com/github/simiacryptus/aicoder/config/Name.kt @@ -0,0 +1,6 @@ +package com.github.simiacryptus.aicoder.config + +@Retention(AnnotationRetention.RUNTIME) +annotation class Name(val value: String) + + diff --git a/src/main/resources/sources/kt/com/github/simiacryptus/aicoder/ui/EditorMenu.kt b/src/main/resources/sources/kt/com/github/simiacryptus/aicoder/ui/EditorMenu.kt new file mode 100644 index 00000000..87363f0f --- /dev/null +++ b/src/main/resources/sources/kt/com/github/simiacryptus/aicoder/ui/EditorMenu.kt @@ -0,0 +1,13 @@ +package com.github.simiacryptus.aicoder.ui + +import com.github.simiacryptus.aicoder.config.AppSettingsState +import com.intellij.openapi.actionSystem.AnAction +import com.intellij.openapi.actionSystem.AnActionEvent + +open class EditorMenu : com.intellij.openapi.actionSystem.DefaultActionGroup() { + override fun getChildren(e: AnActionEvent?): Array { + return AppSettingsState.instance.editorActions.edit(super.getChildren(e)) + } +} + + diff --git a/src/main/resources/sources/kt/com/github/simiacryptus/aicoder/ui/ModelSelectionWidgetFactory.kt b/src/main/resources/sources/kt/com/github/simiacryptus/aicoder/ui/ModelSelectionWidgetFactory.kt new file mode 100644 index 00000000..bed8463a --- /dev/null +++ b/src/main/resources/sources/kt/com/github/simiacryptus/aicoder/ui/ModelSelectionWidgetFactory.kt @@ -0,0 +1,111 @@ +package com.github.simiacryptus.aicoder.ui + +import com.github.simiacryptus.aicoder.config.AppSettingsState +import com.intellij.openapi.project.Project +import com.intellij.openapi.ui.popup.JBPopup +import com.intellij.openapi.wm.StatusBar +import com.intellij.openapi.wm.StatusBarWidget +import com.intellij.openapi.wm.StatusBarWidgetFactory +import com.intellij.ui.CollectionListModel +import com.intellij.ui.SimpleListCellRenderer +import com.intellij.ui.popup.list.ComboBoxPopup +import com.simiacryptus.jopenai.models.ChatModels +import kotlinx.coroutines.CoroutineScope +import javax.swing.JList +import javax.swing.ListCellRenderer +import javax.swing.ListModel + +class ModelSelectionWidgetFactory : StatusBarWidgetFactory { + companion object { + private val log = org.slf4j.LoggerFactory.getLogger(ModelSelectionWidgetFactory::class.java) + } + + class ModelSelectionWidget : StatusBarWidget, StatusBarWidget.MultipleTextValuesPresentation { + + private var statusBar: StatusBar? = null + private var activeModel: String = AppSettingsState.instance.defaultChatModel().modelName + val models = listOf( + ChatModels.GPT4Turbo, + ChatModels.GPT4, + ChatModels.GPT35Turbo, + ) + + override fun ID(): String { + return "ModelSelectionComponent" + } + + override fun getPresentation(): StatusBarWidget.WidgetPresentation { + return this + } + + override fun install(statusBar: StatusBar) { + this.statusBar = statusBar + } + + override fun dispose() { + //connection?.disconnect() + } + + override fun getTooltipText(): String { + return "Current active model" + } + + override fun getSelectedValue(): String { + return activeModel + } + + override fun getPopup(): JBPopup { + val context = object : ComboBoxPopup.Context { + override fun getProject(): Project? { + return null + } + + override fun getModel(): ListModel = CollectionListModel(models.map { it.modelName }) + + override fun getRenderer(): ListCellRenderer { + return object : SimpleListCellRenderer() { + override fun customize( + list: JList, + value: String?, + index: Int, + selected: Boolean, + hasFocus: Boolean + ) { + text = value + } + + } + } + } + return ComboBoxPopup(context, activeModel, { str -> + activeModel = str + AppSettingsState.instance.modelName = str + statusBar?.updateWidget(ID()) + }) + } + } + + override fun getId(): String { + return "ModelSelectionComponent" + } + + override fun getDisplayName(): String { + return "Model Selector" + } + + override fun createWidget(project: Project, scope: CoroutineScope): StatusBarWidget { + return ModelSelectionWidget() + } + + override fun createWidget(project: Project): StatusBarWidget { + return ModelSelectionWidget() + } + + override fun isAvailable(project: Project): Boolean { + return true + } + + override fun canBeEnabledOn(statusBar: StatusBar): Boolean { + return true + } +} diff --git a/src/main/resources/sources/kt/com/github/simiacryptus/aicoder/ui/ProjectMenu.kt b/src/main/resources/sources/kt/com/github/simiacryptus/aicoder/ui/ProjectMenu.kt new file mode 100644 index 00000000..b0d0b037 --- /dev/null +++ b/src/main/resources/sources/kt/com/github/simiacryptus/aicoder/ui/ProjectMenu.kt @@ -0,0 +1,11 @@ +package com.github.simiacryptus.aicoder.ui + +import com.github.simiacryptus.aicoder.config.AppSettingsState +import com.intellij.openapi.actionSystem.AnAction +import com.intellij.openapi.actionSystem.AnActionEvent + +open class ProjectMenu : com.intellij.openapi.actionSystem.DefaultActionGroup() { + override fun getChildren(e: AnActionEvent?): Array { + return AppSettingsState.instance.fileActions.edit(super.getChildren(e)) + } +} \ No newline at end of file diff --git a/src/main/resources/sources/kt/com/github/simiacryptus/aicoder/ui/TemperatureControlWidgetFactory.kt b/src/main/resources/sources/kt/com/github/simiacryptus/aicoder/ui/TemperatureControlWidgetFactory.kt new file mode 100644 index 00000000..a463b64b --- /dev/null +++ b/src/main/resources/sources/kt/com/github/simiacryptus/aicoder/ui/TemperatureControlWidgetFactory.kt @@ -0,0 +1,153 @@ +package com.github.simiacryptus.aicoder.ui + +import com.github.simiacryptus.aicoder.config.AppSettingsState +import com.intellij.openapi.project.Project +import com.intellij.openapi.ui.popup.JBPopupFactory +import com.intellij.openapi.util.IconLoader +import com.intellij.openapi.wm.StatusBar +import com.intellij.openapi.wm.StatusBarWidget +import com.intellij.openapi.wm.StatusBarWidgetFactory +import com.intellij.ui.awt.RelativePoint +import com.intellij.ui.components.JBLabel +import com.intellij.ui.components.Panel +import com.intellij.ui.components.panels.VerticalLayout +import com.intellij.util.Consumer +import kotlinx.coroutines.CoroutineScope +import java.awt.Cursor +import java.awt.Desktop +import java.awt.Point +import java.awt.event.MouseAdapter +import java.awt.event.MouseEvent +import java.net.URI +import java.util.concurrent.Executors +import javax.swing.Icon +import javax.swing.JPanel +import javax.swing.JSlider +import javax.swing.JTabbedPane +import javax.swing.event.ChangeEvent +import javax.swing.event.ChangeListener + + +class TemperatureControlWidgetFactory : StatusBarWidgetFactory { + companion object { + private val log = org.slf4j.LoggerFactory.getLogger(TemperatureControlWidgetFactory::class.java) + val pool = Executors.newCachedThreadPool() + } + + class TemperatureControlWidget : StatusBarWidget, StatusBarWidget.IconPresentation { + + private val temperatureSlider by lazy { + val slider = JSlider(0, 100, (AppSettingsState.instance.temperature * 100).toInt()) + slider.addChangeListener(object : ChangeListener { + override fun stateChanged(e: ChangeEvent?) { + AppSettingsState.instance.temperature = slider.value / 100.0 + } + }) + slider + } + + override fun ID(): String { + return "TemperatureControlComponent" + } + + override fun install(statusBar: StatusBar) { + // No need to listen to file changes for this widget + } + + override fun dispose() { + // No resources to dispose + } + + override fun getTooltipText(): String { + return "AI Coding Assistant\nTemp = ${AppSettingsState.instance.temperature}" + } + + override fun getClickConsumer(): Consumer { + return Consumer { event: MouseEvent -> + val widgetComp = event.component + if (widgetComp != null) { + val modePanel = Panel() + modePanel.layout = VerticalLayout(0) + modePanel.add(JBLabel("AI Coding Assistant")) + + val tabbedPane = JTabbedPane() + + val tempPanel = JPanel() + tempPanel.setLayout(VerticalLayout(0)) + tempPanel.add(temperatureSlider) + tabbedPane.addTab("Temperature", tempPanel) + + val feedbackPanel = JPanel() + feedbackPanel.setLayout(VerticalLayout(5)) + + feedbackPanel.add( + link( + JBLabel("Problem? Request help..."), + URI("https://github.com/SimiaCryptus/intellij-aicoder/issues") + ) + ) + feedbackPanel.add( + link( + JBLabel("Love It? Leave us a review!"), + URI("https://plugins.jetbrains.com/plugin/20724-ai-coding-assistant/reviews") + ) + ) + tabbedPane.addTab("Feedback", feedbackPanel) + + modePanel.add(tabbedPane) + + JBPopupFactory.getInstance().createComponentPopupBuilder(modePanel, null).createPopup() + .show(RelativePoint(widgetComp, Point(0, -modePanel.getPreferredSize().height))) + } + } + } + + private fun link(jbLabel: JBLabel, uri: URI): JBLabel { + jbLabel.setCursor(Cursor(Cursor.HAND_CURSOR)) + jbLabel.addMouseListener(object : MouseAdapter() { + override fun mouseClicked(e: MouseEvent) { + try { + Desktop.getDesktop().browse(uri) + } catch (ex: Exception) { + ex.printStackTrace() + } + } + }) + return jbLabel + } + + override fun getIcon(): Icon? = + IconLoader.findIcon( + url = javaClass.classLoader.getResource("./META-INF/toolbarIcon.svg"), + storeToCache = true + ) + + override fun getPresentation(): StatusBarWidget.WidgetPresentation { + return this + } + } + + override fun getId(): String { + return "TemperatureControlComponent" + } + + override fun getDisplayName(): String { + return "Temperature Control" + } + + override fun createWidget(project: Project, scope: CoroutineScope): StatusBarWidget { + return TemperatureControlWidget() + } + + override fun createWidget(project: Project): StatusBarWidget { + return TemperatureControlWidget() + } + + override fun isAvailable(project: Project): Boolean { + return true + } + + override fun canBeEnabledOn(statusBar: StatusBar): Boolean { + return true + } +} diff --git a/src/main/resources/sources/kt/com/github/simiacryptus/aicoder/ui/TokenCountWidgetFactory.kt b/src/main/resources/sources/kt/com/github/simiacryptus/aicoder/ui/TokenCountWidgetFactory.kt new file mode 100644 index 00000000..3a92ff52 --- /dev/null +++ b/src/main/resources/sources/kt/com/github/simiacryptus/aicoder/ui/TokenCountWidgetFactory.kt @@ -0,0 +1,133 @@ +package com.github.simiacryptus.aicoder.ui + +import com.intellij.openapi.editor.event.DocumentEvent +import com.intellij.openapi.editor.event.DocumentListener +import com.intellij.openapi.editor.event.SelectionEvent +import com.intellij.openapi.editor.event.SelectionListener +import com.intellij.openapi.fileEditor.FileEditorManager +import com.intellij.openapi.fileEditor.FileEditorManagerEvent +import com.intellij.openapi.fileEditor.FileEditorManagerListener +import com.intellij.openapi.project.Project +import com.intellij.openapi.vfs.readText +import com.intellij.openapi.wm.StatusBar +import com.intellij.openapi.wm.StatusBarWidget +import com.intellij.openapi.wm.StatusBarWidgetFactory +import com.simiacryptus.jopenai.GPT4Tokenizer +import kotlinx.coroutines.CoroutineScope +import java.awt.event.MouseEvent +import java.util.concurrent.LinkedBlockingDeque +import java.util.concurrent.ThreadPoolExecutor +import java.util.concurrent.TimeUnit + +class TokenCountWidgetFactory : StatusBarWidgetFactory { + companion object { + private val log = org.slf4j.LoggerFactory.getLogger(TokenCountWidgetFactory::class.java) + val workQueue = LinkedBlockingDeque() + val pool = ThreadPoolExecutor( + /* corePoolSize = */ 1, /* maximumPoolSize = */ 1, + /* keepAliveTime = */ 60L, /* unit = */ TimeUnit.SECONDS, + /* workQueue = */ workQueue + ) + } + + class TokenCountWidget : StatusBarWidget, StatusBarWidget.TextPresentation { + + private var tokenCount: Int = 0 + val codex = GPT4Tokenizer(false) + + override fun ID(): String { + return "StatusBarComponent" + } + + override fun getPresentation(): StatusBarWidget.WidgetPresentation { + return this + } + + override fun install(statusBar: StatusBar) { + val connection = statusBar.project?.messageBus?.connect() + connection?.subscribe(FileEditorManagerListener.FILE_EDITOR_MANAGER, object : FileEditorManagerListener { + override fun selectionChanged(event: FileEditorManagerEvent) { + update(statusBar) { + codex.estimateTokenCount((event.newFile ?: event.oldFile)?.readText() ?: "") + } + + val editor = FileEditorManager.getInstance(statusBar.project!!).selectedTextEditor + editor?.document?.addDocumentListener(object : DocumentListener { + override fun documentChanged(event: DocumentEvent) { + update(statusBar) { codex.estimateTokenCount(editor.document.text) } + } + }) + + editor?.selectionModel?.addSelectionListener(object : SelectionListener { + override fun selectionChanged(event: SelectionEvent) { + update(statusBar) { + val newTokens = event.newRanges?.map { + codex.estimateTokenCount( + event.editor.document.text.substring( + it.startOffset, + it.endOffset + ) + ) + }?.sum() ?: 0 + newTokens + } + } + }) + } + }) + } + + private fun update(statusBar: StatusBar, tokens: () -> Int) { + workQueue.clear() + pool.submit { + tokenCount = tokens() + statusBar.updateWidget(ID()) + } + } + + override fun dispose() { + //connection?.disconnect() + } + + + override fun getText(): String { + return "$tokenCount Tokens" + } + + override fun getTooltipText(): String { + return "Current file token count" + } + + override fun getAlignment(): Float { + return 0.5f + } + + override fun getClickConsumer(): com.intellij.util.Consumer? = null + + + } + + override fun getId(): String { + return "StatusBarComponent" + } + + override fun getDisplayName(): String { + return "Token Counter" + } + + override fun createWidget(project: Project, scope: CoroutineScope): StatusBarWidget { + return TokenCountWidget() + } + + override fun createWidget(project: Project): StatusBarWidget { + return TokenCountWidget() + } + + override fun isAvailable(project: Project): Boolean { + return true + } + + override fun canBeEnabledOn(statusBar: StatusBar): Boolean { + return true + } +} diff --git a/src/main/resources/sources/kt/com/github/simiacryptus/aicoder/util/BlockComment.kt b/src/main/resources/sources/kt/com/github/simiacryptus/aicoder/util/BlockComment.kt new file mode 100644 index 00000000..6137f013 --- /dev/null +++ b/src/main/resources/sources/kt/com/github/simiacryptus/aicoder/util/BlockComment.kt @@ -0,0 +1,54 @@ +@file:Suppress("NAME_SHADOWING") + +package com.github.simiacryptus.aicoder.util + +import com.simiacryptus.jopenai.util.StringUtil +import java.util.* +import java.util.stream.Collectors + +class BlockComment( + private val blockPrefix: CharSequence, + private val linePrefix: CharSequence, + private val blockSuffix: CharSequence, + indent: CharSequence, + vararg textBlock: CharSequence +) : + IndentedText(indent, *textBlock) { + class Factory(private val blockPrefix: String, private val linePrefix: String, private val blockSuffix: String) : + TextBlockFactory { + override fun fromString(text: String?): BlockComment { + var text = text!! + text = StringUtil.stripSuffix( + StringUtil.trimSuffix(text.replace("\t", TAB_REPLACEMENT.toString(), false)), + blockSuffix.trim { it <= ' ' }) + val indent = StringUtil.getWhitespacePrefix(*text.split(DELIMITER.toRegex()).dropLastWhile { it.isEmpty() } + .toTypedArray()) + return BlockComment(blockPrefix, + linePrefix, + blockSuffix, + indent, + *(Arrays.stream(text.split(DELIMITER.toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray()) + .map { s: CharSequence? -> StringUtil.stripPrefix(s!!, indent) } + .map { text: CharSequence? -> StringUtil.trimPrefix(text!!) } + .map { s: CharSequence? -> StringUtil.stripPrefix(s!!, blockPrefix.trim { it <= ' ' }) } + .map { s: CharSequence? -> StringUtil.stripPrefix(s!!, linePrefix.trim { it <= ' ' }) } + .collect(Collectors.toList()).toTypedArray())) + } + + override fun looksLike(text: String?): Boolean { + return text!!.trim { it <= ' ' }.startsWith(blockPrefix) && text.trim { it <= ' ' }.endsWith(blockSuffix) + } + } + + override fun toString(): String { + val indent = this.indent + val delimiter: CharSequence = DELIMITER + indent + val joined: CharSequence = Arrays.stream(rawString()).map { x: CharSequence -> "$linePrefix $x" } + .collect(Collectors.joining(delimiter)) + return blockPrefix.toString() + delimiter + joined + delimiter + blockSuffix + } + + override fun withIndent(indent: CharSequence?): BlockComment { + return BlockComment(blockPrefix, linePrefix, blockSuffix, indent!!, *lines) + } +} diff --git a/src/main/resources/sources/kt/com/github/simiacryptus/aicoder/util/ComputerLanguage.kt b/src/main/resources/sources/kt/com/github/simiacryptus/aicoder/util/ComputerLanguage.kt new file mode 100644 index 00000000..ba934bfb --- /dev/null +++ b/src/main/resources/sources/kt/com/github/simiacryptus/aicoder/util/ComputerLanguage.kt @@ -0,0 +1,475 @@ +package com.github.simiacryptus.aicoder.util + +import com.intellij.openapi.actionSystem.AnActionEvent +import com.intellij.openapi.actionSystem.CommonDataKeys +import java.util.* + +enum class ComputerLanguage(configuration: Configuration) { + Java( + Configuration() + .setDocumentationStyle("JavaDoc") + .setLineComments(LineComment.Factory("//")) + .setBlockComments(BlockComment.Factory("/*", "", "*/")) + .setDocComments(BlockComment.Factory("/**", " * ", " */")) + .setFileExtensions("java") + ), + Cpp( + Configuration() + .setDocumentationStyle("Doxygen") + .setLineComments(LineComment.Factory("//")) + .setBlockComments(BlockComment.Factory("/*", "", "*/")) + .setDocComments(BlockComment.Factory("/**", "*", "*/")) + .setFileExtensions("cpp") + ), + LUA( + Configuration() + .setDocumentationStyle("LuaDoc") + .setLineComments(LineComment.Factory("--")) + .setBlockComments(BlockComment.Factory("--[[", "", "]]")) + .setDocComments(BlockComment.Factory("---[[", "", "]]")) + .setFileExtensions("lua") + ), + SVG( + Configuration() + .setDocumentationStyle("SVG") + .setLineComments(LineComment.Factory("")) + .setDocComments(BlockComment.Factory("")) + .setFileExtensions("svg") + ), + OpenSCAD( + Configuration() + .setDocumentationStyle("OpenSCAD") + .setLineComments(LineComment.Factory("//")) + .setBlockComments(BlockComment.Factory("/*", "", "*/")) + .setDocComments(BlockComment.Factory("/**", "*", "*/")) + .setFileExtensions("scad") + ), + Bash( + Configuration() + .setLineComments(LineComment.Factory("#")) + .setFileExtensions("sh") + ), + Markdown( + Configuration() + .setDocumentationStyle("Markdown") + .setLineComments(BlockComment.Factory("")) + .setBlockComments(BlockComment.Factory("")) + .setDocComments(BlockComment.Factory("")) + .setFileExtensions("md") + ), + Text( + Configuration() + .setDocumentationStyle("Text") + .setLineComments(LineComment.Factory("#")) + .setFileExtensions("txt") + ), + Ada( + Configuration() + .setLineComments(LineComment.Factory("--")) + .setBlockComments(BlockComment.Factory("/*", "", "*/")) + .setDocComments(BlockComment.Factory("/**", "*", "*/")) + .setFileExtensions("ada") + ), + Assembly( + Configuration() + .setLineComments(LineComment.Factory(";")) + .setBlockComments(BlockComment.Factory("/*", "", "*/")) + .setDocComments(BlockComment.Factory("/**", "*", "*/")) + .setFileExtensions("assembly", "asm") + ), + Basic( + Configuration() + .setLineComments(LineComment.Factory("'")) + .setBlockComments(BlockComment.Factory("/*", "", "*/")) + .setDocComments(BlockComment.Factory("/**", "*", "*/")) + .setFileExtensions("basic", "bs") + ), + C( + Configuration() + .setDocumentationStyle("Doxygen") + .setLineComments(LineComment.Factory("//")) + .setBlockComments(BlockComment.Factory("/*", "", "*/")) + .setDocComments(BlockComment.Factory("/**", "*", "*/")) + .setFileExtensions("c") + ), + Clojure( + Configuration() + .setDocumentationStyle("ClojureDocs") + .setLineComments(LineComment.Factory(";")) + .setBlockComments(BlockComment.Factory("/*", "", "*/")) + .setDocComments(BlockComment.Factory("/**", "*", "*/")) + .setFileExtensions("cj") + ), + COBOL( + Configuration() + .setLineComments(LineComment.Factory("*")) + .setBlockComments(BlockComment.Factory("/*", "", "*/")) + .setDocComments(BlockComment.Factory("/**", "*", "*/")) + .setFileExtensions("cobol", "cob") + ), + CSharp( + Configuration() + .setDocumentationStyle("XML") + .setLineComments(LineComment.Factory("//")) + .setBlockComments(BlockComment.Factory("/*", "", "*/")) + .setDocComments(BlockComment.Factory("/**", "*", "*/")) + .setFileExtensions("cs", "c#") + ), + CSS( + Configuration() + .setLineComments(BlockComment.Factory("/*", "", "*/")) + .setBlockComments(BlockComment.Factory("/*", "", "*/")) + .setDocComments(BlockComment.Factory("/**", "*", "*/")) + .setFileExtensions("css") + ), + Dart( + Configuration() + .setLineComments(LineComment.Factory("//")) + .setBlockComments(BlockComment.Factory("/*", "", "*/")) + .setDocComments(BlockComment.Factory("/**", "*", "*/")) + .setFileExtensions("dart") + ), + Delphi( + Configuration() + .setLineComments(LineComment.Factory("//")) + .setBlockComments(BlockComment.Factory("/*", "", "*/")) + .setDocComments(BlockComment.Factory("/**", "*", "*/")) + .setFileExtensions("delphi") + ), + Erlang( + Configuration() + .setLineComments(LineComment.Factory("%")) + .setBlockComments(BlockComment.Factory("%%", "", "%%")) + .setDocComments(BlockComment.Factory("%%%", "%", "%%%")) + .setFileExtensions("erl") + ), + Elixir( + Configuration() + .setLineComments(LineComment.Factory("//")) + .setBlockComments(BlockComment.Factory("/*", "", "*/")) + .setDocComments(BlockComment.Factory("/**", "*", "*/")) + .setFileExtensions("elixir") + ), + FORTRAN( + Configuration() + .setLineComments(LineComment.Factory("!")) + .setBlockComments(BlockComment.Factory("/*", "", "*/")) + .setDocComments(BlockComment.Factory("/**", "*", "*/")) + .setFileExtensions("f", "for", "ftn", "f77", "f90", "f95", "f03", "f08") + ), + FSharp( + Configuration() + .setLineComments(LineComment.Factory("//")) + .setBlockComments(BlockComment.Factory("/*", "", "*/")) + .setDocComments(BlockComment.Factory("/**", "*", "*/")) + .setFileExtensions("f#") + ), + Go( + Configuration() + .setLineComments(LineComment.Factory("//")) + .setBlockComments(BlockComment.Factory("/*", "", "*/")) + .setDocComments(BlockComment.Factory("/**", "*", "*/")) + .setFileExtensions("go") + ), + Groovy( + Configuration() + .setLineComments(LineComment.Factory("//")) + .setBlockComments(BlockComment.Factory("/*", "", "*/")) + .setDocComments(BlockComment.Factory("/**", "*", "*/")) + .setFileExtensions("groovy", "gradle") + ), + Haskell( + Configuration() + .setLineComments(LineComment.Factory("--")) + .setBlockComments(BlockComment.Factory("{-", "-}", "{- -}")) + .setDocComments(BlockComment.Factory("{-|", "|-}", "{-| -}")) + .setFileExtensions("hs") + ), + HTML( + Configuration() + .setLineComments(BlockComment.Factory("")) + .setBlockComments(BlockComment.Factory("")) + .setDocComments(BlockComment.Factory("")) + .setFileExtensions("html") + ), + Julia( + Configuration() + .setLineComments(LineComment.Factory("#")) + .setBlockComments(BlockComment.Factory("/*", "", "*/")) + .setDocComments(BlockComment.Factory("/**", "*", "*/")) + .setFileExtensions("julia") + ), + JavaScript( + Configuration() + .setDocumentationStyle("JSDoc") + .setLineComments(LineComment.Factory("//")) + .setBlockComments(BlockComment.Factory("/*", "", "*/")) + .setDocComments(BlockComment.Factory("/**", "*", "*/")) + .setFileExtensions("js", "javascript") + ), + Json( + Configuration() + .setLineComments(LineComment.Factory("//")) + .setFileExtensions("json") + ), + Kotlin( + Configuration() + .setDocumentationStyle("KDoc") + .setLineComments(LineComment.Factory("//")) + .setBlockComments(BlockComment.Factory("/*", "", "*/")) + .setDocComments(BlockComment.Factory("/**", "*", "*/")) + .setFileExtensions("kt", "kts") + ), + Lisp( + Configuration() + .setLineComments(LineComment.Factory(";")) + .setBlockComments(BlockComment.Factory("/*", "", "*/")) + .setDocComments(BlockComment.Factory("/**", "*", "*/")) + .setFileExtensions("lisp") + ), + Logo( + Configuration() + .setLineComments(LineComment.Factory(";")) + .setBlockComments(BlockComment.Factory("/*", "", "*/")) + .setDocComments(BlockComment.Factory("/**", "*", "*/")) + .setFileExtensions("logo", "log") + ), + MATLAB( + Configuration() + .setLineComments(LineComment.Factory("%")) + .setBlockComments(BlockComment.Factory("/*", "", "*/")) + .setDocComments(BlockComment.Factory("/**", "*", "*/")) + .setFileExtensions("matlab", "m") + ), + OCaml( + Configuration() + .setLineComments(LineComment.Factory("(Params.create(*")) + .setBlockComments(BlockComment.Factory("*))", "", "ocaml")) + .setDocComments(BlockComment.Factory("*))", "", "ocaml")) + .setFileExtensions("ml") + ), + Pascal( + Configuration() + .setLineComments(LineComment.Factory("//")) + .setBlockComments(BlockComment.Factory("/*", "", "*/")) + .setDocComments(BlockComment.Factory("/**", "*", "*/")) + .setFileExtensions("pascal", "pas") + ), + PHP( + Configuration() + .setLineComments(LineComment.Factory("//")) + .setBlockComments(BlockComment.Factory("/*", "", "*/")) + .setDocComments(BlockComment.Factory("/**", "*", "*/")) + .setFileExtensions("php") + ), + Perl( + Configuration() + .setDocumentationStyle("POD") + .setLineComments(LineComment.Factory("#")) + .setBlockComments(BlockComment.Factory("/*", "", "*/")) + .setDocComments(BlockComment.Factory("/**", "*", "*/")) + .setFileExtensions("perl", "pl") + ), + Prolog( + Configuration() + .setLineComments(LineComment.Factory("%")) + .setBlockComments(BlockComment.Factory("/*", "", "*/")) + .setDocComments(BlockComment.Factory("/**", "*", "*/")) + .setFileExtensions("prolog") + ), + Python( + Configuration() + .setDocumentationStyle("PyDoc") + .setLineComments(LineComment.Factory("#")) + .setBlockComments(BlockComment.Factory("/*", "", "*/")) + .setDocComments(BlockComment.Factory("/**", "*", "*/")) + .setFileExtensions("py", "python") + ), + R( + Configuration() + .setLineComments(LineComment.Factory("#")) + .setBlockComments(BlockComment.Factory("/*", "", "*/")) + .setDocComments(BlockComment.Factory("/**", "*", "*/")) + .setFileExtensions("r") + ), + Ruby( + Configuration() + .setLineComments(LineComment.Factory("#")) + .setBlockComments(BlockComment.Factory("/*", "", "*/")) + .setDocComments(BlockComment.Factory("/**", "*", "*/")) + .setFileExtensions("ruby", "rb") + ), + Racket( + Configuration() + .setLineComments(LineComment.Factory("#")) + .setBlockComments(BlockComment.Factory("#|", "", "|#")) + .setDocComments(BlockComment.Factory("#|", "", "|#")) + .setFileExtensions("racket") + ), + Rust( + Configuration() + .setDocumentationStyle("Rustdoc") + .setLineComments(LineComment.Factory("//")) + .setBlockComments(BlockComment.Factory("/*", "", "*/")) + .setDocComments(BlockComment.Factory("/**", "*", "*/")) + .setFileExtensions("rs", "rust") + ), + Scala( + Configuration() + .setDocumentationStyle("ScalaDoc") + .setLineComments(LineComment.Factory("//")) + .setBlockComments(BlockComment.Factory("/*", "", "*/")) + .setDocComments(BlockComment.Factory("/**", "*", "*/")) + .setFileExtensions("scala", "sc") + ), + Scheme( + Configuration() + .setLineComments(LineComment.Factory(";")) + .setBlockComments(BlockComment.Factory("/*", "", "*/")) + .setDocComments(BlockComment.Factory("/**", "*", "*/")) + .setFileExtensions("scheme") + ), + SCSS( + Configuration() + .setDocumentationStyle("SCSS") + .setLineComments(LineComment.Factory("//")) + .setBlockComments(BlockComment.Factory("/*", "", "*/")) + .setDocComments(LineComment.Factory("///")) + .setFileExtensions("scss") + ), + SQL( + Configuration() + .setLineComments(LineComment.Factory("--")) + .setBlockComments(BlockComment.Factory("/*", "", "*/")) + .setDocComments(BlockComment.Factory("/**", "*", "*/")) + .setFileExtensions("sql") + ), + Smalltalk( + Configuration() + .setLineComments(LineComment.Factory("\"")) + .setBlockComments(BlockComment.Factory("/*", "", "*/")) + .setDocComments(BlockComment.Factory("/**", "*", "*/")) + .setFileExtensions("smalltalk", "st") + ), + Swift( + Configuration() + .setLineComments(LineComment.Factory("//")) + .setBlockComments(BlockComment.Factory("/*", "", "*/")) + .setDocComments(BlockComment.Factory("/**", "*", "*/")) + .setFileExtensions("swift") + ), + Tcl( + Configuration() + .setLineComments(LineComment.Factory("#")) + .setBlockComments(BlockComment.Factory("/*", "", "*/")) + .setDocComments(BlockComment.Factory("/**", "*", "*/")) + .setFileExtensions("tcl") + ), + TypeScript( + Configuration() + .setDocumentationStyle("TypeDoc") + .setLineComments(LineComment.Factory("//")) + .setBlockComments(BlockComment.Factory("/*", "", "*/")) + .setDocComments(BlockComment.Factory("/**", "*", "*/")) + .setFileExtensions("typescript", "ts") + ), + VisualBasic( + Configuration() + .setLineComments(LineComment.Factory("'")) + .setBlockComments(BlockComment.Factory("/*", "", "*/")) + .setDocComments(BlockComment.Factory("/**", "*", "*/")) + .setFileExtensions("visualbasic", "vb") + ), + YAML( + Configuration() + .setLineComments(LineComment.Factory("#")) + .setFileExtensions("yaml") + ), + ZShell( + Configuration() + .setLineComments(LineComment.Factory("#")) + .setFileExtensions("zsh") + ); + + val extensions: List + val docStyle: String + val lineComment: TextBlockFactory<*> + val blockComment: TextBlockFactory<*> + private val docComment: TextBlockFactory<*> + + init { + extensions = listOf(*configuration.fileExtensions) + docStyle = configuration.documentationStyle + lineComment = configuration.lineComments!! + blockComment = configuration.getBlockComments()!! + docComment = configuration.getDocComments()!! + } + + fun getCommentModel(text: String?): TextBlockFactory<*> { + if (Objects.requireNonNull(docComment)!!.looksLike(text)) return docComment + return if (Objects.requireNonNull(blockComment)!!.looksLike(text)) blockComment else lineComment + } + + internal class Configuration { + var documentationStyle = "" + private set + var fileExtensions = arrayOf() + private set + var lineComments: TextBlockFactory<*>? = null + private set + private var blockComments: TextBlockFactory<*>? = null + private var docComments: TextBlockFactory<*>? = null + fun setDocumentationStyle(documentationStyle: String): Configuration { + this.documentationStyle = documentationStyle + return this + } + + fun setFileExtensions(vararg fileExtensions: CharSequence): Configuration { + @Suppress("UNCHECKED_CAST") + this.fileExtensions = fileExtensions as Array + return this + } + + fun setLineComments(lineComments: TextBlockFactory<*>): Configuration { + this.lineComments = lineComments + return this + } + + fun getBlockComments(): TextBlockFactory<*>? { + return if (null == blockComments) lineComments else blockComments + } + + fun setBlockComments(blockComments: TextBlockFactory<*>): Configuration { + this.blockComments = blockComments + return this + } + + fun getDocComments(): TextBlockFactory<*>? { + return if (null == docComments) getBlockComments() else docComments + } + + fun setDocComments(docComments: TextBlockFactory<*>): Configuration { + this.docComments = docComments + return this + } + } + + companion object { + @JvmStatic + fun findByExtension(extension: CharSequence): ComputerLanguage? { + return Arrays.stream(values()).filter { x: ComputerLanguage -> + x.extensions.contains( + extension + ) + }.findAny().orElse(null) + } + + @JvmStatic + fun getComputerLanguage(e: AnActionEvent?): ComputerLanguage? { + val file = e?.getData(CommonDataKeys.VIRTUAL_FILE) ?: return null + val extension = if (file.extension != null) file.extension!!.lowercase(Locale.getDefault()) else "" + return findByExtension(extension) + } + } +} diff --git a/src/main/resources/sources/kt/com/github/simiacryptus/aicoder/util/IdeaKotlinInterpreter.kt b/src/main/resources/sources/kt/com/github/simiacryptus/aicoder/util/IdeaKotlinInterpreter.kt new file mode 100644 index 00000000..069b651b --- /dev/null +++ b/src/main/resources/sources/kt/com/github/simiacryptus/aicoder/util/IdeaKotlinInterpreter.kt @@ -0,0 +1,74 @@ +package com.github.simiacryptus.aicoder.util + +import com.intellij.lang.Language +import com.intellij.openapi.application.runReadAction +import com.intellij.openapi.project.Project +import com.intellij.psi.PsiFileFactory +import com.simiacryptus.skyenet.kotlin.KotlinInterpreter +import org.jetbrains.kotlin.cli.common.repl.KotlinJsr223JvmScriptEngineFactoryBase +import org.jetbrains.kotlin.cli.common.repl.ScriptArgsWithTypes +import org.jetbrains.kotlin.jsr223.KotlinJsr223JvmScriptEngine4Idea +import org.jetbrains.kotlin.resolve.AnalyzingUtils +import org.slf4j.LoggerFactory +import javax.script.ScriptEngine +import kotlin.script.experimental.jvm.util.KotlinJars.kotlinScriptStandardJars +import kotlin.script.experimental.jvm.util.scriptCompilationClasspathFromContextOrStdlib + +class IdeaKotlinInterpreter(defs: Map) : KotlinInterpreter(defs) { + companion object { + private val log = LoggerFactory.getLogger(IdeaKotlinInterpreter::class.java) + + var project: Project? = null + } + + override val scriptEngine: ScriptEngine + get() { + val factory = object : KotlinJsr223JvmScriptEngineFactoryBase() { + override fun getScriptEngine(): ScriptEngine = KotlinJsr223JvmScriptEngine4Idea( + factory = this, + templateClasspath = scriptCompilationClasspathFromContextOrStdlib( + keyNames = arrayOf(), + classLoader = KotlinInterpreter::class.java.classLoader!!, + wholeClasspath = true, + ) + kotlinScriptStandardJars, + templateClassName = "kotlin.script.templates.standard.SimpleScriptTemplate", + getScriptArgs = { context, kClasses -> + ScriptArgsWithTypes( + scriptArgs = arrayOf( +// context.getBindings(ScriptContext.ENGINE_SCOPE) + ), + scriptArgsTypes = arrayOf( +// Bindings::class + )) }, + scriptArgsTypes = arrayOf( + //Reflection.getOrCreateKotlinClass(MutableMap::class.java) + ) + ) + } + return factory.scriptEngine + } + + override fun validate(code: String) = try { + val messageCollector = MessageCollectorImpl(code) + val psiFileFactory = PsiFileFactory.getInstance(project!!) + runReadAction { + AnalyzingUtils.checkForSyntacticErrors( + psiFileFactory.createFileFromText( + "Dummy.kt", + Language.findLanguageByID("kotlin")!!, + code + ) + ) + } + if (messageCollector.errors.isEmpty()) { + null + } else RuntimeException( + """ + |${messageCollector.errors.joinToString("\n") { "Error: $it" }} + |${messageCollector.warnings.joinToString("\n") { "Warning: $it" }} + """.trimMargin() + ) + } catch (e: Throwable) { + e + } +} \ No newline at end of file diff --git a/src/main/resources/sources/kt/com/github/simiacryptus/aicoder/util/IdeaOpenAIClient.kt b/src/main/resources/sources/kt/com/github/simiacryptus/aicoder/util/IdeaOpenAIClient.kt new file mode 100644 index 00000000..cb9bdef9 --- /dev/null +++ b/src/main/resources/sources/kt/com/github/simiacryptus/aicoder/util/IdeaOpenAIClient.kt @@ -0,0 +1,196 @@ +package com.github.simiacryptus.aicoder.util + +import com.github.simiacryptus.aicoder.config.AppSettingsState +import com.intellij.openapi.actionSystem.AnActionEvent +import com.intellij.openapi.application.ApplicationManager +import com.intellij.openapi.project.Project +import com.intellij.openapi.ui.DialogWrapper +import com.intellij.ui.components.JBScrollPane +import com.intellij.util.ui.FormBuilder +import com.simiacryptus.jopenai.ApiModel.* +import com.simiacryptus.jopenai.OpenAIClient +import com.simiacryptus.jopenai.models.OpenAIModel +import com.simiacryptus.jopenai.models.OpenAITextModel +import com.simiacryptus.jopenai.util.JsonUtil +import org.apache.hc.core5.http.HttpRequest +import org.slf4j.LoggerFactory +import java.util.concurrent.atomic.AtomicBoolean +import java.util.concurrent.atomic.AtomicReference +import javax.swing.JPanel +import javax.swing.JTextArea + +class IdeaOpenAIClient : OpenAIClient( + key = AppSettingsState.instance.apiKey, + apiBase = AppSettingsState.instance.apiBase, +) { + private val isInRequest = AtomicBoolean(false) + + override fun incrementTokens(model: OpenAIModel?, tokens: Usage) { + AppSettingsState.instance.tokenCounter += tokens.total_tokens + } + + override fun authorize(request: HttpRequest) { + key = UITools.checkApiKey(key) + super.authorize(request) + } + + @Suppress("NAME_SHADOWING") + override fun chat( + chatRequest: ChatRequest, + model: OpenAITextModel + ): ChatResponse { + lastEvent ?: return super.chat(chatRequest, model) + if (isInRequest.getAndSet(true)) { + val response = super.chat(chatRequest, model) + UITools.logAction(""" + |Chat Response: ${JsonUtil.toJson(response.usage!!)} + """.trimMargin().trim()) + return response + } else { + try { + if (!AppSettingsState.instance.editRequests) { + val response = super.chat(chatRequest, model) + UITools.logAction(""" + |Chat Response: ${JsonUtil.toJson(response.usage!!)} + """.trimMargin().trim()) + return response + } + return withJsonDialog(chatRequest, { chatRequest -> + UITools.run( + lastEvent!!.project, "OpenAI Request", true, suppressProgress = false + ) { + val response = super.chat(chatRequest, model) + UITools.logAction(""" + |Chat Response: ${JsonUtil.toJson(response.usage!!)} + """.trimMargin().trim()) + response + } + }, "Edit Chat Request") + } finally { + isInRequest.set(false) + } + } + } + + + override fun complete(request: CompletionRequest, model: OpenAITextModel): CompletionResponse { + lastEvent ?: return super.complete(request, model) + if (isInRequest.getAndSet(true)) { + val response = super.complete(request, model) + UITools.logAction(""" + |Completion Response: ${JsonUtil.toJson(response.usage!!)} + """.trimMargin().trim()) + return response + } else { + try { + if (!AppSettingsState.instance.editRequests) return super.complete(request, model) + return withJsonDialog(request, { + val completionRequest = it + UITools.run( + lastEvent!!.project, "OpenAI Request", true, suppressProgress = false + ) { + val response = super.complete(completionRequest, model) + UITools.logAction(""" + |Completion Response: ${JsonUtil.toJson(response.usage!!)} + """.trimMargin().trim()) + response + } + }, "Edit Completion Request") + } finally { + isInRequest.set(false) + } + } + } + + override fun edit(editRequest: EditRequest): CompletionResponse { + lastEvent ?: return super.edit(editRequest) + if (isInRequest.getAndSet(true)) { + return super.edit(editRequest) + } else { + try { + if (!AppSettingsState.instance.editRequests) return super.edit(editRequest) + return withJsonDialog(editRequest, { request -> + UITools.run( + lastEvent!!.project, "OpenAI Request", true, suppressProgress = false + ) { + super.edit(request) + } + }, "Edit Edit Request") + } finally { + isInRequest.set(false) + } + } + } + + companion object { + + val api: OpenAIClient get() = IdeaOpenAIClient() + var lastEvent: AnActionEvent? = null + private fun uiEdit( + project: Project? = null, + title: String = "Edit Request", + jsonTxt: String + ): String { + return execute { + val json = JTextArea( + /* text = */ "", + /* rows = */ 3, + /* columns = */ 120 + ) + json.isEditable = true + json.lineWrap = false + val jbScrollPane = JBScrollPane(json) + jbScrollPane.horizontalScrollBarPolicy = JBScrollPane.HORIZONTAL_SCROLLBAR_AS_NEEDED + jbScrollPane.verticalScrollBarPolicy = JBScrollPane.VERTICAL_SCROLLBAR_AS_NEEDED + val dialog = object : DialogWrapper(project) { + init { + this.init() + this.title = title + this.setOKButtonText("OK") + this.setCancelButtonText("Cancel") + this.isResizable = true + } + + override fun createCenterPanel(): JPanel? { + val formBuilder = FormBuilder.createFormBuilder() + formBuilder.addLabeledComponentFillVertically("JSON", jbScrollPane) + return formBuilder.panel + } + } + json.text = jsonTxt + dialog.show() + log.warn("dialog.size = " + dialog.size) + if (!dialog.isOK) { + throw RuntimeException("Cancelled") + } + json.text + } ?: jsonTxt + } + + private fun execute( + fn: () -> T + ): T? { + val application = ApplicationManager.getApplication() + val ref: AtomicReference = AtomicReference() + if (null != application) { + application.invokeAndWait { ref.set(fn()) } + } else { + ref.set(fn()) + } + return ref.get() + } + + + fun withJsonDialog( + request: T, + function: (T) -> V, + title: String + ): V { + val project = lastEvent?.project ?: return function(request) + return function(JsonUtil.fromJson(uiEdit(project, title, JsonUtil.toJson(request)), request::class.java)) + } + + private val log = LoggerFactory.getLogger(IdeaOpenAIClient::class.java) + } + +} diff --git a/src/main/resources/sources/kt/com/github/simiacryptus/aicoder/util/LineComment.kt b/src/main/resources/sources/kt/com/github/simiacryptus/aicoder/util/LineComment.kt new file mode 100644 index 00000000..6908063d --- /dev/null +++ b/src/main/resources/sources/kt/com/github/simiacryptus/aicoder/util/LineComment.kt @@ -0,0 +1,55 @@ +package com.github.simiacryptus.aicoder.util + +import com.simiacryptus.jopenai.util.StringUtil.getWhitespacePrefix +import com.simiacryptus.jopenai.util.StringUtil.stripPrefix +import com.simiacryptus.jopenai.util.StringUtil.trimPrefix +import java.util.* +import java.util.stream.Collectors + +class LineComment(private val commentPrefix: CharSequence, indent: CharSequence?, vararg lines: CharSequence) : + IndentedText(indent!!, *lines) { + class Factory(private val commentPrefix: String) : TextBlockFactory { + override fun fromString(text: String?): LineComment { + var textVar = text + textVar = textVar!!.replace(Regex("\t"), TextBlock.TAB_REPLACEMENT.toString()) + val indent = getWhitespacePrefix(*textVar.split(TextBlock.DELIMITER.toRegex()).dropLastWhile { it.isEmpty() } + .toTypedArray()) + return LineComment( + commentPrefix, + indent, + *Arrays.stream(textVar.split(TextBlock.DELIMITER.toRegex()).dropLastWhile { it.isEmpty() } + .toTypedArray()) + .map { s: String? -> + stripPrefix( + s!!, indent + ) + } + .map { obj: CharSequence -> trimPrefix(obj) } + .map { s: CharSequence? -> + stripPrefix( + s!!, + commentPrefix + ) + } + .collect(Collectors.toList()).toTypedArray()) + } + + override fun looksLike(text: String?): Boolean { + return Arrays.stream(text!!.split(TextBlock.DELIMITER.toRegex()).dropLastWhile { it.isEmpty() } + .toTypedArray()).allMatch { x: String -> + x.trim { it <= ' ' }.startsWith( + commentPrefix + ) + } + } + } + + override fun toString(): String { + return "$commentPrefix " + Arrays.stream(rawString()) + .collect(Collectors.joining(TextBlock.DELIMITER + indent + commentPrefix + " ")) + } + + override fun withIndent(indent: CharSequence?): LineComment { + return LineComment(commentPrefix, indent, *lines) + } +} diff --git a/src/main/resources/sources/kt/com/github/simiacryptus/aicoder/util/TextBlockFactory.kt b/src/main/resources/sources/kt/com/github/simiacryptus/aicoder/util/TextBlockFactory.kt new file mode 100644 index 00000000..870e2122 --- /dev/null +++ b/src/main/resources/sources/kt/com/github/simiacryptus/aicoder/util/TextBlockFactory.kt @@ -0,0 +1,12 @@ +package com.github.simiacryptus.aicoder.util + +interface TextBlockFactory { + fun fromString(text: String?): T + + @Suppress("unused") + fun toString(text: T): CharSequence? { + return text.toString() + } + + fun looksLike(text: String?): Boolean +} diff --git a/src/main/resources/sources/kt/com/github/simiacryptus/aicoder/util/UITools.kt b/src/main/resources/sources/kt/com/github/simiacryptus/aicoder/util/UITools.kt new file mode 100644 index 00000000..e4c615dd --- /dev/null +++ b/src/main/resources/sources/kt/com/github/simiacryptus/aicoder/util/UITools.kt @@ -0,0 +1,1102 @@ +@file:Suppress("UNNECESSARY_SAFE_CALL", "UNCHECKED_CAST") + +package com.github.simiacryptus.aicoder.util + +import com.github.simiacryptus.aicoder.config.ActionSettingsRegistry +import com.github.simiacryptus.aicoder.config.AppSettingsState +import com.github.simiacryptus.aicoder.config.Name +import com.google.common.util.concurrent.* +import com.intellij.openapi.actionSystem.AnActionEvent +import com.intellij.openapi.actionSystem.CommonDataKeys +import com.intellij.openapi.actionSystem.PlatformDataKeys +import com.intellij.openapi.application.ApplicationManager +import com.intellij.openapi.command.WriteCommandAction +import com.intellij.openapi.editor.Caret +import com.intellij.openapi.editor.Document +import com.intellij.openapi.fileEditor.FileDocumentManager +import com.intellij.openapi.fileEditor.FileEditorManager +import com.intellij.openapi.progress.ProgressIndicator +import com.intellij.openapi.progress.ProgressManager +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.openapi.ui.DialogWrapper +import com.intellij.openapi.util.TextRange +import com.intellij.openapi.vfs.LocalFileSystem +import com.intellij.openapi.vfs.VirtualFile +import com.intellij.ui.components.JBLabel +import com.intellij.ui.components.JBPasswordField +import com.intellij.ui.components.JBScrollPane +import com.intellij.ui.components.JBTextArea +import com.intellij.util.ui.FormBuilder +import com.simiacryptus.jopenai.OpenAIClient +import com.simiacryptus.jopenai.exceptions.ModerationException +import com.simiacryptus.jopenai.util.StringUtil +import org.jdesktop.swingx.JXButton +import org.slf4j.LoggerFactory +import java.awt.* +import java.awt.event.ComponentAdapter +import java.awt.event.ComponentEvent +import java.awt.event.WindowAdapter +import java.awt.event.WindowEvent +import java.beans.PropertyChangeEvent +import java.io.IOException +import java.io.PrintWriter +import java.io.StringWriter +import java.net.URI +import java.util.* +import java.util.concurrent.* +import java.util.concurrent.atomic.AtomicBoolean +import java.util.concurrent.atomic.AtomicReference +import java.util.function.Supplier +import javax.script.ScriptException +import javax.swing.* +import javax.swing.text.JTextComponent +import kotlin.math.max +import kotlin.reflect.KMutableProperty +import kotlin.reflect.KProperty1 +import kotlin.reflect.KVisibility +import kotlin.reflect.full.memberProperties +import kotlin.reflect.jvm.isAccessible +import kotlin.reflect.jvm.javaField +import kotlin.reflect.jvm.javaType + +object UITools { + private val log = LoggerFactory.getLogger(UITools::class.java) + val retry = WeakHashMap() + + @JvmStatic + fun redoableTask( + event: AnActionEvent, + request: Supplier, + ) { + Futures.addCallback(pool.submit { + request.get() + }, futureCallback(event, request), pool) + } + + @JvmStatic + fun futureCallback( + event: AnActionEvent, + request: Supplier, + ) = object : FutureCallback { + override fun onSuccess(undo: Runnable) { + val requiredData = event.getData(CommonDataKeys.EDITOR) ?: return + val document = requiredData.document + retry[document] = getRetry(event, request, undo) + } + + override fun onFailure(t: Throwable) { + error(log, "Error", t) + } + } + + @JvmStatic + fun getRetry( + event: AnActionEvent, + request: Supplier, + undo: Runnable, + ): Runnable { + return Runnable { + Futures.addCallback( + pool.submit { + WriteCommandAction.runWriteCommandAction(event.project) { undo?.run() } + request.get() + }, + futureCallback(event, request), + pool + ) + } + } + + @JvmStatic + fun replaceString(document: Document, startOffset: Int, endOffset: Int, newText: CharSequence): Runnable { + val oldText: CharSequence = document.getText(TextRange(startOffset, endOffset)) + document.replaceString(startOffset, endOffset, newText) + logEdit( + String.format( + "FWD replaceString from %s to %s (%s->%s): %s", + startOffset, + endOffset, + endOffset - startOffset, + newText.length, + newText + ) + ) + return Runnable { + val verifyTxt = document.getText(TextRange(startOffset, startOffset + newText.length)) + if (verifyTxt != newText) { + val msg = String.format( + "The text range from %d to %d does not match the expected text \"%s\" and is instead \"%s\"", + startOffset, + startOffset + newText.length, + newText, + verifyTxt + ) + throw IllegalStateException(msg) + } + document.replaceString(startOffset, startOffset + newText.length, oldText) + logEdit( + String.format( + "REV replaceString from %s to %s (%s->%s): %s", + startOffset, + startOffset + newText.length, + newText.length, + oldText.length, + oldText + ) + ) + } + } + + @JvmStatic + fun insertString(document: Document, startOffset: Int, newText: CharSequence): Runnable { + document.insertString(startOffset, newText) + logEdit(String.format("FWD insertString @ %s (%s): %s", startOffset, newText.length, newText)) + return Runnable { + val verifyTxt = document.getText(TextRange(startOffset, startOffset + newText.length)) + if (verifyTxt != newText) { + val message = String.format( + "The text range from %d to %d does not match the expected text \"%s\" and is instead \"%s\"", + startOffset, + startOffset + newText.length, + newText, + verifyTxt + ) + throw AssertionError(message) + } + document.deleteString(startOffset, startOffset + newText.length) + logEdit(String.format("REV deleteString from %s to %s", startOffset, startOffset + newText.length)) + } + } + + @JvmStatic + private fun logEdit(message: String) { + log.debug(message) + } + + @JvmStatic + @Suppress("unused") + fun deleteString(document: Document, startOffset: Int, endOffset: Int): Runnable { + val oldText: CharSequence = document.getText(TextRange(startOffset, endOffset)) + document.deleteString(startOffset, endOffset) + return Runnable { + document.insertString(startOffset, oldText) + logEdit(String.format("REV insertString @ %s (%s): %s", startOffset, oldText.length, oldText)) + } + } + + @JvmStatic + fun getIndent(caret: Caret?): CharSequence { + if (null == caret) return "" + val document = caret.editor.document + val documentText = document.text + val lineNumber = document.getLineNumber(caret.selectionStart) + val lines = documentText.split("\n".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray() + if (lines.isEmpty()) return "" + return IndentedText.fromString(lines[max(lineNumber, 0).coerceAtMost(lines.size - 1)]).indent + } + + @JvmStatic + @Suppress("unused") + fun hasSelection(e: AnActionEvent): Boolean { + val caret = e.getData(CommonDataKeys.CARET) + return null != caret && caret.hasSelection() + } + + @JvmStatic + fun getIndent(event: AnActionEvent): CharSequence { + val caret = event.getData(CommonDataKeys.CARET) + val indent: CharSequence = if (null == caret) { + "" + } else { + getIndent(caret) + } + return indent + } + + @JvmStatic + fun queryAPIKey(): CharSequence? { + val panel = JPanel() + val label = JLabel("Enter OpenAI API Key:") + val pass = JPasswordField(100) + panel.add(label) + panel.add(pass) + val options = arrayOf("OK", "Cancel") + return if (JOptionPane.showOptionDialog( + null, + panel, + "API Key", + JOptionPane.NO_OPTION, + JOptionPane.PLAIN_MESSAGE, + null, + options, + options[1] + ) == JOptionPane.OK_OPTION + ) { + val password = pass.password + java.lang.String(password) + } else { + null + } + } + + @JvmStatic + fun readKotlinUI(component: R, settings: T) { + val componentClass: Class<*> = component.javaClass + val declaredUIFields = + componentClass.kotlin.memberProperties.map { it.name }.toSet() + for (settingsField in settings.javaClass.kotlin.memberProperties) { + if (settingsField is KMutableProperty<*>) { + settingsField.isAccessible = true + val settingsFieldName = settingsField.name + try { + var newSettingsValue: Any? = null + if (!declaredUIFields.contains(settingsFieldName)) continue + val uiField: KProperty1 = + (componentClass.kotlin.memberProperties.find { it.name == settingsFieldName } as KProperty1?)!! + var uiVal = uiField.get(component) + if (uiVal is JScrollPane) { + uiVal = uiVal.viewport.view + } + when (settingsField.returnType.javaType.typeName) { + "java.lang.String" -> if (uiVal is JTextComponent) { + newSettingsValue = uiVal.text + } else if (uiVal is ComboBox<*>) { + newSettingsValue = uiVal.item + } + + "int", "java.lang.Integer" -> if (uiVal is JTextComponent) { + newSettingsValue = if (uiVal.text.isBlank()) -1 else uiVal.text.toInt() + } + + "long" -> if (uiVal is JTextComponent) { + newSettingsValue = if (uiVal.text.isBlank()) -1 else uiVal.text.toLong() + } + + "double", "java.lang.Double" -> if (uiVal is JTextComponent) { + newSettingsValue = if (uiVal.text.isBlank()) 0.0 else uiVal.text.toDouble() + } + + "boolean" -> if (uiVal is JCheckBox) { + newSettingsValue = uiVal.isSelected + } else if (uiVal is JTextComponent) { + newSettingsValue = java.lang.Boolean.parseBoolean(uiVal.text) + } + + else -> + if (Enum::class.java.isAssignableFrom(settingsField.returnType.javaType as Class<*>)) { + if (uiVal is ComboBox<*>) { + val comboBox = uiVal + val item = comboBox.item + val enumClass = settingsField.returnType.javaType as Class?> + val string = item.toString() + newSettingsValue = + findValue(enumClass, string) + } + } + } + settingsField.setter.call(settings, newSettingsValue) + } catch (e: Throwable) { + throw RuntimeException("Error processing $settingsField", e) + } + } + } + } + + @JvmStatic + fun findValue(enumClass: Class?>, string: String): Enum<*>? { + enumClass.enumConstants?.filter { it?.name?.compareTo(string, true) == 0 }?.forEach { return it } + return java.lang.Enum.valueOf( + enumClass, + string + ) + } + + @JvmStatic + fun writeKotlinUI(component: R, settings: T) { + val componentClass: Class<*> = component.javaClass + val declaredUIFields = + componentClass.kotlin.memberProperties.map { it.name }.toSet() + val memberProperties = settings.javaClass.kotlin.memberProperties + val publicProperties = memberProperties.filter { + it.visibility == KVisibility.PUBLIC //&& it is KMutableProperty<*> + } + for (settingsField in publicProperties) { + val fieldName = settingsField.name + try { + if (!declaredUIFields.contains(fieldName)) continue + val uiField: KProperty1 = + (componentClass.kotlin.memberProperties.find { it.name == fieldName } as KProperty1?)!! + val settingsVal = settingsField.get(settings) ?: continue + var uiVal = uiField.get(component) + if (uiVal is JScrollPane) { + uiVal = uiVal.viewport.view + } + when (settingsField.returnType.javaType.typeName) { + "java.lang.String" -> if (uiVal is JTextComponent) { + uiVal.text = settingsVal.toString() + } else if (uiVal is ComboBox<*>) { + uiVal.item = settingsVal.toString() + } + + "int", "java.lang.Integer" -> if (uiVal is JTextComponent) { + uiVal.text = (settingsVal as Int).toString() + } + + "long" -> if (uiVal is JTextComponent) { + uiVal.text = (settingsVal as Int).toLong().toString() + } + + "boolean" -> if (uiVal is JCheckBox) { + uiVal.isSelected = (settingsVal as Boolean) + } else if (uiVal is JTextComponent) { + uiVal.text = java.lang.Boolean.toString((settingsVal as Boolean)) + } + + "double", "java.lang.Double" -> if (uiVal is JTextComponent) { + uiVal.text = (settingsVal as Double).toString() + } + + else -> if (uiVal is ComboBox<*>) { + uiVal.item = settingsVal.toString() + } + } + } catch (e: Throwable) { + throw RuntimeException("Error processing $settingsField", e) + } + } + } + + @JvmStatic + fun addKotlinFields(ui: T, formBuilder: FormBuilder, fillVertically: Boolean) { + var first = true + for (field in ui.javaClass.kotlin.memberProperties) { + if (field.javaField == null) continue + try { + val nameAnnotation = field.annotations.find { it is Name } as Name? + val component = field.get(ui) as JComponent + if (nameAnnotation != null) { + if (first && fillVertically) { + first = false + formBuilder.addLabeledComponentFillVertically(nameAnnotation.value + ": ", component) + } else { + formBuilder.addLabeledComponent(JBLabel(nameAnnotation.value + ": "), component, 1, false) + } + } else { + formBuilder.addComponentToRightColumn(component, 1) + } + } catch (e: IllegalAccessException) { + throw RuntimeException(e) + } catch (e: Throwable) { + error(log, "Error processing " + field.name, e) + } + } + } + + @JvmStatic + fun getMaximumSize(factor: Double): Dimension { + val screenSize = Toolkit.getDefaultToolkit().screenSize + return Dimension((screenSize.getWidth() * factor).toInt(), (screenSize.getHeight() * factor).toInt()) + } + + @JvmStatic + fun showOptionDialog(mainPanel: JPanel?, vararg options: Any, title: String, modal: Boolean = true): Int { + val pane = getOptionPane(mainPanel, options) + val rootFrame = JOptionPane.getRootFrame() + pane.componentOrientation = rootFrame.componentOrientation + val dialog = JDialog(rootFrame, title, modal) + dialog.componentOrientation = rootFrame.componentOrientation + + val latch = if (!modal) CountDownLatch(1) else null + configure(dialog, pane, latch) + dialog.isVisible = true + if (!modal) latch?.await() + + dialog.dispose() + return getSelectedValue(pane, options) + } + + @JvmStatic + fun getOptionPane( + mainPanel: JPanel?, + options: Array, + ): JOptionPane { + val pane = JOptionPane( + mainPanel, + JOptionPane.PLAIN_MESSAGE, + JOptionPane.NO_OPTION, + null, + options, + options[0] + ) + pane.initialValue = options[0] + return pane + } + + @JvmStatic + fun configure(dialog: JDialog, pane: JOptionPane, latch: CountDownLatch? = null) { + val contentPane = dialog.contentPane + contentPane.layout = BorderLayout() + contentPane.add(pane, BorderLayout.CENTER) + + if (JDialog.isDefaultLookAndFeelDecorated() && UIManager.getLookAndFeel().supportsWindowDecorations) { + dialog.isUndecorated = true + pane.rootPane.windowDecorationStyle = JRootPane.PLAIN_DIALOG + } + dialog.isResizable = true + dialog.maximumSize = getMaximumSize(0.9) + dialog.pack() + dialog.setLocationRelativeTo(null as Component?) + val adapter: WindowAdapter = windowAdapter(pane, dialog) + dialog.addWindowListener(adapter) + dialog.addWindowFocusListener(adapter) + dialog.addComponentListener(object : ComponentAdapter() { + override fun componentShown(ce: ComponentEvent) { + // reset value to ensure closing works properly + pane.value = JOptionPane.UNINITIALIZED_VALUE + } + }) + pane.addPropertyChangeListener { event: PropertyChangeEvent -> + if (dialog.isVisible && event.source === pane && event.propertyName == JOptionPane.VALUE_PROPERTY && event.newValue != null && event.newValue !== JOptionPane.UNINITIALIZED_VALUE) { + dialog.isVisible = false + latch?.countDown() + } + } + + pane.selectInitialValue() + } + + @JvmStatic + private fun windowAdapter(pane: JOptionPane, dialog: JDialog): WindowAdapter { + val adapter: WindowAdapter = object : WindowAdapter() { + private var gotFocus = false + override fun windowClosing(we: WindowEvent) { + pane.value = null + } + + override fun windowClosed(e: WindowEvent) { + pane.removePropertyChangeListener { event: PropertyChangeEvent -> + if (dialog.isVisible && event.source === pane && event.propertyName == JOptionPane.VALUE_PROPERTY && event.newValue != null && event.newValue !== JOptionPane.UNINITIALIZED_VALUE) { + dialog.isVisible = false + } + } + dialog.contentPane.removeAll() + } + + override fun windowGainedFocus(we: WindowEvent) { + if (!gotFocus) { + pane.selectInitialValue() + gotFocus = true + } + } + } + return adapter + } + + @JvmStatic + private fun getSelectedValue(pane: JOptionPane, options: Array): Int { + val selectedValue = pane.value ?: return JOptionPane.CLOSED_OPTION + var counter = 0 + val maxCounter = options.size + while (counter < maxCounter) { + if (options[counter] == selectedValue) return counter + counter++ + } + return JOptionPane.CLOSED_OPTION + } + + @JvmStatic + fun wrapScrollPane(promptArea: JBTextArea?): JBScrollPane { + val scrollPane = JBScrollPane(promptArea) + scrollPane.horizontalScrollBarPolicy = JScrollPane.HORIZONTAL_SCROLLBAR_AS_NEEDED + scrollPane.verticalScrollBarPolicy = JScrollPane.VERTICAL_SCROLLBAR_AS_NEEDED + return scrollPane + } + + @JvmStatic + fun showCheckboxDialog( + promptMessage: String, + checkboxIds: Array, + checkboxDescriptions: Array, + ): Array { + val formBuilder = FormBuilder.createFormBuilder() + val checkboxMap = HashMap() + for (i in checkboxIds.indices) { + val checkbox = JCheckBox(checkboxDescriptions[i], null as Icon?, true) + checkboxMap[checkboxIds[i]] = checkbox + formBuilder.addComponent(checkbox) + } + val dialogResult = showOptionDialog(formBuilder.panel, "OK", title = promptMessage) + val selectedIds = ArrayList() + if (dialogResult == 0) { + for ((checkboxId, checkbox) in checkboxMap) { + if (checkbox.isSelected) { + selectedIds.add(checkboxId) + } + } + } + return selectedIds.toTypedArray() + } + + @JvmStatic + fun showRadioButtonDialog( + promptMessage: CharSequence, + vararg radioButtonDescriptions: CharSequence, + ): CharSequence? { + val formBuilder = FormBuilder.createFormBuilder() + val radioButtonMap = HashMap() + val buttonGroup = ButtonGroup() + for (i in radioButtonDescriptions.indices) { + val radioButton = JRadioButton(radioButtonDescriptions[i].toString(), null as Icon?, true) + radioButtonMap[radioButtonDescriptions[i].toString()] = radioButton + buttonGroup.add(radioButton) + formBuilder.addComponent(radioButton) + } + val dialogResult = showOptionDialog(formBuilder.panel, "OK", title = promptMessage.toString()) + if (dialogResult == 0) { + for ((radioButtonId, radioButton) in radioButtonMap) { + if (radioButton.isSelected) { + return radioButtonId + } + } + } + return null + } + + @JvmStatic + fun build( + component: T, + fillVertically: Boolean = true, + formBuilder: FormBuilder = FormBuilder.createFormBuilder(), + ): JPanel? { + addKotlinFields(component, formBuilder, fillVertically) + return formBuilder.addComponentFillVertically(JPanel(), 0).panel + } + + @JvmStatic + fun showDialog( + project: Project?, + uiClass: Class, + configClass: Class, + title: String = "Generate Project", + onComplete: (C) -> Unit = { _ -> }, + ): C? { + val component = uiClass.getConstructor().newInstance() + val config = configClass.getConstructor().newInstance() + val dialog = object : DialogWrapper(project) { + init { + this.init() + this.title = title + this.setOKButtonText("Generate") + this.setCancelButtonText("Cancel") + this.isResizable = true + //this.setPreferredFocusedComponent(this) + //this.setContent(this) + } + + override fun createCenterPanel(): JComponent? { + return build(component) + } + } + dialog.show() + if (dialog.isOK) { + readKotlinUI(component, config) + onComplete(config) + } + return config + } + + @JvmStatic + fun getSelectedFolder(e: AnActionEvent): VirtualFile? { + val dataContext = e.dataContext + val data = PlatformDataKeys.VIRTUAL_FILE.getData(dataContext) + if (data != null && data.isDirectory) { + return data + } + val editor = PlatformDataKeys.EDITOR.getData(dataContext) + if (editor != null) { + val file = FileDocumentManager.getInstance().getFile(editor.document) + if (file != null) { + return file.parent + } + } + return null + } + + @JvmStatic + fun getSelectedFile(e: AnActionEvent): VirtualFile? { + val dataContext = e.dataContext + val data = PlatformDataKeys.VIRTUAL_FILE.getData(dataContext) + if (data != null && !data.isDirectory) { + return data + } + val editor = PlatformDataKeys.EDITOR.getData(dataContext) + if (editor != null) { + val file = FileDocumentManager.getInstance().getFile(editor.document) + if (file != null) { + return file + } + } + return null + } + + @JvmStatic + fun writeableFn( + event: AnActionEvent, + fn: () -> Runnable, + ): Runnable { + val runnable = AtomicReference() + WriteCommandAction.runWriteCommandAction(event.project) { runnable.set(fn()) } + return runnable.get() + } + + class ModalTask( + project: Project, title: String, canBeCancelled: Boolean, val task: (ProgressIndicator) -> T + ) : Task.WithResult(project, title, canBeCancelled), Supplier { + private val result = AtomicReference() + private val isError = AtomicBoolean(false) + private val error = AtomicReference() + private val semaphore = Semaphore(0) + override fun compute(indicator: ProgressIndicator): T? { + val currentThread = Thread.currentThread() + val threads = ArrayList() + val scheduledFuture = scheduledPool.scheduleAtFixedRate({ + if (indicator.isCanceled) { + threads.forEach { it.interrupt() } + } + }, 0, 1, TimeUnit.SECONDS) + threads.add(currentThread) + return try { + result.set(task(indicator)) + result.get() + } catch (e: Throwable) { + error(log, "Error running task", e) + isError.set(true) + error.set(e) + null + } finally { + semaphore.release() + threads.remove(currentThread) + scheduledFuture.cancel(true) + } + } + + override fun get(): T { + semaphore.acquire() + semaphore.release() + if (isError.get()) { + throw error.get() + } + return result.get() + } + + } + + class BgTask( + project: Project, title: String, canBeCancelled: Boolean, val task: (ProgressIndicator) -> T + ) : Task.Backgroundable(project, title, canBeCancelled, DEAF), Supplier { + + private val result = AtomicReference() + private val isError = AtomicBoolean(false) + private val error = AtomicReference() + private val semaphore = Semaphore(0) + override fun run(indicator: ProgressIndicator) { + val currentThread = Thread.currentThread() + val threads = ArrayList() + val scheduledFuture = scheduledPool.scheduleAtFixedRate({ + if (indicator.isCanceled) { + threads.forEach { it.interrupt() } + } + }, 0, 1, TimeUnit.SECONDS) + threads.add(currentThread) + try { + val result = task(indicator) + this.result.set(result) + } catch (e: Throwable) { + error(log, "Error running task", e) + error.set(e) + isError.set(true) + } finally { + semaphore.release() + threads.remove(currentThread) + scheduledFuture.cancel(true) + } + } + + override fun get(): T { + semaphore.acquire() + semaphore.release() + if (isError.get()) { + throw error.get() + } + return result.get() + } + } + + @JvmStatic + fun run( + project: Project?, + title: String?, + canBeCancelled: Boolean = true, + suppressProgress: Boolean = true, + task: (ProgressIndicator) -> T, + ): T { + return if (project == null || suppressProgress == AppSettingsState.instance.editRequests) { + checkApiKey() + task(AbstractProgressIndicatorBase()) + } else { + checkApiKey() + val t = if (AppSettingsState.instance.modalTasks) ModalTask(project, title ?: "", canBeCancelled, task) + else BgTask(project, title ?: "", canBeCancelled, task) + ProgressManager.getInstance().run(t) + t.get() + } + } + + @JvmStatic + fun checkApiKey(k: String = AppSettingsState.instance.apiKey): String { + var key = k + if (key.isEmpty()) { + synchronized(OpenAIClient::class.java) { + key = AppSettingsState.instance.apiKey + if (key.isEmpty()) { + key = queryAPIKey()!!.toString() + AppSettingsState.instance.apiKey = key + } + } + } + return key + } + + @JvmStatic + val threadFactory: ThreadFactory = ThreadFactoryBuilder().setNameFormat("API Thread %d").build() + + @JvmStatic + val pool: ListeningExecutorService = MoreExecutors.listeningDecorator( + ThreadPoolExecutor( + /* corePoolSize = */ AppSettingsState.instance.apiThreads, + /* maximumPoolSize = */ AppSettingsState.instance.apiThreads, + /* keepAliveTime = */ 0L, /* unit = */ TimeUnit.MILLISECONDS, + /* workQueue = */ LinkedBlockingQueue(), + /* threadFactory = */ threadFactory, + /* handler = */ ThreadPoolExecutor.AbortPolicy() + ) + ) + + @JvmStatic + val scheduledPool: ListeningScheduledExecutorService = + MoreExecutors.listeningDecorator(ScheduledThreadPoolExecutor(1, threadFactory)) + + @JvmStatic + fun map( + moderateAsync: ListenableFuture, + o: com.google.common.base.Function, + ): ListenableFuture = Futures.transform(moderateAsync, o::apply, pool) + + @JvmStatic + fun filterStringResult( + indent: CharSequence = "", + stripUnbalancedTerminators: Boolean = true, + ): (CharSequence) -> CharSequence { + return { text -> + var result: CharSequence = text.toString().trim { it <= ' ' } + if (stripUnbalancedTerminators) { + result = StringUtil.stripUnbalancedTerminators(result) + } + result = IndentedText.fromString(result.toString()).withIndent(indent).toString() + indent.toString() + result + } + } + + private val errorLog = mutableListOf>() + private val actionLog = mutableListOf() + + @JvmStatic + fun logAction(message: String) { + actionLog += message + } + + private val singleThreadPool = Executors.newSingleThreadExecutor() + + @JvmStatic + fun error(log: org.slf4j.Logger, msg: String, e: Throwable) { + log?.error(msg, e) + errorLog += Pair(msg, e) + singleThreadPool.submit { + if (AppSettingsState.instance.suppressErrors) { + return@submit + } else if (e.matches { ModerationException::class.java.isAssignableFrom(it.javaClass) }) { + JOptionPane.showMessageDialog( + null, + e.message, + "This request was rejected by OpenAI Moderation", + JOptionPane.WARNING_MESSAGE + ) + } else if (e.matches { + java.lang.InterruptedException::class.java.isAssignableFrom(it.javaClass) && it.message?.contains( + "sleep interrupted" + ) == true + }) { + JOptionPane.showMessageDialog( + null, + "This request was cancelled by the user", + "User Cancelled Request", + JOptionPane.WARNING_MESSAGE + ) + } else if (e.matches { IOException::class.java.isAssignableFrom(it.javaClass) && it.message?.contains("Incorrect API key") == true }) { + + val formBuilder = FormBuilder.createFormBuilder() + + formBuilder.addLabeledComponent( + "Error", + JLabel("The API key was rejected by the server.") + ) + + val apiKeyInput = JBPasswordField() + //bugReportTextArea.rows = 40 + apiKeyInput.columns = 80 + apiKeyInput.isEditable = true + //apiKeyInput.text = """""".trimMargin() + formBuilder.addLabeledComponent("API Key", apiKeyInput) + + val openAccountButton = JXButton("Open Account Page") + openAccountButton.addActionListener { + Desktop.getDesktop().browse(URI("https://platform.openai.com/account/api-keys")) + } + formBuilder.addLabeledComponent("OpenAI Account", openAccountButton) + + val testButton = JXButton("Test Key") + testButton.addActionListener { + val apiKey = apiKeyInput.password.joinToString("") + try { + OpenAIClient(key = apiKey).listModels() + JOptionPane.showMessageDialog( + null, + "The API key was accepted by the server. The new value will be saved.", + "Success", + JOptionPane.INFORMATION_MESSAGE + ) + AppSettingsState.instance.apiKey = apiKey + } catch (e: Exception) { + JOptionPane.showMessageDialog( + null, + "The API key was rejected by the server.", + "Failure", + JOptionPane.WARNING_MESSAGE + ) + return@addActionListener + } + } + formBuilder.addLabeledComponent("Validation", testButton) + val showOptionDialog = showOptionDialog( + formBuilder.panel, + "Dismiss", + title = "Error", + modal = true + ) + log.info("showOptionDialog = $showOptionDialog") + } else if (e.matches { ScriptException::class.java.isAssignableFrom(it.javaClass) }) { + val scriptException = + e.get { ScriptException::class.java.isAssignableFrom(it.javaClass) } as ScriptException? + val dynamicActionException = + e.get { ActionSettingsRegistry.DynamicActionException::class.java.isAssignableFrom(it.javaClass) } as ActionSettingsRegistry.DynamicActionException? + val formBuilder = FormBuilder.createFormBuilder() + + formBuilder.addLabeledComponent( + "Error", + JLabel("An error occurred while executing the dynamic action.") + ) + + val bugReportTextArea = JBTextArea() + bugReportTextArea.rows = 40 + bugReportTextArea.columns = 80 + bugReportTextArea.isEditable = false + bugReportTextArea.text = """ + |Action Name: ${dynamicActionException?.actionSetting?.displayText} + |Action ID: ${dynamicActionException?.actionSetting?.id} + |Script Error: ${scriptException?.message} + | + |Error Details: + |``` + |${toString(e)} + |``` + |""".trimMargin() + formBuilder.addLabeledComponent("Error Report", wrapScrollPane(bugReportTextArea)) + + if (dynamicActionException?.actionSetting?.isDynamic == false) { + val openButton = JXButton("Revert to Default") + openButton.addActionListener { + dynamicActionException?.actionSetting?.file?.delete() + } + formBuilder.addLabeledComponent("Revert Built-in Action", openButton) + } + + if (null != dynamicActionException) { + val openButton = JXButton("Open Dynamic Action") + openButton.addActionListener { + dynamicActionException?.file?.let { + val project = ApplicationManager.getApplication().runReadAction { + com.intellij.openapi.project.ProjectManager.getInstance().openProjects.firstOrNull() + } + if (it.exists()) { + ApplicationManager.getApplication().invokeLater { + val virtualFile = LocalFileSystem.getInstance().refreshAndFindFileByIoFile(it) + FileEditorManager.getInstance(project!!).openFile(virtualFile!!, true) + } + } else { + Thread { + showOptionDialog( + formBuilder.panel, + "Dismiss", + title = "Error - File Not Found", + modal = true + ) + }.start() + } + } + + } + formBuilder.addLabeledComponent("View Code", openButton) + } + + val supressFutureErrors = JCheckBox("Suppress Future Error Popups") + supressFutureErrors.isSelected = false + formBuilder.addComponent(supressFutureErrors) + + val showOptionDialog = showOptionDialog( + formBuilder.panel, + "Dismiss", + title = "Error", + modal = true + ) + log.info("showOptionDialog = $showOptionDialog") + if (supressFutureErrors.isSelected) { + AppSettingsState.instance.suppressErrors = true + } + } else { + val formBuilder = FormBuilder.createFormBuilder() + + formBuilder.addLabeledComponent( + "Error", + JLabel("Oops! Something went wrong. An error report has been generated. You can copy and paste the report below into a new issue on our Github page.") + ) + + val bugReportTextArea = JBTextArea() + bugReportTextArea.rows = 40 + bugReportTextArea.columns = 80 + bugReportTextArea.isEditable = false + bugReportTextArea.text = """ + |Log Message: $msg + |Error Message: ${e.message} + |Error Type: ${e.javaClass.name} + |API Base: ${AppSettingsState.instance.apiBase} + |Token Counter: ${AppSettingsState.instance.tokenCounter} + | + |OS: ${System.getProperty("os.name")} / ${System.getProperty("os.version")} / ${System.getProperty("os.arch")} + |Locale: ${Locale.getDefault().country} / ${Locale.getDefault().language} + | + |Error Details: + |``` + |${toString(e)} + |``` + | + |Action History: + | + |${actionLog.joinToString("\n") { "* ${it.replace("\n", "\n ")}" }} + | + |Error History: + | + |${ + errorLog.filter { it.second != e }.joinToString("\n") { + """ + |${it.first} + |``` + |${toString(it.second)} + |``` + |""".trimMargin() + } + } + |""".trimMargin() + formBuilder.addLabeledComponent("System Report", wrapScrollPane(bugReportTextArea)) + + val openButton = JXButton("Open New Issue on our Github page") + openButton.addActionListener { + Desktop.getDesktop().browse(URI("https://github.com/SimiaCryptus/intellij-aicoder/issues/new")) + } + formBuilder.addLabeledComponent("Report Issue/Request Help", openButton) + + val supressFutureErrors = JCheckBox("Suppress Future Error Popups") + supressFutureErrors.isSelected = false + formBuilder.addComponent(supressFutureErrors) + + val showOptionDialog = showOptionDialog( + formBuilder.panel, + "Dismiss", + title = "Error", + modal = true + ) + log.info("showOptionDialog = $showOptionDialog") + if (supressFutureErrors.isSelected) { + AppSettingsState.instance.suppressErrors = true + } + } + } + } + + @JvmStatic + fun Throwable.matches(matchFn: (Throwable) -> Boolean): Boolean { + if (matchFn(this)) return true + if (this.cause != null && this.cause !== this) return this.cause!!.matches(matchFn) + return false + } + + @JvmStatic + fun Throwable.get(matchFn: (Throwable) -> Boolean): Throwable? { + if (matchFn(this)) return this + if (this.cause != null && this.cause !== this) return this.cause!!.get(matchFn) + return null + } + + @JvmStatic + fun toString(e: Throwable): String { + val sw = StringWriter() + val pw = PrintWriter(sw) + e.printStackTrace(pw) + return sw.toString() + } + + // Wrap JOptionPane.showInputDialog + @JvmStatic + fun showInputDialog( + parentComponent: Component?, + message: Any?, + title: String?, + messageType: Int + ): Any? { + val icon = null + val selectionValues = null + val initialSelectionValue = null + val pane = JOptionPane( + message, + messageType, + JOptionPane.OK_CANCEL_OPTION, + icon, + null, + null + ) + pane.wantsInput = true + pane.selectionValues = selectionValues + pane.initialSelectionValue = initialSelectionValue + //pane.isComponentOrientationLeftToRight = true + val dialog = pane.createDialog(parentComponent, title) + pane.selectInitialValue() + dialog.show() + dialog.dispose() + val value = pane.inputValue + return if (value == JOptionPane.UNINITIALIZED_VALUE) null else value + } + +} + diff --git a/src/main/resources/sources/kt/com/github/simiacryptus/aicoder/util/psi/PsiClassContext.kt b/src/main/resources/sources/kt/com/github/simiacryptus/aicoder/util/psi/PsiClassContext.kt new file mode 100644 index 00000000..f4db94d8 --- /dev/null +++ b/src/main/resources/sources/kt/com/github/simiacryptus/aicoder/util/psi/PsiClassContext.kt @@ -0,0 +1,146 @@ +package com.github.simiacryptus.aicoder.util.psi + +import com.github.simiacryptus.aicoder.util.ComputerLanguage +import com.intellij.psi.PsiElement +import com.intellij.psi.PsiElementVisitor +import com.intellij.psi.PsiFile + +class PsiClassContext(val text: String, private val isPrior: Boolean, private val isOverlap: Boolean, val language: ComputerLanguage) { + val children = ArrayList() + + /** + * This java code is initializing a PsiClassContext object. It is doing this by creating a PsiElementVisitor and using it to traverse the PsiFile. + * It is checking the text range of each element and whether it is prior to, overlapping, or within the selectionStart and selectionEnd parameters. + * Depending on the element, it is adding the text of the element to the PsiClassContext object, or recursively visiting its children. + * + * @param psiFile + * @param selectionStart + * @param selectionEnd + * @return + */ + fun init(psiFile: PsiFile?, selectionStart: Int, selectionEnd: Int): PsiClassContext { + object : PsiVisitorBase() { + var currentContext = this@PsiClassContext + var indent = "" + override fun visit(element: PsiElement, self: PsiElementVisitor) { + val text = element.text + val textRange = element.textRange + val textRangeEndOffset = textRange.endOffset + 1 + val textRangeStartOffset = textRange.startOffset + // Check if the element comes before the selection + val isPrior = textRangeEndOffset < selectionStart + // Check if the element overlaps with the selection + val isOverlap = + textRangeStartOffset in selectionStart..selectionEnd || textRangeEndOffset in selectionStart..selectionEnd || selectionStart in textRangeStartOffset..textRangeEndOffset || selectionEnd in textRangeStartOffset..textRangeEndOffset + // Check if the element is within the selection + val within = + selectionStart in textRangeStartOffset until textRangeEndOffset && textRangeStartOffset <= selectionEnd && textRangeEndOffset > selectionEnd + if (PsiUtil.matchesType(element, "ImportList")) { + currentContext.children.add(PsiClassContext(text.trim { it <= ' ' }, isPrior, isOverlap, language)) + } else if (PsiUtil.matchesType(element, "Comment", "DocComment")) { + if (within) { + currentContext.children.add( + PsiClassContext( + indent + text.trim { it <= ' ' }, + false, + true, + language + ) + ) + } + } else if (PsiUtil.matchesType(element, "Field")) { + processChildren( + element, + self, + isPrior, + isOverlap, + indent + PsiUtil.getDeclaration(element).trim { it <= ' ' } + if (isOverlap) " {" else ";") + } else if (PsiUtil.matchesType(element, "Method", "Function", "FunctionDefinition", "Constructor")) { + val methodTerminator = when (language) { + ComputerLanguage.Java -> " { /* ... */}" + ComputerLanguage.Kotlin -> " { /* ... */}" + ComputerLanguage.Scala -> " { /* ... */}" + else -> ";" + } + processChildren( + element, + self, + isPrior, + isOverlap, + indent + PsiUtil.getDeclaration(element) + .trim { it <= ' ' } + (if (isOverlap) " {" else methodTerminator)) + } else if (PsiUtil.matchesType(element, "LocalVariable")) { + currentContext.children.add(PsiClassContext(indent + text.trim { it <= ' ' } + ";", + isPrior, + isOverlap, + language)) + } else if (PsiUtil.matchesType(element, "Class")) { + processChildren( + element, + self, + isPrior, + isOverlap, + indent + text.substring(0, text.indexOf('{')).trim { it <= ' ' } + " {") + if (!isOverlap) { + currentContext.children.add(PsiClassContext("}", isPrior, false, language)) + } + } else if (!isOverlap && PsiUtil.matchesType(element, "CodeBlock", "ForStatement")) { + // Skip + } else { + element.acceptChildren(self) + } + } + + private fun processChildren( + element: PsiElement, + self: PsiElementVisitor, + isPrior: Boolean, + isOverlap: Boolean, + declarationText: String + ): PsiClassContext { + val newNode = PsiClassContext(declarationText, isPrior, isOverlap, language) + currentContext.children.add(newNode) + val prevclassBuffer = currentContext + currentContext = newNode + val prevIndent = indent + indent += " " + element.acceptChildren(self) + indent = prevIndent + currentContext = prevclassBuffer + return newNode + } + }.build(psiFile!!) + return this + } + + override fun toString(): String { + val sb = ArrayList() + sb.add(text) + children.stream().filter { x: PsiClassContext -> x.isPrior }.map { obj: PsiClassContext -> obj.toString() } + .forEach { e: String -> sb.add(e) } + children.stream().filter { x: PsiClassContext -> !x.isOverlap && !x.isPrior } + .map { obj: PsiClassContext -> obj.toString() } + .forEach { e: String -> sb.add(e) } + children.stream().filter { x: PsiClassContext -> x.isOverlap }.map { obj: PsiClassContext -> obj.toString() } + .forEach { e: String -> sb.add(e) } + return sb.stream().reduce { l: String, r: String -> + """ + $l + $r + """.trimIndent() + }.get() + } + + companion object { + @JvmStatic + fun getContext( + psiFile: PsiFile?, + selectionStart: Int, + selectionEnd: Int, + language: ComputerLanguage + ): PsiClassContext { + return PsiClassContext("", false, true, language).init(psiFile, selectionStart, selectionEnd) + } + + } +} diff --git a/src/main/resources/sources/kt/com/github/simiacryptus/aicoder/util/psi/PsiTranslationTree.kt b/src/main/resources/sources/kt/com/github/simiacryptus/aicoder/util/psi/PsiTranslationTree.kt new file mode 100644 index 00000000..937970fa --- /dev/null +++ b/src/main/resources/sources/kt/com/github/simiacryptus/aicoder/util/psi/PsiTranslationTree.kt @@ -0,0 +1,256 @@ +package com.github.simiacryptus.aicoder.util.psi + +import com.github.simiacryptus.aicoder.config.AppSettingsState +import com.github.simiacryptus.aicoder.util.ComputerLanguage +import com.github.simiacryptus.aicoder.util.IdeaOpenAIClient +import com.github.simiacryptus.aicoder.util.UITools.filterStringResult +import com.intellij.openapi.application.runReadAction +import com.intellij.openapi.project.Project +import com.intellij.psi.PsiElement +import com.intellij.psi.PsiElementVisitor +import com.intellij.psi.PsiFile +import com.simiacryptus.jopenai.proxy.ChatProxy +import org.slf4j.LoggerFactory +import java.util.* +import java.util.concurrent.ExecutionException +import java.util.concurrent.TimeoutException +import java.util.regex.Pattern + +class PsiTranslationTree( + private val stubId: String?, + private val prefix: String?, + private val elementText: String?, + val sourceLanguage: ComputerLanguage, + val targetLanguage: ComputerLanguage, +) { + val suffix = StringBuffer() + val children = ArrayList() + + private fun getStubRegex( + targetLanguage: ComputerLanguage?, + translatedOuter: CharSequence?, + ): Regex { + var regex: String + when (targetLanguage) { + ComputerLanguage.Python -> + regex = "(?s)(?<=\n|^)[^\n]*?:\n[^\n]*?$stubId\\s*?pass\\s*?" + + ComputerLanguage.Rust, ComputerLanguage.Kotlin, ComputerLanguage.Scala, ComputerLanguage.Java, ComputerLanguage.JavaScript -> + regex = "(?s)(?<=\n|^)[^\n]*?\\{[^{}]*?$stubId.*?\\}" + + else -> { + regex = "(?s)(?<=\n|^)[^\n]*?\\{[^{}]*?$stubId.*?\\}" + regex = if (Pattern.compile(regex).matcher(translatedOuter!!).find()) regex else stubId!! + } + } + return regex.toRegex() + } + + @Volatile + var translatedResult: CharSequence? = null + + private val stubs: List + get() = listOf( + children.filter { it.stubId != null }, + children.filter { it.stubId == null }.flatMap { it.stubs } + ).flatten() + + private fun executeTranslation( + indent: CharSequence, + ): CharSequence { + if (null == translatedResult) { + synchronized(this) { + if (null == translatedResult) { + val text = translationText() + val filterStringResult = filterStringResult(indent)( + proxy.convert( + text = text, + sourceLanguage.name, + targetLanguage.name + ).code ?: "" + ) + translatedResult = filterStringResult + } + } + } + return translatedResult!! + } + + private fun translationText(): String { + return if (null != stubId) { + elementText ?: "" + } else { + toString() + } + } + + private fun translateTree( + project: Project, + indent: CharSequence, + ) { + executeTranslation(indent) + for (stub in stubs) { + stub.translateTree(project, indent) + } + } + + private fun getTranslatedDocument(): CharSequence = try { + var translated = translatedResult.toString() + if (!stubs.isEmpty()) { + log.warn( + "Translating ${stubs.size} stubs in ${stubId ?: "---"} - Initial Code: \n```\n ${ + translationText().replace( + "\n", + "\n " + ) + }\n```\n" + ) + } + for (child in stubs) { + translated = if (child.stubId == null) translated + else { + val regex = child.getStubRegex(targetLanguage, translated) + val childDoc = child.getTranslatedDocument().toString() + val findAll = regex.findAll(translated).toList() + log.warn( + "Replacing ${findAll.size} instances of ${child.stubId} with: \n```\n ${ + childDoc.replace( + "\n", + "\n " + ) + }\n```\n" + ) + translated.replace(regex, childDoc.replace("\$", "\\\$")) + } + } + log.warn("Translation for ${stubId ?: "---"}: \n```\n ${translated.replace("\n", "\n ")}\n```\n") + translated + } catch (e: InterruptedException) { + throw RuntimeException(e) + } catch (e: ExecutionException) { + throw RuntimeException(e) + } catch (e: TimeoutException) { + throw RuntimeException(e) + } + + inner class Parser(val indent: String = "") : PsiElementVisitor() { + + override fun visitElement(element: PsiElement) { + val text = element.text + if (PsiUtil.matchesType(element, "Class", "ImplItem")) { + val newNode = + PsiTranslationTree(null, getClassDefPrefix(text), element.text, sourceLanguage, targetLanguage) + children.add(newNode) + element.acceptChildren(Parser(indent + " ")) + newNode.suffix.append("}") + } else if (PsiUtil.matchesType( + element, + "Method", + "FunctionDefinition", + "Function", + "StructItem", + "Struct" + ) + ) { + val declaration = PsiUtil.getDeclaration(element).trim() + val stubID = "STUB: " + UUID.randomUUID().toString().substring(0, 8) + // TODO: This needs to support arbitrary languages + val newNode = PsiTranslationTree( + stubID, + stubMethodText(declaration, stubID), + element.text, + sourceLanguage, + targetLanguage + ) + children.add(newNode) + element.acceptChildren(Parser(indent + " ")) + } else if (PsiUtil.matchesType(element, "ImportList", "Field")) { + val newNode = + PsiTranslationTree(null, element.text.trim(), element.text, sourceLanguage, targetLanguage) + children.add(newNode) + element.acceptChildren(Parser(indent + " ")) + } else { + element.acceptChildren(this) + } + } + + private fun stubMethodText(declaration: String, stubID: String): String { + return when (sourceLanguage) { + ComputerLanguage.Python -> String.format( + """ + |%s + | %s + | pass""".trimMargin().trim(), + declaration, + targetLanguage.lineComment.fromString(stubID) + ) + + ComputerLanguage.Go, ComputerLanguage.Kotlin, ComputerLanguage.Scala, ComputerLanguage.Java, ComputerLanguage.JavaScript, ComputerLanguage.Rust -> String.format( + "%s {\n%s\n}\n", + declaration, + targetLanguage.lineComment.fromString(stubID) + ) + + else -> String.format( + "%s {\n%s\n}\n", + declaration, + targetLanguage.lineComment.fromString(stubID) + ) + } + } + + private fun getClassDefPrefix(text: String): String { + val prefixToCurly = text.substring(0, text.indexOf('{')).trim() + return when (sourceLanguage) { + ComputerLanguage.Python -> indent + text.substring(0, text.indexOf(':')).trim() + ":" + ComputerLanguage.Go, ComputerLanguage.Kotlin, ComputerLanguage.Scala, ComputerLanguage.Java, ComputerLanguage.JavaScript, ComputerLanguage.Rust -> { + "$indent$prefixToCurly {" + } + + else -> "$indent$prefixToCurly {" + } + } + + } + + override fun toString(): String { + val sb = ArrayList() + sb.add(prefix ?: "") + if (stubId == null) children.map { it.toString() }.forEach { sb.add(it) } + sb.add(suffix.toString()) + return sb.joinToString("\n") + } + + companion object { + + private val log = LoggerFactory.getLogger(PsiTranslationTree::class.java) + + fun parseFile( + psiFile: PsiFile, + sourceLanguage: ComputerLanguage, + targetLanguage: ComputerLanguage, + ): PsiTranslationTree { + val psiTranslationTree = PsiTranslationTree(null, "", psiFile.text, sourceLanguage, targetLanguage) + runReadAction { + psiFile.accept(psiTranslationTree.Parser()) + } + return psiTranslationTree + } + + interface VirtualAPI { + fun convert(text: String, from_language: String, to_language: String): ConvertedText + data class ConvertedText( + val code: String? = null, + val language: String? = null, + ) + } + + val proxy: VirtualAPI + get() = ChatProxy( + clazz = VirtualAPI::class.java, + api = IdeaOpenAIClient.api, + model = AppSettingsState.instance.defaultChatModel(), + deserializerRetries = 5, + ).create() + } +} diff --git a/src/main/resources/sources/kt/com/github/simiacryptus/aicoder/util/psi/PsiUtil.kt b/src/main/resources/sources/kt/com/github/simiacryptus/aicoder/util/psi/PsiUtil.kt new file mode 100644 index 00000000..a3c301aa --- /dev/null +++ b/src/main/resources/sources/kt/com/github/simiacryptus/aicoder/util/psi/PsiUtil.kt @@ -0,0 +1,256 @@ +package com.github.simiacryptus.aicoder.util.psi + +import com.intellij.openapi.actionSystem.AnActionEvent +import com.intellij.openapi.actionSystem.CommonDataKeys +import com.intellij.openapi.util.TextRange +import com.intellij.psi.PsiElement +import com.intellij.psi.PsiElementVisitor +import com.simiacryptus.jopenai.util.StringUtil +import java.util.* +import java.util.concurrent.atomic.AtomicReference +import java.util.stream.Collectors +import java.util.stream.Stream + +object PsiUtil { + @JvmStatic + val ELEMENTS_CODE = arrayOf( + "Method", + "Field", + "Class", + "Function", + "CssBlock", + "FunctionDefinition" + ) + @JvmStatic + val ELEMENTS_COMMENTS = arrayOf( + "Comment" + ) + + fun getAll(element: PsiElement, vararg types: CharSequence): List { + val elements: MutableList = ArrayList() + val visitor = AtomicReference() + visitor.set(object : PsiElementVisitor() { + override fun visitElement(element: PsiElement) { + if (matchesType(element, *types)) { + elements.add(element) + } else { + element.acceptChildren(visitor.get()) + } + super.visitElement(element) + } + }) + element.accept(visitor.get()) + return elements + } + + @JvmStatic + fun getSmallestIntersecting( + element: PsiElement, + selectionStart: Int, + selectionEnd: Int, + vararg types: CharSequence + ): PsiElement? { + val largest = AtomicReference(null) + val visitor = AtomicReference() + visitor.set(object : PsiElementVisitor() { + override fun visitElement(element: PsiElement) { + val textRange = element.textRange + if (intersects(TextRange(selectionStart, selectionEnd), textRange)) { + if (matchesType(element, *types)) { + largest.updateAndGet { s: PsiElement? -> + if ((s?.text?.length ?: Int.MAX_VALUE) < element.text.length) s else element + } + } + } + //System.out.printf("%s : %s%n", simpleName, element.getText()); + super.visitElement(element) + element.acceptChildren(visitor.get()) + } + }) + element.accept(visitor.get()) + return largest.get() + } + + @JvmStatic + private fun within(textRange: TextRange, vararg offset: Int): Boolean = + offset.any { it in textRange.startOffset..textRange.endOffset } + + private fun intersects(a: TextRange, b: TextRange): Boolean { + return within(a, b.startOffset, b.endOffset) || within(b, a.startOffset, a.endOffset) + } + + + @JvmStatic + fun matchesType(element: PsiElement, vararg types: CharSequence): Boolean { + return matchesType(element.javaClass.simpleName, types) + } + + @JvmStatic + fun matchesType(simpleName: CharSequence, types: Array): Boolean { + var simpleName1 = simpleName + simpleName1 = StringUtil.stripSuffix(simpleName1, "Impl") + simpleName1 = StringUtil.stripPrefix(simpleName1, "Psi") + val str = simpleName1.toString() + return Stream.of(*types) + .map { s: CharSequence? -> + StringUtil.stripSuffix( + s!!, "Impl" + ) + } + .map { s: String? -> + StringUtil.stripPrefix( + s!!, "Psi" + ) + } + .anyMatch { t: CharSequence -> str.endsWith(t.toString()) } + } + + @JvmStatic + private fun getFirstBlock(element: PsiElement, vararg blockType: CharSequence): PsiElement? { + val children = element.children + if (children.isEmpty()) return null + val first = children[0] + return if (matchesType(first, *blockType)) first else null + } + + @JvmStatic + private fun getLargestBlock(element: PsiElement, vararg blockType: CharSequence): PsiElement? { + val largest = AtomicReference(null) + val visitor = AtomicReference() + visitor.set(object : PsiElementVisitor() { + override fun visitElement(element: PsiElement) { + if (matchesType(element, *blockType)) { + largest.updateAndGet { s: PsiElement? -> if (s != null && s.text.length > element.text.length) s else element } + super.visitElement(element) + } else { + super.visitElement(element) + } + element.acceptChildren(visitor.get()) + } + }) + element.accept(visitor.get()) + return largest.get() + } + + fun printTree(element: PsiElement): String { + val builder = StringBuilder() + printTree(element, builder, 0) + return builder.toString() + } + + private fun printTree(element: PsiElement, builder: StringBuilder, level: Int) { + builder.append(" ".repeat(0.coerceAtLeast(level))) + val elementClass: Class = element.javaClass + val simpleName = getName(elementClass) + builder.append(simpleName).append(" ").append(element.text.replace("\n".toRegex(), "\\\\n")) + builder.append("\n") + for (child in element.children) { + printTree(child, builder, level + 1) + } + } + + private fun getName(elementClass: Class<*>): String { + var elementClassVar = elementClass + val stringBuilder = StringBuilder() + val interfaces = getInterfaces(elementClassVar) + while (elementClassVar != Any::class.java) { + if (stringBuilder.isNotEmpty()) stringBuilder.append("/") + stringBuilder.append(elementClassVar.simpleName) + elementClassVar = elementClassVar.superclass + } + stringBuilder.append("[ ") + stringBuilder.append(interfaces.stream().sorted().collect(Collectors.joining(","))) + stringBuilder.append("]") + return stringBuilder.toString() + } + + @JvmStatic + private fun getInterfaces(elementClass: Class<*>): Set { + val strings = Arrays.stream(elementClass.interfaces).map { obj: Class<*> -> obj.simpleName } + .collect( + Collectors.toCollection { HashSet() } + ) + if (elementClass.superclass != Any::class.java) strings.addAll(getInterfaces(elementClass.superclass)) + return strings + } + + @JvmStatic + private fun getLargestContainedEntity(element: PsiElement?, selectionStart: Int, selectionEnd: Int): PsiElement? { + if (null == element) return null + val textRange = element.textRange + if (textRange.startOffset >= selectionStart && textRange.endOffset <= selectionEnd) return element + var largestContainedChild: PsiElement? = null + for (child in element.children) { + val entity = getLargestContainedEntity(child, selectionStart, selectionEnd) + if (null != entity) { + if (largestContainedChild == null || largestContainedChild.textRange.length < entity.textRange.length) { + largestContainedChild = entity + } + } + } + return largestContainedChild + } + + @JvmStatic + fun getLargestContainedEntity(e: AnActionEvent): PsiElement? { + val caret = e.getData(CommonDataKeys.CARET) + ?: return null + var psiFile: PsiElement? = e.getData(CommonDataKeys.PSI_FILE) + ?: return null + val selectionStart = caret.selectionStart + val selectionEnd = caret.selectionEnd + val largestContainedEntity = getLargestContainedEntity(psiFile, selectionStart, selectionEnd) + if (largestContainedEntity != null) psiFile = largestContainedEntity + return psiFile + } + + @JvmStatic + fun getCodeElement( + psiFile: PsiElement?, + selectionStart: Int, + selectionEnd: Int + ) = getSmallestIntersecting(psiFile!!, selectionStart.toInt(), selectionEnd.toInt(), *ELEMENTS_CODE) + + @JvmStatic + fun getDeclaration(element: PsiElement): String { + var declaration: CharSequence = element.text + declaration = StringUtil.stripPrefix(declaration.toString().trim { it <= ' ' }, + getDocComment(element).trim { it <= ' ' }) + declaration = + StringUtil.stripSuffix(declaration.toString().trim { it <= ' ' }, getCode(element).trim { it <= ' ' }) + return declaration.toString().trim { it <= ' ' } + } + + @JvmStatic + fun getCode(element: PsiElement): String { + val codeBlock = getLargestBlock( + element, + "CodeBlock", + "BlockExpr", + "Block", + "BlockExpression", + "StatementList", + "BlockFields" + ) + var code = "" + if (null != codeBlock) { + code = codeBlock.text + } + return code + } + + @JvmStatic + fun getDocComment(element: PsiElement): String { + var docComment = getLargestBlock(element, "DocComment") + if (null == docComment) docComment = getFirstBlock(element, "Comment") + var prefix = "" + if (null != docComment) { + prefix = docComment.text.trim { it <= ' ' } + } + return prefix + } + +} + + + diff --git a/src/main/resources/sources/kt/com/github/simiacryptus/aicoder/util/psi/PsiVisitorBase.kt b/src/main/resources/sources/kt/com/github/simiacryptus/aicoder/util/psi/PsiVisitorBase.kt new file mode 100644 index 00000000..462defb7 --- /dev/null +++ b/src/main/resources/sources/kt/com/github/simiacryptus/aicoder/util/psi/PsiVisitorBase.kt @@ -0,0 +1,21 @@ +package com.github.simiacryptus.aicoder.util.psi + +import com.intellij.psi.PsiElement +import com.intellij.psi.PsiElementVisitor +import com.intellij.psi.PsiFile +import java.util.concurrent.atomic.AtomicReference + +abstract class PsiVisitorBase { + fun build(psiFile: PsiFile) { + val visitor = AtomicReference() + visitor.set(object : PsiElementVisitor() { + override fun visitElement(element: PsiElement) { + visit(element, visitor.get()) + super.visitElement(element) + } + }) + psiFile.accept(visitor.get()) + } + + protected abstract fun visit(element: PsiElement, self: PsiElementVisitor) +}