diff --git a/build.gradle.kts b/build.gradle.kts index d875ee67..0f4d4fca 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -42,7 +42,7 @@ intellij { pluginName.set(properties("pluginName")) version.set(properties("platformVersion")) type.set(properties("platformType")) - plugins.set(listOf("java")) + plugins.set(listOf("java", "Git4Idea")) } changelog { diff --git a/src/main/java/ee/carlrobert/codegpt/actions/GenerateGitCommitMessageAction.java b/src/main/java/ee/carlrobert/codegpt/actions/GenerateGitCommitMessageAction.java index 1b7faa0b..79d40759 100644 --- a/src/main/java/ee/carlrobert/codegpt/actions/GenerateGitCommitMessageAction.java +++ b/src/main/java/ee/carlrobert/codegpt/actions/GenerateGitCommitMessageAction.java @@ -28,6 +28,7 @@ import ee.carlrobert.codegpt.EncodingManager; import ee.carlrobert.codegpt.Icons; import ee.carlrobert.codegpt.completions.CompletionRequestService; import ee.carlrobert.codegpt.settings.GeneralSettings; +import ee.carlrobert.codegpt.settings.configuration.CommitMessageTemplate; import ee.carlrobert.codegpt.ui.OverlayUtil; import ee.carlrobert.llm.client.openai.completion.ErrorDetails; import ee.carlrobert.llm.completion.CompletionEventListener; @@ -94,7 +95,10 @@ public class GenerateGitCommitMessageAction extends AnAction { if (editor != null) { ((EditorEx) editor).setCaretVisible(false); CompletionRequestService.getInstance() - .generateCommitMessageAsync(gitDiff, getEventListener(project, editor.getDocument())); + .generateCommitMessageAsync( + project.getService(CommitMessageTemplate.class).getSystemPrompt(), + gitDiff, + getEventListener(project, editor.getDocument())); } } diff --git a/src/main/java/ee/carlrobert/codegpt/completions/CompletionRequestProvider.java b/src/main/java/ee/carlrobert/codegpt/completions/CompletionRequestProvider.java index 23562a49..a4cde93b 100644 --- a/src/main/java/ee/carlrobert/codegpt/completions/CompletionRequestProvider.java +++ b/src/main/java/ee/carlrobert/codegpt/completions/CompletionRequestProvider.java @@ -1,6 +1,5 @@ package ee.carlrobert.codegpt.completions; -import static ee.carlrobert.codegpt.completions.ConversationType.DEFAULT; import static ee.carlrobert.codegpt.completions.ConversationType.FIX_COMPILE_ERRORS; import static ee.carlrobert.codegpt.credentials.CredentialsStore.CredentialKey.CUSTOM_SERVICE_API_KEY; import static ee.carlrobert.codegpt.util.file.FileUtil.getResourceContent; @@ -59,7 +58,6 @@ import java.util.List; import java.util.Map; import java.util.NoSuchElementException; import java.util.Objects; -import java.util.Set; import java.util.UUID; import java.util.stream.Collectors; import java.util.stream.Stream; @@ -77,8 +75,6 @@ public class CompletionRequestProvider { public static final String FIX_COMPILE_ERRORS_SYSTEM_PROMPT = getResourceContent( "/prompts/fix-compile-errors.txt"); - private static final Set OPENAI_SYSTEM_CONVERSATION_TYPES = Set.of( - DEFAULT, FIX_COMPILE_ERRORS); private final EncodingManager encodingManager = EncodingManager.getInstance(); private final Conversation conversation; @@ -157,7 +153,7 @@ public class CompletionRequestProvider { } var systemPrompt = conversationType == FIX_COMPILE_ERRORS - ? FIX_COMPILE_ERRORS_SYSTEM_PROMPT : ConfigurationSettings.getSystemPrompt(); + ? FIX_COMPILE_ERRORS_SYSTEM_PROMPT : ConfigurationSettings.getSystemPrompt(); var prompt = promptTemplate.buildPrompt( systemPrompt, @@ -287,10 +283,13 @@ public class CompletionRequestProvider { private List buildMessages(CallParameters callParameters) { var message = callParameters.getMessage(); var messages = new ArrayList(); - if (OPENAI_SYSTEM_CONVERSATION_TYPES.contains(callParameters.getConversationType())) { - String content = DEFAULT == callParameters.getConversationType() - ? ConfigurationSettings.getSystemPrompt() : FIX_COMPILE_ERRORS_SYSTEM_PROMPT; - messages.add(new OpenAIChatCompletionStandardMessage("system", content)); + if (callParameters.getConversationType() == ConversationType.DEFAULT) { + String systemPrompt = ConfigurationSettings.getCurrentState().getSystemPrompt(); + messages.add(new OpenAIChatCompletionStandardMessage("system", systemPrompt)); + } + if (callParameters.getConversationType() == ConversationType.FIX_COMPILE_ERRORS) { + messages.add( + new OpenAIChatCompletionStandardMessage("system", FIX_COMPILE_ERRORS_SYSTEM_PROMPT)); } for (var prevMessage : conversation.getMessages()) { diff --git a/src/main/java/ee/carlrobert/codegpt/completions/CompletionRequestService.java b/src/main/java/ee/carlrobert/codegpt/completions/CompletionRequestService.java index f4e3a17b..6f34310f 100644 --- a/src/main/java/ee/carlrobert/codegpt/completions/CompletionRequestService.java +++ b/src/main/java/ee/carlrobert/codegpt/completions/CompletionRequestService.java @@ -93,7 +93,6 @@ public final class CompletionRequestService { callParameters.getMessage(), callParameters.getConversationType()), eventListener); - default -> throw new IllegalArgumentException(); }; } @@ -115,13 +114,13 @@ public final class CompletionRequestService { } public void generateCommitMessageAsync( - String prompt, + String systemPrompt, + String gitDiff, CompletionEventListener eventListener) { var configuration = ConfigurationSettings.getCurrentState(); - var commitMessagePrompt = configuration.getCommitMessagePrompt(); var openaiRequest = new OpenAIChatCompletionRequest.Builder(List.of( - new OpenAIChatCompletionStandardMessage("system", commitMessagePrompt), - new OpenAIChatCompletionStandardMessage("user", prompt))) + new OpenAIChatCompletionStandardMessage("system", systemPrompt), + new OpenAIChatCompletionStandardMessage("user", gitDiff))) .setModel(OpenAISettings.getCurrentState().getModel()) .build(); var selectedService = GeneralSettings.getCurrentState().getSelectedService(); @@ -134,18 +133,18 @@ public final class CompletionRequestService { var httpClient = CompletionClientProvider.getDefaultClientBuilder().build(); EventSources.createFactory(httpClient).newEventSource( CompletionRequestProvider.buildCustomOpenAICompletionRequest( - commitMessagePrompt, - prompt), + systemPrompt, + gitDiff), new OpenAIChatCompletionEventSourceListener(eventListener)); break; case ANTHROPIC: var anthropicSettings = AnthropicSettings.getCurrentState(); var claudeRequest = new ClaudeCompletionRequest(); - claudeRequest.setSystem(commitMessagePrompt); + claudeRequest.setSystem(systemPrompt); claudeRequest.setStream(true); claudeRequest.setMaxTokens(configuration.getMaxTokens()); claudeRequest.setModel(anthropicSettings.getModel()); - claudeRequest.setMessages(List.of(new ClaudeCompletionStandardMessage("user", prompt))); + claudeRequest.setMessages(List.of(new ClaudeCompletionStandardMessage("user", gitDiff))); CompletionClientProvider.getClaudeClient() .getCompletionAsync(claudeRequest, eventListener); break; @@ -164,7 +163,7 @@ public final class CompletionRequestService { } else { promptTemplate = settings.getRemoteModelPromptTemplate(); } - var finalPrompt = promptTemplate.buildPrompt(commitMessagePrompt, prompt, List.of()); + var finalPrompt = promptTemplate.buildPrompt(systemPrompt, gitDiff, List.of()); CompletionClientProvider.getLlamaClient().getChatCompletionAsync( new LlamaCompletionRequest.Builder(finalPrompt) .setN_predict(configuration.getMaxTokens()) diff --git a/src/main/java/ee/carlrobert/codegpt/settings/configuration/ConfigurationComponent.java b/src/main/java/ee/carlrobert/codegpt/settings/configuration/ConfigurationComponent.java index 24607c63..0020137a 100644 --- a/src/main/java/ee/carlrobert/codegpt/settings/configuration/ConfigurationComponent.java +++ b/src/main/java/ee/carlrobert/codegpt/settings/configuration/ConfigurationComponent.java @@ -16,7 +16,6 @@ import com.intellij.ui.TitledSeparator; import com.intellij.ui.ToolbarDecorator; import com.intellij.ui.components.JBCheckBox; import com.intellij.ui.components.JBLabel; -import com.intellij.ui.components.JBTextArea; import com.intellij.ui.components.JBTextField; import com.intellij.ui.components.fields.IntegerField; import com.intellij.ui.table.JBTable; @@ -29,6 +28,7 @@ import ee.carlrobert.codegpt.ui.UIUtil; import java.awt.Dimension; import java.util.Arrays; import java.util.LinkedHashMap; +import java.util.List; import java.util.Map; import javax.swing.BorderFactory; import javax.swing.JComponent; @@ -93,7 +93,7 @@ public class ConfigurationComponent { maxTokensField.setColumns(12); maxTokensField.setValue(configuration.getMaxTokens()); - systemPromptTextArea = new JTextArea(); + systemPromptTextArea = new JTextArea(3, 60); if (configuration.getSystemPrompt().isBlank()) { // for backward compatibility systemPromptTextArea.setText(COMPLETION_SYSTEM_PROMPT); @@ -101,13 +101,12 @@ public class ConfigurationComponent { systemPromptTextArea.setText(configuration.getSystemPrompt()); } systemPromptTextArea.setLineWrap(true); + systemPromptTextArea.setWrapStyleWord(true); systemPromptTextArea.setBorder(JBUI.Borders.empty(8, 4)); - systemPromptTextArea.setColumns(60); - systemPromptTextArea.setRows(3); - commitMessagePromptTextArea = new JBTextArea(configuration.getCommitMessagePrompt(), - 3, 60); + commitMessagePromptTextArea = new JTextArea(configuration.getCommitMessagePrompt(), 3, 60); commitMessagePromptTextArea.setLineWrap(true); + commitMessagePromptTextArea.setWrapStyleWord(true); commitMessagePromptTextArea.setBorder(JBUI.Borders.empty(8, 4)); checkForPluginUpdatesCheckBox = new JBCheckBox( @@ -247,20 +246,19 @@ public class ConfigurationComponent { } private JPanel createCommitMessageConfigurationForm() { - var formBuilder = FormBuilder.createFormBuilder(); - addAssistantFormLabeledComponent( - formBuilder, - "configurationConfigurable.section.commitMessage.systemPromptField.label", - "configurationConfigurable.section.commitMessage.systemPromptField.comment", - JBUI.Panels - .simplePanel(commitMessagePromptTextArea) - .withBorder(JBUI.Borders.customLine( - JBUI.CurrentTheme.CustomFrameDecorations.separatorForeground()))); - formBuilder.addVerticalGap(8); - - var form = formBuilder.getPanel(); - form.setBorder(JBUI.Borders.emptyLeft(16)); - return form; + return FormBuilder.createFormBuilder() + .setFormLeftIndent(16) + .addLabeledComponent( + new JBLabel(CodeGPTBundle.get( + "configurationConfigurable.section.commitMessage.systemPromptField.label")) + .withBorder(JBUI.Borders.emptyLeft(2)), + UI.PanelFactory.panel(commitMessagePromptTextArea) + .resizeX(false) + .withComment(CommitMessageTemplate.Companion.getHtmlDescription()) + .createPanel(), + true + ) + .getPanel(); } private ComponentValidator createTemperatureInputValidator( diff --git a/src/main/kotlin/ee/carlrobert/codegpt/settings/configuration/CommitMessageTemplate.kt b/src/main/kotlin/ee/carlrobert/codegpt/settings/configuration/CommitMessageTemplate.kt new file mode 100644 index 00000000..f074c2c9 --- /dev/null +++ b/src/main/kotlin/ee/carlrobert/codegpt/settings/configuration/CommitMessageTemplate.kt @@ -0,0 +1,41 @@ +package ee.carlrobert.codegpt.settings.configuration + +import com.intellij.openapi.components.Service +import com.intellij.openapi.components.Service.Level.PROJECT +import com.intellij.openapi.components.service +import com.intellij.openapi.project.Project +import ee.carlrobert.codegpt.settings.configuration.Placeholder.BRANCH_NAME +import ee.carlrobert.codegpt.settings.configuration.Placeholder.DATE_ISO_8601 + +@Service(PROJECT) +class CommitMessageTemplate private constructor(project: Project) { + + companion object { + fun getHtmlDescription(): String { + val placeholderDescriptions = listOf(BRANCH_NAME, DATE_ISO_8601).joinToString("\n") { + "
  • ${it.name}: ${it.description}
  • " + } + + return buildString { + append("\n") + append("\n") + append("

    Template for generating commit messages. Use the following placeholders to insert dynamic values:

    \n") + append("
      $placeholderDescriptions
    \n") + append("\n") + append("") + } + } + } + + private val placeholderStrategyMapping: Map = mapOf( + BRANCH_NAME to BranchNamePlaceholderStrategy(project), + DATE_ISO_8601 to DatePlaceholderStrategy() + ) + + fun getSystemPrompt(): String = + service().state.commitMessagePrompt.let { template -> + placeholderStrategyMapping.entries.fold(template) { acc, (placeholder, strategy) -> + acc.replace("{${placeholder.name}}", strategy.getReplacementValue()) + } + } +} \ No newline at end of file diff --git a/src/main/kotlin/ee/carlrobert/codegpt/settings/configuration/Placeholder.kt b/src/main/kotlin/ee/carlrobert/codegpt/settings/configuration/Placeholder.kt new file mode 100644 index 00000000..9a940907 --- /dev/null +++ b/src/main/kotlin/ee/carlrobert/codegpt/settings/configuration/Placeholder.kt @@ -0,0 +1,36 @@ +package ee.carlrobert.codegpt.settings.configuration + +import com.intellij.openapi.project.Project +import git4idea.GitUtil +import git4idea.branch.GitBranchUtil +import java.time.LocalDate + +enum class Placeholder(val description: String) { + DATE_ISO_8601("Current date in ISO 8601 format, e.g. 2021-01-01."), + BRANCH_NAME("The name of the current branch") +} + +interface PlaceholderStrategy { + fun getReplacementValue(): String +} + +class DatePlaceholderStrategy : PlaceholderStrategy { + override fun getReplacementValue(): String { + return LocalDate.now().toString() + } +} + +class BranchNamePlaceholderStrategy(val project: Project) : PlaceholderStrategy { + override fun getReplacementValue(): String { + return try { + val repositories = GitUtil.getRepositoryManager(project).repositories + if (repositories.isEmpty() || repositories.size != 1) { + return "BRANCH-UNKNOWN" + } + + GitBranchUtil.getBranchNameOrRev(repositories[0]) + } catch (ignore: Exception) { + "BRANCH-UNKNOWN" + } + } +} \ No newline at end of file diff --git a/src/main/resources/messages/codegpt.properties b/src/main/resources/messages/codegpt.properties index c31b2b00..a2250f20 100644 --- a/src/main/resources/messages/codegpt.properties +++ b/src/main/resources/messages/codegpt.properties @@ -113,9 +113,8 @@ settingsConfigurable.service.custom.openai.url.label=URL: settingsConfigurable.service.custom.openai.linkToDocs=Link to API docs settingsConfigurable.service.custom.openai.connectionSuccess=Connection successful. settingsConfigurable.service.custom.openai.connectionFailed=Connection failed. -configurationConfigurable.section.commitMessage.title=Commit Message -configurationConfigurable.section.commitMessage.systemPromptField.label=Prompt: -configurationConfigurable.section.commitMessage.systemPromptField.comment=Custom system prompt used for commit message generation. +configurationConfigurable.section.commitMessage.title=Commit Message Template +configurationConfigurable.section.commitMessage.systemPromptField.label=Prompt template: configurationConfigurable.section.inlineCompletion.title=Inline Completion configurationConfigurable.section.inlineCompletion.systemPromptField.label=Prompt: configurationConfigurable.section.inlineCompletion.systemPromptField.comment=Custom system prompt used for inline code generation (Fill in the Middle (FIM) template).
    The {pre}, {suf} and {mid} are replaced depending on the used Model's FIM template. diff --git a/src/test/kotlin/ee/carlrobert/codegpt/settings/configuration/CommitMessageTemplateTest.kt b/src/test/kotlin/ee/carlrobert/codegpt/settings/configuration/CommitMessageTemplateTest.kt new file mode 100644 index 00000000..fe57cc8a --- /dev/null +++ b/src/test/kotlin/ee/carlrobert/codegpt/settings/configuration/CommitMessageTemplateTest.kt @@ -0,0 +1,30 @@ +package ee.carlrobert.codegpt.settings.configuration + +import com.intellij.openapi.components.service +import git4idea.commands.GitCommand +import org.assertj.core.api.Assertions.assertThat +import testsupport.VcsTestCase +import java.time.LocalDate + +class CommitMessageTemplateTest : VcsTestCase() { + + fun `test commit message system prompt construction`() { + git(GitCommand.INIT) + git(GitCommand.CHECKOUT, listOf("-b", "feature/my-cool-feature")) + registerRepository() + service().state.commitMessagePrompt = buildString { + append("Branch: {BRANCH_NAME}\n") + append("Date: {DATE_ISO_8601}") + } + + val systemPrompt = project.service().getSystemPrompt() + + assertThat(systemPrompt).isEqualTo( + buildString { + append("Branch: feature/my-cool-feature\n") + append("Date: ${LocalDate.now()}") + } + ) + } +} + diff --git a/src/test/kotlin/testsupport/VcsTestCase.kt b/src/test/kotlin/testsupport/VcsTestCase.kt new file mode 100644 index 00000000..bc3f84e5 --- /dev/null +++ b/src/test/kotlin/testsupport/VcsTestCase.kt @@ -0,0 +1,54 @@ +package testsupport + +import com.intellij.openapi.components.service +import com.intellij.openapi.vcs.ProjectLevelVcsManager +import com.intellij.openapi.vcs.VcsDirectoryMapping +import com.intellij.openapi.vfs.LocalFileSystem +import com.intellij.testFramework.HeavyPlatformTestCase +import git4idea.GitVcs +import git4idea.commands.Git +import git4idea.commands.GitCommand +import git4idea.commands.GitLineHandler +import git4idea.repo.GitRepository +import git4idea.repo.GitRepositoryManager +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.runBlocking +import org.assertj.core.api.Assertions.assertThat +import org.junit.Assert +import java.nio.file.Files +import java.nio.file.Path + +open class VcsTestCase : HeavyPlatformTestCase() { + + private lateinit var projectDir: Path + + @Throws(Exception::class) + override fun setUp() { + super.setUp() + projectDir = tempDir.createDir() + } + + fun git(command: GitCommand, parameters: List = emptyList()) { + val checkoutHandler = GitLineHandler(project, projectDir.toFile(), command) + checkoutHandler.addParameters(parameters) + service().runCommand(checkoutHandler).throwOnError() + } + + fun registerRepository(): GitRepository = + ProjectLevelVcsManager.getInstance(project).run { + directoryMappings = listOf(VcsDirectoryMapping(projectDir.toString(), GitVcs.NAME)) + Files.createDirectories(projectDir) + Assert.assertFalse( + "There are no VCS roots. Active VCSs: $allActiveVcss", + allVcsRoots.isEmpty() + ) + val file = LocalFileSystem.getInstance().refreshAndFindFileByNioFile(projectDir) + + runBlocking(Dispatchers.IO) { + val repository = project.service().getRepositoryForRoot(file) + assertThat(repository).describedAs("Couldn't find repository for root $projectDir") + .isNotNull() + repository!! + } + } +} \ No newline at end of file