是五月呀!

IDEA插件开发(九)mybatis插件之参数补全

IDEA插件开发时,可以有两种途径提供代码补全:

  • 实现ReferencegetVariants()方法,返回一个数组,类型为String或者PsiElement或者LookupElement。这种方式只支持基本(basic)补全操作。
  • 继承CompletionContributor,支持basic、smart和class name三种补全方式。

本文主要介绍继承CompletionContributor的方式。

先说目标。

还是以之前的mapper文件为例:

1
2
3
4
5
6
7
8
9
<select id="findByUserName" resultType="com.damon4u.demo.domain.User">
select id, user_name
from user
<where>
<if test="userName != null">
user_name like CONCAT('%',#{userName},'%')
</if>
</where>
</select>

我想为sql语句和if条件中的#{userName}添加自动补全。

首先创建一个CompletionContributor的基本实现类,其中包含公共方法,为元素添加自动补全:

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
abstract class BaseParamCompletionContributor extends CompletionContributor {
private static final double PRIORITY = 400.0;
static void addCompletionForPsiParameter(@NotNull final Project project,
@NotNull final CompletionResultSet completionResultSet,
@Nullable final IdDomElement element) {
if (element == null) {
return;
}
final PsiMethod method = JavaUtils.findMethod(project, element).orElse(null);
if (method == null) {
return;
}
final PsiParameter[] parameters = method.getParameterList().getParameters();
if (parameters.length == 1) {
final PsiParameter parameter = parameters[0];
completionResultSet.addElement(buildLookupElementWithIcon(parameter.getName(), parameter.getType().getPresentableText()));
} else {
for (PsiParameter parameter : parameters) {
final Optional<String> annotationValueText = JavaUtils.getAnnotationValueText(parameter, Annotation.PARAM);
completionResultSet.addElement(buildLookupElementWithIcon(annotationValueText.orElseGet(parameter::getName), parameter.getType().getPresentableText()));
}
}
}
private static LookupElement buildLookupElementWithIcon(final String parameterName,
final String parameterType) {
return PrioritizedLookupElement.withPriority(
LookupElementBuilder.create(parameterName)
.withTypeText(parameterType)
.withIcon(PlatformIcons.PARAMETER_ICON),
PRIORITY);
}
}

该方法的逻辑比较清楚,先查出元素所在方法的参数列表,然后将参数构造成LookupElement返回。
构造LookupElement时,需要设置参数名称,参数类型和图标。
实现方式与上一篇IDEA插件开发(八)mybatis插件之参数引用 类似,不多解释。

主要看两个具体的实现类。

重写fillCompletionVariants方法

第一个,为sql语句中的#{userName}添加自动补全。

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
public class SqlParamCompletionContributor extends BaseParamCompletionContributor {
@Override
public void fillCompletionVariants(@NotNull CompletionParameters parameters, @NotNull CompletionResultSet result) {
if (parameters.getCompletionType() != CompletionType.BASIC) {
return;
}
final PsiElement position = parameters.getPosition();
PsiFile topLevelFile = InjectedLanguageManager.getInstance(parameters.getPosition().getProject()).getTopLevelFile(position);
if (DomUtils.isMybatisFile(topLevelFile)) {
if (shouldAddElement(position.getContainingFile(), parameters.getOffset())) {
process(topLevelFile, result, position);
}
}
}
private boolean shouldAddElement(PsiFile file, int offset) {
String text = file.getText();
for (int i = offset - 1; i > 0; i--) {
char c = text.charAt(i);
if (c == '{' && text.charAt(i - 1) == '#') {
return true;
}
}
return false;
}
private void process(PsiFile xmlFile, CompletionResultSet result, PsiElement position) {
int offset = InjectedLanguageManager.getInstance(position.getProject()).injectedToHost(position, position.getTextOffset());
Optional<IdDomElement> idDomElement = MapperUtils.findParentIdDomElement(xmlFile.findElementAt(offset));
if (idDomElement.isPresent()) {
addCompletionForPsiParameter(position.getProject(), result, idDomElement.get());
result.stopHere();
}
}
}

为了方便解释,我们先把注册到plugin.xml的代码贴出来:

1
2
3
<completion.contributor language="SQL"
implementationClass="com.damon4u.plugin.mybatis.completion.SqlParamCompletionContributor"
order="first"/>

第一种方式重写fillCompletionVariants方法,首先也是过滤参数。
在注册时,我们指定了language为SQL,那么该补全器只会传入SQL语句中参数。
然后我们在方法内过滤mybatis的mapper文件中的SQL语句参数。
然后判断是否是#{开头,如果是,才补全。

使用extend方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class TestParamCompletionContributor extends BaseParamCompletionContributor {
public TestParamCompletionContributor() {
extend(CompletionType.BASIC,
XmlPatterns.psiElement().inside(XmlPatterns.xmlAttribute().withName("test")),
new CompletionProvider<CompletionParameters>() {
@Override
protected void addCompletions(@NotNull CompletionParameters parameters,
ProcessingContext context,
@NotNull CompletionResultSet result) {
final PsiElement position = parameters.getPosition();
addCompletionForPsiParameter(position.getProject(), result, MapperUtils.findParentIdDomElement(position).orElse(null));
}
});
}
}

常用的实现方式是在构造函数中调用extend()方法。
第一个参数是补全类型,支持basic,smart和class name。一般选basic就好了。
第二个参数是参数匹配模式。本例子中是找到nametestXmlAttribute内部的元素:XmlPatterns.psiElement().inside(XmlPatterns.xmlAttribute().withName("test"))
第三个参数是CompletionProvider实例,重写addCompletions方法即可。

最后注册到plugin.xml中:

1
2
<completion.contributor language="XML"
implementationClass="com.damon4u.plugin.mybatis.completion.TestParamCompletionContributor"/>

参考:
Code Completion
Completion Contributor