| /* |
| * 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.php.editor; |
| |
| import java.util.Arrays; |
| 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 org.netbeans.api.lexer.Token; |
| import org.netbeans.api.lexer.TokenId; |
| import org.netbeans.api.lexer.TokenSequence; |
| import org.netbeans.api.lexer.TokenUtilities; |
| import org.netbeans.editor.BaseDocument; |
| import org.netbeans.modules.csl.api.OffsetRange; |
| import org.netbeans.modules.php.editor.lexer.LexUtilities; |
| import org.netbeans.modules.php.editor.lexer.PHPTokenId; |
| import org.netbeans.spi.editor.bracesmatching.BraceContext; |
| import org.netbeans.spi.editor.bracesmatching.BracesMatcher; |
| import org.netbeans.spi.editor.bracesmatching.MatcherContext; |
| |
| /** |
| * Implementation of BracesMatcher interface for PHP. It is based on original code |
| * from PHPBracketCompleter.findMatching |
| * |
| * @author Marek Slama |
| */ |
| public final class PHPBracesMatcher implements BracesMatcher, BracesMatcher.ContextLocator { |
| |
| private static final Logger LOGGER = Logger.getLogger(PHPBracesMatcher.class.getName()); |
| |
| MatcherContext context; |
| |
| private boolean findBackward; |
| private int originOffset; |
| private int matchingOffset; |
| private String matchingText; |
| |
| public PHPBracesMatcher(MatcherContext context) { |
| this.context = context; |
| } |
| |
| @Override |
| public int [] findOrigin() throws InterruptedException, BadLocationException { |
| ((AbstractDocument) context.getDocument()).readLock(); |
| try { |
| BaseDocument doc = (BaseDocument) context.getDocument(); |
| int offset = context.getSearchOffset(); |
| |
| TokenSequence<?extends PHPTokenId> ts = LexUtilities.getPHPTokenSequence(doc, offset); |
| |
| if (ts != null) { |
| // #240157 |
| if (searchForward(ts, offset)){ |
| offset--; |
| if (offset < 0) { |
| return null; |
| } |
| } |
| |
| ts.move(offset); |
| |
| if (!ts.moveNext()) { |
| return null; |
| } |
| |
| Token<?extends PHPTokenId> token = ts.token(); |
| |
| if (token == null) { |
| return null; |
| } |
| |
| TokenId id = token.id(); |
| |
| originOffset = ts.offset(); |
| if (LexUtilities.textEquals(token.text(), '(')) { |
| return new int [] {ts.offset(), ts.offset() + token.length()}; |
| } else if (LexUtilities.textEquals(token.text(), ')')) { |
| return new int [] {ts.offset(), ts.offset() + token.length()}; |
| } else if (id == PHPTokenId.PHP_CURLY_OPEN) { |
| return new int [] {ts.offset(), ts.offset() + token.length()}; |
| } else if (id == PHPTokenId.PHP_CURLY_CLOSE) { |
| return new int [] {ts.offset(), ts.offset() + token.length()}; |
| } else if (LexUtilities.textEquals(token.text(), '[')) { |
| return new int [] {ts.offset(), ts.offset() + token.length()}; |
| } else if (LexUtilities.textEquals(token.text(), ']')) { |
| return new int [] {ts.offset(), ts.offset() + token.length()}; |
| } else if (LexUtilities.textEquals(token.text(), '$', '{')) { |
| return new int [] {ts.offset(), ts.offset() + token.length()}; |
| } else if (LexUtilities.textEquals(token.text(), ':')) { |
| do { |
| ts.movePrevious(); |
| token = LexUtilities.findPreviousToken(ts, |
| Arrays.asList(PHPTokenId.PHP_IF, PHPTokenId.PHP_ELSE, PHPTokenId.PHP_ELSEIF, |
| PHPTokenId.PHP_FOR, PHPTokenId.PHP_FOREACH, PHPTokenId.PHP_WHILE, PHPTokenId.PHP_SWITCH, |
| PHPTokenId.PHP_OPENTAG, PHPTokenId.PHP_CURLY_CLOSE, PHPTokenId.PHP_CASE, |
| PHPTokenId.PHP_TOKEN)); |
| id = token.id(); |
| } while (id == PHPTokenId.PHP_TOKEN && !TokenUtilities.textEquals(token.text(), ":")); // NOI18N |
| if (id == PHPTokenId.PHP_IF || id == PHPTokenId.PHP_ELSE || id == PHPTokenId.PHP_ELSEIF |
| || id == PHPTokenId.PHP_FOR || id == PHPTokenId.PHP_FOREACH || id == PHPTokenId.PHP_WHILE |
| || id == PHPTokenId.PHP_SWITCH) { |
| ts.move(offset); |
| ts.moveNext(); |
| token = ts.token(); |
| return new int [] {ts.offset(), ts.offset() + token.length()}; |
| } |
| } else if (id == PHPTokenId.PHP_ENDFOR || id == PHPTokenId.PHP_ENDFOREACH |
| || id == PHPTokenId.PHP_ENDIF || id == PHPTokenId.PHP_ENDSWITCH |
| || id == PHPTokenId.PHP_ENDWHILE) { |
| return new int [] {ts.offset(), ts.offset() + token.length()}; |
| } else if (id == PHPTokenId.PHP_ELSEIF || id == PHPTokenId.PHP_ELSE) { |
| while (token.id() != PHPTokenId.PHP_CURLY_OPEN && !":".equals(token.text().toString()) && ts.moveNext()) { |
| token = LexUtilities.findNextToken(ts, Arrays.asList(PHPTokenId.PHP_TOKEN, PHPTokenId.PHP_CURLY_OPEN)); |
| } |
| if (token.id() == PHPTokenId.PHP_TOKEN && TokenUtilities.textEquals(token.text(), ":") && ts.moveNext()) { // NOI18N |
| ts.move(offset); |
| ts.moveNext(); |
| token = ts.token(); |
| return new int [] {ts.offset(), ts.offset() + token.length()}; |
| } |
| } else { |
| originOffset = -1; |
| } |
| |
| } |
| return null; |
| } finally { |
| ((AbstractDocument) context.getDocument()).readUnlock(); |
| } |
| } |
| |
| @Override |
| public int [] findMatches() throws InterruptedException, BadLocationException { |
| ((AbstractDocument) context.getDocument()).readLock(); |
| try { |
| BaseDocument doc = (BaseDocument) context.getDocument(); |
| int offset = context.getSearchOffset(); |
| |
| TokenSequence<?extends PHPTokenId> ts = LexUtilities.getPHPTokenSequence(doc, offset); |
| |
| if (ts != null) { |
| // #240157 |
| if (searchForward(ts, offset)){ |
| offset--; |
| if (offset < 0) { |
| return null; |
| } |
| } |
| |
| ts.move(offset); |
| |
| if (!ts.moveNext()) { |
| return null; |
| } |
| |
| Token<?extends PHPTokenId> token = ts.token(); |
| |
| if (token == null) { |
| return null; |
| } |
| |
| TokenId id = token.id(); |
| |
| OffsetRange r = null; |
| matchingText = ""; // NOI18N |
| findBackward = false; |
| try { |
| if (LexUtilities.textEquals(token.text(), '(')) { |
| matchingText = ")"; // NOI18N |
| r = LexUtilities.findFwd(doc, ts, PHPTokenId.PHP_TOKEN, '(', PHPTokenId.PHP_TOKEN, ')'); |
| return new int [] {r.getStart(), r.getEnd() }; |
| } else if (LexUtilities.textEquals(token.text(), ')')) { |
| findBackward = true; |
| matchingText = "("; // NOI18N |
| r = LexUtilities.findBwd(doc, ts, PHPTokenId.PHP_TOKEN, '(', PHPTokenId.PHP_TOKEN, ')'); |
| return new int [] {r.getStart(), r.getEnd() }; |
| } else if (id == PHPTokenId.PHP_CURLY_OPEN) { |
| matchingText = "}"; // NOI18N |
| r = LexUtilities.findFwd(doc, ts, PHPTokenId.PHP_CURLY_OPEN, '{', PHPTokenId.PHP_CURLY_CLOSE, '}'); |
| return new int [] {r.getStart(), r.getEnd() }; |
| } else if (id == PHPTokenId.PHP_CURLY_CLOSE) { |
| findBackward = true; |
| r = LexUtilities.findBwd(doc, ts, PHPTokenId.PHP_CURLY_OPEN, '{', PHPTokenId.PHP_CURLY_CLOSE, '}'); |
| matchingText = r.getLength() == 1 ? "{" : "${"; // NOI18N |
| return new int [] {r.getStart(), r.getEnd() }; |
| } else if (LexUtilities.textEquals(token.text(), '[')) { |
| matchingText = "]"; // NOI18N |
| r = LexUtilities.findFwd(doc, ts, PHPTokenId.PHP_TOKEN, '[', PHPTokenId.PHP_TOKEN, ']'); |
| return new int [] {r.getStart(), r.getEnd() }; |
| } else if (LexUtilities.textEquals(token.text(), ']')) { |
| matchingText = "["; // NOI18N |
| findBackward = true; |
| r = LexUtilities.findBwd(doc, ts, PHPTokenId.PHP_TOKEN, '[', PHPTokenId.PHP_TOKEN, ']'); |
| return new int [] {r.getStart(), r.getEnd() }; |
| } else if (LexUtilities.textEquals(token.text(), '$', '{')) { |
| matchingText = "}"; // NOI18N |
| r = LexUtilities.findFwd(doc, ts, PHPTokenId.PHP_TOKEN, '{', PHPTokenId.PHP_CURLY_CLOSE, '}'); |
| return new int [] {r.getStart(), r.getEnd() }; |
| } else if (LexUtilities.textEquals(token.text(), ':')) { |
| r = LexUtilities.findFwdAlternativeSyntax(doc, ts, token); |
| Token<? extends PHPTokenId> t = ts.token(); |
| matchingText = t == null ? "" : t.text().toString(); // NOI18N |
| return new int [] {r.getStart(), r.getEnd() }; |
| } else if (id == PHPTokenId.PHP_ENDFOR || id == PHPTokenId.PHP_ENDFOREACH |
| || id == PHPTokenId.PHP_ENDIF || id == PHPTokenId.PHP_ENDSWITCH |
| || id == PHPTokenId.PHP_ENDWHILE || id == PHPTokenId.PHP_ELSEIF |
| || id == PHPTokenId.PHP_ELSE) { |
| findBackward = true; |
| r = LexUtilities.findBwdAlternativeSyntax(doc, ts, token); |
| matchingText = ":"; // NOI18N |
| return new int [] {r.getStart(), r.getEnd() }; |
| } |
| } finally { |
| matchingOffset = r != null ? r.getStart() : -1; |
| } |
| } |
| return null; |
| } finally { |
| ((AbstractDocument) context.getDocument()).readUnlock(); |
| } |
| } |
| |
| private boolean searchForward(TokenSequence<? extends PHPTokenId> ts, int offset) { |
| // if there is a brace token just before a caret position, search foward |
| // e.g. if (isSomething()^), if (isSomething())^{ |
| // "^" is the caret |
| if (context.isSearchingBackward()) { |
| ts.move(offset); |
| if (ts.movePrevious()) { |
| Token<? extends PHPTokenId> previousToken = ts.token(); |
| if (previousToken != null && isBraceToken(previousToken)) { |
| return true; |
| } |
| } |
| } |
| return false; |
| } |
| |
| private static boolean isBraceToken(Token<? extends PHPTokenId> token) { |
| PHPTokenId id = token.id(); |
| return LexUtilities.textEquals(token.text(), '(') // NOI18N |
| || LexUtilities.textEquals(token.text(), ')') // NOI18N |
| || id == PHPTokenId.PHP_CURLY_OPEN |
| || id == PHPTokenId.PHP_CURLY_CLOSE |
| || LexUtilities.textEquals(token.text(), '[') // NOI18N |
| || LexUtilities.textEquals(token.text(), ']') // NOI18N |
| || LexUtilities.textEquals(token.text(), '$', '{') // NOI18N |
| || LexUtilities.textEquals(token.text(), ':') // NOI18N |
| || id == PHPTokenId.PHP_ENDFOR |
| || id == PHPTokenId.PHP_ENDFOREACH |
| || id == PHPTokenId.PHP_ENDIF |
| || id == PHPTokenId.PHP_ENDSWITCH |
| || id == PHPTokenId.PHP_ENDWHILE |
| || id == PHPTokenId.PHP_ELSEIF |
| || id == PHPTokenId.PHP_ELSE; |
| } |
| |
| @Override |
| public BraceContext findContext(int originOrMatchPosition) { |
| if (findBackward && (matchingText.equals("{") || matchingText.equals(":"))) { // NOI18N |
| if (originOffset != originOrMatchPosition) { |
| return null; |
| } |
| try { |
| return findContextBackwards(); |
| } catch (BadLocationException ex) { |
| LOGGER.log(Level.WARNING, "incorrect offset: " + ex.offsetRequested(), ex); |
| } |
| } |
| return null; |
| } |
| |
| private BraceContext findContextBackwards() throws BadLocationException { |
| ((AbstractDocument) context.getDocument()).readLock(); |
| try { |
| BaseDocument doc = (BaseDocument) context.getDocument(); |
| TokenSequence<?extends PHPTokenId> ts = LexUtilities.getPHPTokenSequence(doc, matchingOffset); |
| if (ts == null) { |
| return null; |
| } |
| ts.move(matchingOffset); |
| if (!ts.moveNext()) { |
| return null; |
| } |
| List<PHPTokenId> lookfor = Arrays.asList( |
| PHPTokenId.PHP_CURLY_CLOSE, // terminator |
| PHPTokenId.PHP_CLASS, PHPTokenId.PHP_INTERFACE, PHPTokenId.PHP_TRAIT, PHPTokenId.PHP_FUNCTION, |
| PHPTokenId.PHP_FOR, PHPTokenId.PHP_FOREACH, |
| PHPTokenId.PHP_DO, PHPTokenId.PHP_WHILE, |
| PHPTokenId.PHP_TRY, PHPTokenId.PHP_CATCH, PHPTokenId.PHP_FINALLY, |
| PHPTokenId.PHP_IF, PHPTokenId.PHP_ELSE, PHPTokenId.PHP_ELSEIF, |
| PHPTokenId.PHP_SWITCH, PHPTokenId.PHP_USE |
| ); |
| Token<? extends PHPTokenId> previousToken = LexUtilities.findPreviousToken(ts, lookfor); |
| if (previousToken == null || previousToken.id() == PHPTokenId.PHP_CURLY_CLOSE) { |
| return null; |
| } |
| |
| PHPTokenId id = previousToken.id(); |
| switch (id) { |
| case PHP_ELSE: |
| return getBraceContextForIfStatement(ts); |
| default: |
| return getBraceContext(ts.offset()); |
| } |
| } finally { |
| ((AbstractDocument) context.getDocument()).readUnlock(); |
| } |
| } |
| |
| private BraceContext getBraceContextForIfStatement(TokenSequence<? extends PHPTokenId> ts) throws BadLocationException { |
| // find "if" |
| int elseStart = ts.offset(); |
| if (elseStart < 0 || matchingOffset < elseStart) { |
| return null; |
| } |
| int balance = 0; |
| int ifStart = -1; |
| int ifEnd = -1; |
| String lastBrace = null; |
| boolean found = false; |
| boolean isAlternative = ":".equals(matchingText); // NOI18N |
| while(ts.movePrevious()) { |
| Token<? extends PHPTokenId> token = ts.token(); |
| PHPTokenId id = token.id(); |
| switch (id) { |
| case PHP_ENDIF: |
| if (isAlternative) { |
| balance++; |
| } |
| break; |
| case PHP_ELSEIF: // fall-through |
| case PHP_IF: |
| if (matchingText.equals(lastBrace)) { |
| if (balance == 0) { |
| ifStart = ts.offset(); |
| found = true; |
| } |
| if (isAlternative && id == PHPTokenId.PHP_IF) { |
| balance--; |
| } |
| } |
| break; |
| case PHP_CURLY_CLOSE: |
| if (!isAlternative) { |
| balance++; |
| } |
| break; |
| case PHP_CURLY_OPEN: |
| if (!isAlternative) { |
| balance--; |
| ifEnd = ts.offset(); |
| } |
| lastBrace = token.text().toString(); |
| break; |
| case PHP_TOKEN: |
| if (isColon(token)) { |
| if (isAlternative) { |
| ifEnd = ts.offset(); |
| } |
| lastBrace = token.text().toString(); |
| } |
| if (isComplexSyntaxOpen(token)) { |
| if (!isAlternative) { |
| balance--; |
| } |
| lastBrace = token.text().toString(); |
| } |
| break; |
| default: |
| break; |
| } |
| if (found) { |
| break; |
| } |
| } |
| if (!found || ifStart == -1 || ifEnd == -1) { |
| // broken code |
| return getBraceContext(elseStart); |
| } |
| BraceContext braceContext = BraceContext.create( |
| context.getDocument().createPosition(ifStart), |
| context.getDocument().createPosition(ifEnd + 1) // + "{" or ":" |
| ); |
| return braceContext.createRelated( |
| context.getDocument().createPosition(elseStart), |
| context.getDocument().createPosition(matchingOffset + 1) // + "{" or ":" |
| ); |
| } |
| |
| private BraceContext getBraceContext(int start) throws BadLocationException { |
| return BraceContext.create( |
| context.getDocument().createPosition(start), |
| context.getDocument().createPosition(matchingOffset + 1) // + "{" or ":" |
| ); |
| } |
| |
| private static boolean isColon(Token<? extends PHPTokenId> token) { |
| return token.id() == PHPTokenId.PHP_TOKEN && TokenUtilities.textEquals(token.text(), ":"); // NOI18N |
| } |
| |
| private static boolean isComplexSyntaxOpen(Token<? extends PHPTokenId> token) { |
| return token.id() == PHPTokenId.PHP_TOKEN && TokenUtilities.textEquals(token.text(), "${"); // NOI18N |
| } |
| |
| } |