From 0899b035e762d3ef9ec2c8228b47f7b7314eec7c Mon Sep 17 00:00:00 2001 From: Bas Leijdekkers Date: Sat, 8 Jun 2019 11:34:41 +0200 Subject: [PATCH] SSR: show filters in editor inlays instead of tool tips (IDEA-210453) GitOrigin-RevId: 34555e014ed4c698b814966333dff0d740506bab --- .../plugin/ui/StructuralSearchDialog.java | 20 ++- .../ui/SubstitutionShortInfoHandler.java | 159 +++++++++++++++--- .../structuralsearch/plugin/ui/UIUtil.java | 4 + .../source/messages/SSRBundle.properties | 7 +- .../util/resources/misc/registry.properties | 2 + 5 files changed, 155 insertions(+), 37 deletions(-) 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 c4c5ab7ca601..263250f17d9c 100644 --- a/platform/structuralsearch/source/com/intellij/structuralsearch/plugin/ui/StructuralSearchDialog.java +++ b/platform/structuralsearch/source/com/intellij/structuralsearch/plugin/ui/StructuralSearchDialog.java @@ -273,13 +273,19 @@ public class StructuralSearchDialog extends DialogWrapper { private void initializeFilterPanel() { final MatchOptions matchOptions = getConfiguration().getMatchOptions(); final CompiledPattern compiledPattern = PatternCompiler.compilePattern(getProject(), matchOptions, false, false); - if (compiledPattern != null) { - myFilterPanel.setCompiledPattern(compiledPattern); - } - if (!myFilterPanel.isInitialized()) { - myFilterPanel.initFilters(UIUtil.getOrAddVariableConstraint(Configuration.CONTEXT_VAR_NAME, myConfiguration)); - } - myFilterPanel.setValid(compiledPattern != null); + ApplicationManager.getApplication().invokeLater(() -> { + if (compiledPattern != null) { + final SubstitutionShortInfoHandler handler = SubstitutionShortInfoHandler.retrieve(mySearchCriteriaEdit.getEditor()); + if (handler != null) { + handler.updateEditorInlays(); + } + myFilterPanel.setCompiledPattern(compiledPattern); + } + if (!myFilterPanel.isInitialized()) { + myFilterPanel.initFilters(UIUtil.getOrAddVariableConstraint(Configuration.CONTEXT_VAR_NAME, myConfiguration)); + } + myFilterPanel.setValid(compiledPattern != null); + }); } private Configuration createConfiguration(Configuration template) { diff --git a/platform/structuralsearch/source/com/intellij/structuralsearch/plugin/ui/SubstitutionShortInfoHandler.java b/platform/structuralsearch/source/com/intellij/structuralsearch/plugin/ui/SubstitutionShortInfoHandler.java index efcb91d8acdf..a8bfcaa104ff 100644 --- a/platform/structuralsearch/source/com/intellij/structuralsearch/plugin/ui/SubstitutionShortInfoHandler.java +++ b/platform/structuralsearch/source/com/intellij/structuralsearch/plugin/ui/SubstitutionShortInfoHandler.java @@ -1,13 +1,16 @@ -// Copyright 2000-2018 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-2019 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.hint.TooltipController; import com.intellij.codeInsight.hint.TooltipGroup; +import com.intellij.codeInsight.template.Template; +import com.intellij.codeInsight.template.TemplateManager; import com.intellij.codeInsight.template.impl.TemplateImplUtil; -import com.intellij.openapi.editor.Document; -import com.intellij.openapi.editor.Editor; -import com.intellij.openapi.editor.LogicalPosition; +import com.intellij.openapi.editor.*; import com.intellij.openapi.editor.event.*; +import com.intellij.openapi.editor.markup.TextAttributes; +import com.intellij.openapi.ui.GraphicsConfig; +import com.intellij.openapi.util.Disposer; import com.intellij.openapi.util.Key; import com.intellij.openapi.util.registry.Registry; import com.intellij.openapi.util.text.StringUtil; @@ -19,6 +22,7 @@ import com.intellij.structuralsearch.plugin.replace.ui.ReplaceConfiguration; import com.intellij.ui.ColorUtil; import com.intellij.ui.HintHint; import com.intellij.util.SmartList; +import com.intellij.util.ui.GraphicsUtil; import com.intellij.util.ui.JBUI; import com.intellij.util.ui.UIUtil; import org.jetbrains.annotations.NotNull; @@ -26,7 +30,10 @@ import org.jetbrains.annotations.Nullable; import javax.swing.*; import java.awt.*; +import java.util.HashMap; +import java.util.HashSet; import java.util.List; +import java.util.Map; import java.util.function.Consumer; public class SubstitutionShortInfoHandler implements DocumentListener, EditorMouseMotionListener, CaretListener { @@ -37,8 +44,9 @@ public class SubstitutionShortInfoHandler implements DocumentListener, EditorMou private final Editor editor; @Nullable private final Consumer myCurrentVariableCallback; public static final Key CURRENT_CONFIGURATION_KEY = Key.create("SS.CurrentConfiguration"); + private final Map> inlays = new HashMap<>(); - SubstitutionShortInfoHandler(@NotNull Editor _editor, @Nullable Consumer currentVariableCallback) { + private SubstitutionShortInfoHandler(@NotNull Editor _editor, @Nullable Consumer currentVariableCallback) { editor = _editor; myCurrentVariableCallback = currentVariableCallback; } @@ -98,7 +106,7 @@ public class SubstitutionShortInfoHandler implements DocumentListener, EditorMou myCurrentVariableCallback.accept(Configuration.CONTEXT_VAR_NAME); } - if (variableName != null) { + if (variableName != null && !text.isEmpty()) { showTooltip(editor, start, end + 1, text); } } @@ -109,6 +117,7 @@ public class SubstitutionShortInfoHandler implements DocumentListener, EditorMou variables.clear(); variables.addAll(TemplateImplUtil.parseVariables(document.getCharsSequence()).keySet()); modificationTimeStamp = document.getModificationStamp(); + updateEditorInlays(); } } @@ -122,6 +131,7 @@ public class SubstitutionShortInfoHandler implements DocumentListener, EditorMou if (event.getOldLength() == event.getNewLength()) return; // to handle backspace & delete (backspace strangely is not reported to the caret listener) handleInputFocusMovement(editor.getCaretModel().getLogicalPosition(), true); + updateEditorInlays(); } public List getVariables() { @@ -131,9 +141,9 @@ public class SubstitutionShortInfoHandler implements DocumentListener, EditorMou @NotNull static String getShortParamString(NamedScriptableDefinition namedScriptableDefinition, boolean editLink) { - final boolean newDialog = Registry.is("ssr.use.new.search.dialog"); + final boolean verbose = !Registry.is("ssr.use.new.search.dialog"); if (namedScriptableDefinition == null) { - return SSRBundle.message(newDialog ? "no.filters.tooltip.message" : "no.constraints.specified.tooltip.message"); + return verbose ? SSRBundle.message("no.constraints.specified.tooltip.message") : ""; } final StringBuilder buf = new StringBuilder(); @@ -142,10 +152,18 @@ public class SubstitutionShortInfoHandler implements DocumentListener, EditorMou final String linkColor = ColorUtil.toHtmlColor(JBUI.CurrentTheme.Link.linkColor()); if (namedScriptableDefinition instanceof MatchVariableConstraint) { final MatchVariableConstraint constraint = (MatchVariableConstraint)namedScriptableDefinition; - if (constraint.isPartOfSearchResults() && !newDialog) { + final String name = constraint.getName(); + if (!Configuration.CONTEXT_VAR_NAME.equals(name)) { + final int maxCount = constraint.getMaxCount(); + final int minCount = constraint.getMinCount(); + if (verbose || minCount != 1 || maxCount != 1) { + append(buf, SSRBundle.message("min.occurs.tooltip.message", minCount, (maxCount == Integer.MAX_VALUE) ? "∞" : maxCount)); + } + } + if (constraint.isPartOfSearchResults() && verbose) { append(buf, SSRBundle.message("target.tooltip.message")); } - if (constraint.getRegExp() != null && !constraint.getRegExp().isEmpty()) { + if (!constraint.getRegExp().isEmpty()) { append(buf, SSRBundle.message("text.tooltip.message", constraint.isInvertRegExp() ? 1 : 0, StringUtil.escapeXmlEntities(constraint.getRegExp()), @@ -161,7 +179,6 @@ public class SubstitutionShortInfoHandler implements DocumentListener, EditorMou append(buf, SSRBundle.message("reference.target.tooltip.message", constraint.isInvertReference() ? 1 : 0, text)); } - constraint.getNameOfExprType(); if (!constraint.getNameOfExprType().isEmpty()) { append(buf, SSRBundle.message("exprtype.tooltip.message", constraint.isInvertExprType() ? 1 : 0, @@ -183,27 +200,17 @@ public class SubstitutionShortInfoHandler implements DocumentListener, EditorMou final String text = StringUtil.escapeXmlEntities(StringUtil.unquoteString(constraint.getWithinConstraint())); append(buf, SSRBundle.message("within.constraints.tooltip.message", constraint.isInvertWithinConstraint() ? 1 : 0, text)); } - - final String name = constraint.getName(); - if (!Configuration.CONTEXT_VAR_NAME.equals(name)) { - final int maxCount = constraint.getMaxCount(); - final int minCount = constraint.getMinCount(); - if (!newDialog || minCount != 1 || maxCount != 1) { - append(buf, SSRBundle.message("min.occurs.tooltip.message", minCount, (maxCount == Integer.MAX_VALUE) ? "∞" : maxCount)); - } - } } final String script = namedScriptableDefinition.getScriptCodeConstraint(); if (script != null && script.length() > 2) { - final String text = "
" + StringUtil.escapeXmlEntities(StringUtil.unquoteString(script)) + "
"; - append(buf, SSRBundle.message("script.tooltip.message", text)); + append(buf, SSRBundle.message("script.tooltip.message")); } - if (buf.length() == 0 && !editLink) { - buf.append(SSRBundle.message(!newDialog ? "no.constraints.specified.tooltip.message" : "no.filters.tooltip.message")); + if (buf.length() == 0 && !editLink && verbose) { + buf.append(SSRBundle.message("no.constraints.specified.tooltip.message")); } - if (editLink && newDialog) { + if (editLink && !verbose && !Registry.is("ssr.use.editor.inlays.instead.of.tool.tips")) { if (buf.length() > 0) buf.append("
"); buf.append(" currentVariableCallback) { @@ -254,4 +264,101 @@ public class SubstitutionShortInfoHandler implements DocumentListener, EditorMou editor.getCaretModel().addCaretListener(handler); editor.putUserData(LISTENER_KEY, handler); } + + void updateEditorInlays() { + if (!Registry.is("ssr.use.editor.inlays.instead.of.tool.tips") || !Registry.is("ssr.use.new.search.dialog")) { + return; + } + final String text = editor.getDocument().getText(); + final Template template = TemplateManager.getInstance(editor.getProject()).createTemplate("", "", text); + final int segmentsCount = template.getSegmentsCount(); + final InlayModel inlayModel = editor.getInlayModel(); + final HashSet variables = new HashSet<>(inlays.keySet()); + final Configuration configuration = editor.getUserData(CURRENT_CONFIGURATION_KEY); + if (configuration == null) return; + int variableNameLength = 0; + for (int i = 0; i < segmentsCount; i++) { + final int offset = template.getSegmentOffset(i); + final String name = template.getSegmentName(i); + variableNameLength += name.length() + 2; + final NamedScriptableDefinition variable = configuration.findVariable(name); + final String labelText = getShortParamString(variable, false); + if (labelText.isEmpty()) { + continue; + } + final Inlay inlay = inlays.get(name); + if (inlay == null) { + inlays.put(name, inlayModel.addInlineElement(offset + variableNameLength, new FilterRenderer(labelText))); + } + else { + final FilterRenderer renderer = inlay.getRenderer(); + renderer.setText(labelText); + inlay.updateSize(); + variables.remove(name); + } + } + final Inlay inlay = inlays.get(Configuration.CONTEXT_VAR_NAME); + if (inlay == null) { + final NamedScriptableDefinition variable = configuration.findVariable(Configuration.CONTEXT_VAR_NAME); + final String labelText = getShortParamString(variable, false); + if (!labelText.isEmpty()) { + inlays.put(Configuration.CONTEXT_VAR_NAME, + inlayModel.addBlockElement(text.length() + variableNameLength, true, false, 0, + new FilterRenderer("complete pattern: " + labelText))); + } + } + for (String variable : variables) { + Disposer.dispose(inlays.remove(variable)); + } + } + + private static class FilterRenderer implements EditorCustomElementRenderer { + + private String myText; + + FilterRenderer(String text) { + myText = text; + } + + public void setText(String text) { + myText = text; + } + + @Override + public int calcWidthInPixels(@NotNull Editor editor) { + return getFontMetrics(editor).stringWidth(myText) + 12; + } + + private static Font getFont() { + return UIManager.getFont("Label.font"); + } + + private static FontMetrics getFontMetrics(Editor editor) { + return editor.getContentComponent().getFontMetrics(getFont()) ; + } + + @Override + public void paint(@NotNull Inlay inlay, @NotNull Graphics g, @NotNull Rectangle r, @NotNull TextAttributes textAttributes) { + final Editor editor = inlay.getEditor(); + final TextAttributes attributes = editor.getColorsScheme().getAttributes(DefaultLanguageHighlighterColors.INLINE_PARAMETER_HINT); + if (attributes == null) { + return; + } + final FontMetrics metrics = getFontMetrics(editor); + final Color backgroundColor = attributes.getBackgroundColor(); + if (backgroundColor != null) { + final GraphicsConfig config = GraphicsUtil.setupAAPainting(g); + GraphicsUtil.paintWithAlpha(g, 0.55f); + g.setColor(backgroundColor); + g.fillRoundRect(r.x + 2, r.y, r.width - 4, r.height, 8, 8); + config.restore(); + } + final Color foregroundColor = attributes.getForegroundColor(); + if (foregroundColor != null) { + g.setColor(foregroundColor); + g.setFont(getFont()); + g.drawString(myText, r.x + 6, r.y + r.height - metrics.getDescent()); + } + } + } } diff --git a/platform/structuralsearch/source/com/intellij/structuralsearch/plugin/ui/UIUtil.java b/platform/structuralsearch/source/com/intellij/structuralsearch/plugin/ui/UIUtil.java index 01071214b791..8127ff1dc230 100644 --- a/platform/structuralsearch/source/com/intellij/structuralsearch/plugin/ui/UIUtil.java +++ b/platform/structuralsearch/source/com/intellij/structuralsearch/plugin/ui/UIUtil.java @@ -25,6 +25,7 @@ import com.intellij.openapi.fileTypes.FileTypes; import com.intellij.openapi.fileTypes.LanguageFileType; import com.intellij.openapi.project.Project; import com.intellij.openapi.ui.popup.Balloon; +import com.intellij.openapi.util.registry.Registry; import com.intellij.openapi.wm.ToolWindowId; import com.intellij.psi.PsiDocumentManager; import com.intellij.psi.PsiElement; @@ -197,6 +198,9 @@ public class UIUtil { completeMatchInfo.addMouseListener(new MouseAdapter() { @Override public void mouseEntered(MouseEvent ignore) { + if (Registry.is("ssr.use.editor.inlays.instead.of.tool.tips") && Registry.is("ssr.use.new.search.dialog")) { + return; + } final Configuration configuration = configurationProducer.get(); if (configuration == null) { return; diff --git a/platform/structuralsearch/source/messages/SSRBundle.properties b/platform/structuralsearch/source/messages/SSRBundle.properties index f7fee41407ff..aae113888fdd 100644 --- a/platform/structuralsearch/source/messages/SSRBundle.properties +++ b/platform/structuralsearch/source/messages/SSRBundle.properties @@ -187,14 +187,13 @@ editvarcontraints.edit.variables=Edit Variables no.constraints.specified.tooltip.message=no constraints specified no.filters.tooltip.message=no filters script.option.text=Script text\: -occurs.tooltip.message=occurs: {0} -min.occurs.tooltip.message=count=[{0},{1}] +min.occurs.tooltip.message=[{0},{1}] target.tooltip.message=target text.tooltip.message=text{0,choice,0#=|1#\u2260}{1}{2,choice,0#|1#', whole words'}{3,choice,0#|1#', within hierarchy'} -hierarchy.tooltip.message=search within hierarchy +hierarchy.tooltip.message=within hierarchy exprtype.tooltip.message=type{0,choice,0#=|1#\u2260}{1}{2,choice,0#|1#', within hierarchy'} expected.type.tooltip.message=expected type{0,choice,0#=|1#\u2260}{1}{2,choice,0#|1#', within hierarchy'} -script.tooltip.message=script=
{0} +script.tooltip.message=script within.constraints.tooltip.message=within{0,choice,0#=|1#\u2260}{1} reference.target.tooltip.message=reference{0,choice,0#=|1#\u2260}{1} complete.match.variable.tooltip.message=Complete Match: {0} diff --git a/platform/util/resources/misc/registry.properties b/platform/util/resources/misc/registry.properties index 74467756f8fe..0e918cb5e037 100644 --- a/platform/util/resources/misc/registry.properties +++ b/platform/util/resources/misc/registry.properties @@ -1544,6 +1544,8 @@ ssr.template.from.selection.builder=false ssr.template.from.selection.builder.description=Allows to build template with live-template-like placeholders when invoking SSR from editor with selection ssr.save.templates.to.ide.instead.of.project.workspace=true ssr.save.templates.to.ide.instead.of.project.workspace.description=Makes saved Structural Search templates globally available for all projects +ssr.use.editor.inlays.instead.of.tool.tips=true +ssr.use.editor.inlays.instead.of.tool.tips.description=Replaces filter tool tips with editor inlays in the new Structural Search dialog jdk.regex.soe.workaround=true jdk.regex.soe.workaround.description=In regular expression pattern replace choice \\n|. with . (and DOT_ALL option) to prevent stack overflow during matching