[regexp] Custom RegExp inspections

Co-authored-by: Louis Vignier <louis.vignier@jetbrains.com>
Co-authored-by: Bas Leijdekkers <bas@jetbrains.com>

GitOrigin-RevId: 025ad2acefe111b0e8d2111446a8190d6d28b965
This commit is contained in:
Louis Vignier
2022-10-19 12:06:03 +02:00
committed by intellij-monorepo-bot
parent fe76e924fb
commit 839640ae3c
18 changed files with 1470 additions and 21 deletions

View File

@@ -15,9 +15,9 @@
<orderEntry type="library" scope="TEST" name="JUnit4" level="project" />
<orderEntry type="module" module-name="intellij.platform.lang.impl" />
<orderEntry type="module" module-name="intellij.platform.testFramework" scope="TEST" />
<orderEntry type="library" name="Jaxen" level="project" />
<orderEntry type="module" module-name="intellij.xml.psi" />
<orderEntry type="library" name="fastutil-min" level="project" />
<orderEntry type="module" module-name="intellij.platform.core.ui" />
<orderEntry type="module" module-name="intellij.platform.util.jdom" />
</component>
</module>

View File

@@ -36,6 +36,9 @@
<categoryKey>inspection.group.name.regexp</categoryKey>
</intentionAction>
<localInspection shortName="CustomRegExpInspection" enabledByDefault="true" level="NON_SWITCHABLE_WARNING" dynamicGroup="true"
bundle="messages.RegExpBundle" groupKey="inspection.group.name.regexp" key="inspection.name.custom.regexp"
implementationClass="org.intellij.lang.regexp.inspection.custom.CustomRegExpInspection" language=""/>
<localInspection language="RegExp" shortName="RegExpRepeatedSpace" enabledByDefault="true" level="WARNING"
bundle="messages.RegExpBundle" groupKey="inspection.group.name.regexp" key="inspection.name.consecutive.spaces"
implementationClass="org.intellij.lang.regexp.inspection.RepeatedSpaceInspection"/>
@@ -84,4 +87,10 @@
bundle="messages.RegExpBundle" groupKey="inspection.group.name.regexp" key="inspection.name.redundant.digit.class.element"
implementationClass="org.intellij.lang.regexp.inspection.RegExpRedundantClassElementInspection"/>
</extensions>
<actions resource-bundle="messages.RegExpBundle">
<action id="open.regexp.dialog"
class="org.intellij.lang.regexp.inspection.custom.OpenRegExpDialogAction"
icon="AllIcons.Actions.Regex"/>
</actions>
</idea-plugin>

View File

@@ -0,0 +1,5 @@
<html>
<body>
Custom Regex Inspection
</body>
</html>

View File

@@ -1,3 +1,9 @@
action.add.pattern.text=Add pattern\u2026
action.add.regexp.replace.template.text=Add RegExp Replace Template\u2026
action.add.regexp.search.template.text=Add RegExp Search Template\u2026
action.open.regexp.dialog.text=Open RegExp Dialog\u2026
button.enable.replace=Enable Replace
button.search.only=Search Only
checker.sample.text=Sample Text
color.settings.bad.character=Bad character
color.settings.brace=Brace
@@ -18,8 +24,13 @@ color.settings.quantifier=Quantifier
color.settings.quote.character=Quote escape
color.settings.redundant.escape.sequence=Redundant escape sequence
color.settings.title.regexp=RegExp
dialog.message.name.must.not.be.empty=Name must not be empty
dialog.message.suppress.id.already.in.use.by.another.inspection=Suppress ID ''{0}'' is already in use by another inspection
dialog.message.suppress.id.must.match.regex.za.z=Suppress ID must match regex [a-zA-Z_0-9.-]+
dialog.title.custom.regexp.inspection=Custom RegExp Inspection
doc.property.block.stands.for.0=Property block stands for {0}
doc.property.block.stands.for.characters.not.matching.0=Property block stands for characters not matching {0}
edit.metadata.button=Edit Metadata...
error.0.repetition.not.allowed.inside.lookbehind={0} repetition not allowed inside lookbehind
error.alternation.alternatives.needs.to.have.the.same.length.inside.lookbehind=Alternation alternatives need to have the same length inside lookbehind
error.atomic.groups.are.not.supported.in.this.regex.dialect=Atomic groups are not supported in this regex dialect
@@ -28,10 +39,10 @@ error.conditional.group.reference.not.allowed.inside.lookbehind=Conditional grou
error.conditionals.are.not.supported.in.this.regex.dialect=Conditionals are not supported in this regex dialect
error.dangling.metacharacter=Dangling quantifier ''{0}''
error.dangling.opening.bracket=Unexpected start of quantifier '{'
error.define.subpattern.contains.more.than.one.branch=DEFINE subpattern contains more than one branch
error.embedded.comments.are.not.supported.in.this.regex.dialect=Embedded comments are not supported in this regex dialect
error.empty.group=Empty group
error.group.reference.is.nested.into.the.named.group.it.refers.to=Group reference is nested into the named group it refers to
error.define.subpattern.contains.more.than.one.branch=DEFINE subpattern contains more than one branch
error.group.reference.not.allowed.inside.lookbehind=Group reference not allowed inside lookbehind
error.group.with.name.0.already.defined=Group with name ''{0}'' already defined
error.illegal.character.range.to.from=Illegal character range (to < from)
@@ -69,27 +80,28 @@ inspection.group.name.regexp=RegExp
inspection.name.anonymous.group.or.numeric.back.reference=Anonymous capturing group or numeric back reference
inspection.name.begin.or.end.anchor.in.unexpected.position=Begin or end anchor in unexpected position
inspection.name.consecutive.spaces=Consecutive spaces
inspection.name.custom.regexp=Custom RegExp inspection
inspection.name.duplicate.branch.in.alternation=Duplicate branch in alternation
inspection.name.duplicate.character.in.class=Duplicate character in character class
inspection.name.empty.branch.in.alternation=Empty branch in alternation
inspection.name.escaped.meta.character=Escaped meta character
inspection.name.octal.escape=Octal escape
inspection.name.redundant.character.escape=Redundant character escape
inspection.name.redundant.digit.class.element=Redundant '\\d', '[:digit:]', or '\\D' class elements
inspection.name.redundant.nested.character.class=Redundant nested character class
inspection.name.simplifiable.expression=Regular expression can be simplified
inspection.name.single.character.alternation=Single character alternation
inspection.name.suspicious.backref=Suspicious back reference
inspection.name.unnecessary.non.capturing.group=Unnecessary non-capturing group
inspection.name.redundant.digit.class.element=Redundant '\\d', '[:digit:]', or '\\D' class elements
inspection.option.ignore.escaped.closing.brackets=Ignore escaped closing brackets '}' and ']'
inspection.quick.fix.remove.duplicate.0.from.character.class=Remove duplicate ''{0}'' from character class
inspection.quick.fix.remove.duplicate.branch=Remove duplicate branch
inspection.quick.fix.remove.duplicate.element.from.character.class=Remove duplicate element from character class
inspection.quick.fix.remove.empty.branch=Remove empty branch
inspection.quick.fix.remove.redundant.escape=Remove redundant escape
inspection.quick.fix.remove.unnecessary.non.capturing.group=Unwrap unnecessary non-capturing group
inspection.quick.fix.remove.redundant.0.class.element=Remove redundant ''{0}''
inspection.quick.fix.remove.redundant.class.element=Remove redundant class element
inspection.quick.fix.remove.redundant.escape=Remove redundant escape
inspection.quick.fix.remove.unnecessary.non.capturing.group=Unwrap unnecessary non-capturing group
inspection.quick.fix.replace.alternation.with.character.class=Replace alternation with character class
inspection.quick.fix.replace.redundant.character.class.with.contents=Replace redundant character class with contents
inspection.quick.fix.replace.with.character.inside.class=Replace with character inside class
@@ -109,13 +121,21 @@ inspection.warning.numeric.back.reference=Numeric back reference
inspection.warning.octal.escape.code.ref.code.in.regexp=Octal escape <code>#ref</code> in RegExp
inspection.warning.potential.exponential.backtracking=Potential exponential backtracking
inspection.warning.redundant.character.escape.0.in.regexp=Redundant character escape <code>{0}</code> in RegExp
inspection.warning.redundant.nested.character.class=Redundant nested character class
inspection.warning.redundant.class.element=Redundant ''{0}'' in RegExp
inspection.warning.redundant.nested.character.class=Redundant nested character class
inspection.warning.single.character.alternation.in.regexp=Single character alternation in RegExp
inspection.warning.unnecessary.non.capturing.group=Unnecessary non-capturing group <code>{0}</code>
intention.family.name.replace=Replace
intention.name.check.regexp=Check RegExp
label.any=Any
label.description=Description:
label.inspection.name=Inspection name:
label.problem.tool.tip=Problem tool tip (use macro #ref to insert highlighted code):
label.regexp.patterns=RegExp patterns:
label.regexp=&RegExp:
label.sample=&Sample:
label.suppress.id=Suppress ID:
no.description.provided.description=No description provided
parse.error.category.shorthand.not.allowed.in.this.regular.expression.dialect=Category shorthand not allowed in this regular expression dialect
parse.error.character.class.expected=Character class expected
parse.error.character.expected=Character expected
@@ -125,8 +145,8 @@ parse.error.closing.brace.or.number.expected='}' or number expected
parse.error.comma.expected=',' expected
parse.error.empty.property=Empty property
parse.error.group.name.expected=Group name expected
parse.error.group.number.expected=Group number expected
parse.error.group.name.or.number.expected=Group name or number expected
parse.error.group.number.expected=Group number expected
parse.error.illegal.category.shorthand=Illegal category shorthand
parse.error.illegal.character.range=Illegal character range
parse.error.negating.a.property.not.allowed.in.this.regular.expression.dialect=Negating a property not allowed in this regular expression dialect
@@ -148,6 +168,10 @@ 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}''
regexp.dialog.language=&Language:
regexp.dialog.replace.template=Replace RegExp:
regexp.dialog.search.template=Search RegExp:
regexp.dialog.title=RegExp
surrounder.atomic.group.pattern=Atomic Group (?:pattern)
surrounder.capturing.group.pattern=Capturing Group (pattern)
surrounder.negative.lookahead.pattern=Negative Lookahead (?!pattern)
@@ -163,4 +187,4 @@ tooltip.more.input.expected=More example input expected
tooltip.no.match=Expression and example do not match
tooltip.pattern.is.too.complex=Regular expression pattern is too complex
warning.duplicate.character.0.inside.character.class=Duplicate character ''{0}'' inside character class
warning.duplicate.predefined.character.class.0.inside.character.class=Duplicate predefined character class ''{0}'' inside character class
warning.duplicate.predefined.character.class.0.inside.character.class=Duplicate predefined character class ''{0}'' inside character class

