blob: 40e66f75af9b26f4a62007b64dba83bd99ea9b35 [file] [log] [blame]
/*******************************************************************************
* Copyright (c) 2000, 2003 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;
/**
* Adapts {@link org.eclipse.jface.text.IDocument} for doing search and
* replace operations.
*
* @since 3.0
*/
public class FindReplaceDocumentAdapter implements CharSequence {
// Shortcuts to findReplace opertion codes
private static final FindReplaceOperationCode FIND_FIRST= FindReplaceOperationCode.FIND_FIRST;
private static final FindReplaceOperationCode FIND_NEXT= FindReplaceOperationCode.FIND_NEXT;
private static final FindReplaceOperationCode REPLACE= FindReplaceOperationCode.REPLACE;
private static final FindReplaceOperationCode REPLACE_FIND_NEXT= FindReplaceOperationCode.REPLACE_FIND_NEXT;
/**
* 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 region of a given search string in the 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 <code>regExSearch</code>.
* @param regExSearch if <code>true</code> findString represents a regular expression
* Must not be used in combination with <code>regExSearch</code>.
* @return the find or replace region or <code>null</code> 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 search(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 <code>DocumentEvent</code> to all
* registered <code>IDocumentListener</code>.
*
* @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 replaceString 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 <code>regExSearch</code>.
* @param regExSearch if <code>true</code> this operation represents a regular expression
* Must not be used in combination with <code>wholeWord</code>.
* @param operationCode specifies what kind of operation is executed
* @return the find or replace region or <code>null</code> 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 FindReplaceOperationCode#FIND_FIRST
* @see FindReplaceOperationCode#FIND_NEXT
* @see FindReplaceOperationCode#REPLACE
* @see FindReplaceOperationCode#REPLACE_FIND_NEXT
*/
public IRegion findReplace(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= "\\Q" + findString + "\\E"; //$NON-NLS-1$ //$NON-NLS-2$
fFindReplaceMatchOffset= startOffset;
if (fFindReplaceMatcher != null && fFindReplaceMatcher.pattern().pattern().equals(findString) && fFindReplaceMatcher.pattern().flags() == patternFlags) {
/*
* Commented out for optimazation:
* 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 == FIND_FIRST || operationCode == FIND_NEXT) {
if (forwardSearch) {
boolean found= false;
if (operationCode == FIND_FIRST)
found= fFindReplaceMatcher.find(startOffset);
else
found= fFindReplaceMatcher.find();
fFindReplaceState= operationCode;
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;
}
/**
* Subsitutes the previous match with the given text.
* Sends a <code>DocumentEvent</code> to all registered <code>IDocumentListener</code>.
*
* @param length the length of the specified range
* @param text the substitution text
* @param isRegExReplace if <code>true</code> <code>text</code> represents a regular expression
* @return the replace region or <code>null</code> 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(FindReplaceOperationCode.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();
}
}