| /* |
| * 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.find.impl; |
| |
| import com.intellij.codeInsight.highlighting.HighlightManager; |
| import com.intellij.codeInsight.highlighting.HighlightManagerImpl; |
| import com.intellij.codeInsight.hint.HintManager; |
| import com.intellij.codeInsight.hint.HintManagerImpl; |
| import com.intellij.codeInsight.hint.HintUtil; |
| import com.intellij.find.*; |
| import com.intellij.find.findUsages.FindUsagesManager; |
| import com.intellij.find.impl.livePreview.SearchResults; |
| import com.intellij.lang.Language; |
| import com.intellij.lang.LanguageParserDefinitions; |
| import com.intellij.lang.ParserDefinition; |
| import com.intellij.lexer.Lexer; |
| import com.intellij.navigation.NavigationItem; |
| import com.intellij.openapi.actionSystem.ActionManager; |
| import com.intellij.openapi.actionSystem.AnAction; |
| import com.intellij.openapi.actionSystem.IdeActions; |
| import com.intellij.openapi.application.ApplicationManager; |
| import com.intellij.openapi.components.PersistentStateComponent; |
| import com.intellij.openapi.components.State; |
| import com.intellij.openapi.components.Storage; |
| import com.intellij.openapi.components.StoragePathMacros; |
| import com.intellij.openapi.diagnostic.Logger; |
| import com.intellij.openapi.editor.*; |
| import com.intellij.openapi.editor.colors.TextAttributesKey; |
| import com.intellij.openapi.editor.ex.FoldingModelEx; |
| import com.intellij.openapi.editor.markup.RangeHighlighter; |
| import com.intellij.openapi.fileEditor.FileEditor; |
| import com.intellij.openapi.fileEditor.TextEditor; |
| import com.intellij.openapi.fileTypes.*; |
| import com.intellij.openapi.fileTypes.impl.AbstractFileType; |
| import com.intellij.openapi.keymap.KeymapUtil; |
| import com.intellij.openapi.project.Project; |
| import com.intellij.openapi.util.*; |
| import com.intellij.openapi.util.text.StringUtil; |
| import com.intellij.openapi.vfs.VirtualFile; |
| import com.intellij.psi.*; |
| import com.intellij.psi.search.SearchScope; |
| import com.intellij.psi.tree.IElementType; |
| import com.intellij.psi.tree.TokenSet; |
| import com.intellij.ui.LightweightHint; |
| import com.intellij.ui.ReplacePromptDialog; |
| import com.intellij.usages.ChunkExtractor; |
| import com.intellij.usages.UsageViewManager; |
| import com.intellij.usages.impl.SyntaxHighlighterOverEditorHighlighter; |
| import com.intellij.util.Consumer; |
| import com.intellij.util.containers.ContainerUtil; |
| import com.intellij.util.containers.Predicate; |
| import com.intellij.util.messages.MessageBus; |
| import com.intellij.util.text.CharArrayUtil; |
| import com.intellij.util.text.StringSearcher; |
| import gnu.trove.THashSet; |
| import org.jdom.Element; |
| import org.jetbrains.annotations.NonNls; |
| import org.jetbrains.annotations.NotNull; |
| import org.jetbrains.annotations.Nullable; |
| |
| import javax.swing.*; |
| import java.awt.*; |
| import java.util.*; |
| import java.util.List; |
| import java.util.regex.Matcher; |
| import java.util.regex.Pattern; |
| import java.util.regex.PatternSyntaxException; |
| |
| @State( |
| name = "FindManager", |
| storages = { |
| @Storage( |
| file = StoragePathMacros.WORKSPACE_FILE |
| )} |
| ) |
| public class FindManagerImpl extends FindManager implements PersistentStateComponent<Element> { |
| private static final Logger LOG = Logger.getInstance("#com.intellij.find.impl.FindManagerImpl"); |
| |
| private final FindUsagesManager myFindUsagesManager; |
| private boolean isFindWasPerformed = false; |
| private boolean isSelectNextOccurrenceWasPerformed = false; |
| private Point myReplaceInFilePromptPos = new Point(-1, -1); |
| private Point myReplaceInProjectPromptPos = new Point(-1, -1); |
| private final FindModel myFindInProjectModel = new FindModel(); |
| private final FindModel myFindInFileModel = new FindModel(); |
| private FindModel myFindNextModel = null; |
| private FindModel myPreviousFindModel = null; |
| private static final FindResultImpl NOT_FOUND_RESULT = new FindResultImpl(); |
| private final Project myProject; |
| private final MessageBus myBus; |
| private static final Key<Boolean> HIGHLIGHTER_WAS_NOT_FOUND_KEY = Key.create("com.intellij.find.impl.FindManagerImpl.HighlighterNotFoundKey"); |
| @NonNls private static final String FIND_USAGES_MANAGER_ELEMENT = "FindUsagesManager"; |
| public static final boolean ourHasSearchInCommentsAndLiterals = true; |
| private FindDialog myFindDialog; |
| |
| public FindManagerImpl(Project project, FindSettings findSettings, UsageViewManager anotherManager, MessageBus bus) { |
| myProject = project; |
| myBus = bus; |
| findSettings.initModelBySetings(myFindInProjectModel); |
| |
| myFindInFileModel.setCaseSensitive(findSettings.isLocalCaseSensitive()); |
| myFindInFileModel.setWholeWordsOnly(findSettings.isLocalWholeWordsOnly()); |
| myFindInFileModel.setRegularExpressions(findSettings.isLocalRegularExpressions()); |
| |
| myFindUsagesManager = new FindUsagesManager(myProject, anotherManager); |
| myFindInProjectModel.setMultipleFiles(true); |
| } |
| |
| @Override |
| public FindModel createReplaceInFileModel() { |
| FindModel model = new FindModel(); |
| model.copyFrom(getFindInFileModel()); |
| model.setReplaceState(true); |
| model.setPromptOnReplace(false); |
| return model; |
| } |
| |
| @Override |
| public Element getState() { |
| Element element = new Element("FindManager"); |
| final Element findUsages = new Element(FIND_USAGES_MANAGER_ELEMENT); |
| element.addContent(findUsages); |
| try { |
| myFindUsagesManager.writeExternal(findUsages); |
| } |
| catch (WriteExternalException e) { |
| LOG.error(e); |
| } |
| return element; |
| } |
| |
| @Override |
| public void loadState(final Element state) { |
| final Element findUsages = state.getChild(FIND_USAGES_MANAGER_ELEMENT); |
| if (findUsages != null) { |
| try { |
| myFindUsagesManager.readExternal(findUsages); |
| } |
| catch (InvalidDataException e) { |
| LOG.error(e); |
| } |
| } |
| } |
| |
| @Override |
| public int showPromptDialog(@NotNull final FindModel model, String title) { |
| return showPromptDialogImpl(model, title, null); |
| } |
| |
| @PromptResultValue |
| public int showPromptDialogImpl(@NotNull final FindModel model, String title, @Nullable final MalformedReplacementStringException exception) { |
| ReplacePromptDialog replacePromptDialog = new ReplacePromptDialog(model.isMultipleFiles(), title, myProject, exception) { |
| @Override |
| @Nullable |
| public Point getInitialLocation() { |
| if (model.isMultipleFiles() && myReplaceInProjectPromptPos.x >= 0 && myReplaceInProjectPromptPos.y >= 0){ |
| return myReplaceInProjectPromptPos; |
| } |
| if (!model.isMultipleFiles() && myReplaceInFilePromptPos.x >= 0 && myReplaceInFilePromptPos.y >= 0){ |
| return myReplaceInFilePromptPos; |
| } |
| return null; |
| } |
| }; |
| |
| replacePromptDialog.show(); |
| |
| if (model.isMultipleFiles()){ |
| myReplaceInProjectPromptPos = replacePromptDialog.getLocation(); |
| } |
| else{ |
| myReplaceInFilePromptPos = replacePromptDialog.getLocation(); |
| } |
| return replacePromptDialog.getExitCode(); |
| } |
| |
| @Override |
| public void showFindDialog(@NotNull final FindModel model, @NotNull final Runnable okHandler) { |
| final Consumer<FindModel> handler = new Consumer<FindModel>(){ |
| @Override |
| public void consume(FindModel findModel) { |
| String stringToFind = findModel.getStringToFind(); |
| if (!StringUtil.isEmpty(stringToFind)) { |
| FindSettings.getInstance().addStringToFind(stringToFind); |
| } |
| if (!findModel.isMultipleFiles()) { |
| setFindWasPerformed(); |
| } |
| if (findModel.isReplaceState()) { |
| FindSettings.getInstance().addStringToReplace(findModel.getStringToReplace()); |
| } |
| if (findModel.isMultipleFiles() && !findModel.isProjectScope()) { |
| FindSettings.getInstance().addDirectory(findModel.getDirectoryName()); |
| |
| if (findModel.getDirectoryName() != null) { |
| myFindInProjectModel.setWithSubdirectories(findModel.isWithSubdirectories()); |
| } |
| } |
| okHandler.run(); |
| } |
| }; |
| if(myFindDialog==null || Disposer.isDisposed(myFindDialog.getDisposable())){ |
| myFindDialog = new FindDialog(myProject, model, handler) { |
| @Override |
| protected void dispose() { |
| super.dispose(); |
| myFindDialog = null; // avoid strong ref! |
| } |
| }; |
| myFindDialog.setModal(true); |
| } |
| else if (myFindDialog.getModel().isReplaceState() != model.isReplaceState()) { |
| myFindDialog.setModel(model); |
| myFindDialog.setOkHandler(handler); |
| return; |
| } |
| myFindDialog.show(); |
| } |
| |
| @Override |
| @NotNull |
| public FindModel getFindInFileModel() { |
| return myFindInFileModel; |
| } |
| |
| @Override |
| @NotNull |
| public FindModel getFindInProjectModel() { |
| myFindInProjectModel.setFromCursor(false); |
| myFindInProjectModel.setForward(true); |
| myFindInProjectModel.setGlobal(true); |
| return myFindInProjectModel; |
| } |
| |
| @Override |
| public boolean findWasPerformed() { |
| return isFindWasPerformed; |
| } |
| |
| @Override |
| public void setFindWasPerformed() { |
| isFindWasPerformed = true; |
| isSelectNextOccurrenceWasPerformed = false; |
| } |
| |
| @Override |
| public boolean selectNextOccurrenceWasPerformed() { |
| return isSelectNextOccurrenceWasPerformed; |
| } |
| |
| @Override |
| public void setSelectNextOccurrenceWasPerformed() { |
| isSelectNextOccurrenceWasPerformed = true; |
| isFindWasPerformed = false; |
| } |
| |
| @Override |
| public FindModel getFindNextModel() { |
| return myFindNextModel; |
| } |
| |
| @Override |
| public FindModel getFindNextModel(@NotNull final Editor editor) { |
| if (myFindNextModel == null) return null; |
| |
| final JComponent header = editor.getHeaderComponent(); |
| if (header instanceof EditorSearchComponent && !isSelectNextOccurrenceWasPerformed) { |
| final EditorSearchComponent searchComponent = (EditorSearchComponent)header; |
| final String textInField = searchComponent.getTextInField(); |
| if (!Comparing.equal(textInField, myFindInFileModel.getStringToFind()) && !textInField.isEmpty()) { |
| FindModel patched = new FindModel(); |
| patched.copyFrom(myFindNextModel); |
| patched.setStringToFind(textInField); |
| return patched; |
| } |
| } |
| |
| return myFindNextModel; |
| } |
| |
| @Override |
| public void setFindNextModel(FindModel findNextModel) { |
| myFindNextModel = findNextModel; |
| myBus.syncPublisher(FIND_MODEL_TOPIC).findNextModelChanged(); |
| } |
| |
| @Override |
| @NotNull |
| public FindResult findString(@NotNull CharSequence text, int offset, @NotNull FindModel model){ |
| return findString(text, offset, model, null); |
| } |
| |
| @NotNull |
| @Override |
| public FindResult findString(@NotNull CharSequence text, int offset, @NotNull FindModel model, @Nullable VirtualFile file) { |
| if (LOG.isDebugEnabled()) { |
| LOG.debug("offset="+offset); |
| LOG.debug("textlength="+text.length()); |
| LOG.debug(model.toString()); |
| } |
| |
| return findStringLoop(text, offset, model, file, getFindContextPredicate(model, file, text)); |
| } |
| |
| private FindResult findStringLoop(CharSequence text, int offset, FindModel model, VirtualFile file, @Nullable Predicate<FindResult> filter) { |
| final char[] textArray = CharArrayUtil.fromSequenceWithoutCopying(text); |
| while(true) { |
| FindResult result = doFindString(text, textArray, offset, model, file); |
| if (filter == null || filter.apply(result)) { |
| if (!model.isWholeWordsOnly()) { |
| return result; |
| } |
| if (!result.isStringFound()) { |
| return result; |
| } |
| if (isWholeWord(text, result.getStartOffset(), result.getEndOffset())) { |
| return result; |
| } |
| } |
| |
| offset = model.isForward() ? result.getStartOffset() + 1 : result.getEndOffset() - 1; |
| if (offset > text.length() || offset < 0) return NOT_FOUND_RESULT; |
| } |
| } |
| |
| private class FindExceptCommentsOrLiteralsData implements Predicate<FindResult> { |
| private final VirtualFile myFile; |
| private final FindModel myFindModel; |
| private final TreeMap<Integer, Integer> mySkipRangesSet; |
| |
| private FindExceptCommentsOrLiteralsData(VirtualFile file, FindModel model, CharSequence text) { |
| myFile = file; |
| myFindModel = model.clone(); |
| |
| TreeMap<Integer, Integer> result = new TreeMap<Integer, Integer>(); |
| |
| if (model.isExceptComments() || model.isExceptCommentsAndStringLiterals()) { |
| addRanges(file, model, text, result, FindModel.SearchContext.IN_COMMENTS); |
| } |
| |
| if (model.isExceptStringLiterals() || model.isExceptCommentsAndStringLiterals()) { |
| addRanges(file, model, text, result, FindModel.SearchContext.IN_STRING_LITERALS); |
| } |
| |
| mySkipRangesSet = result; |
| } |
| |
| private void addRanges(VirtualFile file, |
| FindModel model, |
| CharSequence text, |
| TreeMap<Integer, Integer> result, |
| FindModel.SearchContext searchContext) { |
| FindModel clonedModel = model.clone(); |
| clonedModel.setSearchContext(searchContext); |
| clonedModel.setForward(true); |
| int offset = 0; |
| |
| while(true) { |
| FindResult customResult = findStringLoop(text, offset, clonedModel, file, null); |
| if (!customResult.isStringFound()) break; |
| result.put(customResult.getStartOffset(), customResult.getEndOffset()); |
| offset = Math.max(customResult.getEndOffset(), offset + 1); // avoid loop for zero size reg exps matches |
| if (offset >= text.length()) break; |
| } |
| } |
| |
| boolean isAcceptableFor(FindModel model, VirtualFile file) { |
| return Comparing.equal(myFile, file) && myFindModel.equals(model); |
| } |
| |
| @Override |
| public boolean apply(@Nullable FindResult input) { |
| if (input == null || !input.isStringFound()) return true; |
| NavigableMap<Integer, Integer> map = mySkipRangesSet.headMap(input.getStartOffset(), true); |
| for(Map.Entry<Integer, Integer> e:map.descendingMap().entrySet()) { |
| // [e.key, e.value] intersect with [input.start, input.end] |
| if (e.getKey() <= input.getStartOffset() && (input.getStartOffset() <= e.getValue() || e.getValue() >= input.getEndOffset())) return false; |
| if (e.getValue() <= input.getStartOffset()) break; |
| } |
| return true; |
| } |
| } |
| private static Key<FindExceptCommentsOrLiteralsData> ourExceptCommentsOrLiteralsDataKey = Key.create("except.comments.literals.search.data"); |
| |
| private Predicate<FindResult> getFindContextPredicate(@NotNull FindModel model, VirtualFile file, CharSequence text) { |
| if (file == null) return null; |
| FindModel.SearchContext context = model.getSearchContext(); |
| if( context == FindModel.SearchContext.ANY || context == FindModel.SearchContext.IN_COMMENTS || |
| context == FindModel.SearchContext.IN_STRING_LITERALS) { |
| return null; |
| } |
| |
| FindExceptCommentsOrLiteralsData data = model.getUserData(ourExceptCommentsOrLiteralsDataKey); |
| if (data == null || !data.isAcceptableFor(model, file)) { |
| model.putUserData(ourExceptCommentsOrLiteralsDataKey, data = new FindExceptCommentsOrLiteralsData(file, model, text)); |
| } |
| |
| return data; |
| } |
| |
| @Override |
| public int showMalformedReplacementPrompt(@NotNull FindModel model, String title, MalformedReplacementStringException exception) { |
| return showPromptDialogImpl(model, title, exception); |
| } |
| |
| @Override |
| public FindModel getPreviousFindModel() { |
| return myPreviousFindModel; |
| } |
| |
| @Override |
| public void setPreviousFindModel(FindModel previousFindModel) { |
| myPreviousFindModel = previousFindModel; |
| } |
| |
| private static boolean isWholeWord(CharSequence text, int startOffset, int endOffset) { |
| boolean isWordStart = startOffset == 0 || |
| !Character.isJavaIdentifierPart(text.charAt(startOffset - 1)) || |
| !Character.isJavaIdentifierPart(text.charAt(startOffset)) || |
| startOffset > 1 && text.charAt(startOffset - 2) == '\\'; |
| |
| boolean isWordEnd = endOffset == text.length() || |
| !Character.isJavaIdentifierPart(text.charAt(endOffset)) || |
| endOffset > 0 && !Character.isJavaIdentifierPart(text.charAt(endOffset - 1)); |
| |
| return isWordStart && isWordEnd; |
| } |
| |
| @NotNull |
| private static FindModel normalizeIfMultilined(@NotNull FindModel findmodel) { |
| if (findmodel.isMultiline()) { |
| final FindModel model = new FindModel(); |
| model.copyFrom(findmodel); |
| final String s = model.getStringToFind(); |
| String newStringToFind; |
| |
| if (findmodel.isRegularExpressions()) { |
| newStringToFind = StringUtil.replace(s, "\n", "\\n\\s*"); // add \\s* for convenience |
| } else { |
| newStringToFind = StringUtil.escapeToRegexp(s); |
| model.setRegularExpressions(true); |
| } |
| model.setStringToFind(newStringToFind); |
| |
| return model; |
| } |
| return findmodel; |
| } |
| |
| @NotNull |
| private FindResult doFindString(@NotNull CharSequence text, |
| @Nullable char[] textArray, |
| int offset, |
| @NotNull FindModel findmodel, |
| @Nullable VirtualFile file) { |
| FindModel model = normalizeIfMultilined(findmodel); |
| String toFind = model.getStringToFind(); |
| if (toFind.isEmpty()){ |
| return NOT_FOUND_RESULT; |
| } |
| |
| if (model.isInCommentsOnly() || model.isInStringLiteralsOnly()) { |
| if (file == null) return NOT_FOUND_RESULT; |
| return findInCommentsAndLiterals(text, textArray, offset, model, file); |
| } |
| |
| if (model.isRegularExpressions()){ |
| return findStringByRegularExpression(text, offset, model); |
| } |
| |
| final StringSearcher searcher = createStringSearcher(model); |
| |
| int index; |
| if (model.isForward()){ |
| final int res = searcher.scan(text, textArray, offset, text.length()); |
| index = res < 0 ? -1 : res; |
| } |
| else { |
| index = offset == 0 ? -1 : searcher.scan(text, textArray, 0, offset-1); |
| } |
| if (index < 0){ |
| return NOT_FOUND_RESULT; |
| } |
| return new FindResultImpl(index, index + toFind.length()); |
| } |
| |
| @NotNull |
| private static StringSearcher createStringSearcher(@NotNull FindModel model) { |
| return new StringSearcher(model.getStringToFind(), model.isCaseSensitive(), model.isForward()); |
| } |
| |
| public static void clearPreviousFindData(FindModel model) { |
| model.putUserData(ourCommentsLiteralsSearchDataKey, null); |
| model.putUserData(ourExceptCommentsOrLiteralsDataKey, null); |
| } |
| |
| private static class CommentsLiteralsSearchData { |
| final VirtualFile lastFile; |
| int startOffset = 0; |
| final SyntaxHighlighterOverEditorHighlighter highlighter; |
| |
| TokenSet tokensOfInterest; |
| final StringSearcher searcher; |
| final Matcher matcher; |
| final Set<Language> relevantLanguages; |
| final FindModel model; |
| |
| public CommentsLiteralsSearchData(VirtualFile lastFile, Set<Language> relevantLanguages, |
| SyntaxHighlighterOverEditorHighlighter highlighter, TokenSet tokensOfInterest, |
| StringSearcher searcher, Matcher matcher, FindModel model) { |
| this.lastFile = lastFile; |
| this.highlighter = highlighter; |
| this.tokensOfInterest = tokensOfInterest; |
| this.searcher = searcher; |
| this.matcher = matcher; |
| this.relevantLanguages = relevantLanguages; |
| this.model = model; |
| } |
| } |
| |
| private static final Key<CommentsLiteralsSearchData> ourCommentsLiteralsSearchDataKey = Key.create("comments.literals.search.data"); |
| |
| @NotNull |
| private FindResult findInCommentsAndLiterals(@NotNull CharSequence text, |
| char[] textArray, |
| int offset, |
| @NotNull FindModel model, |
| @NotNull final VirtualFile file) { |
| FileType ftype = file.getFileType(); |
| Language lang = null; |
| if (ftype instanceof LanguageFileType) { |
| lang = ((LanguageFileType)ftype).getLanguage(); |
| } |
| |
| CommentsLiteralsSearchData data = model.getUserData(ourCommentsLiteralsSearchDataKey); |
| if (data == null || !Comparing.equal(data.lastFile, file) || !data.model.equals(model)) { |
| SyntaxHighlighter highlighter = getHighlighter(file, lang); |
| |
| if (highlighter == null) { |
| // no syntax highlighter -> no search |
| return NOT_FOUND_RESULT; |
| } |
| |
| TokenSet tokensOfInterest = TokenSet.EMPTY; |
| Set<Language> relevantLanguages; |
| if (lang != null) { |
| final Language finalLang = lang; |
| relevantLanguages = ApplicationManager.getApplication().runReadAction(new Computable<Set<Language>>() { |
| @Override |
| public Set<Language> compute() { |
| THashSet<Language> result = new THashSet<Language>(); |
| |
| FileViewProvider viewProvider = PsiManager.getInstance(myProject).findViewProvider(file); |
| if (viewProvider != null) { |
| result.addAll(viewProvider.getLanguages()); |
| } |
| |
| if (result.isEmpty()) { |
| result.add(finalLang); |
| } |
| return result; |
| } |
| }); |
| |
| for (Language relevantLanguage:relevantLanguages) { |
| tokensOfInterest = addTokenTypesForLanguage(model, relevantLanguage, tokensOfInterest); |
| } |
| |
| if(model.isInStringLiteralsOnly()) { |
| // TODO: xml does not have string literals defined so we add XmlAttributeValue element type as convenience |
| final Lexer xmlLexer = getHighlighter(null, Language.findLanguageByID("XML")).getHighlightingLexer(); |
| final String marker = "xxx"; |
| xmlLexer.start("<a href=\"" + marker+ "\" />"); |
| |
| while (!marker.equals(xmlLexer.getTokenText())) { |
| xmlLexer.advance(); |
| if (xmlLexer.getTokenType() == null) break; |
| } |
| |
| IElementType convenienceXmlAttrType = xmlLexer.getTokenType(); |
| if (convenienceXmlAttrType != null) { |
| tokensOfInterest = TokenSet.orSet(tokensOfInterest, TokenSet.create(convenienceXmlAttrType)); |
| } |
| } |
| } else { |
| relevantLanguages = ContainerUtil.newHashSet(); |
| if (ftype instanceof AbstractFileType) { |
| if (model.isInCommentsOnly()) { |
| tokensOfInterest = TokenSet.create(CustomHighlighterTokenType.LINE_COMMENT, CustomHighlighterTokenType.MULTI_LINE_COMMENT); |
| } |
| if (model.isInStringLiteralsOnly()) { |
| tokensOfInterest = TokenSet.orSet(tokensOfInterest, TokenSet.create(CustomHighlighterTokenType.STRING, CustomHighlighterTokenType.SINGLE_QUOTED_STRING)); |
| } |
| } |
| } |
| |
| Matcher matcher = model.isRegularExpressions() ? compileRegExp(model, ""):null; |
| StringSearcher searcher = matcher != null ? null: new StringSearcher(model.getStringToFind(), model.isCaseSensitive(), true); |
| SyntaxHighlighterOverEditorHighlighter highlighterAdapter = new SyntaxHighlighterOverEditorHighlighter(highlighter, file, myProject); |
| data = new CommentsLiteralsSearchData(file, relevantLanguages, highlighterAdapter, tokensOfInterest, searcher, matcher, model.clone()); |
| data.highlighter.restart(text); |
| model.putUserData(ourCommentsLiteralsSearchDataKey, data); |
| } |
| |
| int initialStartOffset = model.isForward() && data.startOffset < offset ? data.startOffset : 0; |
| data.highlighter.resetPosition(initialStartOffset); |
| final Lexer lexer = data.highlighter.getHighlightingLexer(); |
| |
| IElementType tokenType; |
| TokenSet tokens = data.tokensOfInterest; |
| |
| int lastGoodOffset = 0; |
| boolean scanningForward = model.isForward(); |
| FindResultImpl prevFindResult = NOT_FOUND_RESULT; |
| |
| while((tokenType = lexer.getTokenType()) != null) { |
| if (lexer.getState() == 0) lastGoodOffset = lexer.getTokenStart(); |
| |
| final TextAttributesKey[] keys = data.highlighter.getTokenHighlights(tokenType); |
| |
| if (tokens.contains(tokenType) || |
| (model.isInStringLiteralsOnly() && ChunkExtractor.isHighlightedAsString(keys)) || |
| (model.isInCommentsOnly() && ChunkExtractor.isHighlightedAsComment(keys)) |
| ) { |
| int start = lexer.getTokenStart(); |
| int end = lexer.getTokenEnd(); |
| if (model.isInStringLiteralsOnly()) { // skip literal quotes itself from matching |
| char c = text.charAt(start); |
| if (c == '"' || c == '\'') { |
| while (start < end && c == text.charAt(start)) { |
| ++start; |
| if (c == text.charAt(end - 1) && start < end) --end; |
| } |
| } |
| } |
| |
| while(true) { |
| FindResultImpl findResult = null; |
| |
| if (data.searcher != null) { |
| int matchStart = data.searcher.scan(text, textArray, start, end); |
| |
| if (matchStart != -1 && matchStart >= start) { |
| final int matchEnd = matchStart + model.getStringToFind().length(); |
| if (matchStart >= offset || !scanningForward) |
| findResult = new FindResultImpl(matchStart, matchEnd); |
| else { |
| start = matchEnd; |
| continue; |
| } |
| } |
| } else { |
| data.matcher.reset(text.subSequence(start, end)); |
| if (data.matcher.find()) { |
| final int matchEnd = start + data.matcher.end(); |
| int matchStart = start + data.matcher.start(); |
| if (matchStart >= offset || !scanningForward) { |
| findResult = new FindResultImpl(matchStart, matchEnd); |
| } |
| else { |
| start = matchEnd; |
| continue; |
| } |
| } |
| } |
| |
| if (findResult != null) { |
| if (scanningForward) { |
| data.startOffset = lastGoodOffset; |
| return findResult; |
| } else { |
| |
| if (findResult.getEndOffset() >= offset) return prevFindResult; |
| prevFindResult = findResult; |
| start = findResult.getEndOffset(); |
| continue; |
| } |
| } |
| break; |
| } |
| } else { |
| Language tokenLang = tokenType.getLanguage(); |
| if (tokenLang != lang && tokenLang != Language.ANY && !data.relevantLanguages.contains(tokenLang)) { |
| tokens = addTokenTypesForLanguage(model, tokenLang, tokens); |
| data.tokensOfInterest = tokens; |
| data.relevantLanguages.add(tokenLang); |
| } |
| } |
| |
| lexer.advance(); |
| } |
| |
| return prevFindResult; |
| } |
| |
| private static TokenSet addTokenTypesForLanguage(FindModel model, Language lang, TokenSet tokensOfInterest) { |
| ParserDefinition definition = LanguageParserDefinitions.INSTANCE.forLanguage(lang); |
| if (definition != null) { |
| tokensOfInterest = TokenSet.orSet(tokensOfInterest, model.isInCommentsOnly() ? definition.getCommentTokens(): TokenSet.EMPTY); |
| tokensOfInterest = TokenSet.orSet(tokensOfInterest, model.isInStringLiteralsOnly() ? definition.getStringLiteralElements() : TokenSet.EMPTY); |
| } |
| return tokensOfInterest; |
| } |
| |
| private static @Nullable SyntaxHighlighter getHighlighter(VirtualFile file, @Nullable Language lang) { |
| SyntaxHighlighter syntaxHighlighter = lang != null ? SyntaxHighlighterFactory.getSyntaxHighlighter(lang, null, file) : null; |
| if (lang == null || syntaxHighlighter instanceof PlainSyntaxHighlighter) { |
| syntaxHighlighter = SyntaxHighlighterFactory.getSyntaxHighlighter(file.getFileType(), null, file); |
| } |
| |
| return syntaxHighlighter; |
| } |
| |
| private static FindResult findStringByRegularExpression(CharSequence text, int startOffset, FindModel model) { |
| Matcher matcher = compileRegExp(model, text); |
| if (matcher == null) { |
| return NOT_FOUND_RESULT; |
| } |
| if (model.isForward()){ |
| if (matcher.find(startOffset)) { |
| if (matcher.end() <= text.length()) { |
| return new FindResultImpl(matcher.start(), matcher.end()); |
| } |
| } |
| return NOT_FOUND_RESULT; |
| } |
| else { |
| int start = -1; |
| int end = -1; |
| while(matcher.find() && matcher.end() < startOffset){ |
| start = matcher.start(); |
| end = matcher.end(); |
| } |
| if (start < 0){ |
| return NOT_FOUND_RESULT; |
| } |
| return new FindResultImpl(start, end); |
| } |
| } |
| |
| private static Matcher compileRegExp(FindModel model, CharSequence text) { |
| Pattern pattern = model.compileRegExp(); |
| return pattern == null ? null : pattern.matcher(text); |
| } |
| |
| @Override |
| public String getStringToReplace(@NotNull String foundString, @NotNull FindModel model) throws MalformedReplacementStringException { |
| String toReplace = model.getStringToReplace(); |
| if (model.isRegularExpressions()) { |
| return getStringToReplaceByRegexp0(foundString, model); |
| } |
| if (model.isPreserveCase()) { |
| return replaceWithCaseRespect (toReplace, foundString); |
| } |
| return toReplace; |
| } |
| |
| @Override |
| public String getStringToReplace(@NotNull String foundString, @NotNull FindModel model, |
| int startOffset, @NotNull CharSequence documentText) throws MalformedReplacementStringException{ |
| String toReplace = model.getStringToReplace(); |
| if (model.isRegularExpressions()) { |
| return getStringToReplaceByRegexp(model, documentText, startOffset); |
| } |
| if (model.isPreserveCase()) { |
| return replaceWithCaseRespect (toReplace, foundString); |
| } |
| return toReplace; |
| } |
| |
| private static String getStringToReplaceByRegexp(@NotNull final FindModel model, @NotNull CharSequence text, int startOffset) throws MalformedReplacementStringException { |
| Matcher matcher = compileRegexAndFindFirst(model, text, startOffset); |
| return getStringToReplaceByRegexp(model, matcher); |
| } |
| |
| private static String getStringToReplaceByRegexp(@NotNull final FindModel model, Matcher matcher) throws MalformedReplacementStringException{ |
| StringBuffer replaced = new StringBuffer(); |
| if (matcher == null) return null; |
| try { |
| String toReplace = StringUtil.unescapeStringCharacters(model.getStringToReplace()); |
| matcher.appendReplacement(replaced, toReplace); |
| |
| return replaced.substring(matcher.start()); |
| } |
| catch (Exception e) { |
| throw createMalformedReplacementException(model, e); |
| } |
| } |
| |
| private static Matcher compileRegexAndFindFirst(FindModel model, CharSequence text, int startOffset) { |
| Matcher matcher = compileRegExp(model, text); |
| |
| if (model.isForward()){ |
| if (!matcher.find(startOffset)) { |
| return null; |
| } |
| if (matcher.end() > text.length()) { |
| return null; |
| } |
| } |
| else { |
| int start = -1; |
| while(matcher.find() && matcher.end() < startOffset){ |
| start = matcher.start(); |
| } |
| if (start < 0){ |
| return null; |
| } |
| } |
| return matcher; |
| } |
| |
| private static MalformedReplacementStringException createMalformedReplacementException(FindModel model, Exception e) { |
| return new MalformedReplacementStringException(FindBundle.message("find.replace.invalid.replacement.string", model.getStringToReplace()), e); |
| } |
| |
| private static String getStringToReplaceByRegexp0(String foundString, final FindModel model) throws MalformedReplacementStringException{ |
| String toFind = model.getStringToFind(); |
| String toReplace = model.getStringToReplace(); |
| Pattern pattern; |
| try{ |
| int flags = Pattern.MULTILINE; |
| if (!model.isCaseSensitive()) { |
| flags |= Pattern.CASE_INSENSITIVE; |
| } |
| pattern = Pattern.compile(toFind, flags); |
| } |
| catch(PatternSyntaxException e){ |
| return toReplace; |
| } |
| |
| Matcher matcher = pattern.matcher(foundString); |
| if (matcher.matches()) { |
| try { |
| return matcher.replaceAll(StringUtil.unescapeStringCharacters(toReplace)); |
| } |
| catch (Exception e) { |
| throw createMalformedReplacementException(model, e); |
| } |
| } |
| else { |
| // There are valid situations (for example, IDEADEV-2543 or positive lookbehind assertions) |
| // where an expression which matches a string in context will not match the same string |
| // separately). |
| return toReplace; |
| } |
| } |
| |
| private static String replaceWithCaseRespect(String toReplace, String foundString) { |
| if (foundString.isEmpty() || toReplace.isEmpty()) return toReplace; |
| StringBuilder buffer = new StringBuilder(); |
| |
| if (Character.isUpperCase(foundString.charAt(0))) { |
| buffer.append(Character.toUpperCase(toReplace.charAt(0))); |
| } |
| else { |
| buffer.append(Character.toLowerCase(toReplace.charAt(0))); |
| } |
| if (toReplace.length() == 1) return buffer.toString(); |
| |
| if (foundString.length() == 1) { |
| buffer.append(toReplace.substring(1)); |
| return buffer.toString(); |
| } |
| |
| boolean isTailUpper = true; |
| boolean isTailLower = true; |
| for (int i = 1; i < foundString.length(); i++) { |
| isTailUpper &= Character.isUpperCase(foundString.charAt(i)); |
| isTailLower &= Character.isLowerCase(foundString.charAt(i)); |
| if (!isTailUpper && !isTailLower) break; |
| } |
| |
| if (isTailUpper) { |
| buffer.append(StringUtil.toUpperCase(toReplace.substring(1))); |
| } |
| else if (isTailLower) { |
| buffer.append(toReplace.substring(1).toLowerCase()); |
| } |
| else { |
| buffer.append(toReplace.substring(1)); |
| } |
| return buffer.toString(); |
| } |
| |
| @Override |
| public boolean canFindUsages(@NotNull PsiElement element) { |
| return element.isValid() && myFindUsagesManager.canFindUsages(element); |
| } |
| |
| @Override |
| public void findUsages(@NotNull PsiElement element) { |
| findUsages(element, false); |
| } |
| |
| @Override |
| public void findUsagesInScope(@NotNull PsiElement element, @NotNull SearchScope searchScope) { |
| myFindUsagesManager.findUsages(element, null, null, false, searchScope); |
| } |
| |
| @Override |
| public void findUsages(@NotNull PsiElement element, boolean showDialog) { |
| myFindUsagesManager.findUsages(element, null, null, showDialog, null); |
| } |
| |
| @Override |
| public void showSettingsAndFindUsages(@NotNull NavigationItem[] targets) { |
| FindUsagesManager.showSettingsAndFindUsages(targets); |
| } |
| |
| @Override |
| public void clearFindingNextUsageInFile() { |
| myFindUsagesManager.clearFindingNextUsageInFile(); |
| } |
| |
| @Override |
| public void findUsagesInEditor(@NotNull PsiElement element, @NotNull FileEditor fileEditor) { |
| if (fileEditor instanceof TextEditor) { |
| TextEditor textEditor = (TextEditor)fileEditor; |
| Editor editor = textEditor.getEditor(); |
| Document document = editor.getDocument(); |
| PsiFile psiFile = PsiDocumentManager.getInstance(myProject).getPsiFile(document); |
| |
| myFindUsagesManager.findUsages(element, psiFile, fileEditor, false, null); |
| } |
| } |
| |
| private static boolean tryToFindNextUsageViaEditorSearchComponent(Editor editor, SearchResults.Direction forwardOrBackward) { |
| if (editor.getHeaderComponent() instanceof EditorSearchComponent) { |
| EditorSearchComponent searchComponent = (EditorSearchComponent)editor.getHeaderComponent(); |
| if (searchComponent.hasMatches()) { |
| if (forwardOrBackward == SearchResults.Direction.UP) { |
| searchComponent.searchBackward(); |
| } else { |
| searchComponent.searchForward(); |
| } |
| return true; |
| } |
| } |
| return false; |
| } |
| |
| @Override |
| public boolean findNextUsageInEditor(@NotNull FileEditor fileEditor) { |
| return findNextUsageInFile(fileEditor, SearchResults.Direction.DOWN); |
| } |
| |
| private boolean findNextUsageInFile(@NotNull FileEditor fileEditor, @NotNull SearchResults.Direction direction) { |
| if (fileEditor instanceof TextEditor) { |
| TextEditor textEditor = (TextEditor)fileEditor; |
| Editor editor = textEditor.getEditor(); |
| if (tryToFindNextUsageViaEditorSearchComponent(editor, direction)) { |
| return true; |
| } |
| |
| RangeHighlighter[] highlighters = ((HighlightManagerImpl)HighlightManager.getInstance(myProject)).getHighlighters(editor); |
| if (highlighters.length > 0) { |
| return highlightNextHighlighter(highlighters, editor, editor.getCaretModel().getOffset(), direction == SearchResults.Direction.DOWN, false); |
| } |
| } |
| |
| if (direction == SearchResults.Direction.DOWN) { |
| return myFindUsagesManager.findNextUsageInFile(fileEditor); |
| } |
| return myFindUsagesManager.findPreviousUsageInFile(fileEditor); |
| } |
| |
| @Override |
| public boolean findPreviousUsageInEditor(@NotNull FileEditor fileEditor) { |
| return findNextUsageInFile(fileEditor, SearchResults.Direction.UP); |
| } |
| |
| private static boolean highlightNextHighlighter(RangeHighlighter[] highlighters, Editor editor, int offset, boolean isForward, boolean secondPass) { |
| RangeHighlighter highlighterToSelect = null; |
| Object wasNotFound = editor.getUserData(HIGHLIGHTER_WAS_NOT_FOUND_KEY); |
| for (RangeHighlighter highlighter : highlighters) { |
| int start = highlighter.getStartOffset(); |
| int end = highlighter.getEndOffset(); |
| if (highlighter.isValid() && start < end) { |
| if (isForward && (start > offset || start == offset && secondPass)) { |
| if (highlighterToSelect == null || highlighterToSelect.getStartOffset() > start) highlighterToSelect = highlighter; |
| } |
| if (!isForward && (end < offset || end == offset && secondPass)) { |
| if (highlighterToSelect == null || highlighterToSelect.getEndOffset() < end) highlighterToSelect = highlighter; |
| } |
| } |
| } |
| if (highlighterToSelect != null) { |
| expandFoldRegionsIfNecessary(editor, highlighterToSelect.getStartOffset(), highlighterToSelect.getEndOffset()); |
| editor.getSelectionModel().setSelection(highlighterToSelect.getStartOffset(), highlighterToSelect.getEndOffset()); |
| editor.getCaretModel().moveToOffset(highlighterToSelect.getStartOffset()); |
| ScrollType scrollType; |
| if (secondPass) { |
| scrollType = isForward ? ScrollType.CENTER_UP : ScrollType.CENTER_DOWN; |
| } |
| else { |
| scrollType = isForward ? ScrollType.CENTER_DOWN : ScrollType.CENTER_UP; |
| } |
| editor.getScrollingModel().scrollToCaret(scrollType); |
| editor.putUserData(HIGHLIGHTER_WAS_NOT_FOUND_KEY, null); |
| return true; |
| } |
| |
| if (wasNotFound == null) { |
| editor.putUserData(HIGHLIGHTER_WAS_NOT_FOUND_KEY, Boolean.TRUE); |
| String message = FindBundle.message("find.highlight.no.more.highlights.found"); |
| if (isForward) { |
| AnAction action=ActionManager.getInstance().getAction(IdeActions.ACTION_FIND_NEXT); |
| String shortcutsText=KeymapUtil.getFirstKeyboardShortcutText(action); |
| if (shortcutsText.isEmpty()) { |
| message = FindBundle.message("find.search.again.from.top.action.message", message); |
| } |
| else { |
| message = FindBundle.message("find.search.again.from.top.hotkey.message", message, shortcutsText); |
| } |
| } |
| else { |
| AnAction action=ActionManager.getInstance().getAction(IdeActions.ACTION_FIND_PREVIOUS); |
| String shortcutsText=KeymapUtil.getFirstKeyboardShortcutText(action); |
| if (shortcutsText.isEmpty()) { |
| message = FindBundle.message("find.search.again.from.bottom.action.message", message); |
| } |
| else { |
| message = FindBundle.message("find.search.again.from.bottom.hotkey.message", message, shortcutsText); |
| } |
| } |
| JComponent component = HintUtil.createInformationLabel(message); |
| final LightweightHint hint = new LightweightHint(component); |
| HintManagerImpl.getInstanceImpl().showEditorHint(hint, editor, HintManager.UNDER, HintManager.HIDE_BY_ANY_KEY | |
| HintManager.HIDE_BY_TEXT_CHANGE | HintManager.HIDE_BY_SCROLLING, 0, false); |
| return true; |
| } |
| if (!secondPass) { |
| offset = isForward ? 0 : editor.getDocument().getTextLength(); |
| return highlightNextHighlighter(highlighters, editor, offset, isForward, true); |
| } |
| |
| return false; |
| } |
| |
| private static void expandFoldRegionsIfNecessary(@NotNull Editor editor, final int startOffset, int endOffset) { |
| final FoldingModel foldingModel = editor.getFoldingModel(); |
| final FoldRegion[] regions; |
| if (foldingModel instanceof FoldingModelEx) { |
| regions = ((FoldingModelEx)foldingModel).fetchTopLevel(); |
| } |
| else { |
| regions = foldingModel.getAllFoldRegions(); |
| } |
| if (regions == null) { |
| return; |
| } |
| int i = Arrays.binarySearch(regions, null, new Comparator<FoldRegion>() { |
| @Override |
| public int compare(FoldRegion o1, FoldRegion o2) { |
| // Find the first region that ends after the given start offset |
| if (o1 == null) { |
| return startOffset - o2.getEndOffset(); |
| } |
| else { |
| return o1.getEndOffset() - startOffset; |
| } |
| } |
| }); |
| if (i < 0) { |
| i = -i - 1; |
| } |
| else { |
| i++; // Don't expand fold region that ends at the start offset. |
| } |
| if (i >= regions.length) { |
| return; |
| } |
| final List<FoldRegion> toExpand = new ArrayList<FoldRegion>(); |
| for (; i < regions.length; i++) { |
| final FoldRegion region = regions[i]; |
| if (region.getStartOffset() >= endOffset) { |
| break; |
| } |
| if (!region.isExpanded()) { |
| toExpand.add(region); |
| } |
| } |
| if (toExpand.isEmpty()) { |
| return; |
| } |
| foldingModel.runBatchFoldingOperation(new Runnable() { |
| @Override |
| public void run() { |
| for (FoldRegion region : toExpand) { |
| region.setExpanded(true); |
| } |
| } |
| }); |
| } |
| |
| @NotNull |
| public FindUsagesManager getFindUsagesManager() { |
| return myFindUsagesManager; |
| } |
| } |