第一部分是来用来监听用户行为,这一部分是监听后,渲染到idea页面的部分。
渲染部分,主要是逻辑就是,根据监听触发渲染后,获取到用户当前的光标位置和光标上下文,将上下文获取到发整合后发送给AI算法部分,获取到AI模型返回的结果后,渲染回显到idea的页面。
首先,获取光标位置:
Caret primaryCaret = editor.getCaretModel().getPrimaryCaret();
//这就是光标的偏移量
int selectionStart = primaryCaret.getSelectionStart();
获取到光标位置后来获取上下文(这个是idea插件获取最简单的方式,暂时先用这个最简单的,后续对代码上下文的拆分会有单独的一部分会单独介绍):
public static String[] getCaretContext(Caret caret, int contextLength) {
Editor editor = caret.getEditor();
Document document = editor.getDocument();
int caretOffset = caret.getOffset();
// 检查是否超出了文档的范围
int start = Math.max(0, caretOffset - contextLength);
int end = Math.min(document.getTextLength(), caretOffset + contextLength);
// 获取光标前后的文本
String beforeText = document.getText(new TextRange(start, caretOffset)).trim();
String caretText = String.valueOf(document.getCharsSequence().charAt(caretOffset)); // 假设我们只关心光标位置的单个字符
String afterText = document.getText(new TextRange(caretOffset, end)).trim();
return new String[]{beforeText, caretText, afterText};
}
这个获取上下文代码中,其实
Editor editor = caret.getEditor();
Document document = editor.getDocument();
int caretOffset = caret.getOffset();
这部分和上文获取光标位置是一致的,都是获取偏移量。
获取到的光标位置和上下文后,整合后发给AI模型,但我们中间包装一次API,统计用量和一些用户验证相关的部分,这个我就先省略掉了,而且大伙还是根据自己项目使用的续写模型自己来整合自己的出入参。
Caret primaryCaret = editor.getCaretModel().getPrimaryCaret();
//当初是简单的获取上下文的2000个偏移量
String[] caretContext = getCaretContext(primaryCaret,2000);
String url = config.autoCompleteUrl;
AutoCompleteVo vo = new AutoCompleteVo();
vo.setBeginCode(stringAtomicReferenceArray.get(0));
vo.setEndCode(stringAtomicReferenceArray.get(2));
String params = JSONObject.toJSONString(vo);
String deviceId = 设备的唯一标识;//我们主要是为了控制每一个用户只能使用一个idea插件,如果换设备,可以重新申请
String json = HttpUtils.sendPostJson(url, params, deviceId);
附上HttpUtils.sendPostJson方法的代码
public static String sendPostJson(String uri, String json, String deviceId) {
String response = "";
try {
URL url = new URL(uri);
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
conn.setRequestMethod("POST");
conn.setRequestProperty("Accept", "application/json");
conn.setRequestProperty("Content-Type", "application/json; charset=utf-8");
// conn.setReadTimeout(1200);
conn.setRequestProperty("deviceId", deviceId);
conn.setConnectTimeout(200);
conn.setDoOutput(true);
String jsonInputString = json;
try (OutputStream os = conn.getOutputStream()) {
byte[] input = jsonInputString.getBytes("utf-8");
os.write(input, 0, input.length);
}
int responseCode = conn.getResponseCode();
log.info("sendPostJson responseCode=" + responseCode);
if (responseCode == HttpURLConnection.HTTP_OK) { // success
try (InputStream is = conn.getInputStream()) {
BufferedReader reader = new BufferedReader(new InputStreamReader(is, "utf-8"));
String line;
while ((line = reader.readLine()) != null) {
response += line;
}
}
}
conn.disconnect();
} catch (Exception e) {
log.error("sendPostJson error", e);
} finally {
return response;
}
}
json是AI模型返回的内容,根据自身返回的内容进行拆解,获取AI模型返回的内容“text”。再接下来就是根据text去渲染到idea插件上。
InlayProperties properties = new InlayProperties();
properties.relatesToPrecedingText(true);
properties.disableSoftWrapping(true);
if (text.split("\n").length > 1) {
Inlay<ContinueMultilineCustomElementRenderer> continueMultilineCustomElementRendererInlay = editor.getInlayModel().addBlockElement(caretOffset , properties, new ContinueMultilineCustomElementRenderer(editor, text));
} else {
Inlay<ContinueCustomElementRenderer> continueCustomElementRendererInlay = editor.getInlayModel().addInlineElement(caretOffset, properties, new ContinueCustomElementRenderer(editor, text));
}
这两个会根据返回的内容来判断是否是多行,如果是多行,使用多行渲染。如果是单行进行单行渲染。渲染方法也是idea插件自己提供的。其中caretOffset是光标位置。还有ContinueMultilineCustomElementRenderer和ContinueCustomElementRenderer,这个类就是渲染内容的类,一个是多行渲染和单行渲染。
ContinueCustomElementRenderer(单行渲染):
package com.xxx.action.autocomplete;
import com.intellij.openapi.actionSystem.ActionGroup;
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.EditorImpl;
import com.intellij.openapi.editor.impl.FontInfo;
import com.intellij.openapi.editor.markup.GutterIconRenderer;
import com.intellij.openapi.editor.markup.TextAttributes;
import com.intellij.ui.JBColor;
import org.jetbrains.annotations.NonNls;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.awt.*;
import java.awt.font.FontRenderContext;
import java.awt.font.GlyphVector;
import java.awt.geom.Rectangle2D;
/**
* 自定义的EditorCustomElementRenderer
* 主要用于编辑器单行续写内容的渲染
* @Date: 2024年06月13日 11:16
*/
public class ContinueCustomElementRenderer implements EditorCustomElementRenderer{
private final Editor editor;
private final String text;
public ContinueCustomElementRenderer(Editor editor, String text) {
this.editor = editor;
this.text = text;
}
private Font getFont() {
Font editorFont = editor.getColorsScheme().getFont(EditorFontType.PLAIN);
Font derivedFont = editorFont.deriveFont(Font.PLAIN);
return derivedFont != null ? derivedFont : editorFont;
}
@Override
public int calcWidthInPixels(@NotNull Inlay inlay) {
EditorImpl editorImpl = (EditorImpl) inlay.getEditor();
return editorImpl.getFontMetrics(Font.PLAIN).stringWidth(text);
}
@Override
public int calcHeightInPixels(@NotNull Inlay inlay) {
return EditorCustomElementRenderer.super.calcHeightInPixels(inlay);
}
@Override
public void paint(@NotNull Inlay inlay, @NotNull Graphics g, @NotNull Rectangle targetRegion, @NotNull TextAttributes textAttributes) {
Graphics2D g2d = (Graphics2D) g.create();
g2d.setColor(JBColor.GRAY);
g2d.setFont(getFont());
g2d.drawString(text, targetRegion.x, targetRegion.y + offsetY());
g2d.dispose();
}
private int offsetY() {
FontRenderContext fontRenderContext = FontInfo.getFontRenderContext(editor.getContentComponent());
GlyphVector glyphVector = getFont().createGlyphVector(fontRenderContext, text);
Rectangle2D visualBounds = glyphVector.getVisualBounds();
double fontHeight = visualBounds.getHeight();
double height = (editor.getLineHeight() + fontHeight) / 2;
return (int) Math.ceil(height);
}
@Override
public void paint(@NotNull Inlay inlay, @NotNull Graphics2D g, @NotNull Rectangle2D targetRegion, @NotNull TextAttributes textAttributes) {
EditorCustomElementRenderer.super.paint(inlay, g, targetRegion, textAttributes);
}
@Override
public @Nullable @NonNls String getContextMenuGroupId(@NotNull Inlay inlay) {
return EditorCustomElementRenderer.super.getContextMenuGroupId(inlay);
}
@Override
public @Nullable ActionGroup getContextMenuGroup(@NotNull Inlay inlay) {
return EditorCustomElementRenderer.super.getContextMenuGroup(inlay);
}
@Override
public @Nullable GutterIconRenderer calcGutterIconRenderer(@NotNull Inlay inlay) {
return EditorCustomElementRenderer.super.calcGutterIconRenderer(inlay);
}
}
ContinueMultilineCustomElementRenderer(多行渲染):
package com.xxx.action.autocomplete;
import com.intellij.openapi.actionSystem.ActionGroup;
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.EditorImpl;
import com.intellij.openapi.editor.impl.FontInfo;
import com.intellij.openapi.editor.markup.GutterIconRenderer;
import com.intellij.openapi.editor.markup.TextAttributes;
import com.intellij.ui.JBColor;
import org.jetbrains.annotations.NonNls;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.awt.*;
import java.awt.font.FontRenderContext;
import java.awt.font.GlyphVector;
import java.awt.geom.Rectangle2D;
/**
* 自定义的EditorCustomElementRenderer
* 主要用于编辑器多行续写内容的渲染
* @Date: 2024年06月13日 11:29
*/
public class ContinueMultilineCustomElementRenderer implements EditorCustomElementRenderer{
private final Editor editor;
private final String text;
public ContinueMultilineCustomElementRenderer(Editor editor, String text) {
this.editor = editor;
this.text = text;
}
private Font getFont() {
Font editorFont = editor.getColorsScheme().getFont(EditorFontType.PLAIN);
Font derivedFont = editorFont.deriveFont(Font.PLAIN);
return derivedFont != null ? derivedFont : editorFont;
}
private int offsetY() {
FontRenderContext fontRenderContext = FontInfo.getFontRenderContext(editor.getContentComponent());
GlyphVector glyphVector = getFont().createGlyphVector(fontRenderContext, text);
Rectangle2D visualBounds = glyphVector.getVisualBounds();
double fontHeight = visualBounds.getHeight();
double height = (editor.getLineHeight() + fontHeight) / 2;
return (int) Math.ceil(height);
}
private int offsetX() {
int currentColumn = editor.getCaretModel().getPrimaryCaret().getLogicalPosition().column;
FontRenderContext fontRenderContext = FontInfo.getFontRenderContext(editor.getContentComponent());
GlyphVector glyphVector = getFont().createGlyphVector(fontRenderContext, text);
Rectangle2D visualBounds = glyphVector.getVisualBounds();
double fontWidth = visualBounds.getWidth();
int widthBeforeCaret = ((EditorImpl) editor).getFontMetrics(Font.PLAIN).stringWidth(text.substring(0, currentColumn));
int horizontalScrollOffset = ((EditorImpl) editor).getScrollingModel().getHorizontalScrollOffset();
return Math.max(0, widthBeforeCaret - horizontalScrollOffset);
}
@Override
public int calcWidthInPixels(@NotNull Inlay inlay) {
String[] lines = text.split("\\r?\\n");
int maxLen = 0;
for (String line : lines) {
int len = ((EditorImpl) inlay.getEditor()).getFontMetrics(Font.PLAIN).stringWidth(line);
maxLen = Math.max(maxLen, len);
}
return maxLen;
}
@Override
public int calcHeightInPixels(@NotNull Inlay inlay) {
String[] lines = text.split("\\r?\\n");
return ((EditorImpl) inlay.getEditor()).getLineHeight() * lines.length;
}
@Override
public void paint(@NotNull Inlay inlay, @NotNull Graphics g, @NotNull Rectangle targetRegion, @NotNull TextAttributes textAttributes) {
Graphics2D g2d = (Graphics2D) g.create();
g2d.setColor(JBColor.GRAY);
g2d.setFont(getFont());
int additionalYOffset = -editor.getLineHeight();
boolean isFirstLine = true;
String[] lines = text.split("\\r?\\n");
for (String line : lines) {
int x = isFirstLine ? targetRegion.x + offsetX() : targetRegion.x;
int y = targetRegion.y + offsetY() + additionalYOffset;
g2d.drawString(line, x, y);
additionalYOffset += editor.getLineHeight();
isFirstLine = false;
}
}
@Override
public void paint(@NotNull Inlay inlay, @NotNull Graphics2D g, @NotNull Rectangle2D targetRegion, @NotNull TextAttributes textAttributes) {
EditorCustomElementRenderer.super.paint(inlay, g, targetRegion, textAttributes);
}
@Override
public @Nullable @NonNls String getContextMenuGroupId(@NotNull Inlay inlay) {
return EditorCustomElementRenderer.super.getContextMenuGroupId(inlay);
}
@Override
public @Nullable ActionGroup getContextMenuGroup(@NotNull Inlay inlay) {
return EditorCustomElementRenderer.super.getContextMenuGroup(inlay);
}
@Override
public @Nullable GutterIconRenderer calcGutterIconRenderer(@NotNull Inlay inlay) {
return EditorCustomElementRenderer.super.calcGutterIconRenderer(inlay);
}
}
以上就是获取光标位置,然后获取上下文,在请求到AI模型后渲染到idea编辑器中。