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
。- 提示需要带可点击链接,在 Notification 的 content 中带上超链接,并设置 Listener。
代码生成
代码(Java)生成主要依赖以下几个接口:
搜索类、接口、包相关:
JavaPsiFacade#getInstance
构建 PSI 相关:
PsiElementFactory(JavaPsiFacade#getElementFactory)
,下面是在已有类上新增 Field 的示例代码,其他类型的新增依样画葫芦就行:1
2
3
4PsiClass 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 | public class SenninhaCreateFileAction extends CreateFileFromTemplateAction { |
在 plugin.xml
中注册:
1 | <action id="Create.Senninha.Class" class="SenninhaCreateFileAction"> |
新增俩个模板 Senninha、Senninha1,没有什么特别的东西,只是有个自定义的的变量 WEATHER
:
1 | package ${PACKAGE_NAME}; |
运行插件,在沙箱中 New|Senninha Class 可以看到我们新增的模板,但这个时候,并不能渲染自定义的变量 WEATHER
。还需要重写 createFile :
1 |
|
这样就可以渲染出我们自定义的 WEATHER
变量了。扩展开来,我们可以在这里检测命名的规范、创建后分配命令字 等各种前置后置操作。
如果想要隐藏模板,可以放置在 resources/fileTemplates/internal
目录下,并且在 plugin.xml
目录显式注册模板名:
1 | <internalFileTemplate name="Senninha"/> |
如果只想在特定目录下 New| 才展示新增的目录,需要重写 isAvailable
方法。
Postfix
Postfix 操作,个人感觉是日常开发中最高频使用的操作之一,形如:new String().var -> String s = new String();
。现在新增一个 PostFix pp ,作用于本地变量,会生成输出在作用域范围内修改过的成员变量。
1 | public class TestSenninhaPostfixBean { |
Postfix 操作:
1 | static { |
注册新的 PostfixTemplateProvider
注册新的 PostfixTemplateProvider ,在 plugin.xml 中注册:
1 | <codeInsight.template.postfixTemplateProvider language="JAVA" |
需要关注的以下俩个方法:
1 |
|
新增具体的 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
135package 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( String id, String name, String example, PostfixTemplateProvider provider){
super(id, name, example, provider);
}
public boolean isApplicable(int newOffset) PsiElement context, Document copyDocument, {
Pair<PsiElement, PsiType> pair = findStartElementAndType(context);
if (pair == null) {
return false;
}
return pair.second.getCanonicalText().equals(TestSenninhaPostfixBean.class.getName());
}
public void expand( PsiElement element, 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() {
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 调用拿到对应的引用。