@@ -11,7 +11,9 @@
*******************************************************************************/
package com.aptana.editor.common.internal.peer;

import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;

import org.eclipse.core.runtime.Assert;
@@ -25,6 +27,7 @@
import org.eclipse.jface.text.TextUtilities;
import org.eclipse.jface.text.source.ICharacterPairMatcher;

import com.aptana.core.util.ArrayUtil;
import com.aptana.editor.common.CommonEditorPlugin;
import com.aptana.editor.common.text.rules.CompositePartitionScanner;
import com.aptana.scope.IScopeSelector;
@@ -47,6 +50,11 @@ public class CharacterPairMatcher implements ICharacterPairMatcher
private final CharPairs fPairs;
private final String fPartitioning;

/**
* Avoid looking up scopes and matching scopes all the time by caching if a given partition type is a comment.
*/
private static Map<String, Boolean> partitionIsComment = new HashMap<String, Boolean>();

/**
* 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>,
@@ -109,7 +117,9 @@ public void clear()
public IRegion match(IDocument doc, int offset)
{
if (doc == null || offset < 0 || offset > doc.getLength())
{
return null;
}
try
{
return performMatch(doc, offset);
@@ -143,24 +153,24 @@ private IRegion performMatch(IDocument doc, int caretOffset) throws BadLocationE
}
}

String currentScope = getScopeAtOffset(doc, charOffset);
ITypedRegion partition = getPartition(doc, charOffset);
// FIXME if we're inside a string or comment, we should limit our search to just this particular partition!
// Drop out if the char is inside a comment
if (fgCommentSelector.matches(currentScope))
if (isComment(doc, partition))
{
return null;
}

boolean isForward = fPairs.isStartCharacter(prevChar);
final String partition = TextUtilities.getContentType(doc, fPartitioning, charOffset, false);
String contentType = partition.getType();
if (fPairs.isAmbiguous(prevChar))
{
// If this is common start tag, look forward, if common end tag look backwards!
if (partition.equals(CompositePartitionScanner.START_SWITCH_TAG))
if (CompositePartitionScanner.START_SWITCH_TAG.equals(contentType))
{
isForward = true;
}
else if (partition.equals(CompositePartitionScanner.END_SWITCH_TAG))
else if (CompositePartitionScanner.END_SWITCH_TAG.equals(contentType))
{
isForward = false;
}
@@ -169,7 +179,7 @@ else if (partition.equals(CompositePartitionScanner.END_SWITCH_TAG))
// Need to look at partition transition to tell if we're at end or beginning!
String partitionAhead = TextUtilities.getContentType(doc, fPartitioning, charOffset + 1, false);
String partitionBehind = TextUtilities.getContentType(doc, fPartitioning, charOffset - 1, false);
if (partition.equals(partitionBehind) && !partition.equals(partitionAhead))
if (contentType.equals(partitionBehind) && !contentType.equals(partitionAhead))
{
// End because we're transitioning out of a partition on this character
isForward = false;
@@ -181,251 +191,197 @@ else if (isUnclosedPair(prevChar, doc, charOffset))
}
}
fAnchor = isForward ? ICharacterPairMatcher.LEFT : ICharacterPairMatcher.RIGHT;
final int searchStartPosition = isForward ? charOffset + 1 : caretOffset - 2;
final int adjustedOffset = isForward ? charOffset : caretOffset;
int searchStartPosition = isForward ? charOffset + 1 : caretOffset - 2;
char endChar = fPairs.getMatching(prevChar);

int endOffset = -1;
if (isForward)
{
endOffset = searchForward(doc, searchStartPosition, prevChar, endChar, contentType);
}
else
{
endOffset = searchBackwards(doc, searchStartPosition, prevChar, endChar, contentType);
}

final DocumentPartitionAccessor partDoc = new DocumentPartitionAccessor(doc, fPartitioning, partition);
int endOffset = findMatchingPeer(partDoc, prevChar, fPairs.getMatching(prevChar), isForward,
isForward ? doc.getLength() : -1, searchStartPosition);
if (endOffset == -1)
{
return null;
}
final int adjustedOffset = isForward ? charOffset : caretOffset;
final int adjustedEndOffset = isForward ? endOffset + 1 : endOffset;
if (adjustedEndOffset == adjustedOffset)
{
return null;
}
return new Region(Math.min(adjustedOffset, adjustedEndOffset), Math.abs(adjustedEndOffset - adjustedOffset));
}

protected String getScopeAtOffset(IDocument doc, int charOffset) throws BadLocationException
protected ITypedRegion getPartition(IDocument doc, int charOffset) throws BadLocationException
{
return CommonEditorPlugin.getDefault().getDocumentScopeManager().getScopeAtOffset(doc, charOffset);
return TextUtilities.getPartition(doc, fPartitioning, charOffset, false);
}

private boolean isUnclosedPair(char c, IDocument document, int offset) throws BadLocationException
private int searchBackwards(IDocument doc, int searchStartPosition, char startChar, char endChar,
String partitionType) throws BadLocationException
{
// TODO Refactor and combine this copy-pasted code from PeerCharacterCloser
int beginning = 0;
// Don't check from very beginning of the document! Be smarter/quicker and check from beginning of
// partition if we can
if (document instanceof IDocumentExtension3)
int stack = 0;
ITypedRegion[] partitions = computePartitioning(doc, 0, searchStartPosition);
// reverse the partitions
partitions = ArrayUtil.reverse(partitions);
for (ITypedRegion p : partitions)
{
try
// skip other partitions that don't match our source partition
if (skipPartition(p.getType(), partitionType))
{
IDocumentExtension3 ext = (IDocumentExtension3) document;
ITypedRegion region = ext.getPartition(IDocumentExtension3.DEFAULT_PARTITIONING, offset, false);
beginning = region.getOffset();
continue;
}
catch (BadPartitioningException e)
int partitionOffset = p.getOffset();
int partitionEnd = partitionOffset + p.getLength() - 1;
int startOffset = Math.min(searchStartPosition, partitionEnd);
int length = startOffset - partitionOffset;
String contents = doc.get(partitionOffset, length + 1);
// Now search backwards through the partition for the end char
for (int i = length; i >= 0; i--)
{
// ignore
char c = contents.charAt(i);
if (c == endChar) // found end char!
{
if (stack == 0)
{
return partitionOffset + i;
}
else
{
stack--;
}
}
else if (c == startChar)
{
stack++;
}
}
}
// Now check leading source and see if we're an unclosed pair.
String previous = document.get(beginning, offset - beginning);
boolean open = false;
int index = -1;
while ((index = previous.indexOf(c, index + 1)) != -1)
{
open = !open;
}
return open;
return -1;
}

/**
* 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
protected ITypedRegion[] computePartitioning(IDocument doc, int offset, int length) throws BadLocationException
{
int pos = startPos;
while (pos != boundary)
return doc.computePartitioning(offset, length);
}

private int searchForward(IDocument doc, int searchStartPosition, char startChar, char endChar,
String startPartition) throws BadLocationException
{
int stack = 0;
ITypedRegion[] partitions = computePartitioning(doc, searchStartPosition, doc.getLength() - searchStartPosition);
for (ITypedRegion p : partitions)
{
final char c = doc.getChar(pos);
if (doc.isMatch(pos, end) && !fgCommentSelector.matches(getScopeAtOffset(doc.fDocument, pos)))
// skip other partitions that don't match our source partition
if (skipPartition(p.getType(), startPartition))
{
return pos;
continue;
}
else if (c == start && doc.inPartition(pos))
// Now search through the partition for the end char
int partitionLength = p.getLength();
int partitionEnd = p.getOffset() + partitionLength;
int startOffset = Math.max(searchStartPosition, p.getOffset());
int length = partitionEnd - startOffset;
String partitionContents = doc.get(startOffset, length);
for (int i = 0; i < length; i++)
{
pos = findMatchingPeer(doc, start, end, searchForward, boundary,
doc.getNextPosition(pos, searchForward));
if (pos == -1)
return -1;
char c = partitionContents.charAt(i);
if (c == endChar)
{
if (stack == 0)
{
// it's a match
return i + startOffset;
}
else
{
// need to close nested pair
stack--;
}
}
else if (c == startChar)
{
// open nested pair
stack++;
}
}
pos = doc.getNextPosition(pos, searchForward);
}
return -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 boolean areSwitchPartitions(String partition1, String partition2)
{
return (CompositePartitionScanner.START_SWITCH_TAG.equals(partition1) || CompositePartitionScanner.END_SWITCH_TAG
.equals(partition1))
&& (CompositePartitionScanner.START_SWITCH_TAG.equals(partition2) || CompositePartitionScanner.END_SWITCH_TAG
.equals(partition2));
}

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(IDocument doc, String partitioning, 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
* @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 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
* if the offset is invalid in this document
*/
public boolean isMatch(int pos, char end) throws BadLocationException
private boolean skipPartition(String toCheck, String originalPartition)
{
if (toCheck == null)
{
return getChar(pos) == end && inPartition(pos);
return true;
}

/**
* 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)
// don't skip same partition
if (toCheck.equals(originalPartition))
{
final ITypedRegion partition = getPartition(pos);
return samePartitions(partition);
return false;
}
// If they're both language switch partitions, don't skip.
return !areSwitchPartitions(toCheck, originalPartition);
}

private boolean samePartitions(ITypedRegion partition)
{
return partition != null
&& (partition.getType().equals(fPartition) || areSwitchPartitions(fPartition, partition.getType()));
}
protected String getScopeAtOffset(IDocument doc, int charOffset) throws BadLocationException
{
return CommonEditorPlugin.getDefault().getDocumentScopeManager().getScopeAtOffset(doc, charOffset);
}

/**
* 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)
private boolean isUnclosedPair(char c, IDocument document, int offset) throws BadLocationException
{
// TODO Refactor and combine this copy-pasted code from PeerCharacterCloser
int beginning = 0;
// Don't check from very beginning of the document! Be smarter/quicker and check from beginning of
// partition if we can
if (document instanceof IDocumentExtension3)
{
final ITypedRegion partition = getPartition(pos);
if (partition == null || samePartitions(partition))
{
return simpleIncrement(pos, searchForward);
}
if (searchForward)
try
{
int end = partition.getOffset() + partition.getLength();
if (pos < end)
return end;
IDocumentExtension3 ext = (IDocumentExtension3) document;
ITypedRegion region = ext.getPartition(IDocumentExtension3.DEFAULT_PARTITIONING, offset, false);
beginning = region.getOffset();
}
else
catch (BadPartitioningException e)
{
int offset = partition.getOffset();
if (pos > offset)
return offset - 1;
// ignore
}
return simpleIncrement(pos, searchForward);
}

private boolean areSwitchPartitions(String partition1, String partition2)
{
return (partition1.equals(CompositePartitionScanner.START_SWITCH_TAG) || partition1
.equals(CompositePartitionScanner.END_SWITCH_TAG))
&& (partition2.equals(CompositePartitionScanner.START_SWITCH_TAG) || partition2
.equals(CompositePartitionScanner.END_SWITCH_TAG));
}

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)
// Now check leading source and see if we're an unclosed pair.
String previous = document.get(beginning, offset - beginning);
boolean open = false;
int index = -1;
while ((index = previous.indexOf(c, index + 1)) != -1)
{
if (fCachedPartition == null || !contains(fCachedPartition, pos))
{
Assert.isTrue(pos >= 0 && pos <= fDocument.getLength());
try
{
fCachedPartition = TextUtilities.getPartition(fDocument, fPartitioning, pos, false);
}
catch (BadLocationException e)
{
fCachedPartition = null;
}
}
return fCachedPartition;
open = !open;
}
return open;
}

private static boolean contains(IRegion region, int pos)
protected boolean isComment(IDocument doc, ITypedRegion partition) throws BadLocationException
{
if (partitionIsComment.containsKey(partition.getType()))
{
int offset = region.getOffset();
return offset <= pos && pos < offset + region.getLength();
return partitionIsComment.get(partition.getType());
}

String scope = getScopeAtOffset(doc, partition.getOffset());
boolean isComment = fgCommentSelector.matches(scope);
partitionIsComment.put(partition.getType(), isComment);
return isComment;
}

/**
@@ -488,7 +444,8 @@ public boolean isOpeningCharacter(char c, boolean searchForward)
if (searchForward && getStartChar(i) == c)
{
return true;
} else if (!searchForward && getEndChar(i) == c)
}
else if (!searchForward && getEndChar(i) == c)
{
return true;
}
@@ -541,7 +498,8 @@ public char getMatching(char c)
if (getStartChar(i) == c)
{
return getEndChar(i);
} else if (getEndChar(i) == c)
}
else if (getEndChar(i) == c)
{
return getStartChar(i);
}

Large diffs are not rendered by default.

@@ -0,0 +1,68 @@
/**
* Aptana Studio
* Copyright (c) 2005-2011 by Appcelerator, Inc. All Rights Reserved.
* Licensed under the terms of the GNU Public License (GPL) v3 (with exceptions).
* Please see the license.html included with this distribution for details.
* Any modifications to this file must keep this entire header intact.
*/
package com.aptana.editor.common.internal.peer;

import java.net.URI;
import java.net.URL;

import org.eclipse.core.runtime.FileLocator;
import org.eclipse.core.runtime.Path;
import org.eclipse.core.runtime.Platform;
import org.eclipse.jface.text.IDocument;
import org.eclipse.jface.text.IRegion;
import org.eclipse.jface.text.source.ICharacterPairMatcher;
import org.eclipse.test.performance.PerformanceTestCase;
import org.eclipse.ui.ide.IDE;

import com.aptana.core.util.ResourceUtil;
import com.aptana.editor.common.AbstractThemeableEditor;
import com.aptana.ui.util.UIUtils;

public class CharacterPairMatcherPerfTest extends PerformanceTestCase
{
private static final char[] pairs = new char[] { '(', ')', '{', '}', '[', ']', '`', '`', '\'', '\'', '"', '"' };
private ICharacterPairMatcher matcher;

@Override
protected void setUp() throws Exception
{
matcher = new CharacterPairMatcher(pairs);
super.setUp();
}

@Override
protected void tearDown() throws Exception
{
if (matcher != null)
{
matcher.dispose();
}
matcher = null;
super.tearDown();
}

public void testPairMatching() throws Exception
{
URL url = FileLocator.find(Platform.getBundle("com.aptana.editor.common.tests"),
Path.fromPortableString("performance/jquery-1.6.4.js"), null);
URI uri = ResourceUtil.resourcePathToURI(url);
AbstractThemeableEditor editorPart = (AbstractThemeableEditor) IDE.openEditor(UIUtils.getActivePage(), uri,
"com.aptana.editor.js", true);
IDocument document = editorPart.getDocumentProvider().getDocument(editorPart.getEditorInput());
for (int i = 0; i < 6400; i++)
{
startMeasuring();
IRegion match = matcher.match(document, 367); // match the opening paren just before function.
matcher.match(document, 368); // match the opening paren just before function.
matcher.match(document, match.getOffset() + match.getLength());
stopMeasuring();
}
commitMeasurements();
assertPerformance();
}
}
@@ -13,6 +13,8 @@
import org.eclipse.jface.text.Document;
import org.eclipse.jface.text.IDocument;
import org.eclipse.jface.text.IRegion;
import org.eclipse.jface.text.ITypedRegion;
import org.eclipse.jface.text.TypedRegion;
import org.eclipse.jface.text.source.ICharacterPairMatcher;

public class CharacterPairMatcherTest extends TestCase
@@ -25,10 +27,19 @@ protected void setUp() throws Exception
{
matcher = new CharacterPairMatcher(pairs)
{
protected String getScopeAtOffset(IDocument doc, int charOffset) throws BadLocationException
@Override
protected ITypedRegion getPartition(IDocument doc, int charOffset) throws BadLocationException
{
return "source.ruby";
};
ITypedRegion[] partitions = computePartitioning(doc, charOffset, 1);
for (ITypedRegion region : partitions)
{
if (charOffset >= region.getOffset() && charOffset < (region.getOffset() + region.getLength()))
{
return region;
}
}
return null;
}
};
super.setUp();
}
@@ -85,15 +96,35 @@ public void testMatchesCharToRightIfNothingOnLeft2()

