devoxx / DevoxxGenieIDEAPlugin

DevoxxGenie is a plugin for IntelliJ IDEA that uses local LLM's (Ollama, LMStudio, GPT4All, Llama.cpp and Exo) and Cloud based LLMs to help review, test, explain your project code.
https://devoxx.com
MIT License
236 stars 31 forks source link

[BOUNTY ~ 975 € Combi ticket for Devoxx Belgium 2025] : Support Tab auto completion #2

Open stephanj opened 7 months ago

stephanj commented 7 months ago

An experimental AcceptAutocompleteAction (pressing tab key) is available and can be activated be enabling the plugin config file

<actions> 
    <action id="com.devoxx.genie.action.AcceptAutocompleteAction"
            class="com.devoxx.genie.actions.AcceptAutocompleteAction"
            text="Accept Autocomplete Suggestion"
            description="Accept autocomplete suggestion">
        <keyboard-shortcut keymap="$default" first-keystroke="TAB"/>
        <keyboard-shortcut keymap="Mac OS X" first-keystroke="TAB"/>
        </action>
</actions>

However the related prompt does not really return useable auto complete code 😂

/**
     * EXPERIMENTAL : Execute continue prompt
     * @param selectedText the selected text
     * @return the prompt
     */
    public String executeGenieContinuePrompt(String selectedText) {
        ChatLanguageModel chatLanguageModel = getChatLanguageModel();
        List<ChatMessage> messages = new ArrayList<>();
        messages.add(new dev.langchain4j.data.message.SystemMessage(
            YOU_ARE_A_SOFTWARE_DEVELOPER_WITH_EXPERT_KNOWLEDGE_IN + "JAVA" + PROGRAMMING_LANGUAGE +
                "\n\nSelected code: " + selectedText));
        messages.add(new UserMessage("Only return the code which finalises the code block."));
        Response<AiMessage> generate = chatLanguageModel.generate(messages);
        return generate.content().text();
    }

Suggestions welcome how we can accomplish this.

xdHampus commented 4 months ago

This would be a really nice addition! I find myself often opening up VSCode besides IDEA just to get auto completion in code.

