mirror of
https://github.com/carlrobertoh/ProxyAI.git
synced 2026-05-12 22:31:24 +00:00
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:
parent
390d8cdd5e
commit
7387cf4536
41 changed files with 1461 additions and 21 deletions
|
|
@ -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");
|
||||
}
|
||||
|
|
|
|||
|
|
@ -48,7 +48,7 @@ public final class EncodingManager {
|
|||
try {
|
||||
return encoding.countTokens(text);
|
||||
} catch (Exception ex) {
|
||||
LOG.error(ex);
|
||||
LOG.warn(ex);
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
@ -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)));
|
||||
}
|
||||
}
|
||||
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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());
|
||||
}
|
||||
}
|
||||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -6,4 +6,5 @@ public enum ConversationType {
|
|||
EDITOR_ACTION,
|
||||
FIX_COMPILE_ERRORS,
|
||||
MULTI_FILE,
|
||||
INLINE_COMPLETION,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
package ee.carlrobert.codegpt.completions;
|
||||
|
||||
class TotalUsageExceededException extends RuntimeException {
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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());
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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());
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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";
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -4,5 +4,6 @@ import java.awt.Point;
|
|||
|
||||
@FunctionalInterface
|
||||
public interface EditorActionEvent {
|
||||
|
||||
void handleAction(EditorAction action, Point locationOnScreen);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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<>();
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue