blob: a0df30abae1cfc2d7e5fbe093e943a82d3ce913c [file] [log] [blame]
/*
* Copyright 2000-2014 JetBrains s.r.o.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.intellij.ide.ui.search;
import com.intellij.application.options.SkipSelfSearchComponent;
import com.intellij.ide.actions.ShowSettingsUtilImpl;
import com.intellij.openapi.options.Configurable;
import com.intellij.openapi.options.ConfigurableGroup;
import com.intellij.openapi.options.MasterDetails;
import com.intellij.openapi.options.SearchableConfigurable;
import com.intellij.openapi.options.ex.GlassPanel;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.ui.popup.JBPopup;
import com.intellij.openapi.ui.popup.JBPopupFactory;
import com.intellij.openapi.util.Condition;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.ui.*;
import com.intellij.ui.components.JBList;
import com.intellij.util.Alarm;
import com.intellij.util.Consumer;
import com.intellij.util.containers.ContainerUtil;
import org.jetbrains.annotations.NonNls;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import javax.swing.*;
import javax.swing.border.Border;
import javax.swing.border.TitledBorder;
import java.awt.*;
import java.awt.event.KeyAdapter;
import java.awt.event.KeyEvent;
import java.util.*;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* User: anna
* Date: 07-Feb-2006
*/
public class SearchUtil {
private static final Pattern HTML_PATTERN = Pattern.compile("<[^<>]*>");
private static final Pattern QUOTED = Pattern.compile("\\\"([^\\\"]+)\\\"");
public static final String HIGHLIGHT_WITH_BORDER = "searchUtil.highlightWithBorder";
public static final String STYLE_END = "</style>";
private SearchUtil() {
}
public static void processProjectConfigurables(Project project, HashMap<SearchableConfigurable, TreeSet<OptionDescription>> options) {
processConfigurables(ShowSettingsUtilImpl.getConfigurables(project, false), options);
}
private static void processConfigurables(final Configurable[] configurables,
final HashMap<SearchableConfigurable, TreeSet<OptionDescription>> options) {
for (Configurable configurable : configurables) {
if (configurable instanceof SearchableConfigurable) {
TreeSet<OptionDescription> configurableOptions = new TreeSet<OptionDescription>();
if (configurable instanceof Configurable.Composite) {
final Configurable[] children = ((Configurable.Composite)configurable).getConfigurables();
processConfigurables(children, options);
}
//ignore invisible root nodes
if (configurable instanceof SearchableConfigurable.Parent && !((SearchableConfigurable.Parent)configurable).isVisible()) {
continue;
}
options.put((SearchableConfigurable)configurable, configurableOptions);
if (configurable instanceof MasterDetails) {
final MasterDetails md = (MasterDetails)configurable;
md.initUi();
_processComponent(configurable, configurableOptions, md.getMaster());
_processComponent(configurable, configurableOptions, md.getDetails().getComponent());
}
else {
_processComponent(configurable, configurableOptions, configurable.createComponent());
}
}
}
}
private static void _processComponent(final Configurable configurable, final TreeSet<OptionDescription> configurableOptions,
final JComponent component) {
if (component == null) return;
processUILabel(configurable.getDisplayName(), configurableOptions, null);
processComponent(component, configurableOptions, null);
}
public static void processComponent(final JComponent component, final Set<OptionDescription> configurableOptions, @NonNls String path) {
if (component instanceof SkipSelfSearchComponent) return;
final Border border = component.getBorder();
if (border instanceof TitledBorder) {
final TitledBorder titledBorder = (TitledBorder)border;
final String title = titledBorder.getTitle();
if (title != null) {
processUILabel(title, configurableOptions, path);
}
}
if (component instanceof JLabel) {
final String label = ((JLabel)component).getText();
if (label != null) {
processUILabel(label, configurableOptions, path);
}
}
else if (component instanceof JCheckBox) {
@NonNls final String checkBoxTitle = ((JCheckBox)component).getText();
if (checkBoxTitle != null) {
processUILabel(checkBoxTitle, configurableOptions, path);
}
}
else if (component instanceof JRadioButton) {
@NonNls final String radioButtonTitle = ((JRadioButton)component).getText();
if (radioButtonTitle != null) {
processUILabel(radioButtonTitle, configurableOptions, path);
}
}
else if (component instanceof JButton) {
@NonNls final String buttonTitle = ((JButton)component).getText();
if (buttonTitle != null) {
processUILabel(buttonTitle, configurableOptions, path);
}
}
if (component instanceof JTabbedPane) {
final JTabbedPane tabbedPane = (JTabbedPane)component;
final int tabCount = tabbedPane.getTabCount();
for (int i = 0; i < tabCount; i++) {
final String title = path != null ? path + '.' + tabbedPane.getTitleAt(i) : tabbedPane.getTitleAt(i);
processUILabel(title, configurableOptions, title);
final Component tabComponent = tabbedPane.getComponentAt(i);
if (tabComponent instanceof JComponent) {
processComponent((JComponent)tabComponent, configurableOptions, title);
}
}
}
else {
final Component[] components = component.getComponents();
if (components != null) {
for (Component child : components) {
if (child instanceof JComponent) {
processComponent((JComponent)child, configurableOptions, path);
}
}
}
}
}
private static void processUILabel(@NonNls final String title, final Set<OptionDescription> configurableOptions, String path) {
final Set<String> words = SearchableOptionsRegistrar.getInstance().getProcessedWordsWithoutStemming(title);
@NonNls final String regex = "[\\W&&[^\\p{Punct}\\p{Blank}]]";
for (String option : words) {
configurableOptions.add(new OptionDescription(option, HTML_PATTERN.matcher(title).replaceAll(" ").replaceAll(regex, " "), path));
}
}
public static Runnable lightOptions(final SearchableConfigurable configurable,
final JComponent component,
final String option,
final GlassPanel glassPanel) {
return new Runnable() {
public void run() {
if (!traverseComponentsTree(configurable, glassPanel, component, option, true)) {
traverseComponentsTree(configurable, glassPanel, component, option, false);
}
}
};
}
private static int getSelection(String tabIdx, final JTabbedPane tabbedPane) {
SearchableOptionsRegistrar searchableOptionsRegistrar = SearchableOptionsRegistrar.getInstance();
for (int i = 0; i < tabbedPane.getTabCount(); i++) {
final Set<String> pathWords = searchableOptionsRegistrar.getProcessedWords(tabIdx);
final String title = tabbedPane.getTitleAt(i);
if (!pathWords.isEmpty()) {
final Set<String> titleWords = searchableOptionsRegistrar.getProcessedWords(title);
pathWords.removeAll(titleWords);
if (pathWords.isEmpty()) return i;
} else if (tabIdx.equalsIgnoreCase(title)) { //e.g. only stop words
return i;
}
}
return -1;
}
public static int getSelection(String tabIdx, final TabbedPaneWrapper tabbedPane) {
SearchableOptionsRegistrar searchableOptionsRegistrar = SearchableOptionsRegistrar.getInstance();
for (int i = 0; i < tabbedPane.getTabCount(); i++) {
final Set<String> pathWords = searchableOptionsRegistrar.getProcessedWords(tabIdx);
final String title = tabbedPane.getTitleAt(i);
final Set<String> titleWords = searchableOptionsRegistrar.getProcessedWords(title);
pathWords.removeAll(titleWords);
if (pathWords.isEmpty()) return i;
}
return -1;
}
private static boolean traverseComponentsTree(final SearchableConfigurable configurable,
GlassPanel glassPanel,
JComponent rootComponent,
String option,
boolean force) {
rootComponent.putClientProperty(HIGHLIGHT_WITH_BORDER, null);
if (option == null || option.trim().length() == 0) return false;
boolean highlight = false;
if (rootComponent instanceof JCheckBox) {
final JCheckBox checkBox = ((JCheckBox)rootComponent);
if (isComponentHighlighted(checkBox.getText(), option, force, configurable)) {
highlight = true;
glassPanel.addSpotlight(checkBox);
}
}
else if (rootComponent instanceof JRadioButton) {
final JRadioButton radioButton = ((JRadioButton)rootComponent);
if (isComponentHighlighted(radioButton.getText(), option, force, configurable)) {
highlight = true;
glassPanel.addSpotlight(radioButton);
}
}
else if (rootComponent instanceof JLabel) {
final JLabel label = ((JLabel)rootComponent);
if (isComponentHighlighted(label.getText(), option, force, configurable)) {
highlight = true;
glassPanel.addSpotlight(label);
}
}
else if (rootComponent instanceof JButton) {
final JButton button = ((JButton)rootComponent);
if (isComponentHighlighted(button.getText(), option, force, configurable)) {
highlight = true;
glassPanel.addSpotlight(button);
}
}
else if (rootComponent instanceof JTabbedPane) {
final JTabbedPane tabbedPane = (JTabbedPane)rootComponent;
final String path = SearchableOptionsRegistrar.getInstance().getInnerPath(configurable, option);
if (path != null) {
final int index = getSelection(path, tabbedPane);
if (index > -1 && index < tabbedPane.getTabCount()) {
if (tabbedPane.getTabComponentAt(index) instanceof JComponent) {
glassPanel.addSpotlight((JComponent)tabbedPane.getTabComponentAt(index));
}
}
}
}
final Component[] components = rootComponent.getComponents();
for (Component component : components) {
if (component instanceof JComponent) {
final boolean innerHighlight = traverseComponentsTree(configurable, glassPanel, (JComponent)component, option, force);
if (!highlight && !innerHighlight) {
final Border border = rootComponent.getBorder();
if (border instanceof TitledBorder) {
final String title = ((TitledBorder)border).getTitle();
if (isComponentHighlighted(title, option, force, configurable)) {
highlight = true;
glassPanel.addSpotlight(rootComponent);
rootComponent.putClientProperty(HIGHLIGHT_WITH_BORDER, Boolean.TRUE);
}
}
}
if (innerHighlight) {
highlight = true;
}
}
}
return highlight;
}
public static boolean isComponentHighlighted(String text, String option, final boolean force, final SearchableConfigurable configurable) {
if (text == null || option == null || option.length() == 0) return false;
final SearchableOptionsRegistrar searchableOptionsRegistrar = SearchableOptionsRegistrar.getInstance();
final Set<String> words = searchableOptionsRegistrar.getProcessedWords(option);
final Set<String> options =
configurable != null ? searchableOptionsRegistrar.replaceSynonyms(words, configurable) : words;
if (options == null || options.isEmpty()) {
return text.toLowerCase().indexOf(option.toLowerCase()) != -1;
}
final Set<String> tokens = searchableOptionsRegistrar.getProcessedWords(text);
if (!force) {
options.retainAll(tokens);
final boolean highlight = !options.isEmpty();
return highlight || text.toLowerCase().indexOf(option.toLowerCase()) != -1;
}
else {
options.removeAll(tokens);
return options.isEmpty();
}
}
public static String markup(@NonNls @NotNull String textToMarkup, @Nullable String filter) {
if (filter == null || filter.length() == 0) {
return textToMarkup;
}
int bodyStart = textToMarkup.indexOf("<body>");
final int bodyEnd = textToMarkup.indexOf("</body>");
final String head;
final String foot;
if (bodyStart >= 0) {
bodyStart += "<body>".length();
head = textToMarkup.substring(0, bodyStart);
if (bodyEnd >= 0) {
foot = textToMarkup.substring(bodyEnd);
} else {
foot = "";
}
textToMarkup = textToMarkup.substring(bodyStart, bodyEnd);
} else {
foot = "";
head = "";
}
final Pattern insideHtmlTagPattern = Pattern.compile("[<[^<>]*>]*<[^<>]*");
final SearchableOptionsRegistrar registrar = SearchableOptionsRegistrar.getInstance();
final HashSet<String> quoted = new HashSet<String>();
filter = processFilter(quoteStrictOccurrences(textToMarkup, filter), quoted);
final Set<String> options = registrar.getProcessedWords(filter);
final Set<String> words = registrar.getProcessedWords(textToMarkup);
for (String option : options) {
if (words.contains(option)) {
textToMarkup = markup(textToMarkup, insideHtmlTagPattern, option);
}
}
for (String stripped : quoted) {
textToMarkup = markup(textToMarkup, insideHtmlTagPattern, stripped);
}
return head + textToMarkup + foot;
}
private static String quoteStrictOccurrences(final String textToMarkup, final String filter) {
String cur = "";
final String s = textToMarkup.toLowerCase();
for (String part : filter.split(" ")) {
if (s.contains(part)) {
cur += "\"" + part + "\" ";
}
else {
cur += part + " ";
}
}
return cur;
}
private static String markup(@NonNls String textToMarkup, final Pattern insideHtmlTagPattern, final String option) {
@NonNls String result = "";
final int styleIdx = textToMarkup.indexOf("<style");
final int styleEndIdx = textToMarkup.indexOf("</style>");
if (styleIdx < 0 || styleEndIdx < 0) {
result = markupInText(textToMarkup, insideHtmlTagPattern, option);
} else {
result = markup(textToMarkup.substring(0, styleIdx), insideHtmlTagPattern, option) + markup(textToMarkup.substring(styleEndIdx + STYLE_END.length()), insideHtmlTagPattern, option);
}
return result;
}
private static String markupInText(String textToMarkup, Pattern insideHtmlTagPattern, String option) {
String result = "";
int beg = 0;
int idx;
while ((idx = StringUtil.indexOfIgnoreCase(textToMarkup, option, beg)) != -1) {
final String prefix = textToMarkup.substring(beg, idx);
final String toMark = textToMarkup.substring(idx, idx + option.length());
if (insideHtmlTagPattern.matcher(prefix).matches()) {
final int lastIdx = textToMarkup.indexOf(">", idx);
result += prefix + textToMarkup.substring(idx, lastIdx + 1);
beg = lastIdx + 1;
}
else {
result += prefix + "<font color='#ffffff' bgColor='#1d5da7'>" + toMark + "</font>";
beg = idx + option.length();
}
}
result += textToMarkup.substring(beg);
return result;
}
public static void appendFragments(String filter,
@NonNls String text,
@SimpleTextAttributes.StyleAttributeConstant int style,
final Color foreground,
final Color background,
final SimpleColoredComponent textRenderer) {
if (text == null) return;
if (filter == null || filter.length() == 0) {
textRenderer.append(text, new SimpleTextAttributes(background, foreground, JBColor.RED, style));
}
else { //markup
final HashSet<String> quoted = new HashSet<String>();
filter = processFilter(quoteStrictOccurrences(text, filter), quoted);
final TreeMap<Integer, String> indx = new TreeMap<Integer, String>();
for (String stripped : quoted) {
int beg = 0;
int idx;
while ((idx = StringUtil.indexOfIgnoreCase(text, stripped, beg)) != -1) {
indx.put(idx, text.substring(idx, idx + stripped.length()));
beg = idx + stripped.length();
}
}
final List<String> selectedWords = new ArrayList<String>();
int pos = 0;
for (Integer index : indx.keySet()) {
final String stripped = indx.get(index);
final int start = index.intValue();
if (pos > start) {
final String highlighted = selectedWords.get(selectedWords.size() - 1);
if (highlighted.length() < stripped.length()){
selectedWords.remove(highlighted);
} else {
continue;
}
}
appendSelectedWords(text, selectedWords, pos, start, filter);
selectedWords.add(stripped);
pos = start + stripped.length();
}
appendSelectedWords(text, selectedWords, pos, text.length(), filter);
int idx = 0;
for (String word : selectedWords) {
text = text.substring(idx);
final String before = text.substring(0, text.indexOf(word));
if (before.length() > 0) textRenderer.append(before, new SimpleTextAttributes(background, foreground, null, style));
idx = text.indexOf(word) + word.length();
textRenderer.append(text.substring(idx - word.length(), idx), new SimpleTextAttributes(background,
foreground, null,
style |
SimpleTextAttributes.STYLE_SEARCH_MATCH));
}
final String after = text.substring(idx, text.length());
if (after.length() > 0) textRenderer.append(after, new SimpleTextAttributes(background, foreground, null, style));
}
}
private static void appendSelectedWords(final String text,
final List<String> selectedWords,
final int pos,
int end,
final String filter) {
if (pos < end) {
final Set<String> filters = SearchableOptionsRegistrar.getInstance().getProcessedWords(filter);
final String[] words = text.substring(pos, end).split("[\\W&&[^-]]+");
for (String word : words) {
if (filters.contains(PorterStemmerUtil.stem(word.toLowerCase()))) {
selectedWords.add(word);
}
}
}
}
@Nullable
private static JBPopup createPopup(final ConfigurableSearchTextField searchField,
final JBPopup[] activePopup,
final Alarm showHintAlarm,
final Consumer<String> selectConfigurable,
final Project project,
final int down) {
final String filter = searchField.getText();
if (filter == null || filter.length() == 0) return null;
final Map<String, Set<String>> hints = SearchableOptionsRegistrar.getInstance().findPossibleExtension(filter, project);
final DefaultListModel model = new DefaultListModel();
final JList list = new JBList(model);
for (String groupName : hints.keySet()) {
model.addElement(groupName);
final Set<String> descriptions = hints.get(groupName);
if (descriptions != null) {
for (String hit : descriptions) {
if (hit == null) continue;
model.addElement(new OptionDescription(null, groupName, hit, null));
}
}
}
list.setCellRenderer(new DefaultListCellRenderer() {
public Component getListCellRendererComponent(JList list, Object value, int index, boolean isSelected, boolean cellHasFocus) {
final Component rendererComponent = super.getListCellRendererComponent(list, value, index, isSelected, cellHasFocus);
if (value instanceof String) {
setText("------ " + value + " ------");
}
else if (value instanceof OptionDescription) {
setText(((OptionDescription)value).getHit());
}
return rendererComponent;
}
});
if (model.size() > 0) {
final Runnable onChosen = new Runnable() {
public void run() {
final Object selectedValue = list.getSelectedValue();
if (selectedValue instanceof OptionDescription) {
final OptionDescription description = ((OptionDescription)selectedValue);
searchField.setText(description.getHit());
searchField.addCurrentTextToHistory();
SwingUtilities.invokeLater(new Runnable() {
public void run() { //do not show look up again
showHintAlarm.cancelAllRequests();
selectConfigurable.consume(description.getConfigurableId());
}
});
}
}
};
final JBPopup popup = JBPopupFactory.getInstance()
.createListPopupBuilder(list)
.setItemChoosenCallback(onChosen)
.setRequestFocus(down != 0)
.createPopup();
list.addKeyListener(new KeyAdapter() {
public void keyPressed(final KeyEvent e) {
if (e.getKeyCode() != KeyEvent.VK_ENTER && e.getKeyCode() != KeyEvent.VK_UP && e.getKeyCode() != KeyEvent.VK_DOWN &&
e.getKeyCode() != KeyEvent.VK_PAGE_UP && e.getKeyCode() != KeyEvent.VK_PAGE_DOWN) {
searchField.requestFocusInWindow();
if (cancelPopups(activePopup) && e.getKeyCode() == KeyEvent.VK_ESCAPE) {
return;
}
if (e.getKeyChar() != KeyEvent.CHAR_UNDEFINED) {
searchField.process(
new KeyEvent(searchField, KeyEvent.KEY_TYPED, e.getWhen(), e.getModifiers(), KeyEvent.VK_UNDEFINED, e.getKeyChar()));
}
}
}
});
if (down > 0) {
if (list.getSelectedIndex() < list.getModel().getSize() - 1) {
list.setSelectedIndex(list.getSelectedIndex() + 1);
}
}
else if (down < 0) {
if (list.getSelectedIndex() > 0) {
list.setSelectedIndex(list.getSelectedIndex() - 1);
}
}
return popup;
}
return null;
}
private static boolean cancelPopups(final JBPopup[] activePopup) {
for (JBPopup popup : activePopup) {
if (popup != null && popup.isVisible()) {
popup.cancel();
return true;
}
}
return false;
}
public static List<Set<String>> findKeys(String filter, Set<String> quoted) {
filter = processFilter(filter.toLowerCase(), quoted);
final List<Set<String>> keySetList = new ArrayList<Set<String>>();
final SearchableOptionsRegistrar optionsRegistrar = SearchableOptionsRegistrar.getInstance();
final Set<String> words = optionsRegistrar.getProcessedWords(filter);
for (String word : words) {
final Set<OptionDescription> descriptions = ((SearchableOptionsRegistrarImpl)optionsRegistrar).getAcceptableDescriptions(word);
Set<String> keySet = new HashSet<String>();
if (descriptions != null) {
for (OptionDescription description : descriptions) {
keySet.add(description.getPath());
}
}
keySetList.add(keySet);
}
if (keySetList.isEmpty() && !StringUtil.isEmptyOrSpaces(filter)) {
keySetList.add(Collections.singleton(filter));
}
return keySetList;
}
public static String processFilter(String filter, Set<String> quoted) {
String withoutQuoted = "";
int beg = 0;
final Matcher matcher = QUOTED.matcher(filter);
while (matcher.find()) {
final int start = matcher.start(1);
withoutQuoted += " " + filter.substring(beg, start);
beg = matcher.end(1);
final String trimmed = filter.substring(start, beg).trim();
if (trimmed.length() > 0) {
quoted.add(trimmed);
}
}
return withoutQuoted + " " + filter.substring(beg);
}
//to process event
public static class ConfigurableSearchTextField extends SearchTextFieldWithStoredHistory {
public ConfigurableSearchTextField() {
super("ALL_CONFIGURABLES_PANEL_SEARCH_HISTORY");
}
public void process(final KeyEvent e) {
((TextFieldWithProcessing)getTextEditor()).processKeyEvent(e);
}
}
public static List<Configurable> expand(ConfigurableGroup[] groups) {
final ArrayList<Configurable> result = new ArrayList<Configurable>();
for (ConfigurableGroup eachGroup : groups) {
result.addAll(expandGroup(eachGroup));
}
return result;
}
public static List<Configurable> expandGroup(final ConfigurableGroup group) {
final Configurable[] configurables = group.getConfigurables();
List<Configurable> result = new ArrayList<Configurable>();
ContainerUtil.addAll(result, configurables);
for (Configurable each : configurables) {
addChildren(each, result);
}
result = ContainerUtil.filter(result, new Condition<Configurable>() {
@Override
public boolean value(Configurable configurable) {
return !(configurable instanceof SearchableConfigurable.Parent) || ((SearchableConfigurable.Parent)configurable).isVisible();
}
});
return result;
}
private static void addChildren(Configurable configurable, List<Configurable> list) {
if (configurable instanceof Configurable.Composite) {
final Configurable[] kids = ((Configurable.Composite)configurable).getConfigurables();
for (Configurable eachKid : kids) {
list.add(eachKid);
addChildren(eachKid, list);
}
}
}
}