diff --git a/CHANGELOG.md b/CHANGELOG.md index db0c9310..0552408e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,11 @@ ### Added - +## [1.0.14] + +### Fixed +- Various issues with "Insert Implementation" action + ## [1.0.13] ### Removed diff --git a/gradle.properties b/gradle.properties index eb836118..f7738d8f 100644 --- a/gradle.properties +++ b/gradle.properties @@ -4,11 +4,11 @@ pluginGroup = com.github.simiacryptus pluginName = intellij-aicoder pluginRepositoryUrl = https://github.com/SimiaCryptus/intellij-aicoder # SemVer format -> https://semver.org -pluginVersion = 1.0.13 +pluginVersion = 1.0.14 # Supported build number ranges and IntelliJ Platform versions -> https://plugins.jetbrains.com/docs/intellij/build-number-ranges.html pluginSinceBuild = 203 -pluginUntilBuild = 223.* +pluginUntilBuild = 231.* # IntelliJ Platform Properties -> https://plugins.jetbrains.com/docs/intellij/tools-gradle-intellij-plugin.html#configuration-intellij-extension @@ -24,6 +24,7 @@ pluginUntilBuild = 223.* platformType = IU platformPlugins = com.intellij.java, org.intellij.scala:2021.3.22, Pythonid:213.7172.26, org.jetbrains.plugins.go:213.7172.6 #platformPlugins = com.intellij.java, org.intellij.scala:2022.3.16, Pythonid:223.8214.52, org.jetbrains.plugins.go:223.8214.52 +#platformPlugins = com.intellij.java # PhpStorm #platformType = PS @@ -33,11 +34,12 @@ platformPlugins = com.intellij.java, org.intellij.scala:2021.3.22, Pythonid:213. #platformType = RD #platformPlugins = JavaScript +# https://mvnrepository.com/artifact/com.jetbrains.intellij.idea/ideaIU platformVersion = 2021.3.3 #platformVersion = 2022.3.1 # Gradle Releases -> https://github.com/gradle/gradle/releases -gradleVersion = 7.5.1 +gradleVersion = 7.6.1 # Opt-out flag for bundling Kotlin standard library -> https://plugins.jetbrains.com/docs/intellij/kotlin.html#kotlin-standard-library # suppress inspection "UnusedProperty" 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 index 7a285a91..120980ac 100644 --- a/src/main/kotlin/com/github/simiacryptus/aicoder/actions/code/DescribeAction.kt +++ b/src/main/kotlin/com/github/simiacryptus/aicoder/actions/code/DescribeAction.kt @@ -67,7 +67,7 @@ class DescribeAction : AnAction() { UITools.redoableRequest(request, indent, event, { newText -> val wrapping = StringTools.lineWrapping( - newText!!.toString().trim { it <= ' ' }, 120 + newText.toString().trim { it <= ' ' }, 120 ) val numberOfLines = wrapping.trim { it <= ' ' }.split("\n".toRegex()).dropLastWhile { it.isEmpty() } .toTypedArray().size @@ -86,7 +86,7 @@ class DescribeAction : AnAction() { editor.document, selectionStart, selectionEnd, - newText!! + newText ) }) } 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 index 86104845..556694bc 100644 --- a/src/main/kotlin/com/github/simiacryptus/aicoder/actions/code/DocAction.kt +++ b/src/main/kotlin/com/github/simiacryptus/aicoder/actions/code/DocAction.kt @@ -58,13 +58,13 @@ class DocAction : AnAction() { val document = event.getRequiredData(CommonDataKeys.EDITOR).document redoableRequest(completionRequest, "", event, { docString -> - language.docComment!!.fromString(docString.toString().trim { it <= ' ' })!!.withIndent(indent) + language.docComment.fromString(docString.toString().trim { it <= ' ' })!!.withIndent(indent) .toString() + "\n" + indent + StringTools.trimPrefix(indentedInput.toString()) }, { docString -> replaceString( document, startOffset, endOffset, - docString!! + docString ) } ) diff --git a/src/main/kotlin/com/github/simiacryptus/aicoder/actions/code/PsiClassContextAction.kt b/src/main/kotlin/com/github/simiacryptus/aicoder/actions/code/PsiClassContextAction.kt index 573316bb..f570c0b0 100644 --- a/src/main/kotlin/com/github/simiacryptus/aicoder/actions/code/PsiClassContextAction.kt +++ b/src/main/kotlin/com/github/simiacryptus/aicoder/actions/code/PsiClassContextAction.kt @@ -43,6 +43,12 @@ class PsiClassContextAction : AnAction() { .filter { x: String -> !x.isEmpty() } .reduce { a: String, b: String -> "$a $b" }.get() val endOffset = psiClassContextActionParams.largestIntersectingComment.textRange.endOffset + val psiClassContext = PsiClassContext.getContext( + psiClassContextActionParams.psiFile, + psiClassContextActionParams.selectionStart, + psiClassContextActionParams.selectionEnd, + computerLanguage + ) val request = settings.createTranslationRequest() .setInstruction("Implement " + humanLanguage + " as " + computerLanguage.name + " code") .setInputType(humanLanguage) @@ -54,13 +60,7 @@ class PsiClassContextAction : AnAction() { .buildCompletionRequest() .appendPrompt( """ - ${ - PsiClassContext.getContext( - psiClassContextActionParams.psiFile, - psiClassContextActionParams.selectionStart, - psiClassContextActionParams.selectionEnd - ) - } + $psiClassContext """.trimIndent() ) 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 index d2066033..a453398f 100644 --- a/src/main/kotlin/com/github/simiacryptus/aicoder/actions/code/RenameVariablesAction.kt +++ b/src/main/kotlin/com/github/simiacryptus/aicoder/actions/code/RenameVariablesAction.kt @@ -40,7 +40,7 @@ class RenameVariablesAction : AnAction() { @NotNull val mainCursor = caretModel.primaryCaret @NotNull val outputLanguage = AppSettingsState.getInstance().humanLanguage val sourceFile = actionEvent.getRequiredData(CommonDataKeys.PSI_FILE) - val codeElement = PsiUtil.getSmallestIntersectingMajorCodeElement(sourceFile, mainCursor) ?: return + val codeElement = PsiUtil.getSmallestIntersectingMajorCodeElement(sourceFile, mainCursor) ?: throw IllegalStateException() @NotNull val programmingLanguage = ComputerLanguage.getComputerLanguage(actionEvent) val appSettings = AppSettingsState.getInstance() @@ -61,7 +61,7 @@ class RenameVariablesAction : AnAction() { val textIndent = UITools.getIndent(textCursor) UITools.redoableRequest(completionRequest, textIndent, actionEvent) { completionText -> - val renameSuggestions = completionText!!.split('\n').stream().map { x -> + val renameSuggestions = completionText.split('\n').stream().map { x -> val kv = StringTools.stripSuffix(StringTools.stripPrefix(x.trim(), "|"), "|").split('|') .map { x -> x.trim() } kv[0] to kv[1] diff --git a/src/main/kotlin/com/github/simiacryptus/aicoder/actions/code/RewordCommentAction.kt b/src/main/kotlin/com/github/simiacryptus/aicoder/actions/code/RewordCommentAction.kt index a3542fe1..46bb98aa 100644 --- a/src/main/kotlin/com/github/simiacryptus/aicoder/actions/code/RewordCommentAction.kt +++ b/src/main/kotlin/com/github/simiacryptus/aicoder/actions/code/RewordCommentAction.kt @@ -59,13 +59,13 @@ class RewordCommentAction : AnAction() { { newText -> indent.toString() + commentModel!!.fromString( StringTools.lineWrapping( - newText!!, 120 + newText, 120 ) )!!.withIndent(indent) },{ newText -> UITools.replaceString( document, startOffset, endOffset, - newText!! + newText ) } ) diff --git a/src/main/kotlin/com/github/simiacryptus/aicoder/actions/markdown/FactCheckLinkedListAction.kt b/src/main/kotlin/com/github/simiacryptus/aicoder/actions/markdown/FactCheckLinkedListAction.kt index c4a4bf95..f3f9b72c 100644 --- a/src/main/kotlin/com/github/simiacryptus/aicoder/actions/markdown/FactCheckLinkedListAction.kt +++ b/src/main/kotlin/com/github/simiacryptus/aicoder/actions/markdown/FactCheckLinkedListAction.kt @@ -24,13 +24,11 @@ class FactCheckLinkedListAction : AnAction() { val settings = AppSettingsState.getInstance() val caret = event.getRequiredData(CommonDataKeys.EDITOR).caretModel.primaryCaret val languageName = ComputerLanguage.getComputerLanguage(event)!!.name - val endOffset: Int - val startOffset: Int val psiFile = PsiUtil.getPsiFile(event)!! val elements = PsiUtil.getAllIntersecting(psiFile, caret.selectionStart, caret.selectionEnd, "ListItem") val elementText = elements.flatMap { it.children.map { it.text } }.toTypedArray() - startOffset = elements.minByOrNull { it.startOffset }?.startOffset ?: caret.selectionStart - endOffset = elements.maxByOrNull { it.endOffset }?.endOffset ?: caret.selectionEnd + val startOffset = elements.minByOrNull { it.startOffset }?.startOffset ?: caret.selectionStart + val endOffset = elements.maxByOrNull { it.endOffset }?.endOffset ?: caret.selectionEnd val replaceString = event.getRequiredData(CommonDataKeys.EDITOR).document.text.substring(startOffset, endOffset) val completionRequest = settings.createTranslationRequest() .setInstruction(getInstruction("Translate each item into a search query that can be used to fact check each item with a search engine")) @@ -43,7 +41,7 @@ class FactCheckLinkedListAction : AnAction() { val document = event.getRequiredData(CommonDataKeys.EDITOR).document redoableRequest(completionRequest, "", event, { newText -> - val queries = newText!!.replace("\"".toRegex(), "").split("\n\\d+\\.\\s+".toRegex()).toTypedArray() + val queries = newText.replace("\"".toRegex(), "").split("\n\\d+\\.\\s+".toRegex()).toTypedArray() if (queries.size != elementText.size) { throw RuntimeException("Invalid response: " + newText) } @@ -55,7 +53,7 @@ class FactCheckLinkedListAction : AnAction() { } }, { newText -> - replaceString(document, startOffset, endOffset, newText!!) + replaceString(document, startOffset, endOffset, newText) }) } diff --git a/src/main/kotlin/com/github/simiacryptus/aicoder/actions/markdown/MarkdownNewTableColsAction.kt b/src/main/kotlin/com/github/simiacryptus/aicoder/actions/markdown/MarkdownNewTableColsAction.kt index e49968d6..ba461fa9 100644 --- a/src/main/kotlin/com/github/simiacryptus/aicoder/actions/markdown/MarkdownNewTableColsAction.kt +++ b/src/main/kotlin/com/github/simiacryptus/aicoder/actions/markdown/MarkdownNewTableColsAction.kt @@ -41,7 +41,7 @@ class MarkdownNewTableColsAction : AnAction() { event.getRequiredData(CommonDataKeys.EDITOR) .document, markdownNewTableColsParams.table.textRange.startOffset, - markdownNewTableColsParams.table.textRange.endOffset, newText!! + markdownNewTableColsParams.table.textRange.endOffset, newText ) } ) diff --git a/src/main/kotlin/com/github/simiacryptus/aicoder/actions/markdown/MarkdownNewTableRowsAction.kt b/src/main/kotlin/com/github/simiacryptus/aicoder/actions/markdown/MarkdownNewTableRowsAction.kt index 6f30b567..d9c245db 100644 --- a/src/main/kotlin/com/github/simiacryptus/aicoder/actions/markdown/MarkdownNewTableRowsAction.kt +++ b/src/main/kotlin/com/github/simiacryptus/aicoder/actions/markdown/MarkdownNewTableRowsAction.kt @@ -35,7 +35,7 @@ class MarkdownNewTableRowsAction : AnAction() { "", event, { transformCompletion(markdownNewTableRowsParams, it) }, - { UITools.insertString(document, endOffset, it!!) }) + { UITools.insertString(document, endOffset, it) }) } class MarkdownNewTableRowsParams constructor(val caret: Caret, val table: PsiElement) 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 ba306ca7..20008b5f 100644 --- a/src/main/kotlin/com/github/simiacryptus/aicoder/util/UITools.kt +++ b/src/main/kotlin/com/github/simiacryptus/aicoder/util/UITools.kt @@ -1,3 +1,5 @@ +@file:Suppress("UNNECESSARY_SAFE_CALL") + package com.github.simiacryptus.aicoder.util import com.github.simiacryptus.aicoder.openai.OpenAI_API @@ -83,7 +85,7 @@ object UITools { event: AnActionEvent, transformCompletion: Function, action: Function, - resultFuture: ListenableFuture = OpenAI_API.complete(event.project!!, request, indent?:""), + resultFuture: ListenableFuture = OpenAI_API.complete(event.project!!, request, indent), progressIndicator: ProgressIndicator? = startProgress() ) { Futures.addCallback(resultFuture, object : FutureCallback { @@ -168,7 +170,7 @@ object UITools { val editor = event.getData(CommonDataKeys.EDITOR) val document = Objects.requireNonNull(editor)!!.document val progressIndicator = startProgress() - val resultFuture = OpenAI_API.edit(event.project!!, request.uiIntercept(), indent!!) + val resultFuture = OpenAI_API.edit(event.project!!, request.uiIntercept(), indent) Futures.addCallback(resultFuture, object : FutureCallback { override fun onSuccess(result: CharSequence?) { progressIndicator?.cancel() diff --git a/src/main/kotlin/com/github/simiacryptus/aicoder/util/psi/PsiClassContext.kt b/src/main/kotlin/com/github/simiacryptus/aicoder/util/psi/PsiClassContext.kt index 147eba50..897bc81e 100644 --- a/src/main/kotlin/com/github/simiacryptus/aicoder/util/psi/PsiClassContext.kt +++ b/src/main/kotlin/com/github/simiacryptus/aicoder/util/psi/PsiClassContext.kt @@ -1,11 +1,12 @@ 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 import java.util.ArrayList -class PsiClassContext(val text: String, val isPrior: Boolean, val isOverlap: Boolean) { +class PsiClassContext(val text: String, val isPrior: Boolean, val isOverlap: Boolean, val language: ComputerLanguage) { val children = ArrayList() /** @@ -36,22 +37,36 @@ class PsiClassContext(val text: String, val isPrior: Boolean, val isOverlap: Boo val within = textRangeStartOffset <= selectionStart && textRangeEndOffset > selectionStart && textRangeStartOffset <= selectionEnd && textRangeEndOffset > selectionEnd if (PsiUtil.matchesType(element, "ImportList")) { - currentContext.children.add(PsiClassContext(text.trim { it <= ' ' }, isPrior, isOverlap)) + 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)) + currentContext.children.add(PsiClassContext(indent + text.trim { it <= ' ' }, false, true, language)) } - } else if (PsiUtil.matchesType(element, "Method", "Field")) { + } 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)) + isOverlap, + language)) } else if (PsiUtil.matchesType(element, "Class")) { processChildren( element, @@ -60,7 +75,7 @@ class PsiClassContext(val text: String, val isPrior: Boolean, val isOverlap: Boo isOverlap, indent + text.substring(0, text.indexOf('{')).trim { it <= ' ' } + " {") if (!isOverlap) { - currentContext.children.add(PsiClassContext("}", isPrior, false)) + currentContext.children.add(PsiClassContext("}", isPrior, false, language)) } } else if (!isOverlap && PsiUtil.matchesType(element, "CodeBlock", "ForStatement")) { // Skip @@ -76,7 +91,7 @@ class PsiClassContext(val text: String, val isPrior: Boolean, val isOverlap: Boo isOverlap: Boolean, declarationText: String ): PsiClassContext { - val newNode = PsiClassContext(declarationText, isPrior, isOverlap) + val newNode = PsiClassContext(declarationText, isPrior, isOverlap, language) currentContext.children.add(newNode) val prevclassBuffer = currentContext currentContext = newNode @@ -110,12 +125,12 @@ class PsiClassContext(val text: String, val isPrior: Boolean, val isOverlap: Boo } companion object { - fun getContext(psiFile: PsiFile, selectionStart: Int, selectionEnd: Int): PsiClassContext { - return PsiClassContext("", false, true).init(psiFile, selectionStart, selectionEnd) + fun getContext(psiFile: PsiFile, selectionStart: Int, selectionEnd: Int, language: ComputerLanguage): PsiClassContext { + return PsiClassContext("", false, true, language).init(psiFile, selectionStart, selectionEnd) } - fun getContext(psiFile: PsiFile): PsiClassContext { - return getContext(psiFile, 0, psiFile.textLength) + fun getContext(language: ComputerLanguage, psiFile: PsiFile): PsiClassContext { + return getContext(psiFile, 0, psiFile.textLength, language) } } } \ No newline at end of file diff --git a/src/main/kotlin/com/github/simiacryptus/aicoder/util/psi/PsiTranslationSkeleton.kt b/src/main/kotlin/com/github/simiacryptus/aicoder/util/psi/PsiTranslationSkeleton.kt index 47a35c60..00e99c8e 100644 --- a/src/main/kotlin/com/github/simiacryptus/aicoder/util/psi/PsiTranslationSkeleton.kt +++ b/src/main/kotlin/com/github/simiacryptus/aicoder/util/psi/PsiTranslationSkeleton.kt @@ -83,7 +83,7 @@ class PsiTranslationSkeleton(private val stubId: String?, text: String?, private .setInputAttribute("language", sourceLanguage.name) .setOutputType("translated") .setOutputAttrute("language", targetLanguage.name) - .buildCompletionRequest(), indent!! + .buildCompletionRequest(), indent ) } } @@ -212,16 +212,16 @@ class PsiTranslationSkeleton(private val stubId: String?, text: String?, private ComputerLanguage.Python -> String.format( "%s\n %s\n pass", declaration, - targetLanguage.lineComment!!.fromString(stubID) + 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) + targetLanguage.lineComment.fromString(stubID) ) - else -> 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)) } } diff --git a/src/main/kotlin/com/github/simiacryptus/aicoder/util/psi/PsiUtil.kt b/src/main/kotlin/com/github/simiacryptus/aicoder/util/psi/PsiUtil.kt index 7ab1ed25..6eb499f5 100644 --- a/src/main/kotlin/com/github/simiacryptus/aicoder/util/psi/PsiUtil.kt +++ b/src/main/kotlin/com/github/simiacryptus/aicoder/util/psi/PsiUtil.kt @@ -110,7 +110,7 @@ object PsiUtil { visitor.set(object : PsiElementVisitor() { override fun visitElement(element: PsiElement) { val textRange = element.textRange - if (within(textRange, selectionStart, selectionEnd)) { + 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 } } @@ -124,6 +124,7 @@ object PsiUtil { return largest.get() } + fun getAllIntersecting( element: PsiElement, selectionStart: Int, @@ -149,7 +150,11 @@ object PsiUtil { } private fun within(textRange: TextRange, vararg offset: Int) : Boolean = - (textRange.startOffset <= offset.maxOrNull()?:0) && (textRange.endOffset > offset.minOrNull()?:0) + offset.filter { it in textRange.startOffset..textRange.endOffset }.isNotEmpty() + + private fun intersects(a: TextRange, b: TextRange): Boolean { + return within(a, b.startOffset, b.endOffset) || within(b, a.startOffset, a.endOffset) + } fun matchesType(element: PsiElement, vararg types: CharSequence): Boolean { diff --git a/src/test/kotlin/com/github/simiacryptus/aicoder/ApplicationDevelopmentUITest.kt b/src/test/kotlin/com/github/simiacryptus/aicoder/ApplicationDevelopmentUITest.kt new file mode 100644 index 00000000..3fda1acd --- /dev/null +++ b/src/test/kotlin/com/github/simiacryptus/aicoder/ApplicationDevelopmentUITest.kt @@ -0,0 +1,464 @@ +package com.github.simiacryptus.aicoder + +import com.github.simiacryptus.aicoder.UITestUtil.Companion.awaitProcessing +import com.github.simiacryptus.aicoder.UITestUtil.Companion.canRunTests +import com.github.simiacryptus.aicoder.UITestUtil.Companion.click +import com.github.simiacryptus.aicoder.UITestUtil.Companion.enterLines +import com.github.simiacryptus.aicoder.UITestUtil.Companion.getEditor +import com.github.simiacryptus.aicoder.UITestUtil.Companion.keyboard +import com.github.simiacryptus.aicoder.UITestUtil.Companion.menuAction +import com.github.simiacryptus.aicoder.UITestUtil.Companion.newFile +import com.github.simiacryptus.aicoder.UITestUtil.Companion.outputDir +import com.github.simiacryptus.aicoder.UITestUtil.Companion.screenshot +import com.github.simiacryptus.aicoder.UITestUtil.Companion.selectText +import com.github.simiacryptus.aicoder.UITestUtil.Companion.testProjectPath +import com.github.simiacryptus.aicoder.UITestUtil.Companion.writeImage +import org.apache.commons.io.FileUtils +import org.junit.Test +import java.awt.event.KeyEvent +import java.io.File +import java.io.FileOutputStream +import java.io.PrintWriter +import java.lang.Thread.sleep + +/** + * See Also: + * https://github.com/JetBrains/intellij-ui-test-robot + * https://joel-costigliola.github.io/assertj/swing/api/org/assertj/swing/core/Robot.html + */ +class ApplicationDevelopmentUITest { + + @Test + fun java_test() { + if (!canRunTests()) return + val (name, description) = test_problem_calculator() + val (language, functionalHandle, seedPrompt) = java(name) + test(name, language, description, seedPrompt, functionalHandle) + } + + private fun java(name: String) = + Triple("java", "static void main", """public class $name {\n""") + + @Test + fun kotlin_test() { + if (!canRunTests()) return + val (name, description) = test_problem_calculator() + val (language, functionalHandle, seedPrompt) = kotlin(name) + test(name, language, description, seedPrompt, functionalHandle) + } + + private fun kotlin(name: String) = + Triple("kt", "fun main", """object $name {\n""") + + @Test + fun scala_test() { + if (!canRunTests()) return + val (name, description) = test_problem_calculator() + val (language, functionalHandle, seedPrompt) = scala(name) + test(name, language, description, seedPrompt, functionalHandle) + } + + private fun scala(name: String) = + Triple("scala", "def main", """object $name {\n""") + + private fun javascript(name: String) = + Triple("js", "function main", """function $name() {\n""") + + @Test + fun javascript_test() { + if (!canRunTests()) return + val (name, description) = test_problem_calculator() + val (language, functionalHandle, seedPrompt) = javascript(name) + test(name, language, description, seedPrompt, functionalHandle) + } + + + private fun test_problem_calculator(): Pair> { + return Pair( + "String_Calculator", + listOf( + // First, two simple directives + "Create a utility function to find all simple addition expressions in a string and replace them with the calculated result.", + "Create a utility function to find all simple multiplication expressions in a string and replace them with the calculated result", + // This directive is more complex, but can be solved by chaining the previous two directives + "Implement a utility function to interpret simple math expressions.", + // Finally, we can test the entire class + "Implement a static main method that tests each class member and validates the output." + ) + ) + } + + private fun test_problem_red_black_tree(): Pair> { + return Pair( + "Red_Black_Tree", + listOf( + "Implement a utility function to insert a new node into the tree.", + "Implement a utility function to remove a node from the tree.", + "Implement a utility function to find a node in the tree.", + "Implement a static main method that tests each class member and validates the output." + ) + ) + } + + + private fun test_problem_morse(): Pair> { + return Pair( + "Text_to_Morse", + listOf( + "Implement a utility function that converts text to Morse code.", + "Implement a static main method that tests each class member and validates the output." + ) + ) + } + + + private fun test_problem_permutations(): Pair> { + return Pair( + "Permutations", + listOf( + "Implement class members needed to represent a single permutation.", + // Group-theory operations + "Implement a member function that returns the inverse permutation.", + "Implement a member function that returns the composition of two permutations.", + // Testing + "Implement a utility function that generates all permutations of a string.", + "Implement a static main method that tests each class member and validates the output." + ) + ) + } + + private fun test_problem_prime(): Pair> { + return Pair( + "Prime", + listOf( + "Implement a utility function that returns true if the given number is prime.", + "Implement a utility function that returns the next prime number after the given number.", + "Implement a static main method that tests each class member and validates the output." + ) + ) + } + + private fun test_problem_fibonacci(): Pair> { + return Pair( + "Fibonacci", + listOf( + "Implement a utility function that returns the nth number in the Fibonacci sequence.", + "Implement a static main method that tests each class member and validates the output." + ) + ) + } + + private fun test_problem_sort(): Pair> { + return Pair( + "Sort", + listOf( + "Implement a utility function that returns the given array sorted.", + "Implement a static main method that tests each class member and validates the output." + ) + ) + } + + private fun test_problem_search(): Pair> { + return Pair( + "Search", + listOf( + "Implement a utility function that returns the index of the given element in the given array.", + "Implement a static main method that tests each class member and validates the output." + ) + ) + } + + private fun test_problem_reverse(): Pair> { + return Pair( + "Reverse", + listOf( + "Implement a utility function that returns the given array in reverse order.", + "Implement a static main method that tests each class member and validates the output." + ) + ) + } + + private fun test_problem_merge(): Pair> { + return Pair( + "Merge", + listOf( + "Implement a utility function that merges two sorted arrays into a single sorted array.", + "Implement a static main method that tests each class member and validates the output." + ) + ) + } + + private fun test_problem_split(): Pair> { + return Pair( + "Split", + listOf( + "Implement a utility function that splits a sorted array into two sorted arrays.", + "Implement a static main method that tests each class member and validates the output." + ) + ) + } + + private fun test_problem_factorial(): Pair> { + return Pair( + "Factorial", + listOf( + "Implement a utility function that returns the factorial of the given number.", + "Implement a static main method that tests each class member and validates the output." + ) + ) + } + + + private fun test( + name: String, + language: String, + description: List, + seedPrompt: String, + functionalHandle: String, + question: String = "What is the big-O runtime and why?" + ) { + val reportPrefix = "${name}_${language}_" + val testOutputFile = File(outputDir, "${name}_${language}.md") + val out = PrintWriter(FileOutputStream(testOutputFile)) + try { + try { + out.println( + """ + + # $name + + In this test we will used AI Coding Assistant to implement the $name class to solve the following problem: + + """.trimIndent() + ) + out.println("```\n$description\n```") + newFile("$name.$language") + + + out.println( + """ + + ## Implementation + + The first step is to translate the problem into code. We can do this by using the "Insert Implementation" command. + + """.trimIndent() + ) + click("//div[@class='EditorComponentImpl']") + keyboard.selectAll() + keyboard.key(KeyEvent.VK_DELETE) + enterLines(seedPrompt) + + for (line in description) { + click("//div[@class='EditorComponentImpl']") + newEndOfLine() + enterLines("// $line") + writeImage( + menuAction("Insert Implementation"), outputDir, + name, "${reportPrefix}menu", out + ) + awaitProcessing() + //keyboard.hotKey(KeyEvent.VK_SHIFT, KeyEvent.VK_UP) + //keyboard.hotKey(KeyEvent.VK_DELETE) + keyboard.hotKey(KeyEvent.VK_SHIFT, KeyEvent.VK_ALT, KeyEvent.VK_DOWN) // Move current line down + keyboard.hotKey(KeyEvent.VK_CONTROL, KeyEvent.VK_ALT, KeyEvent.VK_L) // Reformat + keyboard.hotKey(KeyEvent.VK_CONTROL, KeyEvent.VK_S) // Save + } + + + out.println( + """ + + This results in the following code: + + ```$language""".trimIndent() + ) + out.println(FileUtils.readFileToString(File(testProjectPath, "src/$name.$language"), "UTF-8")) + out.println("```") + + +// out.println( +// """ +// +// ## Execution +// +// This code can be executed by pressing the "Run" button in the top right corner of the IDE. +// What could possibly go wrong? +// +// """.trimIndent() +// ) +// keyboard.hotKey(KeyEvent.VK_CONTROL, KeyEvent.VK_SHIFT, KeyEvent.VK_F10) // Run +// awaitRunCompletion() +// out.println( +// """ +// +// ```""".trimIndent() +// ) +// out.println(componentText("//div[contains(@accessiblename.key, 'editor.accessible.name')]")) +// out.println( +// """ +// ``` +// """.trimIndent() +// ) +// writeImage( +// screenshot("//div[@class='IdeRootPane']"), +// outputDir, +// name, "${reportPrefix}result", out +// ) +// // Close run tab +// sleep(100) +// clickr("//div[@class='ContentTabLabel']") +// sleep(100) +// click("//div[contains(@text.key, 'action.CloseContent.text')]") +// sleep(100) + + + out.println( + """ + + ## Rename Variables + + We can use the "Rename Variables" command to make the code more readable... + + """.trimIndent() + ) + keyboard.hotKey(KeyEvent.VK_CONTROL, KeyEvent.VK_HOME) // Move to top + selectText( + getEditor(), functionalHandle + ) + keyboard.hotKey(KeyEvent.VK_CONTROL, KeyEvent.VK_W) // Select function + writeImage( + menuAction("Rename Variables"), + outputDir, + name, "${reportPrefix}Rename_Variables", out + ) + awaitProcessing() + sleep(1000) + writeImage( + screenshot("//div[@class='JDialog']"), + outputDir, + name, + "${reportPrefix}Rename_Variables_Dialog", + out + ) + click("//div[@text.key='button.ok']") + + + out.println( + """ + + ## Documentation Comments + + We also want good documentation for our code. We can use the "Add Documentation Comments" command to do + + """.trimIndent() + ) + selectText(getEditor(), functionalHandle) + writeImage( + menuAction("Doc Comments"), + outputDir, + name, "${reportPrefix}Add_Doc_Comments", out + ) + awaitProcessing() + keyboard.hotKey(KeyEvent.VK_CONTROL, KeyEvent.VK_HOME) // Move to top + writeImage( + screenshot("//div[@class='IdeRootPane']"), + outputDir, + name, + "${reportPrefix}Add_Doc_Comments2", + out + ) + + + out.println( + """ + + ## Ad-Hoc Questions + + We can also ask questions about the code. For example, we can ask what the big-O runtime is for this code. + + """.trimIndent() + ) + selectText(getEditor(), functionalHandle) + keyboard.hotKey(KeyEvent.VK_CONTROL, KeyEvent.VK_W) // Select function + writeImage( + menuAction("Ask a question"), + outputDir, + name, "${reportPrefix}Ask_Q", out + ) + click("//div[@class='MultiplexingTextField']") + keyboard.enterText(question) + writeImage( + screenshot("//div[@class='JDialog']"), + outputDir, + name, "${reportPrefix}Ask_Q2", out + ) + click("//div[@text.key='button.ok']") + awaitProcessing() + keyboard.hotKey(KeyEvent.VK_CONTROL, KeyEvent.VK_HOME) // Move to top + writeImage( + screenshot("//div[@class='IdeRootPane']"), + outputDir, + name, "${reportPrefix}Ask_Q3", out + ) + + out.println( + """ + + ## Code Comments + + We can also add code comments to the code. This is useful for explaining the code to other developers. + + """.trimIndent() + ) + selectText(getEditor(), functionalHandle) + keyboard.hotKey(KeyEvent.VK_CONTROL, KeyEvent.VK_W) // Select function + writeImage( + menuAction("Code Comments"), + outputDir, + name, "${reportPrefix}Add_Code_Comments", out + ) + awaitProcessing() + writeImage( + screenshot("//div[@class='IdeRootPane']"), + outputDir, + name, + "${reportPrefix}Add_Code_Comments2", + out + ) + keyboard.hotKey(KeyEvent.VK_CONTROL, KeyEvent.VK_ALT, KeyEvent.VK_L) // Reformat + keyboard.hotKey(KeyEvent.VK_CONTROL, KeyEvent.VK_S) // Save + out.println( + """ + + ```$language""".trimIndent() + ) + out.println(FileUtils.readFileToString(File(testProjectPath, "src/$name.$language"), "UTF-8")) + out.println( + """ + ``` + + """.trimIndent() + ) + + + // Close editor + click("//div[@class='InplaceButton']") + } finally { + out.close() + } + } finally { + out.close() // Close file + } + } + + private fun newEndOfLine() { + keyboard.hotKey(KeyEvent.VK_CONTROL, KeyEvent.VK_END) + keyboard.hotKey(KeyEvent.VK_LEFT) + keyboard.hotKey(KeyEvent.VK_ENTER) + keyboard.hotKey(KeyEvent.VK_LEFT) + keyboard.hotKey(KeyEvent.VK_TAB) + } + + +} \ No newline at end of file diff --git a/src/test/kotlin/com/github/simiacryptus/aicoder/UITest.kt b/src/test/kotlin/com/github/simiacryptus/aicoder/UITest.kt index 1c1c30b2..0401867d 100644 --- a/src/test/kotlin/com/github/simiacryptus/aicoder/UITest.kt +++ b/src/test/kotlin/com/github/simiacryptus/aicoder/UITest.kt @@ -1,22 +1,15 @@ package com.github.simiacryptus.aicoder -import com.intellij.remoterobot.RemoteRobot -import com.intellij.remoterobot.fixtures.ComponentFixture -import com.intellij.remoterobot.fixtures.dataExtractor.RemoteText -import com.intellij.remoterobot.search.locators.byXpath -import com.intellij.remoterobot.utils.Keyboard -import com.jetbrains.rd.util.first -import org.apache.commons.io.FileUtils +import com.github.simiacryptus.aicoder.UITestUtil.Companion.canRunTests +import com.github.simiacryptus.aicoder.UITestUtil.Companion.documentJavaImplementation +import com.github.simiacryptus.aicoder.UITestUtil.Companion.documentMarkdownListAppend +import com.github.simiacryptus.aicoder.UITestUtil.Companion.documentMarkdownTableOps +import com.github.simiacryptus.aicoder.UITestUtil.Companion.documentTextAppend +import com.github.simiacryptus.aicoder.UITestUtil.Companion.outputDir import org.junit.Test -import java.awt.Point - -import java.awt.event.KeyEvent -import java.awt.image.BufferedImage import java.io.File import java.io.FileOutputStream import java.io.PrintWriter -import java.lang.Thread.sleep -import javax.imageio.ImageIO /** * See Also: @@ -25,12 +18,6 @@ import javax.imageio.ImageIO */ class UITest { - private val outputDir = File("C:\\Users\\andre\\code\\aicoder\\intellij-aicoder-docs") - private val testProjectPath = File("C:\\Users\\andre\\IdeaProjects\\automated-ui-test-workspace") - private val testIdeUrl = "http://127.0.0.1:8082" - private val robot = RemoteRobot(testIdeUrl) - private val keyboard = Keyboard(robot) - @Test fun javaTests() { if (!canRunTests()) return @@ -161,787 +148,4 @@ class UITest { out.close() // Close file } - private fun canRunTests() = outputDir.exists() && testProjectPath.exists() - - /** - * - * Creates a new file with the given name. - * - * @param name The name of the file to create. - */ - private fun newFile(name: String) { - click("//div[@class='ProjectViewTree']") - keyboard.key(KeyEvent.VK_RIGHT) - keyboard.enterText("src") - keyboard.key(KeyEvent.VK_CONTEXT_MENU) - click("//div[contains(@text.key, 'group.NewGroup.text')]", "//div[contains(@text.key, 'group.WeighingNewGroup.text')]") - sleep(500) - click("//div[@class='HeavyWeightWindow'][.//div[@class='MyMenu']]//div[@class='HeavyWeightWindow']//div[contains(@text.key, 'group.FileMenu.text')]") - keyboard.enterText(name) - keyboard.enter() - } - - private fun isStarted(): Boolean = - robot.findAll( - ComponentFixture::class.java, - byXpath("//div[contains(@accessiblename.key, 'editor.accessible.name')]") - ).isNotEmpty() - - private fun isDialogOpen(): Boolean = - robot.findAll(ComponentFixture::class.java, byXpath("//div[@class='EngravedLabel']")).isNotEmpty() - - private fun isStillRunning(): Boolean { - val resultText = componentText("//div[contains(@accessiblename.key, 'editor.accessible.name')]") ?: return false - return !(resultText.contains("Process finished")) - } - - - private fun componentText(s: String): String { - return getLines(getComponent(s)) - } - - private fun getLines(element: ComponentFixture): String { - val lines = element.data.getAll().groupBy { it.point.y } - return lines.toList().sortedBy { it.first }.map { it.second } - .map { it.toList().sortedBy { it.point.x }.map { it.text }.reduce { a, b -> a + b } } - .reduceOrNull { a, b -> a + "\n" + b }.orEmpty() - } - - fun findText(element: ComponentFixture, text: String): Pair { - val lines: Map> = element.data.getAll().groupBy { it.point.y } - val line = lines.filter { it.value.map { it.text }.reduce { a, b -> a + b }.contains(text) }.first().value - val index = line.map { it.text }.reduce { a, b -> a + b }.indexOf(text) - val lineBuffer = line.toMutableList() - var left = index - while (left > 0) { - val first = lineBuffer.first() - if (first.text.length <= left) { - left -= first.text.length - lineBuffer.removeAt(0) - } - } - val leftPoint = lineBuffer.first().point - left += text.length - var rightPoint: Point = leftPoint - while (left > 0) { - val first = lineBuffer.first() - if (first.text.length <= left) { - left -= first.text.length - rightPoint = lineBuffer.removeAt(0).point - } - } - if(lineBuffer.isNotEmpty()) { - rightPoint = lineBuffer.first().point - } - return Pair(leftPoint, rightPoint) - } - - fun selectText(element: ComponentFixture, text: String) { - val (leftPoint, rightPoint) = findText(element, text) - element.runJs("robot.pressMouse(component, new Point(${leftPoint.x}, ${leftPoint.y}))") - element.runJs("robot.moveMouse(component, new Point(${rightPoint.x}, ${rightPoint.y}))") - element.runJs("robot.releaseMouseButtons()") - } - - fun clickText(element: ComponentFixture, text: String) { - val (leftPoint, rightPoint) = findText(element, text) - element.runJs("robot.click(component, new Point(${leftPoint.x}, ${leftPoint.y}))") - } - - fun rightClickText(element: ComponentFixture, text: String) { - val (leftPoint, rightPoint) = findText(element, text) - element.runJs("robot.rightClick(component, new Point(${leftPoint.x}, ${leftPoint.y}))") - } - - /** - * - * Implements a Java class with the given name and task. - * - * @param name The name of the class to be implemented. - * @param task The task to be implemented in the class. - * @return A [BufferedImage] of the command used. - */ - private fun implementCode(name: String, task: String, prompt: String): BufferedImage { - click("//div[@class='EditorComponentImpl']") - keyboard.selectAll() - keyboard.key(KeyEvent.VK_DELETE) - enterLines(prompt) - val image = menuAction("Insert Implementation") - awaitProcessing() - return image - } - - private fun menuAction(menuText: String): BufferedImage { - keyboard.key(KeyEvent.VK_CONTEXT_MENU) - sleep(100) - val aiCoderMenuItem = getComponent("//div[@text='AI Coder']") - aiCoderMenuItem.click() - val point1 = aiCoderMenuItem.locationOnScreen - sleep(100) - val submenu = - getComponent("//div[@class='HeavyWeightWindow'][.//div[@class='MyMenu']]//div[@class='HeavyWeightWindow']//div[contains(@text, '$menuText')]") - val point2 = submenu.locationOnScreen - submenu.runJs("robot.moveMouse(new Point(${point2.x}, ${point1.y}))") - submenu.runJs("robot.moveMouse(component)") - val image = screenshot("//div[@class='IdeRootPane']") - submenu.click() - return image - } - - /** - * - * Awaits the completion of a running process. - * - * This function will wait until the process is started, then wait until it is finished. - * It will also print out the total time the process took to complete. - */ - private fun awaitRunCompletion() { - while (!isStarted()) sleep(1) - val start = System.currentTimeMillis() - println("Process started") - while (isStillRunning()) { - sleep(100) - } - val end = System.currentTimeMillis() - println("Process ended after ${(end - start) / 1000.0}") - } - - private fun awaitProcessing() { - while (!isDialogOpen()) sleep(1) - val start = System.currentTimeMillis() - println("Dialog opened") - while (isDialogOpen()) sleep(100) - val end = System.currentTimeMillis() - println("Dialog closed after ${(end - start) / 1000.0}") - } - - /** - * - * Documents the Java implementation of the given name and directive. - * - * @param name The name of the Java implementation. - * @param directive The directive for the Java implementation. - * @param out The [PrintWriter] to write the documentation to. - * @param reportDir The [File] directory to save the screenshots to. - */ - private fun documentJavaImplementation( - name: String, - directive: String, - out: PrintWriter, - reportDir: File - ) { - documentImplementation( - name = name, - out1 = out, - directive = directive, - extension = "java", - prompt = "public class $name {\\n // $directive", - reportDir = reportDir, - language = "java", - selector = "static void main" - ) - } - - private fun documentImplementation( - name: String, - out1: PrintWriter, - directive: String, - extension: String, - prompt: String, - reportDir: File, - language: String, - selector: String - ) { - val reportPrefix = "${name}_${language}_" - val testOutputFile = File(outputDir, "$reportPrefix$name.md") - val out2 = PrintWriter(FileOutputStream(testOutputFile)) - - try { - out1.println("[$directive ($language)]($reportPrefix$name.md)\n\n") - val out = out2 - - out.println( - """ - - # $name - - In this test we will used AI Coding Assistant to implement the $name class to solve the following problem: - - """.trimIndent() - ) - out.println("```\n$directive\n```") - newFile("$name.$extension") - - - out.println( - """ - - ## Implementation - - The first step is to translate the problem into code. We can do this by using the "Insert Implementation" command. - - """.trimIndent() - ) - val image = implementCode(name, directive, prompt) - writeImage(image, reportDir, name, "${reportPrefix}menu", out) - keyboard.hotKey(KeyEvent.VK_SHIFT, KeyEvent.VK_UP) - keyboard.hotKey(KeyEvent.VK_DELETE) - keyboard.hotKey(KeyEvent.VK_CONTROL, KeyEvent.VK_ALT, KeyEvent.VK_L) // Reformat - keyboard.hotKey(KeyEvent.VK_CONTROL, KeyEvent.VK_S) // Save - out.println( - """ - - This results in the following code: - - ```$language""".trimIndent() - ) - out.println(FileUtils.readFileToString(File(testProjectPath, "src/$name.$extension"), "UTF-8")) - out.println("```") - - - out.println( - """ - - ## Execution - - This code can be executed by pressing the "Run" button in the top right corner of the IDE. - What could possibly go wrong? - - """.trimIndent() - ) - keyboard.hotKey(KeyEvent.VK_CONTROL, KeyEvent.VK_SHIFT, KeyEvent.VK_F10) // Run - awaitRunCompletion() - out.println( - """ - - ```""".trimIndent() - ) - out.println(componentText("//div[contains(@accessiblename.key, 'editor.accessible.name')]")) - out.println( - """ - ``` - """.trimIndent() - ) - writeImage(screenshot("//div[@class='IdeRootPane']"), reportDir, name, "${reportPrefix}result", out) - // Close run tab - sleep(100) - clickr("//div[@class='ContentTabLabel']") - sleep(100) - click("//div[contains(@text.key, 'action.CloseContent.text')]") - sleep(100) - - - out.println( - """ - - ## Rename Variables - - The code is not very readable. We can use the "Rename Variables" command to make it more readable... - - """.trimIndent() - ) - keyboard.hotKey(KeyEvent.VK_CONTROL, KeyEvent.VK_HOME) // Move to top - selectText(getEditor(), selector) - keyboard.hotKey(KeyEvent.VK_CONTROL, KeyEvent.VK_W) // Select function - writeImage(menuAction("Rename Variables"), reportDir, name, "${reportPrefix}Rename_Variables", out) - awaitProcessing() - sleep(1000) - writeImage( - screenshot("//div[@class='JDialog']"), - reportDir, - name, - "${reportPrefix}Rename_Variables_Dialog", - out - ) - click("//div[@text.key='button.ok']") - - - out.println( - """ - - ## Documentation Comments - - We also want good documentation for our code. We can use the "Add Documentation Comments" command to do this. - - """.trimIndent() - ) - selectText(getEditor(), selector) - writeImage(menuAction("Doc Comments"), reportDir, name, "${reportPrefix}Add_Doc_Comments", out) - awaitProcessing() - keyboard.hotKey(KeyEvent.VK_CONTROL, KeyEvent.VK_HOME) // Move to top - writeImage( - screenshot("//div[@class='IdeRootPane']"), - reportDir, - name, - "${reportPrefix}Add_Doc_Comments2", - out - ) - - - out.println( - """ - - ## Ad-Hoc Questions - - We can also ask questions about the code. For example, we can ask what the big-O runtime is for this code. - - """.trimIndent() - ) - selectText(getEditor(), selector) - keyboard.hotKey(KeyEvent.VK_CONTROL, KeyEvent.VK_W) // Select function - writeImage(menuAction("Ask a question"), reportDir, name, "${reportPrefix}Ask_Q", out) - click("//div[@class='MultiplexingTextField']") - keyboard.enterText("What is the big-O runtime and why?") - writeImage(screenshot("//div[@class='JDialog']"), reportDir, name, "${reportPrefix}Ask_Q2", out) - click("//div[@text.key='button.ok']") - awaitProcessing() - keyboard.hotKey(KeyEvent.VK_CONTROL, KeyEvent.VK_HOME) // Move to top - writeImage(screenshot("//div[@class='IdeRootPane']"), reportDir, name, "${reportPrefix}Ask_Q3", out) - - out.println( - """ - - ## Code Comments - - We can also add code comments to the code. This is useful for explaining the code to other developers. - - """.trimIndent() - ) - selectText(getEditor(), selector) - keyboard.hotKey(KeyEvent.VK_CONTROL, KeyEvent.VK_W) // Select function - writeImage(menuAction("Code Comments"), reportDir, name, "${reportPrefix}Add_Code_Comments", out) - awaitProcessing() - writeImage( - screenshot("//div[@class='IdeRootPane']"), - reportDir, - name, - "${reportPrefix}Add_Code_Comments2", - out - ) - keyboard.hotKey(KeyEvent.VK_CONTROL, KeyEvent.VK_ALT, KeyEvent.VK_L) // Reformat - keyboard.hotKey(KeyEvent.VK_CONTROL, KeyEvent.VK_S) // Save - out.println( - """ - - ```$language""".trimIndent() - ) - out.println(FileUtils.readFileToString(File(testProjectPath, "src/$name.$extension"), "UTF-8")) - out.println( - """ - ``` - - """.trimIndent() - ) - - - out.println( - """ - - ## Conversion to other languages - - ### JavaScript - - We can also convert the code to other languages. For example, we can convert the code to JavaScript. - - """.trimIndent() - ) - clickText(getComponent("//div[@class='ProjectViewTree']"), name) - menuAction("Convert To") - keyboard.hotKey(KeyEvent.VK_DOWN) - writeImage(screenshot("//div[@class='IdeRootPane']"), reportDir, name, "${reportPrefix}Convert_to_js", out) - keyboard.hotKey(KeyEvent.VK_ENTER) - while (!File(testProjectPath, "src/$name.js").exists()) { - sleep(1000) - } - out.println( - """ - - ```js""".trimIndent() - ) - out.println(FileUtils.readFileToString(File(testProjectPath, "src/$name.js"), "UTF-8")) - out.println( - """ - ``` - """.trimIndent() - ) - - - out.println( - """ - ### Conversion to Scala - - We can also convert the code to Scala. - - """.trimIndent() - ) - clickText(getComponent("//div[@class='ProjectViewTree']"), name) - menuAction("Convert To") - keyboard.hotKey(KeyEvent.VK_DOWN) - keyboard.hotKey(KeyEvent.VK_DOWN) - writeImage( - screenshot("//div[@class='IdeRootPane']"), - reportDir, - name, - "${reportPrefix}Convert_to_scala", - out - ) - keyboard.hotKey(KeyEvent.VK_ENTER) - while (!File(testProjectPath, "src/$name.scala").exists()) { - sleep(1000) - } - out.println( - """ - ```scala""".trimIndent() - ) - out.println(FileUtils.readFileToString(File(testProjectPath, "src/$name.scala"), "UTF-8")) - out.println( - """ - ``` - """.trimIndent() - ) - - // Close editor - click("//div[@class='InplaceButton']") - } finally { - out2.close() - } - } - - private fun documentTextAppend( - name: String, - directive: String, - out: PrintWriter, - file: File - ) { - val reportPrefix = "${name}_" - out.println("") - out.println( - """ - # $name - - In this example, we'll use the "Append Text" command to add some text after our prompt. - - ``` - $directive - ``` - - """.trimIndent() - ) - newFile("$name.txt") - - click("//div[@class='EditorComponentImpl']") - keyboard.selectAll() - keyboard.key(KeyEvent.VK_DELETE) - enterLines(directive) - keyboard.selectAll() - val image1 = menuAction("Append Text") - awaitProcessing() - val image = image1 - writeImage(image, file, name, "${reportPrefix}menu", out) - keyboard.hotKey(KeyEvent.VK_CONTROL, KeyEvent.VK_S) // Save - - out.println( - """ - - This generates the following text: - - ```""".trimIndent() - ) - out.println(FileUtils.readFileToString(File(testProjectPath, "src/$name.txt"), "UTF-8")) - out.println( - """ - ``` - - """.trimIndent() - ) - - - out.println( - """ - - ## Edit Text - - We can also edit the text using the "Edit Text" command. - - """.trimIndent() - ) - click("//div[@class='EditorComponentImpl']") - keyboard.hotKey(KeyEvent.VK_CONTROL, KeyEvent.VK_A) // Select all - writeImage(menuAction("Edit Text"), file, name, "${reportPrefix}edit_text", out) - click("//div[@class='MultiplexingTextField']") - keyboard.enterText("Translate into a series of haikus") - writeImage(screenshot("//div[@class='JDialog']"), file, name, "${reportPrefix}edit_text_2", out) - keyboard.enter() - awaitProcessing() - writeImage(screenshot("//div[@class='IdeRootPane']"), file, name, "${reportPrefix}result1", out) - - - out.println( - """ - - We can also replace text using the "Replace Options" command. - - """.trimIndent() - ) - - keyboard.hotKey(KeyEvent.VK_CONTROL, KeyEvent.VK_S) // Save - keyboard.hotKey(KeyEvent.VK_CONTROL, KeyEvent.VK_HOME) - var documentText = FileUtils.readFileToString(File(testProjectPath, "src/$name.txt"), "UTF-8") - val randomLine = documentText.split("\n").map { it.trim() }.filter { it.length<100 }.take(40).toTypedArray().random() - selectText(getEditor(), randomLine) - keyboard.hotKey(KeyEvent.VK_SHIFT, KeyEvent.VK_END) - writeImage(menuAction("Replace Options"), file, name, "${reportPrefix}replace_options", out) - awaitProcessing() - sleep(500) - writeImage(screenshot("//div[@class='JDialog']"), file, name, "${reportPrefix}replace_options_2", out) - sleep(500) - - keyboard.enter() - writeImage(screenshot("//div[@class='IdeRootPane']"), file, name, "${reportPrefix}result2", out) - - keyboard.hotKey(KeyEvent.VK_CONTROL, KeyEvent.VK_S) // Save - documentText = FileUtils.readFileToString(File(testProjectPath, "src/$name.txt"), "UTF-8") - out.println( - """ - - Our text now looks like this: - - ```""".trimIndent() - ) - out.println(documentText) - out.println( - """ - ``` - - """.trimIndent() - ) - - - // Close editor - sleep(1000) - click("//div[@class='InplaceButton']") - } - - private fun getEditor() = getComponent("//div[@class='EditorComponentImpl']") - - private fun documentMarkdownTableOps( - name: String, - directive: String, - out: PrintWriter, - file: File - ) { - val reportPrefix = "${name}_" - out.println("") - out.println( - """ - # $name - - In this demo, we add a table to a Markdown document, and then add columns and rows to the table. - - We start with this seed directive: - - ``` - $directive - ``` - - """.trimIndent() - ) - newFile("$name.md") - - click("//div[@class='EditorComponentImpl']") - keyboard.selectAll() - keyboard.key(KeyEvent.VK_DELETE) - enterLines( - """ - $directive - - |""".trimIndent() - ) - keyboard.selectAll() - writeImage(menuAction("Append Text"), file, name, "${reportPrefix}menu", out) - awaitProcessing() - - keyboard.hotKey(KeyEvent.VK_CONTROL, KeyEvent.VK_S) // Save - out.println( - """ - This gives us the following Markdown document: - - ``` - """.trimIndent() - ) - out.println(FileUtils.readFileToString(File(testProjectPath, "src/$name.md"), "UTF-8")) - out.println( - """ - ``` - - """.trimIndent() - ) - - keyboard.hotKey(KeyEvent.VK_CONTROL, KeyEvent.VK_END) - writeImage(menuAction("Add Table Columns"), file, name, "${reportPrefix}add_columns", out) - awaitProcessing() - - keyboard.hotKey(KeyEvent.VK_CONTROL, KeyEvent.VK_S) // Save - out.println( - """ - - ```""".trimIndent() - ) - out.println(FileUtils.readFileToString(File(testProjectPath, "src/$name.md"), "UTF-8")) - out.println( - """ - ``` - - """.trimIndent() - ) - - keyboard.hotKey(KeyEvent.VK_CONTROL, KeyEvent.VK_Z) // Undo - writeImage(menuAction("Add Table Rows"), file, name, "${reportPrefix}add_rows", out) - awaitProcessing() - - keyboard.hotKey(KeyEvent.VK_CONTROL, KeyEvent.VK_S) // Save - out.println( - """ - - ```""".trimIndent() - ) - out.println(FileUtils.readFileToString(File(testProjectPath, "src/$name.md"), "UTF-8")) - out.println( - """ - ``` - - """.trimIndent() - ) - - writeImage(screenshot("//div[@class='IdeRootPane']"), file, name, "${reportPrefix}result", out) - - // Close editor - sleep(1000) - click("//div[@class='InplaceButton']") - } - - private fun documentMarkdownListAppend( - name: String, - directive: String, - examples: Array, - out: PrintWriter, - file: File - ) { - val reportPrefix = "${name}_" - out.println( - """ - # $name - - In this demo, we add a list to a Markdown document, and then add items to the list. - - ``` - $directive - ``` - - """.trimIndent() - ) - newFile("$name.md") - - click("//div[@class='EditorComponentImpl']") - keyboard.selectAll() - keyboard.key(KeyEvent.VK_DELETE) - enterLines(directive) - keyboard.enter() - keyboard.enter() - keyboard.enterText("1. ") - for (example in examples) { - keyboard.enterText(example) - keyboard.enter() - } - keyboard.selectAll() - writeImage(menuAction("Append Text"), file, name, "${reportPrefix}append_text", out) - awaitProcessing() - - keyboard.hotKey(KeyEvent.VK_CONTROL, KeyEvent.VK_S) // Save - out.println( - """ - - ``` - """.trimIndent() - ) - out.println(FileUtils.readFileToString(File(testProjectPath, "src/$name.md"), "UTF-8")) - out.println( - """ - ``` - - """.trimIndent() - ) - keyboard.hotKey(KeyEvent.VK_CONTROL, KeyEvent.VK_END) // End of document - - writeImage(screenshot("//div[@class='IdeRootPane']"), file, name, "${reportPrefix}result", out) - writeImage(menuAction("Add List Items"), file, name, "${reportPrefix}add_list_items", out) - awaitProcessing() - writeImage(screenshot("//div[@class='IdeRootPane']"), file, name, "${reportPrefix}result2", out) - - keyboard.hotKey(KeyEvent.VK_CONTROL, KeyEvent.VK_S) // Save - out.println( - """ - - ``` - """.trimIndent() - ) - out.println(FileUtils.readFileToString(File(testProjectPath, "src/$name.md"), "UTF-8")) - out.println( - """ - ``` - - """.trimIndent() - ) - - // Close editor - sleep(1000) - click("//div[@class='InplaceButton']") - } - - - private fun writeImage( - screenshot: BufferedImage, - file: File, - name: String, - subname: String, - out: PrintWriter - ) { - ImageIO.write(screenshot, "png", File(file, "$name-$subname.png")) - out.println( - """ - ![$subname](${name}-$subname.png) - - """.trimIndent() - ) - } - - private fun screenshot(path: String) = - getComponent(path).getScreenshot() - - private fun click(vararg path: String) { - getComponent(*path).click() - } - - private fun clickr(path: String) { - getComponent(path).rightClick() - } - - private fun enterLines(input: String) { - input.split("\n").map { line -> { -> keyboard.enterText(line) } }.reduce { a, b -> - { -> - a() - keyboard.enter() - b() - } - }() - } - - private fun getComponent(vararg paths: String) = - paths.flatMap { path -> try { - listOf(robot.find(ComponentFixture::class.java, byXpath(path))).filter { it != null } - } catch (ex : Throwable) { - listOf() - } }.first() - - } \ No newline at end of file diff --git a/src/test/kotlin/com/github/simiacryptus/aicoder/UITestUtil.kt b/src/test/kotlin/com/github/simiacryptus/aicoder/UITestUtil.kt new file mode 100644 index 00000000..5c52fe10 --- /dev/null +++ b/src/test/kotlin/com/github/simiacryptus/aicoder/UITestUtil.kt @@ -0,0 +1,842 @@ +package com.github.simiacryptus.aicoder + +import com.intellij.remoterobot.RemoteRobot +import com.intellij.remoterobot.fixtures.ComponentFixture +import com.intellij.remoterobot.fixtures.dataExtractor.RemoteText +import com.intellij.remoterobot.search.locators.byXpath +import com.intellij.remoterobot.utils.Keyboard +import com.jetbrains.rd.util.first +import com.jetbrains.rd.util.firstOrNull +import org.apache.commons.io.FileUtils +import java.awt.Point + +import java.awt.event.KeyEvent +import java.awt.image.BufferedImage +import java.io.File +import java.io.FileOutputStream +import java.io.PrintWriter +import java.lang.Thread.sleep +import javax.imageio.ImageIO + +/** + * See Also: + * https://github.com/JetBrains/intellij-ui-test-robot + * https://joel-costigliola.github.io/assertj/swing/api/org/assertj/swing/core/Robot.html + */ +class UITestUtil { + + public companion object { + + val outputDir = File("C:\\Users\\andre\\code\\aicoder\\intellij-aicoder-docs") + val testProjectPath = File("C:\\Users\\andre\\IdeaProjects\\automated-ui-test-workspace") + private val testIdeUrl = "http://127.0.0.1:8082" + private val robot = RemoteRobot(testIdeUrl) + val keyboard = Keyboard(robot) + + /** + * + * Creates a new file with the given name. + * + * @param name The name of the file to create. + */ + fun newFile(name: String) { + click("//div[@class='ProjectViewTree']") + keyboard.key(KeyEvent.VK_RIGHT) + keyboard.enterText("src") + keyboard.key(KeyEvent.VK_CONTEXT_MENU) + click( + "//div[contains(@text.key, 'group.NewGroup.text')]", + "//div[contains(@text.key, 'group.WeighingNewGroup.text')]" + ) + sleep(500) + click("//div[@class='HeavyWeightWindow'][.//div[@class='MyMenu']]//div[@class='HeavyWeightWindow']//div[contains(@text.key, 'group.FileMenu.text')]") + keyboard.enterText(name) + keyboard.enter() + sleep(500) + } + + private fun isStarted(): Boolean = + robot.findAll( + ComponentFixture::class.java, + byXpath("//div[contains(@accessiblename.key, 'editor.accessible.name')]") + ).isNotEmpty() + + private fun isDialogOpen(): Boolean = + robot.findAll(ComponentFixture::class.java, byXpath("//div[@class='EngravedLabel']")).isNotEmpty() + + private fun isStillRunning(): Boolean { + val resultText = + componentText("//div[contains(@accessiblename.key, 'editor.accessible.name')]") ?: return false + return !(resultText.contains("Process finished")) + } + + + fun componentText(s: String): String { + return getLines(getComponent(s)) + } + + private fun getLines(element: ComponentFixture): String { + val lines = element.data.getAll().groupBy { it.point.y } + return lines.toList().sortedBy { it.first }.map { it.second } + .map { it.toList().sortedBy { it.point.x }.map { it.text }.reduce { a, b -> a + b } } + .reduceOrNull { a, b -> a + "\n" + b }.orEmpty() + } + + fun findText(element: ComponentFixture, text: String): Pair? { + val lines: Map> = element.data.getAll().groupBy { it.point.y } + val line = lines.filter { it.value.map { it.text }.reduce { a, b -> a + b }.contains(text) }.firstOrNull()?.value + ?: return null + val index = line.map { it.text }.reduce { a, b -> a + b }.indexOf(text) + val lineBuffer = line.toMutableList() + var left = index + while (left > 0) { + val first = lineBuffer.first() + if (first.text.length <= left) { + left -= first.text.length + lineBuffer.removeAt(0) + } + } + val leftPoint = lineBuffer.first().point + left += text.length + var rightPoint: Point = leftPoint + while (left > 0) { + val first = lineBuffer.first() + if (first.text.length <= left) { + left -= first.text.length + rightPoint = lineBuffer.removeAt(0).point + } + } + if (lineBuffer.isNotEmpty()) { + rightPoint = lineBuffer.first().point + } + return Pair(leftPoint, rightPoint) + } + + fun selectText(element: ComponentFixture, text: String) { + keyboard.hotKey(KeyEvent.VK_CONTROL, KeyEvent.VK_HOME) + var findText = findText(element, text) + var pages = 0 + while (null == findText && pages++ < 10) { + keyboard.hotKey(KeyEvent.VK_PAGE_DOWN) + findText = findText(element, text) + } + val (leftPoint: Point, rightPoint: Point) = findText ?: throw Exception("Could not find text $text") + element.runJs("robot.pressMouse(component, new Point(${leftPoint.x}, ${leftPoint.y}))") + element.runJs("robot.moveMouse(component, new Point(${rightPoint.x}, ${rightPoint.y}))") + element.runJs("robot.releaseMouseButtons()") + } + + fun clickText(element: ComponentFixture, text: String) { + val (leftPoint, rightPoint) = findText(element, text) ?: throw Exception("Could not find text $text") + element.runJs("robot.click(component, new Point(${leftPoint.x}, ${leftPoint.y}))") + } + + fun rightClickText(element: ComponentFixture, text: String) { + val (leftPoint, rightPoint) = findText(element, text) ?: throw Exception("Could not find text $text") + element.runJs("robot.rightClick(component, new Point(${leftPoint.x}, ${leftPoint.y}))") + } + + /** + * + * Implements a Java class with the given name and task. + * + * @param name The name of the class to be implemented. + * @param task The task to be implemented in the class. + * @return A [BufferedImage] of the command used. + */ + fun implementCode(prompt: String): BufferedImage { + click("//div[@class='EditorComponentImpl']") + keyboard.selectAll() + keyboard.key(KeyEvent.VK_DELETE) + enterLines(prompt) + val image = menuAction("Insert Implementation") + awaitProcessing() + return image + } + + fun menuAction(menuText: String): BufferedImage { + keyboard.key(KeyEvent.VK_CONTEXT_MENU) + sleep(100) + val aiCoderMenuItem = getComponent("//div[@text='AI Coder']") + aiCoderMenuItem.click() + val point1 = aiCoderMenuItem.locationOnScreen + sleep(100) + val submenu = + getComponent("//div[@class='HeavyWeightWindow'][.//div[@class='MyMenu']]//div[@class='HeavyWeightWindow']//div[contains(@text, '$menuText')]") + val point2 = submenu.locationOnScreen + submenu.runJs("robot.moveMouse(new Point(${point2.x}, ${point1.y}))") + submenu.runJs("robot.moveMouse(component)") + val image = screenshot("//div[@class='IdeRootPane']") + submenu.click() + return image + } + + /** + * + * Awaits the completion of a running process. + * + * This function will wait until the process is started, then wait until it is finished. + * It will also print out the total time the process took to complete. + */ + fun awaitRunCompletion() { + while (!isStarted()) sleep(1) + val start = System.currentTimeMillis() + println("Process started") + while (isStillRunning()) { + sleep(100) + } + val end = System.currentTimeMillis() + println("Process ended after ${(end - start) / 1000.0}") + } + + fun awaitProcessing() { + while (!isDialogOpen()) sleep(1) + val start = System.currentTimeMillis() + println("Dialog opened") + while (isDialogOpen()) sleep(100) + val end = System.currentTimeMillis() + println("Dialog closed after ${(end - start) / 1000.0}") + } + + /** + * + * Documents the Java implementation of the given name and directive. + * + * @param name The name of the Java implementation. + * @param directive The directive for the Java implementation. + * @param out The [PrintWriter] to write the documentation to. + * @param reportDir The [File] directory to save the screenshots to. + */ + fun documentJavaImplementation( + name: String, + directive: String, + out: PrintWriter, + reportDir: File + ) { + documentImplementation( + name = name, + out1 = out, + directive = directive, + extension = "java", + prompt = "public class $name {\\n // $directive", + reportDir = reportDir, + language = "java", + selector = "static void main" + ) + } + + fun documentImplementation( + name: String, + out1: PrintWriter, + directive: String, + extension: String, + prompt: String, + reportDir: File, + language: String, + selector: String + ) { + val reportPrefix = "${name}_${language}_" + val testOutputFile = File(outputDir, "$reportPrefix$name.md") + val out2 = PrintWriter(FileOutputStream(testOutputFile)) + + try { + out1.println("[$directive ($language)]($reportPrefix$name.md)\n\n") + val out = out2 + + out.println( + """ + + # $name + + In this test we will used AI Coding Assistant to implement the $name class to solve the following problem: + + """.trimIndent() + ) + out.println("```\n$directive\n```") + newFile("$name.$extension") + + + out.println( + """ + + ## Implementation + + The first step is to translate the problem into code. We can do this by using the "Insert Implementation" command. + + """.trimIndent() + ) + val image = implementCode(prompt) + writeImage(image, reportDir, name, "${reportPrefix}menu", out) + keyboard.hotKey(KeyEvent.VK_SHIFT, KeyEvent.VK_UP) + keyboard.hotKey(KeyEvent.VK_DELETE) + keyboard.hotKey(KeyEvent.VK_CONTROL, KeyEvent.VK_ALT, KeyEvent.VK_L) // Reformat + keyboard.hotKey(KeyEvent.VK_CONTROL, KeyEvent.VK_S) // Save + out.println( + """ + + This results in the following code: + + ```$language""".trimIndent() + ) + out.println(FileUtils.readFileToString(File(testProjectPath, "src/$name.$extension"), "UTF-8")) + out.println("```") + + + out.println( + """ + + ## Execution + + This code can be executed by pressing the "Run" button in the top right corner of the IDE. + What could possibly go wrong? + + """.trimIndent() + ) + keyboard.hotKey(KeyEvent.VK_CONTROL, KeyEvent.VK_SHIFT, KeyEvent.VK_F10) // Run + awaitRunCompletion() + out.println( + """ + + ```""".trimIndent() + ) + out.println(componentText("//div[contains(@accessiblename.key, 'editor.accessible.name')]")) + out.println( + """ + ``` + """.trimIndent() + ) + writeImage(screenshot("//div[@class='IdeRootPane']"), reportDir, name, "${reportPrefix}result", out) + // Close run tab + sleep(100) + clickr("//div[@class='ContentTabLabel']") + sleep(100) + click("//div[contains(@text.key, 'action.CloseContent.text')]") + sleep(100) + + + out.println( + """ + + ## Rename Variables + + The code is not very readable. We can use the "Rename Variables" command to make it more readable... + + """.trimIndent() + ) + keyboard.hotKey(KeyEvent.VK_CONTROL, KeyEvent.VK_HOME) // Move to top + selectText(getEditor(), selector) + keyboard.hotKey(KeyEvent.VK_CONTROL, KeyEvent.VK_W) // Select function + writeImage(menuAction("Rename Variables"), reportDir, name, "${reportPrefix}Rename_Variables", out) + awaitProcessing() + sleep(1000) + writeImage( + screenshot("//div[@class='JDialog']"), + reportDir, + name, + "${reportPrefix}Rename_Variables_Dialog", + out + ) + click("//div[@text.key='button.ok']") + + + out.println( + """ + + ## Documentation Comments + + We also want good documentation for our code. We can use the "Add Documentation Comments" command to do this. + + """.trimIndent() + ) + selectText(getEditor(), selector) + writeImage(menuAction("Doc Comments"), reportDir, name, "${reportPrefix}Add_Doc_Comments", out) + awaitProcessing() + keyboard.hotKey(KeyEvent.VK_CONTROL, KeyEvent.VK_HOME) // Move to top + writeImage( + screenshot("//div[@class='IdeRootPane']"), + reportDir, + name, + "${reportPrefix}Add_Doc_Comments2", + out + ) + + + out.println( + """ + + ## Ad-Hoc Questions + + We can also ask questions about the code. For example, we can ask what the big-O runtime is for this code. + + """.trimIndent() + ) + selectText(getEditor(), selector) + keyboard.hotKey(KeyEvent.VK_CONTROL, KeyEvent.VK_W) // Select function + writeImage(menuAction("Ask a question"), reportDir, name, "${reportPrefix}Ask_Q", out) + click("//div[@class='MultiplexingTextField']") + keyboard.enterText("What is the big-O runtime and why?") + writeImage(screenshot("//div[@class='JDialog']"), reportDir, name, "${reportPrefix}Ask_Q2", out) + click("//div[@text.key='button.ok']") + awaitProcessing() + keyboard.hotKey(KeyEvent.VK_CONTROL, KeyEvent.VK_HOME) // Move to top + writeImage(screenshot("//div[@class='IdeRootPane']"), reportDir, name, "${reportPrefix}Ask_Q3", out) + + out.println( + """ + + ## Code Comments + + We can also add code comments to the code. This is useful for explaining the code to other developers. + + """.trimIndent() + ) + selectText(getEditor(), selector) + keyboard.hotKey(KeyEvent.VK_CONTROL, KeyEvent.VK_W) // Select function + writeImage(menuAction("Code Comments"), reportDir, name, "${reportPrefix}Add_Code_Comments", out) + awaitProcessing() + writeImage( + screenshot("//div[@class='IdeRootPane']"), + reportDir, + name, + "${reportPrefix}Add_Code_Comments2", + out + ) + keyboard.hotKey(KeyEvent.VK_CONTROL, KeyEvent.VK_ALT, KeyEvent.VK_L) // Reformat + keyboard.hotKey(KeyEvent.VK_CONTROL, KeyEvent.VK_S) // Save + out.println( + """ + + ```$language""".trimIndent() + ) + out.println(FileUtils.readFileToString(File(testProjectPath, "src/$name.$extension"), "UTF-8")) + out.println( + """ + ``` + + """.trimIndent() + ) + + + out.println( + """ + + ## Conversion to other languages + + ### JavaScript + + We can also convert the code to other languages. For example, we can convert the code to JavaScript. + + """.trimIndent() + ) + clickText(getComponent("//div[@class='ProjectViewTree']"), name) + menuAction("Convert To") + keyboard.hotKey(KeyEvent.VK_DOWN) + writeImage( + screenshot("//div[@class='IdeRootPane']"), + reportDir, + name, + "${reportPrefix}Convert_to_js", + out + ) + keyboard.hotKey(KeyEvent.VK_ENTER) + while (!File(testProjectPath, "src/$name.js").exists()) { + sleep(1000) + } + out.println( + """ + + ```js""".trimIndent() + ) + out.println(FileUtils.readFileToString(File(testProjectPath, "src/$name.js"), "UTF-8")) + out.println( + """ + ``` + """.trimIndent() + ) + + + out.println( + """ + ### Conversion to Scala + + We can also convert the code to Scala. + + """.trimIndent() + ) + clickText(getComponent("//div[@class='ProjectViewTree']"), name) + menuAction("Convert To") + keyboard.hotKey(KeyEvent.VK_DOWN) + keyboard.hotKey(KeyEvent.VK_DOWN) + writeImage( + screenshot("//div[@class='IdeRootPane']"), + reportDir, + name, + "${reportPrefix}Convert_to_scala", + out + ) + keyboard.hotKey(KeyEvent.VK_ENTER) + while (!File(testProjectPath, "src/$name.scala").exists()) { + sleep(1000) + } + out.println( + """ + ```scala""".trimIndent() + ) + out.println(FileUtils.readFileToString(File(testProjectPath, "src/$name.scala"), "UTF-8")) + out.println( + """ + ``` + """.trimIndent() + ) + + // Close editor + click("//div[@class='InplaceButton']") + } finally { + out2.close() + } + } + + fun documentTextAppend( + name: String, + directive: String, + out: PrintWriter, + file: File + ) { + val reportPrefix = "${name}_" + out.println("") + out.println( + """ + # $name + + In this example, we'll use the "Append Text" command to add some text after our prompt. + + ``` + $directive + ``` + + """.trimIndent() + ) + newFile("$name.txt") + + click("//div[@class='EditorComponentImpl']") + keyboard.selectAll() + keyboard.key(KeyEvent.VK_DELETE) + enterLines(directive) + keyboard.selectAll() + val image1 = menuAction("Append Text") + awaitProcessing() + val image = image1 + writeImage(image, file, name, "${reportPrefix}menu", out) + keyboard.hotKey(KeyEvent.VK_CONTROL, KeyEvent.VK_S) // Save + + out.println( + """ + + This generates the following text: + + ```""".trimIndent() + ) + out.println(FileUtils.readFileToString(File(testProjectPath, "src/$name.txt"), "UTF-8")) + out.println( + """ + ``` + + """.trimIndent() + ) + + + out.println( + """ + + ## Edit Text + + We can also edit the text using the "Edit Text" command. + + """.trimIndent() + ) + click("//div[@class='EditorComponentImpl']") + keyboard.hotKey(KeyEvent.VK_CONTROL, KeyEvent.VK_A) // Select all + writeImage(menuAction("Edit Text"), file, name, "${reportPrefix}edit_text", out) + click("//div[@class='MultiplexingTextField']") + keyboard.enterText("Translate into a series of haikus") + writeImage(screenshot("//div[@class='JDialog']"), file, name, "${reportPrefix}edit_text_2", out) + keyboard.enter() + awaitProcessing() + writeImage(screenshot("//div[@class='IdeRootPane']"), file, name, "${reportPrefix}result1", out) + + + out.println( + """ + + We can also replace text using the "Replace Options" command. + + """.trimIndent() + ) + + keyboard.hotKey(KeyEvent.VK_CONTROL, KeyEvent.VK_S) // Save + keyboard.hotKey(KeyEvent.VK_CONTROL, KeyEvent.VK_HOME) + var documentText = FileUtils.readFileToString(File(testProjectPath, "src/$name.txt"), "UTF-8") + val randomLine = + documentText.split("\n").map { it.trim() }.filter { it.length < 100 }.take(40).toTypedArray().random() + selectText(getEditor(), randomLine) + keyboard.hotKey(KeyEvent.VK_SHIFT, KeyEvent.VK_END) + writeImage(menuAction("Replace Options"), file, name, "${reportPrefix}replace_options", out) + awaitProcessing() + sleep(500) + writeImage(screenshot("//div[@class='JDialog']"), file, name, "${reportPrefix}replace_options_2", out) + sleep(500) + + keyboard.enter() + writeImage(screenshot("//div[@class='IdeRootPane']"), file, name, "${reportPrefix}result2", out) + + keyboard.hotKey(KeyEvent.VK_CONTROL, KeyEvent.VK_S) // Save + documentText = FileUtils.readFileToString(File(testProjectPath, "src/$name.txt"), "UTF-8") + out.println( + """ + + Our text now looks like this: + + ```""".trimIndent() + ) + out.println(documentText) + out.println( + """ + ``` + + """.trimIndent() + ) + + + // Close editor + sleep(1000) + click("//div[@class='InplaceButton']") + } + + fun getEditor() = getComponent("//div[@class='EditorComponentImpl']") + + fun documentMarkdownTableOps( + name: String, + directive: String, + out: PrintWriter, + file: File + ) { + val reportPrefix = "${name}_" + out.println("") + out.println( + """ + # $name + + In this demo, we add a table to a Markdown document, and then add columns and rows to the table. + + We start with this seed directive: + + ``` + $directive + ``` + + """.trimIndent() + ) + newFile("$name.md") + + click("//div[@class='EditorComponentImpl']") + keyboard.selectAll() + keyboard.key(KeyEvent.VK_DELETE) + enterLines( + """ + $directive + + |""".trimIndent() + ) + keyboard.selectAll() + writeImage(menuAction("Append Text"), file, name, "${reportPrefix}menu", out) + awaitProcessing() + + keyboard.hotKey(KeyEvent.VK_CONTROL, KeyEvent.VK_S) // Save + out.println( + """ + This gives us the following Markdown document: + + ``` + """.trimIndent() + ) + out.println(FileUtils.readFileToString(File(testProjectPath, "src/$name.md"), "UTF-8")) + out.println( + """ + ``` + + """.trimIndent() + ) + + keyboard.hotKey(KeyEvent.VK_CONTROL, KeyEvent.VK_END) + writeImage(menuAction("Add Table Columns"), file, name, "${reportPrefix}add_columns", out) + awaitProcessing() + + keyboard.hotKey(KeyEvent.VK_CONTROL, KeyEvent.VK_S) // Save + out.println( + """ + + ```""".trimIndent() + ) + out.println(FileUtils.readFileToString(File(testProjectPath, "src/$name.md"), "UTF-8")) + out.println( + """ + ``` + + """.trimIndent() + ) + + keyboard.hotKey(KeyEvent.VK_CONTROL, KeyEvent.VK_Z) // Undo + writeImage(menuAction("Add Table Rows"), file, name, "${reportPrefix}add_rows", out) + awaitProcessing() + + keyboard.hotKey(KeyEvent.VK_CONTROL, KeyEvent.VK_S) // Save + out.println( + """ + + ```""".trimIndent() + ) + out.println(FileUtils.readFileToString(File(testProjectPath, "src/$name.md"), "UTF-8")) + out.println( + """ + ``` + + """.trimIndent() + ) + + writeImage(screenshot("//div[@class='IdeRootPane']"), file, name, "${reportPrefix}result", out) + + // Close editor + sleep(1000) + click("//div[@class='InplaceButton']") + } + + fun documentMarkdownListAppend( + name: String, + directive: String, + examples: Array, + out: PrintWriter, + file: File + ) { + val reportPrefix = "${name}_" + out.println( + """ + # $name + + In this demo, we add a list to a Markdown document, and then add items to the list. + + ``` + $directive + ``` + + """.trimIndent() + ) + newFile("$name.md") + + click("//div[@class='EditorComponentImpl']") + keyboard.selectAll() + keyboard.key(KeyEvent.VK_DELETE) + enterLines(directive) + keyboard.enter() + keyboard.enter() + keyboard.enterText("1. ") + for (example in examples) { + keyboard.enterText(example) + keyboard.enter() + } + keyboard.selectAll() + writeImage(menuAction("Append Text"), file, name, "${reportPrefix}append_text", out) + awaitProcessing() + + keyboard.hotKey(KeyEvent.VK_CONTROL, KeyEvent.VK_S) // Save + out.println( + """ + + ``` + """.trimIndent() + ) + out.println(FileUtils.readFileToString(File(testProjectPath, "src/$name.md"), "UTF-8")) + out.println( + """ + ``` + + """.trimIndent() + ) + keyboard.hotKey(KeyEvent.VK_CONTROL, KeyEvent.VK_END) // End of document + + writeImage(screenshot("//div[@class='IdeRootPane']"), file, name, "${reportPrefix}result", out) + writeImage(menuAction("Add List Items"), file, name, "${reportPrefix}add_list_items", out) + awaitProcessing() + writeImage(screenshot("//div[@class='IdeRootPane']"), file, name, "${reportPrefix}result2", out) + + keyboard.hotKey(KeyEvent.VK_CONTROL, KeyEvent.VK_S) // Save + out.println( + """ + + ``` + """.trimIndent() + ) + out.println(FileUtils.readFileToString(File(testProjectPath, "src/$name.md"), "UTF-8")) + out.println( + """ + ``` + + """.trimIndent() + ) + + // Close editor + sleep(1000) + click("//div[@class='InplaceButton']") + } + + + fun writeImage( + screenshot: BufferedImage, + file: File, + name: String, + subname: String, + out: PrintWriter + ) { + ImageIO.write(screenshot, "png", File(file, "$name-$subname.png")) + out.println( + """ + ![$subname](${name}-$subname.png) + + """.trimIndent() + ) + } + + fun screenshot(path: String) = + getComponent(path).getScreenshot() + + fun click(vararg path: String) { + getComponent(*path).click() + } + + fun clickr(path: String) { + getComponent(path).rightClick() + } + + fun enterLines(input: String) { + input.split("\n").map { line -> { -> keyboard.enterText(line, 5) } }.reduce { a, b -> + { -> + a() + keyboard.enter() + b() + } + }() + } + + fun getComponent(vararg paths: String) = + paths.flatMap { path -> + try { + listOf(robot.find(ComponentFixture::class.java, byXpath(path))).filter { it != null } + } catch (ex: Throwable) { + listOf() + } + }.first() + + fun canRunTests() = outputDir.exists() && testProjectPath.exists() + + } + +} \ No newline at end of file