Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[FEATURE REQUEST] side-by-side diff window with editing before apply AI generated changes #339

Closed
Slach opened this issue Nov 25, 2024 · 13 comments
Labels
enhancement New feature or request

Comments

@Slach
Copy link

Slach commented Nov 25, 2024

If you develop Cursor.com Composer with Side-by-Side Diff Mode functionality, you will change the market.

Look to following screenshot:

image

On the right side generation window
On the center side generation represents as side-by-side diff, which allow review changes and even edit it before save and apply to the project

Mental it looks like you say task to junior coder and after generation review it

@Slach Slach changed the title [FEATURE REQUEST] [FEATURE REQUEST] side-by-side diff window with editing before apply AI generated changes Nov 25, 2024
@mydeveloperplanet
Copy link
Collaborator

This would be indeed an interesting feature and would make it easier to apply changes

@stephanj
Copy link
Contributor

Asked Claude how to implement this :)

I'll explain how to implement a side-by-side diff window with editing capabilities for an IntelliJ plugin, similar to what's shown in the image above. This implementation will allow users to see and edit AI-generated changes before applying them.

First, let's create the diff window UI component:

public class DiffWindowPanel extends JPanel {
    private final Project project;
    private final Editor originalEditor;
    private final Editor modifiedEditor;
    private final EditorTextField acceptButton;
    private final String originalContent;
    private final String modifiedContent;

    public DiffWindowPanel(Project project, String originalContent, String modifiedContent) {
        super(new BorderLayout());
        this.project = project;
        this.originalContent = originalContent;
        this.modifiedContent = modifiedContent;

        // Create two editors side by side
        JPanel editorsPanel = new JPanel(new GridLayout(1, 2));
        
        // Create original editor
        originalEditor = createEditor("Original", originalContent, false);
        
        // Create modified editor with editing enabled
        modifiedEditor = createEditor("Modified", modifiedContent, true);
        
        editorsPanel.add(originalEditor.getComponent());
        editorsPanel.add(modifiedEditor.getComponent());
        
        add(editorsPanel, BorderLayout.CENTER);
        add(createButtonPanel(), BorderLayout.SOUTH);
    }

    private Editor createEditor(String title, String content, boolean editable) {
        EditorFactory factory = EditorFactory.getInstance();
        Document document = factory.createDocument(content);
        
        Editor editor = factory.createEditor(
            document,
            project,
            FileTypeManager.getInstance().getFileTypeByExtension("java"),
            !editable
        );

        // Configure editor settings
        EditorSettings settings = editor.getSettings();
        settings.setFoldingOutlineShown(true);
        settings.setLineNumbersShown(true);
        settings.setLineMarkerAreaShown(true);
        settings.setIndentGuidesShown(true);
        settings.setGutterIconsShown(true);

        return editor;
    }

    private JPanel createButtonPanel() {
        JPanel buttonPanel = new JPanel(new FlowLayout(FlowLayout.RIGHT));
        
        JButton acceptButton = new JButton("Accept Changes");
        JButton rejectButton = new JButton("Reject");
        
        acceptButton.addActionListener(e -> applyChanges());
        rejectButton.addActionListener(e -> reject());
        
        buttonPanel.add(rejectButton);
        buttonPanel.add(acceptButton);
        
        return buttonPanel;
    }

    private void applyChanges() {
        ApplicationManager.getApplication().runWriteAction(() -> {
            // Get the modified content
            String modifiedText = modifiedEditor.getDocument().getText();
            
            // Apply changes to the original file
            Document originalDocument = originalEditor.getDocument();
            originalDocument.setText(modifiedText);
            
            // Notify success
            NotificationUtil.showInfo(project, "Changes applied successfully");
            
            // Close diff window
            close();
        });
    }

    private void reject() {
        close();
    }

    private void close() {
        // Dispose editors
        EditorFactory.getInstance().releaseEditor(originalEditor);
        EditorFactory.getInstance().releaseEditor(modifiedEditor);
        
        // Close containing window
        Window window = SwingUtilities.getWindowAncestor(this);
        if (window != null) {
            window.dispose();
        }
    }
}

