diff --git a/RegExpSupport/resources/messages/RegExpBundle.properties b/RegExpSupport/resources/messages/RegExpBundle.properties index 10acca0180cd..c161528276eb 100644 --- a/RegExpSupport/resources/messages/RegExpBundle.properties +++ b/RegExpSupport/resources/messages/RegExpBundle.properties @@ -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 diff --git a/RegExpSupport/src/org/intellij/lang/regexp/validation/CharRangeFix.java b/RegExpSupport/src/org/intellij/lang/regexp/validation/CharRangeFix.java new file mode 100644 index 000000000000..0335bc46e41e --- /dev/null +++ b/RegExpSupport/src/org/intellij/lang/regexp/validation/CharRangeFix.java @@ -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 { + + 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()); + } +} diff --git a/RegExpSupport/src/org/intellij/lang/regexp/validation/RegExpAnnotator.java b/RegExpSupport/src/org/intellij/lang/regexp/validation/RegExpAnnotator.java index 4c71e6b3979a..b55984cde60d 100644 --- a/RegExpSupport/src/org/intellij/lang/regexp/validation/RegExpAnnotator.java +++ b/RegExpSupport/src/org/intellij/lang/regexp/validation/RegExpAnnotator.java @@ -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(); } } diff --git a/RegExpSupport/src/org/intellij/lang/regexp/validation/RepetitionRangeFix.java b/RegExpSupport/src/org/intellij/lang/regexp/validation/RepetitionRangeFix.java new file mode 100644 index 000000000000..30b740924e25 --- /dev/null +++ b/RegExpSupport/src/org/intellij/lang/regexp/validation/RepetitionRangeFix.java @@ -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 { + + 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()); + } +} diff --git a/java/java-tests/testSrc/com/intellij/java/codeInsight/RegExpHighlightingTest.java b/java/java-tests/testSrc/com/intellij/java/codeInsight/RegExpHighlightingTest.java index ba3ca6ced60d..b3f51d1b976e 100644 --- a/java/java-tests/testSrc/com/intellij/java/codeInsight/RegExpHighlightingTest.java +++ b/java/java-tests/testSrc/com/intellij/java/codeInsight/RegExpHighlightingTest.java @@ -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 = "\\\\P{IsBlank}"; 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("[z-a]"); + doTest("[z-a]", + "Swap ", + "[a-z]"); } public void testIllegalCharacterRange4() { @@ -194,7 +196,9 @@ public class RegExpHighlightingTest extends LightJavaCodeInsightFixtureTestCase public void testCountedQuantifier() { doTest("a{2147483647}"); doTest("a{2147483648}"); - doTest("a{ max)\">1,0}"); + doTest("a{ max)\">1,0}", + "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());