Inline Autocompletion Pt.2 (#333)

* Add first draft of inline code completion with mock text

* Adds InsertInlineTextAction for inserting autocomplete suggestion with tab

- Changed to disable suggestions when text is selected
- Adds and removes the insert action based on when it shows the inlay hint

* Request inline code completion

* Move inline completion prompt into txt file

* Add inline completion settings to ConfigurationState

* Fix code style

* Use EditorTrackerListener instead of EditorFactoryListener to enable inline completion

* Code completion requests synchronously without SSE

* Use LlamaClient.getInfill() for inline code completion

* support inlay block element rendering, clean up code

* Use only enclosed Method or Class contents for code completion if possible

* Refactor extracting PsiElement contents in code completion

* bump llm-client

* fix completion call from triggering on EDT, force method params to be nonnull by default

* refactor request building, decrease delay value

* Trigger code completion if cursor is not inside a word

* Improve inlay rendering

* Support cancellable infill requests

* add statusbar widget, disable completions by default

* Show error notification if code completion failed

* Truely disable/enable EditorInlayHandler when completion is turned off/on

* Add CodeCompletionEnabledListener Topic to control enabling/disabling code-completion

* Add progress indicator for code-completion with option to cancel

* Add CodeCompletionServiceTest + refactor inlay ElementRenderers

* several improvements

- replace timer implementation with call debouncing
- use OpenAI /v1/completions API for completions
- code refactoring

* trigger progress indicator only for llama completions

* fix tests

---------

Co-authored-by: James Higgins <james.isaac.higgins@gmail.com>
Co-authored-by: Carl-Robert Linnupuu <carlrobertoh@gmail.com>
This commit is contained in:
Phil 2024-01-31 00:05:31 +01:00 committed by GitHub
parent 390d8cdd5e
commit 7387cf4536
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
41 changed files with 1461 additions and 21 deletions

View file

@ -1,10 +1,17 @@
package ee.carlrobert.codegpt;
import com.intellij.openapi.editor.EditorCustomElementRenderer;
import com.intellij.openapi.editor.Inlay;
import com.intellij.openapi.util.Key;
import ee.carlrobert.embedding.ReferencedFile;
import java.util.List;
public class CodeGPTKeys {
public static final Key<List<ReferencedFile>> SELECTED_FILES = Key.create("selectedFiles");
public static final Key<Inlay<EditorCustomElementRenderer>> SINGLE_LINE_INLAY =
Key.create("codegpt.editor.inlay.single-line");
public static final Key<Inlay<EditorCustomElementRenderer>> MULTI_LINE_INLAY =
Key.create("codegpt.editor.inlay.multi-line");
public static final Key<List<ReferencedFile>> SELECTED_FILES =
Key.create("codegpt.selectedFiles");
}

View file

@ -48,7 +48,7 @@ public final class EncodingManager {
try {
return encoding.countTokens(text);
} catch (Exception ex) {
LOG.error(ex);
LOG.warn(ex);
return 0;
}
}

View file

@ -22,7 +22,6 @@ public class PluginStartupActivity implements StartupActivity {
@Override
public void runActivity(@NotNull Project project) {
EditorActionsUtil.refreshActions();
var authenticationResponse = YouUserManager.getInstance().getAuthenticationResponse();
if (authenticationResponse == null) {
handleYouServiceAuthentication();

View file

@ -0,0 +1,26 @@
package ee.carlrobert.codegpt.actions;
import com.intellij.util.messages.Topic;
import com.intellij.util.messages.Topic.BroadcastDirection;
import ee.carlrobert.codegpt.settings.configuration.ConfigurationState;
import java.util.EventListener;
/**
* {@link EventListener} for changes of {@link ConfigurationState#isCodeCompletionsEnabled()}.
*
* @see EnableCompletionsAction
* @see DisableCompletionsAction
*/
public interface CodeCompletionEnabledListener extends EventListener {
/**
* Topic for subscribing to {@link ConfigurationState#isCodeCompletionsEnabled()} changes.<br/>
* Broadcasts from Application-Level to all projects.
*/
@Topic.AppLevel
Topic<CodeCompletionEnabledListener> TOPIC = new Topic<>(CodeCompletionEnabledListener.class,
BroadcastDirection.TO_DIRECT_CHILDREN);
void onCodeCompletionsEnabledChange(boolean codeCompletionsEnabled);
}

View file

@ -0,0 +1,30 @@
package ee.carlrobert.codegpt.actions;
import com.intellij.openapi.actionSystem.AnAction;
import com.intellij.openapi.actionSystem.AnActionEvent;
import com.intellij.openapi.application.ApplicationManager;
import ee.carlrobert.codegpt.codecompletions.CodeGPTEditorManager;
import ee.carlrobert.codegpt.settings.configuration.ConfigurationState;
import org.jetbrains.annotations.NotNull;
/**
* Disables code-completion.<br/> Publishes message to {@link CodeCompletionEnabledListener#TOPIC}
*/
public class DisableCompletionsAction extends AnAction {
@Override
public void actionPerformed(@NotNull AnActionEvent e) {
ConfigurationState.getInstance().setCodeCompletionsEnabled(false);
CodeGPTEditorManager.getInstance().disposeAllInlays(e.getProject());
ApplicationManager.getApplication()
.getMessageBus().syncPublisher(CodeCompletionEnabledListener.TOPIC)
.onCodeCompletionsEnabledChange(false);
}
@Override
public void update(@NotNull AnActionEvent e) {
var codeCompletionEnabled = ConfigurationState.getInstance().isCodeCompletionsEnabled();
e.getPresentation().setEnabled(codeCompletionEnabled);
e.getPresentation().setVisible(codeCompletionEnabled);
}
}

View file

@ -0,0 +1,28 @@
package ee.carlrobert.codegpt.actions;
import com.intellij.openapi.actionSystem.AnAction;
import com.intellij.openapi.actionSystem.AnActionEvent;
import com.intellij.openapi.application.ApplicationManager;
import ee.carlrobert.codegpt.settings.configuration.ConfigurationState;
import org.jetbrains.annotations.NotNull;
/**
* Enables code-completion.<br/> Publishes message to {@link CodeCompletionEnabledListener#TOPIC}
*/
public class EnableCompletionsAction extends AnAction {
@Override
public void actionPerformed(@NotNull AnActionEvent e) {
ConfigurationState.getInstance().setCodeCompletionsEnabled(true);
ApplicationManager.getApplication()
.getMessageBus().syncPublisher(CodeCompletionEnabledListener.TOPIC)
.onCodeCompletionsEnabledChange(true);
}
@Override
public void update(@NotNull AnActionEvent e) {
var codeCompletionEnabled = ConfigurationState.getInstance().isCodeCompletionsEnabled();
e.getPresentation().setEnabled(!codeCompletionEnabled);
e.getPresentation().setVisible(!codeCompletionEnabled);
}
}

View file

@ -0,0 +1,23 @@
package ee.carlrobert.codegpt.actions;
import com.intellij.icons.AllIcons.General;
import com.intellij.openapi.actionSystem.AnAction;
import com.intellij.openapi.actionSystem.AnActionEvent;
import com.intellij.openapi.options.ShowSettingsUtil;
import ee.carlrobert.codegpt.CodeGPTBundle;
import ee.carlrobert.codegpt.settings.SettingsConfigurable;
import org.jetbrains.annotations.NotNull;
public class OpenSettingsAction extends AnAction {
public OpenSettingsAction() {
super(CodeGPTBundle.get("action.opensettings.title"),
CodeGPTBundle.get("action.opensettings.description"),
General.Settings);
}
@Override
public void actionPerformed(@NotNull AnActionEvent e) {
ShowSettingsUtil.getInstance().showSettingsDialog(e.getProject(), SettingsConfigurable.class);
}
}

View file

@ -0,0 +1,76 @@
package ee.carlrobert.codegpt.codecompletions;
import static ee.carlrobert.codegpt.settings.service.ServiceType.LLAMA_CPP;
import com.intellij.openapi.progress.impl.BackgroundableProcessIndicator;
import com.intellij.openapi.project.Project;
import ee.carlrobert.codegpt.CodeGPTBundle;
import ee.carlrobert.codegpt.settings.state.SettingsState;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicReference;
import okhttp3.sse.EventSource;
public class CallDebouncer {
private final Project project;
private final ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor();
private final ConcurrentHashMap<Object, Future<?>> delayedMap = new ConcurrentHashMap<>();
private final AtomicReference<EventSource> currentCall = new AtomicReference<>();
public CallDebouncer(Project project) {
this.project = project;
}
/**
* Implements a debounce mechanism for {@code callable} with a specified {@code delay}. This means
* the callable is set to execute after the given {@code delay} period. However, if this method is
* invoked again with the same key before the {@code delay} elapses, the scheduled execution will
* be aborted, and therefore the previous request will be cancelled.
*/
public void debounce(Object key, CallRunnable runnable, long delay, TimeUnit unit) {
Future<?> prev = delayedMap.put(key, scheduler.schedule(() -> {
try {
cancelPreviousCall();
var progressIndicator = LLAMA_CPP.equals(SettingsState.getInstance().getSelectedService())
? createProgressIndicator()
: null;
currentCall.set(runnable.call(progressIndicator));
} finally {
delayedMap.remove(key);
}
}, delay, unit));
if (prev != null) {
prev.cancel(true);
}
}
public void shutdown() {
cancelPreviousCall();
scheduler.shutdownNow();
}
public void cancelPreviousCall() {
var call = currentCall.get();
if (call != null) {
call.cancel();
}
}
private BackgroundableProcessIndicator createProgressIndicator() {
return new BackgroundableProcessIndicator(project,
CodeGPTBundle.get("codeCompletion.progress.title"), null, null, true) {
@Override
protected void onRunningChange() {
if (isCanceled()) {
cancelPreviousCall();
CodeGPTEditorManager.getInstance().disposeAllInlays(project);
}
}
};
}
}

View file

@ -0,0 +1,11 @@
package ee.carlrobert.codegpt.codecompletions;
import com.intellij.openapi.progress.impl.BackgroundableProcessIndicator;
import okhttp3.sse.EventSource;
import org.jetbrains.annotations.Nullable;
@FunctionalInterface
public interface CallRunnable {
EventSource call(@Nullable BackgroundableProcessIndicator progressIndicator);
}

View file

@ -0,0 +1,75 @@
package ee.carlrobert.codegpt.codecompletions;
import static java.util.Objects.requireNonNull;
import com.intellij.notification.NotificationType;
import com.intellij.notification.Notifications;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.editor.Editor;
import com.intellij.openapi.progress.impl.BackgroundableProcessIndicator;
import ee.carlrobert.codegpt.CodeGPTBundle;
import ee.carlrobert.codegpt.actions.OpenSettingsAction;
import ee.carlrobert.codegpt.ui.OverlayUtil;
import ee.carlrobert.llm.client.openai.completion.ErrorDetails;
import ee.carlrobert.llm.completion.CompletionEventListener;
import javax.annotation.ParametersAreNonnullByDefault;
import org.jetbrains.annotations.Nullable;
@ParametersAreNonnullByDefault
class CodeCompletionEventListener implements CompletionEventListener {
private static final Logger LOG = Logger.getInstance(CodeCompletionEventListener.class);
private final Editor editor;
private final int caretOffset;
private final BackgroundableProcessIndicator progressIndicator;
public CodeCompletionEventListener(
Editor editor,
int caretOffset,
@Nullable BackgroundableProcessIndicator progressIndicator) {
this.editor = editor;
this.caretOffset = caretOffset;
this.progressIndicator = progressIndicator;
}
@Override
public void onComplete(StringBuilder messageBuilder) {
if (progressIndicator != null) {
progressIndicator.processFinish();
}
var editorManager = CodeGPTEditorManager.getInstance();
editorManager.disposeEditorInlays(editor);
var inlayText = messageBuilder.toString();
if (!inlayText.isEmpty()) {
ApplicationManager.getApplication().invokeLater(() ->
CodeCompletionService.getInstance(requireNonNull(editor.getProject()))
.addInlays(editor, caretOffset, inlayText));
}
}
@Override
public void onError(ErrorDetails error, Throwable ex) {
LOG.error(error.getMessage(), ex);
if (progressIndicator != null) {
progressIndicator.processFinish();
}
Notifications.Bus.notify(OverlayUtil.getDefaultNotification(
String.format(
CodeGPTBundle.get("notification.completionError.description"),
ex.getMessage()),
NotificationType.ERROR)
.addAction(new OpenSettingsAction()), editor.getProject());
}
@Override
public void onCancelled(StringBuilder messageBuilder) {
LOG.info("Completion cancelled");
if (progressIndicator != null) {
progressIndicator.processFinish();
}
}
}

View file

@ -0,0 +1,120 @@
package ee.carlrobert.codegpt.codecompletions;
import com.intellij.openapi.Disposable;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.editor.Editor;
import com.intellij.openapi.editor.event.BulkAwareDocumentListener;
import com.intellij.openapi.editor.event.CaretEvent;
import com.intellij.openapi.editor.event.CaretListener;
import com.intellij.openapi.editor.event.DocumentEvent;
import com.intellij.openapi.editor.event.SelectionEvent;
import com.intellij.openapi.editor.event.SelectionListener;
import ee.carlrobert.codegpt.actions.CodeCompletionEnabledListener;
import ee.carlrobert.codegpt.settings.configuration.ConfigurationState;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
public class CodeCompletionListenerBinder implements Disposable {
private final Editor editor;
private @Nullable EditorDocumentListener documentListener;
private @Nullable EditorSelectionListener selectionListener;
private @Nullable EditorCaretListener caretListener;
public CodeCompletionListenerBinder(Editor editor) {
this.editor = editor;
if (ConfigurationState.getInstance().isCodeCompletionsEnabled()) {
addListeners();
}
ApplicationManager.getApplication()
.getMessageBus()
.connect()
.subscribe(
CodeCompletionEnabledListener.TOPIC,
(CodeCompletionEnabledListener) (completionsEnabled) -> {
if (completionsEnabled) {
addListeners();
if (editor.getProject() != null) {
CodeCompletionService.getInstance(editor.getProject())
.handleCompletions(editor, editor.getCaretModel().getOffset());
}
} else {
removeListeners();
}
});
}
private void addListeners() {
if (documentListener == null) {
documentListener = new EditorDocumentListener();
editor.getDocument().addDocumentListener(documentListener);
}
if (selectionListener == null) {
selectionListener = new EditorSelectionListener();
editor.getSelectionModel().addSelectionListener(selectionListener);
}
if (caretListener == null) {
caretListener = new EditorCaretListener();
editor.getCaretModel().addCaretListener(caretListener);
}
}
private void removeListeners() {
if (documentListener != null) {
editor.getDocument().removeDocumentListener(documentListener);
documentListener = null;
}
if (selectionListener != null) {
editor.getSelectionModel().removeSelectionListener(selectionListener);
selectionListener = null;
}
if (caretListener != null) {
editor.getCaretModel().removeCaretListener(caretListener);
caretListener = null;
}
}
@Override
public void dispose() {
removeListeners();
}
private class EditorSelectionListener implements SelectionListener {
@Override
public void selectionChanged(@NotNull SelectionEvent event) {
CodeGPTEditorManager.getInstance().disposeEditorInlays(editor);
}
}
private class EditorCaretListener implements CaretListener {
@Override
public void caretPositionChanged(@NotNull CaretEvent event) {
var project = editor.getProject();
if (event.getCaret() == null || project == null) {
return;
}
CodeGPTEditorManager.getInstance().disposeEditorInlays(editor);
CodeCompletionService.getInstance(project)
.handleCompletions(editor, event.getCaret().getOffset());
}
}
private class EditorDocumentListener implements BulkAwareDocumentListener {
@Override
public void documentChangedNonBulk(@NotNull DocumentEvent event) {
var project = editor.getProject();
if (project != null) {
CodeGPTEditorManager.getInstance().disposeEditorInlays(editor);
CodeCompletionService.getInstance(project)
.handleCompletions(editor, editor.getCaretModel().getOffset());
}
}
}
}

View file

@ -0,0 +1,38 @@
package ee.carlrobert.codegpt.codecompletions;
import ee.carlrobert.codegpt.settings.state.LlamaSettingsState;
import ee.carlrobert.llm.client.llama.completion.LlamaCompletionRequest;
import ee.carlrobert.llm.client.openai.completion.request.OpenAITextCompletionRequest;
import javax.annotation.ParametersAreNonnullByDefault;
@ParametersAreNonnullByDefault
public class CodeCompletionRequestProvider {
private static final int MAX_TOKENS = 256;
private final InfillRequestDetails details;
public CodeCompletionRequestProvider(InfillRequestDetails details) {
this.details = details;
}
public OpenAITextCompletionRequest buildOpenAIRequest() {
return new OpenAITextCompletionRequest.Builder(details.getPrefix())
.setSuffix(details.getSuffix())
.setStream(true)
.setMaxTokens(MAX_TOKENS)
.setTemperature(0.1)
.build();
}
public LlamaCompletionRequest buildLlamaRequest() {
var promptTemplate = LlamaSettingsState.getInstance().getInfillPromptTemplate();
var prompt = promptTemplate.buildPrompt(details.getPrefix(), details.getSuffix());
return new LlamaCompletionRequest.Builder(prompt)
.setN_predict(MAX_TOKENS)
.setStream(true)
.setTemperature(0.1)
.setStop(promptTemplate.getStopTokens())
.build();
}
}

View file

@ -0,0 +1,265 @@
package ee.carlrobert.codegpt.codecompletions;
import static com.intellij.openapi.components.Service.Level.PROJECT;
import static ee.carlrobert.codegpt.CodeGPTKeys.MULTI_LINE_INLAY;
import static ee.carlrobert.codegpt.CodeGPTKeys.SINGLE_LINE_INLAY;
import static java.util.Objects.requireNonNull;
import static java.util.stream.Collectors.toList;
import com.intellij.openapi.Disposable;
import com.intellij.openapi.actionSystem.ActionManager;
import com.intellij.openapi.actionSystem.AnAction;
import com.intellij.openapi.actionSystem.AnActionEvent;
import com.intellij.openapi.actionSystem.KeyboardShortcut;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.application.ReadAction;
import com.intellij.openapi.command.WriteCommandAction;
import com.intellij.openapi.components.Service;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.editor.Document;
import com.intellij.openapi.editor.Editor;
import com.intellij.openapi.editor.EditorCustomElementRenderer;
import com.intellij.openapi.editor.Inlay;
import com.intellij.openapi.editor.InlayModel;
import com.intellij.openapi.keymap.KeymapManager;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.util.TextRange;
import com.intellij.psi.PsiClass;
import com.intellij.psi.PsiDocumentManager;
import com.intellij.psi.PsiElement;
import com.intellij.psi.PsiFile;
import com.intellij.psi.PsiMethod;
import com.intellij.psi.PsiWhiteSpace;
import com.intellij.util.concurrency.annotations.RequiresBackgroundThread;
import com.intellij.util.concurrency.annotations.RequiresEdt;
import com.intellij.util.concurrency.annotations.RequiresReadLock;
import com.intellij.util.concurrency.annotations.RequiresWriteLock;
import ee.carlrobert.codegpt.actions.CodeCompletionEnabledListener;
import ee.carlrobert.codegpt.completions.CompletionRequestService;
import ee.carlrobert.codegpt.settings.configuration.ConfigurationState;
import ee.carlrobert.codegpt.util.EditorUtil;
import ee.carlrobert.llm.completion.CompletionEventListener;
import java.awt.event.KeyEvent;
import java.util.List;
import java.util.Optional;
import java.util.concurrent.TimeUnit;
import javax.annotation.ParametersAreNonnullByDefault;
import javax.swing.KeyStroke;
import okhttp3.sse.EventSource;
import org.jetbrains.annotations.NotNull;
@Service(PROJECT)
@ParametersAreNonnullByDefault
public final class CodeCompletionService implements Disposable {
public static final String APPLY_INLAY_ACTION_ID = "ApplyInlayAction";
public static final int MAX_OFFSET = 4000;
private static final Logger LOG = Logger.getInstance(CodeCompletionService.class);
private final CallDebouncer callDebouncer;
private CodeCompletionService(Project project) {
this.callDebouncer = new CallDebouncer(project);
ApplicationManager.getApplication()
.getMessageBus()
.connect()
.subscribe(
CodeCompletionEnabledListener.TOPIC,
(CodeCompletionEnabledListener) (completionsEnabled) -> {
if (!completionsEnabled) {
callDebouncer.cancelPreviousCall();
}
});
}
public static CodeCompletionService getInstance(Project project) {
return project.getService(CodeCompletionService.class);
}
public boolean isCompletionAllowed(PsiElement elementAtCaret) {
return elementAtCaret instanceof PsiWhiteSpace;
}
public void handleCompletions(Editor editor, int offset) {
Project project = editor.getProject();
if (project == null
|| project.isDisposed()
|| !ConfigurationState.getInstance().isCodeCompletionsEnabled()
|| !EditorUtil.isSelectedEditor(editor)
|| editor.isViewer()
|| editor.isOneLineMode()
) {
return;
}
var document = editor.getDocument();
PsiFile psiFile = PsiDocumentManager.getInstance(project).getPsiFile(document);
if (psiFile == null) {
return;
}
PsiElement elementAtCaret = ReadAction.compute(() -> psiFile.findElementAt(offset));
var completionService = CodeCompletionService.getInstance(project);
if (!completionService.isCompletionAllowed(elementAtCaret)) {
return;
}
callDebouncer.debounce(
Void.class,
(progressIndicator) -> completionService.fetchCodeCompletion(
elementAtCaret,
offset,
document,
new CodeCompletionEventListener(editor, offset, progressIndicator)),
500,
TimeUnit.MILLISECONDS);
}
/**
* Fetches code-completion (FIM) for the given position ({@code offsetInFile}) in the file. <br/>
* By default tries to find an enclosing {@link PsiMethod} or {@link PsiClass} for the given
* {@code offsetInFile} and only uses their content instead of the entire file's content. If no
* such enclosing {@link PsiElement} can be found, the file's entire content is used instead.
*
* @param elementAtCaret PsiElement at caret
* @param offsetInFile Global offset in the file.
* @param document If the offset is not enclosed in a {@link PsiMethod} nor a
* {@link PsiClass}, the entire file content is used for completion.
* @return Completion String
*/
@RequiresBackgroundThread
public EventSource fetchCodeCompletion(
PsiElement elementAtCaret,
int offsetInFile,
Document document,
CompletionEventListener eventListener) {
InfillRequestDetails requestDetails = tryFindEnclosingPsiElementTextRange(
List.of(PsiMethod.class, PsiClass.class), elementAtCaret)
.map(textRange -> createInfillRequest(
document,
offsetInFile,
textRange.getStartOffset(),
textRange.getEndOffset())
)
.orElse(createInfillRequest(document, offsetInFile));
return CompletionRequestService.getInstance()
.getCodeCompletionAsync(requestDetails, eventListener);
}
@RequiresEdt
public void addInlays(Editor editor, int caretOffset, String inlayText) {
List<String> linesList = inlayText.lines().collect(toList());
String firstLine = linesList.get(0);
String restOfLines = linesList.size() > 1
? String.join("\n", linesList.subList(1, linesList.size()))
: null;
InlayModel inlayModel = editor.getInlayModel();
if (!firstLine.isEmpty()) {
editor.putUserData(SINGLE_LINE_INLAY, inlayModel.addInlineElement(
caretOffset,
true,
Integer.MAX_VALUE,
new InlayInlineElementRenderer(firstLine)));
}
if (restOfLines != null && !restOfLines.isEmpty()) {
editor.putUserData(MULTI_LINE_INLAY, inlayModel.addBlockElement(
caretOffset,
true,
false,
Integer.MAX_VALUE,
new InlayBlockElementRenderer(restOfLines)));
}
registerApplyCompletionAction(() -> WriteCommandAction.runWriteCommandAction(
editor.getProject(),
() -> applyCompletion(editor, inlayText)));
}
@RequiresWriteLock
private void applyCompletion(Editor editor, String text) {
if (editor.isDisposed()) {
LOG.warn("Editor is already disposed");
return;
}
var inlayKeys = List.of(SINGLE_LINE_INLAY, MULTI_LINE_INLAY);
for (var key : inlayKeys) {
Inlay<EditorCustomElementRenderer> inlay = editor.getUserData(key);
if (inlay != null) {
applyCompletion(editor, text, inlay.getOffset());
CodeGPTEditorManager.getInstance().disposeEditorInlays(editor);
return;
}
}
}
@RequiresWriteLock
private void applyCompletion(Editor editor, String text, int offset) {
Document document = editor.getDocument();
document.insertString(offset, text);
editor.getCaretModel().moveToOffset(offset + text.length());
EditorUtil.reformatDocument(
requireNonNull(editor.getProject()),
document,
offset,
offset + text.length());
}
@RequiresReadLock
private Optional<TextRange> tryFindEnclosingPsiElementTextRange(
List<Class<? extends PsiElement>> types,
PsiElement elementAtCaret) {
return ReadAction.compute(() -> {
var element = elementAtCaret;
while (element != null) {
for (Class<? extends PsiElement> type : types) {
if (type.isInstance(element)) {
return Optional.of(element.getTextRange());
}
}
element = element.getParent();
}
return Optional.empty();
});
}
@Override
public void dispose() {
callDebouncer.shutdown();
}
private void registerApplyCompletionAction(Runnable onApply) {
var actionManager = ActionManager.getInstance();
actionManager.registerAction(
APPLY_INLAY_ACTION_ID,
new AnAction() {
@Override
public void actionPerformed(@NotNull AnActionEvent e) {
onApply.run();
}
});
KeymapManager.getInstance().getActiveKeymap().addShortcut(
APPLY_INLAY_ACTION_ID,
new KeyboardShortcut(KeyStroke.getKeyStroke(KeyEvent.VK_TAB, 0), null));
}
private static InfillRequestDetails createInfillRequest(Document document, int offsetInFile) {
int begin = Integer.max(0, offsetInFile - MAX_OFFSET);
int end = Integer.min(document.getTextLength(), offsetInFile + MAX_OFFSET);
return createInfillRequest(document, offsetInFile, begin, end);
}
private static InfillRequestDetails createInfillRequest(
Document document,
int caretOffset,
int start,
int end) {
return new InfillRequestDetails(
document.getText(new TextRange(start, caretOffset)),
document.getText(new TextRange(caretOffset, end)));
}
}

View file

@ -0,0 +1,16 @@
package ee.carlrobert.codegpt.codecompletions;
import com.intellij.openapi.editor.Editor;
import com.intellij.openapi.editor.event.EditorFactoryEvent;
import com.intellij.openapi.editor.event.EditorFactoryListener;
import com.intellij.openapi.editor.ex.util.EditorUtil;
import org.jetbrains.annotations.NotNull;
public class CodeGPTEditorListener implements EditorFactoryListener {
@Override
public void editorCreated(@NotNull EditorFactoryEvent event) {
Editor editor = event.getEditor();
EditorUtil.disposeWithEditor(editor, new CodeCompletionListenerBinder(editor));
}
}

View file

@ -0,0 +1,51 @@
package ee.carlrobert.codegpt.codecompletions;
import static ee.carlrobert.codegpt.CodeGPTKeys.MULTI_LINE_INLAY;
import static ee.carlrobert.codegpt.CodeGPTKeys.SINGLE_LINE_INLAY;
import com.intellij.openapi.actionSystem.ActionManager;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.command.WriteCommandAction;
import com.intellij.openapi.components.Service;
import com.intellij.openapi.editor.Editor;
import com.intellij.openapi.editor.EditorCustomElementRenderer;
import com.intellij.openapi.editor.Inlay;
import com.intellij.openapi.fileEditor.FileEditor;
import com.intellij.openapi.fileEditor.FileEditorManager;
import com.intellij.openapi.fileEditor.TextEditor;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.util.Key;
@Service
public final class CodeGPTEditorManager {
private CodeGPTEditorManager() {
}
public static CodeGPTEditorManager getInstance() {
return ApplicationManager.getApplication().getService(CodeGPTEditorManager.class);
}
public void disposeAllInlays(Project project) {
var allFileEditors = FileEditorManager.getInstance(project).getAllEditors();
for (FileEditor fileEditor : allFileEditors) {
if (fileEditor instanceof TextEditor) {
disposeEditorInlays(((TextEditor) fileEditor).getEditor());
}
}
}
public void disposeEditorInlays(Editor editor) {
ActionManager.getInstance().unregisterAction(CodeCompletionService.APPLY_INLAY_ACTION_ID);
disposeInlay(editor, SINGLE_LINE_INLAY);
disposeInlay(editor, MULTI_LINE_INLAY);
}
private void disposeInlay(Editor editor, Key<Inlay<EditorCustomElementRenderer>> inlayKey) {
Inlay<EditorCustomElementRenderer> inlay = editor.getUserData(inlayKey);
if (inlay != null) {
WriteCommandAction.runWriteCommandAction(editor.getProject(), inlay::dispose);
editor.putUserData(inlayKey, null);
}
}
}

View file

@ -0,0 +1,57 @@
package ee.carlrobert.codegpt.codecompletions;
import static java.lang.String.format;
import java.util.List;
import org.jetbrains.annotations.Nullable;
public enum InfillPromptTemplate {
OPENAI("OpenAI") {
@Override
public String buildPrompt(String prefix, String suffix) {
return format("<|fim_prefix|> %s <|fim_suffix|>%s <|fim_middle|>", prefix, suffix);
}
},
LLAMA("Llama", List.of("<EOT>")) {
@Override
public String buildPrompt(String prefix, String suffix) {
return format("<PRE> %s <SUF>%s <MID>", prefix, suffix);
}
},
STABILITY("Stability AI", List.of("<|endoftext|>")) {
@Override
public String buildPrompt(String prefix, String suffix) {
return format("<fim_prefix>%s<fim_suffix>%s<fim_middle>", prefix, suffix);
}
},
DEEPSEEK_CODER("DeepSeek Coder") {
@Override
public String buildPrompt(String prefix, String suffix) {
return format("<|fim_begin|>%s<|fim_hole|>%s<|fim_end|>", prefix, suffix);
}
};
private final String label;
private final @Nullable List<String> stopTokens;
InfillPromptTemplate(String label) {
this(label, null);
}
InfillPromptTemplate(String label, @Nullable List<String> stopTokens) {
this.label = label;
this.stopTokens = stopTokens;
}
public abstract String buildPrompt(String prefix, String suffix);
@Override
public String toString() {
return label;
}
public List<String> getStopTokens() {
return stopTokens;
}
}

View file

@ -0,0 +1,20 @@
package ee.carlrobert.codegpt.codecompletions;
public class InfillRequestDetails {
private final String prefix;
private final String suffix;
public InfillRequestDetails(String prefix, String suffix) {
this.prefix = prefix;
this.suffix = suffix;
}
public String getPrefix() {
return prefix;
}
public String getSuffix() {
return suffix;
}
}

View file

@ -0,0 +1,57 @@
package ee.carlrobert.codegpt.codecompletions;
import static java.util.stream.Collectors.toList;
import com.intellij.openapi.editor.Editor;
import com.intellij.openapi.editor.EditorCustomElementRenderer;
import com.intellij.openapi.editor.Inlay;
import com.intellij.openapi.editor.colors.EditorFontType;
import com.intellij.openapi.editor.impl.FontInfo;
import com.intellij.openapi.editor.markup.TextAttributes;
import com.intellij.ui.JBColor;
import java.awt.Font;
import java.awt.Graphics2D;
import java.awt.font.FontRenderContext;
import java.awt.geom.Rectangle2D;
import java.util.Comparator;
import java.util.List;
import org.jetbrains.annotations.NotNull;
public class InlayBlockElementRenderer extends InlayElementRenderer {
protected InlayBlockElementRenderer(String inlayText) {
super(inlayText);
}
@Override
public void paint(
@NotNull Inlay inlay,
@NotNull Graphics2D g,
@NotNull Rectangle2D targetRegion,
@NotNull TextAttributes textAttributes) {
Editor editor = inlay.getEditor();
Font font = editor.getColorsScheme()
.getFont(EditorFontType.PLAIN)
.deriveFont(Font.ITALIC);
g.setFont(font);
g.setColor(JBColor.GRAY);
int x = (int) targetRegion.getX();
int fontBaseLineOffset = (int) calculateFontBaseLineOffset(font, editor);
List<String> lines = inlayText.lines().collect(toList());
for (int i = 0; i < lines.size(); i++) {
String line = lines.get(i);
int y = (int) targetRegion.getY() + fontBaseLineOffset + i * editor.getLineHeight();
g.drawString(line, x, y);
}
}
public double calculateFontBaseLineOffset(Font font, Editor editor) {
FontRenderContext fontRenderContext =
FontInfo.getFontRenderContext(editor.getContentComponent());
Rectangle2D visualBounds = font.createGlyphVector(fontRenderContext, "Abc").getVisualBounds();
double fontBaseline = visualBounds.getHeight();
double linePadding = (editor.getLineHeight() - fontBaseline) / 2;
return Math.ceil(fontBaseline + linePadding);
}
}

View file

@ -0,0 +1,38 @@
package ee.carlrobert.codegpt.codecompletions;
import com.intellij.openapi.editor.EditorCustomElementRenderer;
import com.intellij.openapi.editor.Inlay;
import com.intellij.openapi.editor.colors.EditorFontType;
import java.util.Comparator;
import org.jetbrains.annotations.NotNull;
public abstract class InlayElementRenderer implements EditorCustomElementRenderer {
protected final String inlayText;
protected InlayElementRenderer(String inlayText) {
this.inlayText = inlayText;
}
@Override
public int calcWidthInPixels(@NotNull Inlay inlay) {
var longestLine = getInlayText().lines()
.max(Comparator.comparingInt(String::length))
.orElse("");
var editor = inlay.getEditor();
return editor.getContentComponent()
.getFontMetrics(editor.getColorsScheme().getFont(EditorFontType.PLAIN))
.stringWidth(longestLine);
}
@Override
public int calcHeightInPixels(@NotNull Inlay inlay) {
int lineHeight = inlay.getEditor().getLineHeight();
int linesCount = (int) inlayText.lines().count();
return lineHeight * linesCount;
}
public String getInlayText() {
return inlayText;
}
}

View file

@ -0,0 +1,37 @@
package ee.carlrobert.codegpt.codecompletions;
import com.intellij.openapi.editor.Editor;
import com.intellij.openapi.editor.EditorCustomElementRenderer;
import com.intellij.openapi.editor.Inlay;
import com.intellij.openapi.editor.colors.EditorFontType;
import com.intellij.openapi.editor.markup.TextAttributes;
import com.intellij.ui.JBColor;
import java.awt.Font;
import java.awt.Graphics2D;
import java.awt.geom.Rectangle2D;
import org.jetbrains.annotations.NotNull;
public class InlayInlineElementRenderer extends InlayElementRenderer {
protected InlayInlineElementRenderer(String inlayText) {
super(inlayText);
}
@Override
public void paint(
@NotNull Inlay inlay,
@NotNull Graphics2D g,
@NotNull Rectangle2D targetRegion,
@NotNull TextAttributes textAttributes) {
Editor editor = inlay.getEditor();
Font font = editor.getColorsScheme()
.getFont(EditorFontType.PLAIN)
.deriveFont(Font.ITALIC);
g.setFont(font);
g.setColor(JBColor.GRAY);
g.drawString(
inlayText,
(int) targetRegion.getX(),
(int) targetRegion.getY() + editor.getAscent());
}
}

View file

@ -109,6 +109,11 @@ public class CompletionRequestHandler {
completionResponseEventListener.handleCompleted(messageBuilder.toString(), callParameters);
}
@Override
public void onCancelled(StringBuilder messageBuilder) {
completionResponseEventListener.handleCompleted(messageBuilder.toString(), callParameters);
}
@Override
public void onError(ErrorDetails error, Throwable ex) {
try {

View file

@ -27,7 +27,6 @@ import ee.carlrobert.embedding.EmbeddingsService;
import ee.carlrobert.embedding.ReferencedFile;
import ee.carlrobert.llm.client.llama.completion.LlamaCompletionRequest;
import ee.carlrobert.llm.client.openai.completion.OpenAIChatCompletionModel;
import ee.carlrobert.llm.client.openai.completion.OpenAICompletionRequest;
import ee.carlrobert.llm.client.openai.completion.request.OpenAIChatCompletionMessage;
import ee.carlrobert.llm.client.openai.completion.request.OpenAIChatCompletionRequest;
import ee.carlrobert.llm.client.you.completion.YouCompletionRequest;
@ -52,6 +51,9 @@ public class CompletionRequestProvider {
public static final String FIX_COMPILE_ERRORS_SYSTEM_PROMPT = getResourceContent(
"/prompts/fix-compile-errors.txt");
public static final String INLINE_COMPLETION_PROMPT = getResourceContent(
"/prompts/inline-completion-prompt.txt");
private final EncodingManager encodingManager = EncodingManager.getInstance();
private final EmbeddingsService embeddingsService;
private final Conversation conversation;
@ -80,7 +82,7 @@ public class CompletionRequestProvider {
.replace("{QUESTION}", userPrompt);
}
public static OpenAICompletionRequest buildOpenAILookupCompletionRequest(
public static OpenAIChatCompletionRequest buildOpenAILookupCompletionRequest(
String context) {
return new OpenAIChatCompletionRequest.Builder(
List.of(
@ -147,17 +149,19 @@ public class CompletionRequestProvider {
CallParameters callParameters,
boolean useContextualSearch,
@Nullable String overriddenPath) {
var builder = new OpenAIChatCompletionRequest.Builder(
buildMessages(model, callParameters, useContextualSearch))
.setModel(model)
.setMaxTokens(ConfigurationState.getInstance().getMaxTokens())
.setStream(true)
.setTemperature(ConfigurationState.getInstance().getTemperature());
if (overriddenPath != null) {
builder.setOverriddenPath(overriddenPath);
}
return (OpenAIChatCompletionRequest) builder.build();
return builder.build();
}
public List<OpenAIChatCompletionMessage> buildMessages(

View file

@ -1,15 +1,17 @@
package ee.carlrobert.codegpt.completions;
import static ee.carlrobert.codegpt.settings.service.ServiceType.AZURE;
import static ee.carlrobert.codegpt.settings.service.ServiceType.LLAMA_CPP;
import static ee.carlrobert.codegpt.settings.service.ServiceType.OPENAI;
import static ee.carlrobert.codegpt.settings.service.ServiceType.YOU;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.components.Service;
import ee.carlrobert.codegpt.codecompletions.CodeCompletionRequestProvider;
import ee.carlrobert.codegpt.codecompletions.InfillRequestDetails;
import ee.carlrobert.codegpt.credentials.AzureCredentialsManager;
import ee.carlrobert.codegpt.credentials.OpenAICredentialsManager;
import ee.carlrobert.codegpt.settings.configuration.ConfigurationState;
import ee.carlrobert.codegpt.settings.service.ServiceType;
import ee.carlrobert.codegpt.settings.state.AzureSettingsState;
import ee.carlrobert.codegpt.settings.state.OpenAISettingsState;
import ee.carlrobert.codegpt.settings.state.SettingsState;
@ -69,6 +71,22 @@ public final class CompletionRequestService {
}
}
public EventSource getCodeCompletionAsync(
InfillRequestDetails requestDetails,
CompletionEventListener eventListener) {
var requestProvider = new CodeCompletionRequestProvider(requestDetails);
switch (SettingsState.getInstance().getSelectedService()) {
case OPENAI:
return CompletionClientProvider.getOpenAIClient()
.getCompletionAsync(requestProvider.buildOpenAIRequest(), eventListener);
case LLAMA_CPP:
return CompletionClientProvider.getLlamaClient()
.getChatCompletionAsync(requestProvider.buildLlamaRequest(), eventListener);
default:
throw new IllegalArgumentException("Code completion not supported for selected service");
}
}
public void generateCommitMessageAsync(
String prompt,
CompletionEventListener eventListener) {
@ -79,10 +97,10 @@ public final class CompletionRequestService {
.setModel(OpenAISettingsState.getInstance().getModel())
.build();
var selectedService = SettingsState.getInstance().getSelectedService();
if (selectedService == ServiceType.OPENAI) {
if (selectedService == OPENAI) {
CompletionClientProvider.getOpenAIClient().getChatCompletionAsync(request, eventListener);
}
if (selectedService == ServiceType.AZURE) {
if (selectedService == AZURE) {
CompletionClientProvider.getAzureClient().getChatCompletionAsync(request, eventListener);
}
}
@ -106,10 +124,10 @@ public final class CompletionRequestService {
public boolean isRequestAllowed() {
var selectedService = SettingsState.getInstance().getSelectedService();
if (selectedService == ServiceType.AZURE) {
if (selectedService == AZURE) {
return AzureCredentialsManager.getInstance().isCredentialSet();
}
if (selectedService == ServiceType.OPENAI) {
if (selectedService == OPENAI) {
return OpenAICredentialsManager.getInstance().isApiKeySet();
}
return true;

View file

@ -6,4 +6,5 @@ public enum ConversationType {
EDITOR_ACTION,
FIX_COMPILE_ERRORS,
MULTI_FILE,
INLINE_COMPLETION,
}

View file

@ -1,4 +1,5 @@
package ee.carlrobert.codegpt.completions;
class TotalUsageExceededException extends RuntimeException {
}

View file

@ -48,8 +48,10 @@ public class ConfigurationComponent {
private final JBCheckBox autoFormattingCheckBox;
private final JTextArea systemPromptTextArea;
private final JTextArea commitMessagePromptTextArea;
private final JTextArea inlineCompletionPromptTextArea;
private final IntegerField maxTokensField;
private final JBTextField temperatureField;
private final JBTextField inlineDelayField;
public ConfigurationComponent(Disposable parentDisposable, ConfigurationState configuration) {
table = new JBTable(new DefaultTableModel(
@ -68,7 +70,8 @@ public class ConfigurationComponent {
temperatureField = new JBTextField(12);
temperatureField.setText(String.valueOf(configuration.getTemperature()));
var temperatureFieldValidator = createInputValidator(parentDisposable, temperatureField);
var temperatureFieldValidator = createTemperatureInputValidator(parentDisposable,
temperatureField);
temperatureField.getDocument().addDocumentListener(new DocumentListener() {
@Override
public void insertUpdate(DocumentEvent e) {
@ -106,6 +109,33 @@ public class ConfigurationComponent {
commitMessagePromptTextArea.setLineWrap(true);
commitMessagePromptTextArea.setBorder(JBUI.Borders.empty(8, 4));
inlineCompletionPromptTextArea = new JTextArea(configuration.getInlineCompletionPrompt(), 3,
60);
inlineCompletionPromptTextArea.setLineWrap(true);
inlineCompletionPromptTextArea.setBorder(JBUI.Borders.empty(8, 4));
inlineDelayField = new JBTextField(12);
inlineDelayField.setText(String.valueOf(configuration.getTemperature()));
var inlineDelayFieldValidator = createInlineDelayInputValidator(parentDisposable,
inlineDelayField);
inlineDelayField.getDocument().addDocumentListener(new DocumentListener() {
@Override
public void insertUpdate(DocumentEvent e) {
inlineDelayFieldValidator.revalidate();
}
@Override
public void removeUpdate(DocumentEvent e) {
inlineDelayFieldValidator.revalidate();
}
@Override
public void changedUpdate(DocumentEvent e) {
inlineDelayFieldValidator.revalidate();
}
});
checkForPluginUpdatesCheckBox = new JBCheckBox(
CodeGPTBundle.get("configurationConfigurable.checkForPluginUpdates.label"),
configuration.isCheckForPluginUpdates());
@ -135,6 +165,10 @@ public class ConfigurationComponent {
CodeGPTBundle.get("configurationConfigurable.section.commitMessage.title")))
.addComponent(createCommitMessageConfigurationForm())
.addComponentFillVertically(new JPanel(), 0)
.addComponent(new TitledSeparator(
CodeGPTBundle.get("configurationConfigurable.section.inlineCompletion.title")))
.addComponent(createInlineCompletionConfigurationForm())
.addComponentFillVertically(new JPanel(), 0)
.getPanel();
}
@ -227,7 +261,28 @@ public class ConfigurationComponent {
return form;
}
private ComponentValidator createInputValidator(
private JPanel createInlineCompletionConfigurationForm() {
var formBuilder = FormBuilder.createFormBuilder();
addAssistantFormLabeledComponent(
formBuilder,
"configurationConfigurable.section.inlineCompletion.systemPromptField.label",
"configurationConfigurable.section.inlineCompletion.systemPromptField.comment",
JBUI.Panels
.simplePanel(inlineCompletionPromptTextArea)
.withBorder(JBUI.Borders.customLine(
JBUI.CurrentTheme.CustomFrameDecorations.separatorForeground())));
formBuilder.addVerticalGap(8);
addAssistantFormLabeledComponent(
formBuilder,
"configurationConfigurable.section.inlineCompletion.delay.label",
"configurationConfigurable.section.inlineCompletion.delay.comment",
inlineDelayField);
var form = formBuilder.getPanel();
form.setBorder(JBUI.Borders.emptyLeft(16));
return form;
}
private ComponentValidator createTemperatureInputValidator(
Disposable parentDisposable,
JBTextField component) {
var validator = new ComponentValidator(parentDisposable)
@ -254,6 +309,33 @@ public class ConfigurationComponent {
return validator;
}
private ComponentValidator createInlineDelayInputValidator(
Disposable parentDisposable,
JBTextField component) {
var validator = new ComponentValidator(parentDisposable)
.withValidator(() -> {
var valueText = component.getText();
try {
var value = Integer.parseInt(valueText);
if (value <= 0) {
return new ValidationInfo(
CodeGPTBundle.get("validation.error.mustBeGreaterThanZero"),
component);
}
} catch (NumberFormatException e) {
return new ValidationInfo(
CodeGPTBundle.get("validation.error.mustBeNumber"),
component);
}
return null;
})
.andStartOnFocusLost()
.installOn(component);
validator.enableValidation();
return validator;
}
private DefaultTableModel getModel() {
return (DefaultTableModel) table.getModel();
}
@ -280,6 +362,23 @@ public class ConfigurationComponent {
return commitMessagePromptTextArea.getText();
}
public void setInlineCompletionPrompt(String inlineCompletionPrompt) {
inlineCompletionPromptTextArea.setText(inlineCompletionPrompt);
}
public String getInlineCompletionPrompt() {
return inlineCompletionPromptTextArea.getText();
}
public int getInlineDelay() {
return Integer.parseInt(inlineDelayField.getText());
}
public void setInlineDelay(int inlineDelay) {
inlineDelayField.setText(String.valueOf(inlineDelay));
}
public double getTemperature() {
return Double.parseDouble(temperatureField.getText());
}

View file

@ -36,9 +36,12 @@ public class ConfigurationConfigurable implements Configurable {
return !configurationComponent.getTableData().equals(configuration.getTableData())
|| configurationComponent.getMaxTokens() != configuration.getMaxTokens()
|| configurationComponent.getTemperature() != configuration.getTemperature()
|| configurationComponent.getInlineDelay() != configuration.getInlineDelay()
|| !configurationComponent.getSystemPrompt().equals(configuration.getSystemPrompt())
|| !configurationComponent.getCommitMessagePrompt()
.equals(configuration.getCommitMessagePrompt())
|| !configurationComponent.getInlineCompletionPrompt()
.equals(configuration.getInlineCompletionPrompt())
|| configurationComponent.isCheckForPluginUpdates()
!= configuration.isCheckForPluginUpdates()
|| configurationComponent.isCreateNewChatOnEachAction()
@ -57,6 +60,8 @@ public class ConfigurationConfigurable implements Configurable {
configuration.setTemperature(configurationComponent.getTemperature());
configuration.setSystemPrompt(configurationComponent.getSystemPrompt());
configuration.setCommitMessagePrompt(configurationComponent.getCommitMessagePrompt());
configuration.setInlineCompletionPrompt(configurationComponent.getInlineCompletionPrompt());
configuration.setInlineDelay(configurationComponent.getInlineDelay());
configuration.setCheckForPluginUpdates(configurationComponent.isCheckForPluginUpdates());
configuration.setCreateNewChatOnEachAction(
configurationComponent.isCreateNewChatOnEachAction());
@ -74,6 +79,8 @@ public class ConfigurationConfigurable implements Configurable {
configurationComponent.setTemperature(configuration.getTemperature());
configurationComponent.setSystemPrompt(configuration.getSystemPrompt());
configurationComponent.setCommitMessagePrompt(configuration.getCommitMessagePrompt());
configurationComponent.setInlineCompletionPrompt(configuration.getInlineCompletionPrompt());
configurationComponent.setInlineDelay(configuration.getInlineDelay());
configurationComponent.setCheckForPluginUpdates(configuration.isCheckForPluginUpdates());
configurationComponent.setCreateNewChatOnEachAction(
configuration.isCreateNewChatOnEachAction());

View file

@ -2,6 +2,7 @@ package ee.carlrobert.codegpt.settings.configuration;
import static ee.carlrobert.codegpt.completions.CompletionRequestProvider.COMPLETION_SYSTEM_PROMPT;
import static ee.carlrobert.codegpt.completions.CompletionRequestProvider.GENERATE_COMMIT_MESSAGE_SYSTEM_PROMPT;
import static ee.carlrobert.codegpt.completions.CompletionRequestProvider.INLINE_COMPLETION_PROMPT;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.components.PersistentStateComponent;
@ -20,14 +21,17 @@ public class ConfigurationState implements PersistentStateComponent<Configuratio
private String systemPrompt = COMPLETION_SYSTEM_PROMPT;
private String commitMessagePrompt = GENERATE_COMMIT_MESSAGE_SYSTEM_PROMPT;
private String inlineCompletionPrompt = INLINE_COMPLETION_PROMPT;
private int maxTokens = 1000;
private double temperature = 0.1;
private int inlineDelay = 500;
private boolean checkForPluginUpdates = true;
private boolean createNewChatOnEachAction;
private boolean ignoreGitCommitTokenLimit;
private boolean methodNameGenerationEnabled = true;
private boolean captureCompileErrors = true;
private boolean autoFormattingEnabled = true;
private boolean codeCompletionsEnabled;
private Map<String, String> tableData = EditorActionsUtil.DEFAULT_ACTIONS;
public static ConfigurationState getInstance() {
@ -61,6 +65,22 @@ public class ConfigurationState implements PersistentStateComponent<Configuratio
this.commitMessagePrompt = commitMessagePrompt;
}
public String getInlineCompletionPrompt() {
return inlineCompletionPrompt;
}
public void setInlineCompletionPrompt(String inlineCompletionPrompt) {
this.inlineCompletionPrompt = inlineCompletionPrompt;
}
public int getInlineDelay() {
return inlineDelay;
}
public void setInlineDelay(int inlineDelay) {
this.inlineDelay = inlineDelay;
}
public int getMaxTokens() {
return maxTokens;
}
@ -132,4 +152,12 @@ public class ConfigurationState implements PersistentStateComponent<Configuratio
public void setAutoFormattingEnabled(boolean autoFormattingEnabled) {
this.autoFormattingEnabled = autoFormattingEnabled;
}
public boolean isCodeCompletionsEnabled() {
return codeCompletionsEnabled;
}
public void setCodeCompletionsEnabled(boolean codeCompletionsEnabled) {
this.codeCompletionsEnabled = codeCompletionsEnabled;
}
}

View file

@ -5,6 +5,7 @@ import com.intellij.openapi.components.PersistentStateComponent;
import com.intellij.openapi.components.State;
import com.intellij.openapi.components.Storage;
import com.intellij.util.xmlb.XmlSerializerUtil;
import ee.carlrobert.codegpt.codecompletions.InfillPromptTemplate;
import ee.carlrobert.codegpt.completions.HuggingFaceModel;
import ee.carlrobert.codegpt.completions.llama.PromptTemplate;
import ee.carlrobert.codegpt.settings.service.ServiceSelectionForm;
@ -21,6 +22,7 @@ public class LlamaSettingsState implements PersistentStateComponent<LlamaSetting
private String customLlamaServerPath = "";
private HuggingFaceModel huggingFaceModel = HuggingFaceModel.CODE_LLAMA_7B_Q4;
private PromptTemplate promptTemplate = PromptTemplate.LLAMA;
private InfillPromptTemplate infillPromptTemplate = InfillPromptTemplate.LLAMA;
private Integer serverPort = getRandomAvailablePortOrDefault();
private int contextSize = 2048;
private int threads = 8;
@ -216,6 +218,14 @@ public class LlamaSettingsState implements PersistentStateComponent<LlamaSetting
this.repeatPenalty = repeatPenalty;
}
public InfillPromptTemplate getInfillPromptTemplate() {
return infillPromptTemplate;
}
public void setInfillPromptTemplate(InfillPromptTemplate infillPromptTemplate) {
this.infillPromptTemplate = infillPromptTemplate;
}
private static Integer getRandomAvailablePortOrDefault() {
try (ServerSocket socket = new ServerSocket(0)) {
return socket.getLocalPort();

View file

@ -0,0 +1,52 @@
package ee.carlrobert.codegpt.statusbar;
import com.intellij.openapi.actionSystem.ActionGroup;
import com.intellij.openapi.actionSystem.ActionManager;
import com.intellij.openapi.actionSystem.DataContext;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.ui.popup.JBPopupFactory;
import com.intellij.openapi.ui.popup.JBPopupFactory.ActionSelectionAid;
import com.intellij.openapi.ui.popup.ListPopup;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.openapi.wm.StatusBarWidget;
import com.intellij.openapi.wm.impl.status.EditorBasedStatusBarPopup;
import ee.carlrobert.codegpt.CodeGPTBundle;
import ee.carlrobert.codegpt.Icons;
import org.jetbrains.annotations.NonNls;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
public class CodeGPTStatusBarWidget extends EditorBasedStatusBarPopup {
public CodeGPTStatusBarWidget(Project project) {
super(project, false);
}
@Override
protected @NotNull WidgetState getWidgetState(@Nullable VirtualFile file) {
var state = new WidgetState(CodeGPTBundle.get("statusBar.widget.tooltip"), "", true);
state.setIcon(Icons.DefaultSmall);
return state;
}
@Override
protected @Nullable ListPopup createPopup(DataContext context) {
return JBPopupFactory.getInstance()
.createActionGroupPopup(
CodeGPTBundle.get("project.label"),
(ActionGroup) ActionManager.getInstance().getAction("codegpt.statusBarPopup"),
context,
ActionSelectionAid.SPEEDSEARCH,
true);
}
@Override
protected @NotNull StatusBarWidget createInstance(@NotNull Project project) {
return new CodeGPTStatusBarWidget(project);
}
@Override
public @NonNls @NotNull String ID() {
return "ee.carlrobert.codegpt.statusbar.widget";
}
}

View file

@ -0,0 +1,33 @@
package ee.carlrobert.codegpt.statusbar;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.util.Disposer;
import com.intellij.openapi.wm.StatusBarWidget;
import com.intellij.openapi.wm.impl.status.widget.StatusBarEditorBasedWidgetFactory;
import ee.carlrobert.codegpt.CodeGPTBundle;
import org.jetbrains.annotations.Nls;
import org.jetbrains.annotations.NonNls;
import org.jetbrains.annotations.NotNull;
public class CodeGPTStatusBarWidgetFactory extends StatusBarEditorBasedWidgetFactory {
@Override
public @NonNls @NotNull String getId() {
return "ee.carlrobert.codegpt.statusbar.widget";
}
@Override
public @Nls @NotNull String getDisplayName() {
return CodeGPTBundle.get("project.label");
}
@Override
public @NotNull StatusBarWidget createWidget(@NotNull Project project) {
return new CodeGPTStatusBarWidget(project);
}
@Override
public void disposeWidget(@NotNull StatusBarWidget widget) {
Disposer.dispose(widget);
}
}

View file

@ -4,5 +4,6 @@ import java.awt.Point;
@FunctionalInterface
public interface EditorActionEvent {
void handleAction(EditorAction action, Point locationOnScreen);
}

View file

@ -11,7 +11,10 @@ import com.intellij.openapi.editor.Editor;
import com.intellij.openapi.editor.EditorFactory;
import com.intellij.openapi.editor.EditorKind;
import com.intellij.openapi.fileEditor.FileDocumentManager;
import com.intellij.openapi.fileEditor.FileEditor;
import com.intellij.openapi.fileEditor.FileEditorManager;
import com.intellij.openapi.fileEditor.TextEditor;
import com.intellij.openapi.fileEditor.impl.FileEditorManagerImpl;
import com.intellij.openapi.project.Project;
import com.intellij.psi.PsiDocumentManager;
import com.intellij.psi.codeStyle.CodeStyleManager;
@ -79,6 +82,23 @@ public final class EditorUtil {
return null;
}
public static boolean isSelectedEditor(Editor editor) {
Project project = editor.getProject();
if (project != null && !project.isDisposed()) {
FileEditorManager editorManager = FileEditorManager.getInstance(project);
if (editorManager == null) {
return false;
}
if (editorManager instanceof FileEditorManagerImpl) {
Editor current = ((FileEditorManagerImpl) editorManager).getSelectedTextEditor(true);
return current != null && current.equals(editor);
}
FileEditor current = editorManager.getSelectedEditor();
return current instanceof TextEditor && editor.equals(((TextEditor) current).getEditor());
}
return false;
}
public static boolean isMainEditorTextSelected(@NotNull Project project) {
return hasSelection(getSelectedEditor(project));
}
@ -106,7 +126,7 @@ public final class EditorUtil {
})));
}
private static void reformatDocument(
public static void reformatDocument(
@NotNull Project project,
@NotNull Document document,
int startOffset,

View file

@ -14,12 +14,12 @@ import java.util.regex.Pattern;
public class MarkdownUtil {
/**
* Splits a given string into a list of strings where each element is either a code block
* surrounded by triple backticks or a non-code block text.
* Splits a given string into a list of strings where each element is either a code block
* surrounded by triple backticks or a non-code block text.
*
* @param inputMarkdown The input markdown formatted string to be split.
* @return A list of strings where each element is a code block or a non-code block text from the
* input string.
* @param inputMarkdown The input markdown formatted string to be split.
* @return A list of strings where each element is a code block or a non-code block text from the
* input string.
*/
public static List<String> splitCodeBlocks(String inputMarkdown) {
List<String> result = new ArrayList<>();