From c49c3c73fffe6acf7ce7c90833af6e65d0cb294b Mon Sep 17 00:00:00 2001 From: Andrew Charneski Date: Thu, 26 Oct 2023 13:25:00 -0400 Subject: [PATCH] 1.2.17 (#99) * wip * 1.2.17 * Update settings.gradle.kts --- README.md | 5 +- build.gradle.kts | 4 +- .../code/InsertImplementationAction.groovy | 28 ++- .../aicoder/actions/code/PasteAction.groovy | 5 +- .../actions/code/QuestionAction.groovy | 2 +- .../generic/ReplaceOptionsAction.groovy | 2 +- .../aicoder/actions/SelectionAction.kt | 2 + .../aicoder/actions/dev/AppServer.kt | 116 +++++++++ .../aicoder/actions/dev/CodeChatAction.kt | 126 +--------- .../aicoder/actions/dev/CodeChatServer.kt | 87 +++++++ .../actions/dev/LaunchSkyenetAction.kt | 88 +------ .../dev/SkyenetProjectCodingSessionServer.kt | 64 +++++ .../aicoder/config/ActionSettingsRegistry.kt | 2 +- .../aicoder/config/AppSettingsComponent.kt | 8 + .../aicoder/config/AppSettingsState.kt | 2 + .../code/InsertImplementationAction.groovy | 28 ++- .../aicoder/actions/code/PasteAction.groovy | 5 +- .../actions/code/QuestionAction.groovy | 2 +- .../generic/ReplaceOptionsAction.groovy | 2 +- .../aicoder/ApplicationDevelopmentUITest.kt | 14 +- .../com/github/simiacryptus/aicoder/UITest.kt | 6 +- .../github/simiacryptus/aicoder/UITestUtil.kt | 230 +++++++++++------- 22 files changed, 495 insertions(+), 333 deletions(-) create mode 100644 src/main/kotlin/com/github/simiacryptus/aicoder/actions/dev/AppServer.kt create mode 100644 src/main/kotlin/com/github/simiacryptus/aicoder/actions/dev/CodeChatServer.kt create mode 100644 src/main/kotlin/com/github/simiacryptus/aicoder/actions/dev/SkyenetProjectCodingSessionServer.kt diff --git a/README.md b/README.md index beef440a..579d4b2b 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ # **AI Coding Assistant Plugin for IntelliJ** -The only fully open source plugin for IntelliJ that integrates with OpenAI's GPT-4 API. +**Fully open source plugin for IntelliJ that integrates with OpenAI's GPT-4 API** * This requires an OpenAI access token, which you can get by signing up for a developer account at https://platform.openai.com/ * No membership fees! API access is paid but is billed by usage, with no base fee. The plugin is free and open source. @@ -21,6 +21,9 @@ Federation against Ukraine and various other countries, this plugin will not fun _We are working on a solution to this problem_, but cannot yet promise a date for the victory. **_Slava Ukraini!_** +**NOTE**: This project is not affiliated with OpenAI, JetBrains, or any other corporation or organization. +It is provided free of charge, as-is, with no warranty or guarantee of any kind. + ## **Installation & Configuration** To begin with AI Coding Assistant, you will need an [OpenAI access token](https://platform.openai.com/account/api-keys). diff --git a/build.gradle.kts b/build.gradle.kts index 6126f909..ac9a3e3a 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -25,10 +25,10 @@ repositories { val kotlin_version = "1.7.22" val jetty_version = "11.0.15" val slf4j_version = "2.0.5" -val skyenet_version = "1.0.17" +val skyenet_version = "1.0.18" dependencies { - implementation(group = "com.simiacryptus", name = "joe-penai", version = "1.0.19") + implementation(group = "com.simiacryptus", name = "joe-penai", version = "1.0.20") implementation(group = "com.simiacryptus.skyenet", name = "util", version = skyenet_version) implementation(group = "com.simiacryptus.skyenet", name = "core", version = skyenet_version) 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 index d329fb00..3a4baf0e 100644 --- a/src/main/groovy/com/github/simiacryptus/aicoder/actions/code/InsertImplementationAction.groovy +++ b/src/main/groovy/com/github/simiacryptus/aicoder/actions/code/InsertImplementationAction.groovy @@ -3,8 +3,10 @@ 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.application.ApplicationManager import com.intellij.openapi.project.Project import com.simiacryptus.openai.proxy.ChatProxy import kotlin.Pair @@ -85,18 +87,20 @@ class InsertImplementationAction extends SelectionAction { .filter { x -> !x.isEmpty() } .reduce { a, b -> "$a $b" }.get() if(null != state.psiFile) { - def psiClassContext = PsiClassContext.getContext( - state.psiFile, - psiClassContextActionParams.selectionStart, - psiClassContextActionParams.selectionEnd, - computerLanguage - ).toString() - def code = proxy.implementCode( - specification, - psiClassContext, - computerLanguage.name(), - humanLanguage, - ).code + def code = UITools.run(state.project, "Insert Implementation", true, true, { + def psiClassContext = 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( 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 index 227e698f..1809d328 100644 --- a/src/main/groovy/com/github/simiacryptus/aicoder/actions/code/PasteAction.groovy +++ b/src/main/groovy/com/github/simiacryptus/aicoder/actions/code/PasteAction.groovy @@ -11,6 +11,7 @@ import org.jetbrains.annotations.Nullable import java.awt.Toolkit import java.awt.datatransfer.DataFlavor +import java.awt.datatransfer.Transferable class PasteAction extends SelectionAction { PasteAction() { @@ -62,6 +63,8 @@ class PasteAction extends SelectionAction { } private Object getClipboard() { - return Toolkit.getDefaultToolkit().systemClipboard.getContents(null)?.getTransferData(DataFlavor.stringFlavor) + 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/QuestionAction.groovy b/src/main/groovy/com/github/simiacryptus/aicoder/actions/code/QuestionAction.groovy index 47bfb8f3..aa5ec213 100644 --- a/src/main/groovy/com/github/simiacryptus/aicoder/actions/code/QuestionAction.groovy +++ b/src/main/groovy/com/github/simiacryptus/aicoder/actions/code/QuestionAction.groovy @@ -57,7 +57,7 @@ class QuestionAction extends SelectionAction { def blockComment = state.language?.blockComment def fromString = blockComment?.fromString(answer) - return "${state.indent}${fromString.withIndent(state.indent)}\n${state.indent}" + state.selectedText + return "${state.indent}${fromString?.withIndent(state.indent) ?: ""}\n${state.indent}" + state.selectedText } @Override 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 index c21708cd..a67114a2 100644 --- a/src/main/groovy/com/github/simiacryptus/aicoder/actions/generic/ReplaceOptionsAction.groovy +++ b/src/main/groovy/com/github/simiacryptus/aicoder/actions/generic/ReplaceOptionsAction.groovy @@ -40,7 +40,7 @@ class ReplaceOptionsAction extends SelectionAction { } @Override - String processSelection(@Nullable AnActionEvent event, @NotNull SelectionState selectionState, @Nullable String config) { + 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() diff --git a/src/main/kotlin/com/github/simiacryptus/aicoder/actions/SelectionAction.kt b/src/main/kotlin/com/github/simiacryptus/aicoder/actions/SelectionAction.kt index 0faea58a..17dbee6e 100644 --- a/src/main/kotlin/com/github/simiacryptus/aicoder/actions/SelectionAction.kt +++ b/src/main/kotlin/com/github/simiacryptus/aicoder/actions/SelectionAction.kt @@ -76,6 +76,7 @@ abstract class SelectionAction( indent = indent, contextRanges = editorState.contextRanges, psiFile = editorState.psiFile, + project = event.project ), config = config ) @@ -171,6 +172,7 @@ abstract class SelectionAction( val indent: CharSequence? = null, val contextRanges: Array = arrayOf(), val psiFile: PsiFile?, + val project: Project? ) open fun isLanguageSupported(computerLanguage: ComputerLanguage?): Boolean { diff --git a/src/main/kotlin/com/github/simiacryptus/aicoder/actions/dev/AppServer.kt b/src/main/kotlin/com/github/simiacryptus/aicoder/actions/dev/AppServer.kt new file mode 100644 index 00000000..2977daa0 --- /dev/null +++ b/src/main/kotlin/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.body.WebSocketServer +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( + val localName: String, + val port: Int, + project: Project? +) { + + val log by lazy { LoggerFactory.getLogger(javaClass) } + + 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: WebSocketServer) { + 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) + } + } + + fun newWebAppContext(server: WebSocketServer, path: String): WebAppContext { + val context = WebAppContext() + JettyWebSocketServletContainerInitializer.configure(context, null) + context.baseResource = server.baseResource + context.contextPath = path + context.welcomeFiles = arrayOf("index.html") + val webAppContext = context + server.configure(webAppContext, baseUrl = "$domainName/$path") + return webAppContext + } + + 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() + } + } + + 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/kotlin/com/github/simiacryptus/aicoder/actions/dev/CodeChatAction.kt b/src/main/kotlin/com/github/simiacryptus/aicoder/actions/dev/CodeChatAction.kt index 09581fe1..850e907e 100644 --- a/src/main/kotlin/com/github/simiacryptus/aicoder/actions/dev/CodeChatAction.kt +++ b/src/main/kotlin/com/github/simiacryptus/aicoder/actions/dev/CodeChatAction.kt @@ -8,140 +8,24 @@ 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.simiacryptus.openai.OpenAIClient.ChatMessage -import com.simiacryptus.openai.OpenAIClient.ChatRequest -import com.simiacryptus.skyenet.Heart -import com.simiacryptus.skyenet.body.* -import com.simiacryptus.skyenet.heart.WeakGroovyInterpreter -import org.eclipse.jetty.util.resource.Resource import java.awt.Desktop -import java.util.function.Supplier +import java.util.* class CodeChatAction : BaseAction() { - inner class CodeChatServer( - val e: AnActionEvent, - port: Int, - val language: String, - val codeSelection: String, - baseURL: String = "http://localhost:$port", - ) : SkyenetCodingSessionServer( - applicationName = "Code Chat", - model = AppSettingsState.instance.defaultChatModel(), - apiKey = AppSettingsState.instance.apiKey, - ) { - - val rootOperationID = (0..5).map { ('a'..'z').random() }.joinToString("") - var rootMessageTrail: String = "" - - override fun newSession(sessionId: String): SkyenetCodingSession { - val newSession = CodeChatSession(sessionId) -rootMessageTrail = -"""$rootOperationID,

Code:

${htmlEscape(codeSelection)}
""" - newSession.send(rootMessageTrail) - return newSession - } - - private fun htmlEscape(html: String): String { - return html.replace("&", "&").replace("<", "<") - .replace(">", ">").replace("\"", """) - .replace("'", "'") - } - - open inner class CodeChatSession(sessionId: String) : SkyenetCodingSession(sessionId, this@CodeChatServer) { - override fun run(userMessage: String) { - var messageTrail = ChatSession.divInitializer() - send("""$messageTrail
$userMessage
$spinner
""") - messages += ChatMessage(ChatMessage.Role.user, userMessage) - val response = api.chat(chatRequest, model).choices.first()?.message?.content.orEmpty() - messages += ChatMessage(ChatMessage.Role.assistant, response) - messageTrail += ChatSessionFlexmark.renderMarkdown(response) - send(messageTrail) - } - - val messages = listOf( - ChatMessage( - ChatMessage.Role.system, """ - |You are a helpful AI that helps people with coding. - | - |You will be answering questions about the following code: - | - |```$language - |$codeSelection - |``` - | - |Responses may use markdown formatting. - """.trimMargin() - ) - ).toMutableList() - val chatRequest: ChatRequest - get() { - val chatRequest = ChatRequest() - val model = AppSettingsState.instance.defaultChatModel() - chatRequest.model = model.modelName - chatRequest.max_tokens = model.maxTokens - chatRequest.temperature = AppSettingsState.instance.temperature - chatRequest.messages = messages.toTypedArray() - return chatRequest - } - } - - override val baseResource: Resource - get() = ClasspathResource(javaClass.classLoader.getResource(resourceBase)) - - override fun hands() = java.util.HashMap() as java.util.Map - - override fun toString(e: Throwable): String { - return e.message ?: e.toString() - } - - override fun heart(hands: java.util.Map): Heart = object : WeakGroovyInterpreter(hands) { - override fun wrapExecution(fn: Supplier): T? { - return UITools.run( - e.project, "Running Script", false - ) { - fn.get() - } - } - } - } - override fun handle(event: AnActionEvent) { val editor = event.getData(CommonDataKeys.EDITOR) ?: return val caretModel = editor.caretModel val primaryCaret = caretModel.primaryCaret val selectedText = primaryCaret.selectedText ?: editor.document.text val language = ComputerLanguage.getComputerLanguage(event)?.name ?: return - - val port = (8000 + (Math.random() * 8000).toInt()) - val skyenet = CodeChatServer(event, port, language, selectedText, baseURL = "http://localhost:$port") - val server = skyenet.start(port) - - Thread { - try { - UITools.run( - event.project, "Running CodeChat Server on $port", false - ) { - while (!it.isCanceled && server.isRunning) { - Thread.sleep(1000) - } - if(it.isCanceled) { - log.info("Server cancelled") - server.stop() - } else { - log.info("Server stopped") - } - } - } finally { - log.info("Stopping Server") - server.stop() - } - }.start() - + val server = AppServer.getServer(event.project) + val uuid = UUID.randomUUID().toString() + server.addApp("/$uuid", CodeChatServer(event.project!!, language, selectedText)) Thread { Thread.sleep(500) try { - Desktop.getDesktop().browse(server.uri.resolve("/index.html")) + Desktop.getDesktop().browse(server.server.uri.resolve("/$uuid/index.html")) } catch (e: Throwable) { log.warn("Error opening browser", e) } diff --git a/src/main/kotlin/com/github/simiacryptus/aicoder/actions/dev/CodeChatServer.kt b/src/main/kotlin/com/github/simiacryptus/aicoder/actions/dev/CodeChatServer.kt new file mode 100644 index 00000000..ef6aaa0b --- /dev/null +++ b/src/main/kotlin/com/github/simiacryptus/aicoder/actions/dev/CodeChatServer.kt @@ -0,0 +1,87 @@ +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.project.Project +import com.simiacryptus.openai.OpenAIClient +import com.simiacryptus.skyenet.Heart +import com.simiacryptus.skyenet.body.* +import com.simiacryptus.skyenet.heart.WeakGroovyInterpreter +import org.eclipse.jetty.util.resource.Resource +import java.util.HashMap +import java.util.Map +import java.util.function.Supplier + +class CodeChatServer( + val project: Project, + val language: String, + val codeSelection: String, +) : SkyenetBasicChat( + applicationName = "Code Chat", + model = AppSettingsState.instance.defaultChatModel() +) { + + val rootOperationID = (0..5).map { ('a'..'z').random() }.joinToString("") + var rootMessageTrail: String = "" + + override fun newSession(sessionId: String): CodeChatSession { + val newSession = CodeChatSession(sessionId) +rootMessageTrail = +"""$rootOperationID,

Code:

${htmlEscape(codeSelection)}
""" + newSession.send(rootMessageTrail) + return newSession + } + + private fun htmlEscape(html: String): String { + return html.replace("&", "&").replace("<", "<") + .replace(">", ">").replace("\"", """) + .replace("'", "'") + } + + open inner class CodeChatSession(sessionId: String) : BasicChatSession( + parent = this@CodeChatServer, + model = model, + sessionId = sessionId + ) { + override fun run(userMessage: String) { + var messageTrail = ChatSession.divInitializer() + send("""$messageTrail
$userMessage
$spinner
""") + messages += OpenAIClient.ChatMessage(OpenAIClient.ChatMessage.Role.user, userMessage) + val response = api.chat(chatRequest, model).choices.first()?.message?.content.orEmpty() + messages += OpenAIClient.ChatMessage(OpenAIClient.ChatMessage.Role.assistant, response) + messageTrail += ChatSessionFlexmark.renderMarkdown(response) + send(messageTrail) + } + + override val messages = listOf( + OpenAIClient.ChatMessage( + OpenAIClient.ChatMessage.Role.system, """ + |You are a helpful AI that helps people with coding. + | + |You will be answering questions about the following code: + | + |```$language + |$codeSelection + |``` + | + |Responses may use markdown formatting. + """.trimMargin() + ) + ).toMutableList() + + val chatRequest: OpenAIClient.ChatRequest + get() { + val chatRequest = OpenAIClient.ChatRequest() + val model = AppSettingsState.instance.defaultChatModel() + chatRequest.model = model.modelName + chatRequest.max_tokens = model.maxTokens + chatRequest.temperature = AppSettingsState.instance.temperature + chatRequest.messages = messages.toTypedArray() + return chatRequest + } + } + + override val baseResource: Resource + get() = ClasspathResource(javaClass.classLoader.getResource(resourceBase)) + +} \ No newline at end of file diff --git a/src/main/kotlin/com/github/simiacryptus/aicoder/actions/dev/LaunchSkyenetAction.kt b/src/main/kotlin/com/github/simiacryptus/aicoder/actions/dev/LaunchSkyenetAction.kt index 675c467d..b0e547cc 100644 --- a/src/main/kotlin/com/github/simiacryptus/aicoder/actions/dev/LaunchSkyenetAction.kt +++ b/src/main/kotlin/com/github/simiacryptus/aicoder/actions/dev/LaunchSkyenetAction.kt @@ -8,17 +8,9 @@ import com.github.simiacryptus.aicoder.util.UITools import com.intellij.openapi.actionSystem.AnActionEvent import com.intellij.openapi.project.Project import com.intellij.openapi.vfs.VirtualFile -import com.simiacryptus.skyenet.Heart -import com.simiacryptus.skyenet.OutputInterceptor -import com.simiacryptus.skyenet.body.ClasspathResource -import com.simiacryptus.skyenet.body.SessionServerUtil.asJava -import com.simiacryptus.skyenet.body.SkyenetCodingSessionServer -import com.simiacryptus.skyenet.heart.WeakGroovyInterpreter import com.simiacryptus.util.describe.Description -import org.eclipse.jetty.util.resource.Resource import java.awt.Desktop -import java.util.Map -import java.util.function.Supplier +import java.util.UUID class LaunchSkyenetAction : BaseAction() { @@ -35,81 +27,15 @@ class LaunchSkyenetAction : BaseAction() { } override fun handle(e: AnActionEvent) { - // Random port from range 8000-9000 - val port = (8000 + (Math.random() * 1000).toInt()) - val skyenet = object : SkyenetCodingSessionServer( - applicationName = "IdeaAgent", - model = AppSettingsState.instance.defaultChatModel(), - apiKey = AppSettingsState.instance.apiKey - ) { - override val baseResource: Resource - get() = ClasspathResource(javaClass.classLoader.getResource(resourceBase)) - - override fun hands() = Map.of( - "ide", - object : TestTools { - override fun getProject(): Project { - return e.project!! - } - - override fun getSelectedFolder(): VirtualFile { - return UITools.getSelectedFolder(e)!! - } - - val toolStream = - OutputInterceptor.createInterceptorStream( - System.out - //PrintStream(NullOutputStream.NULL_OUTPUT_STREAM) - ) - - override fun print(msg: String) { - toolStream.println(msg) - } - } as Object, - ).asJava - - override fun toString(e: Throwable): String { - return e.message ?: e.toString() - } - - override fun heart(hands: Map): Heart = object : WeakGroovyInterpreter(hands) { - override fun wrapExecution(fn: Supplier): T? { - return UITools.run( - e.project, "Running Script", false - ) { - fn.get() - } - } - } - } - val server = skyenet.start(port) - - - Thread { - try { - UITools.run( - e.project, "Running Skyenet Server on $port", false - ) { - while (!it.isCanceled && server.isRunning) { - Thread.sleep(1000) - } - if(it.isCanceled) { - log.info("Server cancelled") - server.stop() - } else { - log.info("Server stopped") - } - } - } finally { - log.info("Stopping Server") - server.stop() - } - }.start() - + val project = e.project!! + val selectedFolder = UITools.getSelectedFolder(e)!! + val server = AppServer.getServer(e.project) + val uuid = UUID.randomUUID().toString() + server.addApp("/$uuid", SkyenetProjectCodingSessionServer(project, selectedFolder)) Thread { Thread.sleep(500) try { - Desktop.getDesktop().browse(server.uri.resolve("/index.html")) + Desktop.getDesktop().browse(server.server.uri.resolve("/$uuid/index.html")) } catch (e: Throwable) { log.warn("Error opening browser", e) } diff --git a/src/main/kotlin/com/github/simiacryptus/aicoder/actions/dev/SkyenetProjectCodingSessionServer.kt b/src/main/kotlin/com/github/simiacryptus/aicoder/actions/dev/SkyenetProjectCodingSessionServer.kt new file mode 100644 index 00000000..289c01ca --- /dev/null +++ b/src/main/kotlin/com/github/simiacryptus/aicoder/actions/dev/SkyenetProjectCodingSessionServer.kt @@ -0,0 +1,64 @@ +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.project.Project +import com.intellij.openapi.vfs.VirtualFile +import com.simiacryptus.skyenet.Heart +import com.simiacryptus.skyenet.OutputInterceptor +import com.simiacryptus.skyenet.body.ClasspathResource +import com.simiacryptus.skyenet.body.SessionServerUtil.asJava +import com.simiacryptus.skyenet.body.SkyenetCodingSessionServer +import com.simiacryptus.skyenet.heart.WeakGroovyInterpreter +import org.eclipse.jetty.util.resource.Resource +import java.util.Map +import java.util.function.Supplier + +class SkyenetProjectCodingSessionServer( + val project: Project, + val selectedFolder: VirtualFile +) : SkyenetCodingSessionServer( + applicationName = "IdeaAgent", + model = AppSettingsState.instance.defaultChatModel(), + apiKey = AppSettingsState.instance.apiKey +) { + override val baseResource: Resource + get() = ClasspathResource(javaClass.classLoader.getResource(resourceBase)) + + override fun hands() = Map.of( + "ide", + object : LaunchSkyenetAction.TestTools { + override fun getProject(): Project { + return project + } + + override fun getSelectedFolder(): VirtualFile { + return selectedFolder + } + + val toolStream = + OutputInterceptor.createInterceptorStream( + System.out + //PrintStream(NullOutputStream.NULL_OUTPUT_STREAM) + ) + + override fun print(msg: String) { + toolStream.println(msg) + } + } as Object, + ).asJava + + override fun toString(e: Throwable): String { + return e.message ?: e.toString() + } + + override fun heart(hands: Map): Heart = object : WeakGroovyInterpreter(hands) { + override fun wrapExecution(fn: Supplier): T? { + return UITools.run( + project, "Running Script", false + ) { + fn.get() + } + } + } +} \ 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 6f6ea5d9..aeb84ae6 100644 --- a/src/main/kotlin/com/github/simiacryptus/aicoder/config/ActionSettingsRegistry.kt +++ b/src/main/kotlin/com/github/simiacryptus/aicoder/config/ActionSettingsRegistry.kt @@ -11,7 +11,7 @@ import java.util.stream.Collectors class ActionSettingsRegistry { val actionSettings: MutableMap = HashMap() - val version = 1.8 + val version = 1.91 fun edit(superChildren: Array): Array { val children = superChildren.toList().toMutableList() diff --git a/src/main/kotlin/com/github/simiacryptus/aicoder/config/AppSettingsComponent.kt b/src/main/kotlin/com/github/simiacryptus/aicoder/config/AppSettingsComponent.kt index 70c777e8..f9303c71 100644 --- a/src/main/kotlin/com/github/simiacryptus/aicoder/config/AppSettingsComponent.kt +++ b/src/main/kotlin/com/github/simiacryptus/aicoder/config/AppSettingsComponent.kt @@ -32,6 +32,14 @@ class AppSettingsComponent { @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() diff --git a/src/main/kotlin/com/github/simiacryptus/aicoder/config/AppSettingsState.kt b/src/main/kotlin/com/github/simiacryptus/aicoder/config/AppSettingsState.kt index 1e9b512d..c7429e9b 100644 --- a/src/main/kotlin/com/github/simiacryptus/aicoder/config/AppSettingsState.kt +++ b/src/main/kotlin/com/github/simiacryptus/aicoder/config/AppSettingsState.kt @@ -16,6 +16,8 @@ class SimpleEnvelope(var value: String? = null) @Suppress("MemberVisibilityCanBePrivate") @State(name = "org.intellij.sdk.settings.AppSettingsState", storages = [Storage("SdkSettingsPlugin.xml")]) class AppSettingsState : PersistentStateComponent { + val listeningPort: Int = 8081 + val listeningEndpoint: String = "localhost" val modalTasks: Boolean = false var suppressErrors: Boolean = false var apiLog: Boolean = false 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 index d329fb00..3a4baf0e 100644 --- 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 @@ -3,8 +3,10 @@ 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.application.ApplicationManager import com.intellij.openapi.project.Project import com.simiacryptus.openai.proxy.ChatProxy import kotlin.Pair @@ -85,18 +87,20 @@ class InsertImplementationAction extends SelectionAction { .filter { x -> !x.isEmpty() } .reduce { a, b -> "$a $b" }.get() if(null != state.psiFile) { - def psiClassContext = PsiClassContext.getContext( - state.psiFile, - psiClassContextActionParams.selectionStart, - psiClassContextActionParams.selectionEnd, - computerLanguage - ).toString() - def code = proxy.implementCode( - specification, - psiClassContext, - computerLanguage.name(), - humanLanguage, - ).code + def code = UITools.run(state.project, "Insert Implementation", true, true, { + def psiClassContext = 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( 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 index 227e698f..1809d328 100644 --- 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 @@ -11,6 +11,7 @@ import org.jetbrains.annotations.Nullable import java.awt.Toolkit import java.awt.datatransfer.DataFlavor +import java.awt.datatransfer.Transferable class PasteAction extends SelectionAction { PasteAction() { @@ -62,6 +63,8 @@ class PasteAction extends SelectionAction { } private Object getClipboard() { - return Toolkit.getDefaultToolkit().systemClipboard.getContents(null)?.getTransferData(DataFlavor.stringFlavor) + 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 index 47bfb8f3..aa5ec213 100644 --- 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 @@ -57,7 +57,7 @@ class QuestionAction extends SelectionAction { def blockComment = state.language?.blockComment def fromString = blockComment?.fromString(answer) - return "${state.indent}${fromString.withIndent(state.indent)}\n${state.indent}" + state.selectedText + return "${state.indent}${fromString?.withIndent(state.indent) ?: ""}\n${state.indent}" + state.selectedText } @Override 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 index c21708cd..a67114a2 100644 --- 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 @@ -40,7 +40,7 @@ class ReplaceOptionsAction extends SelectionAction { } @Override - String processSelection(@Nullable AnActionEvent event, @NotNull SelectionState selectionState, @Nullable String config) { + 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() diff --git a/src/test/kotlin/com/github/simiacryptus/aicoder/ApplicationDevelopmentUITest.kt b/src/test/kotlin/com/github/simiacryptus/aicoder/ApplicationDevelopmentUITest.kt index 4c4da729..40cd1c61 100644 --- a/src/test/kotlin/com/github/simiacryptus/aicoder/ApplicationDevelopmentUITest.kt +++ b/src/test/kotlin/com/github/simiacryptus/aicoder/ApplicationDevelopmentUITest.kt @@ -1,6 +1,6 @@ package com.github.simiacryptus.aicoder -import com.github.simiacryptus.aicoder.UITestUtil.Companion.awaitProcessing +import com.github.simiacryptus.aicoder.UITestUtil.Companion.awaitBackgroundProgress import com.github.simiacryptus.aicoder.UITestUtil.Companion.canRunTests import com.github.simiacryptus.aicoder.UITestUtil.Companion.click import com.github.simiacryptus.aicoder.UITestUtil.Companion.enterLines @@ -266,7 +266,7 @@ class ApplicationDevelopmentUITest { menuAction("Insert Implementation"), outputDir, name, "${reportPrefix}menu", out ) - awaitProcessing() + awaitBackgroundProgress() //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 @@ -341,7 +341,7 @@ class ApplicationDevelopmentUITest { outputDir, name, "${reportPrefix}Rename_Variables", out ) - awaitProcessing() + awaitBackgroundProgress() sleep(1000) writeImage( screenshot("//div[@class='JDialog']"), @@ -368,7 +368,7 @@ class ApplicationDevelopmentUITest { outputDir, name, "${reportPrefix}Add_Doc_Comments", out ) - awaitProcessing() + awaitBackgroundProgress() keyboard.hotKey(KeyEvent.VK_CONTROL, KeyEvent.VK_HOME) // Move to top writeImage( screenshot("//div[@class='IdeRootPane']"), @@ -403,7 +403,7 @@ class ApplicationDevelopmentUITest { name, "${reportPrefix}Ask_Q2", out ) click("//div[@text.key='button.ok']") - awaitProcessing() + awaitBackgroundProgress() keyboard.hotKey(KeyEvent.VK_CONTROL, KeyEvent.VK_HOME) // Move to top writeImage( screenshot("//div[@class='IdeRootPane']"), @@ -427,7 +427,7 @@ class ApplicationDevelopmentUITest { outputDir, name, "${reportPrefix}Add_Code_Comments", out ) - awaitProcessing() + awaitBackgroundProgress() writeImage( screenshot("//div[@class='IdeRootPane']"), outputDir, @@ -465,4 +465,4 @@ class ApplicationDevelopmentUITest { } -} +} diff --git a/src/test/kotlin/com/github/simiacryptus/aicoder/UITest.kt b/src/test/kotlin/com/github/simiacryptus/aicoder/UITest.kt index 4ae2ab47..06ebaf2b 100644 --- a/src/test/kotlin/com/github/simiacryptus/aicoder/UITest.kt +++ b/src/test/kotlin/com/github/simiacryptus/aicoder/UITest.kt @@ -27,7 +27,7 @@ class UITest { val out = PrintWriter(FileOutputStream(testOutputFile)) out.use { out -> documentJavaImplementation("Text_to_Morse", "Convert text to Morse code", out, outputDir) - documentJavaImplementation("Prime_Numbers", "Print all prime numbers from 1 to 100", out, outputDir) + //documentJavaImplementation("Prime_Numbers", "Print all prime numbers from 1 to 100", out, outputDir) // documentJavaImplementation("Calculate_Pi", "Calculate Pi using the convergence of x = 1+sin x starting at x=3", out, buildDir) // documentJavaImplementation("Java_8", "Demonstrate language features of Java 8", out, buildDir) // documentJavaImplementation("Draw_A_Smile", "Draw a smiley face", out, buildDir) @@ -133,7 +133,7 @@ class UITest { if (!canRunTests()) return val testOutputFile = File(outputDir, "markdown.md") val out = PrintWriter(FileOutputStream(testOutputFile)) - documentMarkdownTableOps("State_Details", "Data Table of State Details", out, outputDir) + //documentMarkdownTableOps("State_Details", "Data Table of State Details", out, outputDir) documentMarkdownListAppend("Puppy_Playtime", "Top 10 Best Ways to Play with Puppies", arrayOf(), out, outputDir) out.close() // Close file } @@ -147,4 +147,4 @@ class UITest { out.close() // Close file } -} +} diff --git a/src/test/kotlin/com/github/simiacryptus/aicoder/UITestUtil.kt b/src/test/kotlin/com/github/simiacryptus/aicoder/UITestUtil.kt index 1dafa72a..700867ad 100644 --- a/src/test/kotlin/com/github/simiacryptus/aicoder/UITestUtil.kt +++ b/src/test/kotlin/com/github/simiacryptus/aicoder/UITestUtil.kt @@ -26,8 +26,8 @@ class UITestUtil { companion object { - val outputDir = File("C:\\Users\\andre\\code\\aicoder\\intellij-aicoder-docs") - val testProjectPath = File("C:\\Users\\andre\\IdeaProjects\\automated-ui-test-workspace") + val outputDir = File("C:\\Users\\andre\\code\\intellij-aicoder\\build") + val testProjectPath = File("C:\\Users\\andre\\ideTest\\TestProject") private const val testIdeUrl = "http://127.0.0.1:8082" private val robot = RemoteRobot(testIdeUrl) val keyboard = Keyboard(robot) @@ -142,13 +142,13 @@ class UITestUtil { keyboard.key(KeyEvent.VK_DELETE) enterLines(prompt) val image = menuAction("Insert Implementation") - awaitProcessing() + awaitBackgroundProgress() return image } fun menuAction(menuText: String): BufferedImage { keyboard.key(KeyEvent.VK_CONTEXT_MENU) - sleep(100) + sleep(500) val aiCoderMenuItem = getComponent("//div[@text='AI Coder']") aiCoderMenuItem.click() val point1 = aiCoderMenuItem.locationOnScreen @@ -171,34 +171,86 @@ class UITestUtil { * It will also print out the total time the process took to complete. */ fun awaitRunCompletion() { - while (!isStarted()) sleep(1) + val timeout = 1 * 60 * 1000 + val startOverall = System.currentTimeMillis() + + while (!isStarted()) { + sleep(1) + if (System.currentTimeMillis() - startOverall > timeout) { + throw RuntimeException("Timeout waiting for process to start") + } + } + val start = System.currentTimeMillis() println("Process started") + while (isStillRunning()) { sleep(100) + if (System.currentTimeMillis() - startOverall > timeout) { + throw RuntimeException("Timeout waiting for process to end") + } } + val end = System.currentTimeMillis() println("Process ended after ${(end - start) / 1000.0}") } - fun awaitProcessing() { - while (!isDialogOpen()) sleep(1) + fun awaitDialog() { + // Await a dialog - it must be open then closed + val timeout = 1 * 60 * 1000 + val startOverall = System.currentTimeMillis() + while (!isDialogOpen()) { + sleep(100) + if (System.currentTimeMillis() - startOverall > timeout) { + throw RuntimeException("Timeout waiting for dialog to open") + } + } val start = System.currentTimeMillis() println("Dialog opened") - while (isDialogOpen()) sleep(100) + while (isDialogOpen()) { + sleep(100) + if (System.currentTimeMillis() - start > timeout) { + throw RuntimeException("Timeout waiting for dialog to close") + } + } 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 awaitBackgroundProgress() { + // Await a background task - it must be open then closed + val timeout = 1 * 60 * 1000 + val startOverall = System.currentTimeMillis() + while (!isBackgroundProgressOpen()) { + sleep(100) + if (System.currentTimeMillis() - startOverall > timeout) { + throw RuntimeException("Timeout waiting for background progress to open") + } + } + val start = System.currentTimeMillis() + println("Background progress opened") + while (isBackgroundProgressOpen()) { + sleep(100) + if (System.currentTimeMillis() - start > timeout) { + throw RuntimeException("Timeout waiting for background progress to close") + } + } + val end = System.currentTimeMillis() + println("Background progress closed after ${(end - start) / 1000.0}") + } + + private fun isBackgroundProgressOpen(): Boolean { + val progressPanel = robot.findAll( + ComponentFixture::class.java, + byXpath("//div[@class='InlineProgressPanel']") + ) + if (progressPanel.isEmpty()) return false + val componentFixture = progressPanel.get(0) + val labels = componentFixture.data.getAll() + if (labels.size == 0) return false + return true + } + fun documentJavaImplementation( name: String, directive: String, @@ -210,7 +262,7 @@ class UITestUtil { out1 = out, directive = directive, extension = "java", - prompt = "public class $name {\\n // $directive", + prompt = "public class $name {\\n // $directive and write a main method to test the code", reportDir = reportDir, language = "java", selector = "static void main" @@ -230,10 +282,10 @@ class UITestUtil { val reportPrefix = "${name}_${language}_" val testOutputFile = File(outputDir, "$reportPrefix$name.md") - PrintWriter(FileOutputStream(testOutputFile)).use { out -> + PrintWriter(FileOutputStream(testOutputFile)).use { report -> out1.println("[$directive ($language)]($reportPrefix$name.md)\n\n") - out.println( + report.println( """ # $name @@ -242,11 +294,11 @@ class UITestUtil { """.trimIndent() ) - out.println("```\n$directive\n```") + report.println("```\n$directive\n```") newFile("$name.$extension") - out.println( + report.println( """ ## Implementation @@ -256,23 +308,23 @@ class UITestUtil { """.trimIndent() ) val image = implementCode(prompt) - writeImage(image, reportDir, name, "${reportPrefix}menu", out) + writeImage(image, reportDir, name, "${reportPrefix}menu", report) 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( + report.println( """ This results in the following code: ```$language""".trimIndent() ) - out.println(FileUtils.readFileToString(File(testProjectPath, "src/$name.$extension"), "UTF-8")) - out.println("```") + report.println(FileUtils.readFileToString(File(testProjectPath, "src/$name.$extension"), "UTF-8")) + report.println("```") - out.println( + report.println( """ ## Execution @@ -284,18 +336,18 @@ class UITestUtil { ) keyboard.hotKey(KeyEvent.VK_CONTROL, KeyEvent.VK_SHIFT, KeyEvent.VK_F10) // Run awaitRunCompletion() - out.println( + report.println( """ ```""".trimIndent() ) - out.println(componentText("//div[contains(@accessiblename.key, 'editor.accessible.name')]")) - out.println( + report.println(componentText("//div[contains(@accessiblename.key, 'editor.accessible.name')]")) + report.println( """ ``` """.trimIndent() ) - writeImage(screenshot("//div[@class='IdeRootPane']"), reportDir, name, "${reportPrefix}result", out) + writeImage(screenshot("//div[@class='IdeRootPane']"), reportDir, name, "${reportPrefix}result", report) // Close run tab sleep(100) clickr("//div[@class='ContentTabLabel']") @@ -304,7 +356,7 @@ class UITestUtil { sleep(100) - out.println( + report.println( """ ## Rename Variables @@ -316,20 +368,20 @@ class UITestUtil { 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() + writeImage(menuAction("Rename Variables"), reportDir, name, "${reportPrefix}Rename_Variables", report) + awaitBackgroundProgress() sleep(1000) writeImage( screenshot("//div[@class='JDialog']"), reportDir, name, "${reportPrefix}Rename_Variables_Dialog", - out + report ) click("//div[@text.key='button.ok']") - out.println( + report.println( """ ## Documentation Comments @@ -339,19 +391,19 @@ class UITestUtil { """.trimIndent() ) selectText(getEditor(), selector) - writeImage(menuAction("Doc Comments"), reportDir, name, "${reportPrefix}Add_Doc_Comments", out) - awaitProcessing() + writeImage(menuAction("Doc Comments"), reportDir, name, "${reportPrefix}Add_Doc_Comments", report) + awaitBackgroundProgress() keyboard.hotKey(KeyEvent.VK_CONTROL, KeyEvent.VK_HOME) // Move to top writeImage( screenshot("//div[@class='IdeRootPane']"), reportDir, name, "${reportPrefix}Add_Doc_Comments2", - out + report ) - out.println( + report.println( """ ## Ad-Hoc Questions @@ -362,16 +414,16 @@ class UITestUtil { ) selectText(getEditor(), selector) keyboard.hotKey(KeyEvent.VK_CONTROL, KeyEvent.VK_W) // Select function - writeImage(menuAction("Ask a question"), reportDir, name, "${reportPrefix}Ask_Q", out) + writeImage(menuAction("Ask a question"), reportDir, name, "${reportPrefix}Ask_Q", report) 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) + writeImage(screenshot("//div[@class='JDialog']"), reportDir, name, "${reportPrefix}Ask_Q2", report) click("//div[@text.key='button.ok']") - awaitProcessing() + awaitBackgroundProgress() keyboard.hotKey(KeyEvent.VK_CONTROL, KeyEvent.VK_HOME) // Move to top - writeImage(screenshot("//div[@class='IdeRootPane']"), reportDir, name, "${reportPrefix}Ask_Q3", out) + writeImage(screenshot("//div[@class='IdeRootPane']"), reportDir, name, "${reportPrefix}Ask_Q3", report) - out.println( + report.println( """ ## Code Comments @@ -382,24 +434,24 @@ class UITestUtil { ) 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(menuAction("Code Comments"), reportDir, name, "${reportPrefix}Add_Code_Comments", report) + awaitBackgroundProgress() writeImage( screenshot("//div[@class='IdeRootPane']"), reportDir, name, "${reportPrefix}Add_Code_Comments2", - out + report ) keyboard.hotKey(KeyEvent.VK_CONTROL, KeyEvent.VK_ALT, KeyEvent.VK_L) // Reformat keyboard.hotKey(KeyEvent.VK_CONTROL, KeyEvent.VK_S) // Save - out.println( + report.println( """ ```$language""".trimIndent() ) - out.println(FileUtils.readFileToString(File(testProjectPath, "src/$name.$extension"), "UTF-8")) - out.println( + report.println(FileUtils.readFileToString(File(testProjectPath, "src/$name.$extension"), "UTF-8")) + report.println( """ ``` @@ -407,7 +459,7 @@ class UITestUtil { ) - out.println( + report.println( """ ## Conversion to other languages @@ -426,26 +478,26 @@ class UITestUtil { reportDir, name, "${reportPrefix}Convert_to_js", - out + report ) keyboard.hotKey(KeyEvent.VK_ENTER) while (!File(testProjectPath, "src/$name.js").exists()) { sleep(1000) } - out.println( + report.println( """ ```js""".trimIndent() ) - out.println(FileUtils.readFileToString(File(testProjectPath, "src/$name.js"), "UTF-8")) - out.println( + report.println(FileUtils.readFileToString(File(testProjectPath, "src/$name.js"), "UTF-8")) + report.println( """ ``` """.trimIndent() ) - out.println( + report.println( """ ### Conversion to Scala @@ -462,18 +514,18 @@ class UITestUtil { reportDir, name, "${reportPrefix}Convert_to_scala", - out + report ) keyboard.hotKey(KeyEvent.VK_ENTER) while (!File(testProjectPath, "src/$name.scala").exists()) { sleep(1000) } - out.println( + report.println( """ ```scala""".trimIndent() ) - out.println(FileUtils.readFileToString(File(testProjectPath, "src/$name.scala"), "UTF-8")) - out.println( + report.println(FileUtils.readFileToString(File(testProjectPath, "src/$name.scala"), "UTF-8")) + report.println( """ ``` """.trimIndent() @@ -487,12 +539,12 @@ class UITestUtil { fun documentTextAppend( name: String, directive: String, - out: PrintWriter, + report: PrintWriter, file: File ) { val reportPrefix = "${name}_" - out.println("") - out.println( + report.println("") + report.println( """ # $name @@ -512,19 +564,19 @@ class UITestUtil { enterLines(directive) keyboard.selectAll() val image = menuAction("Append Text") - awaitProcessing() - writeImage(image, file, name, "${reportPrefix}menu", out) + awaitBackgroundProgress() + writeImage(image, file, name, "${reportPrefix}menu", report) keyboard.hotKey(KeyEvent.VK_CONTROL, KeyEvent.VK_S) // Save - out.println( + report.println( """ This generates the following text: ```""".trimIndent() ) - out.println(FileUtils.readFileToString(File(testProjectPath, "src/$name.txt"), "UTF-8")) - out.println( + report.println(FileUtils.readFileToString(File(testProjectPath, "src/$name.txt"), "UTF-8")) + report.println( """ ``` @@ -532,7 +584,7 @@ class UITestUtil { ) - out.println( + report.println( """ ## Edit Text @@ -543,16 +595,16 @@ class UITestUtil { ) click("//div[@class='EditorComponentImpl']") keyboard.hotKey(KeyEvent.VK_CONTROL, KeyEvent.VK_A) // Select all - writeImage(menuAction("Edit Text"), file, name, "${reportPrefix}edit_text", out) + writeImage(menuAction("Edit Code"), file, name, "${reportPrefix}edit_text", report) click("//div[@class='MultiplexingTextField']") keyboard.enterText("Translate into a series of haikus") - writeImage(screenshot("//div[@class='JDialog']"), file, name, "${reportPrefix}edit_text_2", out) + writeImage(screenshot("//div[@class='JDialog']"), file, name, "${reportPrefix}edit_text_2", report) keyboard.enter() - awaitProcessing() - writeImage(screenshot("//div[@class='IdeRootPane']"), file, name, "${reportPrefix}result1", out) + awaitBackgroundProgress() + writeImage(screenshot("//div[@class='IdeRootPane']"), file, name, "${reportPrefix}result1", report) - out.println( + report.println( """ We can also replace text using the "Replace Options" command. @@ -567,26 +619,26 @@ class UITestUtil { 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() + writeImage(menuAction("Replace Options"), file, name, "${reportPrefix}replace_options", report) + awaitBackgroundProgress() sleep(500) - writeImage(screenshot("//div[@class='JDialog']"), file, name, "${reportPrefix}replace_options_2", out) + writeImage(screenshot("//div[@class='JDialog']"), file, name, "${reportPrefix}replace_options_2", report) sleep(500) keyboard.enter() - writeImage(screenshot("//div[@class='IdeRootPane']"), file, name, "${reportPrefix}result2", out) + writeImage(screenshot("//div[@class='IdeRootPane']"), file, name, "${reportPrefix}result2", report) keyboard.hotKey(KeyEvent.VK_CONTROL, KeyEvent.VK_S) // Save documentText = FileUtils.readFileToString(File(testProjectPath, "src/$name.txt"), "UTF-8") - out.println( + report.println( """ Our text now looks like this: ```""".trimIndent() ) - out.println(documentText) - out.println( + report.println(documentText) + report.println( """ ``` @@ -619,6 +671,9 @@ class UITestUtil { ``` $directive + Rows 1-5 + + | ``` """.trimIndent() @@ -631,12 +686,13 @@ class UITestUtil { enterLines( """ $directive + Rows 1-5 |""".trimIndent() ) keyboard.selectAll() writeImage(menuAction("Append Text"), file, name, "${reportPrefix}menu", out) - awaitProcessing() + awaitBackgroundProgress() keyboard.hotKey(KeyEvent.VK_CONTROL, KeyEvent.VK_S) // Save out.println( @@ -656,7 +712,7 @@ class UITestUtil { keyboard.hotKey(KeyEvent.VK_CONTROL, KeyEvent.VK_END) writeImage(menuAction("Add Table Columns"), file, name, "${reportPrefix}add_columns", out) - awaitProcessing() + awaitBackgroundProgress() keyboard.hotKey(KeyEvent.VK_CONTROL, KeyEvent.VK_S) // Save out.println( @@ -674,7 +730,7 @@ class UITestUtil { keyboard.hotKey(KeyEvent.VK_CONTROL, KeyEvent.VK_Z) // Undo writeImage(menuAction("Add Table Rows"), file, name, "${reportPrefix}add_rows", out) - awaitProcessing() + awaitBackgroundProgress() keyboard.hotKey(KeyEvent.VK_CONTROL, KeyEvent.VK_S) // Save out.println( @@ -732,7 +788,7 @@ class UITestUtil { } keyboard.selectAll() writeImage(menuAction("Append Text"), file, name, "${reportPrefix}append_text", out) - awaitProcessing() + awaitBackgroundProgress() keyboard.hotKey(KeyEvent.VK_CONTROL, KeyEvent.VK_S) // Save out.println( @@ -752,7 +808,7 @@ class UITestUtil { writeImage(screenshot("//div[@class='IdeRootPane']"), file, name, "${reportPrefix}result", out) writeImage(menuAction("Add List Items"), file, name, "${reportPrefix}add_list_items", out) - awaitProcessing() + awaitBackgroundProgress() writeImage(screenshot("//div[@class='IdeRootPane']"), file, name, "${reportPrefix}result2", out) keyboard.hotKey(KeyEvent.VK_CONTROL, KeyEvent.VK_S) // Save