View File

@@ -0,0 +1,313 @@
// Copyright 2000-2022 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package org.intellij.lang.regexp.inspection.custom;
import com.intellij.codeInsight.daemon.HighlightDisplayKey;
import com.intellij.codeInspection.InspectionProfile;
import com.intellij.codeInspection.InspectionsBundle;
import com.intellij.codeInspection.LocalInspectionTool;
import com.intellij.codeInspection.ex.InspectionProfileModifiableModel;
import com.intellij.codeInspection.ex.InspectionToolWrapper;
import com.intellij.find.FindModel;
import com.intellij.ide.DataManager;
import com.intellij.openapi.actionSystem.AnAction;
import com.intellij.openapi.actionSystem.AnActionEvent;
import com.intellij.openapi.actionSystem.CommonDataKeys;
import com.intellij.openapi.actionSystem.DefaultActionGroup;
import com.intellij.openapi.fileTypes.FileType;
import com.intellij.openapi.project.DumbAwareAction;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.ui.popup.JBPopupFactory;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.profile.codeInspection.ui.SingleInspectionProfilePanel;
import com.intellij.ui.AnActionButton;
import com.intellij.ui.DoubleClickListener;
import com.intellij.ui.LayeredIcon;
import com.intellij.ui.ToolbarDecorator;
import com.intellij.ui.awt.RelativePoint;
import com.intellij.ui.components.JBList;
import com.intellij.util.ui.FormBuilder;
import com.intellij.util.ui.JBUI;
import com.intellij.util.ui.UIUtil;
import org.intellij.lang.regexp.RegExpBundle;
import org.intellij.lang.regexp.inspection.custom.RegExpInspectionConfiguration.InspectionPattern;
import org.jetbrains.annotations.Nls;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import javax.swing.*;
import java.awt.*;
import java.awt.event.MouseEvent;
import java.util.Collections;
import java.util.List;
/**
* @author Bas Leijdekkers
*/
public class CustomRegExpFakeInspection extends LocalInspectionTool {
@NotNull private final RegExpInspectionConfiguration myConfiguration;
public CustomRegExpFakeInspection(@NotNull RegExpInspectionConfiguration configuration) {
if (configuration.getPatterns().isEmpty()) throw new IllegalArgumentException();
myConfiguration = configuration;
}
public @NotNull RegExpInspectionConfiguration getConfiguration() {
return myConfiguration;
}
@Nls(capitalization = Nls.Capitalization.Sentence)
@NotNull
@Override
public String getDisplayName() {
return myConfiguration.getName();
}
@NotNull
@Override
public String getShortName() {
return myConfiguration.getUuid();
}
@Override
public boolean isEnabledByDefault() {
return true;
}
@NotNull
@Override
public String getID() {
final HighlightDisplayKey key = HighlightDisplayKey.find(getShortName());
if (key != null) {
return key.getID(); // to avoid using a new suppress id before it is registered.
}
final String suppressId = myConfiguration.getSuppressId();
return !StringUtil.isEmpty(suppressId) ? suppressId : CustomRegExpInspection.SHORT_NAME;
}
@Override
public @Nullable String getAlternativeID() {
return CustomRegExpInspection.SHORT_NAME;
}
@Nullable
@Override
public String getMainToolId() {
return CustomRegExpInspection.SHORT_NAME;
}
@Nls(capitalization = Nls.Capitalization.Sentence)
@Override
public @NotNull String getGroupDisplayName() {
return "RegExp";
}
@Override
public @Nls(capitalization = Nls.Capitalization.Sentence) String @NotNull [] getGroupPath() {
return new String[] {InspectionsBundle.message("group.names.user.defined"), getGroupDisplayName()};
}
@Nullable
@Override
public String getStaticDescription() {
final String description = myConfiguration.getDescription();
if (StringUtil.isEmpty(description)) {
return RegExpBundle.message("no.description.provided.description");
}
return description;
}
@Override
public @NotNull JComponent createOptionsPanel() {
final MyListModel model = new MyListModel();
final JButton button = new JButton(RegExpBundle.message("edit.metadata.button"));
button.addActionListener(__ -> performEditMetaData(button));
final JList<InspectionPattern> list = new JBList<>(model);
list.setVisibleRowCount(3);
list.setCellRenderer(new RegExpInspectionConfigurationCellRenderer());
final JPanel listPanel = ToolbarDecorator.createDecorator(list)
.setAddAction(b -> performAdd(list, b))
.setAddActionName(RegExpBundle.message("action.add.pattern.text"))
.setAddIcon(LayeredIcon.ADD_WITH_DROPDOWN)
.setRemoveAction(b -> performRemove(list))
.setRemoveActionUpdater(e -> list.getSelectedValuesList().size() < list.getModel().getSize())
.setEditAction(b -> performEdit(list))
.setMoveUpAction(b -> performMove(list, true))
.setMoveDownActionUpdater(e -> list.getSelectedValuesList().size() == 1)
.setMoveDownAction(b -> performMove(list, false))
.setMoveDownActionUpdater(e -> list.getSelectedValuesList().size() == 1)
.createPanel();
new DoubleClickListener() {
@Override
protected boolean onDoubleClick(@NotNull MouseEvent e) {
performEdit(list);
return true;
}
}.installOn(list);
final JPanel panel = new FormBuilder()
.addComponent(button)
.addVerticalGap(UIUtil.DEFAULT_VGAP)
.addLabeledComponentFillVertically(RegExpBundle.message("label.regexp.patterns"), listPanel)
.getPanel();
panel.setBorder(JBUI.Borders.emptyTop(10));
return panel;
}
private void performEditMetaData(@NotNull Component context) {
final Project project = CommonDataKeys.PROJECT.getData(DataManager.getInstance().getDataContext(context));
final InspectionProfileModifiableModel profile = getInspectionProfile(context);
if (profile == null) {
return;
}
final CustomRegExpInspection inspection = getRegExpInspection(profile);
final MetaDataDialog dialog = new MetaDataDialog(project, inspection, myConfiguration, false);
if (!dialog.showAndGet()) {
return;
}
inspection.updateConfiguration(myConfiguration);
profile.setModified(true);
profile.getProfileManager().fireProfileChanged(profile);
}
private void performMove(JList<InspectionPattern> list, boolean up) {
final MyListModel model = (MyListModel)list.getModel();
final int index = list.getSelectedIndex();
final List<InspectionPattern> patterns = myConfiguration.getPatterns();
final int newIndex = up ? index - 1 : index + 1;
Collections.swap(patterns, index, newIndex);
model.fireContentsChanged(list);
list.setSelectedIndex(newIndex);
//saveChangesToProfile(list);
}
private void performAdd(JList<InspectionPattern> list, @NotNull AnActionButton b) {
final AnAction[] children = new AnAction[]{new AddTemplateAction(list, false), new AddTemplateAction(list, true)};
final RelativePoint point = b.getPreferredPopupPoint();
JBPopupFactory.getInstance().createActionGroupPopup(null, new DefaultActionGroup(children),
DataManager.getInstance().getDataContext(b.getContextComponent()),
JBPopupFactory.ActionSelectionAid.SPEEDSEARCH, true).show(point);
}
private void performRemove(JList<InspectionPattern> list) {
final List<InspectionPattern> patterns = myConfiguration.getPatterns();
for (InspectionPattern configuration : list.getSelectedValuesList()) {
patterns.remove(configuration);
}
final int size = patterns.size();
final int maxIndex = list.getMaxSelectionIndex();
if (maxIndex != list.getMinSelectionIndex()) {
list.setSelectedIndex(maxIndex);
}
((MyListModel)list.getModel()).fireContentsChanged(list);
if (list.getSelectedIndex() >= size) {
list.setSelectedIndex(size - 1);
}
final int index = list.getSelectedIndex();
list.scrollRectToVisible(list.getCellBounds(index, index));
saveChangesToProfile(list);
}
private void performEdit(JList<InspectionPattern> list) {
final Project project = CommonDataKeys.PROJECT.getData(DataManager.getInstance().getDataContext(list));
if (project == null) return;
final int index = list.getSelectedIndex();
if (index < 0) return;
final List<InspectionPattern> patterns = myConfiguration.getPatterns();
final InspectionPattern configuration = patterns.get(index);
if (configuration == null) return;
final RegExpDialog dialog = new RegExpDialog(project, true, configuration);
if (!dialog.showAndGet()) return;
final InspectionPattern newConfiguration = dialog.getPattern();
patterns.set(index, newConfiguration);
((MyListModel)list.getModel()).fireContentsChanged(list);
saveChangesToProfile(list);
}
private void saveChangesToProfile(JList<InspectionPattern> list) {
final InspectionProfileModifiableModel profile = getInspectionProfile(list);
if (profile == null) return;
final CustomRegExpInspection inspection = getRegExpInspection(profile);
inspection.updateConfiguration(myConfiguration);
profile.setModified(true);
}
@NotNull
private static CustomRegExpInspection getRegExpInspection(@NotNull InspectionProfile profile) {
final InspectionToolWrapper<?, ?> wrapper = profile.getInspectionTool(CustomRegExpInspection.SHORT_NAME, (Project)null);
assert wrapper != null;
return (CustomRegExpInspection)wrapper.getTool();
}
private static InspectionProfileModifiableModel getInspectionProfile(@NotNull Component c) {
final SingleInspectionProfilePanel panel = UIUtil.uiParents(c, true).filter(SingleInspectionProfilePanel.class).first();
if (panel == null) return null;
return panel.getProfile();
}
private final class AddTemplateAction extends DumbAwareAction {
private final JList<InspectionPattern> myList;
private final boolean myReplace;
private AddTemplateAction(JList<InspectionPattern> list, boolean replace) {
super(replace
? RegExpBundle.message("action.add.regexp.replace.template.text")
: RegExpBundle.message("action.add.regexp.search.template.text"));
myList = list;
myReplace = replace;
}
@Override
public void actionPerformed(@NotNull AnActionEvent e) {
final Project project = e.getData(CommonDataKeys.PROJECT);
assert project != null;
FileType fileType = null;
FindModel.SearchContext context = FindModel.SearchContext.ANY;
if (myList.getModel().getSize() > 0) {
final InspectionPattern pattern = myList.getModel().getElementAt(0);
fileType = pattern.fileType();
context = pattern.searchContext();
}
final String replace = myReplace ? "" : null;
final InspectionPattern defaultPattern = new InspectionPattern("", fileType, context, replace);
final RegExpDialog dialog = new RegExpDialog(project, true, defaultPattern);
if (!dialog.showAndGet()) return;
final InspectionPattern configuration = dialog.getPattern();
final List<InspectionPattern> patterns = myConfiguration.getPatterns();
patterns.add(configuration);
((MyListModel)myList.getModel()).fireContentsChanged(myList);
saveChangesToProfile(myList);
}
}
private class MyListModel extends AbstractListModel<InspectionPattern> {
@Override
public int getSize() {
return myConfiguration.getPatterns().size();
}
@Override
public InspectionPattern getElementAt(int index) {
return myConfiguration.getPatterns().get(index);
}
public void fireContentsChanged(Object source) {
fireContentsChanged(source, -1, -1);
}
public void swap(int first, int second) {
if (second == -1) return;
final List<InspectionPattern> patterns = myConfiguration.getPatterns();
final InspectionPattern one = patterns.get(first);
final InspectionPattern two = patterns.get(second);
patterns.set(second, one);
patterns.set(first, two);
}
}
}

View File

@@ -0,0 +1,236 @@
// Copyright 2000-2022 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package org.intellij.lang.regexp.inspection.custom;
import com.intellij.codeInsight.daemon.HighlightDisplayKey;
import com.intellij.codeInsight.daemon.impl.ProblemDescriptorWithReporterName;
import com.intellij.codeInspection.*;
import com.intellij.codeInspection.ex.*;
import com.intellij.find.FindManager;
import com.intellij.find.FindModel;
import com.intellij.find.FindResult;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.application.ModalityState;
import com.intellij.openapi.editor.Document;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.util.TextRange;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.profile.codeInspection.InspectionProfileManager;
import com.intellij.psi.PsiElement;
import com.intellij.psi.PsiFile;
import com.intellij.util.SmartList;
import com.intellij.util.containers.ContainerUtil;
import org.intellij.lang.regexp.RegExpBundle;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.List;
/**
* @author Bas Leijdekkers
*/
public class CustomRegExpInspection extends LocalInspectionTool implements DynamicGroupTool {
public static final String SHORT_NAME = "CustomRegExpInspection";
public final List<RegExpInspectionConfiguration> myConfigurations = new SmartList<>();
private InspectionProfileImpl mySessionProfile;
public CustomRegExpInspection() {
/*
final FileType javaFileType = FileTypeManager.getInstance().getStdFileType("JAVA");
final RegExpInspectionConfiguration one = new RegExpInspectionConfiguration("No spaces within parentheses");
one.patterns.add(new RegExpInspectionConfiguration.InspectionPattern("(\\()\\s+|\\s+(\\))", null, FindModel.SearchContext.EXCEPT_COMMENTS_AND_STRING_LITERALS, "$1"));
one.suppressId = "NoSpaces";
one.description = "We don't like spaces within parentheses in our code style";
myConfigurations.add(one);
final RegExpInspectionConfiguration two = new RegExpInspectionConfiguration("No more than one empty line in Java");
two.patterns.add(new RegExpInspectionConfiguration.InspectionPattern("\\n\\n\\n+", javaFileType, FindModel.SearchContext.EXCEPT_STRING_LITERALS, "\n\n"));
two.suppressId = "EmptyLines";
two.description = "One empty line should be enough for everybody";
myConfigurations.add(two);
final RegExpInspectionConfiguration three = new RegExpInspectionConfiguration("Trailing whitespace");
three.patterns.add(new RegExpInspectionConfiguration.InspectionPattern(" +\\n", PlainTextFileType.INSTANCE, FindModel.SearchContext.ANY, ""));
three.patterns.add(new RegExpInspectionConfiguration.InspectionPattern("\t+\\n", PlainTextFileType.INSTANCE, FindModel.SearchContext.ANY, ""));
three.description = "Trailing whitespace is unnecessary";
myConfigurations.add(three);
final RegExpInspectionConfiguration four = new RegExpInspectionConfiguration("Multiple spaces in Java");
four.patterns.add(new RegExpInspectionConfiguration.InspectionPattern("(?<=\\S) {2,}", javaFileType, FindModel.SearchContext.EXCEPT_COMMENTS, " "));
four.description = "Double spaced";
myConfigurations.add(four);
*/
}
@Override
public void initialize(@NotNull GlobalInspectionContext context) {
super.initialize(context);
mySessionProfile = ((GlobalInspectionContextBase)context).getCurrentProfile();
}
@Override
public void cleanup(@NotNull Project project) {
super.cleanup(project);
mySessionProfile = null;
}
@Override
public boolean runForWholeFile() {
return true;
}
@Override
public ProblemDescriptor @Nullable [] checkFile(@NotNull PsiFile file, @NotNull InspectionManager manager, boolean isOnTheFly) {
final Document document = file.getViewProvider().getDocument();
final CharSequence text = document.getCharsSequence();
final FindManager findManager = FindManager.getInstance(file.getProject());
final Project project = manager.getProject();
final InspectionProfileImpl profile =
(mySessionProfile != null && !isOnTheFly) ? mySessionProfile : InspectionProfileManager.getInstance(project).getCurrentProfile();
final List<ProblemDescriptor> descriptors = new SmartList<>();
for (RegExpInspectionConfiguration configuration : myConfigurations) {
final String uuid = configuration.getUuid();
final ToolsImpl tools = profile.getToolsOrNull(uuid, project);
if (tools != null && !tools.isEnabled(file)) {
continue;
}
addInspectionToProfile(project, profile, configuration); // hack
register(configuration);
for (RegExpInspectionConfiguration.InspectionPattern pattern : configuration.getPatterns()) {
if (file.getFileType() != pattern.fileType()) continue;
final FindModel model = new FindModel();
model.setRegularExpressions(true);
model.setStringToFind(pattern.regExp());
final String replacement = pattern.replacement();
if (replacement != null) {
model.setStringToReplace(replacement);
}
model.setSearchContext(pattern.searchContext());
FindResult result = findManager.findString(text, 0, model);
while (result.isStringFound()) {
final TextRange range = new TextRange(result.getStartOffset(), result.getEndOffset());
PsiElement element = file.findElementAt(result.getStartOffset());
assert element != null;
while (!element.getTextRange().contains(range)) {
element = element.getParent();
}
final TextRange elementRange = element.getTextRange();
final int start = result.getStartOffset() - elementRange.getStartOffset();
final TextRange warningRange = new TextRange(start, result.getEndOffset() - result.getStartOffset() + start);
final String problemDescriptor = StringUtil.defaultIfEmpty(configuration.getProblemDescriptor(), configuration.getName());
final ProblemDescriptor descriptor =
manager.createProblemDescriptor(element, warningRange, problemDescriptor,
ProblemHighlightType.GENERIC_ERROR_OR_WARNING, isOnTheFly,
new CustomRegExpQuickFix(findManager, model, text, result));
descriptors.add(new ProblemDescriptorWithReporterName((ProblemDescriptorBase)descriptor, uuid));
result = findManager.findString(text, result.getEndOffset(), model);
}
}
}
return descriptors.toArray(ProblemDescriptor.EMPTY_ARRAY);
}
public static void register(@NotNull RegExpInspectionConfiguration configuration) {
// modify from single (event) thread, to prevent race conditions.
ApplicationManager.getApplication().invokeLater(() -> {
final String shortName = configuration.getUuid();
final HighlightDisplayKey key = HighlightDisplayKey.find(shortName);
if (key != null) {
if (!isMetaDataChanged(configuration, key)) return;
HighlightDisplayKey.unregister(shortName);
}
final String suppressId = configuration.getSuppressId();
final String name = configuration.getName();
if (suppressId == null) {
HighlightDisplayKey.register(shortName, () -> name, SHORT_NAME);
}
else {
HighlightDisplayKey.register(shortName, () -> name, suppressId, SHORT_NAME);
}
}, ModalityState.NON_MODAL);
}
private static boolean isMetaDataChanged(@NotNull RegExpInspectionConfiguration configuration, @NotNull HighlightDisplayKey key) {
if (StringUtil.isEmpty(configuration.getSuppressId())) {
if (!SHORT_NAME.equals(key.getID())) return true;
}
else if (!configuration.getSuppressId().equals(key.getID())) return true;
return !configuration.getName().equals(HighlightDisplayKey.getDisplayNameByKey(key));
}
public static void addInspectionToProfile(@NotNull Project project,
@NotNull InspectionProfileImpl profile,
@NotNull RegExpInspectionConfiguration configuration) {
final String shortName = configuration.getUuid();
final InspectionToolWrapper<?, ?> toolWrapper = profile.getInspectionTool(shortName, project);
if (toolWrapper != null) {
// already added
return;
}
final CustomRegExpInspectionToolWrapper wrapped = new CustomRegExpInspectionToolWrapper(configuration);
profile.addTool(project, wrapped, null);
profile.setToolEnabled(shortName, true);
}
@Override
public @NotNull List<LocalInspectionToolWrapper> getChildren() {
return ContainerUtil.map(myConfigurations, CustomRegExpInspectionToolWrapper::new);
}
public void addConfiguration(RegExpInspectionConfiguration configuration) {
myConfigurations.add(configuration);
}
public void updateConfiguration(RegExpInspectionConfiguration configuration) {
myConfigurations.remove(configuration);
myConfigurations.add(configuration);
}
public void removeConfigurationWithUuid(String uuid) {
myConfigurations.removeIf(c -> c.getUuid().equals(uuid));
}
public List<RegExpInspectionConfiguration> getConfigurations() {
return myConfigurations;
}
private static class CustomRegExpQuickFix implements LocalQuickFix {
private final int myStartOffset;
private final int myEndOffset;
private final String myReplacement;
private final String myOriginal;
private CustomRegExpQuickFix(FindManager findManager, FindModel findModel, CharSequence text, FindResult result) {
myStartOffset = result.getStartOffset();
myEndOffset = result.getEndOffset();
myOriginal = text.subSequence(myStartOffset, myEndOffset).toString();
try {
myReplacement = findManager.getStringToReplace(myOriginal, findModel, myStartOffset, text);
}
catch (FindManager.MalformedReplacementStringException e) {
throw new RuntimeException(e);
}
}
@Override
public @NotNull String getName() {
return CommonQuickFixBundle.message("fix.replace.with.x", myReplacement);
}
@Override
public @NotNull String getFamilyName() {
return RegExpBundle.message("intention.family.name.replace");
}
@Override
public void applyFix(@NotNull Project project, @NotNull ProblemDescriptor descriptor) {
PsiFile file = descriptor.getPsiElement().getContainingFile();
final Document document = file.getViewProvider().getDocument();
if (myOriginal.equals(document.getText(TextRange.create(myStartOffset, myEndOffset)))) {
document.replaceString(myStartOffset, myEndOffset, myReplacement);
}
}
}
}

View File

@@ -0,0 +1,33 @@
// Copyright 2000-2022 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package org.intellij.lang.regexp.inspection.custom;
import com.intellij.codeInspection.ex.LocalInspectionToolWrapper;
import org.jetbrains.annotations.NotNull;
/**
* @author Bas Leijdekkers
*/
public class CustomRegExpInspectionToolWrapper extends LocalInspectionToolWrapper {
CustomRegExpInspectionToolWrapper(RegExpInspectionConfiguration configuration) {
super(new CustomRegExpFakeInspection(configuration));
}
public int hashCode() {
return myTool.getShortName().hashCode();
}
@Override
public boolean equals(Object obj) {
return obj.getClass() == CustomRegExpInspectionToolWrapper.class &&
((CustomRegExpInspectionToolWrapper)obj).myTool.getShortName().equals(myTool.getShortName());
}
@NotNull
@Override
public LocalInspectionToolWrapper createCopy() {
final CustomRegExpFakeInspection inspection = (CustomRegExpFakeInspection)getTool();
RegExpInspectionConfiguration configuration = inspection.getConfiguration();
return new CustomRegExpInspectionToolWrapper(configuration);
}
}

View File

@@ -0,0 +1,136 @@
// Copyright 2000-2022 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package org.intellij.lang.regexp.inspection.custom;
import com.intellij.codeInsight.daemon.HighlightDisplayKey;
import com.intellij.codeInspection.LocalInspectionTool;
import com.intellij.openapi.editor.colors.EditorFontType;
import com.intellij.openapi.fileTypes.FileType;
import com.intellij.openapi.fileTypes.FileTypeManager;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.ui.DialogWrapper;
import com.intellij.openapi.ui.ValidationInfo;
import com.intellij.openapi.util.NlsSafe;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.ui.EditorTextField;
import com.intellij.util.ObjectUtils;
import com.intellij.util.SmartList;
import com.intellij.util.ui.FormBuilder;
import org.intellij.lang.regexp.RegExpBundle;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import javax.swing.*;
import java.awt.*;
import java.util.List;
import java.util.regex.Pattern;
public class MetaDataDialog extends DialogWrapper {
private final Pattern mySuppressIdPattern = Pattern.compile(LocalInspectionTool.VALID_ID_PATTERN);
private final CustomRegExpInspection myInspection;
@NotNull private final RegExpInspectionConfiguration myConfiguration;
private final boolean myNewInspection;
private final JTextField myNameTextField;
private final JTextField myProblemDescriptorTextField;
private final EditorTextField myDescriptionTextArea;
private final JTextField mySuppressIdTextField;
public MetaDataDialog(Project project, @NotNull CustomRegExpInspection inspection, @NotNull RegExpInspectionConfiguration configuration, boolean newInspection) {
super((Project)null);
myInspection = inspection;
myConfiguration = configuration;
myNewInspection = newInspection;
myNameTextField = new JTextField(configuration.getName());
myProblemDescriptorTextField = new JTextField(configuration.getProblemDescriptor());
final FileType htmlFileType = FileTypeManager.getInstance().getStdFileType("HTML");
myDescriptionTextArea = new EditorTextField(ObjectUtils.notNull(configuration.getDescription(), ""), project, htmlFileType);
myDescriptionTextArea.setOneLineMode(false);
myDescriptionTextArea.setFont(EditorFontType.getGlobalPlainFont());
myDescriptionTextArea.setPreferredSize(new Dimension(375, 125));
myDescriptionTextArea.setMinimumSize(new Dimension(200, 50));
mySuppressIdTextField = new JTextField(configuration.getSuppressId());
setTitle(RegExpBundle.message("dialog.title.custom.regexp.inspection"));
init();
}
@Override
public JComponent getPreferredFocusedComponent() {
return myNameTextField;
}
@Override
protected @NotNull java.util.List<ValidationInfo> doValidateAll() {
final List<ValidationInfo> warnings = new SmartList<>();
final List<RegExpInspectionConfiguration> configurations = myInspection.getConfigurations();
final String name = getName();
if (StringUtil.isEmpty(name)) {
warnings.add(new ValidationInfo(RegExpBundle.message("dialog.message.name.must.not.be.empty"), myNameTextField));
}
final String suppressId = getSuppressId();
if (!StringUtil.isEmpty(suppressId)) {
if (!mySuppressIdPattern.matcher(suppressId).matches()) {
warnings.add(new ValidationInfo(RegExpBundle.message("dialog.message.suppress.id.must.match.regex.za.z"), mySuppressIdTextField));
}
else {
final HighlightDisplayKey key = HighlightDisplayKey.findById(suppressId);
if (key != null && key != HighlightDisplayKey.find(myConfiguration.getUuid())) {
warnings.add(new ValidationInfo(
RegExpBundle.message("dialog.message.suppress.id.already.in.use.by.another.inspection", suppressId), mySuppressIdTextField));
}
else {
for (RegExpInspectionConfiguration configuration : configurations) {
if (suppressId.equals(configuration.getSuppressId()) && !myConfiguration.getUuid().equals(configuration.getUuid())) {
warnings.add(new ValidationInfo(
RegExpBundle.message("dialog.message.suppress.id.already.in.use.by.another.inspection", suppressId), mySuppressIdTextField));
break;
}
}
}
}
}
return warnings;
}
@Override
protected void doOKAction() {
super.doOKAction();
if (getOKAction().isEnabled()) {
myConfiguration.name = getName();
myConfiguration.description = getDescription();
myConfiguration.suppressId = getSuppressId();
myConfiguration.problemDescriptor = getProblemDescriptor();
}
}
@Nullable
@Override
protected JComponent createCenterPanel() {
return new FormBuilder()
.addLabeledComponent(RegExpBundle.message("label.inspection.name"), myNameTextField, true)
.addLabeledComponent(RegExpBundle.message("label.problem.tool.tip"), myProblemDescriptorTextField, true)
.addLabeledComponentFillVertically(RegExpBundle.message("label.description"), myDescriptionTextArea)
.addLabeledComponent(RegExpBundle.message("label.suppress.id"), mySuppressIdTextField)
.getPanel();
}
public @NlsSafe String getName() {
return myNameTextField.getText().trim();
}
public @NlsSafe @Nullable String getDescription() {
return convertEmptyToNull(myDescriptionTextArea.getText());
}
public @NlsSafe @Nullable String getSuppressId() {
return convertEmptyToNull(mySuppressIdTextField.getText());
}
public @NlsSafe @Nullable String getProblemDescriptor() {
return convertEmptyToNull(myProblemDescriptorTextField.getText());
}
private static String convertEmptyToNull(String s) {
return StringUtil.isEmpty(s.trim()) ? null : s;
}
}

View File

@@ -0,0 +1,13 @@
// Copyright 2000-2022 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package org.intellij.lang.regexp.inspection.custom;
import com.intellij.openapi.actionSystem.AnActionEvent;
import com.intellij.openapi.project.DumbAwareAction;
import org.jetbrains.annotations.NotNull;
public class OpenRegExpDialogAction extends DumbAwareAction {
@Override
public void actionPerformed(@NotNull AnActionEvent e) {
new RegExpDialog(e.getProject(), false, null).show();
}
}

View File

@@ -0,0 +1,274 @@
// Copyright 2000-2022 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package org.intellij.lang.regexp.inspection.custom;
import com.intellij.find.FindBundle
import com.intellij.find.FindModel
import com.intellij.find.impl.FindInProjectUtil
import com.intellij.icons.AllIcons
import com.intellij.openapi.actionSystem.*
import com.intellij.openapi.actionSystem.impl.ActionButton
import com.intellij.openapi.editor.Document
import com.intellij.openapi.editor.EditorFactory
import com.intellij.openapi.editor.colors.EditorFontType
import com.intellij.openapi.editor.ex.EditorEx
import com.intellij.openapi.fileTypes.FileType
import com.intellij.openapi.fileTypes.FileTypeManager
import com.intellij.openapi.fileTypes.UnknownFileType
import com.intellij.openapi.project.DumbAware
import com.intellij.openapi.project.DumbAwareAction
import com.intellij.openapi.project.Project
import com.intellij.openapi.ui.ComboBox
import com.intellij.openapi.ui.DialogWrapper
import com.intellij.openapi.ui.popup.JBPopupFactory
import com.intellij.openapi.ui.popup.ListPopup
import com.intellij.ui.EditorTextField
import com.intellij.ui.JBColor
import com.intellij.ui.OnePixelSplitter
import com.intellij.ui.SimpleListCellRenderer
import com.intellij.ui.dsl.builder.IntelliJSpacingConfiguration
import com.intellij.ui.dsl.builder.RightGap
import com.intellij.ui.dsl.builder.Row
import com.intellij.ui.dsl.builder.panel
import com.intellij.ui.dsl.gridLayout.Gaps
import com.intellij.ui.dsl.gridLayout.HorizontalAlign
import com.intellij.ui.dsl.gridLayout.VerticalAlign
import com.intellij.util.ui.JBUI
import org.intellij.lang.regexp.RegExpBundle
import org.intellij.lang.regexp.RegExpFileType
import org.intellij.lang.regexp.inspection.custom.RegExpInspectionConfiguration.InspectionPattern
import java.awt.Dimension
import java.awt.event.FocusAdapter
import java.awt.event.FocusEvent
import javax.swing.JButton
import javax.swing.JComponent
import javax.swing.JLabel
import javax.swing.JList
import javax.swing.border.CompoundBorder
class RegExpDialog(val project: Project?, val editConfiguration: Boolean, defaultPattern: InspectionPattern? = null) : DialogWrapper(project, true) {
private var searchContext: FindModel.SearchContext = FindModel.SearchContext.ANY
private var replace: Boolean = false
set(value) {
field = value
replaceLabel.isEnabled = value
replaceRow.enabled(value)
replaceButton.text = when (value) {
true -> RegExpBundle.message("button.search.only")
false -> RegExpBundle.message("button.enable.replace")
}
if (!editConfiguration) {
setOKButtonText(when (value) {
true -> FindBundle.message("find.replace.command")
false -> FindBundle.message("find.dialog.find.button")
})
}
}
private var replaceEditorFocusedLast = false
private lateinit var fileCombo: ComboBox<FileType>
private lateinit var filterButton: ActionButton
private lateinit var searchEditor: EditorTextField
private lateinit var replaceLabel: JLabel
private lateinit var replaceButton: JButton
private lateinit var replaceRow: Row
private lateinit var replaceEditor: EditorTextField
private lateinit var splitter: OnePixelSplitter
val pattern: InspectionPattern
get() = InspectionPattern(
searchEditor.text,
fileCombo.item,
searchContext,
if (replace) replaceEditor.text else null
)
init {
title = RegExpBundle.message("regexp.dialog.title")
init()
if (!editConfiguration) {
isModal = false
}
defaultPattern?.let { pattern ->
searchEditor.text = pattern.regExp
searchContext = pattern.searchContext
fileCombo.item = pattern.fileType() ?: UnknownFileType.INSTANCE
pattern.replacement?.let { replaceEditor.text = it }
}
replace = defaultPattern?.replacement != null
}
private fun createEditorsPanel(): JComponent = panel {
val intelliJSpacingConfiguration = IntelliJSpacingConfiguration()
panel {
row {
label(RegExpBundle.message("regexp.dialog.search.template"))
.resizableColumn()
.horizontalAlign(HorizontalAlign.FILL)
val fileTypes = mutableListOf(UnknownFileType.INSTANCE)
fileTypes.addAll(
FileTypeManager.getInstance().registeredFileTypes.filterNotNull()
.sortedBy { it.displayName }
.filter { it != UnknownFileType.INSTANCE }
)
fileCombo = comboBox(fileTypes, SimpleTypeRenderer())
.label(RegExpBundle.message("regexp.dialog.language"))
.applyToComponent {
preferredSize.width = 150
isSwingPopup = false
}
.gap(RightGap.SMALL)
.component
filterButton = actionButton(MyFilterAction())
.component
}
}.customize(Gaps(0, intelliJSpacingConfiguration.horizontalIndent, 0, intelliJSpacingConfiguration.horizontalIndent))
row {
searchEditor = cell(createEditor(true))
.resizableColumn()
.horizontalAlign(HorizontalAlign.FILL)
.verticalAlign(VerticalAlign.FILL)
.applyToComponent { addFocusListener(object : FocusAdapter() {
override fun focusGained(e: FocusEvent?) {
replaceEditorFocusedLast = false
}
}) }
.component
}.resizableRow()
panel {
row {
replaceLabel = label(RegExpBundle.message("regexp.dialog.replace.template"))
.resizableColumn()
.horizontalAlign(HorizontalAlign.FILL)
.component
replaceButton = button(if (replace) RegExpBundle.message("button.search.only") else RegExpBundle.message("button.enable.replace")) {
replace = !replace
}.component
}
}.customize(Gaps(10, intelliJSpacingConfiguration.horizontalIndent, 0, intelliJSpacingConfiguration.horizontalIndent))
replaceRow = row {
replaceEditor = cell(createEditor(false))
.resizableColumn()
.horizontalAlign(HorizontalAlign.FILL)
.verticalAlign(VerticalAlign.FILL).applyToComponent { addFocusListener(object : FocusAdapter() {
override fun focusGained(e: FocusEvent?) {
replaceEditorFocusedLast = true
}
}) }
.component
}.resizableRow()
}
override fun createCenterPanel(): JComponent {
splitter = OnePixelSplitter()
splitter.firstComponent = RegExpSampleTree { insertSample(it) }.panel
splitter.secondComponent = createEditorsPanel()
searchEditor.grabFocus()
return splitter
}
private fun insertSample(sample: RegExpSample) {
val editor = if (replaceEditorFocusedLast) replaceEditor else searchEditor
val caret = editor.caretModel.allCarets.firstOrNull() ?: return
val insertOffset = caret.offset
val sampleOffset = if (sample.caretOffset == -1) sample.sample.length else sample.caretOffset
when (caret.hasSelection()) {
true -> {
val selection = caret.selectedText!!
val start = editor.text.dropLast(editor.text.length - caret.selectionStart)
val end = editor.text.drop(caret.selectionEnd)
val sampleStart = sample.sample.dropLast(sample.sample.length - sampleOffset)
val sampleEnd = sample.sample.drop(sampleOffset)
editor.text = start + sampleStart + selection + sampleEnd + end
caret.moveToOffset(caret.selectionStart + sampleStart.length + selection.length)
}
false -> {
val start = editor.text.dropLast(editor.text.length - insertOffset)
val end = editor.text.drop(insertOffset)
editor.text = start + sample.sample + end
caret.moveToOffset(caret.offset + sampleOffset)
}
}
editor.grabFocus()
}
override fun getStyle(): DialogStyle = DialogStyle.COMPACT
fun createEditor(search: Boolean): EditorTextField {
val document = EditorFactory.getInstance().createDocument("")
return MyEditorTextField(document, search).apply {
font = EditorFontType.getGlobalPlainFont()
preferredSize = Dimension(550, 100)
}
}
private inner class MyEditorTextField(document: Document, val search: Boolean) : EditorTextField(document, project, RegExpFileType.INSTANCE, false, false) {
override fun createEditor(): EditorEx {
return super.createEditor().apply {
setHorizontalScrollbarVisible(true)
setVerticalScrollbarVisible(true)
val outerBorder = JBUI.Borders.customLine(JBColor.border(), 1, 0, if (search) 1 else 0, 0)
scrollPane.border = CompoundBorder(
outerBorder,
JBUI.Borders.empty(6, 8, 6, 8)
)
isEmbeddedIntoDialogWrapper = true
}
}
}
inner class SimpleTypeRenderer : SimpleListCellRenderer<FileType?>() {
override fun customize(list: JList<out FileType?>, value: FileType?, index: Int, selected: Boolean, hasFocus: Boolean) {
if (value == null) return
when (value) {
UnknownFileType.INSTANCE -> {
text = RegExpBundle.message("label.any")
icon = AllIcons.FileTypes.Any_type
}
else -> {
text = value.displayName
icon = value.icon ?: AllIcons.FileTypes.Text
}
}
}
}
private inner class MyFilterAction : DumbAwareAction(FindBundle.messagePointer("find.popup.show.filter.popup"), Presentation.NULL_STRING, AllIcons.General.Filter) {
val myGroup: ActionGroup
var listPopup: ListPopup? = null
init {
ActionManager.getInstance().getKeyboardShortcut("ShowFilterPopup")?.let {
shortcutSet = CustomShortcutSet(it)
}
myGroup = DefaultActionGroup().apply {
FindModel.SearchContext.values().forEach { add(MyToggleAction(it, this@MyFilterAction)) }
isPopup = true
}
}
override fun actionPerformed(e: AnActionEvent) {
listPopup = JBPopupFactory.getInstance().createActionGroupPopup(null, myGroup, e.dataContext, false, null, 10)
listPopup?.showUnderneathOf(filterButton)
}
}
private inner class MyToggleAction(val context: FindModel.SearchContext, val action: MyFilterAction) : ToggleAction(FindInProjectUtil.getPresentableName(context)), DumbAware {
override fun isSelected(e: AnActionEvent): Boolean {
return searchContext == context
}
override fun getActionUpdateThread(): ActionUpdateThread {
return ActionUpdateThread.BGT
}
override fun setSelected(e: AnActionEvent, state: Boolean) {
searchContext = context
action.listPopup?.closeOk(null)
filterButton.repaint()
}
}
}

