IntelliJ插件开发-Code Vision Hints

简介

Code Vision Hints是idea Inlay提示中的一种类型,它只能提供block类型的inlay,可以把它添加到字段、方法、类等上面,一个元素如果包含多个提示的话,这些inlay会被展示在同一行上。

Code vision hints可以展示在元素的上面、右边、或者行末尾,具体展示的位置可以在IDE中修改:Preferences | Editor | Inlay Hints | Code vision。

目前已经有许多的插件都使用了Inlay,例如:

  • Java代码中,会在链式调用的每行展示返回类型信息
  • 版本控制的项目中,会展示提交者信息

有两个扩展点可以用于实现code vision:

  • DaemonBoundCodeVisionProvider : PSI改变后会得到通知,例如usages,其他文件变动后会继续计算被使用信息
  • CodeVisionProvider : 不依赖PSI改变通知,例如git的提交信息。

目前在2022.2这个版本测试中,发现CodeVisionProvider有很多bug,很多废弃的方法也需要实现,也许在新版本已经解决了这个问题,如果你依赖IDE版本较老,建议还是直接实现DaemonBoundCodeVisionProvider。

代码示例

实现DaemonBoundCodeVisionProvider
  1. 新建VisionProvider的实现类
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;
        }
    }
}
  1. 注册VisionProvider的实现类
<idea-plugin>
	<extensions defaultExtensionNs="com.intellij">
		<codeInsight.daemonBoundCodeVisionProvider implementation="com.demo.MyCodeVisionProvider"/>
	extensions>
idea-plugin>
实现CodeVisionProvider
  1. 新建VisionProvider的实现类
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;
        }
    }
}
  1. 注册CodeVisionProviders实现类
<idea-plugin>
	<extensions defaultExtensionNs="com.intellij">
		<codeInsight.codeVisionProvider implementation="com.demo.MyCodeVisionProvider"/>
	extensions>
idea-plugin>

参考文献

Code Vision Provider

你可能感兴趣的:(#,Intellij插件,intellij,idea)