Skip to content

Commit

Permalink
Merge pull request #341 from devoxx/issue-339
Browse files Browse the repository at this point in the history
Feat #339 Git view/merge LLM view
  • Loading branch information
stephanj authored Dec 2, 2024
2 parents a14b757 + e9e046b commit c474a1d
Show file tree
Hide file tree
Showing 17 changed files with 587 additions and 7 deletions.
2 changes: 1 addition & 1 deletion build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ plugins {
}

group = "com.devoxx.genie"
version = "0.2.28"
version = "0.2.30"

repositories {
mavenCentral()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,4 +11,3 @@ public interface FileListObserver {

void allFilesRemoved();
}

18 changes: 18 additions & 0 deletions src/main/java/com/devoxx/genie/service/MessageCreationService.java
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,13 @@ public class MessageCreationService {

public static final String CONTEXT_PROMPT = "Context: \n";

private static final String GIT_DIFF_INSTRUCTIONS = """
Please analyze the code and provide ONLY the modified code in your response.
Do not include any explanations or comments.
The response should contain just the modified code wrapped in a code block using the appropriate language identifier.
If multiple files need to be modified, provide each file's content in a separate code block.
""";

@NotNull
public static MessageCreationService getInstance() {
return ApplicationManager.getApplication().getService(MessageCreationService.class);
Expand Down Expand Up @@ -56,6 +63,11 @@ public UserMessage createUserMessage(@NotNull ChatMessageContext chatMessageCont
stringBuilder.append("<SystemPrompt>").append(systemPrompt).append("</SystemPrompt>\n\n");
}

// If git diff is enabled, add special instructions
if (DevoxxGenieStateService.getInstance().getUseDiffMerge()) {
stringBuilder.append("<DiffInstructions>").append(GIT_DIFF_INSTRUCTIONS).append("</DiffInstructions>\n\n");
}

// The user prompt is always appended
appendIfNotEmpty(stringBuilder, "<UserPrompt>" + chatMessageContext.getUserPrompt() + "</UserPrompt>");

Expand Down Expand Up @@ -124,6 +136,12 @@ public UserMessage createUserMessage(@NotNull ChatMessageContext chatMessageCont
String context) {
StringBuilder stringBuilder = new StringBuilder();

// If git diff is enabled, add special instructions at the beginning
if (DevoxxGenieStateService.getInstance().getUseDiffMerge() ||
DevoxxGenieStateService.getInstance().getUseSimpleDiff()) {
stringBuilder.append("<DiffInstructions>").append(GIT_DIFF_INSTRUCTIONS).append("</DiffInstructions>\n\n");
}

// Check if this is the first message in the conversation, if so add the context
if (ChatMemoryService.getInstance().messages(chatMessageContext.getProject()).size() == 1) {
stringBuilder.append("<Context>");
Expand Down
113 changes: 113 additions & 0 deletions src/main/java/com/devoxx/genie/service/gitdiff/GitMergeService.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
package com.devoxx.genie.service.gitdiff;

import com.devoxx.genie.ui.settings.DevoxxGenieStateService;
import com.devoxx.genie.ui.util.NotificationUtil;
import com.intellij.diff.DiffContentFactory;
import com.intellij.diff.DiffManager;
import com.intellij.diff.DiffManagerImpl;
import com.intellij.diff.contents.DiffContent;
import com.intellij.diff.contents.DocumentContent;
import com.intellij.diff.contents.DocumentContentImpl;
import com.intellij.diff.requests.DiffRequest;
import com.intellij.diff.requests.SimpleDiffRequest;
import com.intellij.diff.requests.TextMergeRequestImpl;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.editor.Document;
import com.intellij.openapi.editor.impl.DocumentImpl;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.vfs.VirtualFile;
import org.jetbrains.annotations.NotNull;

import java.util.List;

public class GitMergeService {

private static final Logger LOG = Logger.getInstance(GitMergeService.class);

@NotNull
public static GitMergeService getInstance() {
return ApplicationManager.getApplication().getService(GitMergeService.class);
}

/**
* Shows a three-way merge view with:
* - Left: Original code
* - Center: Merge result (initially empty)
* - Right: LLM's modified version
*/
public void showMerge(@NotNull Project project,
@NotNull String originalContent,
@NotNull String modifiedContent,
@NotNull String title,
@NotNull Document targetDocument) {
DevoxxGenieStateService instance = DevoxxGenieStateService.getInstance();
if (!instance.getUseSimpleDiff() && !instance.getUseDiffMerge()) {
LOG.info("Diff view is disabled");
return;
}

ApplicationManager.getApplication().invokeLater(() -> {
DiffContentFactory factory = DiffContentFactory.getInstance();

// Create contents for three-way merge
DocumentContent originalContentDoc = factory.create(project, originalContent);

DocumentContent modifiedContent1 = factory.create(project, modifiedContent);
DocumentContent targetDocumentContent = factory.create(project, targetDocument);

Document originalDocument = originalContentDoc.getDocument();

TextMergeRequestImpl request = new TextMergeRequestImpl(
project,
targetDocumentContent,
originalDocument.getCharsSequence(),
List.of(originalContentDoc, modifiedContent1, targetDocumentContent),
title,
List.of("Original Code", "LLM Modified Code", "Merge Result")
);

// Show the merge dialog
DiffManager.getInstance().showMerge(project, request);
});
}

/**
* Git diff view
* @param project the project
*/
public void showDiffView(Project project, VirtualFile originalFile, String suggestedContent) {

ApplicationManager.getApplication().runReadAction(() -> {
if (originalFile == null) {
NotificationUtil.sendNotification(project, "Files not found");
return;
}

if (suggestedContent == null || suggestedContent.isEmpty()) {
NotificationUtil.sendNotification(project, "Suggested content is empty");
return;
}
Document originalContent = com.intellij.openapi.fileEditor.FileDocumentManager
.getInstance()
.getDocument(originalFile);

if (originalContent == null) {
NotificationUtil.sendNotification(project, "Error reading file: " + originalFile.getName());
return;
}

DiffContent content1 = new DocumentContentImpl(originalContent);
DiffContent content2 = new DocumentContentImpl(new DocumentImpl(suggestedContent));

DiffRequest diffRequest = new SimpleDiffRequest(
"Diff",
List.of(content1, content2),
List.of("Original code", "LLM suggested")
);

ApplicationManager.getApplication().invokeLater(() ->
DiffManagerImpl.getInstance().showDiff(project, diffRequest));
});
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,20 +21,29 @@
import com.devoxx.genie.ui.settings.DevoxxGenieStateService;
import com.devoxx.genie.ui.topic.AppTopics;
import com.devoxx.genie.ui.util.NotificationUtil;
import com.intellij.diff.DiffManagerImpl;
import com.intellij.diff.contents.DiffContent;
import com.intellij.diff.contents.FileContentImpl;
import com.intellij.diff.requests.DiffRequest;
import com.intellij.diff.requests.SimpleDiffRequest;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.ui.ComboBox;
import com.intellij.openapi.ui.Messages;
import com.intellij.openapi.ui.Splitter;
import com.intellij.openapi.util.Disposer;
import com.intellij.openapi.util.NlsContexts;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.openapi.wm.ToolWindow;
import com.intellij.ui.OnePixelSplitter;
import com.intellij.ui.components.JBScrollPane;
import com.intellij.util.messages.MessageBusConnection;
import lombok.Getter;
import org.apache.xmlbeans.impl.xb.xsdschema.SimpleContentDocument;
import org.jetbrains.annotations.Contract;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

import javax.swing.*;
import java.awt.*;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,17 @@

import com.intellij.openapi.project.Project;

/**
* Interface for listeners that want to be notified when a prompt is submitted.
*
* @see com.intellij.openapi.project.Project
*/
public interface PromptSubmissionListener {
/**
* Called when a prompt is submitted.
*
* @param project the current project
* @param prompt the submitted prompt
*/
void onPromptSubmitted(Project project, String prompt);
}
139 changes: 139 additions & 0 deletions src/main/java/com/devoxx/genie/ui/panel/ChatResponsePanel.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,20 @@

import com.devoxx.genie.model.enumarations.ModelProvider;
import com.devoxx.genie.model.request.ChatMessageContext;
import com.devoxx.genie.model.request.EditorInfo;
import com.devoxx.genie.service.FileListManager;
import com.devoxx.genie.service.ProjectContentService;
import com.devoxx.genie.service.gitdiff.GitMergeService;
import com.devoxx.genie.ui.component.ExpandablePanel;
import com.devoxx.genie.ui.processor.NodeProcessorFactory;
import com.devoxx.genie.ui.settings.DevoxxGenieStateService;
import com.devoxx.genie.util.DefaultLLMSettingsUtil;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.editor.Document;
import com.intellij.openapi.editor.Editor;
import com.intellij.openapi.fileEditor.FileDocumentManager;
import com.intellij.openapi.fileEditor.FileEditorManager;
import com.intellij.openapi.util.Computable;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.ui.JBColor;
import com.knuddels.jtokkit.api.Encoding;
Expand All @@ -22,7 +30,10 @@
import javax.swing.*;
import java.awt.*;
import java.text.NumberFormat;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
import java.util.Objects;

import static com.devoxx.genie.ui.util.DevoxxGenieFontsUtil.SourceCodeProFontPlan14;

Expand Down Expand Up @@ -53,6 +64,16 @@ public ChatResponsePanel(@NotNull ChatMessageContext chatMessageContext) {
private void addResponsePane(@NotNull ChatMessageContext chatMessageContext) {
String markDownResponse = chatMessageContext.getAiMessage().text();
Node document = Parser.builder().build().parse(markDownResponse);

DevoxxGenieStateService stateService = DevoxxGenieStateService.getInstance();

// If git diff is enabled, try to extract code blocks and show diff
if (stateService.getUseDiffMerge()) {
processGitMerge(chatMessageContext, document);
} else if (stateService.getUseSimpleDiff()) {
processGitDiff(chatMessageContext, document);
}

addDocumentNodesToPanel(document);

if (chatMessageContext.hasFiles()) {
Expand All @@ -67,6 +88,65 @@ private void addResponsePane(@NotNull ChatMessageContext chatMessageContext) {
}
}

private void processGitMerge(@NotNull ChatMessageContext chatMessageContext, @NotNull Node document) {
// Get original code from context
String originalCode;
EditorInfo editorInfo = chatMessageContext.getEditorInfo();

// Wrap the document access in a read action
originalCode = ApplicationManager
.getApplication()
.runReadAction((Computable<String>) () -> {
if (editorInfo.getSelectedText() != null) {
return editorInfo.getSelectedText();
} else {
List<VirtualFile> selectedFiles = editorInfo.getSelectedFiles();
if (selectedFiles == null || selectedFiles.isEmpty()) {
return null;
}
VirtualFile originalFile = selectedFiles.get(0);
Document originalDoc = FileDocumentManager.getInstance().getDocument(originalFile);
if (originalDoc == null) {
return null;
}
return originalDoc.getText();
}
});

if (originalCode == null) {
return;
}


// Find the first code block in the response
Node node = document.getFirstChild();
while (node != null) {
if (node instanceof FencedCodeBlock codeBlock) {
String modifiedCode = codeBlock.getLiteral();

Document editorDocument = com.intellij.openapi.application.ApplicationManager.getApplication()
.runReadAction((com.intellij.openapi.util.Computable<Document>) () ->
Objects.requireNonNull(FileEditorManager.getInstance(chatMessageContext.getProject())
.getSelectedTextEditor())
.getDocument()
);

// Show merge using our service
GitMergeService.getInstance().showMerge(
chatMessageContext.getProject(),
originalCode,
modifiedCode,
"Merge LLM Changes",
editorDocument
);

// Only show diff for first code block
break;
}
node = node.getNext();
}
}

private void addMetricExecutionInfo(@NotNull ChatMessageContext chatMessageContext) {
JPanel metricExecutionInfoPanel = new JPanel(new FlowLayout(FlowLayout.LEFT));
metricExecutionInfoPanel.setOpaque(false);
Expand Down Expand Up @@ -99,6 +179,65 @@ private void addMetricExecutionInfo(@NotNull ChatMessageContext chatMessageConte
add(metricExecutionInfoPanel);
}

/**
* Process the git diff.
* @param chatMessageContext the chat message context
* @param document the document
*/
private void processGitDiff(@NotNull ChatMessageContext chatMessageContext, @NotNull Node document) {
// Get original file info
EditorInfo editorInfo = chatMessageContext.getEditorInfo();
if (editorInfo == null) {
return;
}

// Handle single file case
if (editorInfo.getSelectedText() != null) {
Editor editor = FileEditorManager.getInstance(chatMessageContext.getProject())
.getSelectedTextEditor();

if (editor != null) {
String originalCode = editorInfo.getSelectedText();

// Find first code block in response
Node node = document.getFirstChild();
while (node != null) {
if (node instanceof FencedCodeBlock codeBlock) {

GitMergeService.getInstance().showMerge(
chatMessageContext.getProject(),
originalCode,
codeBlock.getLiteral(),
"Merge LLM Changes",
editor.getDocument()
);
break;
}
node = node.getNext();
}
}
}
// Handle multiple files case
else if (editorInfo.getSelectedFiles() != null && !editorInfo.getSelectedFiles().isEmpty()) {
List<VirtualFile> files = editorInfo.getSelectedFiles();
List<String> modifiedContents = new ArrayList<>();

// Collect modified contents from code blocks
Node node = document.getFirstChild();
while (node != null) {
if (node instanceof FencedCodeBlock codeBlock) {
modifiedContents.add(codeBlock.getLiteral());
}
node = node.getNext();
}

GitMergeService.getInstance().showDiffView(
chatMessageContext.getProject(),
files.get(0),
modifiedContents.get(0));
}
}

/**
* Ollama does not count the input context tokens in the token usage, this method fixes this.
*
Expand Down
Loading

0 comments on commit c474a1d

Please sign in to comment.