Now, let's create a service to show the diff window:

public class DiffWindowService {
    public static void showDiff(Project project, String originalContent, String modifiedContent) {
        ApplicationManager.getApplication().invokeLater(() -> {
            // Create diff panel
            DiffWindowPanel diffPanel = new DiffWindowPanel(project, originalContent, modifiedContent);
            
            // Create dialog
            DialogWrapper dialog = new DialogWrapper(project) {
                {
                    init();
                    setTitle("Review AI Generated Changes");
                }

                @Override
                protected @Nullable JComponent createCenterPanel() {
                    return diffPanel;
                }

                @Override
                protected Action @NotNull [] createActions() {
                    // Remove default dialog buttons since we have custom ones
                    return new Action[0];
                }
            };

            dialog.show();
        });
    }
}

To use this in your plugin when showing AI-generated changes:

public class AIChangeAction extends AnAction {
    @Override
    public void actionPerformed(@NotNull AnActionEvent e) {
        Project project = e.getProject();
        if (project == null) return;

        // Get current file editor
        FileEditor fileEditor = FileEditorManager.getInstance(project).getSelectedEditor();
        if (!(fileEditor instanceof TextEditor)) return;
        
        Editor editor = ((TextEditor) fileEditor).getEditor();
        String originalContent = editor.getDocument().getText();
        
        // Get AI generated changes
        String aiModifiedContent = getAIModifiedContent(originalContent);
        
        // Show diff window
        DiffWindowService.showDiff(project, originalContent, aiModifiedContent);
    }

    private String getAIModifiedContent(String originalContent) {
        // Implement your AI modification logic here
        return "AI modified content";
    }
}

Key features of this implementation:

  1. Side-by-side editor view showing original and modified content
  2. Syntax highlighting for both editors
  3. Line numbers and other editor features enabled
  4. Modified side is editable to allow manual adjustments
  5. Accept/Reject buttons to apply or discard changes
  6. Changes are only applied when explicitly accepted
  7. Uses IntelliJ's editor components for native look and feel

To enhance this further, you could:

  1. Add diff highlighting between versions:
private void highlightDifferences() {
    DiffManager diffManager = DiffManager.getInstance();
    SimpleDiffRequest request = new SimpleDiffRequest(
        "Review Changes",
        DiffContentFactory.getInstance().create(originalContent),
        DiffContentFactory.getInstance().create(modifiedContent),
        "Original",
        "Modified"
    );
    
    diffManager.showDiff(project, request);
}
  1. Add undo/redo support for the modified editor:
private void setupUndoRedo(Editor editor) {
    UndoManager undoManager = UndoManager.getInstance(project);
    undoManager.setEditorProvider(new EditorProvider() {
        @Override
        public Editor getEditor() {
            return editor;
        }

        @Override
        public void dispose() {
        }
    });
}
  1. Add a preview button to show how the changes would look:
private JButton createPreviewButton() {
    JButton previewButton = new JButton("Preview");
    previewButton.addActionListener(e -> {
        String modifiedText = modifiedEditor.getDocument().getText();
        FileEditorManager.getInstance(project).openTextEditor(
            new OpenFileDescriptor(project, originalFile),
            true
        ).getDocument().setText(modifiedText);
    });
    return previewButton;
}

This implementation provides a professional-grade diff window similar to IntelliJ's built-in diff tool, but customized for reviewing and applying AI-generated changes. Users can see exactly what changes are being proposed, edit them if needed, and choose whether to apply or reject them.

How would the ai prompt look like to support this feature?

I'll help you create an effective prompt structure that supports a diff window feature for reviewing AI-generated code changes. Here's how to structure the prompt:

public class DiffPromptBuilder {
    private static final String DIFF_TEMPLATE = """
        Act as a code editor that proposes changes to the given code. Format your response using XML tags as follows:
        
        <original_code>
        // Include the original code here exactly as provided
        </original_code>
        
        <modified_code>
        // Include your proposed changes here with full context
        </modified_code>
        
        <explanation>
        // Explain the changes you made and why
        </explanation>
        
        Follow these rules:
        1. Always include complete, compilable code in both sections
        2. Maintain the same package and import statements
        3. Keep the same file structure
        4. Include inline comments explaining key changes
        5. Preserve formatting style of the original code
        """;

