Skip to content

Commit

Permalink
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
chore: Convert actions to Kotlin
Browse files Browse the repository at this point in the history
reneleonhardt committed May 15, 2024
1 parent 9705ab7 commit 9f09aaf
Showing 39 changed files with 1,014 additions and 1,228 deletions.

This file was deleted.

This file was deleted.

This file was deleted.

This file was deleted.

46 changes: 0 additions & 46 deletions src/main/java/ee/carlrobert/codegpt/actions/TrackableAction.java

This file was deleted.

40 changes: 0 additions & 40 deletions src/main/java/ee/carlrobert/codegpt/actions/editor/AskAction.java

This file was deleted.

This file was deleted.

This file was deleted.

This file was deleted.

This file was deleted.

This file was deleted.

This file was deleted.

This file was deleted.

This file was deleted.

This file was deleted.

This file was deleted.

This file was deleted.

This file was deleted.

Original file line number Diff line number Diff line change
@@ -62,7 +62,7 @@ public ChatToolWindowTabbedPane getChatTabbedPane() {
return tabbedPane;
}

public void displaySelectedFilesNotification(List<ReferencedFile> referencedFiles) {
public void displaySelectedFilesNotification(List<? extends ReferencedFile> referencedFiles) {
if (referencedFiles.isEmpty()) {
return;
}
Original file line number Diff line number Diff line change
@@ -115,7 +115,7 @@ public void updateHighlightedTokens(String highlightedText) {
update();
}

public void updateReferencedFilesTokens(List<ReferencedFile> includedFiles) {
public void updateReferencedFilesTokens(List<? extends ReferencedFile> includedFiles) {
totalTokensDetails.setReferencedFilesTokens(includedFiles.stream()
.mapToInt(file -> encodingManager.countTokens(file.getFileContent()))
.sum());
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
package ee.carlrobert.codegpt.actions;

public enum ActionType {
package ee.carlrobert.codegpt.actions

enum class ActionType {
CLEAR_CHAT_WINDOW,
CREATE_NEW_CHAT,
DELETE_ALL_CONVERSATIONS,
Original file line number Diff line number Diff line change
@@ -0,0 +1,196 @@
package ee.carlrobert.codegpt.actions

import com.intellij.notification.Notification
import com.intellij.notification.NotificationType
import com.intellij.notification.Notifications
import com.intellij.openapi.actionSystem.ActionUpdateThread
import com.intellij.openapi.actionSystem.AnAction
import com.intellij.openapi.actionSystem.AnActionEvent
import com.intellij.openapi.application.ApplicationManager
import com.intellij.openapi.command.WriteCommandAction
import com.intellij.openapi.editor.Document
import com.intellij.openapi.editor.Editor
import com.intellij.openapi.editor.ex.EditorEx
import com.intellij.openapi.project.Project
import com.intellij.openapi.ui.Messages
import com.intellij.openapi.vcs.VcsDataKeys
import com.intellij.openapi.vcs.ui.CommitMessage
import com.intellij.vcs.commit.CommitWorkflowUi
import ee.carlrobert.codegpt.CodeGPTBundle
import ee.carlrobert.codegpt.EncodingManager
import ee.carlrobert.codegpt.Icons
import ee.carlrobert.codegpt.completions.CompletionRequestService
import ee.carlrobert.codegpt.completions.CompletionRequestService.isRequestAllowed
import ee.carlrobert.codegpt.settings.GeneralSettings
import ee.carlrobert.codegpt.settings.configuration.CommitMessageTemplate
import ee.carlrobert.codegpt.settings.service.ServiceType
import ee.carlrobert.codegpt.settings.service.ServiceType.YOU
import ee.carlrobert.codegpt.ui.OverlayUtil
import ee.carlrobert.llm.client.openai.completion.ErrorDetails
import ee.carlrobert.llm.completion.CompletionEventListener
import okhttp3.sse.EventSource
import java.io.BufferedReader
import java.io.File
import java.io.IOException
import java.io.InputStreamReader
import java.nio.file.Files
import java.nio.file.Path
import java.util.Objects
import java.util.Optional
import java.util.stream.Collectors.joining

class GenerateGitCommitMessageAction : AnAction(
CodeGPTBundle.get("action.generateCommitMessage.title"),
CodeGPTBundle.get("action.generateCommitMessage.description"),
Icons.Sparkle) {

private val encodingManager: EncodingManager = EncodingManager.getInstance()

override fun update(event: AnActionEvent) {
val commitWorkflowUi = event.getData(VcsDataKeys.COMMIT_WORKFLOW_UI)
if (GeneralSettings.isSelected(YOU) || commitWorkflowUi == null) {
event.presentation.isVisible = false
return
}

val callAllowed = isRequestAllowed()
event.presentation.isEnabled = callAllowed
&& CommitWorkflowChanges(commitWorkflowUi).isFilesSelected
event.presentation.text = CodeGPTBundle.get(
if (callAllowed)
"action.generateCommitMessage.title"
else "action.generateCommitMessage.missingCredentials")
}

override fun actionPerformed(event: AnActionEvent) {
val project = event.project ?: return
project.basePath ?: return

val gitDiff = getGitDiff(event, project)
val tokenCount = encodingManager.countTokens(gitDiff)
if (tokenCount > MAX_TOKEN_COUNT_WARNING
&& OverlayUtil.showTokenSoftLimitWarningDialog(tokenCount) != Messages.OK) {
return
}

val editor = getCommitMessageEditor(event) as? EditorEx ?: return
editor.setCaretVisible(false)
CompletionRequestService.getInstance().generateCommitMessageAsync(
project.getService(CommitMessageTemplate::class.java).getSystemPrompt(),
gitDiff,
getEventListener(project, editor.document))
}

override fun getActionUpdateThread(): ActionUpdateThread {
return ActionUpdateThread.EDT
}

private fun getEventListener(project: Project, document: Document): CompletionEventListener<String> {
return object : CompletionEventListener<String> {
private val messageBuilder = StringBuilder()

override fun onMessage(message: String?, eventSource: EventSource) {
messageBuilder.append(message)
val application = ApplicationManager.getApplication()
application.invokeLater {
application.runWriteAction {
WriteCommandAction.runWriteCommandAction(project) {
document.setText(messageBuilder)
}
}
}
}

override fun onError(error: ErrorDetails, ex: Throwable) {
Notifications.Bus.notify(Notification(
"CodeGPT Notification Group",
"CodeGPT",
error.message,
NotificationType.ERROR))
}
}
}

private fun getCommitMessageEditor(event: AnActionEvent): Editor? {
return (event.getData(VcsDataKeys.COMMIT_MESSAGE_CONTROL) as? CommitMessage)?.editorField?.editor
}

private fun getGitDiff(event: AnActionEvent, project: Project): String {
val commitWorkflowUi = Optional.ofNullable(event.getData(VcsDataKeys.COMMIT_WORKFLOW_UI))
.orElseThrow { IllegalStateException("Could not retrieve commit workflow ui.") }
val changes = CommitWorkflowChanges(commitWorkflowUi)
val projectBasePath = project.basePath
val gitDiff = getGitDiff(projectBasePath, changes.includedVersionedFilePaths, false)
val stagedGitDiff = getGitDiff(projectBasePath, changes.includedVersionedFilePaths, true)
val newFilesContent = getNewFilesDiff(projectBasePath, changes.includedUnversionedFilePaths)

return mapOf(
"Git diff" to gitDiff,
"Staged git diff" to stagedGitDiff,
"New files" to newFilesContent)
.entries.stream()
.filter { !it.value.isNullOrBlank() }
.map { "%s:%n%s".formatted(it.key, it.value) }
.collect(joining("\n\n"))
}

private fun getGitDiff(projectPath: String?, filePaths: List<String>, cached: Boolean): String {
if (filePaths.isEmpty()) {
return ""
}

val process = createGitDiffProcess(projectPath, filePaths, cached)
return BufferedReader(InputStreamReader(process.inputStream)).lines().collect(joining("\n"))
}

private fun getNewFilesDiff(projectPath: String?, filePaths: List<String>): String? {
return filePaths.stream()
.map { pathString ->
val filePath = Path.of(pathString)
val relativePath = Path.of(projectPath).relativize(filePath)
try {
return@map """
New file '$relativePath' content:
${Files.readString(filePath)}
""".trimIndent()
} catch (ignored: IOException) {
return@map null
}
}
.filter(Objects::nonNull)
.collect(joining("\n"))
}

private fun createGitDiffProcess(projectPath: String?, filePaths: List<String>, cached: Boolean): Process {
val command = mutableListOf("git", "diff")
if (cached) {
command.add("--cached")
}
command.addAll(filePaths)

val processBuilder = ProcessBuilder(command)
processBuilder.directory(File(projectPath))
try {
return processBuilder.start()
} catch (ex: IOException) {
throw RuntimeException("Unable to start git diff process", ex)
}
}

internal class CommitWorkflowChanges(commitWorkflowUi: CommitWorkflowUi) {
val includedVersionedFilePaths: List<String> = commitWorkflowUi.getIncludedChanges().stream()
.map { it.virtualFile }
.filter(Objects::nonNull)
.map { it!!.path }
.toList()
val includedUnversionedFilePaths: List<String> =
commitWorkflowUi.getIncludedUnversionedFiles().stream().map { it.path }.toList()

val isFilesSelected =
includedVersionedFilePaths.isNotEmpty() || includedUnversionedFilePaths.isNotEmpty()
}

companion object {
const val MAX_TOKEN_COUNT_WARNING: Int = 4096
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,205 @@
package ee.carlrobert.codegpt.actions

import com.intellij.openapi.actionSystem.AnAction
import com.intellij.openapi.actionSystem.AnActionEvent
import com.intellij.openapi.actionSystem.CommonDataKeys
import com.intellij.openapi.actionSystem.DataContext
import com.intellij.openapi.diagnostic.Logger
import com.intellij.openapi.project.Project
import com.intellij.openapi.ui.DialogBuilder
import com.intellij.openapi.ui.DialogWrapper
import com.intellij.openapi.vfs.VirtualFile
import com.intellij.psi.PsiElement
import com.intellij.ui.CheckboxTreeListener
import com.intellij.ui.CheckedTreeNode
import com.intellij.ui.ScrollPaneFactory
import com.intellij.ui.components.JBLabel
import com.intellij.ui.components.JBTextArea
import com.intellij.util.ui.FormBuilder
import com.intellij.util.ui.JBUI
import com.intellij.util.ui.UI.PanelFactory
import ee.carlrobert.codegpt.CodeGPTBundle
import ee.carlrobert.codegpt.CodeGPTKeys
import ee.carlrobert.codegpt.EncodingManager
import ee.carlrobert.codegpt.ReferencedFile
import ee.carlrobert.codegpt.settings.IncludedFilesSettings
import ee.carlrobert.codegpt.settings.IncludedFilesSettingsState
import ee.carlrobert.codegpt.ui.UIUtil
import ee.carlrobert.codegpt.ui.checkbox.FileCheckboxTree
import ee.carlrobert.codegpt.ui.checkbox.VirtualFileCheckboxTree
import ee.carlrobert.codegpt.util.file.FileUtil.convertLongValue
import java.awt.Dimension
import java.io.IOException
import java.nio.file.Files
import java.nio.file.Paths
import javax.swing.JButton
import javax.swing.JComponent
import javax.swing.SwingUtilities

class IncludeFilesInContextAction @JvmOverloads constructor(
customTitleKey: String? = "action.includeFilesInContext.title") :
AnAction(CodeGPTBundle.get(customTitleKey!!)) {

override fun actionPerformed(e: AnActionEvent) {
val project = e.project ?: return

val checkboxTree = getCheckboxTree(e.dataContext) ?: throw RuntimeException("Could not obtain file tree")

val totalTokensLabel = TotalTokensLabel(checkboxTree.referencedFiles)
checkboxTree.addCheckboxTreeListener(object : CheckboxTreeListener {
override fun nodeStateChanged(node: CheckedTreeNode) {
totalTokensLabel.updateState(node)
}
})

val includedFilesSettings = IncludedFilesSettings.getCurrentState()
val promptTemplateTextArea = UIUtil.createTextArea(includedFilesSettings.promptTemplate)
val repeatableContextTextArea = UIUtil.createTextArea(includedFilesSettings.repeatableContext)
val show = showMultiFilePromptDialog(
project,
promptTemplateTextArea,
repeatableContextTextArea,
totalTokensLabel,
checkboxTree)
if (show == DialogWrapper.OK_EXIT_CODE) {
project.putUserData(CodeGPTKeys.SELECTED_FILES, checkboxTree.referencedFiles)
project.messageBus
.syncPublisher(IncludeFilesInContextNotifier.FILES_INCLUDED_IN_CONTEXT_TOPIC)
.filesIncluded(checkboxTree.referencedFiles)
includedFilesSettings.promptTemplate = promptTemplateTextArea.text
includedFilesSettings.repeatableContext = repeatableContextTextArea.text
}
}

private fun getCheckboxTree(dataContext: DataContext): FileCheckboxTree? {
return CommonDataKeys.VIRTUAL_FILE_ARRAY.getData(dataContext)?.let(::VirtualFileCheckboxTree)
}

private class TotalTokensLabel(referencedFiles: List<ReferencedFile>) : JBLabel() {
private var fileCount = referencedFiles.size
private var totalTokens: Int

init {
totalTokens = calculateTotalTokens(referencedFiles)
updateText()
}

fun updateState(checkedNode: CheckedTreeNode) {
val fileContent = getNodeFileContent(checkedNode) ?: return
val tokenCount = encodingManager.countTokens(fileContent)
if (checkedNode.isChecked) {
totalTokens += tokenCount
fileCount++
} else {
totalTokens -= tokenCount
fileCount--
}
SwingUtilities.invokeLater(::updateText)
}

private fun getNodeFileContent(checkedNode: CheckedTreeNode): String? {
val userObject = checkedNode.userObject
if (userObject is PsiElement) {
return userObject.containingFile?.virtualFile?.let { getVirtualFileContent(it) }
}
return (userObject as? VirtualFile)?.let { getVirtualFileContent(it) }
}

private fun getVirtualFileContent(virtualFile: VirtualFile): String? {
try {
return String(Files.readAllBytes(Paths.get(virtualFile.path)))
} catch (ex: IOException) {
LOG.error(ex)
}
return null
}

private fun updateText() {
text = String.format(
"<html><strong>%d</strong> %s totaling <strong>%s</strong> tokens</html>",
fileCount,
if (fileCount == 1) "file" else "files",
convertLongValue(totalTokens.toLong()))
}

private fun calculateTotalTokens(referencedFiles: List<ReferencedFile>): Int {
return referencedFiles.stream().mapToInt { encodingManager.countTokens(it.fileContent) }.sum()
}

companion object {
private val encodingManager
get() = EncodingManager.getInstance()
}
}

companion object {
private val LOG = Logger.getInstance(IncludeFilesInContextAction::class.java)

private fun showMultiFilePromptDialog(
project: Project,
promptTemplateTextArea: JBTextArea,
repeatableContextTextArea: JBTextArea,
totalTokensLabel: JBLabel,
component: JComponent
): Int {
val dialogBuilder = DialogBuilder(project)
dialogBuilder.setTitle(CodeGPTBundle.get("action.includeFilesInContext.dialog.title"))
dialogBuilder.setActionDescriptors()
val fileTreeScrollPane = ScrollPaneFactory.createScrollPane(component)
fileTreeScrollPane.preferredSize = Dimension(480, component.preferredSize.height + 48)
dialogBuilder.setNorthPanel(
FormBuilder.createFormBuilder()
.addLabeledComponent(
CodeGPTBundle.get("shared.promptTemplate"),
PanelFactory.panel(promptTemplateTextArea).withComment(
"<html><p>The template that will be used to create the final prompt. "
+ "The <strong>{REPEATABLE_CONTEXT}</strong> placeholder must be included "
+ "to correctly map the file contents.</p></html>"
)
.createPanel(),
true
)
.addVerticalGap(4)
.addLabeledComponent(
CodeGPTBundle.get("action.includeFilesInContext.dialog.repeatableContext.label"),
PanelFactory.panel(repeatableContextTextArea).withComment(
"<html><p>The context that will be repeated for each included file. "
+ "Acceptable placeholders include <strong>{FILE_PATH}</strong> and "
+ "<strong>{FILE_CONTENT}</strong>.</p></html>"
)
.createPanel(),
true
)
.addComponent(JBUI.Panels.simplePanel()
.addToRight(getRestoreButton(promptTemplateTextArea, repeatableContextTextArea))
)
.addVerticalGap(16)
.addComponent(JBLabel(CodeGPTBundle.get("action.includeFilesInContext.dialog.description"))
.setCopyable(false)
.setAllowAutoWrapping(true)
)
.addVerticalGap(4)
.addLabeledComponent(totalTokensLabel, fileTreeScrollPane, true)
.addVerticalGap(16)
.panel
)
dialogBuilder.addOkAction().setText(CodeGPTBundle.get("dialog.continue"))
dialogBuilder.addCancelAction()
return dialogBuilder.show()
}

private fun getRestoreButton(promptTemplateTextArea: JBTextArea,
repeatableContextTextArea: JBTextArea): JButton {
val restoreButton = JButton(
CodeGPTBundle.get("action.includeFilesInContext.dialog.restoreToDefaults.label"))
restoreButton.addActionListener { _ ->
val includedFilesSettings = IncludedFilesSettings.getCurrentState()
includedFilesSettings.promptTemplate = IncludedFilesSettingsState.DEFAULT_PROMPT_TEMPLATE
includedFilesSettings.repeatableContext = IncludedFilesSettingsState.DEFAULT_REPEATABLE_CONTEXT
promptTemplateTextArea.text = IncludedFilesSettingsState.DEFAULT_PROMPT_TEMPLATE
repeatableContextTextArea.text = IncludedFilesSettingsState.DEFAULT_REPEATABLE_CONTEXT
}
return restoreButton
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package ee.carlrobert.codegpt.actions

import com.intellij.util.messages.Topic
import ee.carlrobert.codegpt.ReferencedFile

fun interface IncludeFilesInContextNotifier {
fun filesIncluded(includedFiles: List<ReferencedFile>)

companion object {
@JvmField
val FILES_INCLUDED_IN_CONTEXT_TOPIC: Topic<IncludeFilesInContextNotifier> =
Topic.create("filesIncludedInContext", IncludeFilesInContextNotifier::class.java)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package ee.carlrobert.codegpt.actions

import com.intellij.icons.AllIcons
import com.intellij.openapi.actionSystem.AnAction
import com.intellij.openapi.actionSystem.AnActionEvent
import com.intellij.openapi.options.ShowSettingsUtil
import ee.carlrobert.codegpt.CodeGPTBundle
import ee.carlrobert.codegpt.settings.service.ServiceConfigurable

class OpenSettingsAction : AnAction(
CodeGPTBundle.get("action.openSettings.title"),
CodeGPTBundle.get("action.openSettings.description"),
AllIcons.General.Settings) {
override fun actionPerformed(e: AnActionEvent) {
ShowSettingsUtil.getInstance().showSettingsDialog(e.project, ServiceConfigurable::class.java)
}
}
37 changes: 37 additions & 0 deletions src/main/kotlin/ee/carlrobert/codegpt/actions/TrackableAction.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
package ee.carlrobert.codegpt.actions

import com.intellij.openapi.actionSystem.AnAction
import com.intellij.openapi.actionSystem.AnActionEvent
import com.intellij.openapi.editor.Editor
import ee.carlrobert.codegpt.telemetry.TelemetryAction
import javax.swing.Icon

abstract class TrackableAction(
@JvmField protected val editor: Editor,
text: String?,
description: String?,
icon: Icon?,
private val actionType: ActionType
) : AnAction(text, description, icon) {

abstract fun handleAction(e: AnActionEvent)

override fun actionPerformed(e: AnActionEvent) {
try {
handleAction(e)
TelemetryAction.IDE_ACTION
.createActionMessage()
.property("group", null)
.property("action", actionType.name)
.send()
} catch (ex: Exception) {
TelemetryAction.IDE_ACTION_ERROR
.createActionMessage()
.property("group", null)
.property("action", actionType.name)
.error(ex)
.send()
throw ex
}
}
}
29 changes: 29 additions & 0 deletions src/main/kotlin/ee/carlrobert/codegpt/actions/editor/AskAction.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package ee.carlrobert.codegpt.actions.editor

import com.intellij.openapi.actionSystem.ActionUpdateThread
import com.intellij.openapi.actionSystem.AnAction
import com.intellij.openapi.actionSystem.AnActionEvent
import ee.carlrobert.codegpt.Icons
import ee.carlrobert.codegpt.conversations.ConversationsState
import ee.carlrobert.codegpt.toolwindow.chat.ChatToolWindowContentManager

class AskAction : AnAction("New Chat", "Chat with CodeGPT", Icons.Sparkle) {
init {
EditorActionsUtil.registerAction(this)
}

override fun update(event: AnActionEvent) {
event.presentation.isEnabled = event.project != null
}

override fun actionPerformed(event: AnActionEvent) {
val project = event.project ?: return
ConversationsState.getInstance().setCurrentConversation(null)
project.getService(ChatToolWindowContentManager::class.java)
.createNewTabPanel()?.displayLandingView()
}

override fun getActionUpdateThread(): ActionUpdateThread {
return ActionUpdateThread.BGT
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
package ee.carlrobert.codegpt.actions.editor

import com.intellij.openapi.actionSystem.ActionUpdateThread
import com.intellij.openapi.actionSystem.AnAction
import com.intellij.openapi.actionSystem.AnActionEvent
import com.intellij.openapi.actionSystem.CommonDataKeys
import com.intellij.openapi.editor.Editor
import com.intellij.openapi.project.Project
import com.intellij.openapi.util.NlsActions
import javax.swing.Icon

abstract class BaseEditorAction @JvmOverloads constructor(
text: @NlsActions.ActionText String?,
description: @NlsActions.ActionDescription String?,
icon: Icon? = null
) : AnAction(text, description, icon) {
init {
EditorActionsUtil.registerAction(this)
}

protected abstract fun actionPerformed(project: Project, editor: Editor, selectedText: String?)

override fun actionPerformed(event: AnActionEvent) {
val project = event.project ?: return
val editor = event.getData(CommonDataKeys.EDITOR) ?: return
actionPerformed(project, editor, editor.selectionModel.selectedText)
}

override fun update(event: AnActionEvent) {
event.presentation.isEnabled = event.project != null
&& event.getData(CommonDataKeys.EDITOR)?.selectionModel?.selectedText != null
}

override fun getActionUpdateThread(): ActionUpdateThread {
return ActionUpdateThread.EDT
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
package ee.carlrobert.codegpt.actions.editor

import com.intellij.icons.AllIcons
import com.intellij.openapi.editor.Editor
import com.intellij.openapi.project.Project
import com.intellij.openapi.ui.DialogWrapper
import com.intellij.util.ui.FormBuilder
import com.intellij.util.ui.JBUI
import com.intellij.util.ui.UI
import ee.carlrobert.codegpt.conversations.message.Message
import ee.carlrobert.codegpt.toolwindow.chat.ChatToolWindowContentManager
import ee.carlrobert.codegpt.ui.UIUtil
import ee.carlrobert.codegpt.util.file.FileUtil.getFileExtension
import java.awt.event.ActionEvent
import javax.swing.AbstractAction
import javax.swing.JComponent
import javax.swing.JTextArea
import javax.swing.SwingUtilities

class CustomPromptAction internal constructor() :
BaseEditorAction("Custom Prompt", "Custom prompt description", AllIcons.Actions.Run_anything) {
init {
EditorActionsUtil.registerAction(this)
}

override fun actionPerformed(project: Project, editor: Editor, selectedText: String?) {
if (!selectedText.isNullOrBlank()) {
val fileExtension = getFileExtension(editor.virtualFile.name)
val dialog = CustomPromptDialog(previousUserPrompt)
if (dialog.showAndGet()) {
previousUserPrompt = dialog.userPrompt
val message = Message(
String.format("%s%n```%s%n%s%n```", previousUserPrompt, fileExtension, selectedText))
message.userMessage = previousUserPrompt
SwingUtilities.invokeLater {
project.getService(ChatToolWindowContentManager::class.java).sendMessage(message)
}
}
}
}

class CustomPromptDialog(previousUserPrompt: String) : DialogWrapper(true) {
private val userPromptTextArea = JTextArea(previousUserPrompt)

init {
userPromptTextArea.caretPosition = previousUserPrompt.length
title = "Custom Prompt"
setSize(400, rootPane.preferredSize.height)
init()
}

override fun getPreferredFocusedComponent(): JComponent? {
return userPromptTextArea
}

override fun createCenterPanel(): JComponent? {
userPromptTextArea.lineWrap = true
userPromptTextArea.wrapStyleWord = true
userPromptTextArea.margin = JBUI.insets(5)
UIUtil.addShiftEnterInputMap(userPromptTextArea, object : AbstractAction() {
override fun actionPerformed(e: ActionEvent) {
clickDefaultButton()
}
})

return FormBuilder.createFormBuilder()
.addComponent(UI.PanelFactory.panel(userPromptTextArea)
.withLabel("Prefix:")
.moveLabelOnTop()
.withComment("Example: Find bugs in the following code")
.createPanel())
.panel
}

val userPrompt: String
get() = userPromptTextArea.text
}

companion object {
private var previousUserPrompt = ""
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
package ee.carlrobert.codegpt.actions.editor

import com.intellij.openapi.actionSystem.ActionManager
import com.intellij.openapi.actionSystem.AnAction
import com.intellij.openapi.actionSystem.DefaultActionGroup
import com.intellij.openapi.editor.Editor
import com.intellij.openapi.extensions.PluginId
import com.intellij.openapi.project.Project
import ee.carlrobert.codegpt.CodeGPTKeys
import ee.carlrobert.codegpt.ReferencedFile
import ee.carlrobert.codegpt.actions.IncludeFilesInContextAction
import ee.carlrobert.codegpt.conversations.message.Message
import ee.carlrobert.codegpt.settings.configuration.ConfigurationSettings
import ee.carlrobert.codegpt.toolwindow.chat.ChatToolWindowContentManager
import ee.carlrobert.codegpt.util.file.FileUtil.getFileExtension
import org.apache.commons.text.CaseUtils
import java.util.stream.Stream

object EditorActionsUtil {
@JvmField
val DEFAULT_ACTIONS: Map<String, String> = LinkedHashMap(mapOf(
"Find Bugs" to "Find bugs and output code with bugs fixed in the following code: {{selectedCode}}",
"Write Tests" to "Write Tests for the selected code {{selectedCode}}",
"Explain" to "Explain the selected code {{selectedCode}}",
"Refactor" to "Refactor the selected code {{selectedCode}}",
"Optimize" to "Optimize the selected code {{selectedCode}}"))

@JvmField
val DEFAULT_ACTIONS_ARRAY = toArray(DEFAULT_ACTIONS)

@JvmStatic
fun toArray(actionsMap: Map<String, String>): Array<Array<String>> {
return actionsMap.entries.stream()
.map { arrayOf(it.key, it.value) }
.toArray { arrayOfNulls<Array<String>>(it) }
}

@JvmStatic
fun refreshActions() {
val actionGroup = ActionManager.getInstance()
.getAction("action.editor.group.EditorActionGroup") as? DefaultActionGroup ?: return
actionGroup.removeAll()
actionGroup.add(AskAction())
actionGroup.add(CustomPromptAction())
actionGroup.addSeparator()

val configuredActions = ConfigurationSettings.getCurrentState().tableData
configuredActions.forEach { (label: String?, prompt: String) ->
// using label as action description to prevent com.intellij.diagnostic.PluginException
// https://github.com/carlrobertoh/CodeGPT/issues/95
val action: BaseEditorAction = object : BaseEditorAction(label, label) {
override fun actionPerformed(project: Project, editor: Editor, selectedText: String?) {
val fileExtension = getFileExtension(editor.virtualFile.name)
val message = Message(prompt.replace(
"{{selectedCode}}",
String.format("%n```%s%n%s%n```", fileExtension, selectedText)))
message.userMessage = prompt.replace("{{selectedCode}}", "")
val toolWindowContentManager =
project.getService(ChatToolWindowContentManager::class.java)
toolWindowContentManager.toolWindow.show()

message.referencedFilePaths = Stream.ofNullable(project.getUserData(CodeGPTKeys.SELECTED_FILES))
.flatMap(MutableList<ReferencedFile>::stream)
.map { it.filePath }
.toList()
toolWindowContentManager.sendMessage(message)
}
}
actionGroup.add(action)
}
actionGroup.addSeparator()
actionGroup.add(IncludeFilesInContextAction("action.includeFileInContext.title"))
}

@JvmStatic
fun registerAction(action: AnAction) {
val actionManager = ActionManager.getInstance()
val actionId = convertToId(action.templateText)
if (actionManager.getAction(actionId) == null) {
actionManager.registerAction(actionId, action, PluginId.getId("ee.carlrobert.chatgpt"))
}
}

@JvmStatic
fun convertToId(label: String?): String {
return "codegpt." + CaseUtils.toCamelCase(label, true)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
package ee.carlrobert.codegpt.actions.toolwindow

import com.intellij.icons.AllIcons
import com.intellij.openapi.actionSystem.ActionUpdateThread
import com.intellij.openapi.actionSystem.AnAction
import com.intellij.openapi.actionSystem.AnActionEvent
import ee.carlrobert.codegpt.actions.ActionType
import ee.carlrobert.codegpt.actions.editor.EditorActionsUtil.registerAction
import ee.carlrobert.codegpt.conversations.ConversationsState.getCurrentConversation
import ee.carlrobert.codegpt.telemetry.TelemetryAction

class ClearChatWindowAction(private val onActionPerformed: Runnable) :
AnAction("Clear Window", "Clears a chat window", AllIcons.General.Reset) {
init {
registerAction(this)
}

override fun update(event: AnActionEvent) {
super.update(event)
event.presentation.isEnabled = getCurrentConversation()?.messages?.isNotEmpty() == true
}

override fun actionPerformed(event: AnActionEvent) {
try {
onActionPerformed.run()
} finally {
TelemetryAction.IDE_ACTION.createActionMessage()
.property("action", ActionType.CLEAR_CHAT_WINDOW.name)
.send()
}
}

override fun getActionUpdateThread(): ActionUpdateThread {
return ActionUpdateThread.BGT
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package ee.carlrobert.codegpt.actions.toolwindow

import com.intellij.icons.AllIcons
import com.intellij.openapi.actionSystem.AnAction
import com.intellij.openapi.actionSystem.AnActionEvent
import ee.carlrobert.codegpt.actions.ActionType
import ee.carlrobert.codegpt.actions.editor.EditorActionsUtil.registerAction
import ee.carlrobert.codegpt.telemetry.TelemetryAction

class CreateNewConversationAction(private val onCreate: Runnable) :
AnAction("Create New Chat", "Create new chat", AllIcons.General.Add) {

init {
registerAction(this)
}

override fun actionPerformed(event: AnActionEvent) {
try {
event.project ?: return
onCreate.run()
} finally {
TelemetryAction.IDE_ACTION.createActionMessage()
.property("action", ActionType.CREATE_NEW_CHAT.name)
.send()
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
package ee.carlrobert.codegpt.actions.toolwindow

import com.intellij.icons.AllIcons
import com.intellij.openapi.actionSystem.ActionUpdateThread
import com.intellij.openapi.actionSystem.AnAction
import com.intellij.openapi.actionSystem.AnActionEvent
import com.intellij.openapi.ui.Messages
import ee.carlrobert.codegpt.Icons
import ee.carlrobert.codegpt.actions.ActionType
import ee.carlrobert.codegpt.actions.editor.EditorActionsUtil.registerAction
import ee.carlrobert.codegpt.conversations.ConversationService
import ee.carlrobert.codegpt.telemetry.TelemetryAction
import ee.carlrobert.codegpt.toolwindow.chat.ChatToolWindowContentManager

class DeleteAllConversationsAction(private val onRefresh: Runnable) :
AnAction("Delete All", "Delete all conversations", AllIcons.Actions.GC) {
init {
registerAction(this)
}

override fun update(event: AnActionEvent) {
event.project ?: return
event.presentation.isEnabled = ConversationService.getInstance().sortedConversations.isNotEmpty()
}

override fun actionPerformed(event: AnActionEvent) {
val answer = Messages.showYesNoDialog(
"Are you sure you want to delete all conversations?",
"Clear History",
Icons.Default)
if (answer == Messages.YES) {
val project = event.project
if (project != null) {
try {
ConversationService.getInstance().clearAll()
project.getService(ChatToolWindowContentManager::class.java).resetAll()
} finally {
TelemetryAction.IDE_ACTION.createActionMessage()
.property("action", ActionType.DELETE_ALL_CONVERSATIONS.name)
.send()
}
}
onRefresh.run()
}
}

override fun getActionUpdateThread(): ActionUpdateThread {
return ActionUpdateThread.BGT
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
package ee.carlrobert.codegpt.actions.toolwindow

import com.intellij.icons.AllIcons
import com.intellij.openapi.actionSystem.ActionUpdateThread
import com.intellij.openapi.actionSystem.AnAction
import com.intellij.openapi.actionSystem.AnActionEvent
import com.intellij.openapi.ui.Messages
import ee.carlrobert.codegpt.actions.ActionType
import ee.carlrobert.codegpt.actions.editor.EditorActionsUtil.registerAction
import ee.carlrobert.codegpt.conversations.ConversationsState
import ee.carlrobert.codegpt.telemetry.TelemetryAction
import ee.carlrobert.codegpt.ui.OverlayUtil

class DeleteConversationAction(private val onDelete: Runnable) :
AnAction("Delete Conversation", "Delete single conversation", AllIcons.Actions.GC) {
init {
registerAction(this)
}

override fun update(event: AnActionEvent) {
event.presentation.isEnabled = ConversationsState.getCurrentConversation() != null
}

override fun actionPerformed(event: AnActionEvent) {
if (OverlayUtil.showDeleteConversationDialog() == Messages.YES && event.project != null) {
TelemetryAction.IDE_ACTION.createActionMessage()
.property("action", ActionType.DELETE_CONVERSATION.name)
.send()
onDelete.run()
}
}

override fun getActionUpdateThread(): ActionUpdateThread {
return ActionUpdateThread.EDT
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package ee.carlrobert.codegpt.actions.toolwindow

import com.intellij.openapi.actionSystem.ActionUpdateThread
import com.intellij.openapi.actionSystem.AnAction
import com.intellij.openapi.actionSystem.AnActionEvent
import com.intellij.openapi.project.Project
import ee.carlrobert.codegpt.conversations.Conversation
import ee.carlrobert.codegpt.conversations.ConversationsState
import java.util.Optional
import javax.swing.Icon

abstract class MoveAction protected constructor(
text: String?, description: String?, icon: Icon?, private val onRefresh: Runnable) : AnAction(text, description, icon) {

protected abstract fun getConversation(project: Project): Optional<Conversation?>

override fun update(event: AnActionEvent) {
event.presentation.isEnabled = ConversationsState.getCurrentConversation() != null
}

override fun actionPerformed(event: AnActionEvent) {
val project = event.project ?: return
getConversation(project).ifPresent {
ConversationsState.getInstance().setCurrentConversation(it)
onRefresh.run()
}
}

override fun getActionUpdateThread(): ActionUpdateThread {
return ActionUpdateThread.BGT
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package ee.carlrobert.codegpt.actions.toolwindow

import com.intellij.icons.AllIcons
import com.intellij.openapi.project.Project
import ee.carlrobert.codegpt.actions.editor.EditorActionsUtil.registerAction
import ee.carlrobert.codegpt.conversations.Conversation
import ee.carlrobert.codegpt.conversations.ConversationService
import java.util.Optional

class MoveDownAction(onRefresh: Runnable?) :
MoveAction("Move Down", "Move Down", AllIcons.Actions.MoveDown, onRefresh!!) {
init {
registerAction(this)
}

override fun getConversation(project: Project): Optional<Conversation?> {
return ConversationService.getInstance().previousConversation
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package ee.carlrobert.codegpt.actions.toolwindow

import com.intellij.icons.AllIcons
import com.intellij.openapi.project.Project
import ee.carlrobert.codegpt.actions.editor.EditorActionsUtil.registerAction
import ee.carlrobert.codegpt.conversations.Conversation
import ee.carlrobert.codegpt.conversations.ConversationService
import java.util.Optional

class MoveUpAction(onRefresh: Runnable?) : MoveAction("Move Up", "Move up", AllIcons.Actions.MoveUp, onRefresh!!) {
init {
registerAction(this)
}

override fun getConversation(project: Project): Optional<Conversation?> {
return ConversationService.getInstance().nextConversation
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
package ee.carlrobert.codegpt.actions.toolwindow

import com.intellij.icons.AllIcons
import com.intellij.openapi.actionSystem.ActionUpdateThread
import com.intellij.openapi.actionSystem.AnAction
import com.intellij.openapi.actionSystem.AnActionEvent
import com.intellij.openapi.fileEditor.FileEditorManager
import com.intellij.openapi.vfs.VirtualFile
import com.intellij.openapi.wm.ToolWindowManager
import com.intellij.testFramework.LightVirtualFile
import ee.carlrobert.codegpt.actions.ActionType
import ee.carlrobert.codegpt.actions.editor.EditorActionsUtil.registerAction
import ee.carlrobert.codegpt.conversations.ConversationsState.getCurrentConversation
import ee.carlrobert.codegpt.telemetry.TelemetryAction
import java.time.format.DateTimeFormatter
import java.util.Objects.requireNonNull
import java.util.stream.Collectors

class OpenInEditorAction : AnAction("Open In Editor", "Open conversation in editor", AllIcons.Actions.SplitVertically) {
init {
registerAction(this)
}

override fun update(event: AnActionEvent) {
super.update(event)
event.presentation.isEnabled = getCurrentConversation()?.messages?.isNotEmpty() == true
}

override fun actionPerformed(e: AnActionEvent) {
try {
val project = e.project ?: return
val currentConversation = getCurrentConversation() ?: return
val dateTimeStamp = currentConversation.updatedOn
.format(DateTimeFormatter.ofPattern("yyyyMMddHHmm"))
val fileName = String.format("%s_%s.md", currentConversation.model, dateTimeStamp)
val fileContent = currentConversation.messages.stream()
.map { String.format("### User:%n%s%n### CodeGPT:%n%s%n", it.prompt, it.response) }
.collect(Collectors.joining())
val file: VirtualFile = LightVirtualFile(fileName, fileContent)
FileEditorManager.getInstance(project).openFile(file, true)
requireNonNull(ToolWindowManager.getInstance(project).getToolWindow("CodeGPT"))!!
.hide()
} finally {
TelemetryAction.IDE_ACTION.createActionMessage()
.property("action", ActionType.OPEN_CONVERSATION_IN_EDITOR.name)
.send()
}
}

override fun getActionUpdateThread(): ActionUpdateThread {
return ActionUpdateThread.BGT
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package ee.carlrobert.codegpt.actions.toolwindow

import com.intellij.icons.AllIcons
import com.intellij.openapi.actionSystem.ActionUpdateThread
import com.intellij.openapi.actionSystem.AnAction
import com.intellij.openapi.actionSystem.AnActionEvent
import com.intellij.openapi.actionSystem.PlatformDataKeys
import ee.carlrobert.codegpt.actions.editor.EditorActionsUtil.registerAction
import ee.carlrobert.codegpt.util.EditorUtil.hasSelection
import ee.carlrobert.codegpt.util.EditorUtil.isMainEditorTextSelected
import ee.carlrobert.codegpt.util.EditorUtil.replaceMainEditorSelection
import java.util.Objects.requireNonNull

class ReplaceCodeInMainEditorAction :
AnAction("Replace in Main Editor", "Replace code in main editor", AllIcons.Actions.Replace) {
init {
registerAction(this)
}

override fun update(event: AnActionEvent) {
event.presentation.isEnabled = isMainEditorTextSelected(requireNonNull(event.project)!!)
&& hasSelection(event.getData(PlatformDataKeys.EDITOR))
}

override fun actionPerformed(event: AnActionEvent) {
val project = event.project ?: return
val toolWindowEditor = event.getData(PlatformDataKeys.EDITOR) ?: return
replaceMainEditorSelection(project, requireNonNull(toolWindowEditor.selectionModel.selectedText)!!)
}

override fun getActionUpdateThread(): ActionUpdateThread {
return ActionUpdateThread.EDT
}
}

0 comments on commit 9f09aaf

Please sign in to comment.