diff --git a/src/main/java/com/zhongan/devpilot/completions/CompletionUtils.java b/src/main/java/com/zhongan/devpilot/completions/CompletionUtils.java index 36bffa82..6b718afe 100644 --- a/src/main/java/com/zhongan/devpilot/completions/CompletionUtils.java +++ b/src/main/java/com/zhongan/devpilot/completions/CompletionUtils.java @@ -4,6 +4,7 @@ import com.intellij.codeInsight.completion.CompletionResultSet; import com.intellij.openapi.diagnostic.Logger; import com.intellij.openapi.editor.Document; +import com.intellij.openapi.editor.Editor; import com.intellij.openapi.util.TextRange; import com.zhongan.devpilot.completions.general.SuggestionTrigger; import com.zhongan.devpilot.completions.prediction.DevPilotCompletion; @@ -45,12 +46,13 @@ private static String getCursorSuffix(@NotNull Document document, int cursorPosi @Nullable public static DevPilotCompletion createDevpilotCompletion( - @NotNull Document document, - int offset, - String oldPrefix, - ResultEntry result, - int index, - SuggestionTrigger suggestionTrigger) { + Editor editor, + @NotNull Document document, + int offset, + String oldPrefix, + ResultEntry result, + int index, + SuggestionTrigger suggestionTrigger) { String cursorPrefix = CompletionUtils.getCursorPrefix(document, offset); String cursorSuffix = CompletionUtils.getCursorSuffix(document, offset); if (cursorPrefix == null || cursorSuffix == null) { @@ -58,29 +60,57 @@ public static DevPilotCompletion createDevpilotCompletion( } return new DevPilotCompletion( - result.id, - oldPrefix, - result.newPrefix, - result.oldSuffix, - result.newSuffix, - index, - cursorPrefix, - cursorSuffix, - result.completionMetadata, - suggestionTrigger); + editor, + result.id, + oldPrefix, + result.newPrefix, + result.oldSuffix, + result.newSuffix, + index, + cursorPrefix, + cursorSuffix, + result.completionMetadata, + suggestionTrigger); + } + + @Nullable + public static DevPilotCompletion createSimpleDevpilotCompletion( + Editor editor, + int offset, + String oldPrefix, + String newPrefix, + String id, + @NotNull Document document) { + String cursorPrefix = CompletionUtils.getCursorPrefix(document, offset); + String cursorSuffix = CompletionUtils.getCursorSuffix(document, offset); + if (cursorPrefix == null || cursorSuffix == null) { + return null; + } + return new DevPilotCompletion( + editor, + id, + oldPrefix, + newPrefix, + "", + "", + 0, + cursorPrefix, + cursorSuffix, + null, + null); } public static int completionLimit( - CompletionParameters parameters, CompletionResultSet result, boolean isLocked) { + CompletionParameters parameters, CompletionResultSet result, boolean isLocked) { return completionLimit( - parameters.getEditor().getDocument(), - result.getPrefixMatcher().getPrefix(), - parameters.getOffset(), - isLocked); + parameters.getEditor().getDocument(), + result.getPrefixMatcher().getPrefix(), + parameters.getOffset(), + isLocked); } public static int completionLimit( - @NotNull Document document, @NotNull String prefix, int offset, boolean isLocked) { + @NotNull Document document, @NotNull String prefix, int offset, boolean isLocked) { if (isLocked) { return 1; } diff --git a/src/main/java/com/zhongan/devpilot/completions/inline/AcceptDevPilotInlineCompletionByLineAction.java b/src/main/java/com/zhongan/devpilot/completions/inline/AcceptDevPilotInlineCompletionByLineAction.java new file mode 100644 index 00000000..ba3ccdb7 --- /dev/null +++ b/src/main/java/com/zhongan/devpilot/completions/inline/AcceptDevPilotInlineCompletionByLineAction.java @@ -0,0 +1,30 @@ +package com.zhongan.devpilot.completions.inline; + +import com.intellij.codeInsight.hint.HintManagerImpl.ActionToIgnore; +import com.intellij.openapi.actionSystem.DataContext; +import com.intellij.openapi.editor.Caret; +import com.intellij.openapi.editor.Editor; +import com.intellij.openapi.editor.actionSystem.EditorAction; +import com.intellij.openapi.editor.actionSystem.EditorWriteActionHandler; + +public class AcceptDevPilotInlineCompletionByLineAction extends EditorAction implements ActionToIgnore, InlineCompletionAction { + + public static final String ACTION_ID = "AcceptDevPilotInlineCompletionByLineAction"; + + public AcceptDevPilotInlineCompletionByLineAction() { + super(new AcceptInlineCompletionHandler()); + } + + private static class AcceptInlineCompletionHandler extends EditorWriteActionHandler { + + @Override + public void executeWriteAction(Editor editor, Caret caret, DataContext dataContext) { + CompletionPreview.getInstance(editor).applyPreviewByLine(caret != null ? caret : editor.getCaretModel().getCurrentCaret()); + } + + @Override + protected boolean isEnabledForCaret(Editor editor, Caret caret, DataContext dataContext) { + return CompletionPreview.getInstance(editor) != null; + } + } +} \ No newline at end of file diff --git a/src/main/java/com/zhongan/devpilot/completions/inline/CompletionPreview.java b/src/main/java/com/zhongan/devpilot/completions/inline/CompletionPreview.java index 78e64efb..983bcb08 100644 --- a/src/main/java/com/zhongan/devpilot/completions/inline/CompletionPreview.java +++ b/src/main/java/com/zhongan/devpilot/completions/inline/CompletionPreview.java @@ -1,15 +1,23 @@ package com.zhongan.devpilot.completions.inline; import com.intellij.openapi.Disposable; +import com.intellij.openapi.actionSystem.ActionManager; import com.intellij.openapi.application.ApplicationManager; import com.intellij.openapi.diagnostic.Logger; import com.intellij.openapi.editor.Caret; +import com.intellij.openapi.editor.Document; import com.intellij.openapi.editor.Editor; +import com.intellij.openapi.editor.LogicalPosition; +import com.intellij.openapi.editor.event.CaretEvent; +import com.intellij.openapi.editor.event.DocumentEvent; import com.intellij.openapi.editor.ex.util.EditorUtil; import com.intellij.openapi.editor.impl.EditorImpl; +import com.intellij.openapi.keymap.KeymapUtil; import com.intellij.openapi.project.Project; import com.intellij.openapi.util.Disposer; import com.intellij.openapi.util.Key; +import com.intellij.openapi.util.TextRange; +import com.intellij.openapi.util.text.StringUtil; import com.intellij.psi.PsiDocumentManager; import com.intellij.psi.PsiFile; import com.intellij.refactoring.rename.inplace.InplaceRefactoring; @@ -18,18 +26,24 @@ import com.zhongan.devpilot.completions.inline.render.DevPilotInlay; import com.zhongan.devpilot.completions.prediction.DevPilotCompletion; import com.zhongan.devpilot.treesitter.TreeSitterParser; +import com.zhongan.devpilot.util.DevPilotMessageBundle; import com.zhongan.devpilot.util.TelemetryUtils; +import java.util.Collections; import java.util.List; +import java.util.Objects; +import java.util.UUID; +import org.apache.commons.lang3.StringUtils; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; +import static com.zhongan.devpilot.completions.CompletionUtils.createSimpleDevpilotCompletion; import static com.zhongan.devpilot.completions.inline.CompletionPreviewUtils.shouldRemoveSuffix; public class CompletionPreview implements Disposable { private static final Key INLINE_COMPLETION_PREVIEW = - Key.create("INLINE_COMPLETION_PREVIEW"); + Key.create("INLINE_COMPLETION_PREVIEW"); public final Editor editor; @@ -44,7 +58,7 @@ public class CompletionPreview implements Disposable { private InlineCaretListener inlineCaretListener; private CompletionPreview( - @NotNull Editor editor, List completions, int offset) { + @NotNull Editor editor, List completions, int offset) { this.editor = editor; this.completions = completions; this.offset = offset; @@ -108,8 +122,8 @@ private DevPilotCompletion createPreview() { DevPilotCompletion completion = completions.get(currentIndex); if (!(editor instanceof EditorImpl) - || editor.getSelectionModel().hasSelection() - || InplaceRefactoring.getActiveInplaceRenamer(editor) != null) { + || editor.getSelectionModel().hasSelection() + || InplaceRefactoring.getActiveInplaceRenamer(editor) != null) { return null; } @@ -181,10 +195,112 @@ private void applyPreviewInternal(@NotNull Integer cursorOffset, Project project getAutoImportHandler(editor, fileAfterCompletion, startOffset, endOffset).invoke(); }); - TelemetryUtils.completionAccept(completion.id, file); + TelemetryUtils.completionAccept(completion.id, file, completion.getUnacceptedLines()); + } + + public void applyPreviewByLine(@Nullable Caret caret) { + if (caret == null) { + return; + } + + Project project = editor.getProject(); + + if (project == null) { + return; + } + + PsiFile file = PsiDocumentManager.getInstance(project).getPsiFile(editor.getDocument()); + + if (file == null) { + return; + } + + try { + applyPreviewInternalByLine(project, file); + } catch (Throwable e) { + Logger.getInstance(getClass()).warn("Failed in the processes of accepting completion by line", e); + } finally { + Disposer.dispose(this); + } + } + + private void applyPreviewInternalByLine(Project project, PsiFile file) { + DevPilotCompletion completion = completions.get(currentIndex); + Document document = editor.getDocument(); + String line = completion.getNextUnacceptLineState().getLine(); + LogicalPosition currentPos = editor.getCaretModel().getLogicalPosition(); + int insertionOffset = editor.logicalPositionToOffset(currentPos); + if (StringUtils.isEmpty(line)) { + completion.acceptLine(insertionOffset); + line = completion.getNextUnacceptLineState().getLine(); + } + if (completion.getLineStateItems().getIndex() > 0) { + insertionOffset += "\n".length(); // can't remove + } + completion.acceptLine(insertionOffset + line.length()); + document.insertString(insertionOffset, line + "\n"); // offset don't change + editor.getCaretModel().moveToOffset(insertionOffset + line.length()); + Objects.requireNonNull(CompletionPreview.getInstance(editor)).continuePreview(); + PsiFile fileAfterCompletion = PsiDocumentManager.getInstance(project).getPsiFile(editor.getDocument()); + int startOffset = insertionOffset - completion.oldPrefix.length(); + int endOffset = insertionOffset + line.length(); + ApplicationManager.getApplication().executeOnPooledThread(() -> { + getAutoImportHandler(editor, fileAfterCompletion, startOffset, endOffset).invoke(); + }); + TelemetryUtils.completionAccept(completion.id, file, line); + } + + public void continuePreview() { + DevPilotCompletion completion = completions.get(currentIndex); + if (!(editor instanceof EditorImpl) + || editor.getSelectionModel().hasSelection() + || InplaceRefactoring.getActiveInplaceRenamer(editor) != null) { + return; + } + + try { + editor.getDocument().startGuardedBlockChecking(); + DevPilotCompletion simpleDevpilotCompletion = createSimpleDevpilotCompletion(editor, editor.getCaretModel().getOffset(), + "", + completion.getLineStateItems().getUnacceptedLines(), + UUID.randomUUID().toString(), editor.getDocument()); + CompletionPreview.clear(editor); + CompletionPreview.createInstance(editor, Collections.singletonList(simpleDevpilotCompletion), editor.getCaretModel().getOffset()); + } finally { + editor.getDocument().stopGuardedBlockChecking(); + } + } + + public boolean isByLineAcceptCaretChange(CaretEvent caretEvent) { + int newOffset = editor.logicalPositionToOffset(caretEvent.getNewPosition()); + DevPilotCompletion completion = completions.get(currentIndex); + int currentCompletionPosition = completion.getCurrentCompletionPosition(); + return newOffset == currentCompletionPosition; + } + + public boolean isByLineAcceptDocumentChange(DocumentEvent documentEvent) { + int previousOffset = documentEvent.getOffset(); + int newOffset = previousOffset + documentEvent.getNewLength(); + if (newOffset < 0 || previousOffset >= newOffset) return false; // previousOffset == newOffset ctr + z + String addedText = editor.getDocument().getText(new TextRange(previousOffset, newOffset)); + + DevPilotCompletion completion = completions.get(currentIndex); + String completionCode = completion.getCurrentCompletionCode(); + return StringUtils.equals(addedText, completionCode); } private static AutoImportHandler getAutoImportHandler(Editor editor, PsiFile file, int startOffset, int endOffset) { return new AutoImportHandler(startOffset, endOffset, editor, file); } + + public static String byLineAcceptHintText() { + String acceptShortcut = getByLineAcceptShortcutText(); + return String.format("%s %s", acceptShortcut, DevPilotMessageBundle.get("completion.apply.partial.tooltips")); + } + + private static String getByLineAcceptShortcutText() { + return StringUtil.defaultIfEmpty( + KeymapUtil.getFirstKeyboardShortcutText(ActionManager.getInstance().getAction(AcceptDevPilotInlineCompletionByLineAction.ACTION_ID)), + "Missing shortcut key"); + } } diff --git a/src/main/java/com/zhongan/devpilot/completions/inline/DevPilotDocumentListener.java b/src/main/java/com/zhongan/devpilot/completions/inline/DevPilotDocumentListener.java index ed161f79..77ae7181 100644 --- a/src/main/java/com/zhongan/devpilot/completions/inline/DevPilotDocumentListener.java +++ b/src/main/java/com/zhongan/devpilot/completions/inline/DevPilotDocumentListener.java @@ -53,10 +53,15 @@ public void documentChangedNonBulk(@NotNull DocumentEvent event) { } Document document = event.getDocument(); Editor editor = getActiveEditor(document); - if (editor == null || !EditorUtils.isMainEditor(editor)) { + if (editor == null || !EditorUtils.isMainEditor(editor) || editor.getCaretModel().getCaretCount() > 1) { return; } DevPilotCompletion lastShownCompletion = CompletionPreview.getCurrentCompletion(editor); + CompletionPreview completionPreview = CompletionPreview.getInstance(editor); + + if (completionPreview != null && completionPreview.isByLineAcceptDocumentChange(event)) { + return; + } CompletionPreview.clear(editor); int offset = event.getOffset() + event.getNewLength(); diff --git a/src/main/java/com/zhongan/devpilot/completions/inline/InlineCompletionHandler.java b/src/main/java/com/zhongan/devpilot/completions/inline/InlineCompletionHandler.java index a2b7786d..535f5237 100644 --- a/src/main/java/com/zhongan/devpilot/completions/inline/InlineCompletionHandler.java +++ b/src/main/java/com/zhongan/devpilot/completions/inline/InlineCompletionHandler.java @@ -190,6 +190,7 @@ private List retrieveInlineCompletion( } return createCompletions( + editor, completionsResponse, editor.getDocument(), offset, @@ -249,6 +250,8 @@ private void afterCompletionShown(DevPilotCompletion completion, Editor editor) } private List createCompletions( + @NotNull Editor editor, + @NotNull AutocompleteResponse completions, @NotNull Document document, int offset, @@ -257,6 +260,7 @@ private List createCompletions( .mapToObj( index -> CompletionUtils.createDevpilotCompletion( + editor, document, offset, completions.oldPrefix, diff --git a/src/main/java/com/zhongan/devpilot/completions/inline/listeners/InlineCaretListener.java b/src/main/java/com/zhongan/devpilot/completions/inline/listeners/InlineCaretListener.java index 2ee2d9d6..250b0e4e 100644 --- a/src/main/java/com/zhongan/devpilot/completions/inline/listeners/InlineCaretListener.java +++ b/src/main/java/com/zhongan/devpilot/completions/inline/listeners/InlineCaretListener.java @@ -24,13 +24,15 @@ public void caretPositionChanged(@NotNull CaretEvent event) { return; } + if (completionPreview.isByLineAcceptCaretChange(event)) { + return; + } Disposer.dispose(completionPreview); InlineCompletionCache.clear(event.getEditor()); } private boolean isSingleOffsetChange(CaretEvent event) { - return event.getOldPosition().line == event.getNewPosition().line - && event.getOldPosition().column + 1 == event.getNewPosition().column; + return event.getOldPosition().line == event.getNewPosition().line; } @Override diff --git a/src/main/java/com/zhongan/devpilot/completions/inline/render/BlockElementRenderer.java b/src/main/java/com/zhongan/devpilot/completions/inline/render/BlockElementRenderer.java index 785b73b4..57ba16b9 100644 --- a/src/main/java/com/zhongan/devpilot/completions/inline/render/BlockElementRenderer.java +++ b/src/main/java/com/zhongan/devpilot/completions/inline/render/BlockElementRenderer.java @@ -4,6 +4,7 @@ import com.intellij.openapi.editor.EditorCustomElementRenderer; import com.intellij.openapi.editor.Inlay; import com.intellij.openapi.editor.markup.TextAttributes; +import com.zhongan.devpilot.completions.inline.CompletionPreview; import java.awt.Color; import java.awt.Graphics; @@ -21,17 +22,24 @@ public class BlockElementRenderer implements EditorCustomElementRenderer { private Color color; - public BlockElementRenderer(Editor editor, List blockText, boolean deprecated) { + private final boolean needHintInBlock; + + public BlockElementRenderer(Editor editor, List blockText, boolean deprecated, boolean needHintInBlock) { this.editor = editor; this.blockText = blockText; this.deprecated = deprecated; + this.needHintInBlock = needHintInBlock; } @Override public int calcWidthInPixels(Inlay inlay) { String firstLine = blockText.get(0); + boolean hint = blockText.size() > 1; + if (needHintInBlock && hint) { + firstLine = firstLine + " " + CompletionPreview.byLineAcceptHintText(); + } return editor.getContentComponent() - .getFontMetrics(GraphicsUtils.getFont(editor, deprecated)).stringWidth(firstLine); + .getFontMetrics(GraphicsUtils.getFont(editor, firstLine)).stringWidth(firstLine); } @Override @@ -41,16 +49,20 @@ public int calcHeightInPixels(Inlay inlay) { @Override public void paint(Inlay inlay, Graphics g, Rectangle targetRegion, TextAttributes textAttributes) { + boolean hint = blockText.size() > 1; color = color != null ? color : GraphicsUtils.getColor(); g.setColor(color); - g.setFont(GraphicsUtils.getFont(editor, deprecated)); - for (int i = 0; i < blockText.size(); i++) { String line = blockText.get(i); + if (needHintInBlock && i == 0 && hint) { + line = line + " " + CompletionPreview.byLineAcceptHintText(); + hint = false; + } + g.setFont(GraphicsUtils.getFont(editor, line)); g.drawString( - line, - 0, - targetRegion.y + i * editor.getLineHeight() + editor.getAscent() + line, + 0, + targetRegion.y + i * editor.getLineHeight() + editor.getAscent() ); } } diff --git a/src/main/java/com/zhongan/devpilot/completions/inline/render/DefaultDevPilotInlay.java b/src/main/java/com/zhongan/devpilot/completions/inline/render/DefaultDevPilotInlay.java index d3e7c1e6..91f5fdcc 100644 --- a/src/main/java/com/zhongan/devpilot/completions/inline/render/DefaultDevPilotInlay.java +++ b/src/main/java/com/zhongan/devpilot/completions/inline/render/DefaultDevPilotInlay.java @@ -28,8 +28,8 @@ public DefaultDevPilotInlay(Disposable parent) { @Override public Integer getOffset() { return beforeSuffixInlay != null ? beforeSuffixInlay.getOffset() : - afterSuffixInlay != null ? afterSuffixInlay.getOffset() : - blockInlay != null ? blockInlay.getOffset() : null; + afterSuffixInlay != null ? afterSuffixInlay.getOffset() : + blockInlay != null ? blockInlay.getOffset() : null; } @Override @@ -80,17 +80,20 @@ public void render(Editor editor, DevPilotCompletion completion, int offset) { int endIndex = firstLine.indexOf(completion.getOldSuffix()); RenderingInstructions instructions = InlineStringProcessor.determineRendering(lines, completion.getOldSuffix()); - + boolean needHintInBlock = true; switch (instructions.getFirstLine()) { case NoSuffix: - renderNoSuffix(editor, firstLine, completion, offset); + needHintInBlock = false; + renderNoSuffix(editor, firstLine, completion, offset, lines.size() > 1); break; case SuffixOnly: + needHintInBlock = false; renderAfterSuffix(endIndex, completion, firstLine, editor, offset); break; case BeforeAndAfterSuffix: + needHintInBlock = false; renderBeforeSuffix(firstLine, endIndex, editor, completion, offset); renderAfterSuffix(endIndex, completion, firstLine, editor, offset); break; @@ -101,7 +104,7 @@ public void render(Editor editor, DevPilotCompletion completion, int offset) { if (instructions.shouldRenderBlock()) { List otherLines = lines.stream().skip(1).collect(Collectors.toList()); - renderBlock(otherLines, editor, completion, offset); + renderBlock(otherLines, editor, completion, offset, needHintInBlock); } if (instructions.getFirstLine() != FirstLineRendering.None) { @@ -109,9 +112,9 @@ public void render(Editor editor, DevPilotCompletion completion, int offset) { } } - private void renderBlock(List lines, Editor editor, DevPilotCompletion completion, int offset) { + private void renderBlock(List lines, Editor editor, DevPilotCompletion completion, int offset, boolean needHintInBlock) { BlockElementRenderer blockElementRenderer = new BlockElementRenderer(editor, lines, completion.getCompletionMetadata() != null ? - completion.getCompletionMetadata().getIsDeprecated() : false); + completion.getCompletionMetadata().getIsDeprecated() : false, needHintInBlock); Inlay element = editor.getInlayModel().addBlockElement(offset, true, false, 1, blockElementRenderer); if (element != null) { Disposer.register(this, element); @@ -131,17 +134,22 @@ private void renderBeforeSuffix(String firstLine, int endIndex, Editor editor, D beforeSuffixInlay = renderInline(editor, beforeSuffix, completion, offset); } - private void renderNoSuffix(Editor editor, String firstLine, DevPilotCompletion completion, int offset) { - beforeSuffixInlay = renderInline(editor, firstLine, completion, offset); + private void renderNoSuffix(Editor editor, String firstLine, DevPilotCompletion completion, int offset, boolean needHint) { + beforeSuffixInlay = renderInline(editor, firstLine, completion, offset, needHint); } - private Inlay renderInline(Editor editor, String before, DevPilotCompletion completion, int offset) { + private Inlay renderInline(Editor editor, String before, DevPilotCompletion completion, int offset, boolean needHint) { InlineElementRenderer element = new InlineElementRenderer(editor, before, completion.getCompletionMetadata() != null ? - completion.getCompletionMetadata().getIsDeprecated() : false); + completion.getCompletionMetadata().getIsDeprecated() : false, needHint); Inlay inlay = editor.getInlayModel().addInlineElement(offset, true, element); if (inlay != null) { Disposer.register(this, inlay); } return inlay; } + + private Inlay renderInline(Editor editor, String before, DevPilotCompletion completion, int offset) { + return this.renderInline(editor, before, completion, offset, false); + } + } diff --git a/src/main/java/com/zhongan/devpilot/completions/inline/render/GraphicsUtils.java b/src/main/java/com/zhongan/devpilot/completions/inline/render/GraphicsUtils.java index 574c89d0..c5452daf 100644 --- a/src/main/java/com/zhongan/devpilot/completions/inline/render/GraphicsUtils.java +++ b/src/main/java/com/zhongan/devpilot/completions/inline/render/GraphicsUtils.java @@ -1,20 +1,49 @@ package com.zhongan.devpilot.completions.inline.render; +import com.intellij.openapi.application.ApplicationInfo; import com.intellij.openapi.editor.Editor; +import com.intellij.openapi.editor.colors.EditorColorsScheme; import com.intellij.openapi.editor.colors.EditorFontType; import com.intellij.psi.PsiDocumentManager; import com.intellij.psi.codeStyle.CommonCodeStyleSettings; import com.intellij.ui.JBColor; +import com.intellij.util.ui.UIUtil; import java.awt.Color; import java.awt.Font; import java.awt.font.TextAttribute; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; import java.util.HashMap; import java.util.Map; +import javax.swing.JTextArea; + +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + import static java.awt.font.TextAttribute.POSTURE_OBLIQUE; public class GraphicsUtils { + + private static final @Nullable Method getEditorFontSize2DMethod; + + private static Font editFont; + + static { + Method method = null; + if (ApplicationInfo.getInstance().getBuild().getBaselineVersion() >= 221) { + try { + method = EditorColorsScheme.class.getMethod("getEditorFontSize2D"); + } catch (NoSuchMethodException var2) { + } + } + + getEditorFontSize2DMethod = method; + JTextArea area = new JTextArea(); + editFont = area.getFont(); + } + public static Font getFont(Editor editor, boolean deprecated) { Font font = editor.getColorsScheme().getFont(EditorFontType.ITALIC); @@ -38,6 +67,25 @@ public static Font getFont(Editor editor, boolean deprecated) { return new Font(attributes); } + public static Font getFont(@NotNull Editor editor, @NotNull String text) { + + Font font = editor.getColorsScheme().getFont(EditorFontType.PLAIN).deriveFont(2); + Font fallbackFont = UIUtil.getFontWithFallbackIfNeeded(font, text); + return fallbackFont.deriveFont(fontSize(editor)); + } + + public static float fontSize(@NotNull Editor editor) { + + EditorColorsScheme scheme = editor.getColorsScheme(); + if (getEditorFontSize2DMethod != null) { + try { + return (Float) getEditorFontSize2DMethod.invoke(scheme); + } catch (InvocationTargetException | IllegalAccessException var3) { + } + } + return (float) scheme.getEditorFontSize(); + } + public static Color getColor() { return new Color(niceContrastColor().getRGB()); } diff --git a/src/main/java/com/zhongan/devpilot/completions/inline/render/InlineElementRenderer.java b/src/main/java/com/zhongan/devpilot/completions/inline/render/InlineElementRenderer.java index f42a39e8..b5e18fe9 100644 --- a/src/main/java/com/zhongan/devpilot/completions/inline/render/InlineElementRenderer.java +++ b/src/main/java/com/zhongan/devpilot/completions/inline/render/InlineElementRenderer.java @@ -4,6 +4,7 @@ import com.intellij.openapi.editor.EditorCustomElementRenderer; import com.intellij.openapi.editor.Inlay; import com.intellij.openapi.editor.markup.TextAttributes; +import com.zhongan.devpilot.completions.inline.CompletionPreview; import java.awt.Color; import java.awt.Graphics; @@ -20,16 +21,20 @@ public class InlineElementRenderer implements EditorCustomElementRenderer { private Color color; - public InlineElementRenderer(Editor editor, String suffix, boolean deprecated) { + public InlineElementRenderer(Editor editor, String suffix, boolean deprecated, boolean needHint) { this.editor = editor; - this.suffix = suffix; + this.suffix = needHint ? suffix + " " + CompletionPreview.byLineAcceptHintText() : suffix; this.deprecated = deprecated; } + public InlineElementRenderer(Editor editor, String suffix, boolean deprecated) { + this(editor, suffix, deprecated, false); + } + @Override public int calcWidthInPixels(Inlay inlay) { return editor.getContentComponent() - .getFontMetrics(GraphicsUtils.getFont(editor, deprecated)).stringWidth(suffix); + .getFontMetrics(GraphicsUtils.getFont(editor, suffix)).stringWidth(suffix); } @TestOnly @@ -41,7 +46,7 @@ public String getContent() { public void paint(Inlay inlay, Graphics g, Rectangle targetRegion, TextAttributes textAttributes) { color = color != null ? color : GraphicsUtils.getColor(); g.setColor(color); - g.setFont(GraphicsUtils.getFont(editor, deprecated)); + g.setFont(GraphicsUtils.getFont(editor, suffix)); g.drawString(suffix, targetRegion.x, targetRegion.y + editor.getAscent()); } } diff --git a/src/main/java/com/zhongan/devpilot/completions/prediction/DevPilotCompletion.java b/src/main/java/com/zhongan/devpilot/completions/prediction/DevPilotCompletion.java index 5563309a..b8664318 100644 --- a/src/main/java/com/zhongan/devpilot/completions/prediction/DevPilotCompletion.java +++ b/src/main/java/com/zhongan/devpilot/completions/prediction/DevPilotCompletion.java @@ -1,6 +1,7 @@ package com.zhongan.devpilot.completions.prediction; import com.intellij.codeInsight.lookup.impl.LookupCellRenderer; +import com.intellij.openapi.editor.Editor; import com.intellij.openapi.util.TextRange; import com.intellij.util.containers.FList; import com.zhongan.devpilot.completions.Completion; @@ -13,9 +14,13 @@ import org.jetbrains.annotations.Nullable; +import static com.zhongan.devpilot.completions.inline.CompletionPreviewUtils.shouldRemoveSuffix; + public class DevPilotCompletion implements Completion { public final String id; + public Editor editor; + public final String oldPrefix; public final String newPrefix; @@ -37,17 +42,21 @@ public class DevPilotCompletion implements Completion { private String fullSuffix = null; + LineStateItems lineStateItems; + public DevPilotCompletion( - String id, - String oldPrefix, - String newPrefix, - String oldSuffix, - String newSuffix, - int index, - String cursorPrefix, - String cursorSuffix, - @Nullable CompletionMetadata completionMetadata, - SuggestionTrigger suggestionTrigger) { + Editor editor, + String id, + String oldPrefix, + String newPrefix, + String oldSuffix, + String newSuffix, + int index, + String cursorPrefix, + String cursorSuffix, + @Nullable CompletionMetadata completionMetadata, + SuggestionTrigger suggestionTrigger) { + this.editor = editor; this.id = id; this.oldPrefix = oldPrefix; this.newPrefix = newPrefix; @@ -58,20 +67,185 @@ public DevPilotCompletion( this.cursorSuffix = cursorSuffix; this.completionMetadata = completionMetadata; this.suggestionTrigger = suggestionTrigger; + lineStateItems = new LineStateItems(); + init(); } public DevPilotCompletion createAdjustedCompletion(String oldPrefix, String cursorPrefix) { return new DevPilotCompletion( - this.id, - oldPrefix, - this.newPrefix, - this.oldSuffix, - this.newSuffix, - this.index, - cursorPrefix, - this.cursorSuffix, - this.completionMetadata, - this.suggestionTrigger); + this.editor, + this.id, + oldPrefix, + this.newPrefix, + this.oldSuffix, + this.newSuffix, + this.index, + cursorPrefix, + this.cursorSuffix, + this.completionMetadata, + this.suggestionTrigger); + } + + private void init() { + splitLines(prepare(this.getSuffix())); + } + + private String prepare(String suffix) { + int cursorOffset = editor.getCaretModel().getOffset(); + if (shouldRemoveSuffix(this)) { + editor.getDocument().deleteString(cursorOffset, cursorOffset + this.oldSuffix.length()); + } + return suffix; + } + + public static class LineStateItems { + + private List lineStates; + + private int index; + + public void clear() { + lineStates.clear(); + index = 0; + } + + public void acceptLine(int index, int offset) { + if (index < 0 || index >= lineStates.size()) { + return; + } + lineStates.get(index).setAccepted(true); + lineStates.get(index).setOffset(offset); + } + + public LineState getNextLineState() { + return lineStates.get(index); + } + + public String getBeforeLine() { + return lineStates.get(index - 1).line; + } + + public String getUnacceptedLines() { + List result = new ArrayList<>(); + for (LineState lineState : lineStates) { + if (!lineState.isAccepted()) { + result.add(lineState.line); + } + } + return "\n" + String.join("\n", result); // must add "\n", otherwise block preview will occur change line issue. + } + + public void init(List lineStates) { + this.lineStates = lineStates; + this.index = 0; + } + + public List getLineStates() { + return lineStates; + } + + public void setLineStates(List lineStates) { + this.lineStates = lineStates; + } + + public int getIndex() { + return index; + } + + public void setIndex(int index) { + this.index = index; + } + + public static class LineState { + + private boolean accepted = false; + + private String line; + + private int offset; + + public boolean isAccepted() { + return accepted; + } + + public void setAccepted(boolean accepted) { + this.accepted = accepted; + } + + public String getLine() { + return line; + } + + public void setLine(String line) { + this.line = line; + } + + public int getOffset() { + return offset; + } + + public void setOffset(int offset) { + this.offset = offset; + } + } + } + + private void splitLines(String suffix) { + List res = new ArrayList<>(); + StringBuilder currentLine = new StringBuilder(); + for (int i = 0; i < suffix.length(); i++) { + char c = suffix.charAt(i); + if (c == '\n') { + LineStateItems.LineState lineState = new LineStateItems.LineState(); + lineState.setLine(currentLine.toString()); + lineState.setAccepted(false); + res.add(lineState); + currentLine = new StringBuilder(); + } else { + currentLine.append(c); + } + } + if (currentLine.length() > 0) { + LineStateItems.LineState lineState = new LineStateItems.LineState(); + lineState.setLine(currentLine.toString()); + lineState.setAccepted(false); + res.add(lineState); + } + lineStateItems.init(res); + } + + public LineStateItems.LineState getNextUnacceptLineState() { + return lineStateItems.getLineStates().get(lineStateItems.index); + } + + public String getUnacceptedLines() { + return this.lineStateItems.getUnacceptedLines().substring(1); // skip first \n to avoid extra lines count in accept telemetry + } + + public void acceptLine(int offset) { + lineStateItems.acceptLine(this.lineStateItems.index++, offset); + } + + public int getCurrentCompletionPosition() { + int size = lineStateItems.getLineStates().size(); + if (lineStateItems.getIndex() >= size || lineStateItems.getIndex() <= 0) { + return 0; + } + + return lineStateItems.getLineStates().get(lineStateItems.getIndex() - 1).getOffset(); + } + + public String getCurrentCompletionCode() { + int size = lineStateItems.getLineStates().size(); + if (lineStateItems.getIndex() >= size || lineStateItems.getIndex() <= 0) { + return ""; + } + + return lineStateItems.getLineStates().get(lineStateItems.getIndex() - 1).getLine() + "\n"; + } + + public void clear() { + lineStateItems.clear(); } public String getSuffix() { @@ -166,4 +340,12 @@ public String getFullSuffix() { public void setFullSuffix(String fullSuffix) { this.fullSuffix = fullSuffix; } + + public LineStateItems getLineStateItems() { + return lineStateItems; + } + + public void setLineStateItems(LineStateItems lineStateItems) { + this.lineStateItems = lineStateItems; + } } diff --git a/src/main/java/com/zhongan/devpilot/util/TelemetryUtils.java b/src/main/java/com/zhongan/devpilot/util/TelemetryUtils.java index 511c2a1a..c5a78785 100644 --- a/src/main/java/com/zhongan/devpilot/util/TelemetryUtils.java +++ b/src/main/java/com/zhongan/devpilot/util/TelemetryUtils.java @@ -71,7 +71,7 @@ public static void chatAccept(String id, String acceptLines, String language, Ch sendMessage(url, requestJson); } - public static void completionAccept(String id, PsiFile file) { + public static void completionAccept(String id, PsiFile file, String acceptLines) { if (!isTelemetryTurnOn()) { return; } @@ -87,10 +87,10 @@ public static void completionAccept(String id, PsiFile file) { language = lang.getLanguageName(); } - completionAccept(id, language); + completionAccept(id, language, acceptLines); } - public static void completionAccept(String id, String language) { + public static void completionAccept(String id, String language, String acceptLines) { if (!isTelemetryTurnOn()) { return; } @@ -100,7 +100,7 @@ public static void completionAccept(String id, String language) { language = "text"; } - var completionAcceptRequest = new CompletionAcceptRequest(language.toLowerCase(Locale.ROOT)); + var completionAcceptRequest = new CompletionAcceptRequest(language.toLowerCase(Locale.ROOT), acceptLines); var requestJson = JsonUtils.toJson(completionAcceptRequest); if (requestJson == null) { @@ -201,8 +201,11 @@ public void setActionType(String actionType) { static class CompletionAcceptRequest { private String language; - CompletionAcceptRequest(String language) { + private String acceptLines; + + CompletionAcceptRequest(String language, String acceptLines) { this.language = language; + this.acceptLines = acceptLines; } public String getLanguage() { @@ -212,5 +215,14 @@ public String getLanguage() { public void setLanguage(String language) { this.language = language; } + + public String getAcceptLines() { + return acceptLines; + } + + public void setAcceptLines(String acceptLines) { + this.acceptLines = acceptLines; + } + } } diff --git a/src/main/resources/META-INF/plugin.xml b/src/main/resources/META-INF/plugin.xml index 539b0608..db6f598d 100644 --- a/src/main/resources/META-INF/plugin.xml +++ b/src/main/resources/META-INF/plugin.xml @@ -185,6 +185,12 @@ id="AcceptDevPilotInlineCompletionAction" text="Accept Inline Completion"> + + + + + + diff --git a/src/main/resources/messages/devpilot_en.properties b/src/main/resources/messages/devpilot_en.properties index fbdb4e51..d128dc5a 100644 --- a/src/main/resources/messages/devpilot_en.properties +++ b/src/main/resources/messages/devpilot_en.properties @@ -96,6 +96,8 @@ devpilot.notification.network.setting=Go to Check Setting. devpilot.notification.version.message=Devpilot version is too old, please upgrade. devpilot.notification.upgrade.message=Go to upgrade devpilot +completion.apply.partial.tooltips=Accept by line + devpilot.settings.methodShortcutDisplayModeLabel=Method Shortcut Display Mode devpilot.settings.methodShortcutHidden=Hidden devpilot.settings.methodShortcutInlineDisplay=Inline diff --git a/src/main/resources/messages/devpilot_zh.properties b/src/main/resources/messages/devpilot_zh.properties index 30ecd012..8a12fe68 100644 --- a/src/main/resources/messages/devpilot_zh.properties +++ b/src/main/resources/messages/devpilot_zh.properties @@ -13,9 +13,9 @@ com.zhongan.devpilot.actions.editor.popupmenu.BasicEditorAction=DevPilot devpilot.prompt.text.placeholder=\u8BF7\u8F93\u5165\u60A8\u7684\u95EE\u9898 devpilot.reference.content=\u5F15\u7528\uFF1A -devpilot.action.new.chat=\u6253\u5f00DevPilot\u4f1A\u8BDD +devpilot.action.new.chat=\u6253\u5F00DevPilot\u4F1A\u8BDD devpilot.action.new.chat.desc=\u6253\u5F00\u65B0\u7684DevPilot\u4F1A\u8BDD -devpilot.action.reference.chat=\u5f15\u7528\u4ee3\u7801\u5757 +devpilot.action.reference.chat=\u5F15\u7528\u4EE3\u7801\u5757 devpilot.action.generate.comments=\u884C\u5185\u6CE8\u91CA devpilot.action.generate.method.comments=\u751F\u6210\u65B9\u6CD5\u6CE8\u91CA devpilot.action.generate.tests=\u751F\u6210\u5355\u6D4B @@ -94,7 +94,9 @@ devpilot.notification.network.setting=\u72B6\u6001\u68C0\u67E5\u914D\u7F6E devpilot.notification.version.message=DevPilot\u7248\u672C\u592A\u4F4E, \u8BF7\u5347\u7EA7\u7248\u672C devpilot.notification.upgrade.message=\u53BB\u5347\u7EA7\u63D2\u4EF6 -devpilot.settings.methodShortcutDisplayModeLabel=\u65b9\u6cd5\u5feb\u6377\u952e\u663e\u793a\u65b9\u5f0f -devpilot.settings.methodShortcutHidden=\u4e0d\u663e\u793a -devpilot.settings.methodShortcutInlineDisplay=\u5e73\u94fa -devpilot.settings.methodShortcutGroupDisplay=\u5206\u7ec4\u4e0b\u62c9 \ No newline at end of file +completion.apply.partial.tooltips=\u9010\u884C\u91C7\u7EB3 + +devpilot.settings.methodShortcutDisplayModeLabel=\u65B9\u6CD5\u5FEB\u6377\u952E\u663E\u793A\u65B9\u5F0F +devpilot.settings.methodShortcutHidden=\u4E0D\u663E\u793A +devpilot.settings.methodShortcutInlineDisplay=\u5E73\u94FA +devpilot.settings.methodShortcutGroupDisplay=\u5206\u7EC4\u4E0B\u62C9 \ No newline at end of file