| /******************************************************************************* |
| * Copyright (c) 2006, 2012 IBM Corporation and others. |
| * |
| * This program and the accompanying materials |
| * are made available under the terms of the Eclipse Public License 2.0 |
| * which accompanies this distribution, and is available at |
| * https://www.eclipse.org/legal/epl-2.0/ |
| * |
| * SPDX-License-Identifier: EPL-2.0 |
| * |
| * 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 <blockquote>{ |
| * <i>start</i>, <i>end</i>, <i>start</i>, <i>end</i>, ..., <i>start</i>, <i>end</i> |
| * }</blockquote> For instance: |
| * |
| * <pre> |
| * char[] chars = new char[] {'(', ')', '{', '}', '[', ']'}; |
| * new DefaultCharacterPairMatcher(chars, ...); |
| * </pre> |
| * |
| * @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 <blockquote>{ |
| * <i>start</i>, <i>end</i>, <i>start</i>, <i>end</i>, ..., <i>start</i>, <i>end</i> |
| * }</blockquote> For instance: |
| * |
| * <pre> |
| * char[] chars = new char[] {'(', ')', '{', '}', '[', ']'}; |
| * new DefaultCharacterPairMatcher(chars, ...); |
| * </pre> |
| * |
| * @param chars a list of characters |
| * @param partitioning the partitioning to match within |
| * @param caretEitherSideOfBracket controls the matching behavior. When <code>true</code>, the |
| * matching peer will be found when the caret is placed either before or after a |
| * character. When <code>false</code>, 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 <blockquote>{ <i>start</i>, <i>end</i>, |
| * <i>start</i>, <i>end</i>, ..., <i>start</i>, <i>end</i> }</blockquote> For instance: |
| * |
| * <pre> |
| * char[] chars= new char[] { '(', ')', '{', '}', '[', ']' }; |
| * new DefaultCharacterPairMatcher(chars); |
| * </pre> |
| * |
| * @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 <code>doc</code> for the specified end character, <code>end</code>. |
| * |
| * @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]; |
| } |
| |
| } |
| |
| } |