diff --git a/platform/platform-api/src/com/intellij/openapi/ui/DialogWrapper.java b/platform/platform-api/src/com/intellij/openapi/ui/DialogWrapper.java index c120df73f14d..c1948e2af731 100644 --- a/platform/platform-api/src/com/intellij/openapi/ui/DialogWrapper.java +++ b/platform/platform-api/src/com/intellij/openapi/ui/DialogWrapper.java @@ -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 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 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; } diff --git a/platform/structuralsearch/source/com/intellij/structuralsearch/impl/matcher/compiler/PatternCompiler.java b/platform/structuralsearch/source/com/intellij/structuralsearch/impl/matcher/compiler/PatternCompiler.java index 625409e55a7d..59be6822fc6a 100644 --- a/platform/structuralsearch/source/com/intellij/structuralsearch/impl/matcher/compiler/PatternCompiler.java +++ b/platform/structuralsearch/source/com/intellij/structuralsearch/impl/matcher/compiler/PatternCompiler.java @@ -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 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); diff --git a/platform/structuralsearch/source/com/intellij/structuralsearch/plugin/ui/StructuralSearchDialog.java b/platform/structuralsearch/source/com/intellij/structuralsearch/plugin/ui/StructuralSearchDialog.java index 335d13e447fb..22c740ca4a17 100644 --- a/platform/structuralsearch/source/com/intellij/structuralsearch/plugin/ui/StructuralSearchDialog.java +++ b/platform/structuralsearch/source/com/intellij/structuralsearch/plugin/ui/StructuralSearchDialog.java @@ -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 doValidateAll() { + final JRootPane component = getRootPane(); + if (component == null) { + return Collections.emptyList(); + } + List 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); diff --git a/platform/structuralsearch/source/messages/SSRBundle.properties b/platform/structuralsearch/source/messages/SSRBundle.properties index 631752b0707d..21e5b0bc4950 100644 --- a/platform/structuralsearch/source/messages/SSRBundle.properties +++ b/platform/structuralsearch/source/messages/SSRBundle.properties @@ -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