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:
Carl-Robert Linnupuu 2024-02-11 01:26:55 +02:00
parent 056276d626
commit 4ed74a31c1
9 changed files with 179 additions and 56 deletions

View file

@ -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 =

View file

@ -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);
}

View file

@ -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();
}

View file

@ -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);
}
}
}

View file

@ -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();
}
});
}
}

View file

@ -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;
}
}