SSR: rework dialog validation (IDEA-201479)

GitOrigin-RevId: a78aac65bbb1a45abc46f4800730d787cea9c071
This commit is contained in:
Bas Leijdekkers
2021-01-07 17:55:01 +00:00
committed by intellij-monorepo-bot
parent 102b188350
commit fc06e1a491
4 changed files with 115 additions and 145 deletions
@@ -1,4 +1,4 @@
// 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-2021 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.
package com.intellij.openapi.ui;
import com.intellij.CommonBundle;
@@ -331,6 +331,16 @@ public abstract class DialogWrapper {
return true;
}
/**
* Allow to disable continuous validation.
* When disabled {@link #initValidation()} needs to be invoked after every change of the dialog to validate.
*
* @return {@code false} to disable continuous validation
*/
protected boolean continuousValidation() {
return true;
}
/**
* Validates user input and returns {@code null} if everything is fine
* or validation description with component where problem has been found.
@@ -382,7 +392,7 @@ public abstract class DialogWrapper {
private void installErrorPainter() {
if (myErrorPainterInstalled) return;
myErrorPainterInstalled = true;
UIUtil.invokeLaterIfNeeded(() -> IdeGlassPaneUtil.installPainter(getContentPanel(), myErrorPainter, myDisposable));
IdeGlassPaneUtil.installPainter(getContentPanel(), myErrorPainter, myDisposable);
}
protected void updateErrorInfo(@NotNull List<ValidationInfo> info) {
@@ -390,6 +400,10 @@ public abstract class DialogWrapper {
if (updateNeeded) {
SwingUtilities.invokeLater(() -> {
if (myDisposed) return;
if (!info.isEmpty()) {
installErrorPainter();
}
myErrorPainter.setValidationInfo(info);
setErrorInfoAll(info);
myPeer.getRootPane().getGlassPane().repaint();
getOKAction().setEnabled(info.isEmpty() || info.stream().allMatch(info1 -> info1.okEnabled));
@@ -1413,6 +1427,7 @@ public abstract class DialogWrapper {
}
protected void startTrackingValidation() {
if (!continuousValidation()) return;
SwingUtilities.invokeLater(() -> {
if (!myValidationStarted && !myDisposed) {
myValidationStarted = true;
@@ -1425,14 +1440,9 @@ public abstract class DialogWrapper {
myValidationAlarm.cancelAllRequests();
final Runnable validateRequest = () -> {
if (myDisposed) return;
List<ValidationInfo> result = doValidateAll();
if (!result.isEmpty()) {
installErrorPainter();
}
myErrorPainter.setValidationInfo(result);
updateErrorInfo(result);
updateErrorInfo(doValidateAll());
if (!myDisposed) {
if (!myDisposed && continuousValidation()) {
initValidation();
}
};
@@ -1889,7 +1899,7 @@ public abstract class DialogWrapper {
if (!isInplaceValidationToolTipEnabled()) {
DialogEarthquakeShaker.shake(getPeer().getWindow());
}
updateErrorInfo(infoList);
startTrackingValidation();
if(infoList.stream().anyMatch(info1 -> !info1.okEnabled)) return;
}
@@ -1,4 +1,4 @@
// 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-2021 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.
package com.intellij.structuralsearch.impl.matcher.compiler;
import com.intellij.codeInsight.template.Template;
@@ -72,6 +72,9 @@ public final class PatternCompiler {
try {
final List<PsiElement> elements = compileByAllPrefixes(project, options, result, context, prefixes, checkForErrors);
if (elements.isEmpty()) {
return null;
}
final CompiledPattern pattern = context.getPattern();
collectVariableNodes(pattern, elements, checkForErrors);
pattern.setNodes(elements);
@@ -1,4 +1,4 @@
// 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-2021 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.
package com.intellij.structuralsearch.plugin.ui;
import com.intellij.codeInsight.highlighting.HighlightHandlerBase;
@@ -46,15 +46,16 @@ import com.intellij.openapi.fileEditor.TextEditor;
import com.intellij.openapi.fileTypes.LanguageFileType;
import com.intellij.openapi.ide.CopyPasteManager;
import com.intellij.openapi.progress.ProcessCanceledException;
import com.intellij.openapi.progress.util.ProgressIndicatorUtils;
import com.intellij.openapi.project.*;
import com.intellij.openapi.ui.DialogWrapper;
import com.intellij.openapi.ui.MessageType;
import com.intellij.openapi.ui.Splitter;
import com.intellij.openapi.ui.ValidationInfo;
import com.intellij.openapi.ui.popup.Balloon;
import com.intellij.openapi.ui.popup.JBPopupFactory;
import com.intellij.openapi.util.*;
import com.intellij.openapi.util.registry.Registry;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.openapi.wm.WindowManager;
import com.intellij.psi.PsiDocumentManager;
import com.intellij.psi.PsiElement;
@@ -99,9 +100,6 @@ import java.util.Collections;
import java.util.List;
import java.util.Objects;
import static com.intellij.openapi.util.text.StringUtil.isEmpty;
import static com.intellij.openapi.util.text.StringUtil.trimEnd;
/**
* This dialog is used in two ways:
* 1. a non-modal search dialog, showing a scope panel
@@ -155,7 +153,6 @@ public class StructuralSearchDialog extends DialogWrapper implements DocumentLis
private final Alarm myAlarm;
private boolean myUseLastConfiguration;
private final boolean myEditConfigOnly;
private boolean myDoingOkAction;
// components
private final FileTypeChooser myFileTypeChooser = new FileTypeChooser();
@@ -238,6 +235,7 @@ public class StructuralSearchDialog extends DialogWrapper implements DocumentLis
init();
myAlarm = new Alarm(Alarm.ThreadToUse.POOLED_THREAD, myDisposable);
setValidationDelay(100);
}
private void addRestartHighlightingListenerToCurrentEditor() {
@@ -273,41 +271,7 @@ public class StructuralSearchDialog extends DialogWrapper implements DocumentLis
@Override
public void documentChanged(@NotNull final DocumentEvent event) {
initiateValidation();
}
private void initiateValidation() {
if (myAlarm.isDisposed()) return;
myAlarm.cancelAllRequests();
myAlarm.addRequest(() -> {
final boolean success = ProgressIndicatorUtils.runInReadActionWithWriteActionPriority(() -> {
try {
final CompiledPattern compiledPattern = compilePattern();
checkReplacementPattern();
final JRootPane component = getRootPane();
if (component == null) {
return;
}
initializeFilterPanel();
if (compiledPattern != null) {
addMatchHighlights();
}
ApplicationManager.getApplication().invokeLater(() -> {
setSearchTargets(myConfiguration.getMatchOptions());
getOKAction().setEnabled(compiledPattern != null);
}, ModalityState.stateForComponent(component));
}
catch (ProcessCanceledException e) {
throw e;
}
catch (RuntimeException e) {
Logger.getInstance(StructuralSearchDialog.class).error(e);
}
});
if (!success) {
initiateValidation();
}
}, 100);
initValidation();
}
private void initializeFilterPanel() {
@@ -413,14 +377,14 @@ public class StructuralSearchDialog extends DialogWrapper implements DocumentLis
myScopePanel = new ScopePanel(getProject(), myDisposable);
if (!myEditConfigOnly) {
myScopePanel.setRecentDirectories(FindInProjectSettings.getInstance(getProject()).getRecentDirectories());
myScopePanel.setScopeConsumer(scope -> initiateValidation());
myScopePanel.setScopeConsumer(scope -> initValidation());
}
else {
myScopePanel.setVisible(false);
}
myFilterPanel = new FilterPanel(getProject(), myFileType, getDisposable());
myFilterPanel.setConstraintChangedCallback(() -> initiateValidation());
myFilterPanel.setConstraintChangedCallback(() -> initValidation());
myFilterPanel.getComponent().setMinimumSize(new Dimension(300, 50));
mySearchEditorPanel.setSecondComponent(myFilterPanel.getComponent());
@@ -431,7 +395,7 @@ public class StructuralSearchDialog extends DialogWrapper implements DocumentLis
for (String name : matchOptions.getVariableConstraintNames()) {
matchOptions.getVariableConstraint(name).setPartOfSearchResults(name.equals(item));
}
initiateValidation();
initValidation();
});
final JPanel centerPanel = new JPanel(null);
@@ -601,7 +565,7 @@ public class StructuralSearchDialog extends DialogWrapper implements DocumentLis
@Override
public void setSelected(@NotNull AnActionEvent e, boolean state) {
myConfiguration.getMatchOptions().setSearchInjectedCode(state);
initiateValidation();
initValidation();
}
};
final CheckboxAction recursive = new CheckboxAction(SSRBundle.message("recursive.matching.checkbox")) {
@@ -621,7 +585,7 @@ public class StructuralSearchDialog extends DialogWrapper implements DocumentLis
public void setSelected(@NotNull AnActionEvent e, boolean state) {
if (myConfiguration instanceof SearchConfiguration) {
myConfiguration.getMatchOptions().setRecursiveSearch(state);
initiateValidation();
initValidation();
}
}
};
@@ -634,7 +598,7 @@ public class StructuralSearchDialog extends DialogWrapper implements DocumentLis
@Override
public void setSelected(@NotNull AnActionEvent e, boolean state) {
myConfiguration.getMatchOptions().setCaseSensitiveMatch(state);
initiateValidation();
initValidation();
}
};
myFileType = UIUtil.detectFileType(mySearchContext);
@@ -670,7 +634,7 @@ public class StructuralSearchDialog extends DialogWrapper implements DocumentLis
myReplaceCriteriaEdit.setNewDocumentAndFileType(myFileType, replaceDocument);
replaceDocument.putUserData(STRUCTURAL_SEARCH_PATTERN_CONTEXT_ID, contextId);
initiateValidation();
initValidation();
});
final DefaultActionGroup templateActionGroup = new DefaultActionGroup();
templateActionGroup.add(
@@ -839,15 +803,10 @@ public class StructuralSearchDialog extends DialogWrapper implements DocumentLis
@Override
protected void doOKAction() {
myDoingOkAction = true;
super.doOKAction();
removeMatchHighlights();
final CompiledPattern compiledPattern = compilePattern();
myDoingOkAction = false;
if (compiledPattern == null) return;
myAlarm.cancelAllRequests();
myConfiguration.removeUnusedVariables();
super.doOKAction();
if (myEditConfigOnly) return;
final SearchScope scope = myScopePanel.getScope();
@@ -856,16 +815,9 @@ public class StructuralSearchDialog extends DialogWrapper implements DocumentLis
FindInProjectSettings.getInstance(getProject()).addDirectory(directoryScope.getDirectory().getPresentableUrl());
}
final FindSettings findSettings = FindSettings.getInstance();
findSettings.setShowResultsInSeparateView(myOpenInNewTab.isSelected());
try {
ConfigurationManager.getInstance(getProject()).addHistoryConfiguration(myConfiguration);
startSearching();
}
catch (MalformedPatternException ex) {
reportMessage(SSRBundle.message("this.pattern.is.malformed.message", ex.getMessage()), true, mySearchCriteriaEdit);
}
FindSettings.getInstance().setShowResultsInSeparateView(myOpenInNewTab.isSelected());
ConfigurationManager.getInstance(getProject()).addHistoryConfiguration(myConfiguration);
startSearching();
}
public Configuration getConfiguration() {
@@ -873,55 +825,6 @@ public class StructuralSearchDialog extends DialogWrapper implements DocumentLis
return myConfiguration.copy();
}
@Nullable
private CompiledPattern compilePattern() {
final MatchOptions matchOptions = getConfiguration().getMatchOptions();
final Project project = getProject();
try {
final CompiledPattern compiledPattern = PatternCompiler.compilePattern(project, matchOptions, true, !myEditConfigOnly);
reportMessage(null, false, mySearchCriteriaEdit);
return compiledPattern;
}
catch (MalformedPatternException e) {
removeMatchHighlights();
final String message = isEmpty(matchOptions.getSearchPattern())
? null
: SSRBundle.message("this.pattern.is.malformed.message", (e.getMessage() != null) ? e.getMessage() : "");
if (!e.isErrorElement || !Registry.is("ssr.in.editor.problem.highlighting")) {
reportMessage(message, true, mySearchCriteriaEdit);
}
return null;
}
catch (UnsupportedPatternException e) {
removeMatchHighlights();
reportMessage(SSRBundle.message("this.pattern.is.unsupported.message", e.getMessage()), true, mySearchCriteriaEdit);
return null;
}
catch (NoMatchFoundException e) {
removeMatchHighlights();
reportMessage(e.getMessage(), false, myScopePanel);
return null;
}
}
private void checkReplacementPattern() {
if (!myReplace) {
return;
}
try {
Replacer.checkReplacementPattern(getProject(), myConfiguration.getReplaceOptions());
reportMessage(null, false, myReplaceCriteriaEdit);
}
catch (UnsupportedPatternException ex) {
reportMessage(SSRBundle.message("unsupported.replacement.pattern.message", ex.getMessage()), true, myReplaceCriteriaEdit);
}
catch (MalformedPatternException ex) {
if (!ex.isErrorElement || !Registry.is("ssr.in.editor.problem.highlighting")) {
reportMessage(SSRBundle.message("malformed.replacement.pattern.message", ex.getMessage()), true, myReplaceCriteriaEdit);
}
}
}
private void removeMatchHighlights() {
if (myEditConfigOnly || myRangeHighlighters.isEmpty()) {
return;
@@ -1017,25 +920,87 @@ public class StructuralSearchDialog extends DialogWrapper implements DocumentLis
});
}
@Override
protected boolean continuousValidation() {
return false;
}
@Override
protected @NotNull List<ValidationInfo> doValidateAll() {
final JRootPane component = getRootPane();
if (component == null) {
return Collections.emptyList();
}
List<ValidationInfo> errors = new SmartList<>();
if (mySearchCriteriaEdit.getText().trim().isEmpty()) {
errors.add(new ValidationInfo(""));
}
final MatchOptions matchOptions = getConfiguration().getMatchOptions();
try {
final Project project = getProject();
CompiledPattern compiledPattern = null;
try {
compiledPattern = PatternCompiler.compilePattern(project, matchOptions, true, !myEditConfigOnly && !myPerformAction);
}
catch (MalformedPatternException e) {
removeMatchHighlights();
if (!StringUtil.isEmptyOrSpaces(matchOptions.getSearchPattern())) {
final String message = e.getMessage();
errors.add(new ValidationInfo((message == null)
? SSRBundle.message("this.pattern.is.malformed.message")
: message,
mySearchCriteriaEdit));
}
}
catch (UnsupportedPatternException e) {
removeMatchHighlights();
errors.add(new ValidationInfo(e.getMessage(), mySearchCriteriaEdit));
}
catch (NoMatchFoundException e) {
removeMatchHighlights();
errors.add(new ValidationInfo(e.getMessage(), mySearchCriteriaEdit));
}
if (myReplace) {
try {
Replacer.checkReplacementPattern(getProject(), myConfiguration.getReplaceOptions());
}
catch (UnsupportedPatternException e) {
errors.add(new ValidationInfo(e.getMessage(), myReplaceCriteriaEdit));
}
catch (MalformedPatternException e) {
errors.add(new ValidationInfo(e.getMessage(), myReplaceCriteriaEdit));
}
}
initializeFilterPanel();
if (compiledPattern != null) {
addMatchHighlights();
}
ApplicationManager.getApplication().invokeLater(() -> {
setSearchTargets(myConfiguration.getMatchOptions());
}, ModalityState.stateForComponent(component));
}
catch (ProcessCanceledException e) {
throw e;
}
catch (RuntimeException e) {
Logger.getInstance(StructuralSearchDialog.class).error(e);
}
return errors;
}
private Balloon myBalloon;
private void reportMessage(@NlsContexts.PopupContent @Nullable String message, boolean error, @NotNull JComponent component) {
ApplicationManager.getApplication().invokeLater(() -> {
if (isDisposed()) return;
if (myBalloon != null) myBalloon.hide();
component.putClientProperty("JComponent.outline", (!error || message == null) ? null : "error");
component.repaint();
if (message == null) return;
myBalloon = JBPopupFactory.getInstance()
.createHtmlTextBalloonBuilder(message, error ? MessageType.ERROR : MessageType.WARNING, null)
.setHideOnFrameResize(false)
.createBalloon();
if (component != myScopePanel) {
myBalloon.show(new RelativePoint(component, new Point(component.getWidth() / 2, component.getHeight())), Balloon.Position.below);
}
else {
myBalloon.show(new RelativePoint(component, new Point(component.getWidth() / 2, 0)), Balloon.Position.above);
}
myBalloon.showInCenterOf(component);
myBalloon.show(new RelativePoint(component, new Point(component.getWidth() / 2, component.getHeight())), Balloon.Position.below);
Disposer.register(myDisposable, myBalloon);
}, ModalityState.stateForComponent(component));
}
@@ -1192,7 +1157,7 @@ public class StructuralSearchDialog extends DialogWrapper implements DocumentLis
final SearchScope scope = myScopePanel.getScope();
final boolean searchWithinHierarchy = IdeBundle.message("scope.class.hierarchy").equals(scope.getDisplayName());
// We need to reset search within hierarchy scope during online validation since the scope works with user participation
matchOptions.setScope(searchWithinHierarchy && !myDoingOkAction ? GlobalSearchScope.projectScope(getProject()) : scope);
matchOptions.setScope(searchWithinHierarchy && !myPerformAction ? GlobalSearchScope.projectScope(getProject()) : scope);
}
else {
matchOptions.setScope(null);
@@ -1389,7 +1354,7 @@ public class StructuralSearchDialog extends DialogWrapper implements DocumentLis
SubstitutionShortInfoHandler.install(editor, variableName -> {
if (variableName.endsWith(ReplaceConfiguration.REPLACEMENT_VARIABLE_SUFFIX)) {
//noinspection AssignmentToLambdaParameter
variableName = trimEnd(variableName, ReplaceConfiguration.REPLACEMENT_VARIABLE_SUFFIX);
variableName = StringUtil.trimEnd(variableName, ReplaceConfiguration.REPLACEMENT_VARIABLE_SUFFIX);
assert myConfiguration instanceof ReplaceConfiguration;
myFilterPanel.initFilters(UIUtil.getOrAddReplacementVariable(variableName, myConfiguration));
}
@@ -1402,10 +1367,7 @@ public class StructuralSearchDialog extends DialogWrapper implements DocumentLis
}, myDisposable, myReplace);
editor.putUserData(SubstitutionShortInfoHandler.CURRENT_CONFIGURATION_KEY, myConfiguration);
getDocument().putUserData(STRUCTURAL_SEARCH_ERROR_CALLBACK, () -> {
putClientProperty("JComponent.outline", "error");
repaint();
getOKAction().setEnabled(false);
if (!myReplace) removeMatchHighlights();
if (getClientProperty("JComponent.outline") == null) initValidation();
});
TextCompletionUtil.installCompletionHint(editor);
@@ -50,8 +50,7 @@ found.occurrences=Found Matches in {0}
targets.node.text=Structural Search Template
# search dialog messages
this.pattern.is.malformed.message=The specified template is malformed\n {0}
this.pattern.is.unsupported.message=The specified template is unsupported
this.pattern.is.malformed.message=The specified template is malformed
incorrect.pattern.message=Incorrect template
used.templates.history.dialog.title=Used Templates History
@@ -109,10 +108,6 @@ structural.replace.title=Structural Replace
shorten.fully.qualified.names.checkbox=Sh&orten fully qualified names
replacement.template.label=Replace template:
# Replace validation messages
unsupported.replacement.pattern.message=Unsupported replacement template {0}
malformed.replacement.pattern.message=Malformed replacement template {0}
#replacement usage view specific
preview.replacement.button=&Preview Replacement
do.replace.all.button=Replace &All