| /* |
| * 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.codeInsight.navigation; |
| |
| import com.intellij.codeInsight.CodeInsightBundle; |
| import com.intellij.codeInsight.hint.HintManagerImpl; |
| import com.intellij.codeInsight.hint.HintUtil; |
| import com.intellij.codeInsight.template.impl.editorActions.TypedActionHandlerBase; |
| import com.intellij.featureStatistics.FeatureUsageTracker; |
| import com.intellij.openapi.actionSystem.DataContext; |
| import com.intellij.openapi.actionSystem.IdeActions; |
| import com.intellij.openapi.diagnostic.Logger; |
| import com.intellij.openapi.editor.Caret; |
| import com.intellij.openapi.editor.Document; |
| import com.intellij.openapi.editor.Editor; |
| import com.intellij.openapi.editor.ScrollType; |
| import com.intellij.openapi.editor.actionSystem.EditorActionHandler; |
| import com.intellij.openapi.editor.actionSystem.EditorActionManager; |
| import com.intellij.openapi.editor.actionSystem.TypedAction; |
| import com.intellij.openapi.editor.actionSystem.TypedActionHandler; |
| import com.intellij.openapi.editor.colors.EditorColors; |
| import com.intellij.openapi.editor.event.*; |
| import com.intellij.openapi.editor.markup.HighlighterLayer; |
| import com.intellij.openapi.editor.markup.HighlighterTargetArea; |
| import com.intellij.openapi.editor.markup.RangeHighlighter; |
| import com.intellij.openapi.editor.markup.TextAttributes; |
| import com.intellij.openapi.fileEditor.ex.IdeDocumentHistory; |
| import com.intellij.openapi.project.Project; |
| import com.intellij.openapi.util.Key; |
| import com.intellij.ui.HintHint; |
| import com.intellij.ui.JBColor; |
| import com.intellij.ui.LightweightHint; |
| import com.intellij.util.text.StringSearcher; |
| import com.intellij.util.ui.UIUtil; |
| import org.jetbrains.annotations.NonNls; |
| import org.jetbrains.annotations.NotNull; |
| import org.jetbrains.annotations.Nullable; |
| |
| import javax.swing.*; |
| import java.awt.*; |
| import java.util.regex.Matcher; |
| import java.util.regex.Pattern; |
| import java.util.regex.PatternSyntaxException; |
| |
| public class IncrementalSearchHandler { |
| private static final Key<PerEditorSearchData> SEARCH_DATA_IN_EDITOR_VIEW_KEY = Key.create("IncrementalSearchHandler.SEARCH_DATA_IN_EDITOR_VIEW_KEY"); |
| private static final Key<PerHintSearchData> SEARCH_DATA_IN_HINT_KEY = Key.create("IncrementalSearchHandler.SEARCH_DATA_IN_HINT_KEY"); |
| private static final Logger LOG = Logger.getInstance("#com.intellij.codeInsight.navigation.IncrementalSearchHandler"); |
| |
| private static boolean ourActionsRegistered = false; |
| |
| private static class PerHintSearchData { |
| final Project project; |
| final JLabel label; |
| |
| int searchStart; |
| RangeHighlighter segmentHighlighter; |
| boolean ignoreCaretMove = false; |
| |
| public PerHintSearchData(Project project, JLabel label) { |
| this.project = project; |
| this.label = label; |
| } |
| } |
| |
| private static class PerEditorSearchData { |
| LightweightHint hint; |
| String lastSearch; |
| } |
| |
| public static boolean isHintVisible(final Editor editor) { |
| final PerEditorSearchData data = editor.getUserData(SEARCH_DATA_IN_EDITOR_VIEW_KEY); |
| return data != null && data.hint != null && data.hint.isVisible(); |
| } |
| |
| public void invoke(Project project, final Editor editor) { |
| if (!ourActionsRegistered){ |
| ourActionsRegistered = true; |
| |
| EditorActionManager actionManager = EditorActionManager.getInstance(); |
| |
| TypedAction typedAction = actionManager.getTypedAction(); |
| typedAction.setupHandler(new MyTypedHandler(typedAction.getHandler())); |
| |
| actionManager.setActionHandler(IdeActions.ACTION_EDITOR_BACKSPACE, new BackSpaceHandler(actionManager.getActionHandler(IdeActions.ACTION_EDITOR_BACKSPACE))); |
| actionManager.setActionHandler(IdeActions.ACTION_EDITOR_MOVE_CARET_UP, new UpHandler(actionManager.getActionHandler(IdeActions.ACTION_EDITOR_MOVE_CARET_UP))); |
| actionManager.setActionHandler(IdeActions.ACTION_EDITOR_MOVE_CARET_DOWN, new DownHandler(actionManager.getActionHandler(IdeActions.ACTION_EDITOR_MOVE_CARET_DOWN))); |
| } |
| |
| FeatureUsageTracker.getInstance().triggerFeatureUsed("editing.incremental.search"); |
| |
| String selection = editor.getSelectionModel().getSelectedText(); |
| JLabel label2 = new MyLabel(selection == null ? "" : selection); |
| |
| PerEditorSearchData data = editor.getUserData(SEARCH_DATA_IN_EDITOR_VIEW_KEY); |
| if (data == null) { |
| data = new PerEditorSearchData(); |
| } else { |
| if (data.hint != null) { |
| if (data.lastSearch != null) { |
| PerHintSearchData hintData = data.hint.getUserData(SEARCH_DATA_IN_HINT_KEY); |
| //The user has not started typing |
| if ("".equals(hintData.label.getText())) { |
| label2 = new MyLabel(data.lastSearch); |
| } |
| } |
| data.hint.hide(); |
| } |
| } |
| |
| JLabel label1 = new MyLabel(" " + CodeInsightBundle.message("incremental.search.tooltip.prefix")); |
| label1.setFont(UIUtil.getLabelFont().deriveFont(Font.BOLD)); |
| |
| JPanel panel = new MyPanel(label1); |
| panel.add(label1, BorderLayout.WEST); |
| panel.add(label2, BorderLayout.CENTER); |
| panel.setBorder(BorderFactory.createLineBorder(Color.black)); |
| |
| final DocumentListener[] documentListener = new DocumentListener[1]; |
| final CaretListener[] caretListener = new CaretListener[1]; |
| final Document document = editor.getDocument(); |
| |
| final LightweightHint hint = new LightweightHint(panel) { |
| @Override |
| public void hide() { |
| PerHintSearchData data = getUserData(SEARCH_DATA_IN_HINT_KEY); |
| LOG.assertTrue(data != null); |
| String prefix = data.label.getText(); |
| |
| super.hide(); |
| |
| if (data.segmentHighlighter != null){ |
| data.segmentHighlighter.dispose(); |
| } |
| PerEditorSearchData editorData = editor.getUserData(SEARCH_DATA_IN_EDITOR_VIEW_KEY); |
| editorData.hint = null; |
| editorData.lastSearch = prefix; |
| |
| if (documentListener[0] != null){ |
| document.removeDocumentListener(documentListener[0]); |
| } |
| |
| if (caretListener[0] != null){ |
| CaretListener listener = caretListener[0]; |
| editor.getCaretModel().removeCaretListener(listener); |
| } |
| } |
| }; |
| |
| documentListener[0] = new DocumentAdapter() { |
| @Override |
| public void documentChanged(DocumentEvent e) { |
| if (!hint.isVisible()) return; |
| hint.hide(); |
| } |
| }; |
| document.addDocumentListener(documentListener[0]); |
| |
| caretListener[0] = new CaretAdapter() { |
| @Override |
| public void caretPositionChanged(CaretEvent e) { |
| PerHintSearchData data = hint.getUserData(SEARCH_DATA_IN_HINT_KEY); |
| if (data != null && data.ignoreCaretMove) return; |
| if (!hint.isVisible()) return; |
| hint.hide(); |
| } |
| }; |
| CaretListener listener = caretListener[0]; |
| editor.getCaretModel().addCaretListener(listener); |
| |
| final JComponent component = editor.getComponent(); |
| int x = SwingUtilities.convertPoint(component,0,0,component).x; |
| int y = - hint.getComponent().getPreferredSize().height; |
| Point p = SwingUtilities.convertPoint(component,x,y,component.getRootPane().getLayeredPane()); |
| |
| HintManagerImpl.getInstanceImpl().showEditorHint(hint, editor, p, HintManagerImpl.HIDE_BY_ESCAPE | HintManagerImpl.HIDE_BY_TEXT_CHANGE, 0, false, new HintHint(editor, p).setAwtTooltip(false)); |
| |
| PerHintSearchData hintData = new PerHintSearchData(project, label2); |
| hintData.searchStart = editor.getCaretModel().getOffset(); |
| hint.putUserData(SEARCH_DATA_IN_HINT_KEY, hintData); |
| |
| data.hint = hint; |
| editor.putUserData(SEARCH_DATA_IN_EDITOR_VIEW_KEY, data); |
| |
| if (hintData.label.getText().length() > 0) { |
| updatePosition(editor, hintData, true, false); |
| } |
| } |
| |
| private static boolean acceptableRegExp(String pattern) { |
| final int len = pattern.length(); |
| |
| for(int i=0;i<len;++i) { |
| switch(pattern.charAt(i)) { |
| case '*': return true; |
| } |
| } |
| |
| return false; |
| } |
| |
| private static void updatePosition(Editor editor, PerHintSearchData data, boolean nothingIfFailed, boolean searchBack) { |
| final String prefix = data.label.getText(); |
| int matchLength = prefix.length(); |
| int index; |
| |
| if (matchLength == 0) { |
| index = data.searchStart; |
| } |
| else { |
| final Document document = editor.getDocument(); |
| final CharSequence text = document.getCharsSequence(); |
| final int length = document.getTextLength(); |
| final boolean caseSensitive = detectSmartCaseSensitive(prefix); |
| |
| if (acceptableRegExp(prefix)) { |
| @NonNls final StringBuffer buf = new StringBuffer(prefix.length()); |
| final int len = prefix.length(); |
| |
| for (int i = 0; i < len; ++i) { |
| final char ch = prefix.charAt(i); |
| |
| // bother only * withing text |
| if (ch == '*' && i != 0 && i != len - 1) { |
| buf.append("\\w"); |
| } |
| else if ("{}[].+^$*()?".indexOf(ch) != -1) { |
| // do not bother with other metachars |
| buf.append('\\'); |
| } |
| buf.append(ch); |
| } |
| |
| try { |
| Pattern pattern = Pattern.compile(buf.toString(), caseSensitive ? 0 : Pattern.CASE_INSENSITIVE); |
| Matcher matcher = pattern.matcher(text); |
| if (searchBack) { |
| int lastStart = -1; |
| int lastEnd = -1; |
| |
| while (matcher.find() && matcher.start() < data.searchStart) { |
| lastStart = matcher.start(); |
| lastEnd = matcher.end(); |
| } |
| |
| index = lastStart; |
| matchLength = lastEnd - lastStart; |
| } |
| else if (matcher.find(data.searchStart) || !nothingIfFailed && matcher.find(0)) { |
| index = matcher.start(); |
| matchLength = matcher.end() - matcher.start(); |
| } |
| else { |
| index = -1; |
| } |
| } |
| catch (PatternSyntaxException ex) { |
| index = -1; // let the user to make the garbage pattern |
| } |
| } |
| else { |
| StringSearcher searcher = new StringSearcher(prefix, caseSensitive, !searchBack); |
| |
| if (searchBack) { |
| index = searcher.scan(text, 0, data.searchStart); |
| } |
| else { |
| index = searcher.scan(text, data.searchStart, length); |
| index = index < 0 ? -1 : index; |
| } |
| if (index < 0 && !nothingIfFailed) { |
| index = searcher.scan(text); |
| } |
| } |
| } |
| |
| if (nothingIfFailed && index < 0) return; |
| if (data.segmentHighlighter != null) { |
| data.segmentHighlighter.dispose(); |
| data.segmentHighlighter = null; |
| } |
| if (index < 0) { |
| data.label.setForeground(JBColor.RED); |
| } |
| else { |
| data.label.setForeground(JBColor.foreground()); |
| if (matchLength > 0) { |
| TextAttributes attributes = editor.getColorsScheme().getAttributes(EditorColors.SEARCH_RESULT_ATTRIBUTES); |
| data.segmentHighlighter = editor.getMarkupModel() |
| .addRangeHighlighter(index, index + matchLength, HighlighterLayer.LAST + 1, attributes, HighlighterTargetArea.EXACT_RANGE); |
| } |
| data.ignoreCaretMove = true; |
| editor.getCaretModel().moveToOffset(index); |
| editor.getSelectionModel().removeSelection(); |
| editor.getScrollingModel().scrollToCaret(ScrollType.CENTER); |
| data.ignoreCaretMove = false; |
| IdeDocumentHistory.getInstance(data.project).includeCurrentCommandAsNavigation(); |
| } |
| } |
| |
| private static boolean detectSmartCaseSensitive(String prefix) { |
| boolean hasUpperCase = false; |
| for(int i = 0; i < prefix.length(); i++){ |
| char c = prefix.charAt(i); |
| if (Character.isUpperCase(c) && Character.toUpperCase(c) != Character.toLowerCase(c)){ |
| hasUpperCase = true; |
| break; |
| } |
| } |
| return hasUpperCase; |
| } |
| |
| private static class MyLabel extends JLabel { |
| public MyLabel(String text) { |
| super(text); |
| this.setBackground(HintUtil.INFORMATION_COLOR); |
| this.setForeground(JBColor.foreground()); |
| this.setOpaque(true); |
| } |
| } |
| |
| private static class MyPanel extends JPanel{ |
| private final Component myLeft; |
| |
| public MyPanel(Component left) { |
| super(new BorderLayout()); |
| myLeft = left; |
| } |
| |
| @Override |
| public Dimension getPreferredSize() { |
| Dimension size = super.getPreferredSize(); |
| Dimension lSize = myLeft.getPreferredSize(); |
| return new Dimension(size.width + lSize.width, size.height); |
| } |
| |
| public Dimension getTruePreferredSize() { |
| return super.getPreferredSize(); |
| } |
| } |
| |
| public static class MyTypedHandler extends TypedActionHandlerBase { |
| public MyTypedHandler(@Nullable TypedActionHandler originalHandler) { |
| super(originalHandler); |
| } |
| |
| @Override |
| public void execute(@NotNull Editor editor, char charTyped, @NotNull DataContext dataContext) { |
| PerEditorSearchData data = editor.getUserData(SEARCH_DATA_IN_EDITOR_VIEW_KEY); |
| if (data == null || data.hint == null){ |
| if (myOriginalHandler != null) myOriginalHandler.execute(editor, charTyped, dataContext); |
| } |
| else{ |
| LightweightHint hint = data.hint; |
| PerHintSearchData hintData = hint.getUserData(SEARCH_DATA_IN_HINT_KEY); |
| String text = hintData.label.getText(); |
| text += charTyped; |
| hintData.label.setText(text); |
| MyPanel comp = (MyPanel)hint.getComponent(); |
| if (comp.getTruePreferredSize().width > comp.getSize().width){ |
| Rectangle bounds = hint.getBounds(); |
| hint.updateBounds(bounds.x, bounds.y); |
| } |
| updatePosition(editor, hintData, false, false); |
| } |
| } |
| } |
| |
| public static class BackSpaceHandler extends EditorActionHandler{ |
| private final EditorActionHandler myOriginalHandler; |
| |
| public BackSpaceHandler(EditorActionHandler originalAction) { |
| myOriginalHandler = originalAction; |
| } |
| |
| @Override |
| public void doExecute(Editor editor, Caret caret, DataContext dataContext) { |
| PerEditorSearchData data = editor.getUserData(SEARCH_DATA_IN_EDITOR_VIEW_KEY); |
| if (data == null || data.hint == null){ |
| myOriginalHandler.execute(editor, caret, dataContext); |
| } |
| else{ |
| LightweightHint hint = data.hint; |
| PerHintSearchData hintData = hint.getUserData(SEARCH_DATA_IN_HINT_KEY); |
| String text = hintData.label.getText(); |
| if (text.length() > 0){ |
| text = text.substring(0, text.length() - 1); |
| } |
| hintData.label.setText(text); |
| updatePosition(editor, hintData, false, false); |
| } |
| } |
| } |
| |
| public static class UpHandler extends EditorActionHandler { |
| private final EditorActionHandler myOriginalHandler; |
| |
| public UpHandler(EditorActionHandler originalHandler) { |
| myOriginalHandler = originalHandler; |
| } |
| |
| @Override |
| public void doExecute(Editor editor, Caret caret, DataContext dataContext) { |
| PerEditorSearchData data = editor.getUserData(SEARCH_DATA_IN_EDITOR_VIEW_KEY); |
| if (data == null || data.hint == null){ |
| myOriginalHandler.execute(editor, caret, dataContext); |
| } |
| else{ |
| LightweightHint hint = data.hint; |
| PerHintSearchData hintData = hint.getUserData(SEARCH_DATA_IN_HINT_KEY); |
| String prefix = hintData.label.getText(); |
| if (prefix == null) return; |
| hintData.searchStart = editor.getCaretModel().getOffset(); |
| if (hintData.searchStart == 0) return; |
| hintData.searchStart--; |
| updatePosition(editor, hintData, true, true); |
| hintData.searchStart = editor.getCaretModel().getOffset(); |
| } |
| } |
| |
| @Override |
| public boolean isEnabled(Editor editor, DataContext dataContext) { |
| PerEditorSearchData data = editor.getUserData(SEARCH_DATA_IN_EDITOR_VIEW_KEY); |
| return data != null && data.hint != null || myOriginalHandler.isEnabled(editor, dataContext); |
| } |
| } |
| |
| public static class DownHandler extends EditorActionHandler { |
| private final EditorActionHandler myOriginalHandler; |
| |
| public DownHandler(EditorActionHandler originalHandler) { |
| myOriginalHandler = originalHandler; |
| } |
| |
| @Override |
| public void doExecute(Editor editor, Caret caret, DataContext dataContext) { |
| PerEditorSearchData data = editor.getUserData(SEARCH_DATA_IN_EDITOR_VIEW_KEY); |
| if (data == null || data.hint == null){ |
| myOriginalHandler.execute(editor, caret, dataContext); |
| } |
| else{ |
| LightweightHint hint = data.hint; |
| PerHintSearchData hintData = hint.getUserData(SEARCH_DATA_IN_HINT_KEY); |
| String prefix = hintData.label.getText(); |
| if (prefix == null) return; |
| hintData.searchStart = editor.getCaretModel().getOffset(); |
| if (hintData.searchStart == editor.getDocument().getTextLength()) return; |
| hintData.searchStart++; |
| updatePosition(editor, hintData, true, false); |
| hintData.searchStart = editor.getCaretModel().getOffset(); |
| } |
| } |
| |
| @Override |
| public boolean isEnabled(Editor editor, DataContext dataContext) { |
| PerEditorSearchData data = editor.getUserData(SEARCH_DATA_IN_EDITOR_VIEW_KEY); |
| return data != null && data.hint != null || myOriginalHandler.isEnabled(editor, dataContext); |
| } |
| } |
| } |