/******************************************************************************* * Copyright (c) 2006, 2012 IBM Corporation and others. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at * http://www.eclipse.org/legal/epl-v10.html * * Contributors: * Christian Plesner Hansen (plesner@quenta.org) - initial API and implementation *******************************************************************************/ package org.eclipse.jface.text.source; import org.eclipse.core.runtime.Assert; import org.eclipse.jface.text.BadLocationException; import org.eclipse.jface.text.IDocument; import org.eclipse.jface.text.IDocumentExtension3; import org.eclipse.jface.text.IRegion; import org.eclipse.jface.text.ITypedRegion; import org.eclipse.jface.text.Region; import org.eclipse.jface.text.TextUtilities; /** * A character pair matcher that matches a specified set of character * pairs against each other. Only characters that occur in the same * partitioning are matched. * * @since 3.3 */ public class DefaultCharacterPairMatcher implements ICharacterPairMatcher, ICharacterPairMatcherExtension { private int fAnchor= -1; private final CharPairs fPairs; private final String fPartitioning; private final boolean fCaretEitherSideOfBracket; /** * Creates a new character pair matcher that matches the specified characters within the * specified partitioning. The specified list of characters must have the form
{ * start, end, start, end, ..., start, end * }
For instance: * *
	 * char[] chars = new char[] {'(', ')', '{', '}', '[', ']'};
	 * new DefaultCharacterPairMatcher(chars, ...);
	 * 
* * @param chars a list of characters * @param partitioning the partitioning to match within */ public DefaultCharacterPairMatcher(char[] chars, String partitioning) { this(chars, partitioning, false); } /** * Creates a new character pair matcher that matches the specified characters within the * specified partitioning. The specified list of characters must have the form
{ * start, end, start, end, ..., start, end * }
For instance: * *
	 * char[] chars = new char[] {'(', ')', '{', '}', '[', ']'};
	 * new DefaultCharacterPairMatcher(chars, ...);
	 * 
* * @param chars a list of characters * @param partitioning the partitioning to match within * @param caretEitherSideOfBracket controls the matching behavior. When true, the * matching peer will be found when the caret is placed either before or after a * character. When false, the matching peer will be found only when the * caret is placed after a character. * @since 3.8 */ public DefaultCharacterPairMatcher(char[] chars, String partitioning, boolean caretEitherSideOfBracket) { Assert.isLegal(chars.length % 2 == 0); Assert.isNotNull(partitioning); fPairs= new CharPairs(chars); fPartitioning= partitioning; fCaretEitherSideOfBracket= caretEitherSideOfBracket; } /** * Creates a new character pair matcher that matches characters within the default partitioning. * The specified list of characters must have the form
{ start, end, * start, end, ..., start, end }
For instance: * *
	 * char[] chars= new char[] { '(', ')', '{', '}', '[', ']' };
	 * new DefaultCharacterPairMatcher(chars);
	 * 
* * @param chars a list of characters */ public DefaultCharacterPairMatcher(char[] chars) { this(chars, IDocumentExtension3.DEFAULT_PARTITIONING); } @Override public IRegion match(IDocument doc, int offset) { if (doc == null || offset < 0 || offset > doc.getLength()) return null; try { return performMatch(doc, offset); } catch (BadLocationException ble) { return null; } } /** * @see org.eclipse.jface.text.source.ICharacterPairMatcherExtension#match(org.eclipse.jface.text.IDocument, * int, int) * @since 3.8 */ @Override public IRegion match(IDocument document, int offset, int length) { if (document == null || offset < 0 || offset > document.getLength() || Math.abs(length) > 1) return null; try { int sourceCaretOffset= offset + length; if (Math.abs(length) == 1) { char ch= length > 0 ? document.getChar(offset) : document.getChar(sourceCaretOffset); if (!fPairs.contains(ch)) return null; } int adjustment= getOffsetAdjustment(document, sourceCaretOffset, length); sourceCaretOffset+= adjustment; return match(document, sourceCaretOffset); } catch (BadLocationException e) { return null; } } /** * @see org.eclipse.jface.text.source.ICharacterPairMatcherExtension#findEnclosingPeerCharacters(org.eclipse.jface.text.IDocument, * int, int) * @since 3.8 */ @Override public IRegion findEnclosingPeerCharacters(IDocument document, int offset, int length) { if (document == null || offset < 0 || offset > document.getLength()) return null; //maybe a bracket is selected IRegion region= match(document, offset, length); fAnchor= ICharacterPairMatcher.LEFT; //always set the anchor to LEFT if (region != null) { return region; } //bracket is not selected try { final String partition= TextUtilities.getContentType(document, fPartitioning, offset, false); DocumentPartitionAccessor partDoc= new DocumentPartitionAccessor(document, fPartitioning, partition); IRegion enclosingPeers= findEnclosingPeers(document, partDoc, offset, length, 0, document.getLength()); if (enclosingPeers != null) return enclosingPeers; partDoc= new DocumentPartitionAccessor(document, fPartitioning, IDocument.DEFAULT_CONTENT_TYPE); return findEnclosingPeers(document, partDoc, offset, length, 0, document.getLength()); } catch (BadLocationException ble) { fAnchor= -1; return null; } } /** * @see org.eclipse.jface.text.source.ICharacterPairMatcherExtension#isMatchedChar(char) * @since 3.8 */ @Override public boolean isMatchedChar(char ch) { return fPairs.contains(ch); } /** * @see org.eclipse.jface.text.source.ICharacterPairMatcherExtension#isMatchedChar(char, * org.eclipse.jface.text.IDocument, int) * @since 3.8 */ @Override public boolean isMatchedChar(char ch, IDocument document, int offset) { return isMatchedChar(ch); } /** * @see org.eclipse.jface.text.source.ICharacterPairMatcherExtension#isRecomputationOfEnclosingPairRequired(org.eclipse.jface.text.IDocument, * org.eclipse.jface.text.IRegion, org.eclipse.jface.text.IRegion) * @since 3.8 */ @Override public boolean isRecomputationOfEnclosingPairRequired(IDocument document, IRegion currentSelection, IRegion previousSelection) { int previousStartOffset= previousSelection.getOffset(); int currentStartOffset= currentSelection.getOffset(); int previousEndOffset= previousStartOffset + previousSelection.getLength(); int currentEndOffset= currentStartOffset + currentSelection.getLength(); try { String prevEndContentType= TextUtilities.getContentType(document, fPartitioning, previousEndOffset, false); String currEndContentType= TextUtilities.getContentType(document, fPartitioning, currentEndOffset, false); if (!prevEndContentType.equals(currEndContentType)) return true; String prevStartContentType= TextUtilities.getContentType(document, fPartitioning, previousStartOffset, true); String currStartContentType= TextUtilities.getContentType(document, fPartitioning, currentStartOffset, true); if (!prevStartContentType.equals(currStartContentType)) return true; int start; int end; if (currentEndOffset > previousEndOffset) { start= previousEndOffset; end= currentEndOffset; } else { start= currentEndOffset; end= previousEndOffset; } for (int i= Math.max(start - 1, 0); i <= end; i++) { if (isMatchedChar(document.getChar(i))) { return true; } } if (currentStartOffset > previousStartOffset) { start= previousStartOffset; end= currentStartOffset; } else { start= currentStartOffset; end= previousStartOffset; } for (int i= Math.max(start - 1, 0); i <= end; i++) { if (isMatchedChar(document.getChar(i))) { return true; } } } catch (BadLocationException e) { //do nothing } return false; } /** * Computes the adjustment in the start offset for the purpose of finding a matching peer. This * is required as the direction of selection can be right-to-left or left-to-right. * * @param document the document to work on * @param offset the start offset * @param length the selection length * @return the start offset adjustment which can be -1, 0 or +1 * @since 3.8 */ private int getOffsetAdjustment(IDocument document, int offset, int length) { if (length == 0 || Math.abs(length) > 1 || offset >= document.getLength()) return 0; try { if (length < 0) { if (fPairs.isStartCharacter(document.getChar(offset))) { return 1; } } else { if (fCaretEitherSideOfBracket && fPairs.isEndCharacter(document.getChar(offset - 1))) { return -1; } } } catch (BadLocationException e) { //do nothing } return 0; } /* * Performs the actual work of matching for #match(IDocument, int). */ private IRegion performMatch(IDocument doc, int caretOffset) throws BadLocationException { char prevChar= (caretOffset - 1 >= 0) ? doc.getChar(caretOffset - 1) : Character.MIN_VALUE; boolean isForward; final char ch; if (fCaretEitherSideOfBracket) { char currChar= (caretOffset != doc.getLength()) ? doc.getChar(caretOffset) : Character.MIN_VALUE; if (fPairs.isEndCharacter(prevChar) && !fPairs.isEndCharacter(currChar)) { //https://bugs.eclipse.org/bugs/show_bug.cgi?id=372516 caretOffset--; currChar= prevChar; prevChar= doc.getChar(Math.max(caretOffset - 1, 0)); } else if (fPairs.isStartCharacter(currChar) && !fPairs.contains(prevChar)) { caretOffset++; prevChar= currChar; currChar= doc.getChar(caretOffset); } isForward= fPairs.contains(prevChar) && fPairs.isStartCharacter(prevChar); boolean isBackward= fPairs.contains(currChar) && !fPairs.isStartCharacter(currChar); if (!isForward && !isBackward) { return null; } ch= isForward ? prevChar : currChar; } else { if (!fPairs.contains(prevChar)) return null; isForward= fPairs.isStartCharacter(prevChar); ch= prevChar; } fAnchor= isForward ? ICharacterPairMatcher.LEFT : ICharacterPairMatcher.RIGHT; final int searchStartPosition= isForward ? caretOffset : (fCaretEitherSideOfBracket ? caretOffset - 1 : caretOffset - 2); final int adjustedOffset= isForward ? caretOffset - 1 : (fCaretEitherSideOfBracket ? caretOffset + 1 : caretOffset); final String partition= TextUtilities.getContentType(doc, fPartitioning, ((!isForward && fCaretEitherSideOfBracket) ? caretOffset : Math.max(caretOffset - 1, 0)), false); final DocumentPartitionAccessor partDoc= new DocumentPartitionAccessor(doc, fPartitioning, partition); int endOffset= findMatchingPeer(partDoc, ch, fPairs.getMatching(ch), isForward, isForward ? doc.getLength() : -1, searchStartPosition); if (endOffset == -1) return null; final int adjustedEndOffset= isForward ? endOffset + 1 : endOffset; if (adjustedEndOffset == adjustedOffset) return null; return new Region(Math.min(adjustedOffset, adjustedEndOffset), Math.abs(adjustedEndOffset - adjustedOffset)); } /** * Searches doc for the specified end character, end. * * @param doc the document to search * @param start the opening matching character * @param end the end character to search for * @param searchForward search forwards or backwards? * @param boundary a boundary at which the search should stop * @param startPos the start offset * @return the index of the end character if it was found, otherwise -1 * @throws BadLocationException if the document is accessed with invalid offset or line */ private int findMatchingPeer(DocumentPartitionAccessor doc, char start, char end, boolean searchForward, int boundary, int startPos) throws BadLocationException { int pos= startPos; int nestingLevel= 0; while (pos != boundary) { final char c= doc.getChar(pos); if (c == end && doc.inPartition(pos)) { if (nestingLevel == 0) return pos; nestingLevel--; } else if (c == start && doc.inPartition(pos)) { nestingLevel++; } pos= doc.getNextPosition(pos, searchForward); } return -1; } /* * Performs the actual work of finding enclosing peer characters for #findEnclosingPeerCharacters(IDocument, int, int). */ private IRegion findEnclosingPeers(IDocument document, DocumentPartitionAccessor doc, int offset, int length, int lowerBoundary, int upperBoundary) throws BadLocationException { char[] pairs= fPairs.fPairs; int start; int end; if (length >= 0) { start= offset; end= offset + length; } else { end= offset; start= offset + length; } boolean lowerFound= false; boolean upperFound= false; int[][] counts= new int[pairs.length][2]; char currChar= (start != document.getLength()) ? doc.getChar(start) : Character.MIN_VALUE; int pos1; int pos2; if (fPairs.isEndCharacter(currChar)) { pos1= doc.getNextPosition(start, false); pos2= start; } else { pos1= start; pos2= doc.getNextPosition(start, true); } while ((pos1 >= lowerBoundary && !lowerFound) || (pos2 < upperBoundary && !upperFound)) { for (int i= 0; i < counts.length; i++) { counts[i][0]= counts[i][1]= 0; } outer1: while (pos1 >= lowerBoundary && !lowerFound) { final char c= doc.getChar(pos1); int i= getCharacterIndex(c, document, pos1); if (i != -1 && doc.inPartition(pos1)) { if (i % 2 == 0) { counts[i / 2][0]--; //start } else { counts[i / 2][0]++; //end } for (int j= 0; j < counts.length; j++) { if (counts[j][0] == -1) { lowerFound= true; break outer1; } } } pos1= doc.getNextPosition(pos1, false); } outer2: while (pos2 < upperBoundary && !upperFound) { final char c= doc.getChar(pos2); int i= getCharacterIndex(c, document, pos2); if (i != -1 && doc.inPartition(pos2)) { if (i % 2 == 0) { counts[i / 2][1]++; //start } else { counts[i / 2][1]--; //end } for (int j= 0; j < counts.length; j++) { if (counts[j][1] == -1 && counts[j][0] == -1) { upperFound= true; break outer2; } } } pos2= doc.getNextPosition(pos2, true); } if (pos1 > start || pos2 < end - 1) { //match inside selection => discard pos1= doc.getNextPosition(pos1, false); pos2= doc.getNextPosition(pos2, true); lowerFound= false; upperFound= false; } } pos2++; if (pos1 < lowerBoundary || pos2 > upperBoundary) return null; return new Region(pos1, pos2 - pos1); } /** * Determines the index of the character in the char array passed to the constructor of the pair * matcher. * * @param ch the character * @param document the document * @param offset the offset in document * @return the index of the character in the char array passed to the constructor of the pair * matcher, and -1 if the character is not one of the matched characters * @since 3.8 */ private int getCharacterIndex(char ch, IDocument document, int offset) { char[] pairs= fPairs.fPairs; for (int i= 0; i < pairs.length; i++) { if (pairs[i] == ch && isMatchedChar(ch, document, offset)) { return i; } } return -1; } @Override public int getAnchor() { return fAnchor; } @Override public void dispose() { } @Override public void clear() { fAnchor= -1; } /** * Utility class that wraps a document and gives access to * partitioning information. A document is tied to a particular * partition and, when considering whether or not a position is a * valid match, only considers position within its partition. */ private static class DocumentPartitionAccessor { private final IDocument fDocument; private final String fPartitioning, fPartition; private ITypedRegion fCachedPartition; private int fLength; /** * Creates a new partitioned document for the specified document. * * @param doc the document to wrap * @param partitioning the partitioning used * @param partition the partition managed by this document */ public DocumentPartitionAccessor(IDocument doc, String partitioning, String partition) { fDocument= doc; fPartitioning= partitioning; fPartition= partition; fLength= doc.getLength(); } /** * Returns the character at the specified position in this document. * * @param pos an offset within this document * @return the character at the offset * @throws BadLocationException if the offset is invalid in this document */ public char getChar(int pos) throws BadLocationException { return fDocument.getChar(pos); } /** * Returns true if the specified offset is within the partition * managed by this document. * * @param pos an offset within this document * @return true if the offset is within this document's partition */ public boolean inPartition(int pos) { final ITypedRegion partition= getPartition(pos); return partition != null && partition.getType().equals(fPartition); } /** * Returns the next position to query in the search. The position * is not guaranteed to be in this document's partition. * * @param pos an offset within the document * @param searchForward the direction of the search * @return the next position to query */ public int getNextPosition(int pos, boolean searchForward) { final ITypedRegion partition= getPartition(pos); if (partition == null || fPartition.equals(partition.getType())) return simpleIncrement(pos, searchForward); if (searchForward) { int end= partition.getOffset() + partition.getLength(); if (pos < end) return end; } else { int offset= partition.getOffset(); if (pos > offset) return offset - 1; } return simpleIncrement(pos, searchForward); } private int simpleIncrement(int pos, boolean searchForward) { return pos + (searchForward ? 1 : -1); } /** * Returns partition information about the region containing the * specified position. * * @param pos a position within this document. * @return positioning information about the region containing the * position */ private ITypedRegion getPartition(int pos) { if (fCachedPartition == null || !contains(fCachedPartition, pos)) { Assert.isTrue(pos >= 0 && pos <= fLength); try { fCachedPartition= TextUtilities.getPartition(fDocument, fPartitioning, pos, false); } catch (BadLocationException e) { fCachedPartition= null; } } return fCachedPartition; } private static boolean contains(IRegion region, int pos) { int offset= region.getOffset(); return offset <= pos && pos < offset + region.getLength(); } } /** * Utility class that encapsulates access to matching character pairs. */ private static class CharPairs { private final char[] fPairs; public CharPairs(char[] pairs) { fPairs= pairs; } /** * Returns true if the specified character occurs in one of the character pairs. * * @param c a character * @return true exactly if the character occurs in one of the pairs */ public boolean contains(char c) { char[] pairs= fPairs; for (char pair : pairs) { if (c == pair) return true; } return false; } /** * Returns true if the specified character opens a character pair * when scanning in the specified direction. * * @param c a character * @param searchForward the direction of the search * @return whether or not the character opens a character pair */ public boolean isOpeningCharacter(char c, boolean searchForward) { for (int i= 0; i < fPairs.length; i += 2) { if (searchForward && getStartChar(i) == c) return true; else if (!searchForward && getEndChar(i) == c) return true; } return false; } /** * Returns true if the specified character is a start character. * * @param c a character * @return true exactly if the character is a start character */ public boolean isStartCharacter(char c) { return this.isOpeningCharacter(c, true); } /** * Returns true if the specified character is an end character. * * @param c a character * @return true exactly if the character is an end character * @since 3.8 */ public boolean isEndCharacter(char c) { return this.isOpeningCharacter(c, false); } /** * Returns the matching character for the specified character. * * @param c a character occurring in a character pair * @return the matching character */ public char getMatching(char c) { for (int i= 0; i < fPairs.length; i += 2) { if (getStartChar(i) == c) return getEndChar(i); else if (getEndChar(i) == c) return getStartChar(i); } Assert.isTrue(false); return '\0'; } private char getStartChar(int i) { return fPairs[i]; } private char getEndChar(int i) { return fPairs[i + 1]; } } }