Bug 372128: [syntax highlighting] "Highlight enclosing brackets" has
massive performance problems
diff --git a/org.eclipse.jface.text/src/org/eclipse/jface/text/source/DefaultCharacterPairMatcher.java b/org.eclipse.jface.text/src/org/eclipse/jface/text/source/DefaultCharacterPairMatcher.java
index 4983562..318fa54 100644
--- a/org.eclipse.jface.text/src/org/eclipse/jface/text/source/DefaultCharacterPairMatcher.java
+++ b/org.eclipse.jface.text/src/org/eclipse/jface/text/source/DefaultCharacterPairMatcher.java
@@ -9,9 +9,6 @@
  *     Christian Plesner Hansen (plesner@quenta.org) - initial API and implementation
  *******************************************************************************/
 package org.eclipse.jface.text.source;
-import java.util.HashSet;
-import java.util.Set;
-
 import org.eclipse.core.runtime.Assert;
 
 import org.eclipse.jface.text.BadLocationException;
@@ -132,38 +129,94 @@
 		if (document == null || offset < 0 || offset > document.getLength())
 			return null;
 
-		int start;
-		int end;
-		if (length >= 0) {
-			start= offset;
-			end= offset + length;
-		} else {
-			end= offset;
-			start= offset + length;
+		//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;
 		}
 
-		int sourceCaretOffset= offset + length;
-		int adjustment= getOffsetAdjustment(document, sourceCaretOffset, length);
-		sourceCaretOffset+= adjustment;
-
+		//bracket is not selected
 		try {
-			for (int offset1= sourceCaretOffset; offset1 >= 0; offset1--) {
-				char prevChar= document.getChar(Math.max(offset1 - 1, 0));
-				if (fPairs.contains(prevChar) && fPairs.isStartCharacter(prevChar)) {
-					IRegion match= performMatch(document, offset1);
-					if (match != null) {
-						int matchOffset= match.getOffset();
-						int matchLength= match.getLength();
-						if ((matchOffset <= start) && (matchOffset + matchLength > start) && (matchOffset < end) && (matchOffset + matchLength >= end)) {
-							return match;
-						}
-					}
-				}
-			}
+			final String partition1= TextUtilities.getContentType(document, fPartitioning, offset, false);
+			final DocumentPartitionAccessor partDoc= new DocumentPartitionAccessor(document, fPartitioning, partition1);
+			return findEnclosingPeers(document, partDoc, offset, length, 0, document.getLength());
 		} catch (BadLocationException ble) {
+			fAnchor= -1;
 			return null;
 		}
