/******************************************************************************* * Copyright (c) 2006, 2007 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 * Vlad Dumitrescu - adapted to Erlang delimiters *******************************************************************************/ package org.erlide.ui.editors.erl; import java.util.HashSet; import java.util.Set; 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; import org.eclipse.jface.text.source.ICharacterPairMatcher; /* * taken from eclipse 3.3 DefaultCharacterPairMatcher (which isn't in 3.2, unfortunately) */ public class ErlangPairMatcher implements ICharacterPairMatcher { private int fAnchor = -1; private final StringPairs fPairs; private final String fPartitioning; /** * 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 SimpleCharacterPairMatcher(chars, ...); * </pre> * * @param chars * a list of characters * @param partitioning * the partitioning to match within */ public ErlangPairMatcher(final String[] strings, final String partitioning) { fPairs = new StringPairs(strings); fPartitioning = partitioning; } /** * 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 SimpleCharacterPairMatcher(chars); * </pre> * * @param chars * a list of characters */ public ErlangPairMatcher(final String[] strings) { this(strings, IDocumentExtension3.DEFAULT_PARTITIONING); } /* @see ICharacterPairMatcher#match(IDocument, int) */ @Override public IRegion match(final IDocument doc, final int offset) { if (doc == null || offset < 0 || offset > doc.getLength()) { return null; } try { return performMatch(doc, offset); } catch (final BadLocationException ble) { return null; } } /* * Performs the actual work of matching for #match(IDocument, int). */ private IRegion performMatch(final IDocument doc, final int offset) throws BadLocationException { if (offset <= 0) { return null; } String prevString = doc.get(offset - 1, 1); if (!fPairs.contains(prevString)) { if (offset == 1) { return null; } prevString = doc.get(offset - 2, 2); if (!fPairs.contains(prevString)) { return null; } } final boolean isForward = fPairs.isStartString(prevString); fAnchor = isForward ? ICharacterPairMatcher.LEFT : ICharacterPairMatcher.RIGHT; final int searchStartPosition = isForward ? offset : offset - (prevString.length() + 1); final int adjustedOffset = isForward ? offset - 1 : offset; final String partition = TextUtilities.getContentType(doc, fPartitioning, adjustedOffset, false); final DocumentPartitionAccessor partDoc = new DocumentPartitionAccessor(doc, fPartitioning, partition); final int endOffset = findMatchingPeer(partDoc, prevString, fPairs.getMatching(prevString), isForward, isForward ? doc.getLength() - prevString.length() + 1 : -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 string, <code>end</code>. * * @param doc * the document to search * @param start * the opening matching string * @param end * the end string 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 */ private int findMatchingPeer(final DocumentPartitionAccessor doc, final String start, final String end, final boolean searchForward, final int boundary, final int startPos) throws BadLocationException { int pos = startPos; final int length = start.length(); while (pos != boundary) { final String s = doc.get(pos, length); if (doc.isMatch(pos, end)) { return pos; } else if (s.equals(start) && doc.inPartition(pos)) { pos = findMatchingPeer(doc, start, end, searchForward, boundary, doc.getNextPosition(pos, searchForward)); if (pos == -1) { return -1; } } pos = doc.getNextPosition(pos, searchForward); } return -1; } /* @see ICharacterPairMatcher#getAnchor() */ @Override public int getAnchor() { return fAnchor; } /* @see ICharacterPairMatcher#dispose() */ @Override public void dispose() { } /* @see ICharacterPairMatcher#clear() */ @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; /** * 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(final IDocument doc, final String partitioning, final String partition) { fDocument = doc; fPartitioning = partitioning; fPartition = partition; } /** * Returns the character at the specified position in this document. * * @param pos * an offset within this document * @param length * a length of string to get * @return the character at the offset * @throws BadLocationException */ public String get(final int pos, final int length) throws BadLocationException { return fDocument.get(pos, length); } /** * Returns true if the character at the specified position is a valid match for * the specified end character. To be a valid match, it must be in the appropriate * partition and equal to the end character. * * @param pos * an offset within this document * @param end * the end character to match against * @return true exactly if the position represents a valid match * @throws BadLocationException */ public boolean isMatch(final int pos, final String end) throws BadLocationException { return get(pos, end.length()).equals(end) && inPartition(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(final 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(final int pos, final boolean searchForward) { final ITypedRegion partition = getPartition(pos); if (partition == null) { return simpleIncrement(pos, searchForward); } if (fPartition.equals(partition.getType())) { return simpleIncrement(pos, searchForward); } if (searchForward) { final int end = partition.getOffset() + partition.getLength(); if (pos < end) { return end; } } else { final int offset = partition.getOffset(); if (pos > offset) { return offset - 1; } } return simpleIncrement(pos, searchForward); } private int simpleIncrement(final int pos, final 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(final int pos) { if (fCachedPartition == null || !contains(fCachedPartition, pos)) { try { fCachedPartition = TextUtilities.getPartition(fDocument, fPartitioning, pos, false); } catch (final BadLocationException e) { fCachedPartition = null; } } return fCachedPartition; } private static boolean contains(final IRegion region, final int pos) { final int offset = region.getOffset(); return offset <= pos && pos < offset + region.getLength(); } } /** * Utility class that encapsulates access to matching character pairs. */ private static class StringPairs { private final String[] fPairs; public StringPairs(final String[] strings) { fPairs = strings; } /** * 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(final String s) { return getAllCharacters().contains(s); } private Set<String> fStringsCache = null; /** * @return A set containing all characters occurring in character pairs. */ private Set<String> getAllCharacters() { if (fStringsCache == null) { final Set<String> set = new HashSet<>(); for (int i = 0; i < fPairs.length; i++) { set.add(fPairs[i]); } fStringsCache = set; } return fStringsCache; } /** * 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 isOpeningString(final String s, final boolean searchForward) { for (int i = 0; i < fPairs.length; i += 2) { if (searchForward && getStartString(i).equals(s)) { return true; } else if (!searchForward && getEndString(i).equals(s)) { return true; } } return false; } /** * Returns true of the specified string is a start string. * * @param s * a string * @return true exactly if the string is a start string */ public boolean isStartString(final String s) { return isOpeningString(s, true); } /** * Returns the matching character for the specified character. * * @param s * a string occurring in a string pair * @return the matching string */ public String getMatching(final String s) { for (int i = 0; i < fPairs.length; i += 2) { if (getStartString(i).equals(s)) { return getEndString(i); } else if (getEndString(i).equals(s)) { return getStartString(i); } } return ""; } private String getStartString(final int i) { return fPairs[i]; } private String getEndString(final int i) { return fPairs[i + 1]; } } }