SSR: show filters in editor inlays instead of tool tips (IDEA-210453)

GitOrigin-RevId: 34555e014ed4c698b814966333dff0d740506bab
This commit is contained in:
Bas Leijdekkers
2019-06-08 19:14:28 +03:00
committed by intellij-monorepo-bot
parent d7b1667b34
commit 0899b035e7
5 changed files with 155 additions and 37 deletions
@@ -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) {
@@ -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<? super String> myCurrentVariableCallback;
public static final Key<Configuration> CURRENT_CONFIGURATION_KEY = Key.create("SS.CurrentConfiguration");
private final Map<String, Inlay<FilterRenderer>> inlays = new HashMap<>();
SubstitutionShortInfoHandler(@NotNull Editor _editor, @Nullable Consumer<? super String> currentVariableCallback) {
private SubstitutionShortInfoHandler(@NotNull Editor _editor, @Nullable Consumer<? super String> 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<String> 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 = "<pre><code>" + StringUtil.escapeXmlEntities(StringUtil.unquoteString(script)) + "</code></pre>";
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("<br>");
buf.append("<a style=\"color:")
.append(linkColor)
@@ -220,6 +227,9 @@ public class SubstitutionShortInfoHandler implements DocumentListener, EditorMou
}
private static void showTooltip(@NotNull Editor editor, final int start, int end, @NotNull String text) {
if (Registry.is("ssr.use.editor.inlays.instead.of.tool.tips") && Registry.is("ssr.use.new.search.dialog")) {
return;
}
final Rectangle visibleArea = editor.getScrollingModel().getVisibleArea();
final Point left = editor.logicalPositionToXY(editor.offsetToLogicalPosition(start));
final int documentLength = editor.getDocument().getTextLength();
@@ -244,7 +254,7 @@ public class SubstitutionShortInfoHandler implements DocumentListener, EditorMou
}
static SubstitutionShortInfoHandler retrieve(Editor editor) {
return editor.getUserData(LISTENER_KEY);
return editor == null ? null : editor.getUserData(LISTENER_KEY);
}
static void install(Editor editor, @Nullable Consumer<? super String> 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<String> 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<FilterRenderer> 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<FilterRenderer> 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());
}
}
}
}
@@ -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;
@@ -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#'<span style="color:{4}">, whole words</span>'}{3,choice,0#|1#'<span style="color:{4}">, within hierarchy</span>'}
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#'<span style="color:{3}">, within hierarchy</span>'}
expected.type.tooltip.message=expected type{0,choice,0#=|1#\u2260}{1}{2,choice,0#|1#'<span style="color:{3}">, within hierarchy</span>'}
script.tooltip.message=script=<br> {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=<html>Complete Match: {0}
@@ -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