-		return null;
+	}
+
+	/**
+	 * @see org.eclipse.jface.text.source.ICharacterPairMatcherExtension#isMatchedChar(char)
+	 * @since 3.8
+	 */
+	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
+	 */
+	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
+	 */
+	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, previousEndOffset, false);
+			String currStartContentType= TextUtilities.getContentType(document, fPartitioning, currentEndOffset, false);
+			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= start; 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= start; i < end; i++) {
+				if (isMatchedChar(document.getChar(i))) {
+					return true;
+				}
+			}
+		} catch (BadLocationException e) {
+			//do nothing
+		}
+		return false;
 	}
 
 	/**
@@ -261,6 +314,106 @@
 		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];
+		int pos1= doc.getNextPosition(start, false);
+		int pos2= start;
+	
+		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) {
+				//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;
+	}
+
 	/* @see ICharacterPairMatcher#getAnchor() */
 	public int getAnchor() {
 		return fAnchor;
@@ -285,6 +438,7 @@
 		private final IDocument fDocument;
 		private final String fPartitioning, fPartition;
 		private ITypedRegion fCachedPartition;
+		private int fLength;
 
 		/**
 		 * Creates a new partitioned document for the specified document.
@@ -298,6 +452,7 @@
 			fDocument= doc;
 			fPartitioning= partitioning;
 			fPartition= partition;
+			fLength= doc.getLength();
 		}
 
 		/**
@@ -338,7 +493,7 @@
 		}
 
 		/**
-		 * Returns the next position to query in the search.  The position
+		 * 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
@@ -347,8 +502,7 @@
 		 */
 		public int getNextPosition(int pos, boolean searchForward) {
 			final ITypedRegion partition= getPartition(pos);
-			if (partition == null) return simpleIncrement(pos, searchForward);
-			if (fPartition.equals(partition.getType()))
+			if (partition == null || fPartition.equals(partition.getType()))
 				return simpleIncrement(pos, searchForward);
 			if (searchForward) {
 				int end= partition.getOffset() + partition.getLength();
@@ -376,7 +530,7 @@
 		 */
 		private ITypedRegion getPartition(int pos) {
 			if (fCachedPartition == null || !contains(fCachedPartition, pos)) {
-				Assert.isTrue(pos >= 0 && pos <= fDocument.getLength());
+				Assert.isTrue(pos >= 0 && pos <= fLength);
 				try {
 					fCachedPartition= TextUtilities.getPartition(fDocument, fPartitioning, pos, false);
 				} catch (BadLocationException e) {
@@ -405,28 +559,18 @@
 		}
 
 		/**
-		 * Returns true if the specified character pair occurs in one
-		 * of the character 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) {
-			return getAllCharacters().contains(new Character(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(new Character(fPairs[i]));
-				fCharsCache= set;
+			char[] pairs= fPairs;
+			for (int i= 0, n= pairs.length; i < n; i++) {
+				if (c == pairs[i])
+					return true;
 			}
-			return fCharsCache;
+			return false;
 		}
 
 		/**
diff --git a/org.eclipse.jface.text/src/org/eclipse/jface/text/source/ICharacterPairMatcherExtension.java b/org.eclipse.jface.text/src/org/eclipse/jface/text/source/ICharacterPairMatcherExtension.java
index ef87038..fb64f42 100644
--- a/org.eclipse.jface.text/src/org/eclipse/jface/text/source/ICharacterPairMatcherExtension.java
+++ b/org.eclipse.jface.text/src/org/eclipse/jface/text/source/ICharacterPairMatcherExtension.java
@@ -49,4 +49,48 @@
 	 *         enclosing pair
 	 */
 	IRegion findEnclosingPeerCharacters(IDocument document, int offset, int length);
+
+	/**
+	 * Checks whether the character is one of the characters matched by the pair matcher.
+	 * 
+	 * @param ch the character
+	 * @return <code>true</code> if the the character is one of the characters matched by the pair
+	 *         matcher, and <code>false</code> otherwise
+	 */
+	boolean isMatchedChar(char ch);
+
+	/**
+	 * Checks whether the character is one of the characters matched by the pair matcher.
+	 * 
+	 * <p>
+	 * Clients can use this method to handle characters which may have special meaning in some
+	 * situations. E.g. in Java, '<' is used as an angular bracket and as well as less-than operator.
+	 * </p>
+	 * 
+	 * @param ch the character
+	 * @param document the document
+	 * @param offset the offset in document
+	 * @return <code>true</code> if the the character is one of the characters matched by the pair
+	 *         matcher, and <code>false</code> otherwise
+	 */
+	boolean isMatchedChar(char ch, IDocument document, int offset);
+
+	/**
+	 * Computes whether a client needs to recompute the enclosing pair after a selection change in
+	 * the document.
+	 * 
+	 * <p>
+	 * This is intended to be a quick test to determine whether a re-computation of the enclosing pair is
+	 * required, as the re-computation after each selection change via a
+	 * {@link #findEnclosingPeerCharacters(IDocument, int, int)} call can be expensive for some
+	 * clients.
+	 * </p>
+	 * 
+	 * @param document the document to work on
+	 * @param currentSelection the current selection in the document
+	 * @param previousSelection the previous selection in the document
+	 * @return <code>true</code> if the enclosing pair needs to be recomputed, <code>false</code>
+	 *         otherwise
+	 */
+	boolean isRecomputationOfEnclosingPairRequired(IDocument document, IRegion currentSelection, IRegion previousSelection);
 }
diff --git a/org.eclipse.jface.text/src/org/eclipse/jface/text/source/MatchingCharacterPainter.java b/org.eclipse.jface.text/src/org/eclipse/jface/text/source/MatchingCharacterPainter.java
index f32f1a0..4b416d2 100644
--- a/org.eclipse.jface.text/src/org/eclipse/jface/text/source/MatchingCharacterPainter.java
+++ b/org.eclipse.jface.text/src/org/eclipse/jface/text/source/MatchingCharacterPainter.java
@@ -20,21 +20,27 @@
 import org.eclipse.swt.graphics.Rectangle;
 
 import org.eclipse.jface.text.BadLocationException;
+import org.eclipse.jface.text.DocumentEvent;
 import org.eclipse.jface.text.IDocument;
 import org.eclipse.jface.text.IPaintPositionManager;
 import org.eclipse.jface.text.IPainter;
 import org.eclipse.jface.text.IRegion;
+import org.eclipse.jface.text.ITextInputListener;
+import org.eclipse.jface.text.ITextListener;
 import org.eclipse.jface.text.ITextViewerExtension5;
 import org.eclipse.jface.text.Position;
 import org.eclipse.jface.text.Region;
+import org.eclipse.jface.text.TextEvent;
 
 /**
- * Highlights the peer character matching the character near the caret position.
- * This painter can be configured with an
- * {@link org.eclipse.jface.text.source.ICharacterPairMatcher}.
+ * Highlights the peer character matching the character near the caret position, or a pair of peer
+ * characters enclosing the caret position. This painter can be configured with an
+ * {@link org.eclipse.jface.text.source.ICharacterPairMatcher} or an
+ * {@link org.eclipse.jface.text.source.ICharacterPairMatcherExtension}.
  * <p>
- * Clients instantiate and configure object of this class.</p>
- *
+ * Clients instantiate and configure an object of this class.
+ * </p>
+ * 
  * @since 2.1
  */
 public final class MatchingCharacterPainter implements IPainter, PaintListener {
@@ -55,13 +61,62 @@
 	private Position fPairPosition= new Position(0, 0);
 	/** The anchor indicating whether the character is left or right of the caret */
 	private int fAnchor;
-	/** Whether to highlight enclosing peer characters or not. */
-	private boolean fHighlightEnclosingPeerCharcters;
-	/** Whether to highlight the character at caret location or not. */
+
+	/**
+	 * Whether to highlight enclosing peer characters or not.
+	 * 
+	 * @since 3.8
+	 */
+	private boolean fHighlightEnclosingPeerCharacters;
+
+	/**
+	 * Whether to highlight the character at caret location or not.
+	 * 
+	 * @since 3.8
+	 */
 	private boolean fHighlightCharacterAtCaretLocation;
-	/** Whether a character is present at caret location or not. */
+
+	/**
+	 * Whether a character is present at caret location or not.
+	 * 
+	 * @since 3.8
+	 */
 	private boolean fCharacterPresentAtCaretLocation;
 
+	/**
+	 * The document this painter is associated with.
+	 * 
+	 * @since 3.8
+	 */
+	private IDocument fDocument;
+
+	/**
+	 * The previous selection, used to determine the need for computing enclosing brackets.
+	 * 
+	 * @since 3.8
+	 */
+	private IRegion fPreviousSelection;
+
+	/**
+	 * Previous length of the document this painter is associated with.
+	 * 
+	 * @since 3.8
+	 */
+	private int fPreviousLengthOfDocument;
+
+	/**
+	 * Whether the input document has been replaced or not.
+	 * 
+	 * @since 3.8
+	 */
+	private boolean fDocumentChanged;
+
+	/**
+	 * The text viewer change listener.
+	 * 
+	 * @since 3.8
+	 */
+	private TextListener fTextListener;
 
 	/**
 	 * Creates a new MatchingCharacterPainter for the given source viewer using the given character
@@ -75,6 +130,7 @@
 		fSourceViewer= sourceViewer;
 		fMatcher= matcher;
 		fTextWidget= sourceViewer.getTextWidget();
+		fDocument= fSourceViewer.getDocument();
 	}
 
 	/**
@@ -95,7 +151,8 @@
 	 * @since 3.8
 	 */
 	public void setHighlightEnclosingPeerCharacters(boolean highlightEnclosingPeerCharcters) {
-		fHighlightEnclosingPeerCharcters= highlightEnclosingPeerCharcters;
+		fHighlightEnclosingPeerCharacters= highlightEnclosingPeerCharcters;
+		installUninstallTextListener(highlightEnclosingPeerCharcters);
 	}
 
 	/**
@@ -112,6 +169,10 @@
 	 */
 	public void dispose() {
 		if (fMatcher != null) {
+			if (fMatcher instanceof ICharacterPairMatcherExtension && fTextListener != null) {
+				installUninstallTextListener(false);
+			}
+
 			fMatcher.clear();
 			fMatcher= null;
 		}
@@ -185,7 +246,7 @@
 			offset -= region.getOffset();
 		}
 
-		if (fHighlightCharacterAtCaretLocation || (fHighlightEnclosingPeerCharcters && !fCharacterPresentAtCaretLocation)) {
+		if (fHighlightCharacterAtCaretLocation || (fHighlightEnclosingPeerCharacters && !fCharacterPresentAtCaretLocation)) {
 			draw(gc, offset, 1);
 			draw(gc, offset + length - 1, 1);
 		} else {
@@ -262,7 +323,7 @@
 	 */
 	public void paint(int reason) {
 
-		IDocument document= fSourceViewer.getDocument();
+		IDocument document= fDocument;
 		if (document == null) {
 			deactivate(false);
 			return;
@@ -276,7 +337,28 @@
 			ICharacterPairMatcherExtension matcher= (ICharacterPairMatcherExtension)fMatcher;
 			pair= matcher.match(document, selection.getOffset(), selection.getLength());
 			characterPresentAtCaretLocation= (pair != null);
-			if (pair == null && fHighlightEnclosingPeerCharcters) {
+			if (pair == null && fHighlightEnclosingPeerCharacters) {
+
+				int length= document.getLength();
+				boolean lengthChanged= length != fPreviousLengthOfDocument;
+				fPreviousLengthOfDocument= length;
+
+				if (reason != IPainter.CONFIGURATION && !fDocumentChanged && !lengthChanged && selection.equals(fPreviousSelection)) {
+					return;
+				}
+
+				if (reason == IPainter.TEXT_CHANGE) {
+					fPreviousSelection= selection;
+					return;
+				}
+
+				fDocumentChanged= false;
+				if (reason != IPainter.CONFIGURATION && !lengthChanged && fPreviousSelection != null && reason != IPainter.INTERNAL) {
+					if (!matcher.isRecomputationOfEnclosingPairRequired(document, selection, fPreviousSelection)) {
+						fPreviousSelection= selection;
+						return;
+					}
+				}
 				pair= matcher.findEnclosingPeerCharacters(document, selection.getOffset(), selection.getLength());
 			}
 		} else {
@@ -288,6 +370,7 @@
 			characterPresentAtCaretLocation= (pair != null);
 		}
 
+		fPreviousSelection= selection;
 		if (pair == null) {
 			deactivate(true);
 			return;
@@ -340,4 +423,95 @@
 	public void setPositionManager(IPaintPositionManager manager) {
 		fPaintPositionManager= manager;
 	}
+
+	/**
+	 * Installs or uninstalls the text listener depending on the boolean parameter.
+	 * 
+	 * @param install <code>true</code> to install the text listener, <code>false</code> to uninstall 
+	 * 
+	 * @since 3.8
+	 */
+	private void installUninstallTextListener(boolean install) {
+		if (!(fMatcher instanceof ICharacterPairMatcherExtension))
+			return;
+	
+		if (install) {
+			fTextListener= new TextListener();
+			fSourceViewer.addTextInputListener(fTextListener);
+			fSourceViewer.addTextListener(fTextListener);
+		} else {
+			if (fTextListener != null) {
+				fSourceViewer.removeTextInputListener(fTextListener);
+				fSourceViewer.removeTextListener(fTextListener);
+				fTextListener= null;
+			}
+		}
+	}
+
+	/**
+	 * Listens to document changes and if required by those document changes causes a re-computation
+	 * of matching characters.
+	 * 
+	 * @since 3.8
+	 */
+	private class TextListener implements ITextListener, ITextInputListener {
+
+		/**
+		 * @see org.eclipse.jface.text.ITextInputListener#inputDocumentAboutToBeChanged(org.eclipse.jface.text.IDocument,
+		 *      org.eclipse.jface.text.IDocument)
+		 */
+		public void inputDocumentAboutToBeChanged(IDocument oldInput, IDocument newInput) {
+			//do nothing
+		}
+
+		/**
+		 * @see org.eclipse.jface.text.ITextInputListener#inputDocumentChanged(org.eclipse.jface.text.IDocument,
+		 *      org.eclipse.jface.text.IDocument)
+		 */
+		public void inputDocumentChanged(IDocument oldInput, IDocument newInput) {
+			fDocument= newInput;
+			fDocumentChanged= true;
+		}
+
+		/**
+		 * @see org.eclipse.jface.text.ITextListener#textChanged(org.eclipse.jface.text.TextEvent)
+		 */
+		public void textChanged(TextEvent event) {
+			if (!fHighlightEnclosingPeerCharacters || !(fMatcher instanceof ICharacterPairMatcherExtension))
+				return;
+
+			String text= event.getText();
+			String replacedText= event.getReplacedText();
+
+			boolean viewerRedrawState= event.getViewerRedrawState();
+			DocumentEvent documentEvent= event.getDocumentEvent();
+			if (documentEvent == null && !viewerRedrawState)
+				return;
+
+			ICharacterPairMatcherExtension matcher= (ICharacterPairMatcherExtension)fMatcher;
+			boolean found= searchForCharacters(text, matcher) || searchForCharacters(replacedText, matcher);
+
+			if (found || (documentEvent == null && viewerRedrawState)) {
+				paint(IPainter.INTERNAL);
+			}
+		}
+
+		/**
+		 * Searches for matched characters in the given string.
+		 * 
+		 * @param text the string to search
+		 * @param matcher the pair matcher
+		 * @return <code>true</code> if a matched character is found, <code>false</code> otherwise
+		 */
+		private boolean searchForCharacters(String text, ICharacterPairMatcherExtension matcher) {
+			if (text == null)
+				return false;
+			for (int i= 0; i < text.length(); i++) {
+				if (matcher.isMatchedChar(text.charAt(i))) {
+					return true;
+				}
+			}
+			return false;
+		}
+	}
 }