From 52baceff539942c471af72a6138a1d2adf5e2039 Mon Sep 17 00:00:00 2001 From: Andrew Charneski Date: Sun, 25 Feb 2024 12:06:31 -0500 Subject: [PATCH] 1.2.26 --- build.gradle.kts | 2 + gradle.properties | 2 +- settings.gradle.kts | 2 +- .../aicoder/actions/BaseAction.kt | 51 ++++--- .../aicoder/actions/FileContextAction.kt | 38 ++++- .../actions/generic/AnalogueFileAction.kt | 89 +++++++++--- .../generic/DocumentationCompilerAction.kt | 133 ++++++++++++++++++ .../aicoder/config/ActionSettingsRegistry.kt | 2 +- src/main/resources/META-INF/plugin.xml | 13 +- 9 files changed, 280 insertions(+), 52 deletions(-) create mode 100644 src/main/kotlin/com/github/simiacryptus/aicoder/actions/generic/DocumentationCompilerAction.kt diff --git a/build.gradle.kts b/build.gradle.kts index 00f01bac..3ec16c6f 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -30,6 +30,8 @@ val skyenet_version = "1.0.47" val remoterobot_version = "0.11.21" dependencies { + implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.8.0") + implementation(group = "com.simiacryptus.skyenet", name = "kotlin", version = skyenet_version) { exclude(group = "org.jetbrains.kotlin", module = "") diff --git a/gradle.properties b/gradle.properties index 70b9a585..ee75f0a8 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,6 +1,6 @@ pluginName=intellij-aicoder pluginRepositoryUrl=https://github.com/SimiaCryptus/intellij-aicoder -pluginVersion=1.2.25 +pluginVersion=1.2.26 jvmArgs=-Xmx8g org.gradle.jvmargs=-Xmx8g diff --git a/settings.gradle.kts b/settings.gradle.kts index 90c97875..deef8b8f 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -5,4 +5,4 @@ plugins { } //includeBuild("../jo-penai/") -includeBuild("../SkyeNet/") +//includeBuild("../SkyeNet/") diff --git a/src/main/kotlin/com/github/simiacryptus/aicoder/actions/BaseAction.kt b/src/main/kotlin/com/github/simiacryptus/aicoder/actions/BaseAction.kt index ba633684..bb96c680 100644 --- a/src/main/kotlin/com/github/simiacryptus/aicoder/actions/BaseAction.kt +++ b/src/main/kotlin/com/github/simiacryptus/aicoder/actions/BaseAction.kt @@ -9,36 +9,43 @@ import org.slf4j.LoggerFactory import javax.swing.Icon abstract class BaseAction( - name: String? = null, - description: String? = null, - icon: Icon? = null, + name: String? = null, + description: String? = null, + icon: Icon? = null, ) : AnAction(name, description, icon) { - private val log by lazy { LoggerFactory.getLogger(javaClass) } - //override fun getActionUpdateThread(): ActionUpdateThread = ActionUpdateThread.BGT + private val log by lazy { LoggerFactory.getLogger(javaClass) } + //override fun getActionUpdateThread(): ActionUpdateThread = ActionUpdateThread.BGT - val api: OpenAIClient - get() = IdeaOpenAIClient.instance + val api: OpenAIClient + get() = IdeaOpenAIClient.instance - final override fun update(event: AnActionEvent) { - event.presentation.isEnabledAndVisible = isEnabled(event) - super.update(event) - } + final override fun update(event: AnActionEvent) { + event.presentation.isEnabledAndVisible = isEnabled(event) + super.update(event) + } - abstract fun handle(e: AnActionEvent) + abstract fun handle(e: AnActionEvent) - final override fun actionPerformed(e: AnActionEvent) { - UITools.logAction(""" + final override fun actionPerformed(e: AnActionEvent) { + UITools.logAction( + """ |Action: ${javaClass.simpleName} - """.trimMargin().trim()) - IdeaOpenAIClient.lastEvent = e - try { - handle(e) - } catch (e: Throwable) { - UITools.error(log, "Error in Action ${javaClass.simpleName}", e) - } + """.trimMargin().trim() + ) + IdeaOpenAIClient.lastEvent = e + try { + handle(e) + } catch (e: Throwable) { + UITools.error(log, "Error in Action ${javaClass.simpleName}", e) } + } + + open fun isEnabled(event: AnActionEvent): Boolean = true - open fun isEnabled(event: AnActionEvent): Boolean = true + companion object { + val log by lazy { LoggerFactory.getLogger(javaClass) } + val scheduledPool = java.util.concurrent.Executors.newScheduledThreadPool(1) + } } diff --git a/src/main/kotlin/com/github/simiacryptus/aicoder/actions/FileContextAction.kt b/src/main/kotlin/com/github/simiacryptus/aicoder/actions/FileContextAction.kt index d2202ba5..7e9475df 100644 --- a/src/main/kotlin/com/github/simiacryptus/aicoder/actions/FileContextAction.kt +++ b/src/main/kotlin/com/github/simiacryptus/aicoder/actions/FileContextAction.kt @@ -3,12 +3,15 @@ package com.github.simiacryptus.aicoder.actions import com.github.simiacryptus.aicoder.config.AppSettingsState import com.github.simiacryptus.aicoder.util.UITools import com.intellij.openapi.actionSystem.AnActionEvent +import com.intellij.openapi.application.ApplicationManager import com.intellij.openapi.fileEditor.FileEditorManager import com.intellij.openapi.project.Project import com.intellij.openapi.vfs.LocalFileSystem import com.intellij.openapi.vfs.VirtualFile import org.slf4j.LoggerFactory import java.io.File +import java.nio.file.Path +import java.util.concurrent.TimeUnit abstract class FileContextAction( private val supportsFiles: Boolean = true, @@ -42,18 +45,16 @@ abstract class FileContextAction( if (it.isCanceled) throw InterruptedException() } UITools.writeableFn(e) { - val files = newFiles.map { file -> + val files = newFiles.mapNotNull { file -> val localFileSystem = LocalFileSystem.getInstance() - localFileSystem.findFileByIoFile(file.parentFile)?.refresh(false, true) val generatedFile = localFileSystem.findFileByIoFile(file) if (generatedFile == null) { log.warn("Generated file not found: ${file.path}") } else { - generatedFile.refresh(false, false) - FileEditorManager.getInstance(project).openFile(generatedFile, true) + open(project, file.toPath()) } generatedFile - }.filter { it != null }.toTypedArray() + }.toTypedArray() Runnable { files.forEach { it?.delete(this@FileContextAction) } } @@ -78,6 +79,33 @@ abstract class FileContextAction( companion object { private val log = LoggerFactory.getLogger(FileContextAction::class.java) + + fun open(project: Project, outputPath: Path) { + lateinit var function: () -> Unit + function = { + val file = outputPath.toFile() + if (file.exists()) { + // Ensure the IDE is ready for file operations + ApplicationManager.getApplication().invokeLater { + val ioFile = LocalFileSystem.getInstance().refreshAndFindFileByIoFile(file) + if (false == (ioFile?.let { FileEditorManager.getInstance(project).isFileOpen(it) })) { + val localFileSystem = LocalFileSystem.getInstance() + // Refresh the file system to ensure the file is visible + val virtualFile = localFileSystem.refreshAndFindFileByIoFile(file) + virtualFile?.let { + FileEditorManager.getInstance(project).openFile(it, true) + } ?: scheduledPool.schedule(function, 100, TimeUnit.MILLISECONDS) + } else { + scheduledPool.schedule(function, 100, TimeUnit.MILLISECONDS) + } + } + } else { + scheduledPool.schedule(function, 100, TimeUnit.MILLISECONDS) + } + } + scheduledPool.schedule(function, 100, TimeUnit.MILLISECONDS) + } + } } \ No newline at end of file diff --git a/src/main/kotlin/com/github/simiacryptus/aicoder/actions/generic/AnalogueFileAction.kt b/src/main/kotlin/com/github/simiacryptus/aicoder/actions/generic/AnalogueFileAction.kt index 3a2b8032..268d9612 100644 --- a/src/main/kotlin/com/github/simiacryptus/aicoder/actions/generic/AnalogueFileAction.kt +++ b/src/main/kotlin/com/github/simiacryptus/aicoder/actions/generic/AnalogueFileAction.kt @@ -4,7 +4,10 @@ import com.github.simiacryptus.aicoder.actions.FileContextAction import com.github.simiacryptus.aicoder.config.AppSettingsState import com.github.simiacryptus.aicoder.config.Name import com.github.simiacryptus.aicoder.util.UITools +import com.intellij.openapi.application.ApplicationManager +import com.intellij.openapi.fileEditor.FileEditorManager import com.intellij.openapi.project.Project +import com.intellij.openapi.vfs.LocalFileSystem import com.simiacryptus.jopenai.ApiModel import com.simiacryptus.jopenai.ApiModel.ChatMessage import com.simiacryptus.jopenai.ApiModel.Role @@ -13,6 +16,8 @@ import org.apache.commons.io.FileUtils import org.apache.commons.io.IOUtils import java.io.File import java.io.FileInputStream +import java.nio.file.Path +import java.util.concurrent.TimeUnit import javax.swing.JTextArea class AnalogueFileAction : FileContextAction() { @@ -33,39 +38,48 @@ class AnalogueFileAction : FileContextAction() { ) } - class Settings ( - var directive: String = "" + class UserSettings( + var directive: String = "", ) - override fun getConfig(project: Project?): Settings? { - return UITools.showDialog( - project, - SettingsUI::class.java, - Settings::class.java, - "Create Analogue File" + class Settings( + val settings: UserSettings? = null, + val project: Project? = null + ) + + override fun getConfig(project: Project?): Settings { + return Settings( + UITools.showDialog( + project, + SettingsUI::class.java, + UserSettings::class.java, + "Create Analogue File" + ), project ) } override fun processSelection(state: SelectionState, config: Settings?): Array { + val root = getModuleRootForFile(state.selectedFile).toPath() + val selectedFile = state.selectedFile val analogue = generateFile( ProjectFile( - path = state.projectRoot.toPath().relativize(state.selectedFile.toPath()).toString(), - code = IOUtils.toString(FileInputStream(state.selectedFile), "UTF-8") + path = root.relativize(selectedFile.toPath()).toString(), + code = IOUtils.toString(FileInputStream(selectedFile), "UTF-8") ), - config?.directive ?: "" + config?.settings?.directive ?: "" ) - var outputPath = state.projectRoot.toPath().resolve(analogue.path) + var outputPath = root.resolve(analogue.path) if (outputPath.toFile().exists()) { val extension = outputPath.toString().split(".").last() val name = outputPath.toString().split(".").dropLast(1).joinToString(".") val fileIndex = (1..Int.MAX_VALUE).find { - !File(state.projectRoot, "$name.$it.$extension").exists() + !root.resolve("$name.$it.$extension").toFile().exists() } - outputPath = state.projectRoot.toPath().resolve("$name.$fileIndex.$extension") + outputPath = root.resolve("$name.$fileIndex.$extension") } outputPath.parent.toFile().mkdirs() FileUtils.write(outputPath.toFile(), analogue.code, "UTF-8") - Thread.sleep(100) + open(config?.project!!, outputPath) return arrayOf(outputPath.toFile()) } @@ -95,13 +109,9 @@ class AnalogueFileAction : FileContextAction() { ``` """.trimIndent().toContentList(), null ) - ) ) - val response = api.chat( - chatRequest, - AppSettingsState.instance.defaultChatModel() - ).choices.first().message?.content?.trim() + val response = api.chat(chatRequest, model).choices.first().message?.content?.trim() var outputPath = baseFile.path val header = response?.split("\n")?.first() var body = response?.split("\n")?.drop(1)?.joinToString("\n")?.trim() @@ -118,4 +128,43 @@ class AnalogueFileAction : FileContextAction() { code = body ?: "" ) } + + companion object { + fun open(project: Project, outputPath: Path) { + lateinit var function: () -> Unit + function = { + val file = outputPath.toFile() + if (file.exists()) { + // Ensure the IDE is ready for file operations + ApplicationManager.getApplication().invokeLater { + val ioFile = LocalFileSystem.getInstance().refreshAndFindFileByIoFile(file) + if (false == (ioFile?.let { FileEditorManager.getInstance(project).isFileOpen(it) })) { + val localFileSystem = LocalFileSystem.getInstance() + // Refresh the file system to ensure the file is visible + val virtualFile = localFileSystem.refreshAndFindFileByIoFile(file) + virtualFile?.let { + FileEditorManager.getInstance(project).openFile(it, true) + } ?: scheduledPool.schedule(function, 100, TimeUnit.MILLISECONDS) + } else { + scheduledPool.schedule(function, 100, TimeUnit.MILLISECONDS) + } + } + } else { + scheduledPool.schedule(function, 100, TimeUnit.MILLISECONDS) + } + } + scheduledPool.schedule(function, 100, TimeUnit.MILLISECONDS) + } + + fun getModuleRootForFile(file: File): File { + var current = file + while (current.parentFile != null) { + if (current.resolve(".git").exists()) { + return current + } + current = current.parentFile + } + return file + } + } } diff --git a/src/main/kotlin/com/github/simiacryptus/aicoder/actions/generic/DocumentationCompilerAction.kt b/src/main/kotlin/com/github/simiacryptus/aicoder/actions/generic/DocumentationCompilerAction.kt new file mode 100644 index 00000000..92afe676 --- /dev/null +++ b/src/main/kotlin/com/github/simiacryptus/aicoder/actions/generic/DocumentationCompilerAction.kt @@ -0,0 +1,133 @@ +package com.github.simiacryptus.aicoder.actions.generic + +import com.github.simiacryptus.aicoder.actions.FileContextAction +import com.github.simiacryptus.aicoder.config.AppSettingsState +import com.github.simiacryptus.aicoder.config.Name +import com.github.simiacryptus.aicoder.util.UITools +import com.intellij.openapi.application.ApplicationManager +import com.intellij.openapi.fileEditor.FileEditorManager +import com.intellij.openapi.project.Project +import com.intellij.openapi.vfs.LocalFileSystem +import com.simiacryptus.jopenai.ApiModel +import com.simiacryptus.jopenai.util.ClientUtil.toContentList +import org.apache.commons.io.IOUtils +import java.io.File +import java.io.FileInputStream +import java.nio.file.Files +import java.nio.file.Path +import java.util.concurrent.Executors +import java.util.concurrent.Future +import java.util.concurrent.TimeUnit +import javax.swing.JTextArea + +class DocumentationCompilerAction : FileContextAction() { + + class SettingsUI { + @Name("Transformation Message") + var transformationMessage: JTextArea = JTextArea( + "Create user documentation", + 3, + 120 + ) + } + + class UserSettings( + var transformationMessage: String = "Create user documentation", + ) + + class Settings( + val settings: UserSettings? = null, + val project: Project? = null + ) + + override fun getConfig(project: Project?) = Settings( + UITools.showDialog( + project, + SettingsUI::class.java, + UserSettings::class.java, + "Compile Documentation" + ), project + ) + + override fun processSelection(state: SelectionState, config: Settings?): Array { + val root = state.selectedFile.toPath() + var outputPath = root.resolve("compiled_documentation.md") + if (outputPath.toFile().exists()) { + val extension = outputPath.toString().split(".").last() + val name = outputPath.toString().split(".").dropLast(1).joinToString(".") + val fileIndex = (1..Int.MAX_VALUE).find { + !root.resolve("$name.$it.$extension").toFile().exists() + } + outputPath = root.resolve("$name.$fileIndex.$extension") + } + val executorService = Executors.newFixedThreadPool(4) + outputPath.parent.toFile().mkdirs() + val transformationMessage = config?.settings?.transformationMessage ?: "Create user documentation" + val markdownContent = StringBuilder() + try { + val pathList = Files.walk(root) + .filter { Files.isRegularFile(it) && !Files.isDirectory(it) } + .toList().filterNotNull() + .map> { path -> + executorService.submit { + val fileContent = IOUtils.toString(FileInputStream(path.toFile()), "UTF-8") ?: return@submit null + val transformContent = transformContent(fileContent, transformationMessage) + markdownContent.append("# ${root.relativize(path)}\n\n") + markdownContent.append(transformContent.replace("(?s)\n#".toRegex(), "\n##")) + markdownContent.append("\n\n") + path + } + }.toTypedArray().map { future -> future.get() ?: return@map null }.filterNotNull() + Files.write(outputPath, markdownContent.toString().toByteArray()) + open(config?.project!!, outputPath) + return arrayOf(outputPath.toFile()) + } finally { + executorService.shutdown() + } + } + + private fun transformContent(fileContent: String, transformationMessage: String) = api.chat( + ApiModel.ChatRequest( + model = AppSettingsState.instance.defaultChatModel().modelName, + temperature = AppSettingsState.instance.temperature, + messages = listOf( + ApiModel.ChatMessage( + ApiModel.Role.system, """ + You will combine natural language instructions with a user provided code example to document code. + """.trimIndent().toContentList(), null + ), + ApiModel.ChatMessage(ApiModel.Role.user, fileContent.toContentList()), + ApiModel.ChatMessage(ApiModel.Role.user, transformationMessage.toContentList()), + ), + ), + AppSettingsState.instance.defaultChatModel() + ).choices.first().message?.content?.trim() ?: fileContent + + companion object { + fun open(project: Project, outputPath: Path) { + lateinit var function: () -> Unit + function = { + val file = outputPath.toFile() + if (file.exists()) { + // Ensure the IDE is ready for file operations + ApplicationManager.getApplication().invokeLater { + val ioFile = LocalFileSystem.getInstance().refreshAndFindFileByIoFile(file) + if (false == (ioFile?.let { FileEditorManager.getInstance(project).isFileOpen(it) })) { + val localFileSystem = LocalFileSystem.getInstance() + // Refresh the file system to ensure the file is visible + val virtualFile = localFileSystem.refreshAndFindFileByIoFile(file) + virtualFile?.let { + FileEditorManager.getInstance(project).openFile(it, true) + } ?: scheduledPool.schedule(function, 100, TimeUnit.MILLISECONDS) + } else { + scheduledPool.schedule(function, 100, TimeUnit.MILLISECONDS) + } + } + } else { + scheduledPool.schedule(function, 100, TimeUnit.MILLISECONDS) + } + } + scheduledPool.schedule(function, 100, TimeUnit.MILLISECONDS) + } + } +} 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 8fe662da..f10a8f2d 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() - private val version = 2.0026 + private val version = 2.0040 fun edit(superChildren: Array): Array { val children = superChildren.toList().toMutableList() diff --git a/src/main/resources/META-INF/plugin.xml b/src/main/resources/META-INF/plugin.xml index fc7e2571..09b9c295 100644 --- a/src/main/resources/META-INF/plugin.xml +++ b/src/main/resources/META-INF/plugin.xml @@ -81,7 +81,7 @@ text="_Chat Append Text" description="Append text using the Chat API"> @@ -91,7 +91,7 @@ text="_Create File" description="Create file"> @@ -105,6 +105,15 @@ --> + + + +