| /******************************************************************************* |
| * Copyright (c) 2006, 2010 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.mylyn.docs.intent.client.ui.editor; |
| |
| import java.util.HashSet; |
| import java.util.Set; |
| |
| import org.eclipse.core.runtime.Assert; |
| import org.eclipse.jface.text.BadLocationException; |
| import org.eclipse.jface.text.IDocument; |
| import org.eclipse.jface.text.IRegion; |
| import org.eclipse.jface.text.Region; |
| import org.eclipse.jface.text.source.ICharacterPairMatcher; |
| |
| /** |
| * Helper class for match pairs of special characters. Based on |
| * org.eclipse.jface.text.source.DefaultCharacterPairMatcher, but without partitioning constraints: this allow |
| * the pair matcher being efficient even without a correct partitioning. |
| * |
| * @author <a href="mailto:william.piers@obeo.fr">William Piers</a> |
| */ |
| public class IntentPairMatcher implements ICharacterPairMatcher { |
| // CHECKSTYLE:OFF |
| /** |
| * Blocks used by aggregated the matcher. |
| */ |
| protected static final char[] BLOCKS = {'(', ')', '[', ']', '{', '}', |
| }; |
| |
| private int fAnchor = -1; |
| |
| private final CharPairs fPairs; |
| |
| /** |
| * 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> |
| */ |
| public IntentPairMatcher() { |
| Assert.isLegal(BLOCKS.length % 2 == 0); |
| fPairs = new CharPairs(BLOCKS); |
| } |
| |
| /** |
| * {@inheritDoc} |
| * |
| * @see org.eclipse.jface.text.source.ICharacterPairMatcher#match(org.eclipse.jface.text.IDocument, int) |
| */ |
| 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; |
| } |
| } |
| |
| /* |
| * Performs the actual work of matching for #match(IDocument, int). |
| */ |
| private IRegion performMatch(IDocument doc, int caretOffset) throws BadLocationException { |
| final int charOffset = caretOffset - 1; |
| final char prevChar = doc.getChar(Math.max(charOffset, 0)); |
| if (!fPairs.contains(prevChar)) |
| return null; |
| final boolean isForward = fPairs.isStartCharacter(prevChar); |
| fAnchor = isForward ? ICharacterPairMatcher.LEFT : ICharacterPairMatcher.RIGHT; |
| final int searchStartPosition = isForward ? caretOffset : caretOffset - 2; |
| final int adjustedOffset = isForward ? charOffset : caretOffset; |
| int endOffset = findMatchingPeer(doc, prevChar, fPairs.getMatching(prevChar), 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(IDocument doc, char start, char end, boolean searchForward, int boundary, |
| int startPos) throws BadLocationException { |
| int pos = startPos; |
| while (pos != boundary) { |
| final char c = doc.getChar(pos); |
| if (doc.getChar(pos) == end) { |
| return pos; |
| } else if (c == start) { |
| pos = findMatchingPeer(doc, start, end, searchForward, boundary, |
| simpleIncrement(pos, searchForward)); |
| if (pos == -1) |
| return -1; |
| } |
| pos = simpleIncrement(pos, searchForward); |
| } |
| return -1; |
| } |
| |
| /* @see ICharacterPairMatcher#getAnchor() */ |
| public int getAnchor() { |
| return fAnchor; |
| } |
| |
| /* @see ICharacterPairMatcher#dispose() */ |
| public void dispose() { |
| } |
| |
| /* @see ICharacterPairMatcher#clear() */ |
| public void clear() { |
| fAnchor = -1; |
| } |
| |
| /** |
| * 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 pair 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) { |
| return getAllCharacters().contains(Character.valueOf(c)); |
| } |
| |
| private Set<Character> fCharsCache = null; |
| |
| /** |
| * @return A set containing all characters occurring in character pairs. |
| */ |
| private Set<Character> getAllCharacters() { |
| if (fCharsCache == null) { |
| Set<Character> set = new HashSet<Character>(); |
| for (int i = 0; i < fPairs.length; i++) |
| set.add(Character.valueOf(fPairs[i])); |
| fCharsCache = set; |
| } |
| return fCharsCache; |
| } |
| |
| /** |
| * 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 of 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 the matching character for the specified character. |
| * |
| * @param c |
| * a character occurring in a character pair |
| * @return the matching character |
| */ |
| // CHECKSTYLE:OFF |
| 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]; |
| } |
| |
| } |
| |
| private int simpleIncrement(int pos, boolean searchForward) { |
| return pos + (searchForward ? 1 : -1); |
| } |
| // CHECKSTYLE:ON |
| } |