diff --git a/codegpt-core/src/main/resources/prompts/git-message.txt b/codegpt-core/src/main/resources/prompts/git-message.txt new file mode 100644 index 00000000..78d7cb09 --- /dev/null +++ b/codegpt-core/src/main/resources/prompts/git-message.txt @@ -0,0 +1,3 @@ +Write a short and descriptive git commit message for the following git diff. +Use imperative mood, present tense, active voice and verbs. +Your entire response will be passed directly into git commit. \ No newline at end of file diff --git a/src/main/java/ee/carlrobert/codegpt/actions/GenerateGitCommitMessageAction.java b/src/main/java/ee/carlrobert/codegpt/actions/GenerateGitCommitMessageAction.java new file mode 100644 index 00000000..792736a0 --- /dev/null +++ b/src/main/java/ee/carlrobert/codegpt/actions/GenerateGitCommitMessageAction.java @@ -0,0 +1,139 @@ +package ee.carlrobert.codegpt.actions; + +import static com.intellij.openapi.ui.Messages.OK; +import static com.intellij.util.ObjectUtils.tryCast; +import static ee.carlrobert.codegpt.util.file.FileUtils.getResourceContent; +import static java.util.stream.Collectors.joining; + +import com.intellij.notification.Notification; +import com.intellij.notification.NotificationType; +import com.intellij.notification.Notifications; +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.vcs.VcsDataKeys; +import com.intellij.openapi.vcs.ui.CommitMessage; +import ee.carlrobert.codegpt.CodeGPTBundle; +import ee.carlrobert.codegpt.EncodingManager; +import ee.carlrobert.codegpt.Icons; +import ee.carlrobert.codegpt.completions.CompletionClientProvider; +import ee.carlrobert.codegpt.credentials.OpenAICredentialsManager; +import ee.carlrobert.codegpt.settings.state.OpenAISettingsState; +import ee.carlrobert.codegpt.util.OverlayUtils; +import ee.carlrobert.llm.client.openai.completion.ErrorDetails; +import ee.carlrobert.llm.client.openai.completion.chat.request.OpenAIChatCompletionMessage; +import ee.carlrobert.llm.client.openai.completion.chat.request.OpenAIChatCompletionRequest; +import ee.carlrobert.llm.completion.CompletionEventListener; +import java.io.BufferedReader; +import java.io.File; +import java.io.IOException; +import java.io.InputStreamReader; +import java.util.List; +import org.jetbrains.annotations.NotNull; + +public class GenerateGitCommitMessageAction extends AnAction { + + private final EncodingManager encodingManager; + + public GenerateGitCommitMessageAction() { + super( + CodeGPTBundle.get("action.generateCommitMessage.title"), + CodeGPTBundle.get("action.generateCommitMessage.description"), + Icons.SparkleIcon); + encodingManager = EncodingManager.getInstance(); + } + + @Override + public void update(@NotNull AnActionEvent event) { + var apiKeyExists = OpenAICredentialsManager.getInstance().isApiKeySet(); + event.getPresentation().setEnabled(apiKeyExists); + event.getPresentation().setText(CodeGPTBundle.get(apiKeyExists + ? "action.generateCommitMessage.title" + : "action.generateCommitMessage.serviceWarning")); + } + + @Override + public void actionPerformed(@NotNull AnActionEvent event) { + var project = event.getProject(); + if (project == null || project.getBasePath() == null) { + return; + } + + var gitDiff = getGitDiff(project); + var tokenCount = encodingManager.countTokens(gitDiff); + if (tokenCount > 4096 && OverlayUtils.showTokenSoftLimitWarningDialog(tokenCount) != OK) { + return; + } + + var editor = getCommitMessageEditor(event); + if (editor != null) { + ((EditorEx) editor).setCaretVisible(false); + generateMessage(project, editor, gitDiff); + } + } + + private void generateMessage(Project project, Editor editor, String gitDiff) { + CompletionClientProvider.getOpenAIClient().getChatCompletion( + new OpenAIChatCompletionRequest.Builder(List.of( + new OpenAIChatCompletionMessage("system", + getResourceContent("/prompts/git-message.txt")), + new OpenAIChatCompletionMessage("user", gitDiff))) + .setModel(OpenAISettingsState.getInstance().getModel()) + .build(), + getEventListener(project, editor.getDocument())); + } + + private CompletionEventListener getEventListener(Project project, Document document) { + return new CompletionEventListener() { + private final StringBuilder messageBuilder = new StringBuilder(); + + @Override + public void onMessage(String message) { + messageBuilder.append(message); + var application = ApplicationManager.getApplication(); + application.invokeLater(() -> + application.runWriteAction(() -> + WriteCommandAction.runWriteCommandAction(project, () -> + document.setText(messageBuilder)))); + } + + @Override + public void onError(ErrorDetails error, Throwable ex) { + Notifications.Bus.notify(new Notification( + "CodeGPT Notification Group", + "CodeGPT", + error.getMessage(), + NotificationType.ERROR)); + } + }; + } + + private Editor getCommitMessageEditor(AnActionEvent event) { + var commitMessage = tryCast( + event.getData(VcsDataKeys.COMMIT_MESSAGE_CONTROL), + CommitMessage.class); + return commitMessage != null ? commitMessage.getEditorField().getEditor() : null; + } + + // TODO: Get diff based on the user selection + private String getGitDiff(Project project) { + var process = createGitDiffProcess(project.getBasePath()); + var reader = new BufferedReader(new InputStreamReader(process.getInputStream())); + return reader.lines().collect(joining("\n")); + } + + private Process createGitDiffProcess(String projectPath) { + var processBuilder = new ProcessBuilder("git", "diff", "--cached"); + processBuilder.directory(new File(projectPath)); + try { + return processBuilder.start(); + } catch (IOException ex) { + throw new RuntimeException("Unable to start git diff process", ex); + } + } +} diff --git a/src/main/java/ee/carlrobert/codegpt/settings/configuration/ConfigurationState.java b/src/main/java/ee/carlrobert/codegpt/settings/configuration/ConfigurationState.java index 976f3c17..070bddd2 100644 --- a/src/main/java/ee/carlrobert/codegpt/settings/configuration/ConfigurationState.java +++ b/src/main/java/ee/carlrobert/codegpt/settings/configuration/ConfigurationState.java @@ -21,6 +21,7 @@ public class ConfigurationState implements PersistentStateComponent tableData = EditorActionsUtil.DEFAULT_ACTIONS; public static ConfigurationState getInstance() { @@ -77,4 +78,12 @@ public class ConfigurationState implements PersistentStateComponent tableData) { this.tableData = tableData; } + + public boolean isIgnoreGitCommitTokenLimit() { + return ignoreGitCommitTokenLimit; + } + + public void setIgnoreGitCommitTokenLimit(boolean ignoreGitCommitTokenLimit) { + this.ignoreGitCommitTokenLimit = ignoreGitCommitTokenLimit; + } } diff --git a/src/main/java/ee/carlrobert/codegpt/util/OverlayUtils.java b/src/main/java/ee/carlrobert/codegpt/util/OverlayUtils.java index dbed0b77..0bffe6bf 100644 --- a/src/main/java/ee/carlrobert/codegpt/util/OverlayUtils.java +++ b/src/main/java/ee/carlrobert/codegpt/util/OverlayUtils.java @@ -3,6 +3,7 @@ package ee.carlrobert.codegpt.util; import static com.intellij.openapi.ui.Messages.CANCEL; import static com.intellij.openapi.ui.Messages.OK; import static ee.carlrobert.codegpt.Icons.DefaultIcon; +import static java.lang.String.format; import static java.util.Objects.requireNonNull; import com.intellij.execution.ExecutionBundle; @@ -26,6 +27,7 @@ import com.intellij.util.ui.JBUI; import ee.carlrobert.codegpt.CodeGPTBundle; import ee.carlrobert.codegpt.conversations.ConversationsState; import ee.carlrobert.codegpt.indexes.FolderStructureTreePanel; +import ee.carlrobert.codegpt.settings.configuration.ConfigurationState; import java.awt.Point; import java.awt.event.MouseEvent; import javax.swing.JComponent; @@ -73,8 +75,8 @@ public class OverlayUtils { return MessageDialogBuilder.okCancel( CodeGPTBundle.get("dialog.tokenLimitExceeded.title"), CodeGPTBundle.get("dialog.tokenLimitExceeded.description")) - .yesText(CodeGPTBundle.get("dialog.tokenLimitExceeded.continue")) - .noText(CodeGPTBundle.get("dialog.tokenLimitExceeded.cancel")) + .yesText(CodeGPTBundle.get("dialog.continue")) + .noText(CodeGPTBundle.get("dialog.cancel")) .icon(DefaultIcon) .doNotAsk(new DoNotAskOption.Adapter() { @Override @@ -98,6 +100,35 @@ public class OverlayUtils { .guessWindowAndAsk() ? OK : CANCEL; } + public static int showTokenSoftLimitWarningDialog(int tokenCount) { + return MessageDialogBuilder.okCancel( + CodeGPTBundle.get("dialog.tokenSoftLimitExceeded.title"), + format(CodeGPTBundle.get("dialog.tokenSoftLimitExceeded.description"), tokenCount)) + .yesText(CodeGPTBundle.get("dialog.continue")) + .noText(CodeGPTBundle.get("dialog.cancel")) + .icon(DefaultIcon) + .doNotAsk(new DoNotAskOption.Adapter() { + @Override + public void rememberChoice(boolean isSelected, int exitCode) { + if (isSelected) { + ConfigurationState.getInstance().setIgnoreGitCommitTokenLimit(true); + } + } + + @NotNull + @Override + public String getDoNotShowMessage() { + return ExecutionBundle.message("don.t.ask.again"); + } + + @Override + public boolean shouldSaveOptionsOnCancel() { + return true; + } + }) + .guessWindowAndAsk() ? OK : CANCEL; + } + public static void showSelectedEditorSelectionWarning(AnActionEvent event) { var locationOnScreen = ((MouseEvent) event.getInputEvent()).getLocationOnScreen(); locationOnScreen.y = locationOnScreen.y - 16; diff --git a/src/main/resources/META-INF/plugin.xml b/src/main/resources/META-INF/plugin.xml index 889141e7..b303a719 100644 --- a/src/main/resources/META-INF/plugin.xml +++ b/src/main/resources/META-INF/plugin.xml @@ -45,5 +45,11 @@ + + + + diff --git a/src/main/resources/messages/codegpt.properties b/src/main/resources/messages/codegpt.properties index 31b6ff40..770c724f 100644 --- a/src/main/resources/messages/codegpt.properties +++ b/src/main/resources/messages/codegpt.properties @@ -1,5 +1,8 @@ project.label=CodeGPT notification.group.name=CodeGPT notification group +action.generateCommitMessage.title=Generate Message +action.generateCommitMessage.description=Generate commit message using OpenAI service +action.generateCommitMessage.serviceWarning=Messages can only be generated with the OpenAI service. settings.displayName=CodeGPT: Settings settings.openaiQuotaExceeded=OpenAI quota exceeded. settingsConfigurable.displayName.label=Display name: @@ -83,8 +86,10 @@ dialog.deleteConversation.title=Delete Conversation dialog.deleteConversation.description=Are you sure you want to delete this conversation? dialog.tokenLimitExceeded.title=Token Limit Exceeded dialog.tokenLimitExceeded.description=The maximum default token limit has been reached. Do you want to proceed with the conversation despite the higher messaging cost? -dialog.tokenLimitExceeded.continue=Continue -dialog.tokenLimitExceeded.cancel=Cancel +dialog.tokenSoftLimitExceeded.title=Soft Limit Exceeded +dialog.tokenSoftLimitExceeded.description=Warning: The 'git diff' output contains %d tokens, indicating a substantial amount of changes. Are you sure you want to continue? +dialog.cancel=Cancel +dialog.continue=Continue editor.diff.title=CodeGPT Diff editor.diff.local.content.title=CodeGPT suggested code toolwindow.chat.editor.action.copy.title=Copy