    public static String buildPrompt(String originalCode, String requestedChanges) {
        return String.format("""
            %s
            
            Original code to modify:
            ```java
            %s
            ```
            
            Requested changes:
            %s
            
            Return the original and modified code in XML tags as specified above.
            """, DIFF_TEMPLATE, originalCode, requestedChanges);
    }
}

Let's implement a service to handle the AI response:

public class AIDiffResponseParser {
    public static DiffContent parseDiffResponse(String aiResponse) {
        try {
            // Extract content between XML tags
            String originalCode = extractContent(aiResponse, "original_code");
            String modifiedCode = extractContent(aiResponse, "modified_code");
            String explanation = extractContent(aiResponse, "explanation");
            
            return new DiffContent(originalCode, modifiedCode, explanation);
        } catch (Exception e) {
            throw new AIResponseParseException("Failed to parse AI response", e);
        }
    }

    private static String extractContent(String response, String tag) {
        Pattern pattern = Pattern.compile(
            String.format("<%s>(.*?)</%s>", tag, tag), 
            Pattern.DOTALL
        );
        Matcher matcher = pattern.matcher(response);
        
        if (matcher.find()) {
            return matcher.group(1).trim();
        }
        throw new AIResponseParseException("Could not find " + tag + " in response");
    }
}

Here's how to use it in your plugin:

public class AICodeModificationService {
    private final Project project;
    private final LangChainService langChainService; // Your AI service

    public void requestCodeModification(PsiFile file, String requestedChanges) {
        String originalCode = file.getText();
        
        // Build the prompt
        String prompt = DiffPromptBuilder.buildPrompt(originalCode, requestedChanges);
        
        // Get AI response
        langChainService.generateResponse(prompt)
            .thenAccept(aiResponse -> {
                DiffContent diffContent = AIDiffResponseParser.parseDiffResponse(aiResponse);
                
                // Show diff window on EDT
                ApplicationManager.getApplication().invokeLater(() -> {
                    DiffWindowService.showDiff(
                        project,
                        diffContent.originalCode(),
                        diffContent.modifiedCode(),
                        diffContent.explanation()
                    );
                });
            })
            .exceptionally(throwable -> {
                NotificationUtil.showError(project, "Failed to generate code changes: " + 
                    throwable.getMessage());
                return null;
            });
    }
}

Example interaction with the AI:

Request:

Please modify this code to add input validation:

public class UserRegistration {
    public void registerUser(String username, String email) {
        User user = new User(username, email);
        userRepository.save(user);
    }
}

AI Response:

<original_code>
public class UserRegistration {
    public void registerUser(String username, String email) {
        User user = new User(username, email);
        userRepository.save(user);
    }
}
</original_code>

<modified_code>
public class UserRegistration {
    /**
     * Registers a new user with validation
     * @throws IllegalArgumentException if username or email is invalid
     */
    public void registerUser(String username, String email) {
        // Validate username
        if (username == null || username.trim().isEmpty()) {
            throw new IllegalArgumentException("Username cannot be empty");
        }
        if (username.length() < 3 || username.length() > 50) {
            throw new IllegalArgumentException("Username must be between 3 and 50 characters");
        }
        
        // Validate email
        if (email == null || !email.matches("^[A-Za-z0-9+_.-]+@(.+)$")) {
            throw new IllegalArgumentException("Invalid email format");
        }
        
        User user = new User(username.trim(), email.trim());
        userRepository.save(user);
    }
}
</modified_code>

<explanation>
Changes made:
1. Added input validation for username:
   - Checks for null or empty values
   - Enforces length requirements
2. Added email format validation
3. Added trimming of input values
4. Added JavaDoc with throws clause
5. Improved error messages for better debugging
</explanation>