public void testDoesntPairMatchInComments()
{
String source = "# ( { [ `ruby command`, 'single quoted string', \"double quoted string\" ] } )";
final String source = "# ( { [ `ruby command`, 'single quoted string', \"double quoted string\" ] } )";
IDocument document = new Document(source);
matcher = new CharacterPairMatcher(pairs)
{
protected String getScopeAtOffset(IDocument doc, int charOffset)
throws org.eclipse.jface.text.BadLocationException
protected String getScopeAtOffset(IDocument doc, int charOffset) throws BadLocationException
{
return "source.ruby comment.line.hash";
};
}

@Override
protected ITypedRegion getPartition(IDocument doc, int charOffset) throws BadLocationException
{
ITypedRegion[] partitions = computePartitioning(doc, charOffset, 1);
for (ITypedRegion region : partitions)
{
if (charOffset >= region.getOffset() && charOffset < (region.getOffset() + region.getLength()))
{
return region;
}
}
return null;
}

@Override
protected ITypedRegion[] computePartitioning(IDocument doc, int offset, int length)
throws BadLocationException
{
return new TypedRegion[] { new TypedRegion(0, source.length(), "__rb_singleline_comment") };
}
};
assertNull(matcher.match(document, 2));
assertNull(matcher.match(document, 4));
@@ -114,7 +145,30 @@ protected String getScopeAtOffset(IDocument doc, int charOffset) throws BadLocat
if (charOffset >= 2 && charOffset <= 5)
return "source.ruby comment.line.hash";
return "source.ruby";
};
}

