Files
openide/java/java-impl/src/com/intellij/codeInsight/completion/JavaDocCompletionContributor.java
T
2019-10-10 12:34:43 +00:00

551 lines
26 KiB
Java

// Copyright 2000-2019 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
package com.intellij.codeInsight.completion;
import com.intellij.application.options.CodeStyle;
import com.intellij.codeInsight.TailType;
import com.intellij.codeInsight.completion.scope.JavaCompletionProcessor;
import com.intellij.codeInsight.editorActions.wordSelection.DocTagSelectioner;
import com.intellij.codeInsight.javadoc.JavaDocUtil;
import com.intellij.codeInsight.lookup.*;
import com.intellij.codeInsight.template.TemplateManager;
import com.intellij.codeInsight.template.impl.ConstantNode;
import com.intellij.codeInsight.template.impl.TemplateImpl;
import com.intellij.codeInspection.InspectionProfile;
import com.intellij.codeInspection.SuppressionUtilCore;
import com.intellij.codeInspection.javaDoc.JavaDocLocalInspection;
import com.intellij.codeInspection.javaDoc.JavadocHighlightUtil;
import com.intellij.lang.java.JavaLanguage;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.editor.*;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.util.Comparing;
import com.intellij.openapi.util.Conditions;
import com.intellij.openapi.util.TextRange;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.patterns.PsiElementPattern;
import com.intellij.patterns.PsiJavaPatterns;
import com.intellij.profile.codeInspection.InspectionProjectProfileManager;
import com.intellij.psi.*;
import com.intellij.psi.codeStyle.CodeStyleSettings;
import com.intellij.psi.codeStyle.JavaCodeStyleManager;
import com.intellij.psi.filters.TrueFilter;
import com.intellij.psi.impl.JavaConstantExpressionEvaluator;
import com.intellij.psi.impl.source.javadoc.PsiDocParamRef;
import com.intellij.psi.javadoc.*;
import com.intellij.psi.util.InheritanceUtil;
import com.intellij.psi.util.PsiTreeUtil;
import com.intellij.psi.util.TypeConversionUtil;
import com.intellij.util.IncorrectOperationException;
import com.intellij.util.ProcessingContext;
import com.intellij.util.SystemProperties;
import com.intellij.util.containers.ContainerUtil;
import com.intellij.util.text.CharArrayUtil;
import org.jetbrains.annotations.NonNls;
import org.jetbrains.annotations.NotNull;
import java.util.*;
import static com.intellij.patterns.PlatformPatterns.psiElement;
import static com.intellij.patterns.StandardPatterns.string;
public class JavaDocCompletionContributor extends CompletionContributor {
private static final Logger LOG = Logger.getInstance("#com.intellij.codeInsight.completion.JavaDocCompletionContributor");
private static final @NonNls String VALUE_TAG = "value";
private static final @NonNls String LINK_TAG = "link";
private static final InsertHandler<LookupElement> PARAM_DESCRIPTION_INSERT_HANDLER = (context, item) -> {
if (context.getCompletionChar() != Lookup.REPLACE_SELECT_CHAR) return;
context.commitDocument();
PsiDocTag docTag = PsiTreeUtil.findElementOfClassAtOffset(context.getFile(), context.getStartOffset(), PsiDocTag.class, false);
if (docTag != null) {
Document document = context.getDocument();
int tagEnd = DocTagSelectioner.getDocTagRange(docTag, document.getCharsSequence(), 0).getEndOffset();
int tail = context.getTailOffset();
if (tail < tagEnd) {
document.deleteString(tail, tagEnd);
}
}
};
static final PsiElementPattern.Capture<PsiElement> THROWS_TAG_EXCEPTION = psiElement().inside(
psiElement(PsiDocTag.class).withName(
string().oneOf(PsiKeyword.THROWS, "exception")));
public JavaDocCompletionContributor() {
extend(CompletionType.BASIC, PsiJavaPatterns.psiElement(JavaDocTokenType.DOC_TAG_NAME), new TagChooser());
extend(CompletionType.BASIC, PsiJavaPatterns.psiElement().inside(PsiDocComment.class), new CompletionProvider<CompletionParameters>() {
@Override
protected void addCompletions(@NotNull final CompletionParameters parameters, @NotNull final ProcessingContext context, @NotNull CompletionResultSet result) {
final PsiElement position = parameters.getPosition();
boolean isArg = PsiJavaPatterns.psiElement().afterLeaf("(").accepts(position);
PsiDocTag tag = PsiTreeUtil.getParentOfType(position, PsiDocTag.class);
boolean onlyConstants = !isArg && tag != null && tag.getName().equals(VALUE_TAG);
final PsiReference ref = position.getContainingFile().findReferenceAt(parameters.getOffset());
PsiElement refElement = ref == null ? null : ref.getElement();
if (refElement instanceof PsiDocParamRef) {
result = result.withPrefixMatcher(
refElement.getText().substring(0, parameters.getOffset() - refElement.getTextRange().getStartOffset()));
for (PsiNamedElement param : getParametersToSuggest(PsiTreeUtil.getParentOfType(position, PsiDocComment.class))) {
result.addElement(PrioritizedLookupElement.withPriority(LookupElementBuilder.create(param, nameForParamTag(param)), param instanceof PsiTypeParameter ? 0 : 1));
}
}
else if (ref instanceof PsiJavaReference) {
result = JavaCompletionSorting.addJavaSorting(parameters, result);
result.stopHere();
for (LookupElement item : completeJavadocReference(position, (PsiJavaReference)ref)) {
if (onlyConstants) {
Object o = item.getObject();
if (!(o instanceof PsiField)) continue;
PsiField field = (PsiField) o;
if (!(field.hasModifierProperty(PsiModifier.STATIC) && field.getInitializer() != null &&
JavaConstantExpressionEvaluator.computeConstantExpression(field.getInitializer(), false) != null)) continue;
}
if (isArg) {
item = AutoCompletionPolicy.NEVER_AUTOCOMPLETE.applyPolicy(item);
}
result.addElement(item);
}
JavaCompletionContributor.addAllClasses(parameters, result, new JavaCompletionSession(result));
}
if (tag != null && "author".equals(tag.getName())) {
result.addElement(LookupElementBuilder.create(SystemProperties.getUserName()));
}
}
});
extend(CompletionType.SMART, THROWS_TAG_EXCEPTION, new CompletionProvider<CompletionParameters>() {
@Override
public void addCompletions(@NotNull final CompletionParameters parameters, @NotNull final ProcessingContext context, @NotNull final CompletionResultSet result) {
final PsiElement element = parameters.getPosition();
final Set<PsiClass> throwsSet = new HashSet<>();
final PsiMethod method = PsiTreeUtil.getContextOfType(element, PsiMethod.class, true);
if(method != null){
for (PsiClassType ref : method.getThrowsList().getReferencedTypes()) {
final PsiClass exception = ref.resolve();
if (exception != null && throwsSet.add(exception)) {
result.addElement(TailTypeDecorator.withTail(new JavaPsiClassReferenceElement(exception), TailType.HUMBLE_SPACE_BEFORE_WORD));
}
}
}
}
});
}
@NotNull
private List<LookupElement> completeJavadocReference(PsiElement position, PsiJavaReference ref) {
JavaCompletionProcessor processor = new JavaCompletionProcessor(position, TrueFilter.INSTANCE, JavaCompletionProcessor.Options.CHECK_NOTHING, Conditions.alwaysTrue());
ref.processVariants(processor);
return ContainerUtil.map(processor.getResults(), (completionResult) -> {
LookupElement item = createReferenceLookupItem(completionResult.getElement());
item.putUserData(JavaCompletionUtil.FORCE_SHOW_SIGNATURE_ATTR, Boolean.TRUE);
return item;
});
}
private LookupElement createReferenceLookupItem(final Object element) {
if (element instanceof PsiMethod) {
return new JavaMethodCallElement((PsiMethod)element) {
@Override
public void handleInsert(@NotNull InsertionContext context) {
new MethodSignatureInsertHandler().handleInsert(context, this);
}
};
}
if (element instanceof PsiClass) {
JavaPsiClassReferenceElement classElement = new JavaPsiClassReferenceElement((PsiClass)element);
classElement.setInsertHandler(JavaClassNameInsertHandler.JAVA_CLASS_INSERT_HANDLER);
return classElement;
}
return LookupItemUtil.objectToLookupItem(element);
}
private static PsiParameter getDocTagParam(PsiElement tag) {
if (tag instanceof PsiDocTag && "param".equals(((PsiDocTag)tag).getName())) {
PsiDocTagValue value = ((PsiDocTag)tag).getValueElement();
if (value instanceof PsiDocParamRef) {
final PsiReference psiReference = value.getReference();
PsiElement target = psiReference != null ? psiReference.resolve() : null;
if (target instanceof PsiParameter) {
return (PsiParameter)target;
}
}
}
return null;
}
@Override
public void fillCompletionVariants(@NotNull final CompletionParameters parameters, @NotNull final CompletionResultSet result) {
PsiElement position = parameters.getPosition();
if (PsiJavaPatterns.psiElement(JavaDocTokenType.DOC_COMMENT_DATA).accepts(position)) {
final PsiParameter param = getDocTagParam(position.getParent());
if (param != null) {
suggestSimilarParameterDescriptions(result, position, param);
}
if (!(position.getParent() instanceof PsiInlineDocTag)) {
suggestLinkWrappingVariants(parameters, result.withPrefixMatcher(CompletionUtil.findJavaIdentifierPrefix(parameters)), position);
}
suggestCodeLiterals(result, position);
return;
}
super.fillCompletionVariants(parameters, result);
}
private static void suggestCodeLiterals(@NotNull CompletionResultSet result, PsiElement position) {
PsiElement parent = position.getParent();
if (parent instanceof PsiInlineDocTag && !"code".equals(((PsiInlineDocTag)parent).getName())) {
return;
}
if (!result.getPrefixMatcher().getPrefix().isEmpty()) {
for (String keyword : ContainerUtil.ar("null", "true", "false")) {
LookupElementBuilder element = LookupElementBuilder.create(keyword);
result.addElement(parent instanceof PsiInlineDocTag ? element : wrapIntoCodeTag(element));
}
}
}
@NotNull
private static LookupElementBuilder wrapIntoCodeTag(LookupElementBuilder element) {
String tagText = "{@code " + element.getLookupString() + "}";
return element.withPresentableText(tagText).withInsertHandler(
(context, item) -> context.getDocument().replaceString(context.getStartOffset(), context.getTailOffset(), tagText));
}
private void suggestLinkWrappingVariants(@NotNull CompletionParameters parameters,
@NotNull CompletionResultSet result,
PsiElement position) {
PrefixMatcher matcher = result.getPrefixMatcher();
int prefixStart = parameters.getOffset() - matcher.getPrefix().length() - position.getTextRange().getStartOffset();
String text = position.getText();
if (prefixStart > 0 && text.charAt(prefixStart - 1) == '#') {
int classNameStart = findClassNameStart(text, prefixStart - 1);
String mockCommentPrefix = "/** {@link ";
String mockText = mockCommentPrefix + text.substring(classNameStart) + "}*/";
PsiDocComment mockComment = JavaPsiFacade.getElementFactory(position.getProject()).createDocCommentFromText(mockText, position);
PsiJavaReference ref = (PsiJavaReference)mockComment.findReferenceAt(mockCommentPrefix.length() + prefixStart - classNameStart);
assert ref != null : mockText;
for (LookupElement element : completeJavadocReference(ref.getElement(), ref)) {
result.addElement(LookupElementDecorator.withInsertHandler(element, wrapIntoLinkTag((context, item) -> element.handleInsert(context))));
}
}
else if (matcher.getPrefix().length() > 0) {
InsertHandler<JavaPsiClassReferenceElement> handler = wrapIntoLinkTag(JavaClassNameInsertHandler.JAVA_CLASS_INSERT_HANDLER);
AllClassesGetter.processJavaClasses(parameters, matcher, parameters.getInvocationCount() == 1, psiClass ->
result.addElement(AllClassesGetter.createLookupItem(psiClass, handler)));
}
}
private static int findClassNameStart(CharSequence text, int sharpOffset) {
int offset = sharpOffset;
while (offset > 0 && isQualifiedNamePart(text.charAt(offset - 1))) {
offset--;
}
return offset;
}
private static boolean isQualifiedNamePart(char c) {
return c == '.' || Character.isJavaIdentifierPart(c);
}
@NotNull
private static <T extends LookupElement> InsertHandler<T> wrapIntoLinkTag(InsertHandler<T> delegate) {
return (context, item) -> {
Document document = context.getDocument();
String link = "{@link ";
int startOffset = context.getStartOffset();
int qualifierStart = document.getCharsSequence().charAt(startOffset - 1) == '#'
? findClassNameStart(document.getCharsSequence(), startOffset - 1)
: startOffset;
document.insertString(qualifierStart, link);
document.insertString(context.getTailOffset(), "}");
context.setTailOffset(context.getTailOffset() - 1);
context.getOffsetMap().addOffset(CompletionInitializationContext.START_OFFSET, startOffset + link.length());
context.commitDocument();
delegate.handleInsert(context, item);
if (item.getObject() instanceof PsiField) {
context.getEditor().getCaretModel().moveToOffset(context.getTailOffset() + 1);
}
};
}
private static void suggestSimilarParameterDescriptions(CompletionResultSet result, PsiElement position, final PsiParameter param) {
final Set<String> descriptions = new HashSet<>();
position.getContainingFile().accept(new PsiRecursiveElementWalkingVisitor() {
@Override
public void visitElement(PsiElement element) {
PsiParameter param1 = getDocTagParam(element);
if (param1 != null && param1 != param &&
Comparing.equal(param1.getName(), param.getName()) && Comparing.equal(param1.getType(), param.getType())) {
String text = "";
for (PsiElement psiElement : ((PsiDocTag)element).getDataElements()) {
if (psiElement != ((PsiDocTag)element).getValueElement()) {
text += psiElement.getText();
}
}
text = text.trim();
if (text.contains(" ")) {
descriptions.add(text);
}
}
super.visitElement(element);
}
});
for (String description : descriptions) {
result.addElement(PrioritizedLookupElement.withPriority(
LookupElementBuilder.create(description).withInsertHandler(PARAM_DESCRIPTION_INSERT_HANDLER), 1));
}
}
private static class TagChooser extends CompletionProvider<CompletionParameters> {
@Override
protected void addCompletions(@NotNull final CompletionParameters parameters, @NotNull final ProcessingContext context, @NotNull final CompletionResultSet result) {
final List<String> ret = new ArrayList<>();
final PsiElement position = parameters.getPosition();
final PsiDocComment comment = PsiTreeUtil.getParentOfType(position, PsiDocComment.class);
assert comment != null;
PsiElement parent = comment.getContext();
if (parent instanceof PsiJavaFile) {
final PsiJavaFile file = (PsiJavaFile)parent;
if (PsiPackage.PACKAGE_INFO_FILE.equals(file.getName())) {
final String packageName = file.getPackageName();
parent = JavaPsiFacade.getInstance(position.getProject()).findPackage(packageName);
}
}
final boolean isInline = position.getContext() instanceof PsiInlineDocTag;
for (JavadocTagInfo info : JavadocManager.SERVICE.getInstance(position.getProject()).getTagInfos(parent)) {
String tagName = info.getName();
if (tagName.equals(SuppressionUtilCore.SUPPRESS_INSPECTIONS_TAG_NAME)) continue;
if (isInline != info.isInline()) continue;
ret.add(tagName);
addSpecialTags(ret, comment, tagName);
}
InspectionProfile inspectionProfile =
InspectionProjectProfileManager.getInstance(position.getProject()).getCurrentProfile();
JavaDocLocalInspection inspection =
(JavaDocLocalInspection)inspectionProfile.getUnwrappedTool(JavaDocLocalInspection.SHORT_NAME, position);
if (inspection != null) {
final StringTokenizer tokenizer = new StringTokenizer(inspection.myAdditionalJavadocTags, ", ");
while (tokenizer.hasMoreTokens()) {
ret.add(tokenizer.nextToken());
}
}
for (final String s : ret) {
if (isInline) {
result.addElement(LookupElementDecorator.withInsertHandler(LookupElementBuilder.create(s), new InlineInsertHandler()));
} else {
result.addElement(TailTypeDecorator.withTail(LookupElementBuilder.create(s), TailType.INSERT_SPACE));
}
}
result.stopHere(); // no word completions at this point
}
private static void addSpecialTags(final List<? super String> result, PsiDocComment comment, String tagName) {
if ("author".equals(tagName)) {
result.add(tagName + " " + SystemProperties.getUserName());
return;
}
if ("param".equals(tagName)) {
for (PsiNamedElement parameter : getParametersToSuggest(comment)) {
result.add(tagName + " " + nameForParamTag(parameter));
}
return;
}
if ("see".equals(tagName)) {
PsiMember member = PsiTreeUtil.getParentOfType(comment, PsiMember.class);
if (member instanceof PsiClass) {
InheritanceUtil.processSupers((PsiClass)member, false, psiClass -> {
String name = psiClass.getQualifiedName();
if (StringUtil.isNotEmpty(name) && !CommonClassNames.JAVA_LANG_OBJECT.equals(name)) {
result.add("see " + name);
}
return true;
});
}
}
}
}
private static List<PsiNamedElement> getParametersToSuggest(PsiDocComment comment) {
List<PsiNamedElement> allParams = PsiDocParamRef.getAllParameters(comment);
PsiDocTag[] tags = comment.getTags();
return ContainerUtil.filter(allParams, param -> !JavadocHighlightUtil.hasTagForParameter(tags, param));
}
private static String nameForParamTag(PsiNamedElement param) {
String name = param.getName();
return param instanceof PsiTypeParameter ? "<" + name + ">" : name;
}
private static class InlineInsertHandler implements InsertHandler<LookupElement> {
@Override
public void handleInsert(@NotNull InsertionContext context, @NotNull LookupElement item) {
if (context.getCompletionChar() == Lookup.REPLACE_SELECT_CHAR) {
final Project project = context.getProject();
PsiDocumentManager.getInstance(project).commitAllDocuments();
final Editor editor = context.getEditor();
final CaretModel caretModel = editor.getCaretModel();
final int offset = caretModel.getOffset();
final PsiElement element = context.getFile().findElementAt(offset - 1);
PsiDocTag tag = PsiTreeUtil.getParentOfType(element, PsiDocTag.class);
assert tag != null;
for (PsiElement child = tag.getFirstChild(); child != null; child = child.getNextSibling()) {
if (child instanceof PsiDocToken) {
PsiDocToken token = (PsiDocToken)child;
if (token.getTokenType() == JavaDocTokenType.DOC_INLINE_TAG_END) return;
}
}
final String name = tag.getName();
final CharSequence chars = editor.getDocument().getCharsSequence();
final int currentOffset = caretModel.getOffset();
if (chars.charAt(currentOffset) == '}') {
caretModel.moveToOffset(offset + 1);
}
else if (chars.charAt(currentOffset + 1) == '}' && chars.charAt(currentOffset) == ' ') {
caretModel.moveToOffset(offset + 2);
}
else if (name.equals(LINK_TAG)) {
EditorModificationUtil.insertStringAtCaret(editor, " }");
caretModel.moveToOffset(offset + 1);
editor.getScrollingModel().scrollToCaret(ScrollType.RELATIVE);
editor.getSelectionModel().removeSelection();
}
else {
EditorModificationUtil.insertStringAtCaret(editor, "}");
caretModel.moveToOffset(offset + 1);
}
}
}
}
private static class MethodSignatureInsertHandler implements InsertHandler<JavaMethodCallElement> {
@Override
public void handleInsert(@NotNull InsertionContext context, @NotNull JavaMethodCallElement item) {
PsiDocumentManager.getInstance(context.getProject()).commitDocument(context.getEditor().getDocument());
final Editor editor = context.getEditor();
final PsiMethod method = item.getObject();
Document document = editor.getDocument();
final CharSequence chars = document.getCharsSequence();
int endOffset = editor.getCaretModel().getOffset();
final Project project = context.getProject();
int afterSharp = CharArrayUtil.shiftBackwardUntil(chars, endOffset - 1, "#") + 1;
int signatureOffset = afterSharp;
PsiElement element = context.getFile().findElementAt(signatureOffset - 1);
final CodeStyleSettings styleSettings = CodeStyle.getSettings(context.getFile());
PsiDocTag tag = PsiTreeUtil.getParentOfType(element, PsiDocTag.class);
if (context.getCompletionChar() == Lookup.REPLACE_SELECT_CHAR && tag != null) {
PsiDocTagValue valueElement = tag.getValueElement();
int valueEnd = valueElement != null ? valueElement.getTextRange().getEndOffset() : -1;
if (valueEnd >= afterSharp) {
endOffset = valueEnd;
context.setTailOffset(endOffset);
}
}
document.deleteString(afterSharp, endOffset);
editor.getCaretModel().moveToOffset(signatureOffset);
editor.getScrollingModel().scrollToCaret(ScrollType.RELATIVE);
editor.getSelectionModel().removeSelection();
String methodName = method.getName();
int beforeParenth = signatureOffset + methodName.length();
PsiParameter[] parameters = method.getParameterList().getParameters();
String signature = "(" +
StringUtil.join(parameters,
p -> TypeConversionUtil.erasure(p.getType()).getCanonicalText(),
"," + (styleSettings.getCommonSettings(JavaLanguage.INSTANCE).SPACE_AFTER_COMMA ? " " : "")) +
")";
String insertString = methodName + signature;
if (!(tag instanceof PsiInlineDocTag)) {
insertString += " ";
}
else {
if (chars.charAt(signatureOffset) == '}') {
afterSharp++;
}
else {
insertString += "} ";
}
}
document.insertString(signatureOffset, insertString);
RangeMarker paramListMarker = document.createRangeMarker(TextRange.from(beforeParenth, signature.length()));
editor.getCaretModel().moveToOffset(afterSharp + insertString.length());
editor.getScrollingModel().scrollToCaret(ScrollType.RELATIVE);
PsiDocumentManager.getInstance(project).commitDocument(document);
shortenReferences(project, editor, context, beforeParenth + 1);
if (parameters.length > 0) {
startParameterListTemplate(context, editor, document, project, paramListMarker);
}
}
private static void startParameterListTemplate(@NotNull InsertionContext context,
Editor editor,
Document document,
Project project, RangeMarker paramListMarker) {
PsiDocumentManager.getInstance(project).doPostponedOperationsAndUnblockDocument(document);
int tail = editor.getCaretModel().getOffset();
if (paramListMarker.isValid() && tail >= paramListMarker.getEndOffset()) {
PsiDocComment docComment =
PsiTreeUtil.findElementOfClassAtOffset(context.getFile(), paramListMarker.getStartOffset(), PsiDocComment.class, false);
if (docComment != null) {
TemplateImpl template = new TemplateImpl("", "");
ConstantNode node = new ConstantNode(document.getText(TextRange.create(paramListMarker)));
template.addVariable("PARAMETERS", node, node, true);
template.addTextSegment(document.getText(TextRange.create(paramListMarker.getEndOffset(), tail)));
template.addEndVariable();
template.setToShortenLongNames(false);
editor.getCaretModel().moveToOffset(paramListMarker.getStartOffset());
document.deleteString(paramListMarker.getStartOffset(), tail);
TemplateManager.getInstance(project).startTemplate(editor, template);
}
}
}
private static void shortenReferences(final Project project, final Editor editor, InsertionContext context, int offset) {
PsiDocumentManager.getInstance(project).commitDocument(editor.getDocument());
final PsiElement element = context.getFile().findElementAt(offset);
final PsiDocComment docComment = PsiTreeUtil.getParentOfType(element, PsiDocComment.class);
if (!JavaDocUtil.isInsidePackageInfo(docComment)) {
final PsiDocTagValue tagValue = PsiTreeUtil.getParentOfType(element, PsiDocTagValue.class);
if (tagValue != null) {
try {
JavaCodeStyleManager.getInstance(project).shortenClassReferences(tagValue);
}
catch (IncorrectOperationException e) {
LOG.error(e);
}
}
PsiDocumentManager.getInstance(context.getProject()).commitAllDocuments();
}
}
}
}