diff --git a/CHANGELOG.md b/CHANGELOG.md index 1201eeb9..e3a0bce5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,15 @@ ## [Unreleased] +## [1.2.23] + +### Improved + +- Fixed Kotlin dynamic evaluation +- Replaced all Groovy with Kotlin +- Revised configuration UI +- Various fixes + ## [1.2.22] ### Improved diff --git a/build.gradle.kts b/build.gradle.kts index 2430912b..0c9e84aa 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -54,12 +54,15 @@ dependencies { implementation(group = "org.eclipse.jetty.websocket", name = "websocket-servlet", version = jetty_version) implementation(group = "org.slf4j", name = "slf4j-api", version = slf4j_version) - testImplementation(group = "org.slf4j", name = "slf4j-simple", version = slf4j_version) + testImplementation(group = "org.slf4j", name = "slf4j-simple", version = slf4j_version) testImplementation(group = "com.intellij.remoterobot", name = "remote-robot", version = "0.11.16") testImplementation(group = "com.intellij.remoterobot", name = "remote-fixtures", version = "0.11.16") testImplementation(group = "com.squareup.okhttp3", name = "okhttp", version = "3.14.9") + testImplementation(group = "org.junit.jupiter", name = "junit-jupiter-api", version = "5.10.1") + testRuntimeOnly(group = "org.junit.jupiter", name = "junit-jupiter-engine", version = "5.10.1") + } 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 5b51e94a..f554704b 100644 --- a/src/main/kotlin/com/github/simiacryptus/aicoder/actions/SelectionAction.kt +++ b/src/main/kotlin/com/github/simiacryptus/aicoder/actions/SelectionAction.kt @@ -53,9 +53,10 @@ abstract class SelectionAction( var selectedText = primaryCaret.selectedText val editorState = editorState(editor) val (start, end) = retarget(editorState, selectedText, selectionStart, selectionEnd) ?: return - selectedText = editorState.text.substring(start, end) - selectionEnd = end - selectionStart = start + val text = editorState.text + selectedText = text.substring(start.coerceIn(0, text.length), end.coerceIn(0, text.length)) + selectionEnd = end.coerceIn(0, text.length) + selectionStart = start.coerceIn(0, text.length) UITools.redoableTask(e) { val document = e.getData(CommonDataKeys.EDITOR)?.document diff --git a/src/main/kotlin/com/github/simiacryptus/aicoder/actions/code/PasteAction.kt b/src/main/kotlin/com/github/simiacryptus/aicoder/actions/code/PasteAction.kt index 4ba3108b..1c06ceb1 100644 --- a/src/main/kotlin/com/github/simiacryptus/aicoder/actions/code/PasteAction.kt +++ b/src/main/kotlin/com/github/simiacryptus/aicoder/actions/code/PasteAction.kt @@ -9,7 +9,7 @@ import com.simiacryptus.jopenai.proxy.ChatProxy import java.awt.Toolkit import java.awt.datatransfer.DataFlavor -class PasteAction : SelectionAction(false) { +open class PasteAction : SelectionAction(false) { interface VirtualAPI { fun convert(text: String, from_language: String, to_language: String): ConvertedText 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 e393e513..db2f638d 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 @@ -8,7 +8,7 @@ import com.intellij.openapi.actionSystem.AnActionEvent import com.intellij.openapi.project.Project import com.simiacryptus.jopenai.proxy.ChatProxy -class RenameVariablesAction : SelectionAction() { +open class RenameVariablesAction : SelectionAction() { interface RenameAPI { fun suggestRenames( @@ -64,7 +64,7 @@ class RenameVariablesAction : SelectionAction() { } } - private fun choose(renameSuggestions: Map): Set { + open fun choose(renameSuggestions: Map): Set { return UITools.showCheckboxDialog( "Select which items to rename", renameSuggestions.keys.toTypedArray(), diff --git a/src/main/kotlin/com/github/simiacryptus/aicoder/actions/generic/AppendAction.kt b/src/main/kotlin/com/github/simiacryptus/aicoder/actions/generic/AppendAction.kt index 7899c1fd..2f25b9a5 100644 --- a/src/main/kotlin/com/github/simiacryptus/aicoder/actions/generic/AppendAction.kt +++ b/src/main/kotlin/com/github/simiacryptus/aicoder/actions/generic/AppendAction.kt @@ -3,8 +3,7 @@ import com.github.simiacryptus.aicoder.actions.SelectionAction import com.github.simiacryptus.aicoder.config.AppSettingsState import com.intellij.openapi.project.Project -import com.simiacryptus.jopenai.ApiModel.ChatMessage -import com.simiacryptus.jopenai.ApiModel.Role +import com.simiacryptus.jopenai.ApiModel.* import com.simiacryptus.jopenai.ClientUtil.toContentList class AppendAction : SelectionAction() { @@ -13,17 +12,20 @@ import com.simiacryptus.jopenai.ClientUtil.toContentList } override fun processSelection(state: SelectionState, config: String?): String { - val settings = AppSettingsState.instance - val request = settings.createChatRequest().copy( - temperature = settings.temperature, - messages = listOf( - ChatMessage(Role.system, "Append text to the end of the user's prompt".toContentList(), null), - ChatMessage(Role.user, state.selectedText.toString().toContentList(), null) - ), - ) - val chatResponse = api.chat(request, settings.defaultChatModel()) - val b4 = state.selectedText ?: "" - val str = chatResponse.choices[0].message?.content ?: "" - return b4 + if (str.startsWith(b4)) str.substring(b4.length) else str + val settings = AppSettingsState.instance + val request = ChatRequest( + model = settings.defaultChatModel().modelName, + temperature = settings.temperature + ).copy( + temperature = settings.temperature, + messages = listOf( + ChatMessage(Role.system, "Append text to the end of the user's prompt".toContentList(), null), + ChatMessage(Role.user, state.selectedText.toString().toContentList(), null) + ), + ) + val chatResponse = api.chat(request, settings.defaultChatModel()) + val b4 = state.selectedText ?: "" + val str = chatResponse.choices[0].message?.content ?: "" + return b4 + if (str.startsWith(b4)) str.substring(b4.length) else str } } \ No newline at end of file diff --git a/src/main/kotlin/com/github/simiacryptus/aicoder/actions/generic/ReplaceOptionsAction.kt b/src/main/kotlin/com/github/simiacryptus/aicoder/actions/generic/ReplaceOptionsAction.kt index eaa4a8f4..c60a13d2 100644 --- a/src/main/kotlin/com/github/simiacryptus/aicoder/actions/generic/ReplaceOptionsAction.kt +++ b/src/main/kotlin/com/github/simiacryptus/aicoder/actions/generic/ReplaceOptionsAction.kt @@ -11,7 +11,7 @@ import kotlin.math.ceil import kotlin.math.ln import kotlin.math.pow -class ReplaceOptionsAction : SelectionAction() { +open class ReplaceOptionsAction : SelectionAction() { interface VirtualAPI { fun suggestText(template: String, examples: List): Suggestions @@ -52,7 +52,7 @@ class ReplaceOptionsAction : SelectionAction() { return choose(choices ?: listOf()) } - private fun choose(choices: List): String { + open fun choose(choices: List): String { return UITools.showRadioButtonDialog("Select an option to fill in the blank:", *choices.toTypedArray())?.toString() ?: "" } } \ No newline at end of file diff --git a/src/main/kotlin/com/github/simiacryptus/aicoder/actions/markdown/MarkdownImplementActionGroup.kt b/src/main/kotlin/com/github/simiacryptus/aicoder/actions/markdown/MarkdownImplementActionGroup.kt index 5ca450b4..c777a1a6 100644 --- a/src/main/kotlin/com/github/simiacryptus/aicoder/actions/markdown/MarkdownImplementActionGroup.kt +++ b/src/main/kotlin/com/github/simiacryptus/aicoder/actions/markdown/MarkdownImplementActionGroup.kt @@ -37,7 +37,7 @@ class MarkdownImplementActionGroup : ActionGroup() { return actions.toTypedArray() } - class MarkdownImplementAction(private val language: String) : SelectionAction(true) { + open class MarkdownImplementAction(private val language: String) : SelectionAction(true) { init { templatePresentation.text = language templatePresentation.description = language 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 8ef7c92e..04a7653a 100644 --- a/src/main/kotlin/com/github/simiacryptus/aicoder/config/ActionSettingsRegistry.kt +++ b/src/main/kotlin/com/github/simiacryptus/aicoder/config/ActionSettingsRegistry.kt @@ -11,31 +11,44 @@ import java.util.stream.Collectors class ActionSettingsRegistry { val actionSettings: MutableMap = HashMap() - private val version = 2.0005 + private val version = 2.0006 fun edit(superChildren: Array): Array { val children = superChildren.toList().toMutableList() - children.toTypedArray().forEach { + children.toTypedArray().forEach { action -> val language = "kt" - val code: String? = load(it.javaClass, language) + val code: String? = load(action.javaClass, language) if (null != code) { try { - val actionConfig = this.getActionConfig(it) + val actionConfig = this.getActionConfig(action) actionConfig.language = language actionConfig.isDynamic = false - with(it) { + with(action) { templatePresentation.text = actionConfig.displayText templatePresentation.description = actionConfig.displayText } if (!actionConfig.enabled) { - children.remove(it) + children.remove(action) } else if (!actionConfig.file.exists() || actionConfig.file.readText().isBlank() || (actionConfig.version ?: 0.0) < version ) { actionConfig.file.writeText(code) actionConfig.version = version - } else if (!(actionConfig.isDynamic || (actionConfig.version ?: 0.0) >= version)) { + } else { + if (actionConfig.isDynamic || (actionConfig.version ?: 0.0) >= version) { + val localCode = actionConfig.file.readText().dropWhile { !it.isLetter() } + if (!localCode.equals(code)) { + try { + val element = actionConfig.buildAction(localCode) + children.remove(action) + children.add(element) + return@forEach + } catch (e: Throwable) { + log.info("Error loading dynamic ${action.javaClass}", e) + } + } + } val canLoad = try { ActionSettingsRegistry::class.java.classLoader.loadClass(actionConfig.id) true @@ -46,19 +59,12 @@ class ActionSettingsRegistry { actionConfig.file.writeText(code) actionConfig.version = version } else { - children.remove(it) - } - } else { - val localCode = actionConfig.file.readText().drop(1) - if (!localCode.equals(code)) { - val element = actionConfig.buildAction(localCode) - children.remove(it) - children.add(element) + children.remove(action) } } actionConfig.version = version } catch (e: Throwable) { - UITools.error(log, "Error loading ${it.javaClass}", e) + UITools.error(log, "Error loading ${action.javaClass}", e) } } } @@ -193,7 +199,7 @@ class ActionSettingsRegistry { private fun load(path: String): String? { val bytes = EditorMenu::class.java.getResourceAsStream(path)?.readAllBytes() - return bytes?.toString(Charsets.UTF_8)?.drop(1) // XXX Why? '\uFEFF' is first byte + return bytes?.toString(Charsets.UTF_8)?.dropWhile { !it.isLetter() } } fun load(clazz: Class, language: String) = diff --git a/src/main/kotlin/com/github/simiacryptus/aicoder/config/ActionTable.kt b/src/main/kotlin/com/github/simiacryptus/aicoder/config/ActionTable.kt index 76f8d6d6..e274c43e 100644 --- a/src/main/kotlin/com/github/simiacryptus/aicoder/config/ActionTable.kt +++ b/src/main/kotlin/com/github/simiacryptus/aicoder/config/ActionTable.kt @@ -3,6 +3,7 @@ import com.intellij.openapi.application.ApplicationManager import com.intellij.openapi.fileEditor.FileEditorManager import com.intellij.openapi.project.Project +import com.intellij.openapi.project.ProjectManager import com.intellij.openapi.ui.VerticalFlowLayout import com.intellij.openapi.vfs.LocalFileSystem import com.intellij.ui.BooleanTableCellEditor @@ -155,14 +156,17 @@ class ActionTable( override fun actionPerformed(e: ActionEvent?) { val id = dataModel.getValueAt(jtable.selectedRow, 2) val actionSetting = actionSettings.find { it.id == id } + val projectManager = ProjectManager.getInstance() actionSetting?.file?.let { val project = ApplicationManager.getApplication().runReadAction { - com.intellij.openapi.project.ProjectManager.getInstance().openProjects.firstOrNull() + projectManager.openProjects.firstOrNull() ?: projectManager.defaultProject } + if (it.exists()) { ApplicationManager.getApplication().invokeLater { val virtualFile = LocalFileSystem.getInstance().refreshAndFindFileByIoFile(it) - FileEditorManager.getInstance(project!!).openFile(virtualFile!!, true) + val fileEditorManager = FileEditorManager.getInstance(project!!) + val editor = fileEditorManager.openFile(virtualFile!!, true).firstOrNull() } } else { log.warn("File not found: ${it.absolutePath}") @@ -207,6 +211,17 @@ class ActionTable( jtable.columnModel.getColumn(1).headerRenderer = DefaultTableCellRenderer() jtable.columnModel.getColumn(2).headerRenderer = DefaultTableCellRenderer() + // Set the preferred width for the first column (checkboxes) to the header label width + val headerRenderer = jtable.tableHeader.defaultRenderer + val headerValue = jtable.columnModel.getColumn(0).headerValue + val headerComp = headerRenderer.getTableCellRendererComponent(jtable, headerValue, false, false, 0, 0) + jtable.columnModel.getColumn(0).preferredWidth = headerComp.preferredSize.width + + // Set the minimum width for the second column (display text) to accommodate 100 characters + val metrics = jtable.getFontMetrics(jtable.font) + val minWidth = metrics.charWidth('m') * 32 + jtable.columnModel.getColumn(1).minWidth = minWidth + jtable.tableHeader.defaultRenderer = DefaultTableCellRenderer() add(scrollpane, BorderLayout.CENTER) 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 b9d958c1..927b141f 100644 --- a/src/main/kotlin/com/github/simiacryptus/aicoder/config/AppSettingsComponent.kt +++ b/src/main/kotlin/com/github/simiacryptus/aicoder/config/AppSettingsComponent.kt @@ -1,10 +1,9 @@ -@file:Suppress("unused") - package com.github.simiacryptus.aicoder.config import com.intellij.openapi.application.ApplicationManager import com.intellij.openapi.fileEditor.FileEditorManager import com.intellij.openapi.project.Project +import com.intellij.openapi.project.ProjectManager import com.intellij.openapi.ui.ComboBox import com.intellij.openapi.vfs.LocalFileSystem import com.intellij.ui.components.JBCheckBox @@ -12,23 +11,21 @@ import com.intellij.ui.components.JBPasswordField import com.intellij.ui.components.JBTextField import com.simiacryptus.jopenai.ClientUtil import com.simiacryptus.jopenai.models.ChatModels - import org.slf4j.LoggerFactory import java.awt.event.ActionEvent import javax.swing.AbstractAction import javax.swing.JButton -import javax.swing.JComponent -class AppSettingsComponent { +class AppSettingsComponent : com.intellij.openapi.Disposable { @Name("Token Counter") val tokenCounter = JBTextField() @Suppress("unused") val clearCounter = JButton(object : AbstractAction("Clear Token Counter") { - override fun actionPerformed(e: ActionEvent) { - tokenCounter.text = "0" - } + override fun actionPerformed(e: ActionEvent) { + tokenCounter.text = "0" + } }) @Suppress("unused") @@ -57,21 +54,21 @@ class AppSettingsComponent { @Suppress("unused") val openApiLog = JButton(object : AbstractAction("Open API Log") { - override fun actionPerformed(e: ActionEvent) { - ClientUtil.auxiliaryLog?.let { - val project = ApplicationManager.getApplication().runReadAction { - com.intellij.openapi.project.ProjectManager.getInstance().openProjects.firstOrNull() - } - if (it.exists()) { - ApplicationManager.getApplication().invokeLater { - val virtualFile = LocalFileSystem.getInstance().refreshAndFindFileByIoFile(it) - FileEditorManager.getInstance(project!!).openFile(virtualFile!!, true) - } - } else { - log.warn("Log file not found: ${it.absolutePath}") - } + override fun actionPerformed(e: ActionEvent) { + ClientUtil.auxiliaryLog?.let { + val project = ApplicationManager.getApplication().runReadAction { + ProjectManager.getInstance().openProjects.firstOrNull() + } + if (it.exists()) { + ApplicationManager.getApplication().invokeLater { + val virtualFile = LocalFileSystem.getInstance().refreshAndFindFileByIoFile(it) + FileEditorManager.getInstance(project!!).openFile(virtualFile!!, true) } + } else { + log.warn("Log file not found: ${it.absolutePath}") + } } + } }) @@ -96,11 +93,11 @@ class AppSettingsComponent { @Name("File Actions") var fileActions = ActionTable(AppSettingsState.instance.fileActions.actionSettings.values.map { it.copy() } - .toTypedArray().toMutableList()) + .toTypedArray().toMutableList()) @Name("Editor Actions") var editorActions = ActionTable(AppSettingsState.instance.editorActions.actionSettings.values.map { it.copy() } - .toTypedArray().toMutableList()) + .toTypedArray().toMutableList()) init { tokenCounter.isEditable = false @@ -109,16 +106,10 @@ class AppSettingsComponent { this.modelName.addItem(ChatModels.GPT4Turbo.modelName) } - val preferredFocusedComponent: JComponent - get() = apiKey - - class ActionChangedListener { - fun actionChanged() { - } - } - - companion object { + companion object { private val log = LoggerFactory.getLogger(AppSettingsComponent::class.java) - //val ACTIONS_TOPIC = Topic.create("Actions", ActionChangedListener::class.java) } -} + + override fun dispose() { + } +} \ No newline at end of file diff --git a/src/main/kotlin/com/github/simiacryptus/aicoder/config/AppSettingsConfigurable.kt b/src/main/kotlin/com/github/simiacryptus/aicoder/config/AppSettingsConfigurable.kt index 5c0e4e5d..7902b21e 100644 --- a/src/main/kotlin/com/github/simiacryptus/aicoder/config/AppSettingsConfigurable.kt +++ b/src/main/kotlin/com/github/simiacryptus/aicoder/config/AppSettingsConfigurable.kt @@ -1,68 +1,23 @@ -package com.github.simiacryptus.aicoder.config +package com.github.simiacryptus.aicoder.config import com.github.simiacryptus.aicoder.util.UITools -import com.intellij.openapi.options.Configurable -import java.util.* -import javax.swing.JComponent -import javax.swing.JPanel -class AppSettingsConfigurable : Configurable { - private var settingsComponent: AppSettingsComponent? = null +open class AppSettingsConfigurable : UIAdapter(AppSettingsState.instance) { + override fun read(component: AppSettingsComponent, settings: AppSettingsState) { + UITools.readKotlinUIViaReflection(component, settings) + component.editorActions.read(settings.editorActions) + component.fileActions.read(settings.fileActions) + } - @Volatile - private var mainPanel: JPanel? = null - override fun getDisplayName(): String { - return "AICoder Settings" - } + override fun write(settings: AppSettingsState, component: AppSettingsComponent) { + UITools.writeKotlinUIViaReflection(settings, component) + component.editorActions.write(settings.editorActions) + component.fileActions.write(settings.fileActions) + } - override fun getPreferredFocusedComponent(): JComponent? { - return Objects.requireNonNull(settingsComponent)?.preferredFocusedComponent - } + override fun getPreferredFocusedComponent() = component?.apiKey - override fun createComponent(): JComponent? { - if (null == mainPanel) { - synchronized(this) { - if (null == mainPanel) { - settingsComponent = AppSettingsComponent() - reset() - mainPanel = UITools.build(settingsComponent!!, false) - } - } - } - return mainPanel - } + override fun newComponent() = AppSettingsComponent() - - override fun isModified(): Boolean { - val buffer = AppSettingsState() - if (settingsComponent != null) { - UITools.readKotlinUI(settingsComponent!!, buffer) - settingsComponent?.editorActions?.read(buffer.editorActions) - settingsComponent?.fileActions?.read(buffer.fileActions) - } - return buffer != AppSettingsState.instance - } - - override fun apply() { - if (settingsComponent != null) { - UITools.readKotlinUI(settingsComponent!!, AppSettingsState.instance) - settingsComponent?.editorActions?.read(AppSettingsState.instance.editorActions) - settingsComponent?.fileActions?.read(AppSettingsState.instance.fileActions) - } - } - - override fun reset() { - if (settingsComponent != null) { - UITools.writeKotlinUI(settingsComponent!!, AppSettingsState.instance) - settingsComponent?.editorActions?.write(AppSettingsState.instance.editorActions) - settingsComponent?.fileActions?.write(AppSettingsState.instance.fileActions) - } - } - - override fun disposeUIResources() { - settingsComponent = null - } + override fun newSettings() = AppSettingsState() } - - - 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 2010759a..a01c5fd5 100644 --- a/src/main/kotlin/com/github/simiacryptus/aicoder/config/AppSettingsState.kt +++ b/src/main/kotlin/com/github/simiacryptus/aicoder/config/AppSettingsState.kt @@ -6,50 +6,36 @@ import com.intellij.openapi.components.PersistentStateComponent import com.intellij.openapi.components.State import com.intellij.openapi.components.Storage import com.intellij.util.xmlb.XmlSerializerUtil -import com.simiacryptus.jopenai.ApiModel.ChatRequest import com.simiacryptus.jopenai.models.ChatModels -import com.simiacryptus.jopenai.models.OpenAIModel import com.simiacryptus.jopenai.models.OpenAITextModel import com.simiacryptus.jopenai.util.JsonUtil -class SimpleEnvelope(var value: String? = null) - @State(name = "org.intellij.sdk.settings.AppSettingsState", storages = [Storage("SdkSettingsPlugin.xml")]) -class AppSettingsState : PersistentStateComponent { - var listeningPort: Int = 8081 - var listeningEndpoint: String = "localhost" - var modalTasks: Boolean = false - var suppressErrors: Boolean = false - var apiLog: Boolean = false - var apiBase = "https://api.openai.com/v1" - var apiKey = "" - var temperature = 0.1 - var modelName : String = ChatModels.GPT35Turbo.modelName - var tokenCounter = 0 - var humanLanguage = "English" - var devActions = false - var editRequests = false - var apiThreads = 4 +data class AppSettingsState( + var temperature: Double = 0.1, + var modelName: String = ChatModels.GPT35Turbo.modelName, + var listeningPort: Int = 8081, + var listeningEndpoint: String = "localhost", + var humanLanguage: String = "English", + var apiThreads: Int = 4, + var apiBase: String = "https://api.openai.com/v1", + var apiKey: String = "", + var tokenCounter: Int = 0, + var modalTasks: Boolean = false, + var suppressErrors: Boolean = false, + var apiLog: Boolean = false, + var devActions: Boolean = false, + var editRequests: Boolean = false, +) : PersistentStateComponent { + val editorActions = ActionSettingsRegistry() val fileActions = ActionSettingsRegistry() - private val recentCommands = mutableMapOf() - fun createChatRequest(): ChatRequest { - return createChatRequest(defaultChatModel()) - } - fun defaultChatModel(): OpenAITextModel = ChatModels.entries.first { it.modelName == modelName } - private fun createChatRequest(model: OpenAIModel): ChatRequest = ChatRequest( - model = model.modelName, - temperature = temperature - ) - @JsonIgnore - override fun getState(): SimpleEnvelope { - return SimpleEnvelope(JsonUtil.toJson(this)) - } + override fun getState() = SimpleEnvelope(JsonUtil.toJson(this)) fun getRecentCommands(id:String) = recentCommands.computeIfAbsent(id) { MRUItems() } @@ -57,52 +43,12 @@ class AppSettingsState : PersistentStateComponent { state.value ?: return val fromJson = JsonUtil.fromJson(state.value!!, AppSettingsState::class.java) XmlSerializerUtil.copyBean(fromJson, this) - - recentCommands.clear(); recentCommands.putAll(fromJson.recentCommands) - editorActions.actionSettings.clear(); editorActions.actionSettings.putAll(fromJson.editorActions.actionSettings) - fileActions.actionSettings.clear(); fileActions.actionSettings.putAll(fromJson.fileActions.actionSettings) - } - - override fun equals(other: Any?): Boolean { - if (this === other) return true - if (javaClass != other?.javaClass) return false - - other as AppSettingsState - - if (listeningPort != other.listeningPort) return false - if (listeningEndpoint != other.listeningEndpoint) return false - if (modalTasks != other.modalTasks) return false - if (suppressErrors != other.suppressErrors) return false - if (apiLog != other.apiLog) return false - if (apiBase != other.apiBase) return false - if (apiKey != other.apiKey) return false - if (temperature != other.temperature) return false - if (modelName != other.modelName) return false - if (tokenCounter != other.tokenCounter) return false - if (humanLanguage != other.humanLanguage) return false - if (devActions != other.devActions) return false - if (editRequests != other.editRequests) return false - if (apiThreads != other.apiThreads) return false - - return true - } - - override fun hashCode(): Int { - var result = listeningPort - result = 31 * result + listeningEndpoint.hashCode() - result = 31 * result + modalTasks.hashCode() - result = 31 * result + suppressErrors.hashCode() - result = 31 * result + apiLog.hashCode() - result = 31 * result + apiBase.hashCode() - result = 31 * result + apiKey.hashCode() - result = 31 * result + temperature.hashCode() - result = 31 * result + modelName.hashCode() - result = 31 * result + tokenCounter - result = 31 * result + humanLanguage.hashCode() - result = 31 * result + devActions.hashCode() - result = 31 * result + editRequests.hashCode() - result = 31 * result + apiThreads - return result + recentCommands.clear(); + recentCommands.putAll(fromJson.recentCommands) + editorActions.actionSettings.clear(); + editorActions.actionSettings.putAll(fromJson.editorActions.actionSettings) + fileActions.actionSettings.clear(); + fileActions.actionSettings.putAll(fromJson.fileActions.actionSettings) } companion object { @@ -113,3 +59,5 @@ class AppSettingsState : PersistentStateComponent { } } } + + diff --git a/src/main/kotlin/com/github/simiacryptus/aicoder/config/NonReflectionAppSettingsConfigurable.kt b/src/main/kotlin/com/github/simiacryptus/aicoder/config/NonReflectionAppSettingsConfigurable.kt new file mode 100644 index 00000000..099fb6f4 --- /dev/null +++ b/src/main/kotlin/com/github/simiacryptus/aicoder/config/NonReflectionAppSettingsConfigurable.kt @@ -0,0 +1,155 @@ +package com.github.simiacryptus.aicoder.config + +import java.awt.BorderLayout +import java.awt.FlowLayout +import javax.swing.BoxLayout +import javax.swing.JComponent +import javax.swing.JLabel +import javax.swing.JPanel + +class NonReflectionAppSettingsConfigurable : AppSettingsConfigurable() { + + override fun build(component: AppSettingsComponent): JComponent { + val tabbedPane = com.intellij.ui.components.JBTabbedPane() + + // Basic Settings Tab + val basicSettingsPanel = JPanel(BorderLayout()).apply { + add(JPanel(BorderLayout()).apply { + layout = BoxLayout(this, BoxLayout.Y_AXIS) + add(JPanel(FlowLayout(FlowLayout.LEFT)).apply { + add(JLabel("Model:")) + add(component.modelName) + }) + add(JPanel(FlowLayout(FlowLayout.LEFT)).apply { + add(JLabel("Temperature:")) + add(component.temperature) + }) + add(JPanel(FlowLayout(FlowLayout.LEFT)).apply { + add(JLabel("Human Language:")) + add(component.humanLanguage) + }) + add(JPanel(FlowLayout(FlowLayout.LEFT)).apply { + add(JLabel("Token Counter:")) + add(component.tokenCounter) + add(component.clearCounter) + }) + add(JPanel(FlowLayout(FlowLayout.LEFT)).apply { + add(JLabel("API Key:")) + add(component.apiKey) + }) + add(JPanel(FlowLayout(FlowLayout.LEFT)).apply { + add(JLabel("Server Port:")) + add(component.listeningPort) + }) + add(JPanel(FlowLayout(FlowLayout.LEFT)).apply { + add(JLabel("Ignore Errors:")) + add(component.suppressErrors) + }) + }, BorderLayout.NORTH) + } + tabbedPane.addTab("Basic Settings", basicSettingsPanel) + + tabbedPane.addTab("Developer Tools", JPanel(BorderLayout()).apply { + add(JPanel().apply { + layout = BoxLayout(this, BoxLayout.Y_AXIS) + add(JPanel(FlowLayout(FlowLayout.LEFT)).apply { + add(JLabel("Developer Tools:")) + add(component.devActions) + }) + add(JPanel(FlowLayout(FlowLayout.LEFT)).apply { + add(JLabel("Edit API Requests:")) + add(component.editRequests) + }) + add(JPanel(FlowLayout(FlowLayout.LEFT)).apply { + add(JLabel("Enable API Log:")) + add(component.apiLog) + add(component.openApiLog) + }) + add(JPanel(FlowLayout(FlowLayout.LEFT)).apply { + add(JLabel("API Base:")) + add(component.apiBase) + }) + add(JPanel(FlowLayout(FlowLayout.LEFT)).apply { + add(JLabel("Server Endpoint:")) + add(component.listeningEndpoint) + }) + }, BorderLayout.NORTH) + }) + + tabbedPane.addTab("File Actions", JPanel(BorderLayout()).apply { + add(component.fileActions, BorderLayout.CENTER) + }) + + tabbedPane.addTab("Editor Actions", JPanel(BorderLayout()).apply { + add(component.editorActions, BorderLayout.CENTER) + }) + + return tabbedPane + } + + override fun write(settings: AppSettingsState, component: AppSettingsComponent) { + try { + component.tokenCounter.text = settings.tokenCounter.toString() + component.humanLanguage.text = settings.humanLanguage + component.listeningPort.text = settings.listeningPort.toString() + component.listeningEndpoint.text = settings.listeningEndpoint + component.suppressErrors.isSelected = settings.suppressErrors + component.modelName.selectedItem = settings.modelName + component.apiLog.isSelected = settings.apiLog + component.devActions.isSelected = settings.devActions + component.editRequests.isSelected = settings.editRequests + component.temperature.text = settings.temperature.toString() + component.apiKey.text = settings.apiKey + component.apiBase.text = settings.apiBase + component.editorActions.read(settings.editorActions) + component.fileActions.read(settings.fileActions) + } catch (e: Exception) { + log.warn("Error setting UI", e) + } + } + + override fun read(component: AppSettingsComponent, settings: AppSettingsState) { + try { + settings.tokenCounter = component.tokenCounter.text.safeInt() + settings.humanLanguage = component.humanLanguage.text + settings.listeningPort = component.listeningPort.text.safeInt() + settings.listeningEndpoint = component.listeningEndpoint.text + settings.suppressErrors = component.suppressErrors.isSelected + settings.modelName = component.modelName.selectedItem as String + settings.apiLog = component.apiLog.isSelected + settings.devActions = component.devActions.isSelected + settings.editRequests = component.editRequests.isSelected + settings.temperature = component.temperature.text.safeDouble() + settings.apiKey = String(component.apiKey.password) + settings.apiBase = component.apiBase.text + component.editorActions.write(settings.editorActions) + component.fileActions.write(settings.fileActions) + } catch (e: Exception) { + log.warn("Error reading UI", e) + } + } + + companion object { + val log = com.intellij.openapi.diagnostic.Logger.getInstance(NonReflectionAppSettingsConfigurable::class.java) + } +} + +fun String?.safeInt() = if (null == this) 0 else when { + isEmpty() -> 0 + else -> try { + toInt() + } catch (e: NumberFormatException) { + 0 + } +} + +fun String?.safeDouble() = if (null == this) 0.0 else when { + isEmpty() -> 0.0 + else -> try { + toDouble() + } catch (e: NumberFormatException) { + 0.0 + } + + +} diff --git a/src/main/kotlin/com/github/simiacryptus/aicoder/config/SimpleEnvelope.kt b/src/main/kotlin/com/github/simiacryptus/aicoder/config/SimpleEnvelope.kt new file mode 100644 index 00000000..4dfdd5ca --- /dev/null +++ b/src/main/kotlin/com/github/simiacryptus/aicoder/config/SimpleEnvelope.kt @@ -0,0 +1,3 @@ +package com.github.simiacryptus.aicoder.config + +class SimpleEnvelope(var value: String? = null) \ No newline at end of file diff --git a/src/main/kotlin/com/github/simiacryptus/aicoder/config/UIAdapter.kt b/src/main/kotlin/com/github/simiacryptus/aicoder/config/UIAdapter.kt new file mode 100644 index 00000000..7cf75ec0 --- /dev/null +++ b/src/main/kotlin/com/github/simiacryptus/aicoder/config/UIAdapter.kt @@ -0,0 +1,77 @@ +package com.github.simiacryptus.aicoder.config + +import com.github.simiacryptus.aicoder.util.UITools +import com.intellij.openapi.Disposable +import com.intellij.openapi.options.Configurable +import javax.swing.JComponent + +abstract class UIAdapter( + protected val settingsInstance: S, + protected var component: C? = null, +) : Configurable { + + @Volatile + private var mainPanel: JComponent? = null + override fun getDisplayName(): String { + return "AICoder Settings" + } + + override fun getPreferredFocusedComponent(): JComponent? = null + + override fun createComponent(): JComponent? { + if (null == mainPanel) { + synchronized(this) { + if (null == mainPanel) { + val component = newComponent() + this.component = component + mainPanel = build(component) + write(settingsInstance, component) + } + } + } + return mainPanel + } + + abstract fun newComponent(): C + abstract fun newSettings(): S + fun getSettings(component : C? = this.component) = when (component) { + null -> settingsInstance + else -> { + val buffer = newSettings() + read(component, buffer) + buffer + } + } + + override fun isModified() = when { + component == null -> false + getSettings() != settingsInstance -> true + else -> false + } + + override fun apply() { + if (component != null) read(component!!, settingsInstance) + } + + override fun reset() { + if (component != null) write(settingsInstance, component!!) + } + + override fun disposeUIResources() { + val component = component + this.component = null + if(component != null && component is Disposable) component.dispose() + } + + open fun build(component: C): JComponent = + UITools.buildFormViaReflection(component, false)!! + + open fun read(component: C, settings: S) { + UITools.readKotlinUIViaReflection(component, settings) + } + + open fun write(settings: S, component: C) { + UITools.writeKotlinUIViaReflection(settings, component) + } + +} \ No newline at end of file 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 e4c615dd..ec87950c 100644 --- a/src/main/kotlin/com/github/simiacryptus/aicoder/util/UITools.kt +++ b/src/main/kotlin/com/github/simiacryptus/aicoder/util/UITools.kt @@ -63,857 +63,787 @@ import kotlin.reflect.jvm.javaField import kotlin.reflect.jvm.javaType object UITools { - private val log = LoggerFactory.getLogger(UITools::class.java) - val retry = WeakHashMap() - - @JvmStatic - fun redoableTask( - event: AnActionEvent, - request: Supplier, - ) { - Futures.addCallback(pool.submit { - request.get() - }, futureCallback(event, request), pool) - } - - @JvmStatic - fun futureCallback( - event: AnActionEvent, - request: Supplier, - ) = object : FutureCallback { - override fun onSuccess(undo: Runnable) { - val requiredData = event.getData(CommonDataKeys.EDITOR) ?: return - val document = requiredData.document - retry[document] = getRetry(event, request, undo) - } - - override fun onFailure(t: Throwable) { - error(log, "Error", t) - } - } - - @JvmStatic - fun getRetry( - event: AnActionEvent, - request: Supplier, - undo: Runnable, - ): Runnable { - return Runnable { - Futures.addCallback( - pool.submit { - WriteCommandAction.runWriteCommandAction(event.project) { undo?.run() } - request.get() - }, - futureCallback(event, request), - pool - ) - } - } - - @JvmStatic - fun replaceString(document: Document, startOffset: Int, endOffset: Int, newText: CharSequence): Runnable { - val oldText: CharSequence = document.getText(TextRange(startOffset, endOffset)) - document.replaceString(startOffset, endOffset, newText) - logEdit( - String.format( - "FWD replaceString from %s to %s (%s->%s): %s", - startOffset, - endOffset, - endOffset - startOffset, - newText.length, - newText - ) + val retry = WeakHashMap() + + private val log = LoggerFactory.getLogger(UITools::class.java) + private val threadFactory: ThreadFactory = ThreadFactoryBuilder().setNameFormat("API Thread %d").build() + private val pool: ListeningExecutorService by lazy { + MoreExecutors.listeningDecorator( + ThreadPoolExecutor( + /* corePoolSize = */ AppSettingsState.instance.apiThreads, + /* maximumPoolSize = */AppSettingsState.instance.apiThreads, + /* keepAliveTime = */ 0L, + /* unit = */ TimeUnit.MILLISECONDS, + /* workQueue = */ LinkedBlockingQueue(), + /* threadFactory = */ threadFactory, + /* handler = */ ThreadPoolExecutor.AbortPolicy() + ) + ) + } + private val scheduledPool: ListeningScheduledExecutorService by lazy { + MoreExecutors.listeningDecorator(ScheduledThreadPoolExecutor(1, threadFactory)) + } + private val errorLog = mutableListOf>() + private val actionLog = mutableListOf() + private val singleThreadPool = Executors.newSingleThreadExecutor() + + fun redoableTask( + event: AnActionEvent, + request: Supplier, + ) { + Futures.addCallback(pool.submit { + request.get() + }, futureCallback(event, request), pool) + } + + private fun futureCallback( + event: AnActionEvent, + request: Supplier, + ) = object : FutureCallback { + override fun onSuccess(undo: Runnable) { + val requiredData = event.getData(CommonDataKeys.EDITOR) ?: return + val document = requiredData.document + retry[document] = getRetry(event, request, undo) + } + + override fun onFailure(t: Throwable) { + error(log, "Error", t) + } + } + + fun getRetry( + event: AnActionEvent, + request: Supplier, + undo: Runnable, + ): Runnable { + return Runnable { + Futures.addCallback( + pool.submit { + WriteCommandAction.runWriteCommandAction(event.project) { undo?.run() } + request.get() + }, futureCallback(event, request), pool + ) + } + } + + fun replaceString(document: Document, startOffset: Int, endOffset: Int, newText: CharSequence): Runnable { + val oldText: CharSequence = document.getText(TextRange(startOffset, endOffset)) + document.replaceString(startOffset, endOffset, newText) + logEdit( + String.format( + "FWD replaceString from %s to %s (%s->%s): %s", + startOffset, + endOffset, + endOffset - startOffset, + newText.length, + newText + ) + ) + return Runnable { + val verifyTxt = document.getText(TextRange(startOffset, startOffset + newText.length)) + if (verifyTxt != newText) { + val msg = String.format( + "The text range from %d to %d does not match the expected text \"%s\" and is instead \"%s\"", + startOffset, + startOffset + newText.length, + newText, + verifyTxt ) - return Runnable { - val verifyTxt = document.getText(TextRange(startOffset, startOffset + newText.length)) - if (verifyTxt != newText) { - val msg = String.format( - "The text range from %d to %d does not match the expected text \"%s\" and is instead \"%s\"", - startOffset, - startOffset + newText.length, - newText, - verifyTxt - ) - throw IllegalStateException(msg) - } - document.replaceString(startOffset, startOffset + newText.length, oldText) - logEdit( - String.format( - "REV replaceString from %s to %s (%s->%s): %s", - startOffset, - startOffset + newText.length, - newText.length, - oldText.length, - oldText - ) - ) - } - } - - @JvmStatic - fun insertString(document: Document, startOffset: Int, newText: CharSequence): Runnable { - document.insertString(startOffset, newText) - logEdit(String.format("FWD insertString @ %s (%s): %s", startOffset, newText.length, newText)) - return Runnable { - val verifyTxt = document.getText(TextRange(startOffset, startOffset + newText.length)) - if (verifyTxt != newText) { - val message = String.format( - "The text range from %d to %d does not match the expected text \"%s\" and is instead \"%s\"", - startOffset, - startOffset + newText.length, - newText, - verifyTxt - ) - throw AssertionError(message) - } - document.deleteString(startOffset, startOffset + newText.length) - logEdit(String.format("REV deleteString from %s to %s", startOffset, startOffset + newText.length)) - } - } - - @JvmStatic - private fun logEdit(message: String) { - log.debug(message) - } - - @JvmStatic - @Suppress("unused") - fun deleteString(document: Document, startOffset: Int, endOffset: Int): Runnable { - val oldText: CharSequence = document.getText(TextRange(startOffset, endOffset)) - document.deleteString(startOffset, endOffset) - return Runnable { - document.insertString(startOffset, oldText) - logEdit(String.format("REV insertString @ %s (%s): %s", startOffset, oldText.length, oldText)) - } - } - - @JvmStatic - fun getIndent(caret: Caret?): CharSequence { - if (null == caret) return "" - val document = caret.editor.document - val documentText = document.text - val lineNumber = document.getLineNumber(caret.selectionStart) - val lines = documentText.split("\n".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray() - if (lines.isEmpty()) return "" - return IndentedText.fromString(lines[max(lineNumber, 0).coerceAtMost(lines.size - 1)]).indent - } - - @JvmStatic - @Suppress("unused") - fun hasSelection(e: AnActionEvent): Boolean { - val caret = e.getData(CommonDataKeys.CARET) - return null != caret && caret.hasSelection() - } - - @JvmStatic - fun getIndent(event: AnActionEvent): CharSequence { - val caret = event.getData(CommonDataKeys.CARET) - val indent: CharSequence = if (null == caret) { - "" - } else { - getIndent(caret) - } - return indent - } - - @JvmStatic - fun queryAPIKey(): CharSequence? { - val panel = JPanel() - val label = JLabel("Enter OpenAI API Key:") - val pass = JPasswordField(100) - panel.add(label) - panel.add(pass) - val options = arrayOf("OK", "Cancel") - return if (JOptionPane.showOptionDialog( - null, - panel, - "API Key", - JOptionPane.NO_OPTION, - JOptionPane.PLAIN_MESSAGE, - null, - options, - options[1] - ) == JOptionPane.OK_OPTION - ) { - val password = pass.password - java.lang.String(password) - } else { - null - } - } - - @JvmStatic - fun readKotlinUI(component: R, settings: T) { - val componentClass: Class<*> = component.javaClass - val declaredUIFields = - componentClass.kotlin.memberProperties.map { it.name }.toSet() - for (settingsField in settings.javaClass.kotlin.memberProperties) { - if (settingsField is KMutableProperty<*>) { - settingsField.isAccessible = true - val settingsFieldName = settingsField.name - try { - var newSettingsValue: Any? = null - if (!declaredUIFields.contains(settingsFieldName)) continue - val uiField: KProperty1 = - (componentClass.kotlin.memberProperties.find { it.name == settingsFieldName } as KProperty1?)!! - var uiVal = uiField.get(component) - if (uiVal is JScrollPane) { - uiVal = uiVal.viewport.view - } - when (settingsField.returnType.javaType.typeName) { - "java.lang.String" -> if (uiVal is JTextComponent) { - newSettingsValue = uiVal.text - } else if (uiVal is ComboBox<*>) { - newSettingsValue = uiVal.item - } - - "int", "java.lang.Integer" -> if (uiVal is JTextComponent) { - newSettingsValue = if (uiVal.text.isBlank()) -1 else uiVal.text.toInt() - } - - "long" -> if (uiVal is JTextComponent) { - newSettingsValue = if (uiVal.text.isBlank()) -1 else uiVal.text.toLong() - } - - "double", "java.lang.Double" -> if (uiVal is JTextComponent) { - newSettingsValue = if (uiVal.text.isBlank()) 0.0 else uiVal.text.toDouble() - } - - "boolean" -> if (uiVal is JCheckBox) { - newSettingsValue = uiVal.isSelected - } else if (uiVal is JTextComponent) { - newSettingsValue = java.lang.Boolean.parseBoolean(uiVal.text) - } - - else -> - if (Enum::class.java.isAssignableFrom(settingsField.returnType.javaType as Class<*>)) { - if (uiVal is ComboBox<*>) { - val comboBox = uiVal - val item = comboBox.item - val enumClass = settingsField.returnType.javaType as Class?> - val string = item.toString() - newSettingsValue = - findValue(enumClass, string) - } - } - } - settingsField.setter.call(settings, newSettingsValue) - } catch (e: Throwable) { - throw RuntimeException("Error processing $settingsField", e) - } - } - } - } - - @JvmStatic - fun findValue(enumClass: Class?>, string: String): Enum<*>? { - enumClass.enumConstants?.filter { it?.name?.compareTo(string, true) == 0 }?.forEach { return it } - return java.lang.Enum.valueOf( - enumClass, - string + throw IllegalStateException(msg) + } + document.replaceString(startOffset, startOffset + newText.length, oldText) + logEdit( + String.format( + "REV replaceString from %s to %s (%s->%s): %s", + startOffset, + startOffset + newText.length, + newText.length, + oldText.length, + oldText ) - } - - @JvmStatic - fun writeKotlinUI(component: R, settings: T) { - val componentClass: Class<*> = component.javaClass - val declaredUIFields = - componentClass.kotlin.memberProperties.map { it.name }.toSet() - val memberProperties = settings.javaClass.kotlin.memberProperties - val publicProperties = memberProperties.filter { - it.visibility == KVisibility.PUBLIC //&& it is KMutableProperty<*> - } - for (settingsField in publicProperties) { - val fieldName = settingsField.name - try { - if (!declaredUIFields.contains(fieldName)) continue - val uiField: KProperty1 = - (componentClass.kotlin.memberProperties.find { it.name == fieldName } as KProperty1?)!! - val settingsVal = settingsField.get(settings) ?: continue - var uiVal = uiField.get(component) - if (uiVal is JScrollPane) { - uiVal = uiVal.viewport.view - } - when (settingsField.returnType.javaType.typeName) { - "java.lang.String" -> if (uiVal is JTextComponent) { - uiVal.text = settingsVal.toString() - } else if (uiVal is ComboBox<*>) { - uiVal.item = settingsVal.toString() - } - - "int", "java.lang.Integer" -> if (uiVal is JTextComponent) { - uiVal.text = (settingsVal as Int).toString() - } - - "long" -> if (uiVal is JTextComponent) { - uiVal.text = (settingsVal as Int).toLong().toString() - } - - "boolean" -> if (uiVal is JCheckBox) { - uiVal.isSelected = (settingsVal as Boolean) - } else if (uiVal is JTextComponent) { - uiVal.text = java.lang.Boolean.toString((settingsVal as Boolean)) - } - - "double", "java.lang.Double" -> if (uiVal is JTextComponent) { - uiVal.text = (settingsVal as Double).toString() - } - - else -> if (uiVal is ComboBox<*>) { - uiVal.item = settingsVal.toString() - } - } - } catch (e: Throwable) { - throw RuntimeException("Error processing $settingsField", e) - } - } - } - - @JvmStatic - fun addKotlinFields(ui: T, formBuilder: FormBuilder, fillVertically: Boolean) { - var first = true - for (field in ui.javaClass.kotlin.memberProperties) { - if (field.javaField == null) continue - try { - val nameAnnotation = field.annotations.find { it is Name } as Name? - val component = field.get(ui) as JComponent - if (nameAnnotation != null) { - if (first && fillVertically) { - first = false - formBuilder.addLabeledComponentFillVertically(nameAnnotation.value + ": ", component) - } else { - formBuilder.addLabeledComponent(JBLabel(nameAnnotation.value + ": "), component, 1, false) - } - } else { - formBuilder.addComponentToRightColumn(component, 1) - } - } catch (e: IllegalAccessException) { - throw RuntimeException(e) - } catch (e: Throwable) { - error(log, "Error processing " + field.name, e) - } - } - } - - @JvmStatic - fun getMaximumSize(factor: Double): Dimension { - val screenSize = Toolkit.getDefaultToolkit().screenSize - return Dimension((screenSize.getWidth() * factor).toInt(), (screenSize.getHeight() * factor).toInt()) - } - - @JvmStatic - fun showOptionDialog(mainPanel: JPanel?, vararg options: Any, title: String, modal: Boolean = true): Int { - val pane = getOptionPane(mainPanel, options) - val rootFrame = JOptionPane.getRootFrame() - pane.componentOrientation = rootFrame.componentOrientation - val dialog = JDialog(rootFrame, title, modal) - dialog.componentOrientation = rootFrame.componentOrientation - - val latch = if (!modal) CountDownLatch(1) else null - configure(dialog, pane, latch) - dialog.isVisible = true - if (!modal) latch?.await() - - dialog.dispose() - return getSelectedValue(pane, options) - } - - @JvmStatic - fun getOptionPane( - mainPanel: JPanel?, - options: Array, - ): JOptionPane { - val pane = JOptionPane( - mainPanel, - JOptionPane.PLAIN_MESSAGE, - JOptionPane.NO_OPTION, - null, - options, - options[0] + ) + } + } + + fun insertString(document: Document, startOffset: Int, newText: CharSequence): Runnable { + document.insertString(startOffset, newText) + logEdit(String.format("FWD insertString @ %s (%s): %s", startOffset, newText.length, newText)) + return Runnable { + val verifyTxt = document.getText(TextRange(startOffset, startOffset + newText.length)) + if (verifyTxt != newText) { + val message = String.format( + "The text range from %d to %d does not match the expected text \"%s\" and is instead \"%s\"", + startOffset, + startOffset + newText.length, + newText, + verifyTxt ) - pane.initialValue = options[0] - return pane - } - - @JvmStatic - fun configure(dialog: JDialog, pane: JOptionPane, latch: CountDownLatch? = null) { - val contentPane = dialog.contentPane - contentPane.layout = BorderLayout() - contentPane.add(pane, BorderLayout.CENTER) - - if (JDialog.isDefaultLookAndFeelDecorated() && UIManager.getLookAndFeel().supportsWindowDecorations) { - dialog.isUndecorated = true - pane.rootPane.windowDecorationStyle = JRootPane.PLAIN_DIALOG - } - dialog.isResizable = true - dialog.maximumSize = getMaximumSize(0.9) - dialog.pack() - dialog.setLocationRelativeTo(null as Component?) - val adapter: WindowAdapter = windowAdapter(pane, dialog) - dialog.addWindowListener(adapter) - dialog.addWindowFocusListener(adapter) - dialog.addComponentListener(object : ComponentAdapter() { - override fun componentShown(ce: ComponentEvent) { - // reset value to ensure closing works properly - pane.value = JOptionPane.UNINITIALIZED_VALUE - } - }) - pane.addPropertyChangeListener { event: PropertyChangeEvent -> - if (dialog.isVisible && event.source === pane && event.propertyName == JOptionPane.VALUE_PROPERTY && event.newValue != null && event.newValue !== JOptionPane.UNINITIALIZED_VALUE) { - dialog.isVisible = false - latch?.countDown() + throw AssertionError(message) + } + document.deleteString(startOffset, startOffset + newText.length) + logEdit(String.format("REV deleteString from %s to %s", startOffset, startOffset + newText.length)) + } + } + + private fun logEdit(message: String) { + log.debug(message) + } + + @Suppress("unused") + fun deleteString(document: Document, startOffset: Int, endOffset: Int): Runnable { + val oldText: CharSequence = document.getText(TextRange(startOffset, endOffset)) + document.deleteString(startOffset, endOffset) + return Runnable { + document.insertString(startOffset, oldText) + logEdit(String.format("REV insertString @ %s (%s): %s", startOffset, oldText.length, oldText)) + } + } + + fun getIndent(caret: Caret?): CharSequence { + if (null == caret) return "" + val document = caret.editor.document + val documentText = document.text + val lineNumber = document.getLineNumber(caret.selectionStart) + val lines = documentText.split("\n".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray() + if (lines.isEmpty()) return "" + return IndentedText.fromString(lines[max(lineNumber, 0).coerceAtMost(lines.size - 1)]).indent + } + + @Suppress("unused") + fun hasSelection(e: AnActionEvent): Boolean { + val caret = e.getData(CommonDataKeys.CARET) + return null != caret && caret.hasSelection() + } + + fun getIndent(event: AnActionEvent): CharSequence { + val caret = event.getData(CommonDataKeys.CARET) + val indent: CharSequence = if (null == caret) { + "" + } else { + getIndent(caret) + } + return indent + } + + private fun queryAPIKey(): CharSequence? { + val panel = JPanel() + val label = JLabel("Enter OpenAI API Key:") + val pass = JPasswordField(100) + panel.add(label) + panel.add(pass) + val options = arrayOf("OK", "Cancel") + return if (JOptionPane.showOptionDialog( + null, panel, "API Key", JOptionPane.NO_OPTION, JOptionPane.PLAIN_MESSAGE, null, options, options[1] + ) == JOptionPane.OK_OPTION + ) { + String(pass.password) + } else { + null + } + } + + fun readKotlinUIViaReflection(component: R, settings: T) { + val componentClass: Class<*> = component.javaClass + val declaredUIFields = componentClass.kotlin.memberProperties.map { it.name }.toSet() + for (settingsField in settings.javaClass.kotlin.memberProperties) { + if (settingsField is KMutableProperty<*>) { + settingsField.isAccessible = true + val settingsFieldName = settingsField.name + try { + var newSettingsValue: Any? = null + if (!declaredUIFields.contains(settingsFieldName)) continue + val uiField: KProperty1 = + (componentClass.kotlin.memberProperties.find { it.name == settingsFieldName } as KProperty1?)!! + var uiVal = uiField.get(component) + if (uiVal is JScrollPane) { + uiVal = uiVal.viewport.view + } + when (settingsField.returnType.javaType.typeName) { + "java.lang.String" -> if (uiVal is JTextComponent) { + newSettingsValue = uiVal.text + } else if (uiVal is ComboBox<*>) { + newSettingsValue = uiVal.item } - } - pane.selectInitialValue() - } - - @JvmStatic - private fun windowAdapter(pane: JOptionPane, dialog: JDialog): WindowAdapter { - val adapter: WindowAdapter = object : WindowAdapter() { - private var gotFocus = false - override fun windowClosing(we: WindowEvent) { - pane.value = null + "int", "java.lang.Integer" -> if (uiVal is JTextComponent) { + newSettingsValue = if (uiVal.text.isBlank()) -1 else uiVal.text.toInt() } - override fun windowClosed(e: WindowEvent) { - pane.removePropertyChangeListener { event: PropertyChangeEvent -> - if (dialog.isVisible && event.source === pane && event.propertyName == JOptionPane.VALUE_PROPERTY && event.newValue != null && event.newValue !== JOptionPane.UNINITIALIZED_VALUE) { - dialog.isVisible = false - } - } - dialog.contentPane.removeAll() + "long" -> if (uiVal is JTextComponent) { + newSettingsValue = if (uiVal.text.isBlank()) -1 else uiVal.text.toLong() } - override fun windowGainedFocus(we: WindowEvent) { - if (!gotFocus) { - pane.selectInitialValue() - gotFocus = true - } + "double", "java.lang.Double" -> if (uiVal is JTextComponent) { + newSettingsValue = if (uiVal.text.isBlank()) 0.0 else uiVal.text.toDouble() } - } - return adapter - } - @JvmStatic - private fun getSelectedValue(pane: JOptionPane, options: Array): Int { - val selectedValue = pane.value ?: return JOptionPane.CLOSED_OPTION - var counter = 0 - val maxCounter = options.size - while (counter < maxCounter) { - if (options[counter] == selectedValue) return counter - counter++ - } - return JOptionPane.CLOSED_OPTION - } - - @JvmStatic - fun wrapScrollPane(promptArea: JBTextArea?): JBScrollPane { - val scrollPane = JBScrollPane(promptArea) - scrollPane.horizontalScrollBarPolicy = JScrollPane.HORIZONTAL_SCROLLBAR_AS_NEEDED - scrollPane.verticalScrollBarPolicy = JScrollPane.VERTICAL_SCROLLBAR_AS_NEEDED - return scrollPane - } - - @JvmStatic - fun showCheckboxDialog( - promptMessage: String, - checkboxIds: Array, - checkboxDescriptions: Array, - ): Array { - val formBuilder = FormBuilder.createFormBuilder() - val checkboxMap = HashMap() - for (i in checkboxIds.indices) { - val checkbox = JCheckBox(checkboxDescriptions[i], null as Icon?, true) - checkboxMap[checkboxIds[i]] = checkbox - formBuilder.addComponent(checkbox) - } - val dialogResult = showOptionDialog(formBuilder.panel, "OK", title = promptMessage) - val selectedIds = ArrayList() - if (dialogResult == 0) { - for ((checkboxId, checkbox) in checkboxMap) { - if (checkbox.isSelected) { - selectedIds.add(checkboxId) - } + "boolean" -> if (uiVal is JCheckBox) { + newSettingsValue = uiVal.isSelected + } else if (uiVal is JTextComponent) { + newSettingsValue = java.lang.Boolean.parseBoolean(uiVal.text) } - } - return selectedIds.toTypedArray() - } - @JvmStatic - fun showRadioButtonDialog( - promptMessage: CharSequence, - vararg radioButtonDescriptions: CharSequence, - ): CharSequence? { - val formBuilder = FormBuilder.createFormBuilder() - val radioButtonMap = HashMap() - val buttonGroup = ButtonGroup() - for (i in radioButtonDescriptions.indices) { - val radioButton = JRadioButton(radioButtonDescriptions[i].toString(), null as Icon?, true) - radioButtonMap[radioButtonDescriptions[i].toString()] = radioButton - buttonGroup.add(radioButton) - formBuilder.addComponent(radioButton) - } - val dialogResult = showOptionDialog(formBuilder.panel, "OK", title = promptMessage.toString()) - if (dialogResult == 0) { - for ((radioButtonId, radioButton) in radioButtonMap) { - if (radioButton.isSelected) { - return radioButtonId - } + else -> if (Enum::class.java.isAssignableFrom(settingsField.returnType.javaType as Class<*>)) { + if (uiVal is ComboBox<*>) { + val comboBox = uiVal + val item = comboBox.item + val enumClass = settingsField.returnType.javaType as Class?> + val string = item.toString() + newSettingsValue = findValue(enumClass, string) + } } + } + settingsField.setter.call(settings, newSettingsValue) + } catch (e: Throwable) { + throw RuntimeException("Error processing $settingsField", e) } - return null - } - - @JvmStatic - fun build( - component: T, - fillVertically: Boolean = true, - formBuilder: FormBuilder = FormBuilder.createFormBuilder(), - ): JPanel? { - addKotlinFields(component, formBuilder, fillVertically) - return formBuilder.addComponentFillVertically(JPanel(), 0).panel + } } + } - @JvmStatic - fun showDialog( - project: Project?, - uiClass: Class, - configClass: Class, - title: String = "Generate Project", - onComplete: (C) -> Unit = { _ -> }, - ): C? { - val component = uiClass.getConstructor().newInstance() - val config = configClass.getConstructor().newInstance() - val dialog = object : DialogWrapper(project) { - init { - this.init() - this.title = title - this.setOKButtonText("Generate") - this.setCancelButtonText("Cancel") - this.isResizable = true - //this.setPreferredFocusedComponent(this) - //this.setContent(this) - } - - override fun createCenterPanel(): JComponent? { - return build(component) - } - } - dialog.show() - if (dialog.isOK) { - readKotlinUI(component, config) - onComplete(config) + private fun findValue(enumClass: Class?>, string: String): Enum<*>? { + enumClass.enumConstants?.filter { it?.name?.compareTo(string, true) == 0 }?.forEach { return it } + return java.lang.Enum.valueOf( + enumClass, string + ) + } + + fun writeKotlinUIViaReflection(settings: T, component: R) { + val componentClass: Class<*> = component.javaClass + val declaredUIFields = componentClass.kotlin.memberProperties.map { it.name }.toSet() + val memberProperties = settings.javaClass.kotlin.memberProperties + val publicProperties = memberProperties.filter { + it.visibility == KVisibility.PUBLIC //&& it is KMutableProperty<*> + } + for (settingsField in publicProperties) { + val fieldName = settingsField.name + try { + if (!declaredUIFields.contains(fieldName)) continue + val uiField: KProperty1 = + (componentClass.kotlin.memberProperties.find { it.name == fieldName } as KProperty1?)!! + val settingsVal = settingsField.get(settings) ?: continue + var uiVal = uiField.get(component) + if (uiVal is JScrollPane) { + uiVal = uiVal.viewport.view + } + when (settingsField.returnType.javaType.typeName) { + "java.lang.String" -> if (uiVal is JTextComponent) { + uiVal.text = settingsVal.toString() + } else if (uiVal is ComboBox<*>) { + uiVal.item = settingsVal.toString() + } + + "int", "java.lang.Integer" -> if (uiVal is JTextComponent) { + uiVal.text = (settingsVal as Int).toString() + } + + "long" -> if (uiVal is JTextComponent) { + uiVal.text = (settingsVal as Int).toLong().toString() + } + + "boolean" -> if (uiVal is JCheckBox) { + uiVal.isSelected = (settingsVal as Boolean) + } else if (uiVal is JTextComponent) { + uiVal.text = java.lang.Boolean.toString((settingsVal as Boolean)) + } + + "double", "java.lang.Double" -> if (uiVal is JTextComponent) { + uiVal.text = (settingsVal as Double).toString() + } + + else -> if (uiVal is ComboBox<*>) { + uiVal.item = settingsVal.toString() + } + } + } catch (e: Throwable) { + throw RuntimeException("Error processing $settingsField", e) + } + } + } + + private fun addKotlinFields(ui: T, formBuilder: FormBuilder, fillVertically: Boolean) { + var first = true + for (field in ui.javaClass.kotlin.memberProperties) { + if (field.javaField == null) continue + try { + val nameAnnotation = field.annotations.find { it is Name } as Name? + val component = field.get(ui) as JComponent + if (nameAnnotation != null) { + if (first && fillVertically) { + first = false + formBuilder.addLabeledComponentFillVertically(nameAnnotation.value + ": ", component) + } else { + formBuilder.addLabeledComponent(JBLabel(nameAnnotation.value + ": "), component, 1, false) + } + } else { + formBuilder.addComponentToRightColumn(component, 1) + } + } catch (e: IllegalAccessException) { + throw RuntimeException(e) + } catch (e: Throwable) { + error(log, "Error processing " + field.name, e) + } + } + } + + private fun getMaximumSize(factor: Double): Dimension { + val screenSize = Toolkit.getDefaultToolkit().screenSize + return Dimension((screenSize.getWidth() * factor).toInt(), (screenSize.getHeight() * factor).toInt()) + } + + private fun showOptionDialog(mainPanel: JPanel?, vararg options: Any, title: String, modal: Boolean = true): Int { + val pane = getOptionPane(mainPanel, options) + val rootFrame = JOptionPane.getRootFrame() + pane.componentOrientation = rootFrame.componentOrientation + val dialog = JDialog(rootFrame, title, modal) + dialog.componentOrientation = rootFrame.componentOrientation + + val latch = if (!modal) CountDownLatch(1) else null + configure(dialog, pane, latch) + dialog.isVisible = true + if (!modal) latch?.await() + + dialog.dispose() + return getSelectedValue(pane, options) + } + + private fun getOptionPane( + mainPanel: JPanel?, + options: Array, + ): JOptionPane { + val pane = JOptionPane( + mainPanel, JOptionPane.PLAIN_MESSAGE, JOptionPane.NO_OPTION, null, options, options[0] + ) + pane.initialValue = options[0] + return pane + } + + private fun configure(dialog: JDialog, pane: JOptionPane, latch: CountDownLatch? = null) { + val contentPane = dialog.contentPane + contentPane.layout = BorderLayout() + contentPane.add(pane, BorderLayout.CENTER) + + if (JDialog.isDefaultLookAndFeelDecorated() && UIManager.getLookAndFeel().supportsWindowDecorations) { + dialog.isUndecorated = true + pane.rootPane.windowDecorationStyle = JRootPane.PLAIN_DIALOG + } + dialog.isResizable = true + dialog.maximumSize = getMaximumSize(0.9) + dialog.pack() + dialog.setLocationRelativeTo(null as Component?) + val adapter: WindowAdapter = windowAdapter(pane, dialog) + dialog.addWindowListener(adapter) + dialog.addWindowFocusListener(adapter) + dialog.addComponentListener(object : ComponentAdapter() { + override fun componentShown(ce: ComponentEvent) { + // reset value to ensure closing works properly + pane.value = JOptionPane.UNINITIALIZED_VALUE + } + }) + pane.addPropertyChangeListener { event: PropertyChangeEvent -> + if (dialog.isVisible && event.source === pane && event.propertyName == JOptionPane.VALUE_PROPERTY && event.newValue != null && event.newValue !== JOptionPane.UNINITIALIZED_VALUE) { + dialog.isVisible = false + latch?.countDown() + } + } + + pane.selectInitialValue() + } + + private fun windowAdapter(pane: JOptionPane, dialog: JDialog): WindowAdapter { + val adapter: WindowAdapter = object : WindowAdapter() { + private var gotFocus = false + override fun windowClosing(we: WindowEvent) { + pane.value = null + } + + override fun windowClosed(e: WindowEvent) { + pane.removePropertyChangeListener { event: PropertyChangeEvent -> + if (dialog.isVisible && event.source === pane && event.propertyName == JOptionPane.VALUE_PROPERTY && event.newValue != null && event.newValue !== JOptionPane.UNINITIALIZED_VALUE) { + dialog.isVisible = false + } + } + dialog.contentPane.removeAll() + } + + override fun windowGainedFocus(we: WindowEvent) { + if (!gotFocus) { + pane.selectInitialValue() + gotFocus = true + } + } + } + return adapter + } + + private fun getSelectedValue(pane: JOptionPane, options: Array): Int { + val selectedValue = pane.value ?: return JOptionPane.CLOSED_OPTION + var counter = 0 + val maxCounter = options.size + while (counter < maxCounter) { + if (options[counter] == selectedValue) return counter + counter++ + } + return JOptionPane.CLOSED_OPTION + } + + private fun wrapScrollPane(promptArea: JBTextArea?): JBScrollPane { + val scrollPane = JBScrollPane(promptArea) + scrollPane.horizontalScrollBarPolicy = JScrollPane.HORIZONTAL_SCROLLBAR_AS_NEEDED + scrollPane.verticalScrollBarPolicy = JScrollPane.VERTICAL_SCROLLBAR_AS_NEEDED + return scrollPane + } + + fun showCheckboxDialog( + promptMessage: String, + checkboxIds: Array, + checkboxDescriptions: Array, + ): Array { + val formBuilder = FormBuilder.createFormBuilder() + val checkboxMap = HashMap() + for (i in checkboxIds.indices) { + val checkbox = JCheckBox(checkboxDescriptions[i], null as Icon?, true) + checkboxMap[checkboxIds[i]] = checkbox + formBuilder.addComponent(checkbox) + } + val dialogResult = showOptionDialog(formBuilder.panel, "OK", title = promptMessage) + val selectedIds = ArrayList() + if (dialogResult == 0) { + for ((checkboxId, checkbox) in checkboxMap) { + if (checkbox.isSelected) { + selectedIds.add(checkboxId) + } + } + } + return selectedIds.toTypedArray() + } + + fun showRadioButtonDialog( + promptMessage: CharSequence, + vararg radioButtonDescriptions: CharSequence, + ): CharSequence? { + val formBuilder = FormBuilder.createFormBuilder() + val radioButtonMap = HashMap() + val buttonGroup = ButtonGroup() + for (i in radioButtonDescriptions.indices) { + val radioButton = JRadioButton(radioButtonDescriptions[i].toString(), null as Icon?, true) + radioButtonMap[radioButtonDescriptions[i].toString()] = radioButton + buttonGroup.add(radioButton) + formBuilder.addComponent(radioButton) + } + val dialogResult = showOptionDialog(formBuilder.panel, "OK", title = promptMessage.toString()) + if (dialogResult == 0) { + for ((radioButtonId, radioButton) in radioButtonMap) { + if (radioButton.isSelected) { + return radioButtonId + } + } + } + return null + } + + fun buildFormViaReflection( + component: T, + fillVertically: Boolean = true, + formBuilder: FormBuilder = FormBuilder.createFormBuilder(), + ): JPanel? { + addKotlinFields(component, formBuilder, fillVertically) + return formBuilder.addComponentFillVertically(JPanel(), 0).panel + } + + fun showDialog( + project: Project?, + uiClass: Class, + configClass: Class, + title: String = "Generate Project", + onComplete: (C) -> Unit = { _ -> }, + ): C? { + val component = uiClass.getConstructor().newInstance() + val config = configClass.getConstructor().newInstance() + val dialog = object : DialogWrapper(project) { + init { + this.init() + this.title = title + this.setOKButtonText("Generate") + this.setCancelButtonText("Cancel") + this.isResizable = true + //this.setPreferredFocusedComponent(this) + //this.setContent(this) + } + + override fun createCenterPanel(): JComponent? { + return buildFormViaReflection(component) + } + } + dialog.show() + if (dialog.isOK) { + readKotlinUIViaReflection(component, config) + onComplete(config) + } + return config + } + + fun getSelectedFolder(e: AnActionEvent): VirtualFile? { + val dataContext = e.dataContext + val data = PlatformDataKeys.VIRTUAL_FILE.getData(dataContext) + if (data != null && data.isDirectory) { + return data + } + val editor = PlatformDataKeys.EDITOR.getData(dataContext) + if (editor != null) { + val file = FileDocumentManager.getInstance().getFile(editor.document) + if (file != null) { + return file.parent + } + } + return null + } + + fun getSelectedFile(e: AnActionEvent): VirtualFile? { + val dataContext = e.dataContext + val data = PlatformDataKeys.VIRTUAL_FILE.getData(dataContext) + if (data != null && !data.isDirectory) { + return data + } + val editor = PlatformDataKeys.EDITOR.getData(dataContext) + if (editor != null) { + val file = FileDocumentManager.getInstance().getFile(editor.document) + if (file != null) { + return file + } + } + return null + } + + fun writeableFn( + event: AnActionEvent, + fn: () -> Runnable, + ): Runnable { + val runnable = AtomicReference() + WriteCommandAction.runWriteCommandAction(event.project) { runnable.set(fn()) } + return runnable.get() + } + + class ModalTask( + project: Project, title: String, canBeCancelled: Boolean, val task: (ProgressIndicator) -> T + ) : Task.WithResult(project, title, canBeCancelled), Supplier { + private val result = AtomicReference() + private val isError = AtomicBoolean(false) + private val error = AtomicReference() + private val semaphore = Semaphore(0) + override fun compute(indicator: ProgressIndicator): T? { + val currentThread = Thread.currentThread() + val threads = ArrayList() + val scheduledFuture = scheduledPool.scheduleAtFixedRate({ + if (indicator.isCanceled) { + threads.forEach { it.interrupt() } + } + }, 0, 1, TimeUnit.SECONDS) + threads.add(currentThread) + return try { + result.set(task(indicator)) + result.get() + } catch (e: Throwable) { + error(log, "Error running task", e) + isError.set(true) + error.set(e) + null + } finally { + semaphore.release() + threads.remove(currentThread) + scheduledFuture.cancel(true) + } + } + + override fun get(): T { + semaphore.acquire() + semaphore.release() + if (isError.get()) { + throw error.get() + } + return result.get() + } + + } + + class BgTask( + project: Project, title: String, canBeCancelled: Boolean, val task: (ProgressIndicator) -> T + ) : Task.Backgroundable(project, title, canBeCancelled, DEAF), Supplier { + + private val result = AtomicReference() + private val isError = AtomicBoolean(false) + private val error = AtomicReference() + private val semaphore = Semaphore(0) + override fun run(indicator: ProgressIndicator) { + val currentThread = Thread.currentThread() + val threads = ArrayList() + val scheduledFuture = scheduledPool.scheduleAtFixedRate({ + if (indicator.isCanceled) { + threads.forEach { it.interrupt() } + } + }, 0, 1, TimeUnit.SECONDS) + threads.add(currentThread) + try { + val result = task(indicator) + this.result.set(result) + } catch (e: Throwable) { + error(log, "Error running task", e) + error.set(e) + isError.set(true) + } finally { + semaphore.release() + threads.remove(currentThread) + scheduledFuture.cancel(true) + } + } + + override fun get(): T { + semaphore.acquire() + semaphore.release() + if (isError.get()) { + throw error.get() + } + return result.get() + } + } + + fun run( + project: Project?, + title: String?, + canBeCancelled: Boolean = true, + suppressProgress: Boolean = true, + task: (ProgressIndicator) -> T, + ): T { + return if (project == null || suppressProgress == AppSettingsState.instance.editRequests) { + checkApiKey() + task(AbstractProgressIndicatorBase()) + } else { + checkApiKey() + val t = if (AppSettingsState.instance.modalTasks) ModalTask(project, title ?: "", canBeCancelled, task) + else BgTask(project, title ?: "", canBeCancelled, task) + ProgressManager.getInstance().run(t) + t.get() + } + } + + fun checkApiKey(k: String = AppSettingsState.instance.apiKey): String { + var key = k + if (key.isEmpty()) { + synchronized(OpenAIClient::class.java) { + key = AppSettingsState.instance.apiKey + if (key.isEmpty()) { + key = queryAPIKey()?.toString() ?: "" + if (key.isNotEmpty()) AppSettingsState.instance.apiKey = key } - return config + } } + return key + } - @JvmStatic - fun getSelectedFolder(e: AnActionEvent): VirtualFile? { - val dataContext = e.dataContext - val data = PlatformDataKeys.VIRTUAL_FILE.getData(dataContext) - if (data != null && data.isDirectory) { - return data - } - val editor = PlatformDataKeys.EDITOR.getData(dataContext) - if (editor != null) { - val file = FileDocumentManager.getInstance().getFile(editor.document) - if (file != null) { - return file.parent - } - } - return null - } - @JvmStatic - fun getSelectedFile(e: AnActionEvent): VirtualFile? { - val dataContext = e.dataContext - val data = PlatformDataKeys.VIRTUAL_FILE.getData(dataContext) - if (data != null && !data.isDirectory) { - return data - } - val editor = PlatformDataKeys.EDITOR.getData(dataContext) - if (editor != null) { - val file = FileDocumentManager.getInstance().getFile(editor.document) - if (file != null) { - return file - } - } - return null - } + fun map( + moderateAsync: ListenableFuture, + o: com.google.common.base.Function, + ): ListenableFuture = Futures.transform(moderateAsync, o::apply, pool) - @JvmStatic - fun writeableFn( - event: AnActionEvent, - fn: () -> Runnable, - ): Runnable { - val runnable = AtomicReference() - WriteCommandAction.runWriteCommandAction(event.project) { runnable.set(fn()) } - return runnable.get() + fun filterStringResult( + indent: CharSequence = "", + stripUnbalancedTerminators: Boolean = true, + ): (CharSequence) -> CharSequence { + return { text -> + var result: CharSequence = text.toString().trim { it <= ' ' } + if (stripUnbalancedTerminators) { + result = StringUtil.stripUnbalancedTerminators(result) + } + result = IndentedText.fromString(result.toString()).withIndent(indent).toString() + indent.toString() + result } + } - class ModalTask( - project: Project, title: String, canBeCancelled: Boolean, val task: (ProgressIndicator) -> T - ) : Task.WithResult(project, title, canBeCancelled), Supplier { - private val result = AtomicReference() - private val isError = AtomicBoolean(false) - private val error = AtomicReference() - private val semaphore = Semaphore(0) - override fun compute(indicator: ProgressIndicator): T? { - val currentThread = Thread.currentThread() - val threads = ArrayList() - val scheduledFuture = scheduledPool.scheduleAtFixedRate({ - if (indicator.isCanceled) { - threads.forEach { it.interrupt() } - } - }, 0, 1, TimeUnit.SECONDS) - threads.add(currentThread) - return try { - result.set(task(indicator)) - result.get() - } catch (e: Throwable) { - error(log, "Error running task", e) - isError.set(true) - error.set(e) - null - } finally { - semaphore.release() - threads.remove(currentThread) - scheduledFuture.cancel(true) - } - } - override fun get(): T { - semaphore.acquire() - semaphore.release() - if (isError.get()) { - throw error.get() - } - return result.get() - } - - } + fun logAction(message: String) { + actionLog += message + } - class BgTask( - project: Project, title: String, canBeCancelled: Boolean, val task: (ProgressIndicator) -> T - ) : Task.Backgroundable(project, title, canBeCancelled, DEAF), Supplier { - - private val result = AtomicReference() - private val isError = AtomicBoolean(false) - private val error = AtomicReference() - private val semaphore = Semaphore(0) - override fun run(indicator: ProgressIndicator) { - val currentThread = Thread.currentThread() - val threads = ArrayList() - val scheduledFuture = scheduledPool.scheduleAtFixedRate({ - if (indicator.isCanceled) { - threads.forEach { it.interrupt() } - } - }, 0, 1, TimeUnit.SECONDS) - threads.add(currentThread) - try { - val result = task(indicator) - this.result.set(result) - } catch (e: Throwable) { - error(log, "Error running task", e) - error.set(e) - isError.set(true) - } finally { - semaphore.release() - threads.remove(currentThread) - scheduledFuture.cancel(true) - } - } - - override fun get(): T { - semaphore.acquire() - semaphore.release() - if (isError.get()) { - throw error.get() - } - return result.get() - } - } - @JvmStatic - fun run( - project: Project?, - title: String?, - canBeCancelled: Boolean = true, - suppressProgress: Boolean = true, - task: (ProgressIndicator) -> T, - ): T { - return if (project == null || suppressProgress == AppSettingsState.instance.editRequests) { - checkApiKey() - task(AbstractProgressIndicatorBase()) - } else { - checkApiKey() - val t = if (AppSettingsState.instance.modalTasks) ModalTask(project, title ?: "", canBeCancelled, task) - else BgTask(project, title ?: "", canBeCancelled, task) - ProgressManager.getInstance().run(t) - t.get() - } - } + fun error(log: org.slf4j.Logger, msg: String, e: Throwable) { + log?.error(msg, e) + errorLog += Pair(msg, e) + singleThreadPool.submit { + if (AppSettingsState.instance.suppressErrors) { + return@submit + } else if (e.matches { ModerationException::class.java.isAssignableFrom(it.javaClass) }) { + JOptionPane.showMessageDialog( + null, e.message, "This request was rejected by OpenAI Moderation", JOptionPane.WARNING_MESSAGE + ) + } else if (e.matches { + java.lang.InterruptedException::class.java.isAssignableFrom(it.javaClass) && it.message?.contains( + "sleep interrupted" + ) == true + }) { + JOptionPane.showMessageDialog( + null, "This request was cancelled by the user", "User Cancelled Request", JOptionPane.WARNING_MESSAGE + ) + } else if (e.matches { IOException::class.java.isAssignableFrom(it.javaClass) && it.message?.contains("Incorrect API key") == true }) { - @JvmStatic - fun checkApiKey(k: String = AppSettingsState.instance.apiKey): String { - var key = k - if (key.isEmpty()) { - synchronized(OpenAIClient::class.java) { - key = AppSettingsState.instance.apiKey - if (key.isEmpty()) { - key = queryAPIKey()!!.toString() - AppSettingsState.instance.apiKey = key - } - } - } - return key - } + val formBuilder = FormBuilder.createFormBuilder() - @JvmStatic - val threadFactory: ThreadFactory = ThreadFactoryBuilder().setNameFormat("API Thread %d").build() - - @JvmStatic - val pool: ListeningExecutorService = MoreExecutors.listeningDecorator( - ThreadPoolExecutor( - /* corePoolSize = */ AppSettingsState.instance.apiThreads, - /* maximumPoolSize = */ AppSettingsState.instance.apiThreads, - /* keepAliveTime = */ 0L, /* unit = */ TimeUnit.MILLISECONDS, - /* workQueue = */ LinkedBlockingQueue(), - /* threadFactory = */ threadFactory, - /* handler = */ ThreadPoolExecutor.AbortPolicy() + formBuilder.addLabeledComponent( + "Error", JLabel("The API key was rejected by the server.") ) - ) - @JvmStatic - val scheduledPool: ListeningScheduledExecutorService = - MoreExecutors.listeningDecorator(ScheduledThreadPoolExecutor(1, threadFactory)) - - @JvmStatic - fun map( - moderateAsync: ListenableFuture, - o: com.google.common.base.Function, - ): ListenableFuture = Futures.transform(moderateAsync, o::apply, pool) - - @JvmStatic - fun filterStringResult( - indent: CharSequence = "", - stripUnbalancedTerminators: Boolean = true, - ): (CharSequence) -> CharSequence { - return { text -> - var result: CharSequence = text.toString().trim { it <= ' ' } - if (stripUnbalancedTerminators) { - result = StringUtil.stripUnbalancedTerminators(result) - } - result = IndentedText.fromString(result.toString()).withIndent(indent).toString() - indent.toString() + result + val apiKeyInput = JBPasswordField() + //bugReportTextArea.rows = 40 + apiKeyInput.columns = 80 + apiKeyInput.isEditable = true + //apiKeyInput.text = """""".trimMargin() + formBuilder.addLabeledComponent("API Key", apiKeyInput) + + val openAccountButton = JXButton("Open Account Page") + openAccountButton.addActionListener { + Desktop.getDesktop().browse(URI("https://platform.openai.com/account/api-keys")) + } + formBuilder.addLabeledComponent("OpenAI Account", openAccountButton) + + val testButton = JXButton("Test Key") + testButton.addActionListener { + val apiKey = apiKeyInput.password.joinToString("") + try { + OpenAIClient(key = apiKey).listModels() + JOptionPane.showMessageDialog( + null, + "The API key was accepted by the server. The new value will be saved.", + "Success", + JOptionPane.INFORMATION_MESSAGE + ) + AppSettingsState.instance.apiKey = apiKey + } catch (e: Exception) { + JOptionPane.showMessageDialog( + null, "The API key was rejected by the server.", "Failure", JOptionPane.WARNING_MESSAGE + ) + return@addActionListener + } } - } - - private val errorLog = mutableListOf>() - private val actionLog = mutableListOf() + formBuilder.addLabeledComponent("Validation", testButton) + val showOptionDialog = showOptionDialog( + formBuilder.panel, "Dismiss", title = "Error", modal = true + ) + log.info("showOptionDialog = $showOptionDialog") + } else if (e.matches { ScriptException::class.java.isAssignableFrom(it.javaClass) }) { + val scriptException = e.get { ScriptException::class.java.isAssignableFrom(it.javaClass) } as ScriptException? + val dynamicActionException = + e.get { ActionSettingsRegistry.DynamicActionException::class.java.isAssignableFrom(it.javaClass) } as ActionSettingsRegistry.DynamicActionException? + val formBuilder = FormBuilder.createFormBuilder() - @JvmStatic - fun logAction(message: String) { - actionLog += message - } + formBuilder.addLabeledComponent( + "Error", JLabel("An error occurred while executing the dynamic action.") + ) - private val singleThreadPool = Executors.newSingleThreadExecutor() - - @JvmStatic - fun error(log: org.slf4j.Logger, msg: String, e: Throwable) { - log?.error(msg, e) - errorLog += Pair(msg, e) - singleThreadPool.submit { - if (AppSettingsState.instance.suppressErrors) { - return@submit - } else if (e.matches { ModerationException::class.java.isAssignableFrom(it.javaClass) }) { - JOptionPane.showMessageDialog( - null, - e.message, - "This request was rejected by OpenAI Moderation", - JOptionPane.WARNING_MESSAGE - ) - } else if (e.matches { - java.lang.InterruptedException::class.java.isAssignableFrom(it.javaClass) && it.message?.contains( - "sleep interrupted" - ) == true - }) { - JOptionPane.showMessageDialog( - null, - "This request was cancelled by the user", - "User Cancelled Request", - JOptionPane.WARNING_MESSAGE - ) - } else if (e.matches { IOException::class.java.isAssignableFrom(it.javaClass) && it.message?.contains("Incorrect API key") == true }) { - - val formBuilder = FormBuilder.createFormBuilder() - - formBuilder.addLabeledComponent( - "Error", - JLabel("The API key was rejected by the server.") - ) - - val apiKeyInput = JBPasswordField() - //bugReportTextArea.rows = 40 - apiKeyInput.columns = 80 - apiKeyInput.isEditable = true - //apiKeyInput.text = """""".trimMargin() - formBuilder.addLabeledComponent("API Key", apiKeyInput) - - val openAccountButton = JXButton("Open Account Page") - openAccountButton.addActionListener { - Desktop.getDesktop().browse(URI("https://platform.openai.com/account/api-keys")) - } - formBuilder.addLabeledComponent("OpenAI Account", openAccountButton) - - val testButton = JXButton("Test Key") - testButton.addActionListener { - val apiKey = apiKeyInput.password.joinToString("") - try { - OpenAIClient(key = apiKey).listModels() - JOptionPane.showMessageDialog( - null, - "The API key was accepted by the server. The new value will be saved.", - "Success", - JOptionPane.INFORMATION_MESSAGE - ) - AppSettingsState.instance.apiKey = apiKey - } catch (e: Exception) { - JOptionPane.showMessageDialog( - null, - "The API key was rejected by the server.", - "Failure", - JOptionPane.WARNING_MESSAGE - ) - return@addActionListener - } - } - formBuilder.addLabeledComponent("Validation", testButton) - val showOptionDialog = showOptionDialog( - formBuilder.panel, - "Dismiss", - title = "Error", - modal = true - ) - log.info("showOptionDialog = $showOptionDialog") - } else if (e.matches { ScriptException::class.java.isAssignableFrom(it.javaClass) }) { - val scriptException = - e.get { ScriptException::class.java.isAssignableFrom(it.javaClass) } as ScriptException? - val dynamicActionException = - e.get { ActionSettingsRegistry.DynamicActionException::class.java.isAssignableFrom(it.javaClass) } as ActionSettingsRegistry.DynamicActionException? - val formBuilder = FormBuilder.createFormBuilder() - - formBuilder.addLabeledComponent( - "Error", - JLabel("An error occurred while executing the dynamic action.") - ) - - val bugReportTextArea = JBTextArea() - bugReportTextArea.rows = 40 - bugReportTextArea.columns = 80 - bugReportTextArea.isEditable = false - bugReportTextArea.text = """ + val bugReportTextArea = JBTextArea() + bugReportTextArea.rows = 40 + bugReportTextArea.columns = 80 + bugReportTextArea.isEditable = false + bugReportTextArea.text = """ |Action Name: ${dynamicActionException?.actionSetting?.displayText} |Action ID: ${dynamicActionException?.actionSetting?.id} |Script Error: ${scriptException?.message} @@ -923,180 +853,158 @@ object UITools { |${toString(e)} |``` |""".trimMargin() - formBuilder.addLabeledComponent("Error Report", wrapScrollPane(bugReportTextArea)) - - if (dynamicActionException?.actionSetting?.isDynamic == false) { - val openButton = JXButton("Revert to Default") - openButton.addActionListener { - dynamicActionException?.actionSetting?.file?.delete() - } - formBuilder.addLabeledComponent("Revert Built-in Action", openButton) + formBuilder.addLabeledComponent("Error Report", wrapScrollPane(bugReportTextArea)) + + if (dynamicActionException?.actionSetting?.isDynamic == false) { + val openButton = JXButton("Revert to Default") + openButton.addActionListener { + dynamicActionException?.actionSetting?.file?.delete() + } + formBuilder.addLabeledComponent("Revert Built-in Action", openButton) + } + + if (null != dynamicActionException) { + val openButton = JXButton("Open Dynamic Action") + openButton.addActionListener { + dynamicActionException?.file?.let { + val project = ApplicationManager.getApplication().runReadAction { + com.intellij.openapi.project.ProjectManager.getInstance().openProjects.firstOrNull() + } + if (it.exists()) { + ApplicationManager.getApplication().invokeLater { + val virtualFile = LocalFileSystem.getInstance().refreshAndFindFileByIoFile(it) + FileEditorManager.getInstance(project!!).openFile(virtualFile!!, true) } + } else { + Thread { + showOptionDialog( + formBuilder.panel, "Dismiss", title = "Error - File Not Found", modal = true + ) + }.start() + } + } - if (null != dynamicActionException) { - val openButton = JXButton("Open Dynamic Action") - openButton.addActionListener { - dynamicActionException?.file?.let { - val project = ApplicationManager.getApplication().runReadAction { - com.intellij.openapi.project.ProjectManager.getInstance().openProjects.firstOrNull() - } - if (it.exists()) { - ApplicationManager.getApplication().invokeLater { - val virtualFile = LocalFileSystem.getInstance().refreshAndFindFileByIoFile(it) - FileEditorManager.getInstance(project!!).openFile(virtualFile!!, true) - } - } else { - Thread { - showOptionDialog( - formBuilder.panel, - "Dismiss", - title = "Error - File Not Found", - modal = true - ) - }.start() - } - } - - } - formBuilder.addLabeledComponent("View Code", openButton) - } + } + formBuilder.addLabeledComponent("View Code", openButton) + } - val supressFutureErrors = JCheckBox("Suppress Future Error Popups") - supressFutureErrors.isSelected = false - formBuilder.addComponent(supressFutureErrors) - - val showOptionDialog = showOptionDialog( - formBuilder.panel, - "Dismiss", - title = "Error", - modal = true - ) - log.info("showOptionDialog = $showOptionDialog") - if (supressFutureErrors.isSelected) { - AppSettingsState.instance.suppressErrors = true - } - } else { - val formBuilder = FormBuilder.createFormBuilder() - - formBuilder.addLabeledComponent( - "Error", - JLabel("Oops! Something went wrong. An error report has been generated. You can copy and paste the report below into a new issue on our Github page.") - ) - - val bugReportTextArea = JBTextArea() - bugReportTextArea.rows = 40 - bugReportTextArea.columns = 80 - bugReportTextArea.isEditable = false - bugReportTextArea.text = """ - |Log Message: $msg - |Error Message: ${e.message} - |Error Type: ${e.javaClass.name} - |API Base: ${AppSettingsState.instance.apiBase} - |Token Counter: ${AppSettingsState.instance.tokenCounter} - | - |OS: ${System.getProperty("os.name")} / ${System.getProperty("os.version")} / ${System.getProperty("os.arch")} - |Locale: ${Locale.getDefault().country} / ${Locale.getDefault().language} - | - |Error Details: - |``` - |${toString(e)} - |``` - | - |Action History: - | - |${actionLog.joinToString("\n") { "* ${it.replace("\n", "\n ")}" }} - | - |Error History: - | - |${ - errorLog.filter { it.second != e }.joinToString("\n") { - """ - |${it.first} - |``` - |${toString(it.second)} - |``` - |""".trimMargin() - } - } - |""".trimMargin() - formBuilder.addLabeledComponent("System Report", wrapScrollPane(bugReportTextArea)) + val supressFutureErrors = JCheckBox("Suppress Future Error Popups") + supressFutureErrors.isSelected = false + formBuilder.addComponent(supressFutureErrors) - val openButton = JXButton("Open New Issue on our Github page") - openButton.addActionListener { - Desktop.getDesktop().browse(URI("https://github.com/SimiaCryptus/intellij-aicoder/issues/new")) - } - formBuilder.addLabeledComponent("Report Issue/Request Help", openButton) - - val supressFutureErrors = JCheckBox("Suppress Future Error Popups") - supressFutureErrors.isSelected = false - formBuilder.addComponent(supressFutureErrors) - - val showOptionDialog = showOptionDialog( - formBuilder.panel, - "Dismiss", - title = "Error", - modal = true - ) - log.info("showOptionDialog = $showOptionDialog") - if (supressFutureErrors.isSelected) { - AppSettingsState.instance.suppressErrors = true - } - } + val showOptionDialog = showOptionDialog( + formBuilder.panel, "Dismiss", title = "Error", modal = true + ) + log.info("showOptionDialog = $showOptionDialog") + if (supressFutureErrors.isSelected) { + AppSettingsState.instance.suppressErrors = true } - } + } else { + val formBuilder = FormBuilder.createFormBuilder() - @JvmStatic - fun Throwable.matches(matchFn: (Throwable) -> Boolean): Boolean { - if (matchFn(this)) return true - if (this.cause != null && this.cause !== this) return this.cause!!.matches(matchFn) - return false - } + formBuilder.addLabeledComponent( + "Error", + JLabel("Oops! Something went wrong. An error report has been generated. You can copy and paste the report below into a new issue on our Github page.") + ) - @JvmStatic - fun Throwable.get(matchFn: (Throwable) -> Boolean): Throwable? { - if (matchFn(this)) return this - if (this.cause != null && this.cause !== this) return this.cause!!.get(matchFn) - return null - } + val bugReportTextArea = JBTextArea() + bugReportTextArea.rows = 40 + bugReportTextArea.columns = 80 + bugReportTextArea.isEditable = false + bugReportTextArea.text = """ + |Log Message: $msg + |Error Message: ${e.message} + |Error Type: ${e.javaClass.name} + |API Base: ${AppSettingsState.instance.apiBase} + |Token Counter: ${AppSettingsState.instance.tokenCounter} + | + |OS: ${System.getProperty("os.name")} / ${System.getProperty("os.version")} / ${System.getProperty("os.arch")} + |Locale: ${Locale.getDefault().country} / ${Locale.getDefault().language} + | + |Error Details: + |``` + |${toString(e)} + |``` + | + |Action History: + | + |${actionLog.joinToString("\n") { "* ${it.replace("\n", "\n ")}" }} + | + |Error History: + | + |${ + errorLog.filter { it.second != e }.joinToString("\n") { + """ + |${it.first} + |``` + |${toString(it.second)} + |``` + |""".trimMargin() + } + } + |""".trimMargin() + formBuilder.addLabeledComponent("System Report", wrapScrollPane(bugReportTextArea)) - @JvmStatic - fun toString(e: Throwable): String { - val sw = StringWriter() - val pw = PrintWriter(sw) - e.printStackTrace(pw) - return sw.toString() - } + val openButton = JXButton("Open New Issue on our Github page") + openButton.addActionListener { + Desktop.getDesktop().browse(URI("https://github.com/SimiaCryptus/intellij-aicoder/issues/new")) + } + formBuilder.addLabeledComponent("Report Issue/Request Help", openButton) + + val supressFutureErrors = JCheckBox("Suppress Future Error Popups") + supressFutureErrors.isSelected = false + formBuilder.addComponent(supressFutureErrors) - // Wrap JOptionPane.showInputDialog - @JvmStatic - fun showInputDialog( - parentComponent: Component?, - message: Any?, - title: String?, - messageType: Int - ): Any? { - val icon = null - val selectionValues = null - val initialSelectionValue = null - val pane = JOptionPane( - message, - messageType, - JOptionPane.OK_CANCEL_OPTION, - icon, - null, - null + val showOptionDialog = showOptionDialog( + formBuilder.panel, "Dismiss", title = "Error", modal = true ) - pane.wantsInput = true - pane.selectionValues = selectionValues - pane.initialSelectionValue = initialSelectionValue - //pane.isComponentOrientationLeftToRight = true - val dialog = pane.createDialog(parentComponent, title) - pane.selectInitialValue() - dialog.show() - dialog.dispose() - val value = pane.inputValue - return if (value == JOptionPane.UNINITIALIZED_VALUE) null else value - } + log.info("showOptionDialog = $showOptionDialog") + if (supressFutureErrors.isSelected) { + AppSettingsState.instance.suppressErrors = true + } + } + } + } + + private fun Throwable.matches(matchFn: (Throwable) -> Boolean): Boolean { + if (matchFn(this)) return true + if (this.cause != null && this.cause !== this) return this.cause!!.matches(matchFn) + return false + } + + fun Throwable.get(matchFn: (Throwable) -> Boolean): Throwable? { + if (matchFn(this)) return this + if (this.cause != null && this.cause !== this) return this.cause!!.get(matchFn) + return null + } + + fun toString(e: Throwable): String { + val sw = StringWriter() + val pw = PrintWriter(sw) + e.printStackTrace(pw) + return sw.toString() + } + + fun showInputDialog( + parentComponent: Component?, message: Any?, title: String?, messageType: Int + ): Any? { + val icon = null + val selectionValues = null + val initialSelectionValue = null + val pane = JOptionPane( + message, messageType, JOptionPane.OK_CANCEL_OPTION, icon, null, null + ) + pane.wantsInput = true + pane.selectionValues = selectionValues + pane.initialSelectionValue = initialSelectionValue + //pane.isComponentOrientationLeftToRight = true + val dialog = pane.createDialog(parentComponent, title) + pane.selectInitialValue() + dialog.show() + dialog.dispose() + val value = pane.inputValue + return if (value == JOptionPane.UNINITIALIZED_VALUE) null else value + } } diff --git a/src/main/resources/META-INF/plugin.xml b/src/main/resources/META-INF/plugin.xml index d05ee22c..f632f5ba 100644 --- a/src/main/resources/META-INF/plugin.xml +++ b/src/main/resources/META-INF/plugin.xml @@ -11,7 +11,7 @@ diff --git a/src/main/resources/sources/kt/com/github/simiacryptus/aicoder/actions/SelectionAction.kt b/src/main/resources/sources/kt/com/github/simiacryptus/aicoder/actions/SelectionAction.kt index 5b51e94a..f554704b 100644 --- a/src/main/resources/sources/kt/com/github/simiacryptus/aicoder/actions/SelectionAction.kt +++ b/src/main/resources/sources/kt/com/github/simiacryptus/aicoder/actions/SelectionAction.kt @@ -53,9 +53,10 @@ abstract class SelectionAction( var selectedText = primaryCaret.selectedText val editorState = editorState(editor) val (start, end) = retarget(editorState, selectedText, selectionStart, selectionEnd) ?: return - selectedText = editorState.text.substring(start, end) - selectionEnd = end - selectionStart = start + val text = editorState.text + selectedText = text.substring(start.coerceIn(0, text.length), end.coerceIn(0, text.length)) + selectionEnd = end.coerceIn(0, text.length) + selectionStart = start.coerceIn(0, text.length) UITools.redoableTask(e) { val document = e.getData(CommonDataKeys.EDITOR)?.document diff --git a/src/main/resources/sources/kt/com/github/simiacryptus/aicoder/actions/code/PasteAction.kt b/src/main/resources/sources/kt/com/github/simiacryptus/aicoder/actions/code/PasteAction.kt index 4ba3108b..1c06ceb1 100644 --- a/src/main/resources/sources/kt/com/github/simiacryptus/aicoder/actions/code/PasteAction.kt +++ b/src/main/resources/sources/kt/com/github/simiacryptus/aicoder/actions/code/PasteAction.kt @@ -9,7 +9,7 @@ import com.simiacryptus.jopenai.proxy.ChatProxy import java.awt.Toolkit import java.awt.datatransfer.DataFlavor -class PasteAction : SelectionAction(false) { +open class PasteAction : SelectionAction(false) { interface VirtualAPI { fun convert(text: String, from_language: String, to_language: String): ConvertedText diff --git a/src/main/resources/sources/kt/com/github/simiacryptus/aicoder/actions/code/RenameVariablesAction.kt b/src/main/resources/sources/kt/com/github/simiacryptus/aicoder/actions/code/RenameVariablesAction.kt index e393e513..db2f638d 100644 --- a/src/main/resources/sources/kt/com/github/simiacryptus/aicoder/actions/code/RenameVariablesAction.kt +++ b/src/main/resources/sources/kt/com/github/simiacryptus/aicoder/actions/code/RenameVariablesAction.kt @@ -8,7 +8,7 @@ import com.intellij.openapi.actionSystem.AnActionEvent import com.intellij.openapi.project.Project import com.simiacryptus.jopenai.proxy.ChatProxy -class RenameVariablesAction : SelectionAction() { +open class RenameVariablesAction : SelectionAction() { interface RenameAPI { fun suggestRenames( @@ -64,7 +64,7 @@ class RenameVariablesAction : SelectionAction() { } } - private fun choose(renameSuggestions: Map): Set { + open fun choose(renameSuggestions: Map): Set { return UITools.showCheckboxDialog( "Select which items to rename", renameSuggestions.keys.toTypedArray(), diff --git a/src/main/resources/sources/kt/com/github/simiacryptus/aicoder/actions/generic/AppendAction.kt b/src/main/resources/sources/kt/com/github/simiacryptus/aicoder/actions/generic/AppendAction.kt index 7899c1fd..2f25b9a5 100644 --- a/src/main/resources/sources/kt/com/github/simiacryptus/aicoder/actions/generic/AppendAction.kt +++ b/src/main/resources/sources/kt/com/github/simiacryptus/aicoder/actions/generic/AppendAction.kt @@ -3,8 +3,7 @@ import com.github.simiacryptus.aicoder.actions.SelectionAction import com.github.simiacryptus.aicoder.config.AppSettingsState import com.intellij.openapi.project.Project -import com.simiacryptus.jopenai.ApiModel.ChatMessage -import com.simiacryptus.jopenai.ApiModel.Role +import com.simiacryptus.jopenai.ApiModel.* import com.simiacryptus.jopenai.ClientUtil.toContentList class AppendAction : SelectionAction() { @@ -13,17 +12,20 @@ import com.simiacryptus.jopenai.ClientUtil.toContentList } override fun processSelection(state: SelectionState, config: String?): String { - val settings = AppSettingsState.instance - val request = settings.createChatRequest().copy( - temperature = settings.temperature, - messages = listOf( - ChatMessage(Role.system, "Append text to the end of the user's prompt".toContentList(), null), - ChatMessage(Role.user, state.selectedText.toString().toContentList(), null) - ), - ) - val chatResponse = api.chat(request, settings.defaultChatModel()) - val b4 = state.selectedText ?: "" - val str = chatResponse.choices[0].message?.content ?: "" - return b4 + if (str.startsWith(b4)) str.substring(b4.length) else str + val settings = AppSettingsState.instance + val request = ChatRequest( + model = settings.defaultChatModel().modelName, + temperature = settings.temperature + ).copy( + temperature = settings.temperature, + messages = listOf( + ChatMessage(Role.system, "Append text to the end of the user's prompt".toContentList(), null), + ChatMessage(Role.user, state.selectedText.toString().toContentList(), null) + ), + ) + val chatResponse = api.chat(request, settings.defaultChatModel()) + val b4 = state.selectedText ?: "" + val str = chatResponse.choices[0].message?.content ?: "" + return b4 + if (str.startsWith(b4)) str.substring(b4.length) else str } } \ No newline at end of file diff --git a/src/main/resources/sources/kt/com/github/simiacryptus/aicoder/actions/generic/ReplaceOptionsAction.kt b/src/main/resources/sources/kt/com/github/simiacryptus/aicoder/actions/generic/ReplaceOptionsAction.kt index eaa4a8f4..c60a13d2 100644 --- a/src/main/resources/sources/kt/com/github/simiacryptus/aicoder/actions/generic/ReplaceOptionsAction.kt +++ b/src/main/resources/sources/kt/com/github/simiacryptus/aicoder/actions/generic/ReplaceOptionsAction.kt @@ -11,7 +11,7 @@ import kotlin.math.ceil import kotlin.math.ln import kotlin.math.pow -class ReplaceOptionsAction : SelectionAction() { +open class ReplaceOptionsAction : SelectionAction() { interface VirtualAPI { fun suggestText(template: String, examples: List): Suggestions @@ -52,7 +52,7 @@ class ReplaceOptionsAction : SelectionAction() { return choose(choices ?: listOf()) } - private fun choose(choices: List): String { + open fun choose(choices: List): String { return UITools.showRadioButtonDialog("Select an option to fill in the blank:", *choices.toTypedArray())?.toString() ?: "" } } \ No newline at end of file diff --git a/src/main/resources/sources/kt/com/github/simiacryptus/aicoder/actions/markdown/MarkdownImplementActionGroup.kt b/src/main/resources/sources/kt/com/github/simiacryptus/aicoder/actions/markdown/MarkdownImplementActionGroup.kt index 5ca450b4..c777a1a6 100644 --- a/src/main/resources/sources/kt/com/github/simiacryptus/aicoder/actions/markdown/MarkdownImplementActionGroup.kt +++ b/src/main/resources/sources/kt/com/github/simiacryptus/aicoder/actions/markdown/MarkdownImplementActionGroup.kt @@ -37,7 +37,7 @@ class MarkdownImplementActionGroup : ActionGroup() { return actions.toTypedArray() } - class MarkdownImplementAction(private val language: String) : SelectionAction(true) { + open class MarkdownImplementAction(private val language: String) : SelectionAction(true) { init { templatePresentation.text = language templatePresentation.description = language diff --git a/src/main/resources/sources/kt/com/github/simiacryptus/aicoder/config/ActionSettingsRegistry.kt b/src/main/resources/sources/kt/com/github/simiacryptus/aicoder/config/ActionSettingsRegistry.kt index 3db4fc71..04a7653a 100644 --- a/src/main/resources/sources/kt/com/github/simiacryptus/aicoder/config/ActionSettingsRegistry.kt +++ b/src/main/resources/sources/kt/com/github/simiacryptus/aicoder/config/ActionSettingsRegistry.kt @@ -11,31 +11,44 @@ import java.util.stream.Collectors class ActionSettingsRegistry { val actionSettings: MutableMap = HashMap() - private val version = 2.0005 + private val version = 2.0006 fun edit(superChildren: Array): Array { val children = superChildren.toList().toMutableList() - children.toTypedArray().forEach { + children.toTypedArray().forEach { action -> val language = "kt" - val code: String? = load(it.javaClass, language) + val code: String? = load(action.javaClass, language) if (null != code) { try { - val actionConfig = this.getActionConfig(it) + val actionConfig = this.getActionConfig(action) actionConfig.language = language actionConfig.isDynamic = false - with(it) { + with(action) { templatePresentation.text = actionConfig.displayText templatePresentation.description = actionConfig.displayText } if (!actionConfig.enabled) { - children.remove(it) + children.remove(action) } else if (!actionConfig.file.exists() || actionConfig.file.readText().isBlank() || (actionConfig.version ?: 0.0) < version ) { actionConfig.file.writeText(code) actionConfig.version = version - } else if (!(actionConfig.isDynamic || (actionConfig.version ?: 0.0) >= version)) { + } else { + if (actionConfig.isDynamic || (actionConfig.version ?: 0.0) >= version) { + val localCode = actionConfig.file.readText().dropWhile { !it.isLetter() } + if (!localCode.equals(code)) { + try { + val element = actionConfig.buildAction(localCode) + children.remove(action) + children.add(element) + return@forEach + } catch (e: Throwable) { + log.info("Error loading dynamic ${action.javaClass}", e) + } + } + } val canLoad = try { ActionSettingsRegistry::class.java.classLoader.loadClass(actionConfig.id) true @@ -46,19 +59,12 @@ class ActionSettingsRegistry { actionConfig.file.writeText(code) actionConfig.version = version } else { - children.remove(it) - } - } else { - val localCode = actionConfig.file.readText().drop(1) - if (true || !localCode.equals(code)) { // HACK to test compile - val element = actionConfig.buildAction(localCode) - children.remove(it) - children.add(element) + children.remove(action) } } actionConfig.version = version } catch (e: Throwable) { - UITools.error(log, "Error loading ${it.javaClass}", e) + UITools.error(log, "Error loading ${action.javaClass}", e) } } } @@ -118,7 +124,7 @@ class ActionSettingsRegistry { val kotlinInterpreter = IdeaKotlinInterpreter(mapOf()) val scriptEngine = kotlinInterpreter.scriptEngine val eval = scriptEngine.eval(code) - throw Exception("Not implemented") + return eval as Class<*> } catch (e: Throwable) { throw DynamicActionException(e, "Error in Action " + displayText, file, this) } @@ -193,7 +199,7 @@ class ActionSettingsRegistry { private fun load(path: String): String? { val bytes = EditorMenu::class.java.getResourceAsStream(path)?.readAllBytes() - return bytes?.toString(Charsets.UTF_8)?.drop(1) // XXX Why? '\uFEFF' is first byte + return bytes?.toString(Charsets.UTF_8)?.dropWhile { !it.isLetter() } } fun load(clazz: Class, language: String) = diff --git a/src/main/resources/sources/kt/com/github/simiacryptus/aicoder/config/ActionTable.kt b/src/main/resources/sources/kt/com/github/simiacryptus/aicoder/config/ActionTable.kt index 76f8d6d6..e274c43e 100644 --- a/src/main/resources/sources/kt/com/github/simiacryptus/aicoder/config/ActionTable.kt +++ b/src/main/resources/sources/kt/com/github/simiacryptus/aicoder/config/ActionTable.kt @@ -3,6 +3,7 @@ import com.intellij.openapi.application.ApplicationManager import com.intellij.openapi.fileEditor.FileEditorManager import com.intellij.openapi.project.Project +import com.intellij.openapi.project.ProjectManager import com.intellij.openapi.ui.VerticalFlowLayout import com.intellij.openapi.vfs.LocalFileSystem import com.intellij.ui.BooleanTableCellEditor @@ -155,14 +156,17 @@ class ActionTable( override fun actionPerformed(e: ActionEvent?) { val id = dataModel.getValueAt(jtable.selectedRow, 2) val actionSetting = actionSettings.find { it.id == id } + val projectManager = ProjectManager.getInstance() actionSetting?.file?.let { val project = ApplicationManager.getApplication().runReadAction { - com.intellij.openapi.project.ProjectManager.getInstance().openProjects.firstOrNull() + projectManager.openProjects.firstOrNull() ?: projectManager.defaultProject } + if (it.exists()) { ApplicationManager.getApplication().invokeLater { val virtualFile = LocalFileSystem.getInstance().refreshAndFindFileByIoFile(it) - FileEditorManager.getInstance(project!!).openFile(virtualFile!!, true) + val fileEditorManager = FileEditorManager.getInstance(project!!) + val editor = fileEditorManager.openFile(virtualFile!!, true).firstOrNull() } } else { log.warn("File not found: ${it.absolutePath}") @@ -207,6 +211,17 @@ class ActionTable( jtable.columnModel.getColumn(1).headerRenderer = DefaultTableCellRenderer() jtable.columnModel.getColumn(2).headerRenderer = DefaultTableCellRenderer() + // Set the preferred width for the first column (checkboxes) to the header label width + val headerRenderer = jtable.tableHeader.defaultRenderer + val headerValue = jtable.columnModel.getColumn(0).headerValue + val headerComp = headerRenderer.getTableCellRendererComponent(jtable, headerValue, false, false, 0, 0) + jtable.columnModel.getColumn(0).preferredWidth = headerComp.preferredSize.width + + // Set the minimum width for the second column (display text) to accommodate 100 characters + val metrics = jtable.getFontMetrics(jtable.font) + val minWidth = metrics.charWidth('m') * 32 + jtable.columnModel.getColumn(1).minWidth = minWidth + jtable.tableHeader.defaultRenderer = DefaultTableCellRenderer() add(scrollpane, BorderLayout.CENTER) diff --git a/src/main/resources/sources/kt/com/github/simiacryptus/aicoder/config/AppSettingsComponent.kt b/src/main/resources/sources/kt/com/github/simiacryptus/aicoder/config/AppSettingsComponent.kt index b9d958c1..927b141f 100644 --- a/src/main/resources/sources/kt/com/github/simiacryptus/aicoder/config/AppSettingsComponent.kt +++ b/src/main/resources/sources/kt/com/github/simiacryptus/aicoder/config/AppSettingsComponent.kt @@ -1,10 +1,9 @@ -@file:Suppress("unused") - package com.github.simiacryptus.aicoder.config import com.intellij.openapi.application.ApplicationManager import com.intellij.openapi.fileEditor.FileEditorManager import com.intellij.openapi.project.Project +import com.intellij.openapi.project.ProjectManager import com.intellij.openapi.ui.ComboBox import com.intellij.openapi.vfs.LocalFileSystem import com.intellij.ui.components.JBCheckBox @@ -12,23 +11,21 @@ import com.intellij.ui.components.JBPasswordField import com.intellij.ui.components.JBTextField import com.simiacryptus.jopenai.ClientUtil import com.simiacryptus.jopenai.models.ChatModels - import org.slf4j.LoggerFactory import java.awt.event.ActionEvent import javax.swing.AbstractAction import javax.swing.JButton -import javax.swing.JComponent -class AppSettingsComponent { +class AppSettingsComponent : com.intellij.openapi.Disposable { @Name("Token Counter") val tokenCounter = JBTextField() @Suppress("unused") val clearCounter = JButton(object : AbstractAction("Clear Token Counter") { - override fun actionPerformed(e: ActionEvent) { - tokenCounter.text = "0" - } + override fun actionPerformed(e: ActionEvent) { + tokenCounter.text = "0" + } }) @Suppress("unused") @@ -57,21 +54,21 @@ class AppSettingsComponent { @Suppress("unused") val openApiLog = JButton(object : AbstractAction("Open API Log") { - override fun actionPerformed(e: ActionEvent) { - ClientUtil.auxiliaryLog?.let { - val project = ApplicationManager.getApplication().runReadAction { - com.intellij.openapi.project.ProjectManager.getInstance().openProjects.firstOrNull() - } - if (it.exists()) { - ApplicationManager.getApplication().invokeLater { - val virtualFile = LocalFileSystem.getInstance().refreshAndFindFileByIoFile(it) - FileEditorManager.getInstance(project!!).openFile(virtualFile!!, true) - } - } else { - log.warn("Log file not found: ${it.absolutePath}") - } + override fun actionPerformed(e: ActionEvent) { + ClientUtil.auxiliaryLog?.let { + val project = ApplicationManager.getApplication().runReadAction { + ProjectManager.getInstance().openProjects.firstOrNull() + } + if (it.exists()) { + ApplicationManager.getApplication().invokeLater { + val virtualFile = LocalFileSystem.getInstance().refreshAndFindFileByIoFile(it) + FileEditorManager.getInstance(project!!).openFile(virtualFile!!, true) } + } else { + log.warn("Log file not found: ${it.absolutePath}") + } } + } }) @@ -96,11 +93,11 @@ class AppSettingsComponent { @Name("File Actions") var fileActions = ActionTable(AppSettingsState.instance.fileActions.actionSettings.values.map { it.copy() } - .toTypedArray().toMutableList()) + .toTypedArray().toMutableList()) @Name("Editor Actions") var editorActions = ActionTable(AppSettingsState.instance.editorActions.actionSettings.values.map { it.copy() } - .toTypedArray().toMutableList()) + .toTypedArray().toMutableList()) init { tokenCounter.isEditable = false @@ -109,16 +106,10 @@ class AppSettingsComponent { this.modelName.addItem(ChatModels.GPT4Turbo.modelName) } - val preferredFocusedComponent: JComponent - get() = apiKey - - class ActionChangedListener { - fun actionChanged() { - } - } - - companion object { + companion object { private val log = LoggerFactory.getLogger(AppSettingsComponent::class.java) - //val ACTIONS_TOPIC = Topic.create("Actions", ActionChangedListener::class.java) } -} + + override fun dispose() { + } +} \ No newline at end of file diff --git a/src/main/resources/sources/kt/com/github/simiacryptus/aicoder/config/AppSettingsConfigurable.kt b/src/main/resources/sources/kt/com/github/simiacryptus/aicoder/config/AppSettingsConfigurable.kt index 5c0e4e5d..7902b21e 100644 --- a/src/main/resources/sources/kt/com/github/simiacryptus/aicoder/config/AppSettingsConfigurable.kt +++ b/src/main/resources/sources/kt/com/github/simiacryptus/aicoder/config/AppSettingsConfigurable.kt @@ -1,68 +1,23 @@ -package com.github.simiacryptus.aicoder.config +package com.github.simiacryptus.aicoder.config import com.github.simiacryptus.aicoder.util.UITools -import com.intellij.openapi.options.Configurable -import java.util.* -import javax.swing.JComponent -import javax.swing.JPanel -class AppSettingsConfigurable : Configurable { - private var settingsComponent: AppSettingsComponent? = null +open class AppSettingsConfigurable : UIAdapter(AppSettingsState.instance) { + override fun read(component: AppSettingsComponent, settings: AppSettingsState) { + UITools.readKotlinUIViaReflection(component, settings) + component.editorActions.read(settings.editorActions) + component.fileActions.read(settings.fileActions) + } - @Volatile - private var mainPanel: JPanel? = null - override fun getDisplayName(): String { - return "AICoder Settings" - } + override fun write(settings: AppSettingsState, component: AppSettingsComponent) { + UITools.writeKotlinUIViaReflection(settings, component) + component.editorActions.write(settings.editorActions) + component.fileActions.write(settings.fileActions) + } - override fun getPreferredFocusedComponent(): JComponent? { - return Objects.requireNonNull(settingsComponent)?.preferredFocusedComponent - } + override fun getPreferredFocusedComponent() = component?.apiKey - override fun createComponent(): JComponent? { - if (null == mainPanel) { - synchronized(this) { - if (null == mainPanel) { - settingsComponent = AppSettingsComponent() - reset() - mainPanel = UITools.build(settingsComponent!!, false) - } - } - } - return mainPanel - } + override fun newComponent() = AppSettingsComponent() - - override fun isModified(): Boolean { - val buffer = AppSettingsState() - if (settingsComponent != null) { - UITools.readKotlinUI(settingsComponent!!, buffer) - settingsComponent?.editorActions?.read(buffer.editorActions) - settingsComponent?.fileActions?.read(buffer.fileActions) - } - return buffer != AppSettingsState.instance - } - - override fun apply() { - if (settingsComponent != null) { - UITools.readKotlinUI(settingsComponent!!, AppSettingsState.instance) - settingsComponent?.editorActions?.read(AppSettingsState.instance.editorActions) - settingsComponent?.fileActions?.read(AppSettingsState.instance.fileActions) - } - } - - override fun reset() { - if (settingsComponent != null) { - UITools.writeKotlinUI(settingsComponent!!, AppSettingsState.instance) - settingsComponent?.editorActions?.write(AppSettingsState.instance.editorActions) - settingsComponent?.fileActions?.write(AppSettingsState.instance.fileActions) - } - } - - override fun disposeUIResources() { - settingsComponent = null - } + override fun newSettings() = AppSettingsState() } - - - diff --git a/src/main/resources/sources/kt/com/github/simiacryptus/aicoder/config/AppSettingsState.kt b/src/main/resources/sources/kt/com/github/simiacryptus/aicoder/config/AppSettingsState.kt index 2010759a..a01c5fd5 100644 --- a/src/main/resources/sources/kt/com/github/simiacryptus/aicoder/config/AppSettingsState.kt +++ b/src/main/resources/sources/kt/com/github/simiacryptus/aicoder/config/AppSettingsState.kt @@ -6,50 +6,36 @@ import com.intellij.openapi.components.PersistentStateComponent import com.intellij.openapi.components.State import com.intellij.openapi.components.Storage import com.intellij.util.xmlb.XmlSerializerUtil -import com.simiacryptus.jopenai.ApiModel.ChatRequest import com.simiacryptus.jopenai.models.ChatModels -import com.simiacryptus.jopenai.models.OpenAIModel import com.simiacryptus.jopenai.models.OpenAITextModel import com.simiacryptus.jopenai.util.JsonUtil -class SimpleEnvelope(var value: String? = null) - @State(name = "org.intellij.sdk.settings.AppSettingsState", storages = [Storage("SdkSettingsPlugin.xml")]) -class AppSettingsState : PersistentStateComponent { - var listeningPort: Int = 8081 - var listeningEndpoint: String = "localhost" - var modalTasks: Boolean = false - var suppressErrors: Boolean = false - var apiLog: Boolean = false - var apiBase = "https://api.openai.com/v1" - var apiKey = "" - var temperature = 0.1 - var modelName : String = ChatModels.GPT35Turbo.modelName - var tokenCounter = 0 - var humanLanguage = "English" - var devActions = false - var editRequests = false - var apiThreads = 4 +data class AppSettingsState( + var temperature: Double = 0.1, + var modelName: String = ChatModels.GPT35Turbo.modelName, + var listeningPort: Int = 8081, + var listeningEndpoint: String = "localhost", + var humanLanguage: String = "English", + var apiThreads: Int = 4, + var apiBase: String = "https://api.openai.com/v1", + var apiKey: String = "", + var tokenCounter: Int = 0, + var modalTasks: Boolean = false, + var suppressErrors: Boolean = false, + var apiLog: Boolean = false, + var devActions: Boolean = false, + var editRequests: Boolean = false, +) : PersistentStateComponent { + val editorActions = ActionSettingsRegistry() val fileActions = ActionSettingsRegistry() - private val recentCommands = mutableMapOf() - fun createChatRequest(): ChatRequest { - return createChatRequest(defaultChatModel()) - } - fun defaultChatModel(): OpenAITextModel = ChatModels.entries.first { it.modelName == modelName } - private fun createChatRequest(model: OpenAIModel): ChatRequest = ChatRequest( - model = model.modelName, - temperature = temperature - ) - @JsonIgnore - override fun getState(): SimpleEnvelope { - return SimpleEnvelope(JsonUtil.toJson(this)) - } + override fun getState() = SimpleEnvelope(JsonUtil.toJson(this)) fun getRecentCommands(id:String) = recentCommands.computeIfAbsent(id) { MRUItems() } @@ -57,52 +43,12 @@ class AppSettingsState : PersistentStateComponent { state.value ?: return val fromJson = JsonUtil.fromJson(state.value!!, AppSettingsState::class.java) XmlSerializerUtil.copyBean(fromJson, this) - - recentCommands.clear(); recentCommands.putAll(fromJson.recentCommands) - editorActions.actionSettings.clear(); editorActions.actionSettings.putAll(fromJson.editorActions.actionSettings) - fileActions.actionSettings.clear(); fileActions.actionSettings.putAll(fromJson.fileActions.actionSettings) - } - - override fun equals(other: Any?): Boolean { - if (this === other) return true - if (javaClass != other?.javaClass) return false - - other as AppSettingsState - - if (listeningPort != other.listeningPort) return false - if (listeningEndpoint != other.listeningEndpoint) return false - if (modalTasks != other.modalTasks) return false - if (suppressErrors != other.suppressErrors) return false - if (apiLog != other.apiLog) return false - if (apiBase != other.apiBase) return false - if (apiKey != other.apiKey) return false - if (temperature != other.temperature) return false - if (modelName != other.modelName) return false - if (tokenCounter != other.tokenCounter) return false - if (humanLanguage != other.humanLanguage) return false - if (devActions != other.devActions) return false - if (editRequests != other.editRequests) return false - if (apiThreads != other.apiThreads) return false - - return true - } - - override fun hashCode(): Int { - var result = listeningPort - result = 31 * result + listeningEndpoint.hashCode() - result = 31 * result + modalTasks.hashCode() - result = 31 * result + suppressErrors.hashCode() - result = 31 * result + apiLog.hashCode() - result = 31 * result + apiBase.hashCode() - result = 31 * result + apiKey.hashCode() - result = 31 * result + temperature.hashCode() - result = 31 * result + modelName.hashCode() - result = 31 * result + tokenCounter - result = 31 * result + humanLanguage.hashCode() - result = 31 * result + devActions.hashCode() - result = 31 * result + editRequests.hashCode() - result = 31 * result + apiThreads - return result + recentCommands.clear(); + recentCommands.putAll(fromJson.recentCommands) + editorActions.actionSettings.clear(); + editorActions.actionSettings.putAll(fromJson.editorActions.actionSettings) + fileActions.actionSettings.clear(); + fileActions.actionSettings.putAll(fromJson.fileActions.actionSettings) } companion object { @@ -113,3 +59,5 @@ class AppSettingsState : PersistentStateComponent { } } } + + diff --git a/src/main/resources/sources/kt/com/github/simiacryptus/aicoder/config/NonReflectionAppSettingsConfigurable.kt b/src/main/resources/sources/kt/com/github/simiacryptus/aicoder/config/NonReflectionAppSettingsConfigurable.kt new file mode 100644 index 00000000..099fb6f4 --- /dev/null +++ b/src/main/resources/sources/kt/com/github/simiacryptus/aicoder/config/NonReflectionAppSettingsConfigurable.kt @@ -0,0 +1,155 @@ +package com.github.simiacryptus.aicoder.config + +import java.awt.BorderLayout +import java.awt.FlowLayout +import javax.swing.BoxLayout +import javax.swing.JComponent +import javax.swing.JLabel +import javax.swing.JPanel + +class NonReflectionAppSettingsConfigurable : AppSettingsConfigurable() { + + override fun build(component: AppSettingsComponent): JComponent { + val tabbedPane = com.intellij.ui.components.JBTabbedPane() + + // Basic Settings Tab + val basicSettingsPanel = JPanel(BorderLayout()).apply { + add(JPanel(BorderLayout()).apply { + layout = BoxLayout(this, BoxLayout.Y_AXIS) + add(JPanel(FlowLayout(FlowLayout.LEFT)).apply { + add(JLabel("Model:")) + add(component.modelName) + }) + add(JPanel(FlowLayout(FlowLayout.LEFT)).apply { + add(JLabel("Temperature:")) + add(component.temperature) + }) + add(JPanel(FlowLayout(FlowLayout.LEFT)).apply { + add(JLabel("Human Language:")) + add(component.humanLanguage) + }) + add(JPanel(FlowLayout(FlowLayout.LEFT)).apply { + add(JLabel("Token Counter:")) + add(component.tokenCounter) + add(component.clearCounter) + }) + add(JPanel(FlowLayout(FlowLayout.LEFT)).apply { + add(JLabel("API Key:")) + add(component.apiKey) + }) + add(JPanel(FlowLayout(FlowLayout.LEFT)).apply { + add(JLabel("Server Port:")) + add(component.listeningPort) + }) + add(JPanel(FlowLayout(FlowLayout.LEFT)).apply { + add(JLabel("Ignore Errors:")) + add(component.suppressErrors) + }) + }, BorderLayout.NORTH) + } + tabbedPane.addTab("Basic Settings", basicSettingsPanel) + + tabbedPane.addTab("Developer Tools", JPanel(BorderLayout()).apply { + add(JPanel().apply { + layout = BoxLayout(this, BoxLayout.Y_AXIS) + add(JPanel(FlowLayout(FlowLayout.LEFT)).apply { + add(JLabel("Developer Tools:")) + add(component.devActions) + }) + add(JPanel(FlowLayout(FlowLayout.LEFT)).apply { + add(JLabel("Edit API Requests:")) + add(component.editRequests) + }) + add(JPanel(FlowLayout(FlowLayout.LEFT)).apply { + add(JLabel("Enable API Log:")) + add(component.apiLog) + add(component.openApiLog) + }) + add(JPanel(FlowLayout(FlowLayout.LEFT)).apply { + add(JLabel("API Base:")) + add(component.apiBase) + }) + add(JPanel(FlowLayout(FlowLayout.LEFT)).apply { + add(JLabel("Server Endpoint:")) + add(component.listeningEndpoint) + }) + }, BorderLayout.NORTH) + }) + + tabbedPane.addTab("File Actions", JPanel(BorderLayout()).apply { + add(component.fileActions, BorderLayout.CENTER) + }) + + tabbedPane.addTab("Editor Actions", JPanel(BorderLayout()).apply { + add(component.editorActions, BorderLayout.CENTER) + }) + + return tabbedPane + } + + override fun write(settings: AppSettingsState, component: AppSettingsComponent) { + try { + component.tokenCounter.text = settings.tokenCounter.toString() + component.humanLanguage.text = settings.humanLanguage + component.listeningPort.text = settings.listeningPort.toString() + component.listeningEndpoint.text = settings.listeningEndpoint + component.suppressErrors.isSelected = settings.suppressErrors + component.modelName.selectedItem = settings.modelName + component.apiLog.isSelected = settings.apiLog + component.devActions.isSelected = settings.devActions + component.editRequests.isSelected = settings.editRequests + component.temperature.text = settings.temperature.toString() + component.apiKey.text = settings.apiKey + component.apiBase.text = settings.apiBase + component.editorActions.read(settings.editorActions) + component.fileActions.read(settings.fileActions) + } catch (e: Exception) { + log.warn("Error setting UI", e) + } + } + + override fun read(component: AppSettingsComponent, settings: AppSettingsState) { + try { + settings.tokenCounter = component.tokenCounter.text.safeInt() + settings.humanLanguage = component.humanLanguage.text + settings.listeningPort = component.listeningPort.text.safeInt() + settings.listeningEndpoint = component.listeningEndpoint.text + settings.suppressErrors = component.suppressErrors.isSelected + settings.modelName = component.modelName.selectedItem as String + settings.apiLog = component.apiLog.isSelected + settings.devActions = component.devActions.isSelected + settings.editRequests = component.editRequests.isSelected + settings.temperature = component.temperature.text.safeDouble() + settings.apiKey = String(component.apiKey.password) + settings.apiBase = component.apiBase.text + component.editorActions.write(settings.editorActions) + component.fileActions.write(settings.fileActions) + } catch (e: Exception) { + log.warn("Error reading UI", e) + } + } + + companion object { + val log = com.intellij.openapi.diagnostic.Logger.getInstance(NonReflectionAppSettingsConfigurable::class.java) + } +} + +fun String?.safeInt() = if (null == this) 0 else when { + isEmpty() -> 0 + else -> try { + toInt() + } catch (e: NumberFormatException) { + 0 + } +} + +fun String?.safeDouble() = if (null == this) 0.0 else when { + isEmpty() -> 0.0 + else -> try { + toDouble() + } catch (e: NumberFormatException) { + 0.0 + } + + +} diff --git a/src/main/resources/sources/kt/com/github/simiacryptus/aicoder/config/SimpleEnvelope.kt b/src/main/resources/sources/kt/com/github/simiacryptus/aicoder/config/SimpleEnvelope.kt new file mode 100644 index 00000000..4dfdd5ca --- /dev/null +++ b/src/main/resources/sources/kt/com/github/simiacryptus/aicoder/config/SimpleEnvelope.kt @@ -0,0 +1,3 @@ +package com.github.simiacryptus.aicoder.config + +class SimpleEnvelope(var value: String? = null) \ No newline at end of file diff --git a/src/main/resources/sources/kt/com/github/simiacryptus/aicoder/config/UIAdapter.kt b/src/main/resources/sources/kt/com/github/simiacryptus/aicoder/config/UIAdapter.kt new file mode 100644 index 00000000..7cf75ec0 --- /dev/null +++ b/src/main/resources/sources/kt/com/github/simiacryptus/aicoder/config/UIAdapter.kt @@ -0,0 +1,77 @@ +package com.github.simiacryptus.aicoder.config + +import com.github.simiacryptus.aicoder.util.UITools +import com.intellij.openapi.Disposable +import com.intellij.openapi.options.Configurable +import javax.swing.JComponent + +abstract class UIAdapter( + protected val settingsInstance: S, + protected var component: C? = null, +) : Configurable { + + @Volatile + private var mainPanel: JComponent? = null + override fun getDisplayName(): String { + return "AICoder Settings" + } + + override fun getPreferredFocusedComponent(): JComponent? = null + + override fun createComponent(): JComponent? { + if (null == mainPanel) { + synchronized(this) { + if (null == mainPanel) { + val component = newComponent() + this.component = component + mainPanel = build(component) + write(settingsInstance, component) + } + } + } + return mainPanel + } + + abstract fun newComponent(): C + abstract fun newSettings(): S + fun getSettings(component : C? = this.component) = when (component) { + null -> settingsInstance + else -> { + val buffer = newSettings() + read(component, buffer) + buffer + } + } + + override fun isModified() = when { + component == null -> false + getSettings() != settingsInstance -> true + else -> false + } + + override fun apply() { + if (component != null) read(component!!, settingsInstance) + } + + override fun reset() { + if (component != null) write(settingsInstance, component!!) + } + + override fun disposeUIResources() { + val component = component + this.component = null + if(component != null && component is Disposable) component.dispose() + } + + open fun build(component: C): JComponent = + UITools.buildFormViaReflection(component, false)!! + + open fun read(component: C, settings: S) { + UITools.readKotlinUIViaReflection(component, settings) + } + + open fun write(settings: S, component: C) { + UITools.writeKotlinUIViaReflection(settings, component) + } + +} \ No newline at end of file diff --git a/src/main/resources/sources/kt/com/github/simiacryptus/aicoder/util/UITools.kt b/src/main/resources/sources/kt/com/github/simiacryptus/aicoder/util/UITools.kt index e4c615dd..ec87950c 100644 --- a/src/main/resources/sources/kt/com/github/simiacryptus/aicoder/util/UITools.kt +++ b/src/main/resources/sources/kt/com/github/simiacryptus/aicoder/util/UITools.kt @@ -63,857 +63,787 @@ import kotlin.reflect.jvm.javaField import kotlin.reflect.jvm.javaType object UITools { - private val log = LoggerFactory.getLogger(UITools::class.java) - val retry = WeakHashMap() - - @JvmStatic - fun redoableTask( - event: AnActionEvent, - request: Supplier, - ) { - Futures.addCallback(pool.submit { - request.get() - }, futureCallback(event, request), pool) - } - - @JvmStatic - fun futureCallback( - event: AnActionEvent, - request: Supplier, - ) = object : FutureCallback { - override fun onSuccess(undo: Runnable) { - val requiredData = event.getData(CommonDataKeys.EDITOR) ?: return - val document = requiredData.document - retry[document] = getRetry(event, request, undo) - } - - override fun onFailure(t: Throwable) { - error(log, "Error", t) - } - } - - @JvmStatic - fun getRetry( - event: AnActionEvent, - request: Supplier, - undo: Runnable, - ): Runnable { - return Runnable { - Futures.addCallback( - pool.submit { - WriteCommandAction.runWriteCommandAction(event.project) { undo?.run() } - request.get() - }, - futureCallback(event, request), - pool - ) - } - } - - @JvmStatic - fun replaceString(document: Document, startOffset: Int, endOffset: Int, newText: CharSequence): Runnable { - val oldText: CharSequence = document.getText(TextRange(startOffset, endOffset)) - document.replaceString(startOffset, endOffset, newText) - logEdit( - String.format( - "FWD replaceString from %s to %s (%s->%s): %s", - startOffset, - endOffset, - endOffset - startOffset, - newText.length, - newText - ) + val retry = WeakHashMap() + + private val log = LoggerFactory.getLogger(UITools::class.java) + private val threadFactory: ThreadFactory = ThreadFactoryBuilder().setNameFormat("API Thread %d").build() + private val pool: ListeningExecutorService by lazy { + MoreExecutors.listeningDecorator( + ThreadPoolExecutor( + /* corePoolSize = */ AppSettingsState.instance.apiThreads, + /* maximumPoolSize = */AppSettingsState.instance.apiThreads, + /* keepAliveTime = */ 0L, + /* unit = */ TimeUnit.MILLISECONDS, + /* workQueue = */ LinkedBlockingQueue(), + /* threadFactory = */ threadFactory, + /* handler = */ ThreadPoolExecutor.AbortPolicy() + ) + ) + } + private val scheduledPool: ListeningScheduledExecutorService by lazy { + MoreExecutors.listeningDecorator(ScheduledThreadPoolExecutor(1, threadFactory)) + } + private val errorLog = mutableListOf>() + private val actionLog = mutableListOf() + private val singleThreadPool = Executors.newSingleThreadExecutor() + + fun redoableTask( + event: AnActionEvent, + request: Supplier, + ) { + Futures.addCallback(pool.submit { + request.get() + }, futureCallback(event, request), pool) + } + + private fun futureCallback( + event: AnActionEvent, + request: Supplier, + ) = object : FutureCallback { + override fun onSuccess(undo: Runnable) { + val requiredData = event.getData(CommonDataKeys.EDITOR) ?: return + val document = requiredData.document + retry[document] = getRetry(event, request, undo) + } + + override fun onFailure(t: Throwable) { + error(log, "Error", t) + } + } + + fun getRetry( + event: AnActionEvent, + request: Supplier, + undo: Runnable, + ): Runnable { + return Runnable { + Futures.addCallback( + pool.submit { + WriteCommandAction.runWriteCommandAction(event.project) { undo?.run() } + request.get() + }, futureCallback(event, request), pool + ) + } + } + + fun replaceString(document: Document, startOffset: Int, endOffset: Int, newText: CharSequence): Runnable { + val oldText: CharSequence = document.getText(TextRange(startOffset, endOffset)) + document.replaceString(startOffset, endOffset, newText) + logEdit( + String.format( + "FWD replaceString from %s to %s (%s->%s): %s", + startOffset, + endOffset, + endOffset - startOffset, + newText.length, + newText + ) + ) + return Runnable { + val verifyTxt = document.getText(TextRange(startOffset, startOffset + newText.length)) + if (verifyTxt != newText) { + val msg = String.format( + "The text range from %d to %d does not match the expected text \"%s\" and is instead \"%s\"", + startOffset, + startOffset + newText.length, + newText, + verifyTxt ) - return Runnable { - val verifyTxt = document.getText(TextRange(startOffset, startOffset + newText.length)) - if (verifyTxt != newText) { - val msg = String.format( - "The text range from %d to %d does not match the expected text \"%s\" and is instead \"%s\"", - startOffset, - startOffset + newText.length, - newText, - verifyTxt - ) - throw IllegalStateException(msg) - } - document.replaceString(startOffset, startOffset + newText.length, oldText) - logEdit( - String.format( - "REV replaceString from %s to %s (%s->%s): %s", - startOffset, - startOffset + newText.length, - newText.length, - oldText.length, - oldText - ) - ) - } - } - - @JvmStatic - fun insertString(document: Document, startOffset: Int, newText: CharSequence): Runnable { - document.insertString(startOffset, newText) - logEdit(String.format("FWD insertString @ %s (%s): %s", startOffset, newText.length, newText)) - return Runnable { - val verifyTxt = document.getText(TextRange(startOffset, startOffset + newText.length)) - if (verifyTxt != newText) { - val message = String.format( - "The text range from %d to %d does not match the expected text \"%s\" and is instead \"%s\"", - startOffset, - startOffset + newText.length, - newText, - verifyTxt - ) - throw AssertionError(message) - } - document.deleteString(startOffset, startOffset + newText.length) - logEdit(String.format("REV deleteString from %s to %s", startOffset, startOffset + newText.length)) - } - } - - @JvmStatic - private fun logEdit(message: String) { - log.debug(message) - } - - @JvmStatic - @Suppress("unused") - fun deleteString(document: Document, startOffset: Int, endOffset: Int): Runnable { - val oldText: CharSequence = document.getText(TextRange(startOffset, endOffset)) - document.deleteString(startOffset, endOffset) - return Runnable { - document.insertString(startOffset, oldText) - logEdit(String.format("REV insertString @ %s (%s): %s", startOffset, oldText.length, oldText)) - } - } - - @JvmStatic - fun getIndent(caret: Caret?): CharSequence { - if (null == caret) return "" - val document = caret.editor.document - val documentText = document.text - val lineNumber = document.getLineNumber(caret.selectionStart) - val lines = documentText.split("\n".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray() - if (lines.isEmpty()) return "" - return IndentedText.fromString(lines[max(lineNumber, 0).coerceAtMost(lines.size - 1)]).indent - } - - @JvmStatic - @Suppress("unused") - fun hasSelection(e: AnActionEvent): Boolean { - val caret = e.getData(CommonDataKeys.CARET) - return null != caret && caret.hasSelection() - } - - @JvmStatic - fun getIndent(event: AnActionEvent): CharSequence { - val caret = event.getData(CommonDataKeys.CARET) - val indent: CharSequence = if (null == caret) { - "" - } else { - getIndent(caret) - } - return indent - } - - @JvmStatic - fun queryAPIKey(): CharSequence? { - val panel = JPanel() - val label = JLabel("Enter OpenAI API Key:") - val pass = JPasswordField(100) - panel.add(label) - panel.add(pass) - val options = arrayOf("OK", "Cancel") - return if (JOptionPane.showOptionDialog( - null, - panel, - "API Key", - JOptionPane.NO_OPTION, - JOptionPane.PLAIN_MESSAGE, - null, - options, - options[1] - ) == JOptionPane.OK_OPTION - ) { - val password = pass.password - java.lang.String(password) - } else { - null - } - } - - @JvmStatic - fun readKotlinUI(component: R, settings: T) { - val componentClass: Class<*> = component.javaClass - val declaredUIFields = - componentClass.kotlin.memberProperties.map { it.name }.toSet() - for (settingsField in settings.javaClass.kotlin.memberProperties) { - if (settingsField is KMutableProperty<*>) { - settingsField.isAccessible = true - val settingsFieldName = settingsField.name - try { - var newSettingsValue: Any? = null - if (!declaredUIFields.contains(settingsFieldName)) continue - val uiField: KProperty1 = - (componentClass.kotlin.memberProperties.find { it.name == settingsFieldName } as KProperty1?)!! - var uiVal = uiField.get(component) - if (uiVal is JScrollPane) { - uiVal = uiVal.viewport.view - } - when (settingsField.returnType.javaType.typeName) { - "java.lang.String" -> if (uiVal is JTextComponent) { - newSettingsValue = uiVal.text - } else if (uiVal is ComboBox<*>) { - newSettingsValue = uiVal.item - } - - "int", "java.lang.Integer" -> if (uiVal is JTextComponent) { - newSettingsValue = if (uiVal.text.isBlank()) -1 else uiVal.text.toInt() - } - - "long" -> if (uiVal is JTextComponent) { - newSettingsValue = if (uiVal.text.isBlank()) -1 else uiVal.text.toLong() - } - - "double", "java.lang.Double" -> if (uiVal is JTextComponent) { - newSettingsValue = if (uiVal.text.isBlank()) 0.0 else uiVal.text.toDouble() - } - - "boolean" -> if (uiVal is JCheckBox) { - newSettingsValue = uiVal.isSelected - } else if (uiVal is JTextComponent) { - newSettingsValue = java.lang.Boolean.parseBoolean(uiVal.text) - } - - else -> - if (Enum::class.java.isAssignableFrom(settingsField.returnType.javaType as Class<*>)) { - if (uiVal is ComboBox<*>) { - val comboBox = uiVal - val item = comboBox.item - val enumClass = settingsField.returnType.javaType as Class?> - val string = item.toString() - newSettingsValue = - findValue(enumClass, string) - } - } - } - settingsField.setter.call(settings, newSettingsValue) - } catch (e: Throwable) { - throw RuntimeException("Error processing $settingsField", e) - } - } - } - } - - @JvmStatic - fun findValue(enumClass: Class?>, string: String): Enum<*>? { - enumClass.enumConstants?.filter { it?.name?.compareTo(string, true) == 0 }?.forEach { return it } - return java.lang.Enum.valueOf( - enumClass, - string + throw IllegalStateException(msg) + } + document.replaceString(startOffset, startOffset + newText.length, oldText) + logEdit( + String.format( + "REV replaceString from %s to %s (%s->%s): %s", + startOffset, + startOffset + newText.length, + newText.length, + oldText.length, + oldText ) - } - - @JvmStatic - fun writeKotlinUI(component: R, settings: T) { - val componentClass: Class<*> = component.javaClass - val declaredUIFields = - componentClass.kotlin.memberProperties.map { it.name }.toSet() - val memberProperties = settings.javaClass.kotlin.memberProperties - val publicProperties = memberProperties.filter { - it.visibility == KVisibility.PUBLIC //&& it is KMutableProperty<*> - } - for (settingsField in publicProperties) { - val fieldName = settingsField.name - try { - if (!declaredUIFields.contains(fieldName)) continue - val uiField: KProperty1 = - (componentClass.kotlin.memberProperties.find { it.name == fieldName } as KProperty1?)!! - val settingsVal = settingsField.get(settings) ?: continue - var uiVal = uiField.get(component) - if (uiVal is JScrollPane) { - uiVal = uiVal.viewport.view - } - when (settingsField.returnType.javaType.typeName) { - "java.lang.String" -> if (uiVal is JTextComponent) { - uiVal.text = settingsVal.toString() - } else if (uiVal is ComboBox<*>) { - uiVal.item = settingsVal.toString() - } - - "int", "java.lang.Integer" -> if (uiVal is JTextComponent) { - uiVal.text = (settingsVal as Int).toString() - } - - "long" -> if (uiVal is JTextComponent) { - uiVal.text = (settingsVal as Int).toLong().toString() - } - - "boolean" -> if (uiVal is JCheckBox) { - uiVal.isSelected = (settingsVal as Boolean) - } else if (uiVal is JTextComponent) { - uiVal.text = java.lang.Boolean.toString((settingsVal as Boolean)) - } - - "double", "java.lang.Double" -> if (uiVal is JTextComponent) { - uiVal.text = (settingsVal as Double).toString() - } - - else -> if (uiVal is ComboBox<*>) { - uiVal.item = settingsVal.toString() - } - } - } catch (e: Throwable) { - throw RuntimeException("Error processing $settingsField", e) - } - } - } - - @JvmStatic - fun addKotlinFields(ui: T, formBuilder: FormBuilder, fillVertically: Boolean) { - var first = true - for (field in ui.javaClass.kotlin.memberProperties) { - if (field.javaField == null) continue - try { - val nameAnnotation = field.annotations.find { it is Name } as Name? - val component = field.get(ui) as JComponent - if (nameAnnotation != null) { - if (first && fillVertically) { - first = false - formBuilder.addLabeledComponentFillVertically(nameAnnotation.value + ": ", component) - } else { - formBuilder.addLabeledComponent(JBLabel(nameAnnotation.value + ": "), component, 1, false) - } - } else { - formBuilder.addComponentToRightColumn(component, 1) - } - } catch (e: IllegalAccessException) { - throw RuntimeException(e) - } catch (e: Throwable) { - error(log, "Error processing " + field.name, e) - } - } - } - - @JvmStatic - fun getMaximumSize(factor: Double): Dimension { - val screenSize = Toolkit.getDefaultToolkit().screenSize - return Dimension((screenSize.getWidth() * factor).toInt(), (screenSize.getHeight() * factor).toInt()) - } - - @JvmStatic - fun showOptionDialog(mainPanel: JPanel?, vararg options: Any, title: String, modal: Boolean = true): Int { - val pane = getOptionPane(mainPanel, options) - val rootFrame = JOptionPane.getRootFrame() - pane.componentOrientation = rootFrame.componentOrientation - val dialog = JDialog(rootFrame, title, modal) - dialog.componentOrientation = rootFrame.componentOrientation - - val latch = if (!modal) CountDownLatch(1) else null - configure(dialog, pane, latch) - dialog.isVisible = true - if (!modal) latch?.await() - - dialog.dispose() - return getSelectedValue(pane, options) - } - - @JvmStatic - fun getOptionPane( - mainPanel: JPanel?, - options: Array, - ): JOptionPane { - val pane = JOptionPane( - mainPanel, - JOptionPane.PLAIN_MESSAGE, - JOptionPane.NO_OPTION, - null, - options, - options[0] + ) + } + } + + fun insertString(document: Document, startOffset: Int, newText: CharSequence): Runnable { + document.insertString(startOffset, newText) + logEdit(String.format("FWD insertString @ %s (%s): %s", startOffset, newText.length, newText)) + return Runnable { + val verifyTxt = document.getText(TextRange(startOffset, startOffset + newText.length)) + if (verifyTxt != newText) { + val message = String.format( + "The text range from %d to %d does not match the expected text \"%s\" and is instead \"%s\"", + startOffset, + startOffset + newText.length, + newText, + verifyTxt ) - pane.initialValue = options[0] - return pane - } - - @JvmStatic - fun configure(dialog: JDialog, pane: JOptionPane, latch: CountDownLatch? = null) { - val contentPane = dialog.contentPane - contentPane.layout = BorderLayout() - contentPane.add(pane, BorderLayout.CENTER) - - if (JDialog.isDefaultLookAndFeelDecorated() && UIManager.getLookAndFeel().supportsWindowDecorations) { - dialog.isUndecorated = true - pane.rootPane.windowDecorationStyle = JRootPane.PLAIN_DIALOG - } - dialog.isResizable = true - dialog.maximumSize = getMaximumSize(0.9) - dialog.pack() - dialog.setLocationRelativeTo(null as Component?) - val adapter: WindowAdapter = windowAdapter(pane, dialog) - dialog.addWindowListener(adapter) - dialog.addWindowFocusListener(adapter) - dialog.addComponentListener(object : ComponentAdapter() { - override fun componentShown(ce: ComponentEvent) { - // reset value to ensure closing works properly - pane.value = JOptionPane.UNINITIALIZED_VALUE - } - }) - pane.addPropertyChangeListener { event: PropertyChangeEvent -> - if (dialog.isVisible && event.source === pane && event.propertyName == JOptionPane.VALUE_PROPERTY && event.newValue != null && event.newValue !== JOptionPane.UNINITIALIZED_VALUE) { - dialog.isVisible = false - latch?.countDown() + throw AssertionError(message) + } + document.deleteString(startOffset, startOffset + newText.length) + logEdit(String.format("REV deleteString from %s to %s", startOffset, startOffset + newText.length)) + } + } + + private fun logEdit(message: String) { + log.debug(message) + } + + @Suppress("unused") + fun deleteString(document: Document, startOffset: Int, endOffset: Int): Runnable { + val oldText: CharSequence = document.getText(TextRange(startOffset, endOffset)) + document.deleteString(startOffset, endOffset) + return Runnable { + document.insertString(startOffset, oldText) + logEdit(String.format("REV insertString @ %s (%s): %s", startOffset, oldText.length, oldText)) + } + } + + fun getIndent(caret: Caret?): CharSequence { + if (null == caret) return "" + val document = caret.editor.document + val documentText = document.text + val lineNumber = document.getLineNumber(caret.selectionStart) + val lines = documentText.split("\n".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray() + if (lines.isEmpty()) return "" + return IndentedText.fromString(lines[max(lineNumber, 0).coerceAtMost(lines.size - 1)]).indent + } + + @Suppress("unused") + fun hasSelection(e: AnActionEvent): Boolean { + val caret = e.getData(CommonDataKeys.CARET) + return null != caret && caret.hasSelection() + } + + fun getIndent(event: AnActionEvent): CharSequence { + val caret = event.getData(CommonDataKeys.CARET) + val indent: CharSequence = if (null == caret) { + "" + } else { + getIndent(caret) + } + return indent + } + + private fun queryAPIKey(): CharSequence? { + val panel = JPanel() + val label = JLabel("Enter OpenAI API Key:") + val pass = JPasswordField(100) + panel.add(label) + panel.add(pass) + val options = arrayOf("OK", "Cancel") + return if (JOptionPane.showOptionDialog( + null, panel, "API Key", JOptionPane.NO_OPTION, JOptionPane.PLAIN_MESSAGE, null, options, options[1] + ) == JOptionPane.OK_OPTION + ) { + String(pass.password) + } else { + null + } + } + + fun readKotlinUIViaReflection(component: R, settings: T) { + val componentClass: Class<*> = component.javaClass + val declaredUIFields = componentClass.kotlin.memberProperties.map { it.name }.toSet() + for (settingsField in settings.javaClass.kotlin.memberProperties) { + if (settingsField is KMutableProperty<*>) { + settingsField.isAccessible = true + val settingsFieldName = settingsField.name + try { + var newSettingsValue: Any? = null + if (!declaredUIFields.contains(settingsFieldName)) continue + val uiField: KProperty1 = + (componentClass.kotlin.memberProperties.find { it.name == settingsFieldName } as KProperty1?)!! + var uiVal = uiField.get(component) + if (uiVal is JScrollPane) { + uiVal = uiVal.viewport.view + } + when (settingsField.returnType.javaType.typeName) { + "java.lang.String" -> if (uiVal is JTextComponent) { + newSettingsValue = uiVal.text + } else if (uiVal is ComboBox<*>) { + newSettingsValue = uiVal.item } - } - pane.selectInitialValue() - } - - @JvmStatic - private fun windowAdapter(pane: JOptionPane, dialog: JDialog): WindowAdapter { - val adapter: WindowAdapter = object : WindowAdapter() { - private var gotFocus = false - override fun windowClosing(we: WindowEvent) { - pane.value = null + "int", "java.lang.Integer" -> if (uiVal is JTextComponent) { + newSettingsValue = if (uiVal.text.isBlank()) -1 else uiVal.text.toInt() } - override fun windowClosed(e: WindowEvent) { - pane.removePropertyChangeListener { event: PropertyChangeEvent -> - if (dialog.isVisible && event.source === pane && event.propertyName == JOptionPane.VALUE_PROPERTY && event.newValue != null && event.newValue !== JOptionPane.UNINITIALIZED_VALUE) { - dialog.isVisible = false - } - } - dialog.contentPane.removeAll() + "long" -> if (uiVal is JTextComponent) { + newSettingsValue = if (uiVal.text.isBlank()) -1 else uiVal.text.toLong() } - override fun windowGainedFocus(we: WindowEvent) { - if (!gotFocus) { - pane.selectInitialValue() - gotFocus = true - } + "double", "java.lang.Double" -> if (uiVal is JTextComponent) { + newSettingsValue = if (uiVal.text.isBlank()) 0.0 else uiVal.text.toDouble() } - } - return adapter - } - @JvmStatic - private fun getSelectedValue(pane: JOptionPane, options: Array): Int { - val selectedValue = pane.value ?: return JOptionPane.CLOSED_OPTION - var counter = 0 - val maxCounter = options.size - while (counter < maxCounter) { - if (options[counter] == selectedValue) return counter - counter++ - } - return JOptionPane.CLOSED_OPTION - } - - @JvmStatic - fun wrapScrollPane(promptArea: JBTextArea?): JBScrollPane { - val scrollPane = JBScrollPane(promptArea) - scrollPane.horizontalScrollBarPolicy = JScrollPane.HORIZONTAL_SCROLLBAR_AS_NEEDED - scrollPane.verticalScrollBarPolicy = JScrollPane.VERTICAL_SCROLLBAR_AS_NEEDED - return scrollPane - } - - @JvmStatic - fun showCheckboxDialog( - promptMessage: String, - checkboxIds: Array, - checkboxDescriptions: Array, - ): Array { - val formBuilder = FormBuilder.createFormBuilder() - val checkboxMap = HashMap() - for (i in checkboxIds.indices) { - val checkbox = JCheckBox(checkboxDescriptions[i], null as Icon?, true) - checkboxMap[checkboxIds[i]] = checkbox - formBuilder.addComponent(checkbox) - } - val dialogResult = showOptionDialog(formBuilder.panel, "OK", title = promptMessage) - val selectedIds = ArrayList() - if (dialogResult == 0) { - for ((checkboxId, checkbox) in checkboxMap) { - if (checkbox.isSelected) { - selectedIds.add(checkboxId) - } + "boolean" -> if (uiVal is JCheckBox) { + newSettingsValue = uiVal.isSelected + } else if (uiVal is JTextComponent) { + newSettingsValue = java.lang.Boolean.parseBoolean(uiVal.text) } - } - return selectedIds.toTypedArray() - } - @JvmStatic - fun showRadioButtonDialog( - promptMessage: CharSequence, - vararg radioButtonDescriptions: CharSequence, - ): CharSequence? { - val formBuilder = FormBuilder.createFormBuilder() - val radioButtonMap = HashMap() - val buttonGroup = ButtonGroup() - for (i in radioButtonDescriptions.indices) { - val radioButton = JRadioButton(radioButtonDescriptions[i].toString(), null as Icon?, true) - radioButtonMap[radioButtonDescriptions[i].toString()] = radioButton - buttonGroup.add(radioButton) - formBuilder.addComponent(radioButton) - } - val dialogResult = showOptionDialog(formBuilder.panel, "OK", title = promptMessage.toString()) - if (dialogResult == 0) { - for ((radioButtonId, radioButton) in radioButtonMap) { - if (radioButton.isSelected) { - return radioButtonId - } + else -> if (Enum::class.java.isAssignableFrom(settingsField.returnType.javaType as Class<*>)) { + if (uiVal is ComboBox<*>) { + val comboBox = uiVal + val item = comboBox.item + val enumClass = settingsField.returnType.javaType as Class?> + val string = item.toString() + newSettingsValue = findValue(enumClass, string) + } } + } + settingsField.setter.call(settings, newSettingsValue) + } catch (e: Throwable) { + throw RuntimeException("Error processing $settingsField", e) } - return null - } - - @JvmStatic - fun build( - component: T, - fillVertically: Boolean = true, - formBuilder: FormBuilder = FormBuilder.createFormBuilder(), - ): JPanel? { - addKotlinFields(component, formBuilder, fillVertically) - return formBuilder.addComponentFillVertically(JPanel(), 0).panel + } } + } - @JvmStatic - fun showDialog( - project: Project?, - uiClass: Class, - configClass: Class, - title: String = "Generate Project", - onComplete: (C) -> Unit = { _ -> }, - ): C? { - val component = uiClass.getConstructor().newInstance() - val config = configClass.getConstructor().newInstance() - val dialog = object : DialogWrapper(project) { - init { - this.init() - this.title = title - this.setOKButtonText("Generate") - this.setCancelButtonText("Cancel") - this.isResizable = true - //this.setPreferredFocusedComponent(this) - //this.setContent(this) - } - - override fun createCenterPanel(): JComponent? { - return build(component) - } - } - dialog.show() - if (dialog.isOK) { - readKotlinUI(component, config) - onComplete(config) + private fun findValue(enumClass: Class?>, string: String): Enum<*>? { + enumClass.enumConstants?.filter { it?.name?.compareTo(string, true) == 0 }?.forEach { return it } + return java.lang.Enum.valueOf( + enumClass, string + ) + } + + fun writeKotlinUIViaReflection(settings: T, component: R) { + val componentClass: Class<*> = component.javaClass + val declaredUIFields = componentClass.kotlin.memberProperties.map { it.name }.toSet() + val memberProperties = settings.javaClass.kotlin.memberProperties + val publicProperties = memberProperties.filter { + it.visibility == KVisibility.PUBLIC //&& it is KMutableProperty<*> + } + for (settingsField in publicProperties) { + val fieldName = settingsField.name + try { + if (!declaredUIFields.contains(fieldName)) continue + val uiField: KProperty1 = + (componentClass.kotlin.memberProperties.find { it.name == fieldName } as KProperty1?)!! + val settingsVal = settingsField.get(settings) ?: continue + var uiVal = uiField.get(component) + if (uiVal is JScrollPane) { + uiVal = uiVal.viewport.view + } + when (settingsField.returnType.javaType.typeName) { + "java.lang.String" -> if (uiVal is JTextComponent) { + uiVal.text = settingsVal.toString() + } else if (uiVal is ComboBox<*>) { + uiVal.item = settingsVal.toString() + } + + "int", "java.lang.Integer" -> if (uiVal is JTextComponent) { + uiVal.text = (settingsVal as Int).toString() + } + + "long" -> if (uiVal is JTextComponent) { + uiVal.text = (settingsVal as Int).toLong().toString() + } + + "boolean" -> if (uiVal is JCheckBox) { + uiVal.isSelected = (settingsVal as Boolean) + } else if (uiVal is JTextComponent) { + uiVal.text = java.lang.Boolean.toString((settingsVal as Boolean)) + } + + "double", "java.lang.Double" -> if (uiVal is JTextComponent) { + uiVal.text = (settingsVal as Double).toString() + } + + else -> if (uiVal is ComboBox<*>) { + uiVal.item = settingsVal.toString() + } + } + } catch (e: Throwable) { + throw RuntimeException("Error processing $settingsField", e) + } + } + } + + private fun addKotlinFields(ui: T, formBuilder: FormBuilder, fillVertically: Boolean) { + var first = true + for (field in ui.javaClass.kotlin.memberProperties) { + if (field.javaField == null) continue + try { + val nameAnnotation = field.annotations.find { it is Name } as Name? + val component = field.get(ui) as JComponent + if (nameAnnotation != null) { + if (first && fillVertically) { + first = false + formBuilder.addLabeledComponentFillVertically(nameAnnotation.value + ": ", component) + } else { + formBuilder.addLabeledComponent(JBLabel(nameAnnotation.value + ": "), component, 1, false) + } + } else { + formBuilder.addComponentToRightColumn(component, 1) + } + } catch (e: IllegalAccessException) { + throw RuntimeException(e) + } catch (e: Throwable) { + error(log, "Error processing " + field.name, e) + } + } + } + + private fun getMaximumSize(factor: Double): Dimension { + val screenSize = Toolkit.getDefaultToolkit().screenSize + return Dimension((screenSize.getWidth() * factor).toInt(), (screenSize.getHeight() * factor).toInt()) + } + + private fun showOptionDialog(mainPanel: JPanel?, vararg options: Any, title: String, modal: Boolean = true): Int { + val pane = getOptionPane(mainPanel, options) + val rootFrame = JOptionPane.getRootFrame() + pane.componentOrientation = rootFrame.componentOrientation + val dialog = JDialog(rootFrame, title, modal) + dialog.componentOrientation = rootFrame.componentOrientation + + val latch = if (!modal) CountDownLatch(1) else null + configure(dialog, pane, latch) + dialog.isVisible = true + if (!modal) latch?.await() + + dialog.dispose() + return getSelectedValue(pane, options) + } + + private fun getOptionPane( + mainPanel: JPanel?, + options: Array, + ): JOptionPane { + val pane = JOptionPane( + mainPanel, JOptionPane.PLAIN_MESSAGE, JOptionPane.NO_OPTION, null, options, options[0] + ) + pane.initialValue = options[0] + return pane + } + + private fun configure(dialog: JDialog, pane: JOptionPane, latch: CountDownLatch? = null) { + val contentPane = dialog.contentPane + contentPane.layout = BorderLayout() + contentPane.add(pane, BorderLayout.CENTER) + + if (JDialog.isDefaultLookAndFeelDecorated() && UIManager.getLookAndFeel().supportsWindowDecorations) { + dialog.isUndecorated = true + pane.rootPane.windowDecorationStyle = JRootPane.PLAIN_DIALOG + } + dialog.isResizable = true + dialog.maximumSize = getMaximumSize(0.9) + dialog.pack() + dialog.setLocationRelativeTo(null as Component?) + val adapter: WindowAdapter = windowAdapter(pane, dialog) + dialog.addWindowListener(adapter) + dialog.addWindowFocusListener(adapter) + dialog.addComponentListener(object : ComponentAdapter() { + override fun componentShown(ce: ComponentEvent) { + // reset value to ensure closing works properly + pane.value = JOptionPane.UNINITIALIZED_VALUE + } + }) + pane.addPropertyChangeListener { event: PropertyChangeEvent -> + if (dialog.isVisible && event.source === pane && event.propertyName == JOptionPane.VALUE_PROPERTY && event.newValue != null && event.newValue !== JOptionPane.UNINITIALIZED_VALUE) { + dialog.isVisible = false + latch?.countDown() + } + } + + pane.selectInitialValue() + } + + private fun windowAdapter(pane: JOptionPane, dialog: JDialog): WindowAdapter { + val adapter: WindowAdapter = object : WindowAdapter() { + private var gotFocus = false + override fun windowClosing(we: WindowEvent) { + pane.value = null + } + + override fun windowClosed(e: WindowEvent) { + pane.removePropertyChangeListener { event: PropertyChangeEvent -> + if (dialog.isVisible && event.source === pane && event.propertyName == JOptionPane.VALUE_PROPERTY && event.newValue != null && event.newValue !== JOptionPane.UNINITIALIZED_VALUE) { + dialog.isVisible = false + } + } + dialog.contentPane.removeAll() + } + + override fun windowGainedFocus(we: WindowEvent) { + if (!gotFocus) { + pane.selectInitialValue() + gotFocus = true + } + } + } + return adapter + } + + private fun getSelectedValue(pane: JOptionPane, options: Array): Int { + val selectedValue = pane.value ?: return JOptionPane.CLOSED_OPTION + var counter = 0 + val maxCounter = options.size + while (counter < maxCounter) { + if (options[counter] == selectedValue) return counter + counter++ + } + return JOptionPane.CLOSED_OPTION + } + + private fun wrapScrollPane(promptArea: JBTextArea?): JBScrollPane { + val scrollPane = JBScrollPane(promptArea) + scrollPane.horizontalScrollBarPolicy = JScrollPane.HORIZONTAL_SCROLLBAR_AS_NEEDED + scrollPane.verticalScrollBarPolicy = JScrollPane.VERTICAL_SCROLLBAR_AS_NEEDED + return scrollPane + } + + fun showCheckboxDialog( + promptMessage: String, + checkboxIds: Array, + checkboxDescriptions: Array, + ): Array { + val formBuilder = FormBuilder.createFormBuilder() + val checkboxMap = HashMap() + for (i in checkboxIds.indices) { + val checkbox = JCheckBox(checkboxDescriptions[i], null as Icon?, true) + checkboxMap[checkboxIds[i]] = checkbox + formBuilder.addComponent(checkbox) + } + val dialogResult = showOptionDialog(formBuilder.panel, "OK", title = promptMessage) + val selectedIds = ArrayList() + if (dialogResult == 0) { + for ((checkboxId, checkbox) in checkboxMap) { + if (checkbox.isSelected) { + selectedIds.add(checkboxId) + } + } + } + return selectedIds.toTypedArray() + } + + fun showRadioButtonDialog( + promptMessage: CharSequence, + vararg radioButtonDescriptions: CharSequence, + ): CharSequence? { + val formBuilder = FormBuilder.createFormBuilder() + val radioButtonMap = HashMap() + val buttonGroup = ButtonGroup() + for (i in radioButtonDescriptions.indices) { + val radioButton = JRadioButton(radioButtonDescriptions[i].toString(), null as Icon?, true) + radioButtonMap[radioButtonDescriptions[i].toString()] = radioButton + buttonGroup.add(radioButton) + formBuilder.addComponent(radioButton) + } + val dialogResult = showOptionDialog(formBuilder.panel, "OK", title = promptMessage.toString()) + if (dialogResult == 0) { + for ((radioButtonId, radioButton) in radioButtonMap) { + if (radioButton.isSelected) { + return radioButtonId + } + } + } + return null + } + + fun buildFormViaReflection( + component: T, + fillVertically: Boolean = true, + formBuilder: FormBuilder = FormBuilder.createFormBuilder(), + ): JPanel? { + addKotlinFields(component, formBuilder, fillVertically) + return formBuilder.addComponentFillVertically(JPanel(), 0).panel + } + + fun showDialog( + project: Project?, + uiClass: Class, + configClass: Class, + title: String = "Generate Project", + onComplete: (C) -> Unit = { _ -> }, + ): C? { + val component = uiClass.getConstructor().newInstance() + val config = configClass.getConstructor().newInstance() + val dialog = object : DialogWrapper(project) { + init { + this.init() + this.title = title + this.setOKButtonText("Generate") + this.setCancelButtonText("Cancel") + this.isResizable = true + //this.setPreferredFocusedComponent(this) + //this.setContent(this) + } + + override fun createCenterPanel(): JComponent? { + return buildFormViaReflection(component) + } + } + dialog.show() + if (dialog.isOK) { + readKotlinUIViaReflection(component, config) + onComplete(config) + } + return config + } + + fun getSelectedFolder(e: AnActionEvent): VirtualFile? { + val dataContext = e.dataContext + val data = PlatformDataKeys.VIRTUAL_FILE.getData(dataContext) + if (data != null && data.isDirectory) { + return data + } + val editor = PlatformDataKeys.EDITOR.getData(dataContext) + if (editor != null) { + val file = FileDocumentManager.getInstance().getFile(editor.document) + if (file != null) { + return file.parent + } + } + return null + } + + fun getSelectedFile(e: AnActionEvent): VirtualFile? { + val dataContext = e.dataContext + val data = PlatformDataKeys.VIRTUAL_FILE.getData(dataContext) + if (data != null && !data.isDirectory) { + return data + } + val editor = PlatformDataKeys.EDITOR.getData(dataContext) + if (editor != null) { + val file = FileDocumentManager.getInstance().getFile(editor.document) + if (file != null) { + return file + } + } + return null + } + + fun writeableFn( + event: AnActionEvent, + fn: () -> Runnable, + ): Runnable { + val runnable = AtomicReference() + WriteCommandAction.runWriteCommandAction(event.project) { runnable.set(fn()) } + return runnable.get() + } + + class ModalTask( + project: Project, title: String, canBeCancelled: Boolean, val task: (ProgressIndicator) -> T + ) : Task.WithResult(project, title, canBeCancelled), Supplier { + private val result = AtomicReference() + private val isError = AtomicBoolean(false) + private val error = AtomicReference() + private val semaphore = Semaphore(0) + override fun compute(indicator: ProgressIndicator): T? { + val currentThread = Thread.currentThread() + val threads = ArrayList() + val scheduledFuture = scheduledPool.scheduleAtFixedRate({ + if (indicator.isCanceled) { + threads.forEach { it.interrupt() } + } + }, 0, 1, TimeUnit.SECONDS) + threads.add(currentThread) + return try { + result.set(task(indicator)) + result.get() + } catch (e: Throwable) { + error(log, "Error running task", e) + isError.set(true) + error.set(e) + null + } finally { + semaphore.release() + threads.remove(currentThread) + scheduledFuture.cancel(true) + } + } + + override fun get(): T { + semaphore.acquire() + semaphore.release() + if (isError.get()) { + throw error.get() + } + return result.get() + } + + } + + class BgTask( + project: Project, title: String, canBeCancelled: Boolean, val task: (ProgressIndicator) -> T + ) : Task.Backgroundable(project, title, canBeCancelled, DEAF), Supplier { + + private val result = AtomicReference() + private val isError = AtomicBoolean(false) + private val error = AtomicReference() + private val semaphore = Semaphore(0) + override fun run(indicator: ProgressIndicator) { + val currentThread = Thread.currentThread() + val threads = ArrayList() + val scheduledFuture = scheduledPool.scheduleAtFixedRate({ + if (indicator.isCanceled) { + threads.forEach { it.interrupt() } + } + }, 0, 1, TimeUnit.SECONDS) + threads.add(currentThread) + try { + val result = task(indicator) + this.result.set(result) + } catch (e: Throwable) { + error(log, "Error running task", e) + error.set(e) + isError.set(true) + } finally { + semaphore.release() + threads.remove(currentThread) + scheduledFuture.cancel(true) + } + } + + override fun get(): T { + semaphore.acquire() + semaphore.release() + if (isError.get()) { + throw error.get() + } + return result.get() + } + } + + fun run( + project: Project?, + title: String?, + canBeCancelled: Boolean = true, + suppressProgress: Boolean = true, + task: (ProgressIndicator) -> T, + ): T { + return if (project == null || suppressProgress == AppSettingsState.instance.editRequests) { + checkApiKey() + task(AbstractProgressIndicatorBase()) + } else { + checkApiKey() + val t = if (AppSettingsState.instance.modalTasks) ModalTask(project, title ?: "", canBeCancelled, task) + else BgTask(project, title ?: "", canBeCancelled, task) + ProgressManager.getInstance().run(t) + t.get() + } + } + + fun checkApiKey(k: String = AppSettingsState.instance.apiKey): String { + var key = k + if (key.isEmpty()) { + synchronized(OpenAIClient::class.java) { + key = AppSettingsState.instance.apiKey + if (key.isEmpty()) { + key = queryAPIKey()?.toString() ?: "" + if (key.isNotEmpty()) AppSettingsState.instance.apiKey = key } - return config + } } + return key + } - @JvmStatic - fun getSelectedFolder(e: AnActionEvent): VirtualFile? { - val dataContext = e.dataContext - val data = PlatformDataKeys.VIRTUAL_FILE.getData(dataContext) - if (data != null && data.isDirectory) { - return data - } - val editor = PlatformDataKeys.EDITOR.getData(dataContext) - if (editor != null) { - val file = FileDocumentManager.getInstance().getFile(editor.document) - if (file != null) { - return file.parent - } - } - return null - } - @JvmStatic - fun getSelectedFile(e: AnActionEvent): VirtualFile? { - val dataContext = e.dataContext - val data = PlatformDataKeys.VIRTUAL_FILE.getData(dataContext) - if (data != null && !data.isDirectory) { - return data - } - val editor = PlatformDataKeys.EDITOR.getData(dataContext) - if (editor != null) { - val file = FileDocumentManager.getInstance().getFile(editor.document) - if (file != null) { - return file - } - } - return null - } + fun map( + moderateAsync: ListenableFuture, + o: com.google.common.base.Function, + ): ListenableFuture = Futures.transform(moderateAsync, o::apply, pool) - @JvmStatic - fun writeableFn( - event: AnActionEvent, - fn: () -> Runnable, - ): Runnable { - val runnable = AtomicReference() - WriteCommandAction.runWriteCommandAction(event.project) { runnable.set(fn()) } - return runnable.get() + fun filterStringResult( + indent: CharSequence = "", + stripUnbalancedTerminators: Boolean = true, + ): (CharSequence) -> CharSequence { + return { text -> + var result: CharSequence = text.toString().trim { it <= ' ' } + if (stripUnbalancedTerminators) { + result = StringUtil.stripUnbalancedTerminators(result) + } + result = IndentedText.fromString(result.toString()).withIndent(indent).toString() + indent.toString() + result } + } - class ModalTask( - project: Project, title: String, canBeCancelled: Boolean, val task: (ProgressIndicator) -> T - ) : Task.WithResult(project, title, canBeCancelled), Supplier { - private val result = AtomicReference() - private val isError = AtomicBoolean(false) - private val error = AtomicReference() - private val semaphore = Semaphore(0) - override fun compute(indicator: ProgressIndicator): T? { - val currentThread = Thread.currentThread() - val threads = ArrayList() - val scheduledFuture = scheduledPool.scheduleAtFixedRate({ - if (indicator.isCanceled) { - threads.forEach { it.interrupt() } - } - }, 0, 1, TimeUnit.SECONDS) - threads.add(currentThread) - return try { - result.set(task(indicator)) - result.get() - } catch (e: Throwable) { - error(log, "Error running task", e) - isError.set(true) - error.set(e) - null - } finally { - semaphore.release() - threads.remove(currentThread) - scheduledFuture.cancel(true) - } - } - override fun get(): T { - semaphore.acquire() - semaphore.release() - if (isError.get()) { - throw error.get() - } - return result.get() - } - - } + fun logAction(message: String) { + actionLog += message + } - class BgTask( - project: Project, title: String, canBeCancelled: Boolean, val task: (ProgressIndicator) -> T - ) : Task.Backgroundable(project, title, canBeCancelled, DEAF), Supplier { - - private val result = AtomicReference() - private val isError = AtomicBoolean(false) - private val error = AtomicReference() - private val semaphore = Semaphore(0) - override fun run(indicator: ProgressIndicator) { - val currentThread = Thread.currentThread() - val threads = ArrayList() - val scheduledFuture = scheduledPool.scheduleAtFixedRate({ - if (indicator.isCanceled) { - threads.forEach { it.interrupt() } - } - }, 0, 1, TimeUnit.SECONDS) - threads.add(currentThread) - try { - val result = task(indicator) - this.result.set(result) - } catch (e: Throwable) { - error(log, "Error running task", e) - error.set(e) - isError.set(true) - } finally { - semaphore.release() - threads.remove(currentThread) - scheduledFuture.cancel(true) - } - } - - override fun get(): T { - semaphore.acquire() - semaphore.release() - if (isError.get()) { - throw error.get() - } - return result.get() - } - } - @JvmStatic - fun run( - project: Project?, - title: String?, - canBeCancelled: Boolean = true, - suppressProgress: Boolean = true, - task: (ProgressIndicator) -> T, - ): T { - return if (project == null || suppressProgress == AppSettingsState.instance.editRequests) { - checkApiKey() - task(AbstractProgressIndicatorBase()) - } else { - checkApiKey() - val t = if (AppSettingsState.instance.modalTasks) ModalTask(project, title ?: "", canBeCancelled, task) - else BgTask(project, title ?: "", canBeCancelled, task) - ProgressManager.getInstance().run(t) - t.get() - } - } + fun error(log: org.slf4j.Logger, msg: String, e: Throwable) { + log?.error(msg, e) + errorLog += Pair(msg, e) + singleThreadPool.submit { + if (AppSettingsState.instance.suppressErrors) { + return@submit + } else if (e.matches { ModerationException::class.java.isAssignableFrom(it.javaClass) }) { + JOptionPane.showMessageDialog( + null, e.message, "This request was rejected by OpenAI Moderation", JOptionPane.WARNING_MESSAGE + ) + } else if (e.matches { + java.lang.InterruptedException::class.java.isAssignableFrom(it.javaClass) && it.message?.contains( + "sleep interrupted" + ) == true + }) { + JOptionPane.showMessageDialog( + null, "This request was cancelled by the user", "User Cancelled Request", JOptionPane.WARNING_MESSAGE + ) + } else if (e.matches { IOException::class.java.isAssignableFrom(it.javaClass) && it.message?.contains("Incorrect API key") == true }) { - @JvmStatic - fun checkApiKey(k: String = AppSettingsState.instance.apiKey): String { - var key = k - if (key.isEmpty()) { - synchronized(OpenAIClient::class.java) { - key = AppSettingsState.instance.apiKey - if (key.isEmpty()) { - key = queryAPIKey()!!.toString() - AppSettingsState.instance.apiKey = key - } - } - } - return key - } + val formBuilder = FormBuilder.createFormBuilder() - @JvmStatic - val threadFactory: ThreadFactory = ThreadFactoryBuilder().setNameFormat("API Thread %d").build() - - @JvmStatic - val pool: ListeningExecutorService = MoreExecutors.listeningDecorator( - ThreadPoolExecutor( - /* corePoolSize = */ AppSettingsState.instance.apiThreads, - /* maximumPoolSize = */ AppSettingsState.instance.apiThreads, - /* keepAliveTime = */ 0L, /* unit = */ TimeUnit.MILLISECONDS, - /* workQueue = */ LinkedBlockingQueue(), - /* threadFactory = */ threadFactory, - /* handler = */ ThreadPoolExecutor.AbortPolicy() + formBuilder.addLabeledComponent( + "Error", JLabel("The API key was rejected by the server.") ) - ) - @JvmStatic - val scheduledPool: ListeningScheduledExecutorService = - MoreExecutors.listeningDecorator(ScheduledThreadPoolExecutor(1, threadFactory)) - - @JvmStatic - fun map( - moderateAsync: ListenableFuture, - o: com.google.common.base.Function, - ): ListenableFuture = Futures.transform(moderateAsync, o::apply, pool) - - @JvmStatic - fun filterStringResult( - indent: CharSequence = "", - stripUnbalancedTerminators: Boolean = true, - ): (CharSequence) -> CharSequence { - return { text -> - var result: CharSequence = text.toString().trim { it <= ' ' } - if (stripUnbalancedTerminators) { - result = StringUtil.stripUnbalancedTerminators(result) - } - result = IndentedText.fromString(result.toString()).withIndent(indent).toString() - indent.toString() + result + val apiKeyInput = JBPasswordField() + //bugReportTextArea.rows = 40 + apiKeyInput.columns = 80 + apiKeyInput.isEditable = true + //apiKeyInput.text = """""".trimMargin() + formBuilder.addLabeledComponent("API Key", apiKeyInput) + + val openAccountButton = JXButton("Open Account Page") + openAccountButton.addActionListener { + Desktop.getDesktop().browse(URI("https://platform.openai.com/account/api-keys")) + } + formBuilder.addLabeledComponent("OpenAI Account", openAccountButton) + + val testButton = JXButton("Test Key") + testButton.addActionListener { + val apiKey = apiKeyInput.password.joinToString("") + try { + OpenAIClient(key = apiKey).listModels() + JOptionPane.showMessageDialog( + null, + "The API key was accepted by the server. The new value will be saved.", + "Success", + JOptionPane.INFORMATION_MESSAGE + ) + AppSettingsState.instance.apiKey = apiKey + } catch (e: Exception) { + JOptionPane.showMessageDialog( + null, "The API key was rejected by the server.", "Failure", JOptionPane.WARNING_MESSAGE + ) + return@addActionListener + } } - } - - private val errorLog = mutableListOf>() - private val actionLog = mutableListOf() + formBuilder.addLabeledComponent("Validation", testButton) + val showOptionDialog = showOptionDialog( + formBuilder.panel, "Dismiss", title = "Error", modal = true + ) + log.info("showOptionDialog = $showOptionDialog") + } else if (e.matches { ScriptException::class.java.isAssignableFrom(it.javaClass) }) { + val scriptException = e.get { ScriptException::class.java.isAssignableFrom(it.javaClass) } as ScriptException? + val dynamicActionException = + e.get { ActionSettingsRegistry.DynamicActionException::class.java.isAssignableFrom(it.javaClass) } as ActionSettingsRegistry.DynamicActionException? + val formBuilder = FormBuilder.createFormBuilder() - @JvmStatic - fun logAction(message: String) { - actionLog += message - } + formBuilder.addLabeledComponent( + "Error", JLabel("An error occurred while executing the dynamic action.") + ) - private val singleThreadPool = Executors.newSingleThreadExecutor() - - @JvmStatic - fun error(log: org.slf4j.Logger, msg: String, e: Throwable) { - log?.error(msg, e) - errorLog += Pair(msg, e) - singleThreadPool.submit { - if (AppSettingsState.instance.suppressErrors) { - return@submit - } else if (e.matches { ModerationException::class.java.isAssignableFrom(it.javaClass) }) { - JOptionPane.showMessageDialog( - null, - e.message, - "This request was rejected by OpenAI Moderation", - JOptionPane.WARNING_MESSAGE - ) - } else if (e.matches { - java.lang.InterruptedException::class.java.isAssignableFrom(it.javaClass) && it.message?.contains( - "sleep interrupted" - ) == true - }) { - JOptionPane.showMessageDialog( - null, - "This request was cancelled by the user", - "User Cancelled Request", - JOptionPane.WARNING_MESSAGE - ) - } else if (e.matches { IOException::class.java.isAssignableFrom(it.javaClass) && it.message?.contains("Incorrect API key") == true }) { - - val formBuilder = FormBuilder.createFormBuilder() - - formBuilder.addLabeledComponent( - "Error", - JLabel("The API key was rejected by the server.") - ) - - val apiKeyInput = JBPasswordField() - //bugReportTextArea.rows = 40 - apiKeyInput.columns = 80 - apiKeyInput.isEditable = true - //apiKeyInput.text = """""".trimMargin() - formBuilder.addLabeledComponent("API Key", apiKeyInput) - - val openAccountButton = JXButton("Open Account Page") - openAccountButton.addActionListener { - Desktop.getDesktop().browse(URI("https://platform.openai.com/account/api-keys")) - } - formBuilder.addLabeledComponent("OpenAI Account", openAccountButton) - - val testButton = JXButton("Test Key") - testButton.addActionListener { - val apiKey = apiKeyInput.password.joinToString("") - try { - OpenAIClient(key = apiKey).listModels() - JOptionPane.showMessageDialog( - null, - "The API key was accepted by the server. The new value will be saved.", - "Success", - JOptionPane.INFORMATION_MESSAGE - ) - AppSettingsState.instance.apiKey = apiKey - } catch (e: Exception) { - JOptionPane.showMessageDialog( - null, - "The API key was rejected by the server.", - "Failure", - JOptionPane.WARNING_MESSAGE - ) - return@addActionListener - } - } - formBuilder.addLabeledComponent("Validation", testButton) - val showOptionDialog = showOptionDialog( - formBuilder.panel, - "Dismiss", - title = "Error", - modal = true - ) - log.info("showOptionDialog = $showOptionDialog") - } else if (e.matches { ScriptException::class.java.isAssignableFrom(it.javaClass) }) { - val scriptException = - e.get { ScriptException::class.java.isAssignableFrom(it.javaClass) } as ScriptException? - val dynamicActionException = - e.get { ActionSettingsRegistry.DynamicActionException::class.java.isAssignableFrom(it.javaClass) } as ActionSettingsRegistry.DynamicActionException? - val formBuilder = FormBuilder.createFormBuilder() - - formBuilder.addLabeledComponent( - "Error", - JLabel("An error occurred while executing the dynamic action.") - ) - - val bugReportTextArea = JBTextArea() - bugReportTextArea.rows = 40 - bugReportTextArea.columns = 80 - bugReportTextArea.isEditable = false - bugReportTextArea.text = """ + val bugReportTextArea = JBTextArea() + bugReportTextArea.rows = 40 + bugReportTextArea.columns = 80 + bugReportTextArea.isEditable = false + bugReportTextArea.text = """ |Action Name: ${dynamicActionException?.actionSetting?.displayText} |Action ID: ${dynamicActionException?.actionSetting?.id} |Script Error: ${scriptException?.message} @@ -923,180 +853,158 @@ object UITools { |${toString(e)} |``` |""".trimMargin() - formBuilder.addLabeledComponent("Error Report", wrapScrollPane(bugReportTextArea)) - - if (dynamicActionException?.actionSetting?.isDynamic == false) { - val openButton = JXButton("Revert to Default") - openButton.addActionListener { - dynamicActionException?.actionSetting?.file?.delete() - } - formBuilder.addLabeledComponent("Revert Built-in Action", openButton) + formBuilder.addLabeledComponent("Error Report", wrapScrollPane(bugReportTextArea)) + + if (dynamicActionException?.actionSetting?.isDynamic == false) { + val openButton = JXButton("Revert to Default") + openButton.addActionListener { + dynamicActionException?.actionSetting?.file?.delete() + } + formBuilder.addLabeledComponent("Revert Built-in Action", openButton) + } + + if (null != dynamicActionException) { + val openButton = JXButton("Open Dynamic Action") + openButton.addActionListener { + dynamicActionException?.file?.let { + val project = ApplicationManager.getApplication().runReadAction { + com.intellij.openapi.project.ProjectManager.getInstance().openProjects.firstOrNull() + } + if (it.exists()) { + ApplicationManager.getApplication().invokeLater { + val virtualFile = LocalFileSystem.getInstance().refreshAndFindFileByIoFile(it) + FileEditorManager.getInstance(project!!).openFile(virtualFile!!, true) } + } else { + Thread { + showOptionDialog( + formBuilder.panel, "Dismiss", title = "Error - File Not Found", modal = true + ) + }.start() + } + } - if (null != dynamicActionException) { - val openButton = JXButton("Open Dynamic Action") - openButton.addActionListener { - dynamicActionException?.file?.let { - val project = ApplicationManager.getApplication().runReadAction { - com.intellij.openapi.project.ProjectManager.getInstance().openProjects.firstOrNull() - } - if (it.exists()) { - ApplicationManager.getApplication().invokeLater { - val virtualFile = LocalFileSystem.getInstance().refreshAndFindFileByIoFile(it) - FileEditorManager.getInstance(project!!).openFile(virtualFile!!, true) - } - } else { - Thread { - showOptionDialog( - formBuilder.panel, - "Dismiss", - title = "Error - File Not Found", - modal = true - ) - }.start() - } - } - - } - formBuilder.addLabeledComponent("View Code", openButton) - } + } + formBuilder.addLabeledComponent("View Code", openButton) + } - val supressFutureErrors = JCheckBox("Suppress Future Error Popups") - supressFutureErrors.isSelected = false - formBuilder.addComponent(supressFutureErrors) - - val showOptionDialog = showOptionDialog( - formBuilder.panel, - "Dismiss", - title = "Error", - modal = true - ) - log.info("showOptionDialog = $showOptionDialog") - if (supressFutureErrors.isSelected) { - AppSettingsState.instance.suppressErrors = true - } - } else { - val formBuilder = FormBuilder.createFormBuilder() - - formBuilder.addLabeledComponent( - "Error", - JLabel("Oops! Something went wrong. An error report has been generated. You can copy and paste the report below into a new issue on our Github page.") - ) - - val bugReportTextArea = JBTextArea() - bugReportTextArea.rows = 40 - bugReportTextArea.columns = 80 - bugReportTextArea.isEditable = false - bugReportTextArea.text = """ - |Log Message: $msg - |Error Message: ${e.message} - |Error Type: ${e.javaClass.name} - |API Base: ${AppSettingsState.instance.apiBase} - |Token Counter: ${AppSettingsState.instance.tokenCounter} - | - |OS: ${System.getProperty("os.name")} / ${System.getProperty("os.version")} / ${System.getProperty("os.arch")} - |Locale: ${Locale.getDefault().country} / ${Locale.getDefault().language} - | - |Error Details: - |``` - |${toString(e)} - |``` - | - |Action History: - | - |${actionLog.joinToString("\n") { "* ${it.replace("\n", "\n ")}" }} - | - |Error History: - | - |${ - errorLog.filter { it.second != e }.joinToString("\n") { - """ - |${it.first} - |``` - |${toString(it.second)} - |``` - |""".trimMargin() - } - } - |""".trimMargin() - formBuilder.addLabeledComponent("System Report", wrapScrollPane(bugReportTextArea)) + val supressFutureErrors = JCheckBox("Suppress Future Error Popups") + supressFutureErrors.isSelected = false + formBuilder.addComponent(supressFutureErrors) - val openButton = JXButton("Open New Issue on our Github page") - openButton.addActionListener { - Desktop.getDesktop().browse(URI("https://github.com/SimiaCryptus/intellij-aicoder/issues/new")) - } - formBuilder.addLabeledComponent("Report Issue/Request Help", openButton) - - val supressFutureErrors = JCheckBox("Suppress Future Error Popups") - supressFutureErrors.isSelected = false - formBuilder.addComponent(supressFutureErrors) - - val showOptionDialog = showOptionDialog( - formBuilder.panel, - "Dismiss", - title = "Error", - modal = true - ) - log.info("showOptionDialog = $showOptionDialog") - if (supressFutureErrors.isSelected) { - AppSettingsState.instance.suppressErrors = true - } - } + val showOptionDialog = showOptionDialog( + formBuilder.panel, "Dismiss", title = "Error", modal = true + ) + log.info("showOptionDialog = $showOptionDialog") + if (supressFutureErrors.isSelected) { + AppSettingsState.instance.suppressErrors = true } - } + } else { + val formBuilder = FormBuilder.createFormBuilder() - @JvmStatic - fun Throwable.matches(matchFn: (Throwable) -> Boolean): Boolean { - if (matchFn(this)) return true - if (this.cause != null && this.cause !== this) return this.cause!!.matches(matchFn) - return false - } + formBuilder.addLabeledComponent( + "Error", + JLabel("Oops! Something went wrong. An error report has been generated. You can copy and paste the report below into a new issue on our Github page.") + ) - @JvmStatic - fun Throwable.get(matchFn: (Throwable) -> Boolean): Throwable? { - if (matchFn(this)) return this - if (this.cause != null && this.cause !== this) return this.cause!!.get(matchFn) - return null - } + val bugReportTextArea = JBTextArea() + bugReportTextArea.rows = 40 + bugReportTextArea.columns = 80 + bugReportTextArea.isEditable = false + bugReportTextArea.text = """ + |Log Message: $msg + |Error Message: ${e.message} + |Error Type: ${e.javaClass.name} + |API Base: ${AppSettingsState.instance.apiBase} + |Token Counter: ${AppSettingsState.instance.tokenCounter} + | + |OS: ${System.getProperty("os.name")} / ${System.getProperty("os.version")} / ${System.getProperty("os.arch")} + |Locale: ${Locale.getDefault().country} / ${Locale.getDefault().language} + | + |Error Details: + |``` + |${toString(e)} + |``` + | + |Action History: + | + |${actionLog.joinToString("\n") { "* ${it.replace("\n", "\n ")}" }} + | + |Error History: + | + |${ + errorLog.filter { it.second != e }.joinToString("\n") { + """ + |${it.first} + |``` + |${toString(it.second)} + |``` + |""".trimMargin() + } + } + |""".trimMargin() + formBuilder.addLabeledComponent("System Report", wrapScrollPane(bugReportTextArea)) - @JvmStatic - fun toString(e: Throwable): String { - val sw = StringWriter() - val pw = PrintWriter(sw) - e.printStackTrace(pw) - return sw.toString() - } + val openButton = JXButton("Open New Issue on our Github page") + openButton.addActionListener { + Desktop.getDesktop().browse(URI("https://github.com/SimiaCryptus/intellij-aicoder/issues/new")) + } + formBuilder.addLabeledComponent("Report Issue/Request Help", openButton) + + val supressFutureErrors = JCheckBox("Suppress Future Error Popups") + supressFutureErrors.isSelected = false + formBuilder.addComponent(supressFutureErrors) - // Wrap JOptionPane.showInputDialog - @JvmStatic - fun showInputDialog( - parentComponent: Component?, - message: Any?, - title: String?, - messageType: Int - ): Any? { - val icon = null - val selectionValues = null - val initialSelectionValue = null - val pane = JOptionPane( - message, - messageType, - JOptionPane.OK_CANCEL_OPTION, - icon, - null, - null + val showOptionDialog = showOptionDialog( + formBuilder.panel, "Dismiss", title = "Error", modal = true ) - pane.wantsInput = true - pane.selectionValues = selectionValues - pane.initialSelectionValue = initialSelectionValue - //pane.isComponentOrientationLeftToRight = true - val dialog = pane.createDialog(parentComponent, title) - pane.selectInitialValue() - dialog.show() - dialog.dispose() - val value = pane.inputValue - return if (value == JOptionPane.UNINITIALIZED_VALUE) null else value - } + log.info("showOptionDialog = $showOptionDialog") + if (supressFutureErrors.isSelected) { + AppSettingsState.instance.suppressErrors = true + } + } + } + } + + private fun Throwable.matches(matchFn: (Throwable) -> Boolean): Boolean { + if (matchFn(this)) return true + if (this.cause != null && this.cause !== this) return this.cause!!.matches(matchFn) + return false + } + + fun Throwable.get(matchFn: (Throwable) -> Boolean): Throwable? { + if (matchFn(this)) return this + if (this.cause != null && this.cause !== this) return this.cause!!.get(matchFn) + return null + } + + fun toString(e: Throwable): String { + val sw = StringWriter() + val pw = PrintWriter(sw) + e.printStackTrace(pw) + return sw.toString() + } + + fun showInputDialog( + parentComponent: Component?, message: Any?, title: String?, messageType: Int + ): Any? { + val icon = null + val selectionValues = null + val initialSelectionValue = null + val pane = JOptionPane( + message, messageType, JOptionPane.OK_CANCEL_OPTION, icon, null, null + ) + pane.wantsInput = true + pane.selectionValues = selectionValues + pane.initialSelectionValue = initialSelectionValue + //pane.isComponentOrientationLeftToRight = true + val dialog = pane.createDialog(parentComponent, title) + pane.selectInitialValue() + dialog.show() + dialog.dispose() + val value = pane.inputValue + return if (value == JOptionPane.UNINITIALIZED_VALUE) null else value + } } diff --git a/src/test/kotlin/com/github/simiacryptus/aicoder/UITestUtil.kt b/src/test/kotlin/com/github/simiacryptus/aicoder/UITestUtil.kt index b4be777a..780332bc 100644 --- a/src/test/kotlin/com/github/simiacryptus/aicoder/UITestUtil.kt +++ b/src/test/kotlin/com/github/simiacryptus/aicoder/UITestUtil.kt @@ -144,7 +144,7 @@ class UITestUtil { val aiCoderMenuItem = getComponent("//div[@text='AI Coder']") aiCoderMenuItem.click() val point1 = aiCoderMenuItem.locationOnScreen - sleep(100) + sleep(500) val submenu = getComponent("""//div[@class="HeavyWeightWindow"][.//div[@class="MyMenu"]]//div[@class="HeavyWeightWindow"]//div[contains(@text, '$menuText')]""") val point2 = submenu.locationOnScreen diff --git a/src/test/kotlin/com/github/simiacryptus/aicoder/actions/code/RenameVariablesActionTest.kt b/src/test/kotlin/com/github/simiacryptus/aicoder/actions/code/RenameVariablesActionTest.kt index 2308ca4c..cddd579a 100644 --- a/src/test/kotlin/com/github/simiacryptus/aicoder/actions/code/RenameVariablesActionTest.kt +++ b/src/test/kotlin/com/github/simiacryptus/aicoder/actions/code/RenameVariablesActionTest.kt @@ -11,8 +11,8 @@ class RenameVariablesActionTest : ActionTestBase() { @Test fun testProcessing() { testScript_SelectionAction(object : RenameVariablesAction() { - override fun choose(renameSuggestions: Map): Array { - return renameSuggestions.keys.toTypedArray() + override fun choose(renameSuggestions: Map): Set { + return renameSuggestions.keys.toSet() } }, "/RenameVariablesActionTest.md") }