@Override
protected ITypedRegion getPartition(IDocument doc, int charOffset) throws BadLocationException
{
ITypedRegion[] partitions = computePartitioning(doc, charOffset, 1);
for (ITypedRegion region : partitions)
{
if (charOffset >= region.getOffset() && charOffset < (region.getOffset() + region.getLength()))
{
return region;
}
}
return null;
}

@Override
protected ITypedRegion[] computePartitioning(IDocument doc, int offset, int length)
throws BadLocationException
{
return new TypedRegion[] { new TypedRegion(0, 2, "__rb__dftl_partition_content_type"),
new TypedRegion(2, 4, "__rb_singleline_comment"),
new TypedRegion(6, 1, "__rb__dftl_partition_content_type") };
}
};
IRegion region = matcher.match(document, 0);
assertNotNull(region);
@@ -124,42 +178,39 @@ protected String getScopeAtOffset(IDocument doc, int charOffset) throws BadLocat
assertNull(matcher.match(document, 4));
}

private void assertRawMatch(IDocument document, int leftOffsetToMatch, int rightOffsetToMatch, int offset,
int length)
/**
* Assumes a symmetrical document where the offset given is the offset from the doc start to the left side of the
* pair, and doc.length - 1 - offset is the right side of the pair.
*
* @param document
* @param source
* @param offset
*/
private void assertMatch(IDocument document, String source, int offset)
{
// left
IRegion region = matcher.match(document, leftOffsetToMatch);
assertNotNull(region);
assertEquals("offset", offset, region.getOffset());
assertEquals("length", length, region.getLength());
assertEquals(ICharacterPairMatcher.LEFT, matcher.getAnchor());
// right
region = matcher.match(document, rightOffsetToMatch);
assertNotNull(region);
assertEquals("offset", offset, region.getOffset());
assertEquals("length", length, region.getLength());
assertEquals(ICharacterPairMatcher.RIGHT, matcher.getAnchor());
int j = source.length() - offset - 1;
assertMatch(document, source, offset, j);
}

