void (@NotNull CompletionParameters parameters, @NotNull CompletionResultSet result, @NotNull PsiElement position, boolean forceInlineTag) { TextRange rangeBefore = new TextRange(position.getTextRange().getStartOffset(), parameters.getOffset()); boolean hasOpeningBrace = rangeBefore.isEmpty() && position.getPrevSibling() != null && position.getPrevSibling().textMatches("{"); boolean isInline = hasOpeningBrace || forceInlineTag || !parameters.getEditor().getDocument().getText(rangeBefore).isBlank(); List<String> tags = getTags(position, isInline); for (String tag : tags) { if (isInline) { String lookupString = hasOpeningBrace ? "@" + tag : "{@" + tag + "}"; result.addElement(LookupElementDecorator.withInsertHandler(LookupElementBuilder.create(lookupString), new InlineInsertHandler())); } else { result.addElement(TailTypeDecorator.withTail((LookupElement)LookupElementBuilder.create("@" + tag), TailTypes.insertSpaceType())); } } } 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().isEmpty()) { 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(@NotNull PsiElement element) { PsiParameter param1 = getDocTagParam(element); if (param1 != null && param1 != param && Objects.equals(param1.getName(), param.getName()) && Comparing.equal(param1.getType(), param.getType())) { StringBuilder sb = new StringBuilder(); for (PsiElement psiElement : ((PsiDocTag)element).getDataElements()) { if (psiElement != ((PsiDocTag)element).getValueElement()) { sb.append(psiElement.getText()); } } String text = sb.toString().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 PsiElement position = parameters.getPosition(); final boolean isInline = position.getContext() instanceof PsiInlineDocTag; final List<String> ret = getTags(position, isInline); for (final String s : ret) { if (isInline) { result.addElement(LookupElementDecorator.withInsertHandler(LookupElementBuilder.create(s), new InlineInsertHandler())); } else { result.addElement(TailTypeDecorator.withTail(LookupElementBuilder.create(s), TailTypes.insertSpaceType())); } } result.stopHere(); // no word completions at this point } } @NotNull private static List<String> getTags(PsiElement position, boolean isInline) { final PsiDocComment comment = PsiTreeUtil.getParentOfType(position, PsiDocComment.class); assert comment != null; JavadocTagInfo[] infos = getTagInfos(position, comment); final List<String> ret = new ArrayList<>(); for (JavadocTagInfo info : infos) { String tagName = info.getName(); if (tagName.equals(SuppressionUtilCore.SUPPRESS_INSPECTIONS_TAG_NAME)) continue; if (isInline != info.isInline() && !(PsiUtil.isLanguageLevel16OrHigher(comment) && tagName.equals("return"))) continue; if (addSpecialTags(ret, comment, tagName)) { ret.add(tagName); } } InspectionProfile inspectionProfile = InspectionProjectProfileManager.getInstance(position.getProject()).getCurrentProfile(); JavadocDeclarationInspection inspection = (JavadocDeclarationInspection)inspectionProfile.getUnwrappedTool(JavadocDeclarationInspection.SHORT_NAME, position); if (inspection != null) { final StringTokenizer tokenizer = new StringTokenizer(inspection.ADDITIONAL_TAGS, ", "); while (tokenizer.hasMoreTokens()) { ret.add(tokenizer.nextToken()); } } return ret; } /** * @return true if simple tag (without parameter) should be added as well */ private static boolean addSpecialTags(final List<? super String> result, PsiDocComment comment, String tagName) { if ("author".equals(tagName)) { result.add(tagName + " " + SystemProperties.getUserName()); return true; } if ("param".equals(tagName)) { List<PsiNamedElement> parameters = getParametersToSuggest(comment); for (PsiNamedElement parameter : parameters) { result.add(tagName + " " + nameForParamTag(parameter)); } return !parameters.isEmpty(); } 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; }); } } return true; } private static JavadocTagInfo @NotNull [] getTagInfos(PsiElement position, PsiDocComment comment) { PsiElement parent = comment.getContext(); if (parent instanceof PsiJavaFile file && PsiPackage.PACKAGE_INFO_FILE.equals(file.getName())) { final String packageName = file.getPackageName(); parent = JavaPsiFacade.getInstance(position.getProject()).findPackage(packageName); } return JavadocManager.getInstance(position.getProject()).getTagInfos(parent); } private static List<PsiNamedElement> getParametersToSuggest(PsiDocComment comment) { List<PsiNamedElement> allParams = PsiDocParamRef.getAllParameters(comment); PsiDocTag[] tags = comment.getTags(); return ContainerUtil.filter(allParams, param -> !MissingJavadocInspection.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) { String lookupString = item.getLookupString(); String tagName = lookupString.startsWith("@") ? lookupString.substring(1) : lookupString.startsWith("{@") ? lookupString.substring(2, lookupString.length() - 1) : lookupString; boolean hasParameter = INLINE_TAGS_WITH_PARAMETER.contains(tagName); if (hasParameter) { final Editor editor = context.getEditor(); final CaretModel caretModel = editor.getCaretModel(); int offset = caretModel.getOffset(); if (lookupString.endsWith("}")) { offset--; } if (context.getCompletionChar() != ' ') { context.getDocument().insertString(offset, " "); caretModel.moveToOffset(offset + 1); } } 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 token && 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)) { EditorModificationUtilEx.insertStringAtCaret(editor, " }"); caretModel.moveToOffset(offset + 1); editor.getScrollingModel().scrollToCaret(ScrollType.RELATIVE); editor.getSelectionModel().removeSelection(); } else { EditorModificationUtilEx.insertStringAtCaret(editor, "}"); caretModel.moveToOffset(offset + 1); }