mirror of
https://github.com/carlrobertoh/ProxyAI.git
synced 2026-05-13 07:02:34 +00:00
feat: second set of autocomplete improvements
- support typing as suggested functionality - do not fetch completions on cursor change - other minor fixes
This commit is contained in:
parent
056276d626
commit
4ed74a31c1
9 changed files with 179 additions and 56 deletions
|
|
@ -8,6 +8,8 @@ import java.util.List;
|
|||
|
||||
public class CodeGPTKeys {
|
||||
|
||||
public static final Key<String> PREVIOUS_INLAY_TEXT =
|
||||
Key.create("codegpt.editor.inlay.prev-value");
|
||||
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 =
|
||||
|
|
|
|||
|
|
@ -11,8 +11,8 @@ import org.jetbrains.annotations.NotNull;
|
|||
public class OpenSettingsAction extends AnAction {
|
||||
|
||||
public OpenSettingsAction() {
|
||||
super(CodeGPTBundle.get("action.opensettings.title"),
|
||||
CodeGPTBundle.get("action.opensettings.description"),
|
||||
super(CodeGPTBundle.get("action.openSettings.title"),
|
||||
CodeGPTBundle.get("action.openSettings.description"),
|
||||
General.Settings);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,10 +1,10 @@
|
|||
package ee.carlrobert.codegpt.codecompletions;
|
||||
|
||||
import static ee.carlrobert.codegpt.CodeGPTKeys.PREVIOUS_INLAY_TEXT;
|
||||
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;
|
||||
|
|
@ -13,7 +13,9 @@ 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 java.io.IOException;
|
||||
import javax.annotation.ParametersAreNonnullByDefault;
|
||||
import javax.swing.SwingUtilities;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
@ParametersAreNonnullByDefault
|
||||
|
|
@ -40,18 +42,26 @@ class CodeCompletionEventListener implements CompletionEventListener<String> {
|
|||
progressIndicator.processFinish();
|
||||
}
|
||||
|
||||
PREVIOUS_INLAY_TEXT.set(editor, messageBuilder.toString());
|
||||
CodeGPTEditorManager.getInstance().disposeEditorInlays(editor);
|
||||
|
||||
var inlayText = messageBuilder.toString();
|
||||
if (!inlayText.isEmpty()) {
|
||||
ApplicationManager.getApplication().invokeLater(() ->
|
||||
SwingUtilities.invokeLater(() -> {
|
||||
if (editor.getCaretModel().getOffset() == caretOffset) {
|
||||
var inlayText = messageBuilder.toString();
|
||||
if (!inlayText.isEmpty()) {
|
||||
CodeCompletionService.getInstance(requireNonNull(editor.getProject()))
|
||||
.addInlays(editor, caretOffset, inlayText));
|
||||
}
|
||||
.addInlays(editor, caretOffset, inlayText);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(ErrorDetails error, Throwable ex) {
|
||||
// TODO: temp fix
|
||||
if (ex instanceof IOException && "Canceled".equals(error.getMessage())) {
|
||||
return;
|
||||
}
|
||||
|
||||
LOG.error(error.getMessage(), ex);
|
||||
if (progressIndicator != null) {
|
||||
progressIndicator.processFinish();
|
||||
|
|
@ -66,7 +76,7 @@ class CodeCompletionEventListener implements CompletionEventListener<String> {
|
|||
|
||||
@Override
|
||||
public void onCancelled(StringBuilder messageBuilder) {
|
||||
LOG.info("Completion cancelled");
|
||||
LOG.debug("Completion cancelled");
|
||||
if (progressIndicator != null) {
|
||||
progressIndicator.processFinish();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,10 @@
|
|||
package ee.carlrobert.codegpt.codecompletions;
|
||||
|
||||
import static ee.carlrobert.codegpt.CodeGPTKeys.PREVIOUS_INLAY_TEXT;
|
||||
|
||||
import com.intellij.openapi.Disposable;
|
||||
import com.intellij.openapi.application.ApplicationManager;
|
||||
import com.intellij.openapi.command.CommandProcessor;
|
||||
import com.intellij.openapi.editor.Editor;
|
||||
import com.intellij.openapi.editor.event.BulkAwareDocumentListener;
|
||||
import com.intellij.openapi.editor.event.CaretEvent;
|
||||
|
|
@ -11,6 +14,9 @@ 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.ConfigurationSettings;
|
||||
import java.util.List;
|
||||
import java.util.regex.PatternSyntaxException;
|
||||
import javax.swing.SwingUtilities;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
|
|
@ -31,16 +37,12 @@ public class CodeCompletionListenerBinder implements Disposable {
|
|||
|
||||
ApplicationManager.getApplication()
|
||||
.getMessageBus()
|
||||
.connect()
|
||||
.connect(this)
|
||||
.subscribe(
|
||||
CodeCompletionEnabledListener.TOPIC,
|
||||
(CodeCompletionEnabledListener) (completionsEnabled) -> {
|
||||
if (completionsEnabled) {
|
||||
addListeners();
|
||||
if (editor.getProject() != null) {
|
||||
CodeCompletionService.getInstance(editor.getProject())
|
||||
.handleCompletions(editor, editor.getCaretModel().getOffset());
|
||||
}
|
||||
} else {
|
||||
removeListeners();
|
||||
}
|
||||
|
|
@ -94,14 +96,12 @@ public class CodeCompletionListenerBinder implements Disposable {
|
|||
|
||||
@Override
|
||||
public void caretPositionChanged(@NotNull CaretEvent event) {
|
||||
var project = editor.getProject();
|
||||
if (event.getCaret() == null || project == null) {
|
||||
return;
|
||||
if (!"Typing".equals(CommandProcessor.getInstance().getCurrentCommandName())) {
|
||||
CodeGPTEditorManager.getInstance().disposeEditorInlays(editor);
|
||||
if (editor.getProject() != null) {
|
||||
CodeCompletionService.getInstance(editor.getProject()).cancelPreviousCall();
|
||||
}
|
||||
}
|
||||
|
||||
CodeGPTEditorManager.getInstance().disposeEditorInlays(editor);
|
||||
CodeCompletionService.getInstance(project)
|
||||
.handleCompletions(editor, event.getCaret().getOffset());
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -109,12 +109,48 @@ public class CodeCompletionListenerBinder implements Disposable {
|
|||
|
||||
@Override
|
||||
public void documentChangedNonBulk(@NotNull DocumentEvent event) {
|
||||
var project = editor.getProject();
|
||||
if (project != null) {
|
||||
ApplicationManager.getApplication().executeOnPooledThread(() -> {
|
||||
CodeGPTEditorManager.getInstance().disposeEditorInlays(editor);
|
||||
CodeCompletionService.getInstance(project)
|
||||
.handleCompletions(editor, editor.getCaretModel().getOffset());
|
||||
|
||||
var commandName = CommandProcessor.getInstance().getCurrentCommandName();
|
||||
if (CommandProcessor.getInstance().isUndoTransparentActionInProgress()
|
||||
|| isCommandExcluded(commandName)) {
|
||||
return;
|
||||
}
|
||||
|
||||
var project = editor.getProject();
|
||||
if (project != null) {
|
||||
var codeCompletionService = CodeCompletionService.getInstance(project);
|
||||
var caretOffset = event.getOffset() + event.getNewLength();
|
||||
var charTyped = event.getNewFragment().toString().trim();
|
||||
SwingUtilities.invokeLater(() -> {
|
||||
if (isTypingAsSuggested(charTyped)) {
|
||||
try {
|
||||
var previousInlayText = PREVIOUS_INLAY_TEXT.get(editor).replaceFirst(charTyped, "");
|
||||
codeCompletionService.addInlays(editor, caretOffset, previousInlayText);
|
||||
} catch (PatternSyntaxException e) {
|
||||
// ignore
|
||||
}
|
||||
} else {
|
||||
codeCompletionService.handleCompletions(editor, caretOffset);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private boolean isTypingAsSuggested(String charTyped) {
|
||||
if (charTyped.isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
var prevInlay = PREVIOUS_INLAY_TEXT.get(editor);
|
||||
return prevInlay != null && prevInlay.startsWith(charTyped);
|
||||
}
|
||||
|
||||
private boolean isCommandExcluded(String commandName) {
|
||||
return commandName != null
|
||||
&& List.of("Up", "Down", "Left", "Right", "Move Caret").contains(commandName);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,8 +2,8 @@ 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.PREVIOUS_INLAY_TEXT;
|
||||
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.codeInsight.lookup.LookupManager;
|
||||
|
|
@ -31,6 +31,7 @@ import com.intellij.psi.PsiFile;
|
|||
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.CodeGPTKeys;
|
||||
import ee.carlrobert.codegpt.actions.CodeCompletionEnabledListener;
|
||||
import ee.carlrobert.codegpt.completions.CompletionRequestService;
|
||||
import ee.carlrobert.codegpt.settings.configuration.ConfigurationSettings;
|
||||
|
|
@ -39,6 +40,8 @@ import java.awt.event.KeyEvent;
|
|||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.regex.PatternSyntaxException;
|
||||
import java.util.stream.Stream;
|
||||
import javax.annotation.ParametersAreNonnullByDefault;
|
||||
import javax.swing.KeyStroke;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
|
@ -51,30 +54,29 @@ public final class CodeCompletionService implements Disposable {
|
|||
|
||||
private static final Logger LOG = Logger.getInstance(CodeCompletionService.class);
|
||||
|
||||
private final Project project;
|
||||
private final CallDebouncer callDebouncer;
|
||||
|
||||
private CodeCompletionService(Project project) {
|
||||
this.project = project;
|
||||
this.callDebouncer = new CallDebouncer(project);
|
||||
ApplicationManager.getApplication()
|
||||
.getMessageBus()
|
||||
.connect()
|
||||
.subscribe(
|
||||
CodeCompletionEnabledListener.TOPIC,
|
||||
(CodeCompletionEnabledListener) (completionsEnabled) -> {
|
||||
if (!completionsEnabled) {
|
||||
callDebouncer.cancelPreviousCall();
|
||||
}
|
||||
});
|
||||
|
||||
subscribeToFeatureToggleEvents();
|
||||
}
|
||||
|
||||
public static CodeCompletionService getInstance(Project project) {
|
||||
return project.getService(CodeCompletionService.class);
|
||||
}
|
||||
|
||||
public void cancelPreviousCall() {
|
||||
callDebouncer.cancelPreviousCall();
|
||||
}
|
||||
|
||||
public void handleCompletions(Editor editor, int offset) {
|
||||
Project project = editor.getProject();
|
||||
if (project == null
|
||||
|| project.isDisposed()
|
||||
PREVIOUS_INLAY_TEXT.set(editor, null);
|
||||
|
||||
if (project.isDisposed()
|
||||
|| TypeOverHandler.getPendingTypeOverAndReset(editor)
|
||||
|| !ConfigurationSettings.getCurrentState().isCodeCompletionsEnabled()
|
||||
|| !EditorUtil.isSelectedEditor(editor)
|
||||
|| LookupManager.getActiveLookup(editor) != null
|
||||
|
|
@ -90,6 +92,11 @@ public final class CodeCompletionService implements Disposable {
|
|||
}
|
||||
|
||||
var request = InfillRequestDetails.fromDocumentWithMaxOffset(document, offset);
|
||||
if (Stream.of(request.getSuffix(), request.getPrefix())
|
||||
.anyMatch(item -> item == null || item.isEmpty())) {
|
||||
return;
|
||||
}
|
||||
|
||||
callDebouncer.debounce(
|
||||
Void.class,
|
||||
(progressIndicator) -> CompletionRequestService.getInstance().getCodeCompletionAsync(
|
||||
|
|
@ -101,9 +108,15 @@ public final class CodeCompletionService implements Disposable {
|
|||
|
||||
@RequiresEdt
|
||||
public void addInlays(Editor editor, int caretOffset, String inlayText) {
|
||||
PREVIOUS_INLAY_TEXT.set(editor, inlayText);
|
||||
|
||||
if (LookupManager.getActiveLookup(editor) != null || inlayText.isBlank()) {
|
||||
return;
|
||||
}
|
||||
|
||||
List<String> linesList = inlayText.lines().collect(toList());
|
||||
String firstLine = linesList.get(0);
|
||||
String restOfLines = linesList.size() > 1
|
||||
var firstLine = linesList.get(0);
|
||||
var restOfLines = linesList.size() > 1
|
||||
? String.join("\n", linesList.subList(1, linesList.size()))
|
||||
: null;
|
||||
InlayModel inlayModel = editor.getInlayModel();
|
||||
|
|
@ -126,7 +139,7 @@ public final class CodeCompletionService implements Disposable {
|
|||
}
|
||||
|
||||
registerApplyCompletionAction(() -> WriteCommandAction.runWriteCommandAction(
|
||||
editor.getProject(),
|
||||
project,
|
||||
() -> applyCompletion(editor, inlayText)));
|
||||
}
|
||||
|
||||
|
|
@ -146,19 +159,20 @@ public final class CodeCompletionService implements Disposable {
|
|||
return;
|
||||
}
|
||||
}
|
||||
editor.putUserData(CodeGPTKeys.PREVIOUS_INLAY_TEXT, null);
|
||||
}
|
||||
|
||||
@RequiresWriteLock
|
||||
private void applyCompletion(Editor editor, String text, int offset) {
|
||||
Document document = editor.getDocument();
|
||||
document.insertString(offset, text);
|
||||
try {
|
||||
document.insertString(offset, text);
|
||||
} catch (PatternSyntaxException e) {
|
||||
// ignore
|
||||
}
|
||||
editor.getCaretModel().moveToOffset(offset + text.length());
|
||||
if (ConfigurationSettings.getCurrentState().isAutoFormattingEnabled()) {
|
||||
EditorUtil.reformatDocument(
|
||||
requireNonNull(editor.getProject()),
|
||||
document,
|
||||
offset,
|
||||
offset + text.length());
|
||||
EditorUtil.reformatDocument(project, document, offset, offset + text.length());
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -200,4 +214,17 @@ public final class CodeCompletionService implements Disposable {
|
|||
APPLY_INLAY_ACTION_ID,
|
||||
new KeyboardShortcut(KeyStroke.getKeyStroke(KeyEvent.VK_TAB, 0), null));
|
||||
}
|
||||
|
||||
private void subscribeToFeatureToggleEvents() {
|
||||
ApplicationManager.getApplication()
|
||||
.getMessageBus()
|
||||
.connect(this)
|
||||
.subscribe(
|
||||
CodeCompletionEnabledListener.TOPIC,
|
||||
(CodeCompletionEnabledListener) (completionsEnabled) -> {
|
||||
if (!completionsEnabled) {
|
||||
cancelPreviousCall();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,37 @@
|
|||
package ee.carlrobert.codegpt.codecompletions;
|
||||
|
||||
import com.intellij.codeInsight.editorActions.TypedHandlerDelegate;
|
||||
import com.intellij.openapi.command.CommandProcessor;
|
||||
import com.intellij.openapi.editor.Editor;
|
||||
import com.intellij.openapi.fileTypes.FileType;
|
||||
import com.intellij.openapi.project.Project;
|
||||
import com.intellij.openapi.util.Key;
|
||||
import com.intellij.psi.PsiFile;
|
||||
import java.util.stream.Stream;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
public class TypeOverHandler extends TypedHandlerDelegate {
|
||||
|
||||
private static final Key<Long> TYPE_OVER_STAMP = Key.create("codegpt.typeOverStamp");
|
||||
|
||||
public static boolean getPendingTypeOverAndReset(@NotNull Editor editor) {
|
||||
Long stamp = TYPE_OVER_STAMP.get(editor);
|
||||
if (stamp == null) {
|
||||
return false;
|
||||
}
|
||||
TYPE_OVER_STAMP.set(editor, null);
|
||||
return stamp == editor.getDocument().getModificationStamp();
|
||||
}
|
||||
|
||||
@NotNull
|
||||
public TypedHandlerDelegate.@NotNull Result beforeCharTyped(char c, @NotNull Project project,
|
||||
@NotNull Editor editor, @NotNull PsiFile file, @NotNull FileType fileType) {
|
||||
boolean validTypeOver = Stream.of(')', '}', ']', '"', '\'', '>', ';').anyMatch(it -> it == c);
|
||||
if (validTypeOver && CommandProcessor.getInstance().getCurrentCommand() != null) {
|
||||
TYPE_OVER_STAMP.set(editor, editor.getDocument().getModificationStamp());
|
||||
} else {
|
||||
TYPE_OVER_STAMP.set(editor, null);
|
||||
}
|
||||
return Result.CONTINUE;
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue