IJPL-223228 [regex]: use intention implementation as basis for quick-fixes for char ranges and repetition range

GitOrigin-RevId: 2ca234590cef988044c6781978f7b84954f965bd
This commit is contained in:
Bas Leijdekkers
2026-01-04 14:04:50 +00:00
committed by intellij-monorepo-bot
parent 777fa5cd08
commit ff5fb1d005
5 changed files with 112 additions and 11 deletions
@@ -170,6 +170,8 @@ parse.error.unclosed.posix.bracket.expression=Unclosed POSIX bracket expression
parse.error.unclosed.property=Unclosed property
parse.error.unicode.character.name.expected=Unicode character name expected
parse.error.unmatched.closing.bracket=Unmatched closing ''{0}''
quickfix.family.name.flip.bounds=Flip bounds
regexp.dialog.language=&Language:
# Flags for java.util.regex.Pattern
@@ -0,0 +1,41 @@
// Copyright 2000-2026 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package org.intellij.lang.regexp.validation;
import com.intellij.modcommand.ActionContext;
import com.intellij.modcommand.ModPsiUpdater;
import com.intellij.modcommand.Presentation;
import com.intellij.modcommand.PsiUpdateModCommandAction;
import org.intellij.lang.regexp.RegExpBundle;
import org.intellij.lang.regexp.inspection.RegExpReplacementUtil;
import org.intellij.lang.regexp.psi.RegExpChar;
import org.intellij.lang.regexp.psi.RegExpCharRange;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
/**
* @author Bas Leijdekkers
*/
final class CharRangeFix extends PsiUpdateModCommandAction<RegExpCharRange> {
CharRangeFix(@NotNull RegExpCharRange charRange) {
super(charRange);
}
@Override
public @NotNull String getFamilyName() {
return RegExpBundle.message("quickfix.family.name.flip.bounds");
}
@Override
protected @Nullable Presentation getPresentation(@NotNull ActionContext context, @NotNull RegExpCharRange charRange) {
final RegExpChar from = charRange.getFrom();
final RegExpChar to = charRange.getTo();
if (to == null) return null;
return Presentation.of(RegExpBundle.message("intention.name.flip.elements", from.getText(), to.getText()));
}
@Override
protected void invoke(@NotNull ActionContext context, @NotNull RegExpCharRange charRange, @NotNull ModPsiUpdater updater) {
RegExpReplacementUtil.flipLeftRight(charRange.getFrom(), charRange.getTo());
}
}
@@ -100,11 +100,11 @@ public final class RegExpAnnotator extends RegExpElementVisitor implements Annot
}
final int fromCodePoint = from.getValue();
final int toCodePoint = to.getValue();
if (fromCodePoint == -1 || toCodePoint == -1) {
return;
}
if (toCodePoint < fromCodePoint) {
myHolder.newAnnotation(HighlightSeverity.ERROR, RegExpBundle.message("error.illegal.character.range.to.from")).range(range).create();
if (fromCodePoint != -1 && toCodePoint != -1 && toCodePoint < fromCodePoint) {
myHolder.newAnnotation(HighlightSeverity.ERROR, RegExpBundle.message("error.illegal.character.range.to.from"))
.range(range)
.withFix(new CharRangeFix(range))
.create();
}
}
@@ -399,7 +399,9 @@ public final class RegExpAnnotator extends RegExpElementVisitor implements Annot
if (minValue != null && maxValue != null) {
if (minValue.longValue() > maxValue.longValue() || minValue.doubleValue() > maxValue.doubleValue()) {
final TextRange range = new TextRange(minElement.getTextOffset(), maxElement.getTextOffset() + maxElement.getTextLength());
myHolder.newAnnotation(HighlightSeverity.ERROR, RegExpBundle.message("error.illegal.repetition.range.min.max")).range(range)
myHolder.newAnnotation(HighlightSeverity.ERROR, RegExpBundle.message("error.illegal.repetition.range.min.max"))
.range(range)
.withFix(new RepetitionRangeFix(quantifier))
.create();
}
}
@@ -0,0 +1,41 @@
// Copyright 2000-2026 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package org.intellij.lang.regexp.validation;
import com.intellij.modcommand.ActionContext;
import com.intellij.modcommand.ModPsiUpdater;
import com.intellij.modcommand.Presentation;
import com.intellij.modcommand.PsiUpdateModCommandAction;
import org.intellij.lang.regexp.RegExpBundle;
import org.intellij.lang.regexp.inspection.RegExpReplacementUtil;
import org.intellij.lang.regexp.psi.RegExpNumber;
import org.intellij.lang.regexp.psi.RegExpQuantifier;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
/**
* @author Bas Leijdekkers
*/
final class RepetitionRangeFix extends PsiUpdateModCommandAction<RegExpQuantifier> {
RepetitionRangeFix(@NotNull RegExpQuantifier quantifier) {
super(quantifier);
}
@Override
public @NotNull String getFamilyName() {
return RegExpBundle.message("quickfix.family.name.flip.bounds");
}
@Override
protected @Nullable Presentation getPresentation(@NotNull ActionContext context, @NotNull RegExpQuantifier quantifier) {
final RegExpNumber min = quantifier.getMin();
final RegExpNumber max = quantifier.getMax();
if (min == null || max == null) return null;
return Presentation.of(RegExpBundle.message("intention.name.flip.elements", min.getText(), max.getText()));
}
@Override
protected void invoke(@NotNull ActionContext context, @NotNull RegExpQuantifier quantifier, @NotNull ModPsiUpdater updater) {
RegExpReplacementUtil.flipLeftRight(quantifier.getMin(), quantifier.getMax());
}
}
@@ -1,4 +1,4 @@
// Copyright 2000-2022 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
// Copyright 2000-2026 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.intellij.java.codeInsight;
import com.intellij.ide.highlighter.JavaFileType;
@@ -31,7 +31,7 @@ public class RegExpHighlightingTest extends LightJavaCodeInsightFixtureTestCase
// needs only partial escaping
@NonNls String code = "<weak_warning descr=\"'\\\\P{IsBlank}' can be simplified to '[^ \\\\t]'\">\\\\P{IsBlank}</weak_warning>";
myFixture.enableInspections(new RegExpSimplifiableInspection());
myFixture.configureByText(JavaFileType.INSTANCE, "class X {{ java.util.regex.Pattern.compile(\"" + code + "\"); }}");
myFixture.configureByText(JavaFileType.INSTANCE, wrap(code));
myFixture.testHighlighting();
}
@@ -108,7 +108,9 @@ public class RegExpHighlightingTest extends LightJavaCodeInsightFixtureTestCase
}
public void testIllegalCharacterRange3() {
doTest("[<error descr=\"Illegal character range (to < from)\">z-a</error>]");
doTest("[<error descr=\"Illegal character range (to < from)\">z-a</error>]",
"Swap ",
"[a-z]");
}
public void testIllegalCharacterRange4() {
@@ -194,7 +196,9 @@ public class RegExpHighlightingTest extends LightJavaCodeInsightFixtureTestCase
public void testCountedQuantifier() {
doTest("a{2147483647}");
doTest("a{<error descr=\"Repetition value too large\">2147483648</error>}");
doTest("a{<error descr=\"Illegal repetition range (min > max)\">1,0</error>}");
doTest("a{<error descr=\"Illegal repetition range (min > max)\">1,0</error>}",
"Swap ",
"a{0,1}");
}
public void testOptions() {
@@ -261,9 +265,20 @@ public class RegExpHighlightingTest extends LightJavaCodeInsightFixtureTestCase
private void doTest(@NonNls String code) {
code = StringUtil.escapeBackSlashes(code);
myFixture.configureByText(JavaFileType.INSTANCE, "class X {{ java.util.regex.Pattern.compile(\"" + code + "\"); }}");
myFixture.configureByText(JavaFileType.INSTANCE, wrap(code));
myFixture.testHighlighting();
}
private void doTest(@NonNls String code, @NonNls String fixPrefix, @NonNls String result) {
myFixture.configureByText(JavaFileType.INSTANCE, wrap(StringUtil.escapeBackSlashes(code)));
myFixture.testHighlighting();
myFixture.launchAction(myFixture.findSingleIntention(fixPrefix));
myFixture.checkResult(wrap(result));
}
private static @NonNls @NotNull String wrap(@NonNls String code) {
return "class X {{ java.util.regex.Pattern.compile(\"" + code + "\"); }}";
}
private void doTestUnexpectedAnchor(@NonNls String code) {
myFixture.enableInspections(new UnexpectedAnchorInspection());