View File

@@ -0,0 +1,165 @@
// Copyright 2000-2022 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package org.intellij.lang.regexp.inspection.custom;
import com.intellij.find.FindModel;
import com.intellij.openapi.fileTypes.FileType;
import com.intellij.openapi.fileTypes.FileTypeManager;
import com.intellij.openapi.fileTypes.UnknownFileType;
import com.intellij.openapi.util.NlsSafe;
import com.intellij.util.SmartList;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.nio.charset.StandardCharsets;
import java.util.List;
import java.util.Objects;
import java.util.UUID;
/**
* @author Bas Leijdekkers
*/
public class RegExpInspectionConfiguration implements Comparable<RegExpInspectionConfiguration> {
public List<InspectionPattern> patterns;
public @NotNull String name;
public String description;
public String uuid;
public String suppressId;
public String problemDescriptor;
public RegExpInspectionConfiguration(@NotNull String name) {
this.name = name;
uuid = UUID.nameUUIDFromBytes(name.getBytes(StandardCharsets.UTF_8)).toString();
patterns = new SmartList<>();
}
@SuppressWarnings("unused")
public RegExpInspectionConfiguration() {}
public RegExpInspectionConfiguration(RegExpInspectionConfiguration other) {
patterns = new SmartList<>(other.patterns);
name = other.name;
description = other.description;
uuid = other.uuid;
suppressId = other.suppressId;
problemDescriptor = other.problemDescriptor;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
RegExpInspectionConfiguration that = (RegExpInspectionConfiguration)o;
return uuid.equals(that.uuid);
}
@Override
public int hashCode() {
return getUuid().hashCode();
}
public RegExpInspectionConfiguration copy() {
return new RegExpInspectionConfiguration(this);
}
public List<InspectionPattern> getPatterns() {
return patterns;
}
public @NlsSafe @NotNull String getName() {
return name;
}
public @NlsSafe String getDescription() {
return description;
}
public String getUuid() {
return uuid;
}
public @NlsSafe String getSuppressId() {
return suppressId;
}
public @NlsSafe String getProblemDescriptor() {
return problemDescriptor;
}
@Override
public int compareTo(@NotNull RegExpInspectionConfiguration o) {
int result = name.compareToIgnoreCase(o.name);
if (result == 0) result = uuid.compareTo(o.uuid);
return result;
}
public static final class InspectionPattern {
public static final InspectionPattern EMPTY_REPLACE_PATTERN = new InspectionPattern("", null, FindModel.SearchContext.ANY, "");
public @NotNull String regExp;
private @Nullable FileType fileType;
public @Nullable String _fileType;
public @NotNull FindModel.SearchContext searchContext;
public @Nullable String replacement;
public InspectionPattern(
@NotNull String regExp,
@Nullable FileType fileType,
@NotNull FindModel.SearchContext searchContext,
@Nullable String replacement
) {
this.regExp = regExp;
this.fileType = fileType;
if (this.fileType != null) {
_fileType = this.fileType.getName();
}
this.searchContext = searchContext;
this.replacement = replacement;
}
@SuppressWarnings("unused")
public InspectionPattern() {
}
public @NlsSafe String regExp() { return regExp; }
public @Nullable FileType fileType() {
if (fileType == null && _fileType != null) {
fileType = FileTypeManager.getInstance().findFileTypeByName(_fileType);
if (fileType == null) {
fileType = UnknownFileType.INSTANCE;
}
}
return fileType;
}
public FindModel.SearchContext searchContext() { return searchContext; }
public @NlsSafe @Nullable String replacement() { return replacement; }
@Override
public boolean equals(Object obj) {
if (obj == this) return true;
if (obj == null || obj.getClass() != getClass()) return false;
var that = (InspectionPattern)obj;
return Objects.equals(regExp, that.regExp) &&
Objects.equals(fileType, that.fileType) &&
Objects.equals(searchContext, that.searchContext) &&
Objects.equals(replacement, that.replacement);
}
@Override
public int hashCode() {
return Objects.hash(regExp, fileType, searchContext, replacement);
}
@Override
public String toString() {
return "InspectionPattern[" +
"regExp=" + regExp + ", " +
"fileType=" + fileType + ", " +
"searchContext=" + searchContext + ", " +
"replacement=" + replacement + ']';
}
}
}

View File