The parser would extract these sections and display them in the diff window, allowing the developer to:

  1. See the original code on the left
  2. See and edit the modified code on the right
  3. Review the explanation of changes
  4. Accept, reject, or modify the changes before applying them

A more complex example with the assistant modifying multiple files would look like:

<change_set>
    <file path="src/main/java/com/example/UserRegistration.java">
        <original_code>
            // Original code here
        </original_code>
        <modified_code>
            // Modified code here
        </modified_code>
    </file>
    
    <file path="src/main/java/com/example/User.java">
        <original_code>
            // Original code here
        </original_code>
        <modified_code>
            // Modified code here
        </modified_code>
    </file>
    
    <explanation>
        Changes made across multiple files:
        1. UserRegistration.java:
           - Added input validation
           - Improved error handling
        2. User.java:
           - Added validation annotations
           - Added toString method
    </explanation>
</change_set>

This structured format allows your plugin to:

  1. Parse the response reliably
  2. Handle multiple file changes
  3. Show clear diffs for each file
  4. Provide context for the changes
  5. Support batch accepting/rejecting changes

The key is having a well-defined response format that your plugin can parse and display in the diff window, while maintaining all the context and structure needed for safe code modifications.

@stephanj stephanj added the enhancement New feature or request label Dec 1, 2024
@stephanj
Copy link
Contributor

stephanj commented Dec 2, 2024

Looking good, didn't follow the above Claude suggestions 😅
Using the embedded Git Merge viewer from IDEA...

AddJavaDocUsingOllama.mp4

@Slach
Copy link
Author

Slach commented Dec 2, 2024

@stephanj main approach don't apply changes to local files before review and changing

@Slach
Copy link
Author

Slach commented Dec 2, 2024

@stephanj 3-way merge is not necessary, 2-way merge side-by-side diff looks from a cognitive perspective much better

stephanj added a commit that referenced this issue Dec 2, 2024
stephanj added a commit that referenced this issue Dec 2, 2024
Feat #339 Git view/merge LLM view
@stephanj
Copy link
Contributor

stephanj commented Dec 2, 2024

Here you go, will release v0.3.0 so we can test. Once okay'ed by several people we can push it to the IDEA marketplace.

JavadocGitSimple.mp4

@stephanj stephanj closed this as completed Dec 2, 2024
@stephanj
Copy link
Contributor

stephanj commented Dec 2, 2024

I've actually tested it with Ollama and Llama 3.2 3B and it actually worked amazingly well 👀

@mydeveloperplanet
Copy link
Collaborator

Two comments:

  • I often use /explain between coding tasks. The setting for showing the diff window is now 'hidden' in the settings which makes it a bit difficult to switch. Adding a dropdown to the chat window to switch easily?
  • When I ask to add javadoc to two added files, only a diff for one file is shown although the LLM responded with javadoc for both files

@stephanj
Copy link
Contributor

stephanj commented Dec 3, 2024

  • +1 for easy switch
  • Please create an issue for more than one file, because that's possible but not yet implemented. Would be nice if you include an example prompt/setting so we can easily replicate it.

@stephanj
Copy link
Contributor

stephanj commented Dec 4, 2024

It appears that we're not allowed to use the "Git Diff" manager classes because they're flagged as internal.
Looks like we'll need to develop our own... any suggestions?! 🧞‍♂️ #ThereGoesMyHolidays

image

@stephanj stephanj reopened this Dec 4, 2024
@stephanj
Copy link
Contributor

stephanj commented Dec 4, 2024

Jetbrains informed me to use DiffManagerEx (External) which did the trick

@stephanj stephanj closed this as completed Dec 4, 2024
@Slach
Copy link
Author

Slach commented Dec 4, 2024

So will you release 0.3.1 with DiffManagerEx?

@stephanj
Copy link
Contributor

stephanj commented Dec 4, 2024

Done and available on the marketplace, enjoy. Make sure to let us know what works and what doesn't.

stephanj added a commit that referenced this issue Dec 9, 2024
stephanj added a commit that referenced this issue Dec 9, 2024
Feat #339 Git view/merge LLM view
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New feature or request
Projects
None yet
Development

No branches or pull requests

3 participants