/* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you 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.netbeans.modules.editor.java; import com.sun.source.tree.ClassTree; import com.sun.source.tree.IfTree; import com.sun.source.tree.StatementTree; import com.sun.source.tree.Tree; import com.sun.source.util.TreePath; import java.io.IOException; import java.util.Collections; import java.util.LinkedList; import java.util.List; import javax.swing.text.AbstractDocument; import javax.swing.text.BadLocationException; import javax.swing.text.Document; import org.netbeans.api.java.lexer.JavaTokenId; import org.netbeans.api.java.source.CompilationController; import org.netbeans.api.java.source.JavaSource; import org.netbeans.api.java.source.Task; import org.netbeans.api.lexer.Language; import org.netbeans.api.lexer.TokenHierarchy; import org.netbeans.api.lexer.TokenId; import org.netbeans.api.lexer.TokenSequence; import org.netbeans.modules.parsing.api.ParserManager; import org.netbeans.modules.parsing.api.ResultIterator; import org.netbeans.modules.parsing.api.Source; import org.netbeans.modules.parsing.api.UserTask; import org.netbeans.spi.editor.bracesmatching.BraceContext; import org.netbeans.spi.editor.bracesmatching.BracesMatcher; import org.netbeans.spi.editor.bracesmatching.BracesMatcherFactory; import org.netbeans.spi.editor.bracesmatching.MatcherContext; import org.netbeans.spi.editor.bracesmatching.support.BracesMatcherSupport; import org.openide.util.Exceptions; /** * * @author Vita Stejskal */ public final class JavaBracesMatcher implements BracesMatcher, BracesMatcherFactory, BracesMatcher.ContextLocator { private static final char [] PAIRS = new char [] { '(', ')', '[', ']', '{', '}' }; //NOI18N private static final JavaTokenId [] PAIR_TOKEN_IDS = new JavaTokenId [] { JavaTokenId.LPAREN, JavaTokenId.RPAREN, JavaTokenId.LBRACKET, JavaTokenId.RBRACKET, JavaTokenId.LBRACE, JavaTokenId.RBRACE }; private final MatcherContext context; private int originOffset; private char originChar; private char matchingChar; private boolean backward; private List<TokenSequence<?>> sequences; public JavaBracesMatcher() { this(null); } private JavaBracesMatcher(MatcherContext context) { this.context = context; } private JavaBracesMatcher(MatcherContext context, int searchOffset) { this(context); this.searchOffset = searchOffset; } private int searchOffset = -1; private int getSearchOffset() { return searchOffset >=0 ? searchOffset : context.getSearchOffset(); } // ----------------------------------------------------- // BracesMatcher implementation // ----------------------------------------------------- public int[] findOrigin() throws BadLocationException, InterruptedException { ((AbstractDocument) context.getDocument()).readLock(); try { int [] origin = BracesMatcherSupport.findChar( context.getDocument(), getSearchOffset(), context.getLimitOffset(), PAIRS ); if (origin != null) { originOffset = origin[0]; originChar = PAIRS[origin[1]]; matchingChar = PAIRS[origin[1] + origin[2]]; backward = origin[2] < 0; // Filter out block and line comments TokenHierarchy<Document> th = TokenHierarchy.get(context.getDocument()); sequences = getEmbeddedTokenSequences(th, originOffset, backward, JavaTokenId.language()); if (!sequences.isEmpty()) { // Check special tokens TokenSequence<?> seq = sequences.get(sequences.size() - 1); seq.move(originOffset); if (seq.moveNext()) { if (seq.token().id() == JavaTokenId.BLOCK_COMMENT || seq.token().id() == JavaTokenId.LINE_COMMENT ) { return null; } } } return new int [] { originOffset, originOffset + 1 }; } else { return null; } } finally { ((AbstractDocument) context.getDocument()).readUnlock(); } } public int[] findMatches() throws InterruptedException, BadLocationException { ((AbstractDocument) context.getDocument()).readLock(); try { if (!sequences.isEmpty()) { TokenSequence<?> seq = sequences.get(sequences.size() - 1); TokenHierarchy<Document> th = TokenHierarchy.get(context.getDocument()); List<TokenSequence<?>> list; if (backward) { list = th.tokenSequenceList(seq.languagePath(), 0, originOffset); } else { list = th.tokenSequenceList(seq.languagePath(), originOffset + 1, context.getDocument().getLength()); } int counter = 0; seq.move(originOffset); if (seq.moveNext()) { if (seq.token().id() == JavaTokenId.STRING_LITERAL) { for(TokenSequenceIterator tsi = new TokenSequenceIterator(list, backward); tsi.hasMore(); ) { TokenSequence<?> sq = tsi.getSequence(); if (sq.token().id() == JavaTokenId.STRING_LITERAL) { CharSequence text = sq.token().text(); if (backward) { // check the character at the left from the caret int bound = originOffset - sq.offset(); if (bound >= 0) bound = Math.min(text.length() - 1, bound); for(int i = bound - 1; i > 0; i--) { if (originChar == text.charAt(i)) { counter++; } else if (matchingChar == text.charAt(i)) { if (counter == 0) { return new int [] {sq.offset() + i, sq.offset() + i + 1}; } else { counter--; } } } } else { // check the character at the right from the caret int bound = originOffset - sq.offset() + 1; if (bound < 0 || bound > text.length()) bound = 1; for(int i = bound; i < text.length() - 1; i++) { if (originChar == text.charAt(i)) { counter++; } else if (matchingChar == text.charAt(i)) { if (counter == 0) { return new int [] {sq.offset() + i, sq.offset() + i + 1}; } else { counter--; } } } } } } return null; } } JavaTokenId originId = getTokenId(originChar); JavaTokenId lookingForId = getTokenId(matchingChar); for(TokenSequenceIterator tsi = new TokenSequenceIterator(list, backward); tsi.hasMore(); ) { TokenSequence<?> sq = tsi.getSequence(); if (originId == sq.token().id()) { counter++; } else if (lookingForId == sq.token().id()) { if (counter == 0) { matchStart = sq.offset(); return new int [] { sq.offset(), sq.offset() + sq.token().length() }; } else { counter--; } } } } return null; } finally { ((AbstractDocument) context.getDocument()).readUnlock(); } } /** * Start of the matched brace/bracket */ private int matchStart; /** * Provides better context if the matched counterpart character is the opening curly brace. */ @Override public BraceContext findContext(int originOrMatchPosition) { if (backward && matchingChar == '{') { try { return findContextBackwards(originOrMatchPosition); } catch (BadLocationException | IOException ex) { Exceptions.printStackTrace(ex); } } return null; } public BraceContext findContextBackwards(final int p2) throws BadLocationException, IOException { // sanity check, do not accept anything but the original offset for now. if (p2 != originOffset) { return null; } final int position = matchStart; JavaSource src = JavaSource.forDocument(context.getDocument()); if (src == null) { return null; } final BraceContext[] ret = new BraceContext[1]; src.runUserActionTask(new Task<CompilationController>() { @Override public void run(CompilationController ctrl) throws Exception { ctrl.toPhase(JavaSource.Phase.PARSED); TreePath path = ctrl.getTreeUtilities().pathFor(position + 1); if (path == null) { return; } StatementTree block; // unwrap compound statements, so path.getLeaf() can be matched to statement parts below if (path.getLeaf().getKind() == Tree.Kind.BLOCK) { block = ((StatementTree)path.getLeaf()); path = path.getParentPath(); } else { block = null; } switch (path.getLeaf().getKind()) { case IF: { IfTree ifTree = (IfTree)path.getLeaf(); // the path may be the else branch of the if if (block == ifTree.getElseStatement()) { // the related region is the if statement up to the 'then' statement final int[] elseStart = { (int)ctrl.getTrees().getSourcePositions().getStartPosition( ctrl.getCompilationUnit(), ifTree.getElseStatement())}; // must use lexer to iterate backwards from block start to 'else' keyword. The keyword position // is not a part of the Tree context.getDocument().render(new Runnable() { public void run() { TokenHierarchy h = TokenHierarchy.get(context.getDocument()); TokenSequence seq = h.tokenSequence(); int off = seq.move(elseStart[0]); if (off == 0 && seq.moveNext() && seq.token().id() == JavaTokenId.LBRACE) { while (seq.movePrevious()) { TokenId id = seq.token().id(); if (!(id == JavaTokenId.WHITESPACE || id == JavaTokenId.BLOCK_COMMENT || id == JavaTokenId.LINE_COMMENT)) { break; } } if (seq.token().id() == JavaTokenId.ELSE) { elseStart[0] = seq.offset(); } } } }); // the context is the else statement up to the brace position int ifStart = (int)ctrl.getTrees().getSourcePositions().getStartPosition( ctrl.getCompilationUnit(), ifTree); int ifEnd; if (ifTree.getThenStatement().getKind() == Tree.Kind.BLOCK) { ifEnd = (int)ctrl.getTrees().getSourcePositions().getStartPosition( ctrl.getCompilationUnit(), ifTree.getThenStatement()); } else { ifEnd = (int)ctrl.getTrees().getSourcePositions().getEndPosition( ctrl.getCompilationUnit(), ifTree.getCondition()); } BraceContext rel = BraceContext.create( context.getDocument().createPosition(ifStart), context.getDocument().createPosition(ifEnd + 1)); ret[0] = rel.createRelated( context.getDocument().createPosition(elseStart[0]), context.getDocument().createPosition(position + 1)); return; } } // fall through case SWITCH: case WHILE_LOOP: case METHOD: case NEW_CLASS: case CASE: { // take start of the command as the context long start = ctrl.getTrees().getSourcePositions().getStartPosition( ctrl.getCompilationUnit(), path.getLeaf()); ret[0] = BraceContext.create( context.getDocument().createPosition((int)start), context.getDocument().createPosition(position)); return; } case CLASS: { long start = ctrl.getTrees().getSourcePositions().getStartPosition( ctrl.getCompilationUnit(), block != null ? block : path.getLeaf()); ret[0] = BraceContext.create( context.getDocument().createPosition((int)start), context.getDocument().createPosition(position)); return; } default: return; } } }, true); return ret[0]; } // ----------------------------------------------------- // private implementation // ----------------------------------------------------- private JavaTokenId getTokenId(char ch) { for(int i = 0; i < PAIRS.length; i++) { if (PAIRS[i] == ch) { return PAIR_TOKEN_IDS[i]; } } return null; } public static List<TokenSequence<?>> getEmbeddedTokenSequences( TokenHierarchy<?> th, int offset, boolean backwardBias, Language<?> language ) { List<TokenSequence<?>> sequences = th.embeddedTokenSequences(offset, backwardBias); for(int i = sequences.size() - 1; i >= 0; i--) { TokenSequence<?> seq = sequences.get(i); if (seq.language() == language) { break; } else { sequences.remove(i); } } return sequences; } private static final class TokenSequenceIterator { private final List<TokenSequence<?>> list; private final boolean backward; private int index; public TokenSequenceIterator(List<TokenSequence<?>> list, boolean backward) { this.list = list; this.backward = backward; this.index = -1; } public boolean hasMore() { return backward ? hasPrevious() : hasNext(); } public TokenSequence<?> getSequence() { assert index >= 0 && index < list.size() : "No sequence available, call hasMore() first."; //NOI18N return list.get(index); } private boolean hasPrevious() { boolean anotherSeq = false; if (index == -1) { index = list.size() - 1; anotherSeq = true; } for( ; index >= 0; index--) { TokenSequence<?> seq = list.get(index); if (anotherSeq) { seq.moveEnd(); } if (seq.movePrevious()) { return true; } anotherSeq = true; } return false; } private boolean hasNext() { boolean anotherSeq = false; if (index == -1) { index = 0; anotherSeq = true; } for( ; index < list.size(); index++) { TokenSequence<?> seq = list.get(index); if (anotherSeq) { seq.moveStart(); } if (seq.moveNext()) { return true; } anotherSeq = true; } return false; } } // End of TokenSequenceIterator class // ----------------------------------------------------- // BracesMatcherFactory implementation // ----------------------------------------------------- /** */ public BracesMatcher createMatcher(MatcherContext context) { return new JavaBracesMatcher(context); } }