IDEA Plugin 开发经验

​ IDEA 插件开发,主要参考官方的 开发文档 ,里面列举了各种插件开发的扩展点。这里主要记录一些开发过程中的经验。目前开发过的插件类型包括:代码生成/PSI、模板代码创建后代码生成、代码检查/Inspection、Live Template、Postfix。

开发过程中的一些通用经验

  • Enable Internal Mode
    • 可以查看 PsiFile 的结构
    • UI Inspector
  • Explore Api
  • SDK code samples
    • 扩展点 demo
  • 常用接口
    • UnusedSymbolUtil 搜索 element 是否有引用,2022 之后的版本带有 Inlay Usage,可以直接查看 PsiMember 的 Usage Count,具体实现在 JavaTelescope#usagesCount
    • 提示需要带可点击链接,在 Notificationcontent 中带上超链接,并设置 Listener

代码生成

​ 代码(Java)生成主要依赖以下几个接口:

  • 搜索类、接口、包相关:JavaPsiFacade#getInstance

  • 构建 PSI 相关:PsiElementFactory(JavaPsiFacade#getElementFactory),下面是在已有类上新增 Field 的示例代码,其他类型的新增依样画葫芦就行:

    1
    2
    3
    4
    PsiClass existPsiClass = null;
    PsiElementFactory elementFactory = JavaPsiFacade.getElementFactory(project);
    PsiField field = elementFactory.createFieldFromText("public static int helloWorld = 0;", null);
    existPsiClass.add(field);

    解析某个特定的 Class 的 PSI 结构的时候,可以使用 Tools|View PSI structure 帮助解析。(Enable Internal Mode)

模板 Template

​ Template 的新增其实不需要依赖插件,直接在 File|New|Edit Templates… 就可以新增。只是这种方式局限在动态的部分只能使用内置的变量(见:FileTemplateManagerImpl#getDefaultProperties)。如果需要使用自定义变量或者在新建文件后还要做一些后置操作,就需要借助插件开发。

​ 插件中新增一个普通 Template ,只需要在资源目录 resources/fileTemplates 新建 Test.java.ft ,在 New|Class 的目录中,就会出现 Test 的模板了。但是这样并不能实现动态属性。

​ 为了实现动态属性,需要新增一个 Action 继承 CreateFileFromTemplateAction 如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class SenninhaCreateFileAction extends CreateFileFromTemplateAction {
public SenninhaCreateFileAction() {
super("Senninha Class", "Create Senninha Class", CLASS_ICON);
}

@Override
protected void buildDialog(@NotNull Project project, @NotNull PsiDirectory directory, CreateFileFromTemplateDialog.@NotNull Builder builder) {
builder
.setTitle("New Senninha Class")
// 在这里动态新增 模板
.addKind("Senninha", CLASS_ICON, "Senninha")
.addKind("Senninha1", CLASS_ICON, "Senninha1");
}

@Override
protected @NlsContexts.Command String getActionName(PsiDirectory directory, @NonNls @NotNull String newName, @NonNls String templateName) {
return "Create Senninha: " + newName;
}
}

​ 在 plugin.xml 中注册:

1
2
3
<action id="Create.Senninha.Class" class="SenninhaCreateFileAction">
<add-to-group group-id="NewGroup1" anchor="after" relative-to-action="NewClass"/>
</action>

​ 新增俩个模板 Senninha、Senninha1,没有什么特别的东西,只是有个自定义的的变量 WEATHER

1
2
3
4
5
6
package ${PACKAGE_NAME};

// ${WEATHER}
public class ${NAME} {

}

​ 运行插件,在沙箱中 New|Senninha Class 可以看到我们新增的模板,但这个时候,并不能渲染自定义的变量 WEATHER。还需要重写 createFile

1
2
3
4
5
6
7
8
9
10
11
12
@Override
protected PsiFile createFile(String name, String templateName, PsiDirectory dir) {
FileTemplate template = FileTemplateManager.getInstance(dir.getProject()).getTemplate(templateName);
// FileTemplate template = FileTemplateManager.getInstance(dir.getProject()).getInternalTemplate(templateName);
try {
Properties properties = FileTemplateManager.getInstance(dir.getProject()).getDefaultProperties();
properties.setProperty("WEATHER", "Typhoon");
return FileTemplateUtil.createFromTemplate(template, name, properties, dir).getContainingFile();
} catch (Exception e) {
throw new RuntimeException(e);
}
}

​ 这样就可以渲染出我们自定义的 WEATHER 变量了。扩展开来,我们可以在这里检测命名的规范创建后分配命令字 等各种前置后置操作。

​ 如果想要隐藏模板,可以放置在 resources/fileTemplates/internal 目录下,并且在 plugin.xml 目录显式注册模板名:

1
<internalFileTemplate name="Senninha"/>

​ 如果只想在特定目录下 New| 才展示新增的目录,需要重写 isAvailable 方法。

Postfix

​ Postfix 操作,个人感觉是日常开发中最高频使用的操作之一,形如:new String().var -> String s = new String(); 。现在新增一个 PostFix pp ,作用于本地变量,会生成输出在作用域范围内修改过的成员变量。

1
2
3
4
5
6
7
8
9
10
public class TestSenninhaPostfixBean {
int a;
int b;

static {
TestSenninhaPostfixBean bean = new TestSenninhaPostfixBean();
bean.a = 100;
bean.b = 10000;
}
}

Postfix 操作:

1
2
3
4
5
6
static {
TestSenninhaPostfixBean bean = new TestSenninhaPostfixBean();
bean.a = 100;
bean.b = 10000;
bean.pp -> System.out.println("Touch field: a,b");
}
注册新的 PostfixTemplateProvider

​ 注册新的 PostfixTemplateProvider ,在 plugin.xml 中注册:

1
2
<codeInsight.template.postfixTemplateProvider language="JAVA"
implementationClass="io.github.isenninha.postfix.JavaPostfixTemplateProviderImpl"/>

​ 需要关注的以下俩个方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
@Override
public @NotNull Set<PostfixTemplate> getTemplates() {
// 新增自定义的 postfix
Set<PostfixTemplate> set = new HashSet<>();
set.add(new SenninhaPostFix("pp", "pp", "Print touch field.", this));
return set;
}


@Override
public boolean isTerminalSymbol(char currentChar) {
return currentChar == '.'; // dot 符号作为 postfix 标记
}
新增具体的 PostFix

​ 新增 PostFix 继承自 PostfixTemplate ,主要关注以下几个方法:

  • isApplicable:根据传入的 PsiElement 决定是否需要展开 postfix。

  • expand:具体的展开操作。

    具体代码:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    103
    104
    105
    106
    107
    108
    109
    110
    111
    112
    113
    114
    115
    116
    117
    118
    119
    120
    121
    122
    123
    124
    125
    126
    127
    128
    129
    130
    131
    132
    133
    134
    135
    package io.github.isenninha.postfix;

    import com.intellij.codeInsight.hint.HintManager;
    import com.intellij.codeInsight.template.postfix.templates.PostfixTemplate;
    import com.intellij.codeInsight.template.postfix.templates.PostfixTemplateProvider;
    import com.intellij.openapi.editor.Document;
    import com.intellij.openapi.editor.Editor;
    import com.intellij.openapi.util.NlsSafe;
    import com.intellij.openapi.util.Pair;
    import com.intellij.psi.*;
    import com.intellij.psi.util.PsiTreeUtil;
    import io.github.isenninha.example.TestSenninhaPostfixBean;
    import org.jetbrains.annotations.NonNls;
    import org.jetbrains.annotations.NotNull;
    import org.jetbrains.annotations.Nullable;

    import java.util.ArrayList;
    import java.util.List;
    import java.util.stream.Collectors;

    /**
    * @author senninha
    */
    public class SenninhaPostFix extends PostfixTemplate {
    protected SenninhaPostFix(@Nullable @NonNls String id, @NotNull @NlsSafe String name, @NotNull @NlsSafe String example, @Nullable PostfixTemplateProvider provider) {
    super(id, name, example, provider);
    }

    @Override
    public boolean isApplicable(@NotNull PsiElement context, @NotNull Document copyDocument, int newOffset) {
    Pair<PsiElement, PsiType> pair = findStartElementAndType(context);
    if (pair == null) {
    return false;
    }
    return pair.second.getCanonicalText().equals(TestSenninhaPostfixBean.class.getName());
    }

    @Override
    public void expand(@NotNull PsiElement element, @NotNull Editor editor) {
    Pair<PsiElement, PsiType> pair = findStartElementAndType(element);
    if (pair == null) {
    return;
    }
    String localVariableName = element.getText();
    int startOffset = pair.first.getTextOffset();
    int endOffset = element.getTextOffset();
    PsiCodeBlock codeBlock = findCodeBlock(pair.first);
    if (codeBlock == null) {
    return;
    }
    List<PsiIdentifier> modifiedFieldIdentifierList = new ArrayList<>();
    JavaRecursiveElementVisitor visitor = new JavaRecursiveElementVisitor() {
    @Override
    public void visitAssignmentExpression(PsiAssignmentExpression expression) {
    int offset = expression.getTextOffset();
    if (offset < startOffset || offset >= endOffset) {
    return;
    }
    if (!(expression.getLExpression() instanceof PsiReferenceExpression)) {
    return;
    }
    PsiReferenceExpression lExpression = (PsiReferenceExpression) expression.getLExpression();
    if (lExpression.getQualifierExpression() == null) {
    return;
    }
    if (!lExpression.getQualifierExpression().getText().equals(localVariableName)) {
    return;
    }
    PsiIdentifier modifiedFieldIdentifier = PsiTreeUtil.getChildOfType(lExpression, PsiIdentifier.class);
    modifiedFieldIdentifierList.add(modifiedFieldIdentifier);
    }
    };
    codeBlock.accept(visitor);
    if (!modifiedFieldIdentifierList.isEmpty()) {
    HintManager.getInstance().showInformationHint(editor, modifiedFieldIdentifierList.stream().map(PsiIdentifier::getText).collect(Collectors.joining(",")));
    editor.getDocument().replaceString(element.getTextOffset(), element.getTextOffset() + localVariableName.length(),
    String.format("System.out.println(\"Touch field: %s\");",
    modifiedFieldIdentifierList.stream().map(PsiElement::getText).collect(Collectors.joining(","))));
    }
    }

    private static PsiCodeBlock findCodeBlock(PsiElement psiElement) {
    PsiElement parent = psiElement;
    if (parent instanceof PsiLocalVariable) {
    // 最简单的情况 localVariable
    if (parent.getParent().getParent() instanceof PsiCodeBlock) {
    return (PsiCodeBlock) parent.getParent().getParent();
    }
    } else if (parent instanceof PsiParameter) {
    if (parent.getParent() instanceof PsiParameterList) {
    // 方法参数的情况
    parent = parent.getParent();
    PsiElement sibling = parent.getNextSibling();
    while (sibling != null && !(sibling instanceof PsiCodeBlock)) {
    sibling = sibling.getNextSibling();
    }
    if (sibling != null) {
    return (PsiCodeBlock) sibling;
    }
    } else {
    // foreach 的情况
    PsiElement sibling = parent.getNextSibling();
    while (sibling != null && !(sibling instanceof PsiBlockStatement)) {
    sibling = sibling.getNextSibling();
    }
    if (sibling != null) {
    return (PsiCodeBlock) sibling.getChildren()[0];
    }
    }
    }
    return null;
    }

    private static Pair<PsiElement, PsiType> findStartElementAndType(PsiElement element) {
    if (!(element instanceof PsiIdentifier)) {
    return null;
    }
    PsiElement parent = element.getParent();
    if (!(parent instanceof PsiReferenceExpression)) {
    return null;
    }
    PsiReferenceExpression expression = (PsiReferenceExpression) parent;
    PsiElement resolve = expression.resolve();
    if (resolve == null) {
    return null;
    }
    if (resolve instanceof PsiParameter) {
    return new Pair<>(resolve, ((PsiParameter) resolve).getType());
    }
    if (resolve instanceof PsiLocalVariable) {
    return new Pair<>(resolve, ((PsiLocalVariable) resolve).getType());
    }
    return null;
    }
    }

静态代码检查

​ 静态代码检查在官方文档中有个完整的例子:inspection_basics

​ 在实际开发过程中,可以通过 Tools -> View Psi Structures 来查看目标代码的结构,然后根据对应的结构,通过重写 JavaElementVisitor 对应的方法进行扫描检查。其中有一个非常高频的用法是查找对应的结构元素对应的引用,比如:PsiClass、PsiMethod ,可以通过 PsiReference#resolve 调用拿到对应的引用。

TBC