package com.jetbrains.lang.dart.ide.completion; import com.intellij.codeInsight.AutoPopupController; import com.intellij.codeInsight.CodeInsightSettings; import com.intellij.codeInsight.completion.*; import com.intellij.codeInsight.completion.util.ParenthesesInsertHandler; import com.intellij.codeInsight.lookup.LookupElement; import com.intellij.codeInsight.lookup.LookupElementBuilder; import com.intellij.codeInsight.template.TemplateBuilderFactory; import com.intellij.codeInsight.template.TemplateBuilderImpl; import com.intellij.codeInsight.template.impl.TextExpression; import com.intellij.icons.AllIcons; import com.intellij.ide.highlighter.HtmlFileType; import com.intellij.injected.editor.VirtualFileWindow; import com.intellij.lang.Language; import com.intellij.lang.html.HTMLLanguage; import com.intellij.lang.injection.InjectedLanguageManager; import com.intellij.lang.xml.XMLLanguage; import com.intellij.openapi.editor.Document; import com.intellij.openapi.editor.Editor; import com.intellij.openapi.project.Project; import com.intellij.openapi.util.Pair; import com.intellij.openapi.util.TextRange; import com.intellij.openapi.util.text.StringUtil; import com.intellij.openapi.vfs.VirtualFile; import com.intellij.psi.PsiDocumentManager; import com.intellij.psi.PsiElement; import com.intellij.psi.PsiFile; import com.intellij.psi.PsiReference; import com.intellij.psi.impl.source.resolve.reference.impl.PsiMultiReference; import com.intellij.ui.LayeredIcon; import com.intellij.ui.RowIcon; import com.intellij.util.PlatformIcons; import com.intellij.util.ProcessingContext; import com.jetbrains.lang.dart.DartLanguage; import com.jetbrains.lang.dart.DartYamlFileTypeFactory; import com.jetbrains.lang.dart.analyzer.DartAnalysisServerService; import com.jetbrains.lang.dart.ide.codeInsight.DartCodeInsightSettings; import com.jetbrains.lang.dart.psi.DartNewExpression; import com.jetbrains.lang.dart.psi.DartStringLiteralExpression; import com.jetbrains.lang.dart.psi.DartUriElement; import com.jetbrains.lang.dart.sdk.DartSdk; import com.jetbrains.lang.dart.util.DartResolveUtil; import com.jetbrains.lang.dart.util.PubspecYamlUtil; import org.apache.commons.lang3.StringUtils; import org.dartlang.analysis.server.protocol.*; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import javax.swing.*; import java.util.List; import static com.intellij.patterns.PlatformPatterns.psiElement; import static com.intellij.patterns.PlatformPatterns.psiFile; import static com.intellij.patterns.StandardPatterns.or; public class DartServerCompletionContributor extends CompletionContributor { public DartServerCompletionContributor() { extend(CompletionType.BASIC, or(psiElement().withLanguage(DartLanguage.INSTANCE), psiElement().inFile(psiFile().withLanguage(HTMLLanguage.INSTANCE)), psiElement().inFile(psiFile().withName(DartYamlFileTypeFactory.DOT_ANALYSIS_OPTIONS))), new CompletionProvider<CompletionParameters>() { @Override protected void addCompletions(@NotNull final CompletionParameters parameters, @NotNull final ProcessingContext context, @NotNull final CompletionResultSet originalResultSet) { VirtualFile file = DartResolveUtil.getRealVirtualFile(parameters.getOriginalFile()); if (file instanceof VirtualFileWindow) { file = ((VirtualFileWindow)file).getDelegate(); } if (file == null) return; final Project project = parameters.getOriginalFile().getProject(); if (file.getFileType() == HtmlFileType.INSTANCE && PubspecYamlUtil.findPubspecYamlFile(project, file) == null) { return; } final DartSdk sdk = DartSdk.getDartSdk(project); if (sdk == null || !DartAnalysisServerService.isDartSdkVersionSufficient(sdk)) return; final DartAnalysisServerService das = DartAnalysisServerService.getInstance(project); das.updateFilesContent(); final int offset = InjectedLanguageManager.getInstance(project).injectedToHost(parameters.getOriginalFile(), parameters.getOffset()); final String completionId = das.completion_getSuggestions(file, offset); if (completionId == null) return; final String uriPrefix = getPrefixIfCompletingUri(parameters); final CompletionResultSet resultSet = uriPrefix != null ? originalResultSet.withPrefixMatcher(uriPrefix) : originalResultSet; das.addCompletions(file, completionId, (replacementOffset, replacementLength, suggestion) -> { final CompletionResultSet updatedResultSet; if (uriPrefix != null) { updatedResultSet = resultSet; } else { final String specialPrefix = getPrefixForSpecialCases(parameters, replacementOffset); if (specialPrefix != null) { updatedResultSet = resultSet.withPrefixMatcher(specialPrefix); } else { updatedResultSet = resultSet; } } final LookupElement lookupElement = createLookupElement(project, suggestion); updatedResultSet.addElement(lookupElement); }); } }); } @Nullable private static String getPrefixIfCompletingUri(@NotNull final CompletionParameters parameters) { final PsiElement psiElement = parameters.getOriginalPosition(); final PsiElement parent = psiElement != null ? psiElement.getParent() : null; final PsiElement parentParent = parent instanceof DartStringLiteralExpression ? parent.getParent() : null; if (parentParent instanceof DartUriElement) { final int uriStringOffset = ((DartUriElement)parentParent).getUriStringAndItsRange().second.getStartOffset(); if (parameters.getOffset() >= parentParent.getTextRange().getStartOffset() + uriStringOffset) { return parentParent.getText().substring(uriStringOffset, parameters.getOffset() - parentParent.getTextRange().getStartOffset()); } } return null; } /** * Handles completion provided by angular_analyzer_plugin in HTML files and inside string literals; * our PSI doesn't allow top calculate prefix in such cases */ @Nullable private static String getPrefixForSpecialCases(@NotNull final CompletionParameters parameters, final int replacementOffset) { final PsiElement psiElement = parameters.getOriginalPosition(); if (psiElement == null) return null; final PsiElement parent = psiElement.getParent(); final Language language = psiElement.getContainingFile().getLanguage(); if (parent instanceof DartStringLiteralExpression || language.isKindOf(XMLLanguage.INSTANCE)) { return getPrefixUsingServerData(parameters, replacementOffset); } return null; } @Nullable private static String getPrefixUsingServerData(@NotNull final CompletionParameters parameters, final int replacementOffset) { PsiElement element = parameters.getOriginalPosition(); if (element == null) return null; final InjectedLanguageManager manager = InjectedLanguageManager.getInstance(element.getProject()); final PsiFile injectedContext = parameters.getOriginalFile(); final int completionOffset = manager.injectedToHost(injectedContext, parameters.getOffset()); final TextRange range = manager.injectedToHost(injectedContext, element.getTextRange()); if (completionOffset < range.getStartOffset() || completionOffset > range.getEndOffset()) return null; // shouldn't happen if (replacementOffset > completionOffset) return null; // shouldn't happen while (element != null) { final int elementStartOffset = manager.injectedToHost(injectedContext, element.getTextRange().getStartOffset()); if (elementStartOffset <= replacementOffset) { break; // that's good, we can use this element to calculate prefix } element = element.getParent(); } if (element != null) { final int startOffset = manager.injectedToHost(injectedContext, element.getTextRange().getStartOffset()); return element.getText().substring(replacementOffset - startOffset, completionOffset - startOffset); } return null; } @Override public void beforeCompletion(@NotNull final CompletionInitializationContext context) { final PsiElement psiElement = context.getFile().findElementAt(context.getStartOffset()); final PsiElement parent = psiElement != null ? psiElement.getParent() : null; if (parent instanceof DartStringLiteralExpression) { final PsiElement parentParent = parent.getParent(); if (parentParent instanceof DartUriElement) { final Pair<String, TextRange> uriAndRange = ((DartUriElement)parentParent).getUriStringAndItsRange(); context.setReplacementOffset(parentParent.getTextRange().getStartOffset() + uriAndRange.second.getEndOffset()); } else { // If replacement context is not set explicitly then com.intellij.codeInsight.completion.CompletionProgressIndicator#duringCompletion // implementation looks for the reference at caret and on Tab replaces the whole reference. // angular_analyzer_plugin provides angular-specific completion inside Dart string literals. Without the following hack Tab replaces // too much useful text. This hack is not ideal though as it may leave a piece of tail not replaced. // TODO: use replacementLength received from the server context.setReplacementOffset(context.getReplacementOffset()); } } else { PsiReference reference = context.getFile().findReferenceAt(context.getStartOffset()); if (reference instanceof PsiMultiReference && ((PsiMultiReference)reference).getReferences().length > 0) { reference.getRangeInElement(); // to ensure that references are sorted by range reference = ((PsiMultiReference)reference).getReferences()[0]; } if (reference instanceof DartNewExpression) { // historically DartNewExpression is a reference; it can appear here only in situation like new Foo(o.<caret>); // without the following hack closing paren is replaced on Tab. We won't get here if at least one symbol after dot typed. context.setReplacementOffset(context.getStartOffset()); } } } private static Icon applyOverlay(Icon base, boolean condition, Icon overlay) { if (condition) { return new LayeredIcon(base, overlay); } return base; } private static Icon applyVisibility(Icon base, boolean isPrivate) { RowIcon result = new RowIcon(2); result.setIcon(base, 0); Icon visibility = isPrivate ? PlatformIcons.PRIVATE_ICON : PlatformIcons.PUBLIC_ICON; result.setIcon(visibility, 1); return result; } private static LookupElement createLookupElement(@NotNull final Project project, @NotNull final CompletionSuggestion suggestion) { final Element element = suggestion.getElement(); final Location location = element == null ? null : element.getLocation(); final DartLookupObject lookupObject = new DartLookupObject(project, location); final String lookupString = suggestion.getCompletion(); LookupElementBuilder lookup = LookupElementBuilder.create(lookupObject, lookupString); // keywords are bold if (suggestion.getKind().equals(CompletionSuggestionKind.KEYWORD)) { lookup = lookup.bold(); } final int dotIndex = lookupString.indexOf('.'); if (dotIndex > 0 && dotIndex < lookupString.length() - 1 && StringUtil.isJavaIdentifier(lookupString.substring(0, dotIndex)) && StringUtil.isJavaIdentifier(lookupString.substring(dotIndex + 1))) { // 'path.Context' should match 'Conte' prefix lookup = lookup.withLookupString(lookupString.substring(dotIndex + 1)); } boolean shouldSetSelection = true; if (element != null) { // @deprecated if (element.isDeprecated()) { lookup = lookup.strikeout(); } // append type parameters final String typeParameters = element.getTypeParameters(); if (typeParameters != null) { lookup = lookup.appendTailText(typeParameters, false); } // append parameters final String parameters = element.getParameters(); if (parameters != null) { lookup = lookup.appendTailText(parameters, false); } // append return type final String returnType = element.getReturnType(); if (!StringUtils.isEmpty(returnType)) { lookup = lookup.withTypeText(returnType, true); } // icon Icon icon = getBaseImage(element); if (icon != null) { icon = applyVisibility(icon, element.isPrivate()); icon = applyOverlay(icon, element.isFinal(), AllIcons.Nodes.FinalMark); icon = applyOverlay(icon, element.isConst(), AllIcons.Nodes.FinalMark); lookup = lookup.withIcon(icon); } // Prepare for typing arguments, if any. if (CompletionSuggestionKind.INVOCATION.equals(suggestion.getKind())) { shouldSetSelection = false; final List<String> parameterNames = suggestion.getParameterNames(); if (parameterNames != null) { lookup = lookup.withInsertHandler((context, item) -> { // like in JavaCompletionUtil.insertParentheses() final boolean needRightParenth = CodeInsightSettings.getInstance().AUTOINSERT_PAIR_BRACKET || parameterNames.isEmpty() && context.getCompletionChar() != '('; if (parameterNames.isEmpty()) { final ParenthesesInsertHandler<LookupElement> handler = ParenthesesInsertHandler.getInstance(false, false, false, needRightParenth, false); handler.handleInsert(context, item); } else { final ParenthesesInsertHandler<LookupElement> handler = ParenthesesInsertHandler.getInstance(true, false, false, needRightParenth, false); handler.handleInsert(context, item); // Show parameters popup. final Editor editor = context.getEditor(); final PsiElement psiElement = lookupObject.getElement(); if (DartCodeInsightSettings.getInstance().INSERT_DEFAULT_ARG_VALUES) { // Insert argument defaults if provided. final String argumentListString = suggestion.getDefaultArgumentListString(); if (argumentListString != null) { final Document document = editor.getDocument(); int offset = editor.getCaretModel().getOffset(); // At this point caret is expected to be right after the opening paren. // But if user was completing using Tab over the existing method call with arguments then old arguments are still there, // if so, skip inserting argumentListString final CharSequence text = document.getCharsSequence(); if (text.charAt(offset - 1) == '(' && text.charAt(offset) == ')') { document.insertString(offset, argumentListString); PsiDocumentManager.getInstance(project).commitDocument(document); final TemplateBuilderImpl builder = (TemplateBuilderImpl)TemplateBuilderFactory.getInstance().createTemplateBuilder(context.getFile()); final int[] ranges = suggestion.getDefaultArgumentListTextRanges(); // Only proceed if ranges are provided and well-formed. if (ranges != null && (ranges.length & 1) == 0) { int index = 0; while (index < ranges.length) { final int start = ranges[index]; final int length = ranges[index + 1]; final String arg = argumentListString.substring(start, start + length); final TextExpression expression = new TextExpression(arg); final TextRange range = new TextRange(offset + start, offset + start + length); index += 2; builder.replaceRange(range, "group_" + (index - 1), expression, true); } builder.run(editor, true); } } } } AutoPopupController.getInstance(project).autoPopupParameterInfo(editor, psiElement); } }); } } } // Use selection offset / length. if (shouldSetSelection) { lookup = lookup.withInsertHandler((context, item) -> { final Editor editor = context.getEditor(); final int startOffset = context.getStartOffset() + suggestion.getSelectionOffset(); final int endOffset = startOffset + suggestion.getSelectionLength(); editor.getCaretModel().moveToOffset(startOffset); if (endOffset > startOffset) { editor.getSelectionModel().setSelection(startOffset, endOffset); } }); } return PrioritizedLookupElement.withPriority(lookup, suggestion.getRelevance()); } private static Icon getBaseImage(Element element) { final String elementKind = element.getKind(); if (elementKind.equals(ElementKind.CLASS) || elementKind.equals(ElementKind.CLASS_TYPE_ALIAS)) { if (element.isAbstract()) { return AllIcons.Nodes.AbstractClass; } return AllIcons.Nodes.Class; } else if (elementKind.equals(ElementKind.ENUM)) { return AllIcons.Nodes.Enum; } else if (elementKind.equals(ElementKind.ENUM_CONSTANT) || elementKind.equals(ElementKind.FIELD)) { return AllIcons.Nodes.Field; } else if (elementKind.equals(ElementKind.COMPILATION_UNIT)) { return PlatformIcons.FILE_ICON; } else if (elementKind.equals(ElementKind.CONSTRUCTOR)) { return AllIcons.Nodes.ClassInitializer; } else if (elementKind.equals(ElementKind.GETTER)) { return element.isTopLevelOrStatic() ? AllIcons.Nodes.PropertyReadStatic : AllIcons.Nodes.PropertyRead; } else if (elementKind.equals(ElementKind.SETTER)) { return element.isTopLevelOrStatic() ? AllIcons.Nodes.PropertyWriteStatic : AllIcons.Nodes.PropertyWrite; } else if (elementKind.equals(ElementKind.METHOD)) { if (element.isAbstract()) { return AllIcons.Nodes.AbstractMethod; } return AllIcons.Nodes.Method; } else if (elementKind.equals(ElementKind.FUNCTION)) { return AllIcons.Nodes.Function; } else if (elementKind.equals(ElementKind.FUNCTION_TYPE_ALIAS)) { return AllIcons.Nodes.Annotationtype; } else if (elementKind.equals(ElementKind.TOP_LEVEL_VARIABLE)) { return AllIcons.Nodes.Variable; } else { return null; } } }