diff --git a/src/main/kotlin/com/github/blarc/ai/commits/intellij/plugin/AICommitAction.kt b/src/main/kotlin/com/github/blarc/ai/commits/intellij/plugin/AICommitAction.kt index 029dbdb..212cb40 100644 --- a/src/main/kotlin/com/github/blarc/ai/commits/intellij/plugin/AICommitAction.kt +++ b/src/main/kotlin/com/github/blarc/ai/commits/intellij/plugin/AICommitAction.kt @@ -1,25 +1,23 @@ package com.github.blarc.ai.commits.intellij.plugin import com.github.blarc.ai.commits.intellij.plugin.AICommitsBundle.message +import com.github.blarc.ai.commits.intellij.plugin.AICommitsUtils.commonBranch +import com.github.blarc.ai.commits.intellij.plugin.AICommitsUtils.computeDiff +import com.github.blarc.ai.commits.intellij.plugin.AICommitsUtils.constructPrompt +import com.github.blarc.ai.commits.intellij.plugin.AICommitsUtils.isPromptTooLarge import com.github.blarc.ai.commits.intellij.plugin.notifications.Notification import com.github.blarc.ai.commits.intellij.plugin.notifications.sendNotification import com.github.blarc.ai.commits.intellij.plugin.settings.AppSettings import com.intellij.openapi.actionSystem.AnAction import com.intellij.openapi.actionSystem.AnActionEvent -import com.intellij.openapi.diff.impl.patch.IdeaTextPatchBuilder -import com.intellij.openapi.diff.impl.patch.UnifiedDiffWriter import com.intellij.openapi.progress.runBackgroundableTask import com.intellij.openapi.project.DumbAware -import com.intellij.openapi.project.Project import com.intellij.openapi.vcs.VcsDataKeys -import com.intellij.openapi.vcs.changes.Change import com.intellij.vcs.commit.AbstractCommitWorkflowHandler import com.knuddels.jtokkit.Encodings import com.knuddels.jtokkit.api.ModelType -import git4idea.repo.GitRepositoryManager import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.runBlocking -import java.io.StringWriter class AICommitAction : AnAction(), DumbAware { override fun actionPerformed(e: AnActionEvent) { @@ -41,14 +39,8 @@ class AICommitAction : AnAction(), DumbAware { return@runBackgroundableTask } - var branch = commonBranch(includedChanges, project) - if (branch == null) { - sendNotification(Notification.noCommonBranch()) - // hardcoded fallback branch - branch = "main" - } - - val prompt = AppSettings.instance.getPrompt(diff, branch) + val branch = commonBranch(includedChanges, project) + val prompt = constructPrompt(AppSettings.instance.currentPrompt.content, diff, branch) if (isPromptTooLarge(prompt)) { sendNotification(Notification.promptTooLarge()) return@runBackgroundableTask @@ -72,70 +64,4 @@ class AICommitAction : AnAction(), DumbAware { } } } - - private fun computeDiff( - includedChanges: List, - project: Project - ): String { - - val gitRepositoryManager = GitRepositoryManager.getInstance(project) - - // go through included changes, create a map of repository to changes and discard nulls - val changesByRepository = includedChanges - .filter { - it.virtualFile?.path?.let { path -> - AICommitsUtils.isPathExcluded(path, project) - } ?: false - } - .mapNotNull { change -> - change.virtualFile?.let { file -> - gitRepositoryManager.getRepositoryForFileQuick( - file - ) to change - } - } - .groupBy({ it.first }, { it.second }) - - - // compute diff for each repository - return changesByRepository - .map { (repository, changes) -> - repository?.let { - val filePatches = IdeaTextPatchBuilder.buildPatch( - project, - changes, - repository.root.toNioPath(), false, true - ) - - val stringWriter = StringWriter() - stringWriter.write("Repository: ${repository.root.path}\n") - UnifiedDiffWriter.write(project, filePatches, stringWriter, "\n", null) - stringWriter.toString() - } - } - .joinToString("\n") - } - - private fun isPromptTooLarge(prompt: String): Boolean { - val registry = Encodings.newDefaultEncodingRegistry() - - /* - * Try to find the model type based on the model id by finding the longest matching model type - * If no model type matches, let the request go through and let the OpenAI API handle it - */ - val modelType = ModelType.values() - .filter { AppSettings.instance.openAIModelId.contains(it.name) } - .maxByOrNull { it.name.length } - ?: return false - - val encoding = registry.getEncoding(modelType.encodingType) - return encoding.countTokens(prompt) > modelType.maxContextLength - } - - private fun commonBranch(changes: List, project: Project): String? { - val repositoryManager = GitRepositoryManager.getInstance(project) - return changes.map { - repositoryManager.getRepositoryForFileQuick(it.virtualFile)?.currentBranchName - }.groupingBy { it }.eachCount().maxByOrNull { it.value }?.key - } -} \ No newline at end of file +} diff --git a/src/main/kotlin/com/github/blarc/ai/commits/intellij/plugin/AICommitsUtils.kt b/src/main/kotlin/com/github/blarc/ai/commits/intellij/plugin/AICommitsUtils.kt index 0224ee3..d20b84a 100644 --- a/src/main/kotlin/com/github/blarc/ai/commits/intellij/plugin/AICommitsUtils.kt +++ b/src/main/kotlin/com/github/blarc/ai/commits/intellij/plugin/AICommitsUtils.kt @@ -1,9 +1,18 @@ package com.github.blarc.ai.commits.intellij.plugin +import com.github.blarc.ai.commits.intellij.plugin.notifications.Notification +import com.github.blarc.ai.commits.intellij.plugin.notifications.sendNotification import com.github.blarc.ai.commits.intellij.plugin.settings.AppSettings import com.github.blarc.ai.commits.intellij.plugin.settings.ProjectSettings import com.intellij.openapi.components.service +import com.intellij.openapi.diff.impl.patch.IdeaTextPatchBuilder +import com.intellij.openapi.diff.impl.patch.UnifiedDiffWriter import com.intellij.openapi.project.Project +import com.intellij.openapi.vcs.changes.Change +import com.knuddels.jtokkit.Encodings +import com.knuddels.jtokkit.api.ModelType +import git4idea.repo.GitRepositoryManager +import java.io.StringWriter import java.nio.file.FileSystems object AICommitsUtils { @@ -11,6 +20,7 @@ object AICommitsUtils { fun isPathExcluded(path: String, project: Project) : Boolean { return !AppSettings.instance.isPathExcluded(path) && !project.service().isPathExcluded(path) } + fun matchesGlobs(text: String, globs: Set): Boolean { val fileSystem = FileSystems.getDefault() for (globString in globs) { @@ -21,4 +31,89 @@ object AICommitsUtils { } return false } -} \ No newline at end of file + + fun constructPrompt(promptContent: String, diff: String, branch: String): String { + var content = promptContent + content = content.replace("{locale}", AppSettings.instance.locale.displayLanguage) + content = content.replace("{branch}", branch) + + return if (content.contains("{diff}")) { + content.replace("{diff}", diff) + } else { + "$content\n$diff" + } + } + + fun commonBranch(changes: List, project: Project): String { + val repositoryManager = GitRepositoryManager.getInstance(project) + var branch = changes.map { + repositoryManager.getRepositoryForFileQuick(it.virtualFile)?.currentBranchName + }.groupingBy { it }.eachCount().maxByOrNull { it.value }?.key + + if (branch == null) { + sendNotification(Notification.noCommonBranch()) + // hardcoded fallback branch + branch = "main" + } + return branch + } + + fun computeDiff( + includedChanges: List, + project: Project + ): String { + + val gitRepositoryManager = GitRepositoryManager.getInstance(project) + + // go through included changes, create a map of repository to changes and discard nulls + val changesByRepository = includedChanges + .filter { + it.virtualFile?.path?.let { path -> + AICommitsUtils.isPathExcluded(path, project) + } ?: false + } + .mapNotNull { change -> + change.virtualFile?.let { file -> + gitRepositoryManager.getRepositoryForFileQuick( + file + ) to change + } + } + .groupBy({ it.first }, { it.second }) + + + // compute diff for each repository + return changesByRepository + .map { (repository, changes) -> + repository?.let { + val filePatches = IdeaTextPatchBuilder.buildPatch( + project, + changes, + repository.root.toNioPath(), false, true + ) + + val stringWriter = StringWriter() + stringWriter.write("Repository: ${repository.root.path}\n") + UnifiedDiffWriter.write(project, filePatches, stringWriter, "\n", null) + stringWriter.toString() + } + } + .joinToString("\n") + } + + fun isPromptTooLarge(prompt: String): Boolean { + val registry = Encodings.newDefaultEncodingRegistry() + + /* + * Try to find the model type based on the model id by finding the longest matching model type + * If no model type matches, let the request go through and let the OpenAI API handle it + */ + val modelType = ModelType.entries + .filter { AppSettings.instance.openAIModelId.contains(it.name) } + .maxByOrNull { it.name.length } + ?: return false + + val encoding = registry.getEncoding(modelType.encodingType) + return encoding.countTokens(prompt) > modelType.maxContextLength + } +} diff --git a/src/main/kotlin/com/github/blarc/ai/commits/intellij/plugin/OpenAIService.kt b/src/main/kotlin/com/github/blarc/ai/commits/intellij/plugin/OpenAIService.kt index 7d5b900..9e3a177 100644 --- a/src/main/kotlin/com/github/blarc/ai/commits/intellij/plugin/OpenAIService.kt +++ b/src/main/kotlin/com/github/blarc/ai/commits/intellij/plugin/OpenAIService.kt @@ -11,7 +11,7 @@ import com.intellij.openapi.application.ApplicationManager import com.intellij.openapi.components.Service -@Service +@Service(Service.Level.APP) class OpenAIService { companion object { diff --git a/src/main/kotlin/com/github/blarc/ai/commits/intellij/plugin/settings/AppSettings.kt b/src/main/kotlin/com/github/blarc/ai/commits/intellij/plugin/settings/AppSettings.kt index 4d9691d..998d165 100644 --- a/src/main/kotlin/com/github/blarc/ai/commits/intellij/plugin/settings/AppSettings.kt +++ b/src/main/kotlin/com/github/blarc/ai/commits/intellij/plugin/settings/AppSettings.kt @@ -51,18 +51,6 @@ class AppSettings : PersistentStateComponent { get() = ApplicationManager.getApplication().getService(AppSettings::class.java) } - fun getPrompt(diff: String, branch: String): String { - var content = currentPrompt.content - content = content.replace("{locale}", locale.displayLanguage) - content = content.replace("{branch}", branch) - - return if (content.contains("{diff}")) { - content.replace("{diff}", diff) - } else { - "$content\n$diff" - } - } - fun saveOpenAIToken(token: String) { try { PasswordSafe.instance.setPassword(getCredentialAttributes(openAITokenTitle), token) diff --git a/src/main/kotlin/com/github/blarc/ai/commits/intellij/plugin/settings/ProjectSettings.kt b/src/main/kotlin/com/github/blarc/ai/commits/intellij/plugin/settings/ProjectSettings.kt index 19d3116..e7167ca 100644 --- a/src/main/kotlin/com/github/blarc/ai/commits/intellij/plugin/settings/ProjectSettings.kt +++ b/src/main/kotlin/com/github/blarc/ai/commits/intellij/plugin/settings/ProjectSettings.kt @@ -2,6 +2,7 @@ package com.github.blarc.ai.commits.intellij.plugin.settings import com.github.blarc.ai.commits.intellij.plugin.AICommitsUtils import com.intellij.openapi.components.PersistentStateComponent +import com.intellij.openapi.components.Service import com.intellij.openapi.components.State import com.intellij.openapi.components.Storage import com.intellij.util.xmlb.XmlSerializerUtil @@ -10,6 +11,7 @@ import com.intellij.util.xmlb.XmlSerializerUtil name = ProjectSettings.SERVICE_NAME, storages = [Storage("AICommit.xml")] ) +@Service(Service.Level.PROJECT) class ProjectSettings : PersistentStateComponent { companion object { @@ -29,4 +31,4 @@ class ProjectSettings : PersistentStateComponent { } -} \ No newline at end of file +} diff --git a/src/main/kotlin/com/github/blarc/ai/commits/intellij/plugin/settings/prompt/PromptTable.kt b/src/main/kotlin/com/github/blarc/ai/commits/intellij/plugin/settings/prompt/PromptTable.kt index 8c696b8..aff3b18 100644 --- a/src/main/kotlin/com/github/blarc/ai/commits/intellij/plugin/settings/prompt/PromptTable.kt +++ b/src/main/kotlin/com/github/blarc/ai/commits/intellij/plugin/settings/prompt/PromptTable.kt @@ -2,10 +2,17 @@ package com.github.blarc.ai.commits.intellij.plugin.settings.prompt import ai.grazie.utils.applyIf import com.github.blarc.ai.commits.intellij.plugin.AICommitsBundle.message +import com.github.blarc.ai.commits.intellij.plugin.AICommitsUtils +import com.github.blarc.ai.commits.intellij.plugin.AICommitsUtils.commonBranch +import com.github.blarc.ai.commits.intellij.plugin.AICommitsUtils.computeDiff +import com.github.blarc.ai.commits.intellij.plugin.AICommitsUtils.isPromptTooLarge import com.github.blarc.ai.commits.intellij.plugin.createColumn import com.github.blarc.ai.commits.intellij.plugin.notBlank import com.github.blarc.ai.commits.intellij.plugin.settings.AppSettings import com.github.blarc.ai.commits.intellij.plugin.unique +import com.intellij.dvcs.repo.VcsRepositoryManager +import com.intellij.ide.DataManager +import com.intellij.openapi.actionSystem.CommonDataKeys import com.intellij.openapi.ui.DialogWrapper import com.intellij.ui.components.JBTextArea import com.intellij.ui.components.JBTextField @@ -13,10 +20,15 @@ import com.intellij.ui.dsl.builder.Align import com.intellij.ui.dsl.builder.bindText import com.intellij.ui.dsl.builder.panel import com.intellij.ui.table.TableView +import com.intellij.ui.util.minimumWidth +import com.intellij.ui.util.preferredHeight +import com.intellij.ui.util.preferredWidth import com.intellij.util.ui.ListTableModel +import git4idea.branch.GitBranchWorker import java.awt.event.MouseAdapter import java.awt.event.MouseEvent import javax.swing.ListSelectionModel.SINGLE_SELECTION +import kotlin.math.max class PromptTable { private var prompts = AppSettings.instance.prompts @@ -98,14 +110,18 @@ class PromptTable { val promptNameTextField = JBTextField() val promptDescriptionTextField = JBTextField() val promptContentTextArea = JBTextArea() + val promptPreviewTextArea = JBTextArea() + lateinit var branch: String + lateinit var diff: String init { title = newPrompt?.let { message("settings.prompt.edit.title") } ?: message("settings.prompt.add.title") setOKButtonText(newPrompt?.let { message("actions.update") } ?: message("actions.add")) - setSize(700, 500) promptContentTextArea.wrapStyleWord = true promptContentTextArea.lineWrap = true + promptContentTextArea.rows = 15 + promptContentTextArea.autoscrolls = false if (!prompt.canBeChanged) { isOKActionEnabled = false @@ -114,6 +130,25 @@ class PromptTable { promptContentTextArea.isEditable = false } + promptPreviewTextArea.wrapStyleWord = true + promptPreviewTextArea.lineWrap = true + promptPreviewTextArea.isEditable = false + promptPreviewTextArea.rows = 25 + promptPreviewTextArea.columns = 100 + promptPreviewTextArea.autoscrolls = false + + DataManager.getInstance().dataContextFromFocusAsync.onSuccess { + val project = it.getData(CommonDataKeys.PROJECT) + val changes = VcsRepositoryManager.getInstance(project!!).repositories.stream() + .map { r -> GitBranchWorker.loadTotalDiff(r, r.currentBranchName!!) } + .flatMap { r -> r.stream() } + .toList() + + branch = commonBranch(changes, project) + diff = computeDiff(changes, project) + setPreview(prompt.content) + } + init() } @@ -135,17 +170,30 @@ class PromptTable { row { label(message("settings.prompt.content")) } - row() { - cell(promptContentTextArea) - .align(Align.FILL) + row { + scrollCell(promptContentTextArea) .bindText(prompt::content) .validationOnApply { notBlank(it.text) } - .resizableColumn() - }.resizableRow() + .onChanged { setPreview(it.text)} + .align(Align.FILL) + } + row { + label("Preview") + } + row { + scrollCell(promptPreviewTextArea) + .align(Align.FILL) + } row { comment(message("settings.prompt.comment")) } } + private fun setPreview(promptContent: String) { + val constructPrompt = AICommitsUtils.constructPrompt(promptContent, diff, branch) + promptPreviewTextArea.text = constructPrompt.substring(0, constructPrompt.length.coerceAtMost(10000)) + promptPreviewTextArea.caretPosition = max(0, promptContentTextArea.caretPosition - 10) + } + } -} \ No newline at end of file +} diff --git a/src/main/resources/META-INF/plugin.xml b/src/main/resources/META-INF/plugin.xml index 4f20490..58be311 100644 --- a/src/main/resources/META-INF/plugin.xml +++ b/src/main/resources/META-INF/plugin.xml @@ -60,19 +60,20 @@ Read more: https://plugins.jetbrains.com/docs/intellij/plugin-extension-points.html --> - - + parentId="tools" + instance="com.github.blarc.ai.commits.intellij.plugin.settings.AppSettingsConfigurable" + key="name" + /> + key="settings.exclusions.group.title" + /> + implementation="com.github.blarc.ai.commits.intellij.plugin.listeners.ApplicationStartupListener" + /> diff --git a/src/main/resources/messages/MyBundle.properties b/src/main/resources/messages/MyBundle.properties index 87c3ca4..3d45b0a 100644 --- a/src/main/resources/messages/MyBundle.properties +++ b/src/main/resources/messages/MyBundle.properties @@ -46,7 +46,7 @@ settings.prompt.content=Content validation.required=This value is required. validation.number=Value is not a number. validation.temperature=Temperature should be between 0 and 2. -settings.prompt.comment=You can use variables {locale}, {diff} and {branch} to customise your prompt. +settings.prompt.comment=You can use variables {locale}, {diff} and {branch} to customise your prompt. Prompt preview shows only the first 10000 characters. actions.update=Update actions.add=Add settings.prompt.edit.title=Edit Prompt