From 9f735750f6709c9f8a0c03f49d04f7f0b1aea629 Mon Sep 17 00:00:00 2001 From: Shiwani Gupta Date: Sun, 20 Oct 2024 21:47:13 +0530 Subject: [PATCH] Improve AI assistance for generating commit messages Implemented a new method in JeddictChatModel for generating commit message suggestions based on the provided git diff and initial commit message. Updated LearnFix and AskAIProjectAction classes to utilize this new functionality for improved AI-driven commit message assistance. Also added a user-friendly dialog in UIUtil for entering initial commit messages. --- .../github/jeddict/ai/JeddictChatModel.java | 68 +++++++++ .../ai/actions/AskAIProjectAction.java | 2 +- .../actions/GenerateCommitMessageAction.java | 130 ++++++++++++++++++ .../io/github/jeddict/ai/hints/LearnFix.java | 21 ++- .../io/github/jeddict/ai/util/UIUtil.java | 38 +++++ 5 files changed, 253 insertions(+), 6 deletions(-) create mode 100644 src/main/java/io/github/jeddict/ai/actions/GenerateCommitMessageAction.java diff --git a/src/main/java/io/github/jeddict/ai/JeddictChatModel.java b/src/main/java/io/github/jeddict/ai/JeddictChatModel.java index b26b8d9..fa882e0 100644 --- a/src/main/java/io/github/jeddict/ai/JeddictChatModel.java +++ b/src/main/java/io/github/jeddict/ai/JeddictChatModel.java @@ -721,6 +721,74 @@ public String enhanceExpressionStatement(String classContent, String parentConte return enhanced; } + public String generateCommitMessageSuggestions(String gitDiffOutput, String initialCommitMessage) { + StringBuilder prompt = new StringBuilder(); + prompt.append("You are an API server that generates commit message suggestions based on the provided 'git diff' and 'git status' output in HTML format. ") + .append("Please provide various types of commit messages based on the changes: \n") + .append("- Very Short\n") + .append("- Short\n") + .append("- Medium\n") + .append("- Long\n") + .append("- Descriptive\n\n") + .append("Here is the 'git diff' and 'git status' output:\n") + .append(gitDiffOutput.replace("\n", "
")) // Use
for HTML rendering + .append("\n"); + + // Add initial commit message to the prompt if it is not empty or null + if (initialCommitMessage != null && !initialCommitMessage.isEmpty()) { + prompt.append("Initial Commit Message:\n").append(initialCommitMessage).append("
"); + } + + prompt.append("Please respond with the commit messages strictly in HTML format only. " + + "Do not use any Markdown or code formatting (like backticks or triple backticks). " + + "Ensure the HTML content is well-structured, styled using Bootstrap CSS, " + + "and split any long lines (over 100 characters) with
tags for multiline display. " + + "Your response should look like this:
" + + "

Very Short:

Commit message here
" + + "

Short:

Commit message here
" + + "

Medium:

Commit message here
" + + "

Long:

Commit message here
" + + "

Descriptive:

Commit message here
" + + "Do not include any other text or formatting outside of HTML."); + + // Generate the commit message suggestions + String answer = generate(prompt.toString()); + + // Wrap long lines with
tags + answer = wrapLongLinesWithBr(removeCodeBlockMarkers(answer), 100); + + return answer; + } + + private String wrapLongLinesWithBr(String input, int maxLineLength) { + StringBuilder wrapped = new StringBuilder(); + String[] lines = input.split("
"); // Split by existing line breaks + + for (String line : lines) { + String[] words = line.split(" "); // Split line into words + StringBuilder currentLine = new StringBuilder(); + + for (String word : words) { + // Check if adding the next word exceeds the maximum line length + if (currentLine.length() + word.length() + 1 > maxLineLength) { + // If current line is not empty, append it to the result + if (currentLine.length() > 0) { + wrapped.append(currentLine.toString().trim()).append("
"); + currentLine.setLength(0); // Reset current line + } + } + currentLine.append(word).append(" "); // Append the word with a space + } + + // Append any remaining words in the current line + if (currentLine.length() > 0) { + wrapped.append(currentLine.toString().trim()).append("
"); + } + } + + return wrapped.toString(); + } + public String generateHtmlDescriptionForProject(String projectContent, String query) { String prompt = "You are an API server that provides answer to query of following project in HTML. " + "Do not include additional text or explanations outside of the HTML content.\n\n" diff --git a/src/main/java/io/github/jeddict/ai/actions/AskAIProjectAction.java b/src/main/java/io/github/jeddict/ai/actions/AskAIProjectAction.java index a50bede..950c838 100644 --- a/src/main/java/io/github/jeddict/ai/actions/AskAIProjectAction.java +++ b/src/main/java/io/github/jeddict/ai/actions/AskAIProjectAction.java @@ -41,7 +41,7 @@ displayName = "#CTL_AskAIProjectAction", lazy = false, asynchronous = true) @ActionReferences({ @ActionReference(path = "Projects/Actions", position = 100),}) -@Messages({"CTL_AskAIProjectAction=Ask AI"}) +@Messages({"CTL_AskAIProjectAction=AI Query Project"}) public final class AskAIProjectAction extends AbstractAction implements ContextAwareAction { @Override diff --git a/src/main/java/io/github/jeddict/ai/actions/GenerateCommitMessageAction.java b/src/main/java/io/github/jeddict/ai/actions/GenerateCommitMessageAction.java new file mode 100644 index 0000000..577de80 --- /dev/null +++ b/src/main/java/io/github/jeddict/ai/actions/GenerateCommitMessageAction.java @@ -0,0 +1,130 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package io.github.jeddict.ai.actions; + +import io.github.jeddict.ai.hints.LearnFix; +import io.github.jeddict.ai.settings.PreferencesManager; +import io.github.jeddict.ai.util.UIUtil; +import java.awt.event.ActionEvent; +import java.io.BufferedReader; +import java.io.File; +import java.io.IOException; +import java.io.InputStreamReader; +import javax.swing.AbstractAction; +import javax.swing.Action; +import org.netbeans.api.project.Project; +import org.openide.awt.ActionID; +import org.openide.awt.ActionReference; +import org.openide.awt.ActionReferences; +import org.openide.awt.ActionRegistration; +import org.openide.awt.DynamicMenuContent; +import org.openide.filesystems.FileUtil; +import org.openide.util.ContextAwareAction; +import org.openide.util.Lookup; +import org.openide.util.NbBundle.Messages; + +/** + * + * @author Shiwani Gupta + */ +@ActionID( + category = "Project", + id = "io.github.jeddict.ai.actions.GenerateCommitMessageAction") +@ActionRegistration( + displayName = "#CTL_GenerateCommitMessageAction", lazy = false, asynchronous = true) +@ActionReferences({ + @ActionReference(path = "Projects/Actions", position = 100),}) +@Messages({"CTL_GenerateCommitMessageAction=AI Commit Message"}) +public final class GenerateCommitMessageAction extends AbstractAction implements ContextAwareAction { + + @Override + public void actionPerformed(ActionEvent ev) { + // This can remain empty as the context action will handle the logic + } + + @Override + public Action createContextAwareInstance(Lookup actionContext) { + if (actionContext != null) { + Project project = actionContext.lookup(Project.class); + boolean isGitProject = project != null && isGitRepository(project); + return new GenerateCommitMessageAction.ContextAction( + isGitProject && PreferencesManager.getInstance().isAiAssistantActivated(), + project + ); + } + return new GenerateCommitMessageAction.ContextAction(false, null); + } + + private boolean isGitRepository(Project project) { + File projectDir = FileUtil.toFile(project.getProjectDirectory()); // Get the project directory as a File + if (projectDir != null) { + File gitDir = new File(projectDir, ".git"); // Check for .git directory + return gitDir.exists() && gitDir.isDirectory(); // Ensure it exists and is a directory + } + return false; + } + + private static final class ContextAction extends AbstractAction { + + private final Project project; + + private ContextAction(boolean enable, Project project) { + super(Bundle.CTL_GenerateCommitMessageAction()); + this.putValue(DynamicMenuContent.HIDE_WHEN_DISABLED, true); + this.setEnabled(enable); + this.project = project; + } + + @Override + public void actionPerformed(ActionEvent evt) { + StringBuilder sb = new StringBuilder(); + sb.append("git diff \n\n"); + String diffOutput = runGitCommand("diff"); + sb.append(diffOutput); + sb.append("\n\n git status \n\n"); + String statusOutput = runGitCommand("status"); + sb.append(statusOutput); + String intitalCommitMessage = UIUtil.askForInitialCommitMessage(); + LearnFix learnFix = new LearnFix(io.github.jeddict.ai.completion.Action.QUERY); + learnFix.askQueryForProjectCommit(project, sb.toString(), intitalCommitMessage); + } + + private String runGitCommand(String command) { + StringBuilder output = new StringBuilder(); + try { + ProcessBuilder processBuilder = new ProcessBuilder("git", command); + File projectDir = FileUtil.toFile(project.getProjectDirectory()); // Convert FileObject to File + processBuilder.directory(projectDir); // Set the working directory to the project root + Process process = processBuilder.start(); + + try (BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()))) { + String line; + while ((line = reader.readLine()) != null) { + output.append(line).append("\n"); + } + } + process.waitFor(); + } catch (IOException | InterruptedException e) { + e.printStackTrace(); // Handle exceptions appropriately in your production code + } + return output.toString(); + } + + } +} diff --git a/src/main/java/io/github/jeddict/ai/hints/LearnFix.java b/src/main/java/io/github/jeddict/ai/hints/LearnFix.java index 9e88d93..568be2f 100644 --- a/src/main/java/io/github/jeddict/ai/hints/LearnFix.java +++ b/src/main/java/io/github/jeddict/ai/hints/LearnFix.java @@ -89,6 +89,7 @@ public class LearnFix extends JavaFix { private int currentResponseIndex = -1; private String javaCode = null; private String projectContent; + private String commitChanges; private PreferencesManager pm = PreferencesManager.getInstance(); public LearnFix(TreePathHandle tpHandle, Action action, TreePath treePath) { @@ -148,7 +149,7 @@ protected void performRewrite(JavaFix.TransformationContext tc) throws Exception name = ((ClassTree) leaf).getSimpleName().toString(); } - displayHtmlContent(removeCodeBlockMarkers(response), name); + displayHtmlContent(removeCodeBlockMarkers(response), name+ " AI Assistance"); } } @@ -176,7 +177,15 @@ public void askQueryForProject(Project project, String userQuery) { projectContent = inputForAI.toString(); String response = new JeddictChatModel().generateHtmlDescriptionForProject(projectContent, userQuery); - displayHtmlContent(removeCodeBlockMarkers(response), projectName); + displayHtmlContent(removeCodeBlockMarkers(response), projectName+ " Projetc GenAI"); + } + + public void askQueryForProjectCommit(Project project, String commitChanges, String intitalCommitMessage) { + ProjectInformation info = ProjectUtils.getInformation(project); + String projectName = info.getDisplayName(); + String response = new JeddictChatModel().generateCommitMessageSuggestions(commitChanges, intitalCommitMessage); + displayHtmlContent(removeCodeBlockMarkers(response), projectName+ " GenAI Commit"); + this.commitChanges = commitChanges; } public void askQueryForPackage(Collection selectedPackages, String userQuery) { @@ -208,7 +217,7 @@ public void askQueryForPackage(Collection selectedPackages } projectContent = inputForAI.toString(); String response = new JeddictChatModel().generateHtmlDescriptionForProject(projectContent, userQuery); - displayHtmlContent(removeCodeBlockMarkers(response), projectName); + displayHtmlContent(removeCodeBlockMarkers(response), projectName+ " Package GenAI"); } } @@ -223,7 +232,7 @@ private void displayHtmlContent(final String response, String title) { Preferences prefs = Preferences.userNodeForPackage(AssistantTopComponent.class); prefs.putBoolean(AssistantTopComponent.PREFERENCE_KEY, true); - topComponent = new AssistantTopComponent(title + " AI Assistance"); + topComponent = new AssistantTopComponent(title); updateEditor(response).addHyperlinkListener((HyperlinkEvent e) -> { if (HyperlinkEvent.EventType.ACTIVATED.equals(e.getEventType())) { try { @@ -395,7 +404,9 @@ private void handleQuestion(String question, JButton submitButton) { } } String response; - if (treePath == null && projectContent != null) { + if(commitChanges != null) { + response = new JeddictChatModel().generateCommitMessageSuggestions(commitChanges, question); + } else if (treePath == null || projectContent != null) { response = new JeddictChatModel().generateHtmlDescription(projectContent, null, null, prevChat, question); } else { response = new JeddictChatModel().generateHtmlDescription(null, treePath.getCompilationUnit().toString(), treePath.getLeaf() instanceof MethodTree ? treePath.getLeaf().toString() : null, prevChat, question); diff --git a/src/main/java/io/github/jeddict/ai/util/UIUtil.java b/src/main/java/io/github/jeddict/ai/util/UIUtil.java index 3102971..aaa32a4 100644 --- a/src/main/java/io/github/jeddict/ai/util/UIUtil.java +++ b/src/main/java/io/github/jeddict/ai/util/UIUtil.java @@ -99,4 +99,42 @@ public static String askQuery() { } return query; } + + public static String askForInitialCommitMessage() { + // Create a JTextArea for multiline input + JTextArea textArea = new JTextArea(10, 30); // 10 rows, 30 columns + textArea.setWrapStyleWord(true); + textArea.setLineWrap(true); + + // Add the text area to a JScrollPane + JScrollPane scrollPane = new JScrollPane(textArea); + + // Create a JPanel to hold the scroll pane + JPanel panel = new JPanel(); + panel.add(scrollPane); + + // Show the custom dialog + int option = JOptionPane.showConfirmDialog( + null, + panel, + "Please enter the initial commit message (optional).", + JOptionPane.OK_CANCEL_OPTION, + JOptionPane.PLAIN_MESSAGE, + AssistantTopComponent.icon + ); + + // Check the user's choice + if (option != JOptionPane.OK_OPTION) { + return null; // Exit if the user cancels the input + } + + String initialMessage = textArea.getText().trim(); + + // If the input is empty, return null to indicate no initial message + if (initialMessage.isEmpty()) { + return null; // No input provided + } + + return initialMessage; // Return the provided initial commit message +} }