private void assertMatch(IDocument document, String source, int i)
private void assertMatch(IDocument document, String source, int leftPairOffset, int rightPairOffset)
{
int j = source.length() - i - 1;
assertMatch(document, source, i, j);
int length = (rightPairOffset - leftPairOffset) + 1;
assertRawMatch(document, leftPairOffset, rightPairOffset, leftPairOffset, length);
}

private void assertMatch(IDocument document, String source, int i, int j)
private void assertRawMatch(IDocument document, int leftOffsetToMatch, int rightOffsetToMatch, int offset,
int length)
{
int length = (j - i) + 1;
// left
IRegion region = matcher.match(document, i + 1);
assertNotNull(region);
assertEquals("offset", i, region.getOffset());
IRegion region = matcher.match(document, leftOffsetToMatch);
assertNotNull("Failed to match forwards from left side of pair", region);
assertEquals("offset", offset, region.getOffset());
assertEquals("length", length, region.getLength());
assertEquals(ICharacterPairMatcher.LEFT, matcher.getAnchor());
// right
region = matcher.match(document, j + 1);
assertNotNull(region);
assertEquals("offset", i, region.getOffset());
region = matcher.match(document, rightOffsetToMatch);
assertNotNull("Failed to match backwards from right side of pair", region);
assertEquals("offset", offset, region.getOffset());
assertEquals("length", length, region.getLength());
assertEquals(ICharacterPairMatcher.RIGHT, matcher.getAnchor());
}