I tried a Copilot replacement for VSCode(https://github.com/ex3ndr/llama-coder) that uses Ollama and it seemed to work well. Perhaps their solution could serve as an inspiration 😄

stephanj commented 4 months ago

Claude Sonnet 3.5 suggestion:

This kind of feature involves integrating with IntelliJ's code completion system and potentially using an AI model to generate suggestions. Here's a high-level approach to implement this feature:

  1. Create a Custom Completion Contributor

First, you'll need to create a custom completion contributor that integrates with IntelliJ's completion system:

import com.intellij.codeInsight.completion.*;
import com.intellij.codeInsight.lookup.LookupElementBuilder;
import com.intellij.patterns.PlatformPatterns;
import com.intellij.util.ProcessingContext;
import org.jetbrains.annotations.NotNull;

public class DevoxxGenieCompletionContributor extends CompletionContributor {

    public DevoxxGenieCompletionContributor() {
        extend(CompletionType.BASIC, PlatformPatterns.psiElement(),
            new CompletionProvider<CompletionParameters>() {
                @Override
                protected void addCompletions(@NotNull CompletionParameters parameters,
                                              @NotNull ProcessingContext context,
                                              @NotNull CompletionResultSet result) {
                    // Generate and add completions here
                    String suggestion = generateCodeSuggestion(parameters);
                    if (suggestion != null) {
                        result.addElement(LookupElementBuilder.create(suggestion));
                    }
                }
            }
        );
    }

    private String generateCodeSuggestion(CompletionParameters parameters) {
        // Implement your code suggestion logic here
        // This could involve calling an AI model or using other heuristics
        return null; // Replace with actual suggestion
    }
}
  1. Register the Completion Contributor

Register your completion contributor in your plugin.xml:

<extensions defaultExtensionNs="com.intellij">
    <completion.contributor language="JAVA" 
                            implementationClass="com.devoxx.genie.DevoxxGenieCompletionContributor"/>
</extensions>
  1. Implement Code Suggestion Logic

The core of your autocompletion feature will be in the generateCodeSuggestion method. This is where you'll need to analyze the current context and generate appropriate suggestions. Here's a basic example:

private String generateCodeSuggestion(CompletionParameters parameters) {
    PsiElement element = parameters.getPosition();
    PsiFile file = element.getContainingFile();

    // Get the text up to the current caret position
    Document document = PsiDocumentManager.getInstance(element.getProject()).getDocument(file);
    if (document == null) return null;

    int offset = parameters.getOffset();
    String textUpToCaret = document.getText(new TextRange(0, offset));

    // Here you would typically send this context to an AI model or use other logic
    // to generate a suitable completion

    // For this example, let's just suggest a simple method
    return "public void exampleMethod() {\n    // TODO: Implement method\n}";
}
  1. Integrate with an AI Model (Optional)

For more advanced suggestions like those provided by GitHub Copilot, you'd need to integrate with an AI model. This could be done by sending the current code context to an API and receiving suggestions. Here's a conceptual example:

import com.devoxx.genie.service.AIModelService;

private String generateCodeSuggestion(CompletionParameters parameters) {
    // ... (previous code to get context)

    AIModelService aiService = AIModelService.getInstance(parameters.getProject());
    return aiService.generateSuggestion(textUpToCaret);
}
  1. Handle Streaming Suggestions (Optional)

For a more responsive experience, you might want to stream suggestions as they're generated:

public class DevoxxGenieCompletionContributor extends CompletionContributor {
    @Override
    public void fillCompletionVariants(@NotNull CompletionParameters parameters, @NotNull CompletionResultSet result) {
        CompletionResultSet resultSet = result.withPrefixMatcher(new PlainPrefixMatcher(""));
        CompletionResultSet finalResultSet = resultSet;

        ApplicationManager.getApplication().executeOnPooledThread(() -> {
            String partialSuggestion = "";
            while (!resultSet.isStopped()) {
                // Generate next part of the suggestion
                String nextPart = generateNextPartOfSuggestion();
                if (nextPart == null) break;

                partialSuggestion += nextPart;
                finalResultSet.addElement(LookupElementBuilder.create(partialSuggestion));
            }
        });
    }
}
  1. Respect IntelliJ's Coding Style

Ensure your suggestions respect the user's code style settings:

private String formatSuggestion(String suggestion, Project project) {
    CodeStyleManager codeStyleManager = CodeStyleManager.getInstance(project);
    PsiElementFactory factory = PsiElementFactory.getInstance(project);
    PsiJavaFile file = (PsiJavaFile) factory.createFileFromText("dummy.java", JavaFileType.INSTANCE, suggestion);
    codeStyleManager.reformat(file);
    return file.getText();
}

This approach provides a foundation for implementing Copilot-like code autocompletion in your DevoxxGenie plugin. Remember that advanced AI-powered suggestions would require integration with a sophisticated language model, which is a significant undertaking in itself. You might start with simpler heuristic-based suggestions and gradually enhance the capability as your plugin evolves.

stephanj commented 1 month ago

Rough Prototype 🤩 👇🏼

package com.devoxx.genie.service.completion;

import com.devoxx.genie.chatmodel.ollama.OllamaChatModelFactory;
import com.devoxx.genie.model.ChatModel;
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.project.Project;
import com.intellij.openapi.util.Computable;
import dev.langchain4j.data.message.AiMessage;
import dev.langchain4j.data.message.ChatMessage;
import dev.langchain4j.data.message.SystemMessage;
import dev.langchain4j.data.message.UserMessage;
import dev.langchain4j.model.chat.ChatLanguageModel;
import dev.langchain4j.model.output.Response;
import org.jetbrains.annotations.NotNull;

import java.util.ArrayList;
import java.util.List;

public class CodeCompletionService {

    private final ChatModel llamaChatModel;
    private final List<ChatMessage> messages = new ArrayList<>();

    public CodeCompletionService() {
        llamaChatModel = new ChatModel();
        llamaChatModel.setModelName("llama3.1:latest");
        llamaChatModel.setTemperature(0.7);
        llamaChatModel.setTopP(0.9);
        llamaChatModel.setMaxRetries(3);

        messages.add(new SystemMessage("You're an auto complete coding genius! " +
            "Only return the code completion so we can insert it where the developer stopped typing." +
            "Do not return any other messages.  Make sure the code completion is valid code."));
    }

    public void completeCode(Project project, Editor editor) {
        ApplicationManager.getApplication().executeOnPooledThread(() -> {
            String suggestion = ApplicationManager.getApplication().runReadAction(
                (Computable<String>) () -> generateCodeSuggestion(editor)
            );
            if (suggestion != null) {
                ApplicationManager.getApplication().invokeLater(() -> {
                    insertSuggestion(project, editor, suggestion);
                });
            }
        });
    }

    private String generateCodeSuggestion(@NotNull Editor editor) {
        Document document = editor.getDocument();
        int offset = editor.getCaretModel().getOffset();
        String textUpToCaret = document.getText().substring(0, offset);

        OllamaChatModelFactory ollamaChatModelFactory = new OllamaChatModelFactory();

        ChatMessage userMessage = new UserMessage("Complete this code. Don't repeat the full code, only return the rest of the code. :" + textUpToCaret);
        messages.add(userMessage);

        ChatLanguageModel chatModel = ollamaChatModelFactory.createChatModel(llamaChatModel);
        Response<AiMessage> generate = chatModel.generate(messages);

        System.out.println(">>Original code: " + textUpToCaret);
        System.out.println(">>Code suggestion: " + generate.content().text());

        return generate.content().text();
    }

    private void insertSuggestion(Project project, Editor editor, String suggestion) {
        WriteCommandAction.runWriteCommandAction(project, "AI Code Completion", null, () -> {
            int caretOffset = editor.getCaretModel().getOffset();
            editor.getDocument().insertString(caretOffset, suggestion);
            editor.getCaretModel().moveToOffset(caretOffset + suggestion.length());
        });
    }
}
package com.devoxx.genie.action;

import com.devoxx.genie.service.completion.CodeCompletionService;
import com.devoxx.genie.ui.settings.DevoxxGenieStateService;
import com.intellij.openapi.actionSystem.*;
import com.intellij.openapi.editor.Editor;
import com.intellij.openapi.project.DumbAwareAction;
import com.intellij.openapi.project.Project;
import com.intellij.psi.PsiFile;
import org.jetbrains.annotations.NotNull;

public class CodeCompleteAction extends DumbAwareAction {

    private final CodeCompletionService codeCompletionService;

    public CodeCompleteAction() {
        super("AI Code Complete");
        this.codeCompletionService = new CodeCompletionService();
    }

    @Override
    public void actionPerformed(@NotNull AnActionEvent e) {
        Project project = e.getProject();

        if (project != null) {
            Editor editor = e.getData(CommonDataKeys.EDITOR);

            if (editor != null) {
                if (editor.getCaretModel().getVisualPosition().column == 0) {
                    AnAction defaultTabAction = ActionManager.getInstance().getAction(IdeActions.ACTION_EDITOR_TAB);
                    defaultTabAction.actionPerformed(e);
                    return;
                }
                if (DevoxxGenieStateService.getInstance().getAutoCompleteCode()) {
                    codeCompletionService.completeCode(project, editor);
                } else {
                    // If AI completion is disabled, perform the default TAB action
                    AnAction defaultTabAction = ActionManager.getInstance().getAction(IdeActions.ACTION_EDITOR_TAB);
                    if (defaultTabAction != null) {
                        defaultTabAction.actionPerformed(e);
                    }
                }
            }
        }
    }

    @Override
    public void update(@NotNull AnActionEvent e) {
        Project project = e.getProject();
        Editor editor = e.getData(CommonDataKeys.EDITOR);
        e.getPresentation().setEnabledAndVisible(project != null && editor != null);
    }

    @Override
    public @NotNull ActionUpdateThread getActionUpdateThread() {
        return ActionUpdateThread.BGT;
    }

    @Override
    public boolean isDumbAware() {
        return true;
    }
}

Plugin config

<action id="com.devoxx.genie.action.CodeCompleteAction"
                class="com.devoxx.genie.action.CodeCompleteAction"
                text="AI Code Complete"
                description="Complete code using AI">
    <keyboard-shortcut first-keystroke="TAB" keymap="$default"/>
    <add-to-group group-id="EditorActions" anchor="last"/>
</action>
bengbengbalabalabeng commented 3 days ago

I would like to ask, is the above prototype example still unable to achieve better results? I don't see anything like that in the repository yet. @stephanj

stephanj commented 3 days ago

The prototype serves as a basic implementation to help you get started. The goal is to achieve functionality akin to CoPilot, where code completion is seamlessly inserted at the correct location, with only the suggested code block being displayed.