blob: c3fc814181b3e1beb15c12c7796585bf6531a890 [file] [log] [blame]
/*
* Copyright 2000-2013 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.codeInsight.highlighting;
import com.intellij.lang.Language;
import com.intellij.lang.LanguageBraceMatching;
import com.intellij.lang.PairedBraceMatcher;
import com.intellij.openapi.editor.Document;
import com.intellij.openapi.editor.Editor;
import com.intellij.openapi.editor.ex.EditorEx;
import com.intellij.openapi.editor.highlighter.EditorHighlighter;
import com.intellij.openapi.editor.highlighter.HighlighterIterator;
import com.intellij.openapi.extensions.Extensions;
import com.intellij.openapi.fileTypes.FileType;
import com.intellij.openapi.fileTypes.FileTypeExtensionPoint;
import com.intellij.openapi.fileTypes.LanguageFileType;
import com.intellij.openapi.util.Comparing;
import com.intellij.psi.PsiFile;
import com.intellij.psi.tree.IElementType;
import com.intellij.util.containers.Stack;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.jetbrains.annotations.TestOnly;
import java.util.HashMap;
import java.util.Map;
public class BraceMatchingUtil {
public static final int UNDEFINED_TOKEN_GROUP = -1;
private BraceMatchingUtil() {
}
public static boolean isPairedBracesAllowedBeforeTypeInFileType(@NotNull IElementType lbraceType,
final IElementType tokenType,
@NotNull FileType fileType) {
try {
return getBraceMatcher(fileType, lbraceType).isPairedBracesAllowedBeforeType(lbraceType, tokenType);
}
catch (AbstractMethodError incompatiblePluginThatWeDoNotCare) {
// Do nothing
}
return true;
}
private static final Map<FileType, BraceMatcher> BRACE_MATCHERS = new HashMap<FileType, BraceMatcher>();
public static void registerBraceMatcher(@NotNull FileType fileType, @NotNull BraceMatcher braceMatcher) {
BRACE_MATCHERS.put(fileType, braceMatcher);
}
@TestOnly
public static int getMatchedBraceOffset(@NotNull Editor editor, boolean forward, @NotNull PsiFile file) {
Document document = editor.getDocument();
int offset = editor.getCaretModel().getOffset();
EditorHighlighter editorHighlighter = ((EditorEx)editor).getHighlighter();
HighlighterIterator iterator = editorHighlighter.createIterator(offset);
boolean matched = matchBrace(document.getCharsSequence(), file.getFileType(), iterator, forward);
assert matched;
return iterator.getStart();
}
private static class MatchBraceContext {
private final CharSequence fileText;
private final FileType fileType;
private final HighlighterIterator iterator;
private final boolean forward;
private final IElementType brace1Token;
private final int group;
private final String brace1TagName;
private final boolean isStrict;
private final boolean isCaseSensitive;
@NotNull
private final BraceMatcher myMatcher;
private final Stack<IElementType> myBraceStack = new Stack<IElementType>();
private final Stack<String> myTagNameStack = new Stack<String>();
MatchBraceContext(@NotNull CharSequence fileText, @NotNull FileType fileType, @NotNull HighlighterIterator iterator, boolean forward) {
this(fileText, fileType, iterator, forward,isStrictTagMatching(getBraceMatcher(fileType, iterator), fileType, getTokenGroup(iterator.getTokenType(), fileType)));
}
MatchBraceContext(@NotNull CharSequence fileText, @NotNull FileType fileType, @NotNull HighlighterIterator iterator, boolean forward, boolean strict) {
this.fileText = fileText;
this.fileType = fileType;
this.iterator = iterator;
this.forward = forward;
myMatcher = getBraceMatcher(fileType, iterator);
brace1Token = this.iterator.getTokenType();
group = getTokenGroup(brace1Token, this.fileType);
brace1TagName = getTagName(myMatcher, this.fileText, this.iterator);
isCaseSensitive = areTagsCaseSensitive(myMatcher, this.fileType, group);
isStrict = strict;
}
boolean doBraceMatch() {
myBraceStack.clear();
myTagNameStack.clear();
myBraceStack.push(brace1Token);
if (isStrict) {
myTagNameStack.push(brace1TagName);
}
boolean matched = false;
while (true) {
if (!forward) {
iterator.retreat();
}
else {
iterator.advance();
}
if (iterator.atEnd()) {
break;
}
IElementType tokenType = iterator.getTokenType();
if (getTokenGroup(tokenType, fileType) != group) {
continue;
}
String tagName = getTagName(myMatcher, fileText, iterator);
if (!isStrict && !Comparing.equal(brace1TagName, tagName, isCaseSensitive)) continue;
if (forward ? isLBraceToken(iterator, fileText, fileType) : isRBraceToken(iterator, fileText, fileType)) {
myBraceStack.push(tokenType);
if (isStrict) {
myTagNameStack.push(tagName);
}
}
else if (forward ? isRBraceToken(iterator, fileText, fileType) : isLBraceToken(iterator, fileText, fileType)) {
IElementType topTokenType = myBraceStack.pop();
String topTagName = null;
if (isStrict) {
topTagName = myTagNameStack.pop();
}
if (!isStrict) {
final IElementType baseType = myMatcher.getOppositeBraceTokenType(tokenType);
if (myBraceStack.contains(baseType)) {
while (!isPairBraces(topTokenType, tokenType, fileType) && !myBraceStack.empty()) {
topTokenType = myBraceStack.pop();
}
}
else if ((brace1TagName == null || !brace1TagName.equals(tagName)) && !isPairBraces(topTokenType, tokenType, fileType)) {
// Ignore non-matched opposite-direction brace for non-strict processing.
myBraceStack.push(topTokenType);
continue;
}
}
if (!isPairBraces(topTokenType, tokenType, fileType)
|| isStrict && !Comparing.equal(topTagName, tagName, isCaseSensitive))
{
matched = false;
break;
}
if (myBraceStack.isEmpty()) {
matched = true;
break;
}
}
}
return matched;
}
}
public static synchronized boolean matchBrace(@NotNull CharSequence fileText,
@NotNull FileType fileType,
@NotNull HighlighterIterator iterator,
boolean forward) {
return new MatchBraceContext(fileText, fileType, iterator, forward).doBraceMatch();
}
public static synchronized boolean matchBrace(@NotNull CharSequence fileText,
@NotNull FileType fileType,
@NotNull HighlighterIterator iterator,
boolean forward,
boolean isStrict) {
return new MatchBraceContext(fileText, fileType, iterator, forward, isStrict).doBraceMatch();
}
public static boolean findStructuralLeftBrace(@NotNull FileType fileType, @NotNull HighlighterIterator iterator, @NotNull CharSequence fileText) {
final Stack<IElementType> braceStack = new Stack<IElementType>();
final Stack<String> tagNameStack = new Stack<String>();
BraceMatcher matcher = getBraceMatcher(fileType, iterator);
while (!iterator.atEnd()) {
if (isStructuralBraceToken(fileType, iterator, fileText)) {
if (isRBraceToken(iterator, fileText, fileType)) {
braceStack.push(iterator.getTokenType());
tagNameStack.push(getTagName(matcher, fileText, iterator));
}
if (isLBraceToken(iterator, fileText, fileType)) {
if (braceStack.isEmpty()) return true;
final int group = matcher.getBraceTokenGroupId(iterator.getTokenType());
final IElementType topTokenType = braceStack.pop();
final IElementType tokenType = iterator.getTokenType();
boolean isStrict = isStrictTagMatching(matcher, fileType, group);
boolean isCaseSensitive = areTagsCaseSensitive(matcher, fileType, group);
String topTagName = null;
String tagName = null;
if (isStrict) {
topTagName = tagNameStack.pop();
tagName = getTagName(matcher, fileText, iterator);
}
if (!isPairBraces(topTokenType, tokenType, fileType)
|| isStrict && !Comparing.equal(topTagName, tagName, isCaseSensitive)) {
return false;
}
}
}
iterator.retreat();
}
return false;
}
public static boolean isStructuralBraceToken(@NotNull FileType fileType, @NotNull HighlighterIterator iterator, @NotNull CharSequence text) {
BraceMatcher matcher = getBraceMatcher(fileType, iterator);
return matcher.isStructuralBrace(iterator, text, fileType);
}
public static boolean isLBraceToken(@NotNull HighlighterIterator iterator, @NotNull CharSequence fileText, @NotNull FileType fileType) {
final BraceMatcher braceMatcher = getBraceMatcher(fileType, iterator);
return braceMatcher.isLBraceToken(iterator, fileText, fileType);
}
public static boolean isRBraceToken(@NotNull HighlighterIterator iterator, @NotNull CharSequence fileText, @NotNull FileType fileType) {
final BraceMatcher braceMatcher = getBraceMatcher(fileType, iterator);
return braceMatcher.isRBraceToken(iterator, fileText, fileType);
}
public static boolean isPairBraces(@NotNull IElementType tokenType1, @NotNull IElementType tokenType2, @NotNull FileType fileType) {
BraceMatcher matcher = getBraceMatcher(fileType, tokenType1);
return matcher.isPairBraces(tokenType1, tokenType2);
}
private static int getTokenGroup(IElementType tokenType, FileType fileType) {
BraceMatcher matcher = getBraceMatcher(fileType, tokenType);
return matcher.getBraceTokenGroupId(tokenType);
}
// TODO: better name for this method
public static int findLeftmostLParen(HighlighterIterator iterator,
IElementType lparenTokenType,
CharSequence fileText,
FileType fileType) {
int lastLbraceOffset = -1;
Stack<IElementType> braceStack = new Stack<IElementType>();
for (; !iterator.atEnd(); iterator.retreat()) {
final IElementType tokenType = iterator.getTokenType();
if (isLBraceToken(iterator, fileText, fileType)) {
if (!braceStack.isEmpty()) {
IElementType topToken = braceStack.pop();
if (!isPairBraces(tokenType, topToken, fileType)) {
break; // unmatched braces
}
}
else {
if (tokenType == lparenTokenType) {
lastLbraceOffset = iterator.getStart();
}
else {
break;
}
}
}
else if (isRBraceToken(iterator, fileText, fileType)) {
braceStack.push(iterator.getTokenType());
}
}
return lastLbraceOffset;
}
public static int findLeftLParen(HighlighterIterator iterator,
IElementType lparenTokenType,
CharSequence fileText,
FileType fileType) {
int lastLbraceOffset = -1;
Stack<IElementType> braceStack = new Stack<IElementType>();
for (; !iterator.atEnd(); iterator.retreat()) {
final IElementType tokenType = iterator.getTokenType();
if (isLBraceToken(iterator, fileText, fileType)) {
if (!braceStack.isEmpty()) {
IElementType topToken = braceStack.pop();
if (!isPairBraces(tokenType, topToken, fileType)) {
break; // unmatched braces
}
}
else {
if (tokenType == lparenTokenType) {
return iterator.getStart();
}
else {
break;
}
}
}
else if (isRBraceToken(iterator, fileText, fileType)) {
braceStack.push(iterator.getTokenType());
}
}
return lastLbraceOffset;
}
// TODO: better name for this method
public static int findRightmostRParen(HighlighterIterator iterator,
IElementType rparenTokenType,
CharSequence fileText,
FileType fileType) {
int lastRbraceOffset = -1;
Stack<IElementType> braceStack = new Stack<IElementType>();
for (; !iterator.atEnd(); iterator.advance()) {
final IElementType tokenType = iterator.getTokenType();
if (isRBraceToken(iterator, fileText, fileType)) {
if (!braceStack.isEmpty()) {
IElementType topToken = braceStack.pop();
if (!isPairBraces(tokenType, topToken, fileType)) {
break; // unmatched braces
}
}
else {
if (tokenType == rparenTokenType) {
lastRbraceOffset = iterator.getStart();
}
else {
break;
}
}
}
else if (isLBraceToken(iterator, fileText, fileType)) {
braceStack.push(iterator.getTokenType());
}
}
return lastRbraceOffset;
}
private static class BraceMatcherHolder {
private static final BraceMatcher ourDefaultBraceMatcher = new DefaultBraceMatcher();
}
@NotNull
public static BraceMatcher getBraceMatcher(@NotNull FileType fileType, @NotNull HighlighterIterator iterator) {
return getBraceMatcher(fileType, iterator.getTokenType());
}
@NotNull
public static BraceMatcher getBraceMatcher(@NotNull FileType fileType, @NotNull IElementType type) {
return getBraceMatcher(fileType, type.getLanguage());
}
@NotNull
public static BraceMatcher getBraceMatcher(@NotNull FileType fileType, @NotNull Language lang) {
PairedBraceMatcher matcher = LanguageBraceMatching.INSTANCE.forLanguage(lang);
if (matcher != null) {
if (matcher instanceof XmlAwareBraceMatcher) {
return (XmlAwareBraceMatcher)matcher;
}
else {
return new PairedBraceMatcherAdapter(matcher, lang);
}
}
final BraceMatcher byFileType = getBraceMatcherByFileType(fileType);
if (byFileType != null) return byFileType;
if (fileType instanceof LanguageFileType) {
final Language language = ((LanguageFileType)fileType).getLanguage();
if (lang != language) {
final FileType type1 = lang.getAssociatedFileType();
if (type1 != null) {
final BraceMatcher braceMatcher = getBraceMatcherByFileType(type1);
if (braceMatcher != null) {
return braceMatcher;
}
}
matcher = LanguageBraceMatching.INSTANCE.forLanguage(language);
if (matcher != null) {
return new PairedBraceMatcherAdapter(matcher, language);
}
}
}
return BraceMatcherHolder.ourDefaultBraceMatcher;
}
@Nullable
private static BraceMatcher getBraceMatcherByFileType(@NotNull FileType fileType) {
BraceMatcher braceMatcher = BRACE_MATCHERS.get(fileType);
if (braceMatcher != null) return braceMatcher;
for (FileTypeExtensionPoint<BraceMatcher> ext : Extensions.getExtensions(BraceMatcher.EP_NAME)) {
if (fileType.getName().equals(ext.filetype)) {
braceMatcher = ext.getInstance();
BRACE_MATCHERS.put(fileType, braceMatcher);
return braceMatcher;
}
}
return null;
}
private static boolean isStrictTagMatching(@NotNull BraceMatcher matcher, @NotNull FileType fileType, final int group) {
return matcher instanceof XmlAwareBraceMatcher && ((XmlAwareBraceMatcher)matcher).isStrictTagMatching(fileType, group);
}
private static boolean areTagsCaseSensitive(@NotNull BraceMatcher matcher, @NotNull FileType fileType, final int tokenGroup) {
return matcher instanceof XmlAwareBraceMatcher && ((XmlAwareBraceMatcher)matcher).areTagsCaseSensitive(fileType, tokenGroup);
}
@Nullable
private static String getTagName(@NotNull BraceMatcher matcher, @NotNull CharSequence fileText, @NotNull HighlighterIterator iterator) {
if (matcher instanceof XmlAwareBraceMatcher) return ((XmlAwareBraceMatcher)matcher).getTagName(fileText, iterator);
return null;
}
private static class DefaultBraceMatcher implements BraceMatcher {
@Override
public int getBraceTokenGroupId(final IElementType tokenType) {
return UNDEFINED_TOKEN_GROUP;
}
@Override
public boolean isLBraceToken(final HighlighterIterator iterator, final CharSequence fileText, final FileType fileType) {
return false;
}
@Override
public boolean isRBraceToken(final HighlighterIterator iterator, final CharSequence fileText, final FileType fileType) {
return false;
}
@Override
public boolean isPairBraces(final IElementType tokenType, final IElementType tokenType2) {
return false;
}
@Override
public boolean isStructuralBrace(final HighlighterIterator iterator, final CharSequence text, final FileType fileType) {
return false;
}
@Override
public IElementType getOppositeBraceTokenType(@NotNull final IElementType type) {
return null;
}
@Override
public boolean isPairedBracesAllowedBeforeType(@NotNull final IElementType lbraceType, @Nullable final IElementType contextType) {
return true;
}
@Override
public int getCodeConstructStart(final PsiFile file, final int openingBraceOffset) {
return openingBraceOffset;
}
}
}