Code Vision Hints是idea Inlay提示中的一种类型,它只能提供block类型的inlay,可以把它添加到字段、方法、类等上面,一个元素如果包含多个提示的话,这些inlay会被展示在同一行上。
Code vision hints可以展示在元素的上面、右边、或者行末尾,具体展示的位置可以在IDE中修改:Preferences | Editor | Inlay Hints | Code vision。
目前已经有许多的插件都使用了Inlay,例如:
有两个扩展点可以用于实现code vision:
目前在2022.2这个版本测试中,发现CodeVisionProvider有很多bug,很多废弃的方法也需要实现,也许在新版本已经解决了这个问题,如果你依赖IDE版本较老,建议还是直接实现DaemonBoundCodeVisionProvider。
public class MyCodeVisionProvider implements DaemonBoundCodeVisionProvider {
public static final String GROUP_ID = "com.demo";
public static final String ID = "myPlugin";
public static final String NAME = "my plugin";
@NotNull
@Override
public CodeVisionAnchorKind getDefaultAnchor() {
// 默认展示在元素的顶部
return CodeVisionAnchorKind.Top;
}
@NotNull
@Override
public String getId() {
return ID;
}
@NotNull
@Override
public String getGroupId() {
return GROUP_ID;
}
@Nls
@NotNull
@Override
public String getName() {
return NAME;
}
@NotNull
@Override
public List<CodeVisionRelativeOrdering> getRelativeOrderings() {
// 设置展示顺序为第一个
return List.of(CodeVisionRelativeOrdering.CodeVisionRelativeOrderingFirst.INSTANCE);
}
// 设置展示场景:java文件的方法上展示
@NotNull
@Override
public List<Pair<TextRange, CodeVisionEntry>> computeForEditor(@NotNull Editor editor, @NotNull PsiFile file) {
List<Pair<TextRange, CodeVisionEntry>> lenses = new ArrayList<>();
String languageId = file.getLanguage().getID();
if (!"JAVA".equalsIgnoreCase(languageId)) {
return lenses;
}
SyntaxTraverser<PsiElement> traverser = SyntaxTraverser.psiTraverser(file);
for (PsiElement element : traverser) {
if (!(element instanceof PsiMethod)) {
continue;
}
if (!InlayHintsUtils.isFirstInLine(element)) {
continue;
}
String hint = getName();
TextRange range = InlayHintsUtils.INSTANCE.getTextRangeWithoutLeadingCommentsAndWhitespaces(element);
lenses.add(new Pair(range, new ClickableTextCodeVisionEntry(hint, getId()
, new MyClickHandler((PsiMethod) element), null, hint, "", List.of())));
}
return lenses;
}
@NotNull
@Override
@Deprecated
public List<Pair<TextRange, CodeVisionEntry>> computeForEditor(@NotNull Editor editor) {
// 过时方法,不用实现
return List.of();
}
// Inlay被点击后的处理逻辑
@Override
public void handleClick(@NotNull Editor editor, @NotNull TextRange textRange, @NotNull CodeVisionEntry entry) {
if (entry instanceof CodeVisionPredefinedActionEntry) {
((CodeVisionPredefinedActionEntry)entry).onClick(editor);
}
}
@RequiredArgsConstructor
static class MyClickHandler implements Function2<MouseEvent, Editor, Unit> {
private final PsiMethod psiMethod;
// 点击inlay后的响应:打开一个popup显示一组菜单
public Unit invoke(MouseEvent event, Editor editor) {
TextRange range = InlayHintsUtils.INSTANCE.getTextRangeWithoutLeadingCommentsAndWhitespaces(psiMethod);
int startOffset = range.getStartOffset();
int endOffset = range.getEndOffset();
editor.getSelectionModel().setSelection(startOffset, endOffset);
AnAction action1 = ActionManager.getInstance().getAction("MyPlugin.Action1");
AnAction action2 = ActionManager.getInstance().getAction("MyPlugin.Action2");
DefaultActionGroup actionGroup = new DefaultActionGroup(List.of(action1, action2));
ListPopup popup = JBPopupFactory.getInstance().createActionGroupPopup(null, actionGroup
, EditorUtil.getEditorDataContext(editor), JBPopupFactory.ActionSelectionAid.SPEEDSEARCH, true);
popup.show(new RelativePoint(event));
return null;
}
}
}
<idea-plugin>
<extensions defaultExtensionNs="com.intellij">
<codeInsight.daemonBoundCodeVisionProvider implementation="com.demo.MyCodeVisionProvider"/>
extensions>
idea-plugin>
public class MyCodeVisionProvider implements CodeVisionProvider<Unit> {
public static final String GROUP_ID = "com.demo";
public static final String ID = "myPlugin";
public static final String NAME = "my plugin";
private static final Key<Long> MODIFICATION_STAMP_KEY = Key.create("myPlugin.modificationStamp");
private static final Key<Integer> MODIFICATION_STAMP_COUNT_KEY = KeyWithDefaultValue.create("myPlugin.modificationStampCount", 0);
// 每次文档事件都会调用2次shouldRecomputeForEditor,这里设置4因为编辑器刚打开的时候,没关闭的文件首次刷新可能导致渲染了宽度为0的Inlay。
private static final int MAX_MODIFICATION_STAMP_COUNT = 4;
@NotNull
@Override
public CodeVisionAnchorKind getDefaultAnchor() {
return CodeVisionAnchorKind.Top;
}
@NotNull
@Override
public String getGroupId() {
return GROUP_ID;
}
@NotNull
@Override
public String getId() {
return ID;
}
@Nls
@NotNull
@Override
public String getName() {
return NAME;
}
@NotNull
@Override
public List<CodeVisionRelativeOrdering> getRelativeOrderings() {
return List.of(CodeVisionRelativeOrdering.CodeVisionRelativeOrderingFirst.INSTANCE);
}
@NotNull
@Override
@Deprecated(message = "use getPlaceholderCollector")
// 已被废弃,不用实现
public List<TextRange> collectPlaceholders(@NotNull Editor editor) {
return List.of();
}
@Nullable
@Override
// 不需要实现,computeCodeVision做了相同的事情
public CodeVisionPlaceholderCollector getPlaceholderCollector(@NotNull Editor editor, @Nullable PsiFile psiFile) {
return null;
}
@NotNull
@Override
@Deprecated(message = "Use computeCodeVision instead")
// 已被废弃,不用实现
public List<Pair<TextRange, CodeVisionEntry>> computeForEditor(@NotNull Editor editor, Unit uiData) {
return List.of();
}
@NotNull
@Override
public CodeVisionState computeCodeVision(@NotNull Editor editor, Unit uiData) {
List<PsiMethod> psiMethods = getPsiMethods(editor);
List<Pair<TextRange, CodeVisionEntry>> lenses = new ArrayList<>();
for (PsiMethod psiMethod : psiMethods) {
TextRange range = InlayHintsUtils.INSTANCE.getTextRangeWithoutLeadingCommentsAndWhitespaces(psiMethod);
MyClickHandler handler = new MyClickHandler(psiMethod);
CodeVisionEntry entry = new ClickableTextCodeVisionEntry(getName(), getId()
, handler, null, getName(), getName(), List.of());
lenses.add(new Pair<>(range, entry));
}
return new CodeVisionState.Ready(lenses);
}
private List<PsiMethod> getPsiMethods(Editor editor) {
return ApplicationManager.getApplication().runReadAction((Computable<List<PsiMethod>>) () -> {
List<PsiMethod> psiMethods = new ArrayList<>();
PsiFile psiFile = PsiDocumentManager.getInstance(Objects.requireNonNull(editor.getProject()))
.getPsiFile(editor.getDocument());
if (psiFile == null) {
return psiMethods;
}
List<Pair<TextRange, CodeVisionEntry>> lenses = new ArrayList<>();
SyntaxTraverser<PsiElement> traverser = SyntaxTraverser.psiTraverser(psiFile);
for (PsiElement element : traverser) {
if (!(element instanceof PsiMethod)) {
continue;
}
if (!InlayHintsUtils.isFirstInLine(element)) {
continue;
}
psiMethods.add((PsiMethod)element);
}
return psiMethods;
});
}
@Override
public void handleClick(@NotNull Editor editor, @NotNull TextRange textRange, @NotNull CodeVisionEntry entry) {
if (entry instanceof CodeVisionPredefinedActionEntry) {
((CodeVisionPredefinedActionEntry)entry).onClick(editor);
}
}
@Override
public void handleExtraAction(@NotNull Editor editor, @NotNull TextRange textRange, @NotNull String s) {}
@Override
public Unit precomputeOnUiThread(@NotNull Editor editor) {
return null;
}
@Override
public boolean shouldRecomputeForEditor(@NotNull Editor editor, @Nullable Unit uiData) {
return ApplicationManager.getApplication().runReadAction((Computable<Boolean>) () -> {
if (editor.isDisposed() || !editor.isInsertMode()) {
return false;
}
Project project = editor.getProject();
if (project == null) {
return false;
}
Document document = editor.getDocument();
PsiFile psiFile = PsiDocumentManager.getInstance(project).getPsiFile(document);
if (psiFile == null) {
return false;
}
String languageId = psiFile.getLanguage().getID();
if (!"JAVA".equalsIgnoreCase(languageId)) {
return false;
}
Long prevStamp = MODIFICATION_STAMP_KEY.get(editor);
long nowStamp = getDocumentStamp(editor.getDocument());
if (prevStamp == null || prevStamp != nowStamp) {
Integer count = MODIFICATION_STAMP_COUNT_KEY.get(editor);
if (count + 1 < MAX_MODIFICATION_STAMP_COUNT) {
MODIFICATION_STAMP_COUNT_KEY.set(editor, count + 1);
return true;
} else {
MODIFICATION_STAMP_COUNT_KEY.set(editor, 0);
MODIFICATION_STAMP_KEY.set(editor, nowStamp);
return true;
}
}
return false;
});
}
private static long getDocumentStamp(@NotNull Document document) {
if (document instanceof DocumentEx) {
return ((DocumentEx)document).getModificationSequence();
}
return document.getModificationStamp();
}
@RequiredArgsConstructor
static class MyClickHandler implements Function2<MouseEvent, Editor, Unit> {
private final PsiMethod psiMethod;
public Unit invoke(MouseEvent event, Editor editor) {
TextRange range = InlayHintsUtils.INSTANCE.getTextRangeWithoutLeadingCommentsAndWhitespaces(psiMethod);
int startOffset = range.getStartOffset();
int endOffset = range.getEndOffset();
editor.getSelectionModel().setSelection(startOffset, endOffset);
AnAction action1 = ActionManager.getInstance().getAction("MyPlugin.Action1");
AnAction action2 = ActionManager.getInstance().getAction("MyPlugin.Action2");
DefaultActionGroup actionGroup = new DefaultActionGroup(List.of(action1, action2));
ListPopup popup = JBPopupFactory.getInstance().createActionGroupPopup(null, actionGroup
, EditorUtil.getEditorDataContext(editor), JBPopupFactory.ActionSelectionAid.SPEEDSEARCH, true);
popup.show(new RelativePoint(event));
return null;
}
}
}
<idea-plugin>
<extensions defaultExtensionNs="com.intellij">
<codeInsight.codeVisionProvider implementation="com.demo.MyCodeVisionProvider"/>
extensions>
idea-plugin>
Code Vision Provider