mirror of
https://gitflic.ru/project/openide/openide.git
synced 2026-09-27 10:03:11 +07:00
IDEA-70451 RegEx case transformation options during replace
This commit is contained in:
@@ -423,6 +423,22 @@ public class FindManagerTest extends DaemonAnalyzerTestCase {
|
||||
assertEquals(text, getEditor().getDocument().getText());
|
||||
}
|
||||
|
||||
public void testReplaceWithLocalLowerCase() {
|
||||
doTestRegexpReplace("SOMETHING", "ome", "\\l$0", "SoMETHING");
|
||||
}
|
||||
|
||||
public void testReplaceWithLocalUpperCase() {
|
||||
doTestRegexpReplace("something", "ome", "\\u$0", "sOmething");
|
||||
}
|
||||
|
||||
public void testReplaceWithRegionLowerCase() {
|
||||
doTestRegexpReplace("SOMETHING", "ome", "\\L$0\\E", "SomeTHING");
|
||||
}
|
||||
|
||||
public void testReplaceWithRegionUpperCase() {
|
||||
doTestRegexpReplace("something", "ome", "\\U$0\\E", "sOMEthing");
|
||||
}
|
||||
|
||||
public void testReplaceRegexpWithNewLine() {
|
||||
FindModel findModel = new FindModel();
|
||||
findModel.setStringToFind("xxx");
|
||||
@@ -750,4 +766,15 @@ public class FindManagerTest extends DaemonAnalyzerTestCase {
|
||||
DumbServiceImpl.getInstance(getProject()).setDumb(false);
|
||||
}
|
||||
}
|
||||
|
||||
private void doTestRegexpReplace(String initialText, String searchString, String replaceString, String expectedResult) {
|
||||
configureByText(FileTypes.PLAIN_TEXT, initialText);
|
||||
FindModel model = new FindModel();
|
||||
model.setRegularExpressions(true);
|
||||
model.setStringToFind(searchString);
|
||||
model.setStringToReplace(replaceString);
|
||||
model.setPromptOnReplace(false);
|
||||
assertTrue(FindUtil.replace(myProject, myEditor, 0, model));
|
||||
assertEquals(expectedResult, myEditor.getDocument().getText());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2000-2014 JetBrains s.r.o.
|
||||
* Copyright 2000-2015 JetBrains s.r.o.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -756,13 +756,10 @@ public class FindManagerImpl extends FindManager {
|
||||
}
|
||||
|
||||
private static String getStringToReplaceByRegexp(@NotNull final FindModel model, Matcher matcher) throws MalformedReplacementStringException{
|
||||
StringBuffer replaced = new StringBuffer();
|
||||
if (matcher == null) return null;
|
||||
try {
|
||||
String toReplace = StringUtil.unescapeStringCharacters(model.getStringToReplace());
|
||||
matcher.appendReplacement(replaced, toReplace);
|
||||
|
||||
return replaced.substring(matcher.start());
|
||||
String toReplace = model.getStringToReplace();
|
||||
return new RegExReplacementBuilder(matcher).createReplacement(toReplace);
|
||||
}
|
||||
catch (Exception e) {
|
||||
throw createMalformedReplacementException(model, e);
|
||||
|
||||
@@ -0,0 +1,276 @@
|
||||
/*
|
||||
* Copyright 2000-2015 JetBrains s.r.o.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.intellij.find.impl;
|
||||
|
||||
import com.intellij.openapi.diagnostic.Logger;
|
||||
import com.intellij.openapi.util.SystemInfo;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.regex.Matcher;
|
||||
|
||||
/**
|
||||
* Generates a replacement string for search/replace operation using regular expressions.
|
||||
* <p>
|
||||
* The logic is based on java.util.regex.Matcher.appendReplacement method, special characters (\n, \r, \t, \f, \b, \xNNNN)
|
||||
* and case conversion characters (\l, \u, \L, \U, \E) are additionally supported.
|
||||
* <p>
|
||||
* Instances of this class are not safe for use by multiple concurrent threads, just as {@link Matcher} instances are.
|
||||
*/
|
||||
public class RegExReplacementBuilder {
|
||||
private static final Logger LOGGER = Logger.getInstance(RegExReplacementBuilder.class);
|
||||
|
||||
private static Method ourNamedGroupValueRetriever;
|
||||
|
||||
static {
|
||||
if (SystemInfo.isJavaVersionAtLeast("1.7")) {
|
||||
try {
|
||||
Method method = Matcher.class.getMethod("group", String.class);
|
||||
Class<?> returnType = method.getReturnType();
|
||||
if (String.class.equals(returnType)) {
|
||||
ourNamedGroupValueRetriever = method;
|
||||
}
|
||||
else {
|
||||
throw new RuntimeException("Unexpected method return value: " + returnType);
|
||||
}
|
||||
}
|
||||
catch (Exception e) {
|
||||
LOGGER.warn("Error accessing method java.util.regex.Matcher.group(java.lang.String)", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@NotNull private final Matcher myMatcher;
|
||||
|
||||
private String myTemplate;
|
||||
private int myCursor;
|
||||
private StringBuilder myReplacement;
|
||||
private List<CaseConversionRegion> myConversionRegions;
|
||||
|
||||
public RegExReplacementBuilder(@NotNull Matcher matcher) {
|
||||
myMatcher = matcher;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a replacement string from provided template value, substituting referenced capturing group values, and processing supported
|
||||
* special and control characters.
|
||||
* <p>
|
||||
* Matcher used to create this instance of RegExReplacementBuilder is supposed to be in a state
|
||||
* created by a successful {@link Matcher#find() find()} or {@link Matcher#find(int) find(int)} invocation.
|
||||
*/
|
||||
public String createReplacement(String template) {
|
||||
myTemplate = template;
|
||||
resetState();
|
||||
while (myCursor < myTemplate.length()) {
|
||||
char nextChar = myTemplate.charAt(myCursor++);
|
||||
if (nextChar == '\\') {
|
||||
processEscapedChar();
|
||||
} else if (nextChar == '$') {
|
||||
processGroupValue();
|
||||
} else {
|
||||
myReplacement.append(nextChar);
|
||||
}
|
||||
}
|
||||
return generateResult();
|
||||
}
|
||||
|
||||
private void resetState() {
|
||||
myCursor = 0;
|
||||
myReplacement = new StringBuilder();
|
||||
myConversionRegions = new ArrayList<CaseConversionRegion>();
|
||||
}
|
||||
|
||||
private void processEscapedChar() {
|
||||
char nextChar;
|
||||
if (myCursor == myTemplate.length()) throw new IllegalArgumentException("character to be escaped is missing");
|
||||
nextChar = myTemplate.charAt(myCursor++);
|
||||
switch (nextChar) {
|
||||
case 'n':
|
||||
myReplacement.append('\n'); break;
|
||||
case 'r':
|
||||
myReplacement.append('\r'); break;
|
||||
case 'b':
|
||||
myReplacement.append('\b'); break;
|
||||
case 't':
|
||||
myReplacement.append('\t'); break;
|
||||
case 'f':
|
||||
myReplacement.append('\f'); break;
|
||||
case 'x':
|
||||
if (myCursor + 4 <= myTemplate.length()) {
|
||||
try {
|
||||
int code = Integer.parseInt(myTemplate.substring(myCursor, myCursor + 4), 16);
|
||||
myCursor += 4;
|
||||
myReplacement.append((char)code);
|
||||
}
|
||||
catch (NumberFormatException ignored) {}
|
||||
}
|
||||
break;
|
||||
case 'l': startConversionForCharacter(false); break;
|
||||
case 'u': startConversionForCharacter(true); break;
|
||||
case 'L': startConversionForRegion(false); break;
|
||||
case 'U': startConversionForRegion(true); break;
|
||||
case 'E': resetConversionState(); break;
|
||||
default:
|
||||
myReplacement.append(nextChar);
|
||||
}
|
||||
}
|
||||
|
||||
private void processGroupValue() {
|
||||
char nextChar;
|
||||
if (myCursor == myTemplate.length()) throw new IllegalArgumentException("Illegal group reference: group index is missing");
|
||||
nextChar = myTemplate.charAt(myCursor++);
|
||||
String group;
|
||||
if (nextChar == '{') {
|
||||
StringBuilder gsb = new StringBuilder();
|
||||
while (myCursor < myTemplate.length()) {
|
||||
nextChar = myTemplate.charAt(myCursor);
|
||||
if (isLatinLetter(nextChar) || isDigit(nextChar)) {
|
||||
gsb.append(nextChar);
|
||||
myCursor++;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (gsb.length() == 0) throw new IllegalArgumentException("named capturing group has 0 length name");
|
||||
if (nextChar != '}') throw new IllegalArgumentException("named capturing group is missing trailing '}'");
|
||||
String gname = gsb.toString();
|
||||
if (isDigit(gname.charAt(0))) {
|
||||
throw new IllegalArgumentException("capturing group name {" + gname + "} starts with digit character");
|
||||
}
|
||||
myCursor++;
|
||||
group = getNamedGroupValue(myMatcher, gname);
|
||||
} else {
|
||||
// The first number is always a group
|
||||
int refNum = (int)nextChar - '0';
|
||||
if (refNum < 0 || refNum > 9) throw new IllegalArgumentException("Illegal group reference");
|
||||
// Capture the largest legal group string
|
||||
while (true) {
|
||||
if (myCursor >= myTemplate.length()) break;
|
||||
int nextDigit = myTemplate.charAt(myCursor) - '0';
|
||||
if (nextDigit < 0 || nextDigit > 9) break;
|
||||
int newRefNum = (refNum * 10) + nextDigit;
|
||||
if (myMatcher.groupCount() < newRefNum) break;
|
||||
refNum = newRefNum;
|
||||
myCursor++;
|
||||
}
|
||||
group = myMatcher.group(refNum);
|
||||
}
|
||||
if (group != null) {
|
||||
myReplacement.append(group);
|
||||
}
|
||||
}
|
||||
|
||||
private String generateResult() {
|
||||
StringBuilder result;
|
||||
if (myConversionRegions.isEmpty()) {
|
||||
result = myReplacement;
|
||||
}
|
||||
else {
|
||||
CaseConversionRegion lastRegion = myConversionRegions.get(myConversionRegions.size() - 1);
|
||||
if (lastRegion.end < 0 || lastRegion.end > myReplacement.length()) {
|
||||
lastRegion.end = myReplacement.length();
|
||||
}
|
||||
result = new StringBuilder();
|
||||
int currentOffset = 0;
|
||||
for (CaseConversionRegion conversionRegion : myConversionRegions) {
|
||||
result.append(myReplacement, currentOffset, conversionRegion.start);
|
||||
String region = myReplacement.substring(conversionRegion.start, conversionRegion.end);
|
||||
result.append(conversionRegion.toUpperCase ? region.toUpperCase(Locale.getDefault()) : region.toLowerCase(Locale.getDefault()));
|
||||
currentOffset = conversionRegion.end;
|
||||
}
|
||||
result.append(myReplacement, currentOffset, myReplacement.length());
|
||||
}
|
||||
return result.toString();
|
||||
}
|
||||
|
||||
private void startConversionForCharacter(boolean toUpperCase) {
|
||||
int currentOffset = myReplacement.length();
|
||||
CaseConversionRegion lastRegion = myConversionRegions.isEmpty() ? null : myConversionRegions.get(myConversionRegions.size() - 1);
|
||||
if (lastRegion == null || lastRegion.end >= 0 && lastRegion.end <= currentOffset) {
|
||||
myConversionRegions.add(new CaseConversionRegion(currentOffset, currentOffset + 1, toUpperCase));
|
||||
}
|
||||
}
|
||||
|
||||
private void startConversionForRegion(boolean toUpperCase) {
|
||||
int currentOffset = myReplacement.length();
|
||||
CaseConversionRegion lastRegion = myConversionRegions.isEmpty() ? null : myConversionRegions.get(myConversionRegions.size() - 1);
|
||||
if (lastRegion == null) {
|
||||
myConversionRegions.add(new CaseConversionRegion(currentOffset, -1, toUpperCase));
|
||||
}
|
||||
else if (lastRegion.start == currentOffset) {
|
||||
lastRegion.end = -1;
|
||||
lastRegion.toUpperCase = toUpperCase;
|
||||
}
|
||||
else {
|
||||
if (lastRegion.end == -1) {
|
||||
if (lastRegion.toUpperCase == toUpperCase) {
|
||||
return;
|
||||
}
|
||||
lastRegion.end = currentOffset;
|
||||
}
|
||||
myConversionRegions.add(new CaseConversionRegion(currentOffset, -1, toUpperCase));
|
||||
}
|
||||
}
|
||||
|
||||
private void resetConversionState() {
|
||||
if (!myConversionRegions.isEmpty()) {
|
||||
int currentOffset = myReplacement.length();
|
||||
int lastIndex = myConversionRegions.size() - 1;
|
||||
CaseConversionRegion lastRegion = myConversionRegions.get(lastIndex);
|
||||
if (lastRegion.start >= currentOffset) {
|
||||
myConversionRegions.remove(lastIndex);
|
||||
}
|
||||
else if (lastRegion.end == -1) {
|
||||
lastRegion.end = currentOffset;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static String getNamedGroupValue(Matcher matcher, String groupName) {
|
||||
// Support for named capturing groups was added to Matcher's API only in Java 7,
|
||||
// so we need to use reflection to access corresponding method
|
||||
if (ourNamedGroupValueRetriever == null) throw new IllegalArgumentException("Illegal group reference");
|
||||
try {
|
||||
return (String)ourNamedGroupValueRetriever.invoke(matcher, groupName);
|
||||
}
|
||||
catch (Exception e) {
|
||||
throw new RuntimeException("Illegal group reference", e);
|
||||
}
|
||||
}
|
||||
|
||||
private static boolean isLatinLetter(int ch) {
|
||||
return ((ch-'a')|('z'-ch)) >= 0 || ((ch-'A')|('Z'-ch)) >= 0;
|
||||
}
|
||||
|
||||
private static boolean isDigit(int ch) {
|
||||
return ((ch-'0')|('9'-ch)) >= 0;
|
||||
}
|
||||
|
||||
private static class CaseConversionRegion {
|
||||
private final int start;
|
||||
private int end;
|
||||
private boolean toUpperCase;
|
||||
|
||||
private CaseConversionRegion(int start, int end, boolean toUpperCase) {
|
||||
this.start = start;
|
||||
this.end = end;
|
||||
this.toUpperCase = toUpperCase;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user