@@ -0,0 +1,41 @@
// Copyright 2000-2022 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package org.intellij.lang.regexp.inspection.custom;
import com.intellij.icons.AllIcons;
import com.intellij.openapi.fileTypes.FileType;
import com.intellij.ui.ColoredListCellRenderer;
import com.intellij.ui.SimpleTextAttributes;
import org.jetbrains.annotations.NotNull;
import javax.swing.*;
import static com.intellij.openapi.util.text.StringUtil.shortenTextWithEllipsis;
/**
* @author Bas Leijdekkers
*/
public class RegExpInspectionConfigurationCellRenderer extends ColoredListCellRenderer<RegExpInspectionConfiguration.InspectionPattern> {
@Override
protected void customizeCellRenderer(@NotNull JList<? extends RegExpInspectionConfiguration.InspectionPattern> list,
RegExpInspectionConfiguration.InspectionPattern value,
int index,
boolean selected,
boolean hasFocus) {
final FileType fileType = value.fileType();
setIcon((fileType == null) ? AllIcons.FileTypes.Any_type : fileType.getIcon());
final String regExp = value.regExp();
final String replacement = value.replacement();
append("'", SimpleTextAttributes.GRAY_ATTRIBUTES);
if (replacement != null) {
append(shortenTextWithEllipsis(regExp, 49, 0, true), SimpleTextAttributes.REGULAR_ATTRIBUTES);
append("' ⇨ '", SimpleTextAttributes.GRAY_ATTRIBUTES);
append(shortenTextWithEllipsis(replacement, 49, 0, true), SimpleTextAttributes.REGULAR_ATTRIBUTES);
}
else {
append(shortenTextWithEllipsis(regExp, 100, 0, true), SimpleTextAttributes.REGULAR_ATTRIBUTES);
}
append("'", SimpleTextAttributes.GRAY_ATTRIBUTES);
setEnabled(list.isEnabled());
}
}

View File

@@ -0,0 +1,130 @@
// Copyright 2000-2022 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package org.intellij.lang.regexp.inspection.custom
import com.intellij.ide.DefaultTreeExpander
import com.intellij.ide.ui.search.SearchUtil
import com.intellij.openapi.util.NlsSafe
import com.intellij.ui.*
import com.intellij.ui.components.JBScrollPane
import com.intellij.ui.tree.TreePathUtil
import com.intellij.ui.treeStructure.Tree
import com.intellij.util.ui.JBUI
import com.intellij.util.ui.UIUtil
import java.awt.event.MouseEvent
import java.util.function.Function
import javax.swing.JComponent
import javax.swing.JTree
import javax.swing.tree.*
data class RegExpSample(
@NlsSafe val name: String,
@NlsSafe val sample: String,
val caretOffset: Int,
@NlsSafe val category: String,
val userDefined: Boolean,
)
class RegExpSampleTree(val doubleClickConsumer: (RegExpSample) -> Unit) {
val treeModel: TreeModel
val tree: Tree
val panel: JComponent
get() = JBScrollPane(tree).apply { border = JBUI.Borders.empty() }
init {
val root = DefaultMutableTreeNode(null)
treeModel = DefaultTreeModel(root)
tree = Tree(treeModel).apply {
isRootVisible = false
showsRootHandles = true
dragEnabled = false
isEditable = false
selectionModel.selectionMode = TreeSelectionModel.SINGLE_TREE_SELECTION
val speedSearch = TreeSpeedSearch(
this,
false,
Function { treePath: TreePath ->
val treeNode = treePath.lastPathComponent as DefaultMutableTreeNode
val userObject = treeNode.userObject
if (userObject is RegExpSample) userObject.name else userObject.toString()
}
)
cellRenderer = MyTreeCellRenderer(speedSearch)
}
val groups = DefaultMutableTreeNode("Groups")
groups.add(DefaultMutableTreeNode(RegExpSample("Capturing", "()", 1, "Groups", false)))
groups.add(DefaultMutableTreeNode(RegExpSample("Named", "(?<name>)", 8, "Groups", false)))
groups.add(DefaultMutableTreeNode(RegExpSample("Non-capturing", "(?:)", 3, "Groups", false)))
groups.add(DefaultMutableTreeNode(RegExpSample("Lookahead", "(?=)", 3, "Groups", false)))
groups.add(DefaultMutableTreeNode(RegExpSample("Negative lookahead", "(?!)", 3, "Groups", false)))
groups.add(DefaultMutableTreeNode(RegExpSample("Lookbehind", "(?<=)", 4, "Groups", false)))
groups.add(DefaultMutableTreeNode(RegExpSample("Negative lookbehind", "(?<!)", 4, "Groups", false)))
groups.add(DefaultMutableTreeNode(RegExpSample("Atomic", "(?>)", 3, "Groups", false)))
groups.add(DefaultMutableTreeNode(RegExpSample("Comment", "(?#)", 3, "Groups", false)))
root.add(groups)
val anchors = DefaultMutableTreeNode("Anchors")
anchors.add(DefaultMutableTreeNode(RegExpSample("String or line start", "^", -1, "Anchors", false)))
anchors.add(DefaultMutableTreeNode(RegExpSample("String start", "\\A", -1, "Anchors", false)))
anchors.add(DefaultMutableTreeNode(RegExpSample("String or line end", "$", -1, "Anchors", false)))
anchors.add(DefaultMutableTreeNode(RegExpSample("String end", "\\Z", -1, "Anchors", false)))
anchors.add(DefaultMutableTreeNode(RegExpSample("Word boundary", "\\b", -1, "Anchors", false)))
anchors.add(DefaultMutableTreeNode(RegExpSample("Non-word boundary", "\\B", -1, "Anchors", false)))
anchors.add(DefaultMutableTreeNode(RegExpSample("Word start", "\\<", -1, "Anchors", false)))
anchors.add(DefaultMutableTreeNode(RegExpSample("Word end", "\\>", -1, "Anchors", false)))
root.add(anchors)
val classes = DefaultMutableTreeNode("Character Classes")
classes.add(DefaultMutableTreeNode(RegExpSample("Control character", "\\c", -1, "Classes", false)))
classes.add(DefaultMutableTreeNode(RegExpSample("Whitespace", "\\s", -1, "Classes", false)))
classes.add(DefaultMutableTreeNode(RegExpSample("Non-whitespace", "\\S", -1, "Classes", false)))
classes.add(DefaultMutableTreeNode(RegExpSample("Digit", "\\d", -1, "Classes", false)))
classes.add(DefaultMutableTreeNode(RegExpSample("Non-digit", "\\D", -1, "Classes", false)))
classes.add(DefaultMutableTreeNode(RegExpSample("Word", "\\w", -1, "Classes", false)))
classes.add(DefaultMutableTreeNode(RegExpSample("Non-word", "\\W", -1, "Classes", false)))
classes.add(DefaultMutableTreeNode(RegExpSample("Hexadecimal digit", "\\x", -1, "Classes", false)))
classes.add(DefaultMutableTreeNode(RegExpSample("Octal digit", "\\O", -1, "Classes", false)))
root.add(classes)
treeModel.reload()
DefaultTreeExpander(tree).expandAll()
object: DoubleClickListener() {
override fun onDoubleClick(event: MouseEvent): Boolean {
val path = tree.selectionPath ?: return false
val node = TreePathUtil.toTreeNode(path) as? DefaultMutableTreeNode
val sample = node?.userObject as? RegExpSample ?: return false
doubleClickConsumer(sample)
return true
}
}.installOn(tree)
}
private inner class MyTreeCellRenderer(private val mySpeedSearch: TreeSpeedSearch) : ColoredTreeCellRenderer() {
override fun customizeCellRenderer(tree: JTree, value: Any, selected: Boolean, expanded: Boolean, leaf: Boolean, row: Int, hasFocus: Boolean) {
val treeNode = value as DefaultMutableTreeNode
val userObject = treeNode.userObject ?: return
val background = UIUtil.getTreeBackground(selected, hasFocus)
val foreground = UIUtil.getTreeForeground(selected, hasFocus)
val text: String
val style: Int
when (userObject) {
is RegExpSample -> {
text = userObject.name
style = SimpleTextAttributes.STYLE_PLAIN
}
else -> {
text = userObject.toString()
style = SimpleTextAttributes.STYLE_BOLD
}
}
SearchUtil.appendFragments(mySpeedSearch.enteredPrefix, text, style, foreground, background, this)
if (userObject is RegExpSample) {
append(ColoredText.singleFragment(" ${userObject.sample}", SimpleTextAttributes.GRAYED_ATTRIBUTES))
}
}
}
}

View File

