/******************************************************************************* * Copyright (c) 2000, 2004 IBM Corporation and others. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Common Public License v1.0 * which accompanies this distribution, and is available at * http://www.eclipse.org/legal/cpl-v10.html * * Contributors: * IBM Corporation - initial API and implementation *******************************************************************************/ package org.eclipse.jface.text; import java.util.regex.Matcher; import java.util.regex.Pattern; import java.util.regex.PatternSyntaxException; /** * Provides search and replace operations on * {@link org.eclipse.jface.text.IDocument}. *
* Replaces
* {@link org.eclipse.jface.text.IDocument#search(int, String, boolean, boolean, boolean)}.
*
* @since 3.0
*/
public class FindReplaceDocumentAdapter implements CharSequence {
/**
* Internal type for operation codes.
*/
private static class FindReplaceOperationCode {
}
// Find/replace operation codes.
private static final FindReplaceOperationCode FIND_FIRST= new FindReplaceOperationCode();
private static final FindReplaceOperationCode FIND_NEXT= new FindReplaceOperationCode();
private static final FindReplaceOperationCode REPLACE= new FindReplaceOperationCode();
private static final FindReplaceOperationCode REPLACE_FIND_NEXT= new FindReplaceOperationCode();
/**
* The adapted document.
*/
private IDocument fDocument;
/**
* State for findReplace.
*/
private FindReplaceOperationCode fFindReplaceState= null;
/**
* The matcher used in findReplace.
*/
private Matcher fFindReplaceMatcher;
/**
* The match offset from the last findReplace call.
*/
private int fFindReplaceMatchOffset;
/**
* Constructs a new find replace document adapter.
*
* @param document the adapted document
*/
public FindReplaceDocumentAdapter(IDocument document) {
Assert.isNotNull(document);
fDocument= document;
}
/**
* Returns the location of a given string in this adapter's document based on a set of search criteria.
*
* @param startOffset document offset at which search starts
* @param findString the string to find
* @param forwardSearch the search direction
* @param caseSensitive indicates whether lower and upper case should be distinguished
* @param wholeWord indicates whether the findString should be limited by white spaces as
* defined by Character.isWhiteSpace. Must not be used in combination with regExSearch
.
* @param regExSearch if true
findString represents a regular expression
* Must not be used in combination with wholeWord
.
* @return the find or replace region or null
if there was no match
* @throws BadLocationException if startOffset is an invalid document offset
* @throws PatternSyntaxException if a regular expression has invalid syntax
*/
public IRegion find(int startOffset, String findString, boolean forwardSearch, boolean caseSensitive, boolean wholeWord, boolean regExSearch) throws BadLocationException {
Assert.isTrue(!(regExSearch && wholeWord));
// Adjust offset to special meaning of -1
if (startOffset == -1 && forwardSearch)
startOffset= 0;
if (startOffset == -1 && !forwardSearch)
startOffset= length() - 1;
return findReplace(FIND_FIRST, startOffset, findString, null, forwardSearch, caseSensitive, wholeWord, regExSearch);
}
/**
* Stateful findReplace executes a FIND, REPLACE, REPLACE_FIND or FIND_FIRST operation.
* In case of REPLACE and REPLACE_FIND it sends a DocumentEvent
to all
* registered IDocumentListener
.
*
* @param startOffset document offset at which search starts
* this value is only used in the FIND_FIRST operation and otherwise ignored
* @param findString the string to find
* this value is only used in the FIND_FIRST operation and otherwise ignored
* @param replaceText the string to replace the current match
* this value is only used in the REPLACE and REPLACE_FIND operations and otherwise ignored
* @param forwardSearch the search direction
* @param caseSensitive indicates whether lower and upper case should be distinguished
* @param wholeWord indicates whether the findString should be limited by white spaces as
* defined by Character.isWhiteSpace. Must not be used in combination with regExSearch
.
* @param regExSearch if true
this operation represents a regular expression
* Must not be used in combination with wholeWord
.
* @param operationCode specifies what kind of operation is executed
* @return the find or replace region or null
if there was no match
* @throws BadLocationException if startOffset is an invalid document offset
* @throws IllegalStateException if a REPLACE or REPLACE_FIND operation is not preceded by a successful FIND operation
* @throws PatternSyntaxException if a regular expression has invalid syntax
*/
private IRegion findReplace(final FindReplaceOperationCode operationCode, int startOffset, String findString, String replaceText, boolean forwardSearch, boolean caseSensitive, boolean wholeWord, boolean regExSearch) throws BadLocationException {
// Validate option combinations
Assert.isTrue(!(regExSearch && wholeWord));
// Validate state
if ((operationCode == REPLACE || operationCode == REPLACE_FIND_NEXT) && (fFindReplaceState != FIND_FIRST && fFindReplaceState != FIND_NEXT))
throw new IllegalStateException("illegal findReplace state: cannot replace without preceding find"); //$NON-NLS-1$
if (operationCode == FIND_FIRST) {
// Reset
if (findString == null || findString.length() == 0)
return null;
// Validate start offset
if (startOffset < 0 || startOffset >= length())
throw new BadLocationException();
int patternFlags= 0;
if (regExSearch)
patternFlags |= Pattern.MULTILINE;
if (!caseSensitive)
patternFlags |= Pattern.CASE_INSENSITIVE;
if (wholeWord)
findString= "\\b" + findString + "\\b"; //$NON-NLS-1$ //$NON-NLS-2$
if (!regExSearch && !wholeWord)
findString= asRegPattern(findString);
fFindReplaceMatchOffset= startOffset;
if (fFindReplaceMatcher != null && fFindReplaceMatcher.pattern().pattern().equals(findString) && fFindReplaceMatcher.pattern().flags() == patternFlags) {
/*
* Commented out for optimization:
* The call is not needed since FIND_FIRST uses find(int) which resets the matcher
*/
// fFindReplaceMatcher.reset();
} else {
Pattern pattern= Pattern.compile(findString, patternFlags);
fFindReplaceMatcher= pattern.matcher(this);
}
}
// Set state
fFindReplaceState= operationCode;
if (operationCode == REPLACE || operationCode == REPLACE_FIND_NEXT) {
if (regExSearch) {
Pattern pattern= fFindReplaceMatcher.pattern();
Matcher replaceTextMatcher= pattern.matcher(fFindReplaceMatcher.group());
try {
replaceText= replaceTextMatcher.replaceFirst(replaceText);
} catch (IndexOutOfBoundsException ex) {
throw new PatternSyntaxException(ex.getLocalizedMessage(), replaceText, -1);
}
}
int offset= fFindReplaceMatcher.start();
fDocument.replace(offset, fFindReplaceMatcher.group().length(), replaceText);
if (operationCode == REPLACE) {
return new Region(offset, replaceText.length());
}
}
if (operationCode != REPLACE) {
if (forwardSearch) {
boolean found= false;
if (operationCode == FIND_FIRST)
found= fFindReplaceMatcher.find(startOffset);
else
found= fFindReplaceMatcher.find();
if (operationCode == REPLACE_FIND_NEXT)
fFindReplaceState= FIND_NEXT;
if (found && fFindReplaceMatcher.group().length() > 0) {
return new Region(fFindReplaceMatcher.start(), fFindReplaceMatcher.group().length());
} else {
return null;
}
} else { // backward search
boolean found= fFindReplaceMatcher.find(0);
int index= -1;
int length= -1;
while (found && fFindReplaceMatcher.start() <= fFindReplaceMatchOffset) {
index= fFindReplaceMatcher.start();
length= fFindReplaceMatcher.group().length();
found= fFindReplaceMatcher.find(index + 1);
}
fFindReplaceMatchOffset= index;
if (index > -1) {
// must set matcher to correct position
fFindReplaceMatcher.find(index);
return new Region(index, length);
} else
return null;
}
}
return null;
}
/**
* Converts a non-regex string to a pattern
* that can be used with the regex search engine.
*
* @param string the non-regex pattern
* @return the string converted to a regex pattern
*/
private String asRegPattern(String string) {
StringBuffer out= new StringBuffer(string.length());
boolean quoting= false;
for (int i= 0, length= string.length(); i < length; i++) {
char ch= string.charAt(i);
if (ch == '\\') {
if (quoting) {
out.append("\\E"); //$NON-NLS-1$
quoting= false;
}
out.append("\\\\"); //$NON-NLS-1$
continue;
}
if (!quoting) {
out.append("\\Q"); //$NON-NLS-1$
quoting= true;
}
out.append(ch);
}
if (quoting)
out.append("\\E"); //$NON-NLS-1$
return out.toString();
}
/**
* Substitutes the previous match with the given text.
* Sends a DocumentEvent
to all registered IDocumentListener
.
*
* @param text the substitution text
* @param regExReplace if true
text
represents a regular expression
* @return the replace region or null
if there was no match
* @throws BadLocationException if startOffset is an invalid document offset
* @throws IllegalStateException if a REPLACE or REPLACE_FIND operation is not preceded by a successful FIND operation
* @throws PatternSyntaxException if a regular expression has invalid syntax
*
* @see DocumentEvent
* @see IDocumentListener
*/
public IRegion replace(String text, boolean regExReplace) throws BadLocationException {
return findReplace(REPLACE, -1, null, text, false, false, false, regExReplace);
}
// ---------- CharSequence implementation ----------
/*
* @see java.lang.CharSequence#length()
*/
public int length() {
return fDocument.getLength();
}
/*
* @see java.lang.CharSequence#charAt(int)
*/
public char charAt(int index) {
try {
return fDocument.getChar(index);
} catch (BadLocationException e) {
throw new IndexOutOfBoundsException();
}
}
/*
* @see java.lang.CharSequence#subSequence(int, int)
*/
public CharSequence subSequence(int start, int end) {
try {
return fDocument.get(start, end - start);
} catch (BadLocationException e) {
throw new IndexOutOfBoundsException();
}
}
/*
* @see java.lang.Object#toString()
*/
public String toString() {
return fDocument.get();
}
}