package org.intellij.plugins.markdown.structureView; import com.intellij.ide.IdeBundle; import com.intellij.ide.structureView.StructureViewTreeElement; import com.intellij.ide.structureView.impl.common.PsiTreeElementBase; import com.intellij.ide.util.treeView.smartTree.SortableTreeElement; import com.intellij.lang.ASTNode; import com.intellij.navigation.ItemPresentation; import com.intellij.navigation.LocationPresentation; import com.intellij.navigation.NavigationItem; import com.intellij.openapi.ui.Queryable; import com.intellij.openapi.util.text.StringUtil; import com.intellij.psi.PsiElement; import com.intellij.psi.impl.source.PsiFileImpl; import com.intellij.psi.impl.source.tree.TreeUtil; import com.intellij.psi.tree.IElementType; import com.intellij.psi.tree.TokenSet; import com.intellij.psi.util.PsiUtilCore; import com.intellij.util.Consumer; import org.intellij.plugins.markdown.lang.MarkdownElementTypes; import org.intellij.plugins.markdown.lang.psi.MarkdownPsiElement; import org.intellij.plugins.markdown.lang.psi.impl.MarkdownFile; import org.intellij.plugins.markdown.lang.psi.impl.MarkdownHeaderImpl; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import java.util.*; import static org.intellij.plugins.markdown.lang.MarkdownTokenTypeSets.*; class MarkdownStructureElement extends PsiTreeElementBase<PsiElement> implements SortableTreeElement, LocationPresentation, Queryable { static final TokenSet PRESENTABLE_TYPES = HEADERS; static final TokenSet TRANSPARENT_CONTAINERS = TokenSet.create(MARKDOWN_FILE, UNORDERED_LIST, ORDERED_LIST, LIST_ITEM, BLOCK_QUOTE); private static final ItemPresentation DUMMY_PRESENTATION = new MarkdownBasePresentation() { @Nullable @Override public String getPresentableText() { return null; } @Nullable @Override public String getLocationString() { return null; } }; private static final List<TokenSet> HEADER_ORDER = Arrays.asList( TokenSet.create(MarkdownElementTypes.MARKDOWN_FILE_ELEMENT_TYPE), HEADER_LEVEL_1_SET, HEADER_LEVEL_2_SET, HEADER_LEVEL_3_SET, HEADER_LEVEL_4_SET, HEADER_LEVEL_5_SET, HEADER_LEVEL_6_SET); MarkdownStructureElement(@NotNull PsiElement element) { super(element); } @Override public boolean canNavigate() { return getElement() instanceof NavigationItem && ((NavigationItem)getElement()).canNavigate(); } @Override public boolean canNavigateToSource() { return getElement() instanceof NavigationItem && ((NavigationItem)getElement()).canNavigateToSource(); } @Override public void navigate(boolean requestFocus) { if (getElement() instanceof NavigationItem) { ((NavigationItem)getElement()).navigate(requestFocus); } } @NotNull @Override public String getAlphaSortKey() { return StringUtil.notNullize(getElement() instanceof NavigationItem ? ((NavigationItem)getElement()).getName() : null); } @Override public boolean isSearchInLocationString() { return true; } @Nullable @Override public String getPresentableText() { final PsiElement tag = getElement(); if (tag == null) { return IdeBundle.message("node.structureview.invalid"); } return getPresentation().getPresentableText(); } @Override public String getLocationString() { return getPresentation().getLocationString(); } @NotNull @Override public ItemPresentation getPresentation() { if (getElement() instanceof PsiFileImpl) { ItemPresentation filePresent = ((PsiFileImpl)getElement()).getPresentation(); return filePresent != null ? filePresent : DUMMY_PRESENTATION; } if (getElement() instanceof NavigationItem) { final ItemPresentation itemPresent = ((NavigationItem)getElement()).getPresentation(); if (itemPresent != null) { return itemPresent; } } return DUMMY_PRESENTATION; } @NotNull @Override public Collection<StructureViewTreeElement> getChildrenBase() { final List<StructureViewTreeElement> childrenElements = new ArrayList<>(); final PsiElement myElement = getElement(); if (myElement == null) return childrenElements; final PsiElement structureContainer = myElement instanceof MarkdownFile ? myElement.getFirstChild() : getParentOfType(myElement, TRANSPARENT_CONTAINERS); if (structureContainer == null) { return Collections.emptyList(); } final MarkdownPsiElement currentHeader = myElement instanceof MarkdownHeaderImpl ? ((MarkdownHeaderImpl)myElement) : null; processContainer(structureContainer, currentHeader, currentHeader, element -> childrenElements.add(new MarkdownStructureElement(element))); return childrenElements; } private static void processContainer(@NotNull PsiElement container, @Nullable PsiElement sameLevelRestriction, @Nullable MarkdownPsiElement from, @NotNull Consumer<? super PsiElement> resultConsumer) { PsiElement nextSibling = from == null ? container.getFirstChild() : from.getNextSibling(); PsiElement maxContentLevel = null; while (nextSibling != null) { if (TRANSPARENT_CONTAINERS.contains(PsiUtilCore.getElementType(nextSibling)) && maxContentLevel == null) { processContainer(nextSibling, null, null, resultConsumer); } else if (nextSibling instanceof MarkdownHeaderImpl) { if (sameLevelRestriction != null && isSameLevelOrHigher(nextSibling, sameLevelRestriction)) { break; } if (maxContentLevel == null || isSameLevelOrHigher(nextSibling, maxContentLevel)) { maxContentLevel = nextSibling; final IElementType type = nextSibling.getNode().getElementType(); if (PRESENTABLE_TYPES.contains(type)) { resultConsumer.consume(nextSibling); } } } nextSibling = nextSibling.getNextSibling(); } } private static boolean isSameLevelOrHigher(@NotNull PsiElement psiA, @NotNull PsiElement psiB) { IElementType typeA = psiA.getNode().getElementType(); IElementType typeB = psiB.getNode().getElementType(); return headerLevel(typeA) <= headerLevel(typeB); } private static int headerLevel(@NotNull IElementType curLevelType) { for (int i = 0; i < HEADER_ORDER.size(); i++) { if (HEADER_ORDER.get(i).contains(curLevelType)) { return i; } } // not a header so return lowest level return Integer.MAX_VALUE; } @Nullable private static PsiElement getParentOfType(@NotNull PsiElement myElement, @NotNull TokenSet types) { final ASTNode parentNode = TreeUtil.findParent(myElement.getNode(), types); return parentNode == null ? null : parentNode.getPsi(); } @NotNull @Override public String getLocationPrefix() { return " "; } @NotNull @Override public String getLocationSuffix() { return ""; } @Override public void putInfo(@NotNull Map<String, String> info) { info.put("text", getPresentableText()); if (!(getElement() instanceof PsiFileImpl)) { info.put("location", getLocationString()); } } }