-
Notifications
You must be signed in to change notification settings - Fork 290
Commit
There are no files selected for viewing
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.
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 |
---|---|---|
@@ -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) | ||
} | ||
} |
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 | ||
} | ||
} | ||
} |
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 | ||
} | ||
} |