| /* |
| * 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.javadoc; |
| |
| import java.util.Arrays; |
| import java.util.Collection; |
| import java.util.HashSet; |
| import java.util.List; |
| import java.util.logging.Level; |
| import java.util.logging.Logger; |
| import javax.swing.text.AbstractDocument; |
| import javax.swing.text.BadLocationException; |
| import javax.swing.text.Document; |
| import org.netbeans.api.java.lexer.JavadocTokenId; |
| import org.netbeans.api.lexer.Token; |
| import org.netbeans.api.lexer.TokenHierarchy; |
| import org.netbeans.api.lexer.TokenId; |
| import org.netbeans.api.lexer.TokenSequence; |
| import org.netbeans.api.lexer.TokenUtilities; |
| 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; |
| |
| /** |
| * |
| * @author Vita Stejskal |
| */ |
| public final class JavadocBracesMatcher implements BracesMatcher, BracesMatcherFactory { |
| |
| private static final Logger LOG = Logger.getLogger(JavadocBracesMatcher.class.getName()); |
| private static final Collection<String> VOID_TAGS = new HashSet<>(Arrays.asList(new String[]{ |
| "<area>", "<base>", "<br>", "<col>", "<command>", "<embed>", "<hr>", "<img>", //NOI18N |
| "<input>", "<keygen>", "<link>", "<meta>", "<param>", "<source>", "<track>", "<wbr>" //NOI18N |
| })); |
| |
| private final MatcherContext context; |
| |
| private TokenSequence<? extends TokenId> jdocSeq; |
| private int jdocStart; |
| private int jdocEnd; |
| |
| // private int [] matchingArea; |
| |
| private BracesMatcher defaultMatcher; |
| |
| public JavadocBracesMatcher() { |
| this(null); |
| } |
| |
| private JavadocBracesMatcher(MatcherContext context) { |
| this.context = context; |
| } |
| |
| // ----------------------------------------------------- |
| // BracesMatcher implementation |
| // ----------------------------------------------------- |
| |
| public int[] findOrigin() throws BadLocationException, InterruptedException { |
| ((AbstractDocument) context.getDocument()).readLock(); |
| try { |
| int caretOffset = context.getSearchOffset(); |
| boolean backward = context.isSearchingBackward(); |
| |
| TokenHierarchy<Document> th = TokenHierarchy.get(context.getDocument()); |
| List<TokenSequence<?>> sequences = th.embeddedTokenSequences(caretOffset, backward); |
| |
| for(int i = sequences.size() - 1; i >= 0; i--) { |
| TokenSequence<? extends TokenId> seq = sequences.get(i); |
| if (seq.language() == JavadocTokenId.language()) { |
| jdocSeq = seq; |
| if (i > 0) { |
| TokenSequence<? extends TokenId> javaSeq = sequences.get(i - 1); |
| jdocStart = javaSeq.offset(); |
| jdocEnd = javaSeq.offset() + javaSeq.token().length(); |
| } else { |
| // jdocSeq is the top level sequence, ie the whole document is just javadoc |
| jdocStart = 0; |
| jdocEnd = context.getDocument().getLength(); |
| } |
| break; |
| } |
| } |
| |
| if (jdocSeq == null) { |
| if (LOG.isLoggable(Level.FINE)) { |
| LOG.fine("Not javadoc TokenSequence."); //NOI18N |
| } |
| return null; |
| } |
| |
| // if (caretOffset >= jdocStart && |
| // ((backward && caretOffset <= jdocStart + 3) || |
| // (!backward && caretOffset < jdocStart + 3)) |
| // ) { |
| // matchingArea = new int [] { jdocEnd - 2, jdocEnd }; |
| // return new int [] { jdocStart, jdocStart + 3 }; |
| // } |
| // |
| // if (caretOffset <= jdocEnd && |
| // ((backward && caretOffset > jdocEnd - 2) || |
| // (!backward && caretOffset >= jdocEnd - 2)) |
| // ) { |
| // matchingArea = new int [] { jdocStart, jdocStart + 3 }; |
| // return new int [] { jdocEnd - 2, jdocEnd }; |
| // } |
| |
| // look for tags first |
| jdocSeq.move(caretOffset); |
| if (jdocSeq.moveNext()) { |
| if (isTag(jdocSeq.token()) && !isTypeParameterTag(jdocSeq) && !isUninterpretedTag(jdocSeq)) { |
| if (jdocSeq.offset() < caretOffset || !backward) { |
| return prepareOffsets(jdocSeq, true); |
| } |
| } |
| |
| while(moveTheSequence(jdocSeq, backward, context.getLimitOffset())) { |
| if (isTag(jdocSeq.token())) { |
| if (isTypeParameterTag(jdocSeq) || isUninterpretedTag(jdocSeq)) { |
| // do not treat type parameter and {@code} and {@literal} content as HTML tag |
| break; |
| } |
| return prepareOffsets(jdocSeq, true); |
| } |
| } |
| } |
| |
| defaultMatcher = BracesMatcherSupport.defaultMatcher(context, jdocStart, jdocEnd); |
| return defaultMatcher.findOrigin(); |
| } finally { |
| ((AbstractDocument) context.getDocument()).readUnlock(); |
| } |
| } |
| |
| public int[] findMatches() throws InterruptedException, BadLocationException { |
| ((AbstractDocument) context.getDocument()).readLock(); |
| try { |
| if (defaultMatcher != null) { |
| return defaultMatcher.findMatches(); |
| } |
| |
| // if (matchingArea != null) { |
| // return matchingArea; |
| // } |
| |
| assert jdocSeq != null : "No javadoc token sequence"; //NOI18N |
| |
| Token<? extends TokenId> tag = jdocSeq.token(); |
| assert tag.id() == JavadocTokenId.HTML_TAG : "Wrong token"; //NOI18N |
| |
| if (isSingleTag(tag) || isVoidTag(tag)) { |
| return new int [] { jdocSeq.offset(), jdocSeq.offset() + jdocSeq.token().length() }; |
| } |
| |
| boolean backward = !isOpeningTag(tag); |
| int cnt = 0; |
| |
| while(moveTheSequence(jdocSeq, backward, -1)) { |
| if (!isTag(jdocSeq.token())) { |
| continue; |
| } |
| |
| if (matchTags(tag, jdocSeq.token())) { |
| if ((backward && !isOpeningTag(jdocSeq.token())) || |
| (!backward && isOpeningTag(jdocSeq.token())) |
| ) { |
| cnt++; |
| } else { |
| if (cnt == 0) { |
| return prepareOffsets(jdocSeq, false); |
| } else { |
| cnt--; |
| } |
| } |
| } |
| } |
| |
| return null; |
| } finally { |
| ((AbstractDocument) context.getDocument()).readUnlock(); |
| } |
| } |
| |
| // ----------------------------------------------------- |
| // private implementation |
| // ----------------------------------------------------- |
| |
| private boolean moveTheSequence(TokenSequence<? extends TokenId> seq, boolean backward, int offsetLimit) { |
| if (backward) { |
| if (seq.movePrevious()) { |
| int e = seq.offset() + seq.token().length(); |
| return offsetLimit == -1 ? true : e > offsetLimit; |
| } |
| } else { |
| if (seq.moveNext()) { |
| int s = seq.offset(); |
| return offsetLimit == -1 ? true : s < offsetLimit; |
| } |
| } |
| return false; |
| } |
| |
| private static boolean isTag(Token<? extends TokenId> tag) { |
| CharSequence s = tag.text(); |
| int l = s.length(); |
| |
| boolean b = tag.id() == JavadocTokenId.HTML_TAG && |
| l >= 3 && |
| s.charAt(0) == '<' && //NOI18N |
| s.charAt(l - 1) == '>'; //NOI18N |
| |
| if (b) { |
| if (s.charAt(1) == '/') { //NOI18N |
| b = l >= 4 && Character.isLetterOrDigit(s.charAt(2)); |
| } else { |
| b = Character.isLetterOrDigit(s.charAt(1)); |
| } |
| } |
| |
| return b; |
| } |
| |
| private static boolean isSingleTag(Token<? extends TokenId> tag) { |
| return TokenUtilities.endsWith(tag.text(), "/>"); //NOI18N |
| } |
| |
| private static boolean isVoidTag(Token<? extends TokenId> tag) { |
| return VOID_TAGS.contains(tag.text().toString()); |
| } |
| |
| private static boolean isOpeningTag(Token<? extends TokenId> tag) { |
| return !TokenUtilities.startsWith(tag.text(), "</"); //NOI18N |
| } |
| |
| /** |
| * simple check whether selected token is type parameter {@code @param <T>} |
| * @param seq token sequence with selected token |
| * @return {@code true} when the token should not be interpreted. |
| */ |
| private static boolean isTypeParameterTag(TokenSequence<? extends TokenId> seq) { |
| int index = seq.index(); |
| try { |
| if (!seq.movePrevious() || seq.token().id() != JavadocTokenId.OTHER_TEXT) { |
| return false; |
| } |
| |
| return seq.movePrevious() && seq.token().id() == JavadocTokenId.TAG |
| && "@param".contentEquals(seq.token().text()); // NOI18N |
| } finally { |
| seq.moveIndex(index); |
| seq.moveNext(); |
| } |
| } |
| |
| /** |
| * simple check whether selected token is part of {@code {@code} or {@literal}} |
| * @param seq token sequence with selected token |
| * @return {@code true} when the token should not be interpreted. |
| */ |
| private static boolean isUninterpretedTag(TokenSequence<? extends TokenId> seq) { |
| int index = seq.index(); |
| try { |
| boolean lastCheck = false; |
| while (seq.movePrevious()) { |
| Token<? extends TokenId> token = seq.token(); |
| if (token.id() == JavadocTokenId.OTHER_TEXT) { |
| if (lastCheck) { |
| return token.text().charAt(token.length() - 1) == '{'; |
| } else if (TokenUtilities.indexOf(token.text(), '}') >= 0) { |
| return false; |
| } |
| } |
| |
| if (token.id() == JavadocTokenId.TAG) { |
| CharSequence text = token.text(); |
| lastCheck = "@literal".contentEquals(text) || "@code".contentEquals(text); // NOI18N |
| continue; |
| } |
| } |
| return false; |
| } finally { |
| seq.moveIndex(index); |
| seq.moveNext(); |
| } |
| } |
| |
| private static boolean matchTags(Token<? extends TokenId> t1, Token<? extends TokenId> t2) { |
| assert t1.length() >= 2 && t1.text().charAt(0) == '<' : t1 + " is not a tag."; //NOI18N |
| assert t2.length() >= 2 && t2.text().charAt(0) == '<' : t2 + " is not a tag."; //NOI18N |
| |
| int idx1 = 1; |
| int idx2 = 1; |
| |
| if (t1.text().charAt(1) == '/') { |
| idx1++; |
| } |
| |
| if (t2.text().charAt(1) == '/') { |
| idx2++; |
| } |
| |
| for( ; idx1 < t1.length() && idx2 < t2.length(); idx1++, idx2++) { |
| char ch1 = t1.text().charAt(idx1); |
| char ch2 = t2.text().charAt(idx2); |
| |
| if (ch1 != ch2) { |
| return !Character.isLetterOrDigit(ch1) || !Character.isLetterOrDigit(ch2); |
| } |
| |
| if (!Character.isLetterOrDigit(ch1)) { |
| return true; |
| } |
| } |
| |
| return false; |
| } |
| |
| private static int [] prepareOffsets(TokenSequence<? extends TokenId> seq, boolean includeToken) { |
| int s = seq.offset(); |
| int e = seq.offset() + seq.token().length(); |
| CharSequence token = seq.token().text(); |
| |
| if (token.charAt(1) == '/') { //NOI18N |
| return new int [] { s, e }; |
| } else { |
| int he = e; |
| |
| for(int i = 1; i < token.length(); i++) { |
| char ch = token.charAt(i); |
| if (!Character.isLetterOrDigit(ch) && ch != '>') { //NOI18N |
| he = s + i; |
| break; |
| } |
| } |
| |
| if (includeToken) { |
| // first the boundaries, than the highlight |
| return new int [] { s, e, s, he }; |
| } else { |
| return new int [] { s, he }; |
| } |
| } |
| } |
| |
| // ----------------------------------------------------- |
| // BracesMatcherFactory implementation |
| // ----------------------------------------------------- |
| |
| /** */ |
| public BracesMatcher createMatcher(MatcherContext context) { |
| return new JavadocBracesMatcher(context); |
| } |
| |
| } |