blob: 6ae3068a039dff493a9871d48135da81001f1275 [file] [log] [blame]
/*
* Copyright 2000-2014 JetBrains s.r.o.
*
* 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 com.intellij.xml.impl;
import com.intellij.codeInsight.highlighting.BraceMatchingUtil;
import com.intellij.codeInsight.highlighting.XmlAwareBraceMatcher;
import com.intellij.lang.BracePair;
import com.intellij.lang.LanguageBraceMatching;
import com.intellij.lang.PairedBraceMatcher;
import com.intellij.lang.Language;
import com.intellij.openapi.editor.highlighter.HighlighterIterator;
import com.intellij.openapi.fileTypes.FileType;
import com.intellij.openapi.fileTypes.StdFileTypes;
import com.intellij.psi.PsiFile;
import com.intellij.psi.TokenType;
import com.intellij.psi.tree.IElementType;
import com.intellij.psi.tree.xml.IXmlLeafElementType;
import com.intellij.psi.xml.XmlTokenType;
import com.intellij.util.containers.BidirectionalMap;
import com.intellij.xml.util.HtmlUtil;
import com.intellij.ide.highlighter.XmlLikeFileType;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.List;
/**
* @author Maxim.Mossienko
* Date: Apr 15, 2008
* Time: 4:27:25 PM
*/
public class XmlBraceMatcher implements XmlAwareBraceMatcher {
private static final int XML_TAG_TOKEN_GROUP = 1;
private static final int XML_VALUE_DELIMITER_GROUP = 2;
private static final BidirectionalMap<IElementType, IElementType> PAIRING_TOKENS = new BidirectionalMap<IElementType, IElementType>();
static {
PAIRING_TOKENS.put(XmlTokenType.XML_TAG_END, XmlTokenType.XML_START_TAG_START);
PAIRING_TOKENS.put(XmlTokenType.XML_CDATA_START, XmlTokenType.XML_CDATA_END);
PAIRING_TOKENS.put(XmlTokenType.XML_EMPTY_ELEMENT_END, XmlTokenType.XML_START_TAG_START);
PAIRING_TOKENS.put(XmlTokenType.XML_ATTRIBUTE_VALUE_START_DELIMITER, XmlTokenType.XML_ATTRIBUTE_VALUE_END_DELIMITER);
}
@Override
public int getBraceTokenGroupId(IElementType tokenType) {
final Language l = tokenType.getLanguage();
PairedBraceMatcher matcher = LanguageBraceMatching.INSTANCE.forLanguage(l);
if (matcher != null) {
BracePair[] pairs = matcher.getPairs();
for (BracePair pair : pairs) {
if (pair.getLeftBraceType() == tokenType || pair.getRightBraceType() == tokenType ) {
return l.hashCode();
}
}
}
if (tokenType instanceof IXmlLeafElementType) {
return tokenType == XmlTokenType.XML_ATTRIBUTE_VALUE_START_DELIMITER || tokenType == XmlTokenType.XML_ATTRIBUTE_VALUE_END_DELIMITER
? XML_VALUE_DELIMITER_GROUP
: XML_TAG_TOKEN_GROUP;
}
else{
return BraceMatchingUtil.UNDEFINED_TOKEN_GROUP;
}
}
@Override
public boolean isLBraceToken(HighlighterIterator iterator, CharSequence fileText, FileType fileType) {
final IElementType tokenType = iterator.getTokenType();
PairedBraceMatcher matcher = LanguageBraceMatching.INSTANCE.forLanguage(tokenType.getLanguage());
if (matcher != null) {
BracePair[] pairs = matcher.getPairs();
for (BracePair pair : pairs) {
if (pair.getLeftBraceType() == tokenType) return true;
}
}
return tokenType == XmlTokenType.XML_START_TAG_START ||
tokenType == XmlTokenType.XML_ATTRIBUTE_VALUE_START_DELIMITER ||
tokenType == XmlTokenType.XML_CDATA_START;
}
@Override
public boolean isRBraceToken(HighlighterIterator iterator, CharSequence fileText, FileType fileType) {
final IElementType tokenType = iterator.getTokenType();
PairedBraceMatcher matcher = LanguageBraceMatching.INSTANCE.forLanguage(tokenType.getLanguage());
if (matcher != null) {
BracePair[] pairs = matcher.getPairs();
for (BracePair pair : pairs) {
if (pair.getRightBraceType() == tokenType) return true;
}
}
if (tokenType == XmlTokenType.XML_EMPTY_ELEMENT_END ||
tokenType == XmlTokenType.XML_ATTRIBUTE_VALUE_END_DELIMITER ||
tokenType == XmlTokenType.XML_CDATA_END)
{
return true;
}
else if (tokenType == XmlTokenType.XML_TAG_END) {
final boolean result = findEndTagStart(iterator);
if (isFileTypeWithSingleHtmlTags(fileType)) {
final String tagName = getTagName(fileText, iterator);
if (tagName != null && HtmlUtil.isSingleHtmlTag(tagName)) {
return !result;
}
}
return result;
}
else {
return false;
}
}
protected boolean isFileTypeWithSingleHtmlTags(final FileType fileType) {
return fileType == StdFileTypes.HTML;
}
@Override
public boolean isPairBraces(IElementType tokenType1, IElementType tokenType2) {
PairedBraceMatcher matcher = LanguageBraceMatching.INSTANCE.forLanguage(tokenType1.getLanguage());
if (matcher != null) {
BracePair[] pairs = matcher.getPairs();
for (BracePair pair : pairs) {
if (pair.getLeftBraceType() == tokenType1 ) return pair.getRightBraceType() == tokenType2;
if (pair.getRightBraceType() == tokenType1 ) return pair.getLeftBraceType() == tokenType2;
}
}
if (tokenType2.equals(PAIRING_TOKENS.get(tokenType1))) return true;
List<IElementType> keys = PAIRING_TOKENS.getKeysByValue(tokenType1);
return keys != null && keys.contains(tokenType2);
}
@Override
public boolean isStructuralBrace(HighlighterIterator iterator,CharSequence text, FileType fileType) {
IElementType tokenType = iterator.getTokenType();
PairedBraceMatcher matcher = LanguageBraceMatching.INSTANCE.forLanguage(tokenType.getLanguage());
if (matcher != null) {
BracePair[] pairs = matcher.getPairs();
for (BracePair pair : pairs) {
if ((pair.getLeftBraceType() == tokenType || pair.getRightBraceType() == tokenType) &&
pair.isStructural()) return true;
}
}
if (fileType instanceof XmlLikeFileType) {
return isXmlStructuralBrace(iterator, text, fileType, tokenType);
}
return false;
}
protected boolean isXmlStructuralBrace(HighlighterIterator iterator, CharSequence text, FileType fileType, IElementType tokenType) {
return tokenType == XmlTokenType.XML_START_TAG_START ||
tokenType == XmlTokenType.XML_TAG_END ||
tokenType == XmlTokenType.XML_EMPTY_ELEMENT_END ||
tokenType == XmlTokenType.XML_TAG_END && isFileTypeWithSingleHtmlTags(fileType) && isEndOfSingleHtmlTag(text, iterator);
}
@Override
public boolean isPairedBracesAllowedBeforeType(@NotNull final IElementType lbraceType, @Nullable final IElementType contextType) {
return true;
}
@Override
public boolean isStrictTagMatching(final FileType fileType, final int braceGroupId) {
switch(braceGroupId){
case XML_TAG_TOKEN_GROUP:
// Other xml languages may have nonbalanced tag names
return isStrictTagMatchingForFileType(fileType);
default:
return false;
}
}
protected boolean isStrictTagMatchingForFileType(final FileType fileType) {
return fileType == StdFileTypes.XML ||
fileType == StdFileTypes.XHTML;
}
@Override
public boolean areTagsCaseSensitive(final FileType fileType, final int braceGroupId) {
switch(braceGroupId){
case XML_TAG_TOKEN_GROUP:
return fileType == StdFileTypes.XML;
default:
return false;
}
}
private static boolean findEndTagStart(HighlighterIterator iterator) {
IElementType tokenType = iterator.getTokenType();
int balance = 0;
int count = 0;
while(balance >= 0){
iterator.retreat();
count++;
if (iterator.atEnd()) break;
tokenType = iterator.getTokenType();
if (tokenType == XmlTokenType.XML_TAG_END || tokenType == XmlTokenType.XML_EMPTY_ELEMENT_END){
balance++;
}
else if (tokenType == XmlTokenType.XML_END_TAG_START || tokenType == XmlTokenType.XML_START_TAG_START){
balance--;
}
}
while(count-- > 0) iterator.advance();
return tokenType == XmlTokenType.XML_END_TAG_START;
}
private boolean isEndOfSingleHtmlTag(CharSequence text,HighlighterIterator iterator) {
String tagName = getTagName(text,iterator);
return tagName != null && HtmlUtil.isSingleHtmlTag(tagName);
}
@Override
public String getTagName(CharSequence fileText, HighlighterIterator iterator) {
final IElementType tokenType = iterator.getTokenType();
String name = null;
if (tokenType == XmlTokenType.XML_START_TAG_START) {
iterator.advance();
IElementType tokenType1 = iterator.atEnd() ? null : iterator.getTokenType();
boolean wasWhiteSpace = false;
if (isWhitespace(tokenType1)) {
wasWhiteSpace = true;
iterator.advance();
tokenType1 = iterator.atEnd() ? null : iterator.getTokenType();
}
if (tokenType1 == XmlTokenType.XML_TAG_NAME ||
tokenType1 == XmlTokenType.XML_NAME
) {
name = fileText.subSequence(iterator.getStart(), iterator.getEnd()).toString();
}
if (wasWhiteSpace) iterator.retreat();
iterator.retreat();
}
else if (tokenType == XmlTokenType.XML_TAG_END || tokenType == XmlTokenType.XML_EMPTY_ELEMENT_END) {
int balance = 0;
int count = 0;
IElementType tokenType1 = iterator.getTokenType();
while (balance >=0) {
iterator.retreat();
count++;
if (iterator.atEnd()) break;
tokenType1 = iterator.getTokenType();
if (tokenType1 == XmlTokenType.XML_TAG_END || tokenType1 == XmlTokenType.XML_EMPTY_ELEMENT_END) {
balance++;
}
else if (tokenType1 == XmlTokenType.XML_TAG_NAME) {
balance--;
}
}
if (tokenType1 == XmlTokenType.XML_TAG_NAME) {
name = fileText.subSequence(iterator.getStart(), iterator.getEnd()).toString();
}
while (count-- > 0) iterator.advance();
}
return name;
}
protected boolean isWhitespace(final IElementType tokenType1) {
return tokenType1 == TokenType.WHITE_SPACE;
}
@Override
public IElementType getOppositeBraceTokenType(@NotNull final IElementType type) {
PairedBraceMatcher matcher = LanguageBraceMatching.INSTANCE.forLanguage(type.getLanguage());
if (matcher != null) {
BracePair[] pairs = matcher.getPairs();
for (BracePair pair : pairs) {
if (pair.getLeftBraceType() == type ) return pair.getRightBraceType();
if (pair.getRightBraceType() == type ) return pair.getLeftBraceType();
}
}
return null;
}
@Override
public int getCodeConstructStart(final PsiFile file, int openingBraceOffset) {
return openingBraceOffset;
}
}