001: /*******************************************************************************
002: * Copyright (c) 2006 IBM Corporation and others.
003: * All rights reserved. This program and the accompanying materials
004: * are made available under the terms of the Eclipse Public License v1.0
005: * which accompanies this distribution, and is available at
006: * http://www.eclipse.org/legal/epl-v10.html
007: *
008: * Contributors:
009: * Christian Plesner Hansen (plesner@quenta.org) - initial API and implementation
010: *******************************************************************************/package org.eclipse.jface.text.source;
011:
012: import java.util.HashSet;
013: import java.util.Set;
014:
015: import org.eclipse.core.runtime.Assert;
016:
017: import org.eclipse.jface.text.BadLocationException;
018: import org.eclipse.jface.text.IDocument;
019: import org.eclipse.jface.text.IDocumentExtension3;
020: import org.eclipse.jface.text.IRegion;
021: import org.eclipse.jface.text.ITypedRegion;
022: import org.eclipse.jface.text.Region;
023: import org.eclipse.jface.text.TextUtilities;
024:
025: /**
026: * A character pair matcher that matches a specified set of character
027: * pairs against each other. Only characters that occur in the same
028: * partitioning are matched.
029: *
030: * @since 3.3
031: */
032: public class DefaultCharacterPairMatcher implements
033: ICharacterPairMatcher {
034:
035: private int fAnchor = -1;
036: private final CharPairs fPairs;
037: private final String fPartitioning;
038:
039: /**
040: * Creates a new character pair matcher that matches the specified
041: * characters within the specified partitioning. The specified
042: * list of characters must have the form
043: * <blockquote>{ <i>start</i>, <i>end</i>, <i>start</i>, <i>end</i>, ..., <i>start</i>, <i>end</i> }</blockquote>
044: * For instance:
045: * <pre>
046: * char[] chars = new char[] {'(', ')', '{', '}', '[', ']'};
047: * new SimpleCharacterPairMatcher(chars, ...);
048: * </pre>
049: *
050: * @param chars a list of characters
051: * @param partitioning the partitioning to match within
052: */
053: public DefaultCharacterPairMatcher(char[] chars, String partitioning) {
054: Assert.isLegal(chars.length % 2 == 0);
055: Assert.isNotNull(partitioning);
056: fPairs = new CharPairs(chars);
057: fPartitioning = partitioning;
058: }
059:
060: /**
061: * Creates a new character pair matcher that matches characters
062: * within the default partitioning. The specified list of
063: * characters must have the form
064: * <blockquote>{ <i>start</i>, <i>end</i>, <i>start</i>, <i>end</i>, ..., <i>start</i>, <i>end</i> }</blockquote>
065: * For instance:
066: * <pre>
067: * char[] chars = new char[] {'(', ')', '{', '}', '[', ']'};
068: * new SimpleCharacterPairMatcher(chars);
069: * </pre>
070: *
071: * @param chars a list of characters
072: */
073: public DefaultCharacterPairMatcher(char[] chars) {
074: this (chars, IDocumentExtension3.DEFAULT_PARTITIONING);
075: }
076:
077: /* @see ICharacterPairMatcher#match(IDocument, int) */
078: public IRegion match(IDocument doc, int offset) {
079: if (doc == null || offset < 0 || offset > doc.getLength())
080: return null;
081: try {
082: return performMatch(doc, offset);
083: } catch (BadLocationException ble) {
084: return null;
085: }
086: }
087:
088: /*
089: * Performs the actual work of matching for #match(IDocument, int).
090: */
091: private IRegion performMatch(IDocument doc, int offset)
092: throws BadLocationException {
093: final char prevChar = doc.getChar(Math.max(offset - 1, 0));
094: if (!fPairs.contains(prevChar))
095: return null;
096: final boolean isForward = fPairs.isStartCharacter(prevChar);
097: fAnchor = isForward ? ICharacterPairMatcher.LEFT
098: : ICharacterPairMatcher.RIGHT;
099: final int searchStartPosition = isForward ? offset : offset - 2;
100: final int adjustedOffset = isForward ? offset - 1 : offset;
101: final String partition = TextUtilities.getContentType(doc,
102: fPartitioning, adjustedOffset, false);
103: final DocumentPartitionAccessor partDoc = new DocumentPartitionAccessor(
104: doc, fPartitioning, partition);
105: int endOffset = findMatchingPeer(partDoc, prevChar, fPairs
106: .getMatching(prevChar), isForward, isForward ? doc
107: .getLength() : -1, searchStartPosition);
108: if (endOffset == -1)
109: return null;
110: final int adjustedEndOffset = isForward ? endOffset + 1
111: : endOffset;
112: if (adjustedEndOffset == adjustedOffset)
113: return null;
114: return new Region(Math.min(adjustedOffset, adjustedEndOffset),
115: Math.abs(adjustedEndOffset - adjustedOffset));
116: }
117:
118: /**
119: * Searches <code>doc</code> for the specified end character, <code>end</code>.
120: *
121: * @param doc the document to search
122: * @param start the opening matching character
123: * @param end the end character to search for
124: * @param searchForward search forwards or backwards?
125: * @param boundary a boundary at which the search should stop
126: * @param startPos the start offset
127: * @return the index of the end character if it was found, otherwise -1
128: * @throws BadLocationException
129: */
130: private int findMatchingPeer(DocumentPartitionAccessor doc,
131: char start, char end, boolean searchForward, int boundary,
132: int startPos) throws BadLocationException {
133: int pos = startPos;
134: while (pos != boundary) {
135: final char c = doc.getChar(pos);
136: if (doc.isMatch(pos, end)) {
137: return pos;
138: } else if (c == start && doc.inPartition(pos)) {
139: pos = findMatchingPeer(doc, start, end, searchForward,
140: boundary, doc.getNextPosition(pos,
141: searchForward));
142: if (pos == -1)
143: return -1;
144: }
145: pos = doc.getNextPosition(pos, searchForward);
146: }
147: return -1;
148: }
149:
150: /* @see ICharacterPairMatcher#getAnchor() */
151: public int getAnchor() {
152: return fAnchor;
153: }
154:
155: /* @see ICharacterPairMatcher#dispose() */
156: public void dispose() {
157: }
158:
159: /* @see ICharacterPairMatcher#clear() */
160: public void clear() {
161: fAnchor = -1;
162: }
163:
164: /**
165: * Utility class that wraps a document and gives access to
166: * partitioning information. A document is tied to a particular
167: * partition and, when considering whether or not a position is a
168: * valid match, only considers position within its partition.
169: */
170: private static class DocumentPartitionAccessor {
171:
172: private final IDocument fDocument;
173: private final String fPartitioning, fPartition;
174: private ITypedRegion fCachedPartition;
175:
176: /**
177: * Creates a new partitioned document for the specified document.
178: *
179: * @param doc the document to wrap
180: * @param partitioning the partitioning used
181: * @param partition the partition managed by this document
182: */
183: public DocumentPartitionAccessor(IDocument doc,
184: String partitioning, String partition) {
185: fDocument = doc;
186: fPartitioning = partitioning;
187: fPartition = partition;
188: }
189:
190: /**
191: * Returns the character at the specified position in this document.
192: *
193: * @param pos an offset within this document
194: * @return the character at the offset
195: * @throws BadLocationException
196: */
197: public char getChar(int pos) throws BadLocationException {
198: return fDocument.getChar(pos);
199: }
200:
201: /**
202: * Returns true if the character at the specified position is a
203: * valid match for the specified end character. To be a valid
204: * match, it must be in the appropriate partition and equal to the
205: * end character.
206: *
207: * @param pos an offset within this document
208: * @param end the end character to match against
209: * @return true exactly if the position represents a valid match
210: * @throws BadLocationException
211: */
212: public boolean isMatch(int pos, char end)
213: throws BadLocationException {
214: return getChar(pos) == end && inPartition(pos);
215: }
216:
217: /**
218: * Returns true if the specified offset is within the partition
219: * managed by this document.
220: *
221: * @param pos an offset within this document
222: * @return true if the offset is within this document's partition
223: */
224: public boolean inPartition(int pos) {
225: final ITypedRegion partition = getPartition(pos);
226: return partition != null
227: && partition.getType().equals(fPartition);
228: }
229:
230: /**
231: * Returns the next position to query in the search. The position
232: * is not guaranteed to be in this document's partition.
233: *
234: * @param pos an offset within the document
235: * @param searchForward the direction of the search
236: * @return the next position to query
237: */
238: public int getNextPosition(int pos, boolean searchForward) {
239: final ITypedRegion partition = getPartition(pos);
240: if (partition == null)
241: return simpleIncrement(pos, searchForward);
242: if (fPartition.equals(partition.getType()))
243: return simpleIncrement(pos, searchForward);
244: if (searchForward) {
245: int end = partition.getOffset() + partition.getLength();
246: if (pos < end)
247: return end;
248: } else {
249: int offset = partition.getOffset();
250: if (pos > offset)
251: return offset - 1;
252: }
253: return simpleIncrement(pos, searchForward);
254: }
255:
256: private int simpleIncrement(int pos, boolean searchForward) {
257: return pos + (searchForward ? 1 : -1);
258: }
259:
260: /**
261: * Returns partition information about the region containing the
262: * specified position.
263: *
264: * @param pos a position within this document.
265: * @return positioning information about the region containing the
266: * position
267: */
268: private ITypedRegion getPartition(int pos) {
269: if (fCachedPartition == null
270: || !contains(fCachedPartition, pos)) {
271: Assert.isTrue(pos >= 0 && pos <= fDocument.getLength());
272: try {
273: fCachedPartition = TextUtilities.getPartition(
274: fDocument, fPartitioning, pos, false);
275: } catch (BadLocationException e) {
276: fCachedPartition = null;
277: }
278: }
279: return fCachedPartition;
280: }
281:
282: private static boolean contains(IRegion region, int pos) {
283: int offset = region.getOffset();
284: return offset <= pos && pos < offset + region.getLength();
285: }
286:
287: }
288:
289: /**
290: * Utility class that encapsulates access to matching character pairs.
291: */
292: private static class CharPairs {
293:
294: private final char[] fPairs;
295:
296: public CharPairs(char[] pairs) {
297: fPairs = pairs;
298: }
299:
300: /**
301: * Returns true if the specified character pair occurs in one
302: * of the character pairs.
303: *
304: * @param c a character
305: * @return true exactly if the character occurs in one of the pairs
306: */
307: public boolean contains(char c) {
308: return getAllCharacters().contains(new Character(c));
309: }
310:
311: private Set/*<Character>*/fCharsCache = null;
312:
313: /**
314: * @return A set containing all characters occurring in character pairs.
315: */
316: private Set/*<Character>*/getAllCharacters() {
317: if (fCharsCache == null) {
318: Set/*<Character>*/set = new HashSet/*<Character>*/();
319: for (int i = 0; i < fPairs.length; i++)
320: set.add(new Character(fPairs[i]));
321: fCharsCache = set;
322: }
323: return fCharsCache;
324: }
325:
326: /**
327: * Returns true if the specified character opens a character pair
328: * when scanning in the specified direction.
329: *
330: * @param c a character
331: * @param searchForward the direction of the search
332: * @return whether or not the character opens a character pair
333: */
334: public boolean isOpeningCharacter(char c, boolean searchForward) {
335: for (int i = 0; i < fPairs.length; i += 2) {
336: if (searchForward && getStartChar(i) == c)
337: return true;
338: else if (!searchForward && getEndChar(i) == c)
339: return true;
340: }
341: return false;
342: }
343:
344: /**
345: * Returns true of the specified character is a start character.
346: *
347: * @param c a character
348: * @return true exactly if the character is a start character
349: */
350: public boolean isStartCharacter(char c) {
351: return this .isOpeningCharacter(c, true);
352: }
353:
354: /**
355: * Returns the matching character for the specified character.
356: *
357: * @param c a character occurring in a character pair
358: * @return the matching character
359: */
360: public char getMatching(char c) {
361: for (int i = 0; i < fPairs.length; i += 2) {
362: if (getStartChar(i) == c)
363: return getEndChar(i);
364: else if (getEndChar(i) == c)
365: return getStartChar(i);
366: }
367: Assert.isTrue(false);
368: return '\0';
369: }
370:
371: private char getStartChar(int i) {
372: return fPairs[i];
373: }
374:
375: private char getEndChar(int i) {
376: return fPairs[i + 1];
377: }
378:
379: }
380:
381: }
|