@@ -12,6 +12,9 @@ public final class InspectionsConfigTreeComparator {
.thenComparing(n -> getDisplayTextToSort(n.getText()), NaturalComparator.INSTANCE);
public static String getDisplayTextToSort(String s) {
if (s.equals("User Defined")) {
return " User Defined";
}
if (s.isEmpty()) {
return s;
}

View File

@@ -276,8 +276,8 @@ error.in.groovy.parser=Error in Groovy parser
SSRInspection.family.name=Replace Structurally
SSRInspection.display.name=Structural search inspection
SSRInspection.add.search.template.button=Add Search Template\u2026
SSRInspection.add.replace.template.button=Add Replace Template\u2026
SSRInspection.add.search.template.button=Add Structural Search Inspection\u2026
SSRInspection.add.replace.template.button=Add Structural Replace Inspection\u2026
overwrite.message=A template with the same name already exists. Replacing it will overwrite its current contents.
overwrite.title="{0}" Exists, Replace?
template.in.use.message=Template ''{0}'' is used from template ''{1}''. Are you sure you want to remove it?
@@ -288,12 +288,13 @@ search.script.problem=Structural Search script threw an exception: {0}
complete.match.variable.name=Complete match
template.in.use.title=Template ''{0}'' In Use
user.defined.group.name=User Defined
structural.search.group.name=Structural search
edit.metadata.button=Edit Metadata\u2026
add.pattern.action=Add Template
templates.title=Templates:
add.inspection.button=Add Structural Search \\&\\& Replace Inspection
remove.inspection.button=Remove Structural Search \\&\\& Replace Inspection
add.inspection.button=Add Custom Inspection
remove.inspection.button=Remove Custom Inspection
meta.data.dialog.title=Structural Search Inspection
inspection.name.label=Inspection name:
@@ -331,4 +332,6 @@ replace.configuration.display.text={0} \u21E8 {1}
# SSR advertising in the empty inspection tree
inspection.tree.create.inspection.search.template=Using a Structural Search Template\u2026
inspection.tree.create.inspection.replace.template=Using a Structural Replace Template\u2026
inspection.tree.create.inspection.replace.template=Using a Structural Replace Template\u2026
action.add.regexp.replace.inspection.text=Add RegExp Replace Inspection\u2026
action.add.regexp.search.inspection.text=Add RegExp Search Inspection\u2026

View File

@@ -1,13 +1,16 @@
// Copyright 2000-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
// Copyright 2000-2022 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.intellij.structuralsearch.inspection;
import com.intellij.codeInspection.InspectionProfile;
import com.intellij.codeInspection.InspectionProfileEntry;
import com.intellij.codeInspection.ex.InspectionProfileImpl;
import com.intellij.codeInspection.ex.InspectionProfileModifiableModel;
import com.intellij.codeInspection.ex.InspectionToolWrapper;
import com.intellij.openapi.project.Project;
import com.intellij.profile.codeInspection.ui.SingleInspectionProfilePanel;
import com.intellij.util.ui.UIUtil;
import org.intellij.lang.regexp.inspection.custom.CustomRegExpInspection;
import org.jetbrains.annotations.NonNls;
import org.jetbrains.annotations.NotNull;
import java.awt.*;
@@ -21,9 +24,17 @@ public final class InspectionProfileUtil {
@NotNull
public static SSBasedInspection getStructuralSearchInspection(@NotNull InspectionProfile profile) {
final InspectionToolWrapper<?, ?> wrapper = profile.getInspectionTool(SSBasedInspection.SHORT_NAME, (Project)null);
return (SSBasedInspection)getInspection(profile, SSBasedInspection.SHORT_NAME);
}
public static CustomRegExpInspection getCustomRegExpInspection(@NotNull InspectionProfile profile) {
return (CustomRegExpInspection)getInspection(profile, CustomRegExpInspection.SHORT_NAME);
}
public static InspectionProfileEntry getInspection(@NotNull InspectionProfile profile, @NonNls String shortName) {
final InspectionToolWrapper<?, ?> wrapper = profile.getInspectionTool(shortName, (Project)null);
assert wrapper != null;
return (SSBasedInspection)wrapper.getTool();
return wrapper.getTool();
}
public static InspectionProfileModifiableModel getInspectionProfile(@NotNull Component c) {

View File

@@ -2,6 +2,7 @@
package com.intellij.structuralsearch.inspection;
import com.intellij.codeInsight.daemon.HighlightDisplayKey;
import com.intellij.codeInspection.InspectionsBundle;
import com.intellij.codeInspection.LocalInspectionTool;
import com.intellij.codeInspection.ex.InspectionProfileModifiableModel;
import com.intellij.ide.DataManager;
@@ -105,6 +106,11 @@ public class StructuralSearchFakeInspection extends LocalInspectionTool {
return SSRBundle.message("structural.search.group.name");
}
@Override
public @Nls(capitalization = Nls.Capitalization.Sentence) String @NotNull [] getGroupPath() {
return new String[] {InspectionsBundle.message("group.names.user.defined"), getGroupDisplayName()};
}
@Nullable
@Override
public String getStaticDescription() {

View File

@@ -2,6 +2,7 @@
package com.intellij.structuralsearch.inspection;
import com.intellij.codeInsight.daemon.HighlightDisplayKey;
import com.intellij.codeInspection.InspectionProfileEntry;
import com.intellij.codeInspection.LocalInspectionTool;
import com.intellij.codeInspection.ex.InspectionProfileImpl;
import com.intellij.codeInspection.ex.InspectionProfileModifiableModel;
@@ -30,6 +31,7 @@ import com.intellij.ui.EditorTextField;
import com.intellij.util.ObjectUtils;
import com.intellij.util.SmartList;
import com.intellij.util.ui.FormBuilder;
import org.intellij.lang.regexp.inspection.custom.*;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
@@ -51,7 +53,9 @@ public class StructuralSearchProfileActionProvider extends InspectionProfileActi
enableSSIfDisabled(panel.getProfile(), panel.getProject());
final DefaultActionGroup actionGroup = new DefaultActionGroup(
new AddInspectionAction(panel, SSRBundle.message("SSRInspection.add.search.template.button"), false),
new AddInspectionAction(panel, SSRBundle.message("SSRInspection.add.replace.template.button"), true)
new AddInspectionAction(panel, SSRBundle.message("SSRInspection.add.replace.template.button"), true),
new AddCustomRegExpInspectionAction(panel, SSRBundle.message("action.add.regexp.search.inspection.text"), false),
new AddCustomRegExpInspectionAction(panel, SSRBundle.message("action.add.regexp.replace.inspection.text"), true)
);
actionGroup.setPopup(true);
actionGroup.registerCustomShortcutSet(CommonShortcuts.getNew(), panel);
@@ -86,7 +90,9 @@ public class StructuralSearchProfileActionProvider extends InspectionProfileActi
@Override
public void update(@NotNull AnActionEvent e) {
e.getPresentation().setEnabled(myPanel.getSelectedTool() instanceof StructuralSearchInspectionToolWrapper);
final InspectionToolWrapper<?, ?> selectedTool = myPanel.getSelectedTool();
e.getPresentation().setEnabled(selectedTool instanceof CustomRegExpInspectionToolWrapper ||
selectedTool instanceof StructuralSearchInspectionToolWrapper);
}
@Override
@@ -98,16 +104,57 @@ public class StructuralSearchProfileActionProvider extends InspectionProfileActi
public void actionPerformed(@NotNull AnActionEvent e) {
final InspectionToolWrapper<?, ?> selectedTool = myPanel.getSelectedTool();
final String shortName = selectedTool.getShortName();
final String mainToolId = selectedTool.getMainToolId();
myPanel.removeSelectedRow();
final InspectionProfileModifiableModel profile = myPanel.getProfile();
final SSBasedInspection inspection = InspectionProfileUtil.getStructuralSearchInspection(profile);
inspection.removeConfigurationsWithUuid(shortName);
final InspectionProfileEntry inspection = InspectionProfileUtil.getInspection(profile, mainToolId);
if (inspection instanceof SSBasedInspection ssBasedInspection) {
ssBasedInspection.removeConfigurationsWithUuid(shortName);
}
else if (inspection instanceof CustomRegExpInspection customRegExpInspection) {
customRegExpInspection.removeConfigurationWithUuid(shortName);
}
profile.removeTool(selectedTool);
profile.setModified(true);
InspectionProfileUtil.fireProfileChanged(profile);
}
}
static final class AddCustomRegExpInspectionAction extends DumbAwareAction {
private final SingleInspectionProfilePanel myPanel;
private final boolean myReplace;
AddCustomRegExpInspectionAction(@NotNull SingleInspectionProfilePanel panel, @NlsActions.ActionText String text, boolean replace) {
super(text);
myPanel = panel;
myReplace = replace;
}
@Override
public void actionPerformed(@NotNull AnActionEvent e) {
final RegExpDialog dialog = new RegExpDialog(e.getProject(), true, myReplace ? RegExpInspectionConfiguration.InspectionPattern.EMPTY_REPLACE_PATTERN : null);
if (myReplace) {
// do something?
}
if (!dialog.showAndGet()) return;
final RegExpInspectionConfiguration.InspectionPattern pattern = dialog.getPattern();
final InspectionProfileModifiableModel profile = myPanel.getProfile();
final CustomRegExpInspection inspection = InspectionProfileUtil.getCustomRegExpInspection(profile);
final RegExpInspectionConfiguration configuration = new RegExpInspectionConfiguration("new inspection");
configuration.patterns.add(pattern);
final Project project = getEventProject(e);
final MetaDataDialog metaDataDialog = new MetaDataDialog(project, inspection, configuration, true);
if (!metaDataDialog.showAndGet()) return;
inspection.addConfiguration(configuration);
CustomRegExpInspection.addInspectionToProfile(project, profile, configuration);
profile.setModified(true);
InspectionProfileUtil.fireProfileChanged(profile);
myPanel.selectInspectionTool(configuration.getUuid());
}
}
static final class AddInspectionAction extends DumbAwareAction {
private final SingleInspectionProfilePanel myPanel;
private final boolean myReplace;
@@ -210,7 +257,7 @@ public class StructuralSearchProfileActionProvider extends InspectionProfileActi
}
@Override
public @Nullable JComponent getPreferredFocusedComponent() {
public JComponent getPreferredFocusedComponent() {
return myNameTextField;
}