/*******************************************************************************
* Copyright (c) 2012-2015 Codenvy, S.A.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
* Codenvy, S.A. - initial API and implementation
*******************************************************************************/
package org.eclipse.che.ide.ext.java.jdt.text;
import org.eclipse.che.ide.api.text.BadLocationException;
import org.eclipse.che.ide.api.text.Region;
import org.eclipse.che.ide.api.text.RegionImpl;
import org.eclipse.che.ide.runtime.Assert;
import com.google.gwt.regexp.shared.MatchResult;
import com.google.gwt.regexp.shared.RegExp;
import java.util.regex.PatternSyntaxException;
/**
* Provides search and replace operations on {@link org.eclipse.che.ide.legacy.client.api.text.eclipse.Document.text.IDocument}.
* <p/>
* Replaces {@link org.eclipse.che.ide.legacy.client.api.text.eclipse.Document.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 Document fDocument;
/** State for findReplace. */
private FindReplaceOperationCode fFindReplaceState = null;
/** The matcher used in findReplace. */
private RegExp regExp;
/** 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(Document 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 <code>regExSearch</code>.
* @param regExSearch
* if <code>true</code> findString represents a regular expression Must not be used in combination with
* <code>wholeWord</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 Region 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);
}
/**
* 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 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 <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 org.eclipse.che.ide.api.text.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 Region findReplace(final FindReplaceOperationCode operationCode, int startOffset, String findString,
String replaceText, boolean forwardSearch, boolean caseSensitive, boolean wholeWord)
throws BadLocationException {
// 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();
String patternFlags = "g";
if (caseSensitive)
patternFlags += "i";
if (wholeWord)
findString = "\\b" + findString + "\\b"; //$NON-NLS-1$ //$NON-NLS-2$
if (!wholeWord)
findString = asRegPattern(findString);
fFindReplaceMatchOffset = startOffset;
regExp = RegExp.compile(findString, patternFlags);
regExp.setLastIndex(fFindReplaceMatchOffset);
}
// Set state
fFindReplaceState = operationCode;
if (operationCode != REPLACE) {
if (forwardSearch) {
MatchResult matchResult = regExp.exec(String.valueOf(this));
if (matchResult != null && matchResult.getGroupCount() > 0 && !matchResult.getGroup(0).isEmpty())
return new RegionImpl(matchResult.getIndex(), matchResult.getGroup(0).length());
return null;
}
// backward search
regExp.setLastIndex(0);
MatchResult matchResult = regExp.exec(String.valueOf(this));
boolean found = matchResult != null;
int index = -1;
int length = -1;
while (found && matchResult.getIndex() + matchResult.getGroup(0).length() <= fFindReplaceMatchOffset + 1) {
index = matchResult.getIndex();
length = matchResult.getGroup(0).length();
regExp.setLastIndex(index + 1);
matchResult = regExp.exec(String.valueOf(this));
found = matchResult != null;
}
fFindReplaceMatchOffset = index;
if (index > -1) {
// must set matcher to correct position
regExp.setLastIndex(index);
matchResult = regExp.exec(String.valueOf(this));
return new RegionImpl(index, length);
}
return null;
}
return null;
}
/**
* Substitutes the previous match with the given text. Sends a <code>DocumentEvent</code> to all registered
* <code>IDocumentListener</code>.
*
* @param text
* the substitution text
* @param regExReplace
* 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 DocumentListener
*/
public Region replace(String text, boolean regExReplace) throws BadLocationException {
// TODO
// return findReplace(REPLACE, -1, null, text, false, false, false, regExReplace);
return null;
}
// ---------- 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();
}
/**
* 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());
for (int i = 0, length = string.length(); i < length; i++) {
char ch = string.charAt(i);
if (ch == '\\') {
out.append("\\\\"); //$NON-NLS-1$
} else if (ch == '*') {
out.append("\\*");
} else {
out.append(ch);
}
}
return out.toString();
}
/**
* Escapes special characters in the string, such that the resulting pattern matches the given string.
*
* @param string
* the string to escape
* @return a regex pattern that matches the given string
* @since 3.5
*/
public static String escapeForRegExPattern(String string) {
// implements https://bugs.eclipse.org/bugs/show_bug.cgi?id=44422
StringBuffer pattern = new StringBuffer(string.length() + 16);
int length = string.length();
for (int i = 0; i < length; i++) {
char ch = string.charAt(i);
switch (ch) {
case '\\':
case '(':
case ')':
case '[':
case ']':
case '{':
case '}':
case '.':
case '?':
case '*':
case '+':
case '|':
case '^':
case '$':
pattern.append('\\').append(ch);
break;
case '\r':
if (i + 1 < length && string.charAt(i + 1) == '\n')
i++;
//$FALL-THROUGH$
break;
case '\n':
pattern.append("\\R"); //$NON-NLS-1$
break;
case '\t':
pattern.append("\\t"); //$NON-NLS-1$
break;
case '\f':
pattern.append("\\f"); //$NON-NLS-1$
break;
case 0x07:
pattern.append("\\a"); //$NON-NLS-1$
break;
case 0x1B:
pattern.append("\\e"); //$NON-NLS-1$
break;
default:
if (0 <= ch && ch < 0x20) {
pattern.append("\\x"); //$NON-NLS-1$
pattern.append(Integer.toHexString(ch).toUpperCase());
} else {
pattern.append(ch);
}
}
}
return pattern.toString();
}
}