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..9dd7a01 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,75 @@ 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 previousCaretOffset= previousSelection.getOffset() + previousSelection.getLength(); + int currentCaretOffset= currentSelection.getOffset() + currentSelection.getLength(); + + try { + String prevContentType= TextUtilities.getContentType(document, fPartitioning, previousCaretOffset, false); + String currContentType= TextUtilities.getContentType(document, fPartitioning, currentCaretOffset, false); + + if (!prevContentType.equals(currContentType)) + return true; + + int start; + int end; + if (currentCaretOffset > previousCaretOffset) { + start= previousCaretOffset; + end= currentCaretOffset; + } else { + start= currentCaretOffset; + end= previousCaretOffset; + } + for (int i= start; i < end; i++) { + if (isMatchedChar(document.getChar(i))) { + return true; + } + } + } catch (BadLocationException e) { + //do nothing + } + return false; } /** @@ -261,6 +295,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 +419,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 +433,7 @@ fDocument= doc; fPartitioning= partitioning; fPartition= partition; + fLength= doc.getLength(); } /** @@ -338,7 +474,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 +483,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 +511,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 +540,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/**/ fCharsCache= null; - /** - * @return A set containing all characters occurring in character pairs. - */ - private Set/**/ getAllCharacters() { - if (fCharsCache == null) { - Set/**/ set= new HashSet/**/(); - 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..aacbb88 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 true if the the character is one of the characters matched by the pair + * matcher, and false otherwise + */ + boolean isMatchedChar(char ch); + + /** + * Checks whether the character is one of the characters matched by the pair matcher. + * + *

+ * 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. + *

+ * + * @param ch the character + * @param document the document + * @param offset the offset in document + * @return true if the the character is one of the characters matched by the pair + * matcher, and false 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. + * + *

+ * This is intended to be quick test to determine whether a re-computation of 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. + *

+ * + * @param document the document to work on + * @param currentSelection the current selection in the document + * @param previousSelection the previous selection in the document + * @return true if the enclosing pair needs to be recomputed, false + * 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..e6cfbda 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}. *

- * Clients instantiate and configure object of this class.

- * + * Clients instantiate and configure an object of this class. + *

+ * * @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,100 @@ public void setPositionManager(IPaintPositionManager manager) { fPaintPositionManager= manager; } + + /** + * Installs or uninstalls the text listener depending on the boolean parameter. + * + * @param install whether to install or uninstall the text listener + * + * @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) + * @since 3.8 + */ + 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) + * @since 3.8 + */ + public void inputDocumentChanged(IDocument oldInput, IDocument newInput) { + fDocument= newInput; + fDocumentChanged= true; + } + + /** + * @see org.eclipse.jface.text.ITextListener#textChanged(org.eclipse.jface.text.TextEvent) + * @since 3.8 + */ + 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 true if a matched character is found, false otherwise + * + * @since 3.8 + */ + 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; + } + } }