Java tutorial
/* * Copyright 2012-2013 Sergey Ignatov * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.intellij.erlang; 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.lang.ASTNode; import com.intellij.openapi.actionSystem.IdeActions; import com.intellij.openapi.editor.Document; import com.intellij.openapi.editor.Editor; import com.intellij.openapi.module.Module; import com.intellij.openapi.module.ModuleUtilCore; import com.intellij.openapi.project.Project; import com.intellij.openapi.util.Condition; import com.intellij.openapi.util.Pair; import com.intellij.openapi.util.TextRange; import com.intellij.openapi.util.io.FileUtil; import com.intellij.openapi.util.text.StringUtil; import com.intellij.openapi.vfs.LocalFileSystem; import com.intellij.openapi.vfs.VfsUtil; import com.intellij.openapi.vfs.VirtualFile; import com.intellij.psi.PsiComment; import com.intellij.psi.PsiElement; import com.intellij.psi.PsiFile; import com.intellij.psi.PsiFileFactory; import com.intellij.psi.formatter.FormatterUtil; import com.intellij.psi.impl.source.tree.LeafPsiElement; import com.intellij.psi.impl.source.tree.TreeUtil; import com.intellij.psi.search.GlobalSearchScope; import com.intellij.psi.tree.IElementType; import com.intellij.psi.util.PsiTreeUtil; import com.intellij.util.Function; import com.intellij.util.ProcessingContext; import com.intellij.util.containers.ContainerUtil; import com.intellij.util.text.CaseInsensitiveStringHashingStrategy; import gnu.trove.THashSet; import org.intellij.erlang.facet.ErlangFacet; import org.intellij.erlang.parser.ErlangLexer; import org.intellij.erlang.parser.ErlangParserUtil; import org.intellij.erlang.parser.GeneratedParserUtilBase; import org.intellij.erlang.psi.*; import org.intellij.erlang.psi.impl.ErlangFileImpl; import org.intellij.erlang.psi.impl.ErlangPsiImplUtil; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import javax.swing.*; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.List; import static com.intellij.patterns.PlatformPatterns.psiElement; import static com.intellij.patterns.StandardPatterns.instanceOf; /** * @author ignatov */ public class ErlangCompletionContributor extends CompletionContributor { public static final int MODULE_PRIORITY = 10; public static final int KEYWORD_PRIORITY = -10; public static final int MODULE_FUNCTIONS_PRIORITY = -4; public static final int BIF_PRIORITY = -5; public static final THashSet<String> KEYWORDS_WITH_PARENTHESIS = ContainerUtil.newTroveSet( CaseInsensitiveStringHashingStrategy.INSTANCE, "include", "include_lib", "module", "export", "export_type", "import", "define", "record", "behaviour"); @Override public void beforeCompletion(@NotNull CompletionInitializationContext context) { PsiFile file = context.getFile(); if (ErlangParserUtil.isApplicationConfigFileType(file)) return; int startOffset = context.getStartOffset(); PsiElement elementAt = file.findElementAt(startOffset); PsiElement parent = elementAt == null ? null : elementAt.getParent(); ErlangExport export = PsiTreeUtil.getPrevSiblingOfType(parent, ErlangExport.class); ErlangExportTypeAttribute exportType = PsiTreeUtil.getParentOfType(elementAt, ErlangExportTypeAttribute.class); ErlangRecordTuple recordTuple = PsiTreeUtil.getPrevSiblingOfType(parent, ErlangRecordTuple.class); PsiElement previousByOffset = elementAt != null ? PsiTreeUtil.prevVisibleLeaf(elementAt) : startOffset > 0 ? file.findElementAt(startOffset - 1) : null; //noinspection unchecked ErlangCompositeElement typeParent = PsiTreeUtil.getParentOfType(elementAt, ErlangTypeSig.class, ErlangTypedRecordFields.class, ErlangTypeDefinition.class); if (parent instanceof ErlangExport || parent instanceof ErlangExportFunctions || parent instanceof ErlangImportDirective || parent instanceof ErlangImportFunctions || exportType != null || export != null || prevIsRadix(elementAt) || is(previousByOffset, ErlangTypes.ERL_RADIX) || (previousByOffset != null && previousByOffset.getParent() instanceof ErlangRecordField || parent instanceof ErlangRecordTuple || recordTuple != null || parent instanceof ErlangRecordField) && !is(previousByOffset, ErlangTypes.ERL_OP_EQ) || typeParent != null) { context.setDummyIdentifier("a"); } } private static boolean is(@Nullable PsiElement element, IElementType type) { return element != null && element.getNode().getElementType() == type; } public ErlangCompletionContributor() { extend(CompletionType.BASIC, psiElement().inFile(instanceOf(ErlangFileImpl.class)), new CompletionProvider<CompletionParameters>() { @Override protected void addCompletions(@NotNull CompletionParameters parameters, ProcessingContext context, @NotNull CompletionResultSet result) { PsiElement position = parameters.getPosition(); PsiFile file = position.getContainingFile(); if (ErlangParserUtil.isApplicationConfigFileType(file)) return; boolean inConsole = ErlangParserUtil.isConsole(file); PsiElement parent = position.getParent().getParent(); PsiElement originalPosition = parameters.getOriginalPosition(); PsiElement originalParent = originalPosition != null ? originalPosition.getParent() : null; if (originalParent instanceof ErlangIncludeString && originalPosition instanceof LeafPsiElement && ErlangTypes.ERL_STRING == ((LeafPsiElement) originalPosition).getElementType()) { TextRange range = new TextRange( ((LeafPsiElement) originalPosition).getStartOffset() + 1, parameters.getOffset()); String includeText = range.getLength() >= 0 ? range.substring(file.getText()) : ""; if (parent instanceof ErlangInclude) { result.addAllElements(getModulePathLookupElements(file, includeText)); } else if (parent instanceof ErlangIncludeLib) { result.addAllElements(getLibPathLookupElements(file, includeText)); } } if (originalParent instanceof ErlangStringLiteral || originalPosition instanceof PsiComment) return; if (parent instanceof ErlangType) { result.addAllElements(ErlangPsiImplUtil.getTypeLookupElements(file, true, false)); } if (originalParent instanceof ErlangRecordExpression || prevIsRadix(originalPosition) || prevIsRadix(parent)) { result.addAllElements(ErlangPsiImplUtil.getRecordLookupElements(file)); } else if (originalParent instanceof ErlangExportFunctions && file instanceof ErlangFile) { result.addAllElements(ErlangPsiImplUtil .createFunctionLookupElements(((ErlangFile) file).getFunctions(), true)); } else { ErlangColonQualifiedExpression colonQualified = PsiTreeUtil.getParentOfType(position, ErlangColonQualifiedExpression.class); if (colonQualified != null && (PsiTreeUtil.getParentOfType(position, ErlangClauseBody.class) != null || inConsole)) { ErlangQAtom qAtom = ErlangPsiImplUtil.getQAtom(colonQualified); result.addAllElements( ErlangPsiImplUtil.getFunctionLookupElements(file, false, qAtom)); } else if (parent instanceof ErlangRecordField || parent instanceof ErlangRecordFields) { Pair<List<ErlangTypedExpr>, List<ErlangQAtom>> recordFields = ErlangPsiImplUtil .getRecordFields(parent); final boolean withoutEq = is(parent.getFirstChild(), ErlangTypes.ERL_DOT); result.addAllElements(ContainerUtil.map(recordFields.first, new Function<ErlangTypedExpr, LookupElement>() { @Override public LookupElement fun(ErlangTypedExpr a) { return createFieldLookupElement(a.getName(), withoutEq); } })); result.addAllElements(ContainerUtil.map(recordFields.second, new Function<ErlangQAtom, LookupElement>() { @Override public LookupElement fun(ErlangQAtom a) { return createFieldLookupElement(a.getText(), withoutEq); } })); return; } else if (parent instanceof ErlangMacros) { return; } else if (PsiTreeUtil.getParentOfType(position, ErlangExport.class) == null) { for (String keyword : suggestKeywords(position)) { result.addElement(createKeywordLookupElement(keyword)); } int invocationCount = parameters.getInvocationCount(); boolean moduleCompletion = invocationCount > 0 && invocationCount % 2 == 0; //noinspection unchecked boolean inside = PsiTreeUtil.getParentOfType(position, ErlangClauseBody.class, ErlangFunTypeSigs.class, ErlangTypeRef.class) != null; if ((inside || inConsole) && moduleCompletion) { suggestModules(result, position, true); } else { //noinspection unchecked if (PsiTreeUtil.getParentOfType(position, ErlangImportDirective.class, ErlangImportFunctions.class) instanceof ErlangImportDirective) { suggestModules(result, position, false); } else { String shortcut = getActionShortcut(IdeActions.ACTION_CODE_COMPLETION); CompletionService.getCompletionService().setAdvertisementText( shortcut + " to activate module name completion"); } } } if (colonQualified == null && parent instanceof ErlangExpression && (ErlangPsiImplUtil.inFunction(position) || inConsole)) { result.addAllElements( ErlangPsiImplUtil.getFunctionLookupElements(file, false, null)); } } } }); } private static LookupElement createFieldLookupElement(@NotNull String text, boolean withoutEq) { return LookupElementBuilder.create(text).withIcon(ErlangIcons.FIELD) .withInsertHandler(withoutEq ? null : new SingleCharInsertHandler('=')); } @NotNull private static LookupElement createKeywordLookupElement(@NotNull String keyword) { boolean needHandler = KEYWORDS_WITH_PARENTHESIS.contains(keyword); boolean needQuotas = "include".equalsIgnoreCase(keyword) || "include_lib".equalsIgnoreCase(keyword); boolean needBrackets = "export".equalsIgnoreCase(keyword) || "export_type".equalsIgnoreCase(keyword); return PrioritizedLookupElement.withPriority(LookupElementBuilder.create(keyword) .withInsertHandler(needHandler ? new ErlangKeywordInsertHandler(needQuotas, needBrackets) : null) .withTailText(needHandler ? "()" : null).bold(), KEYWORD_PRIORITY); } private static List<LookupElement> getLibPathLookupElements(final PsiFile file, final String includeText) { if (FileUtil.isAbsolute(includeText)) return Collections.emptyList(); final VirtualFile virtualFile = file.getOriginalFile().getVirtualFile(); List<LookupElement> result = new ArrayList<LookupElement>(); List<String> split = StringUtil.split(includeText, "/"); if (split.isEmpty()) { split = Collections.singletonList(""); } String appName = split.get(0); String pathSeparator = includeText.endsWith("/") ? "/" : ""; String libRelativePath = split.size() > 1 ? StringUtil.join(split.subList(1, split.size()), "/") + pathSeparator : ""; boolean completingAppName = split.size() == 1 && !includeText.endsWith("/"); List<VirtualFile> appDirs = getApplicationDirectories(file.getProject(), appName, !completingAppName); List<VirtualFile> matchingFiles = new ArrayList<VirtualFile>(); for (final VirtualFile appRoot : appDirs) { final String appFullName = appRoot != null ? appRoot.getName() : null; String appShortName = appFullName != null ? getAppShortName(appFullName) : null; if (appRoot == null) continue; if (completingAppName) { result.add(getDefaultPathLookupElementBuilder(includeText, appRoot, appShortName) .withPresentableText(appShortName + "/").withTypeText("in " + appFullName, true)); continue; } addMatchingFiles(appRoot, libRelativePath, matchingFiles); result.addAll(ContainerUtil.mapNotNull(matchingFiles, new Function<VirtualFile, LookupElement>() { @Nullable @Override public LookupElement fun(VirtualFile f) { return f == virtualFile ? null : getDefaultPathLookupElementBuilder(includeText, f, null) .withTypeText("in " + appFullName, true); } })); matchingFiles.clear(); } result.addAll(getModulePathLookupElements(file, includeText)); return result; } private static List<VirtualFile> getApplicationDirectories(Project project, final String appName, boolean nameIsComplete) { GlobalSearchScope searchScope = GlobalSearchScope.allScope(project); if (nameIsComplete) { return ContainerUtil.createMaybeSingletonList( ErlangApplicationIndex.getApplicationDirectoryByName(appName, searchScope)); } return ContainerUtil.filter(ErlangApplicationIndex.getAllApplicationDirectories(project, searchScope), new Condition<VirtualFile>() { @Override public boolean value(VirtualFile virtualFile) { return virtualFile != null && virtualFile.getName().startsWith(appName); } }); } private static String getAppShortName(String appFullName) { int dashIdx = appFullName.indexOf('-'); return dashIdx != -1 ? appFullName.substring(0, dashIdx) : appFullName; } private static List<LookupElement> getModulePathLookupElements(PsiFile file, final String includeText) { final VirtualFile virtualFile = file.getOriginalFile().getVirtualFile(); VirtualFile parentFile = virtualFile != null ? virtualFile.getParent() : null; List<LookupElement> result = new ArrayList<LookupElement>(); if (FileUtil.isAbsolute(includeText)) return result; //search in this module's directory if (parentFile != null) { List<VirtualFile> relativeToParent = new ArrayList<VirtualFile>(); addMatchingFiles(parentFile, includeText, relativeToParent); result.addAll(ContainerUtil.mapNotNull(relativeToParent, new Function<VirtualFile, LookupElement>() { @Nullable @Override public LookupElement fun(VirtualFile f) { return f == virtualFile ? null : getDefaultPathLookupElementBuilder(includeText, f, null); } })); } //search in include directories Module module = ModuleUtilCore.findModuleForPsiElement(file); ErlangFacet facet = module != null ? ErlangFacet.getFacet(module) : null; if (facet != null) { List<VirtualFile> relativeToIncludeDirs = new ArrayList<VirtualFile>(); for (String includePath : facet.getConfiguration().getIncludePaths()) { final VirtualFile includeDir = LocalFileSystem.getInstance().findFileByPath(includePath); if (includeDir != null && includeDir.isDirectory()) { addMatchingFiles(includeDir, includeText, relativeToIncludeDirs); result.addAll(ContainerUtil.mapNotNull(relativeToIncludeDirs, new Function<VirtualFile, LookupElement>() { @Nullable @Override public LookupElement fun(VirtualFile f) { return f == virtualFile ? null : getDefaultPathLookupElementBuilder(includeText, f, null); } })); } relativeToIncludeDirs.clear(); } } return result; } private static LookupElementBuilder getDefaultPathLookupElementBuilder(String includeText, VirtualFile lookedUpFile, @Nullable String appName) { String slash = lookedUpFile.isDirectory() ? "/" : ""; Icon icon = lookedUpFile.isDirectory() ? ErlangIcons.MODULE : ErlangFileType.getIconForFile(lookedUpFile.getName()); return LookupElementBuilder.create(getCompletedString(includeText, lookedUpFile, appName)) .withPresentableText(lookedUpFile.getName() + slash).withIcon(icon) .withInsertHandler(new RunCompletionInsertHandler()); } private static String getCompletedString(String beforeCompletion, VirtualFile lookedUpFile, @Nullable String appName) { String prefixPath = beforeCompletion.substring(0, beforeCompletion.lastIndexOf('/') + 1); String completion = (appName == null ? lookedUpFile.getName() : appName); String pathSeparator = (appName != null || lookedUpFile.isDirectory()) ? "/" : ""; return prefixPath + completion + pathSeparator; } private static void addMatchingFiles(VirtualFile searchRoot, String includeText, List<VirtualFile> result) { String[] split = includeText.split("/"); if (split.length != 0) { int joinEndIndex = includeText.endsWith("/") ? split.length : split.length - 1; String childPrefix = joinEndIndex == split.length ? "" : split[split.length - 1]; VirtualFile directory = VfsUtil.findRelativeFile(StringUtil.join(split, 0, joinEndIndex, "/"), searchRoot); VirtualFile[] children = directory != null ? directory.getChildren() : null; if (children == null) return; for (VirtualFile child : children) { ErlangFileType childType = ErlangFileType.getFileType(child.getName()); if (child.getName().startsWith(childPrefix) && (child.isDirectory() || childType == ErlangFileType.HEADER || childType == ErlangFileType.MODULE)) { result.add(child); } } } } private static boolean prevIsRadix(@Nullable PsiElement element) { if (element == null) return false; ASTNode prev = FormatterUtil.getPreviousNonWhitespaceSibling(element.getNode()); return prev != null && prev.getElementType() == ErlangTypes.ERL_RADIX; } private static void suggestModules(CompletionResultSet result, PsiElement position, boolean withColon) { Project project = position.getProject(); Collection<String> names = ErlangModuleIndex.getNames(project); for (String name : names) { result.addElement( PrioritizedLookupElement .withPriority( LookupElementBuilder.create(name).withIcon(ErlangIcons.MODULE) .withInsertHandler(withColon ? new SingleCharInsertHandler(':') : null), MODULE_PRIORITY)); } } private static Collection<String> suggestKeywords(PsiElement position) { TextRange posRange = position.getTextRange(); ErlangFile posFile = (ErlangFile) position.getContainingFile(); final TextRange range = new TextRange(0, posRange.getStartOffset()); final String text = range.isEmpty() ? CompletionInitializationContext.DUMMY_IDENTIFIER : range.substring(posFile.getText()); PsiFile file = PsiFileFactory.getInstance(posFile.getProject()).createFileFromText("a.erl", ErlangLanguage.INSTANCE, text, true, false); int completionOffset = posRange.getStartOffset() - range.getStartOffset(); GeneratedParserUtilBase.CompletionState state = new GeneratedParserUtilBase.CompletionState( completionOffset) { @Override public String convertItem(Object o) { if (o instanceof IElementType && ErlangLexer.KEYWORDS.contains((IElementType) o)) return o.toString(); return o instanceof String ? (String) o : null; } }; file.putUserData(GeneratedParserUtilBase.COMPLETION_STATE_KEY, state); TreeUtil.ensureParsed(file.getNode()); return state.items; } private static class RunCompletionInsertHandler implements InsertHandler<LookupElement> { @Override public void handleInsert(final InsertionContext context, LookupElement item) { if (item.getLookupString().endsWith("/")) context.setLaterRunnable(new Runnable() { @Override public void run() { new CodeCompletionHandlerBase(CompletionType.BASIC).invokeCompletion(context.getProject(), context.getEditor()); } }); } } private static class ErlangKeywordInsertHandler extends ParenthesesInsertHandler<LookupElement> { private final boolean myNeedQuotas; private final boolean myInsertBrackets; public ErlangKeywordInsertHandler(boolean needQuotas, boolean insertBrackets) { myNeedQuotas = needQuotas; myInsertBrackets = insertBrackets; } @Override protected boolean placeCaretInsideParentheses(InsertionContext context, LookupElement item) { return true; } @Override public void handleInsert(InsertionContext context, LookupElement item) { super.handleInsert(context, item); Editor editor = context.getEditor(); Document document = editor.getDocument(); if (!document.getText().substring(context.getTailOffset()).startsWith(".")) { document.insertString(context.getTailOffset(), "."); } if (insertQuotas()) doInsert(editor, document, "\"", "\""); if (insertBrackets()) doInsert(editor, document, "[", "]"); } private static void doInsert(Editor editor, Document document, String open, String closed) { int offset = editor.getCaretModel().getOffset(); document.insertString(offset, open); document.insertString(offset + 1, closed); editor.getCaretModel().moveToOffset(offset + 1); } private boolean insertBrackets() { return myInsertBrackets; } private boolean insertQuotas() { return myNeedQuotas; } } }