| /* |
| * Copyright 2000-2013 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.usages; |
| |
| import com.intellij.injected.editor.DocumentWindow; |
| import com.intellij.lang.injection.InjectedLanguageManager; |
| import com.intellij.lexer.Lexer; |
| import com.intellij.openapi.diagnostic.Logger; |
| import com.intellij.openapi.editor.*; |
| import com.intellij.openapi.editor.colors.EditorColorsScheme; |
| import com.intellij.openapi.editor.colors.TextAttributesKey; |
| import com.intellij.openapi.editor.markup.TextAttributes; |
| import com.intellij.openapi.fileTypes.FileType; |
| import com.intellij.openapi.fileTypes.PlainSyntaxHighlighter; |
| import com.intellij.openapi.fileTypes.SyntaxHighlighter; |
| import com.intellij.openapi.fileTypes.SyntaxHighlighterFactory; |
| import com.intellij.openapi.project.Project; |
| import com.intellij.openapi.util.Segment; |
| import com.intellij.openapi.util.TextRange; |
| import com.intellij.openapi.util.text.StringUtil; |
| import com.intellij.psi.PsiDocumentManager; |
| import com.intellij.psi.PsiFile; |
| import com.intellij.psi.tree.IElementType; |
| import com.intellij.reference.SoftReference; |
| import com.intellij.usageView.UsageTreeColors; |
| import com.intellij.usageView.UsageTreeColorsScheme; |
| import com.intellij.usages.impl.SyntaxHighlighterOverEditorHighlighter; |
| import com.intellij.usages.impl.rules.UsageType; |
| import com.intellij.util.Processor; |
| import com.intellij.util.containers.FactoryMap; |
| import com.intellij.util.text.CharArrayUtil; |
| import com.intellij.util.text.StringFactory; |
| import org.jetbrains.annotations.NotNull; |
| import org.jetbrains.annotations.Nullable; |
| |
| import java.awt.*; |
| import java.lang.ref.WeakReference; |
| import java.util.ArrayList; |
| import java.util.List; |
| import java.util.Map; |
| |
| /** |
| * @author peter |
| */ |
| public class ChunkExtractor { |
| private static final Logger LOG = Logger.getInstance("#com.intellij.usages.ChunkExtractor"); |
| public static final int MAX_LINE_LENGTH_TO_SHOW = 200; |
| public static final int OFFSET_BEFORE_TO_SHOW_WHEN_LONG_LINE = 1; |
| public static final int OFFSET_AFTER_TO_SHOW_WHEN_LONG_LINE = 1; |
| |
| private final EditorColorsScheme myColorsScheme; |
| |
| private final Document myDocument; |
| private long myDocumentStamp; |
| private final SyntaxHighlighterOverEditorHighlighter myHighlighter; |
| |
| private abstract static class WeakFactory<T> { |
| private WeakReference<T> myRef; |
| |
| @NotNull |
| protected abstract T create(); |
| |
| @NotNull |
| public T getValue() { |
| final T cur = SoftReference.dereference(myRef); |
| if (cur != null) return cur; |
| final T result = create(); |
| myRef = new WeakReference<T>(result); |
| return result; |
| } |
| } |
| |
| private static final ThreadLocal<WeakFactory<Map<PsiFile, ChunkExtractor>>> ourExtractors = new ThreadLocal<WeakFactory<Map<PsiFile, ChunkExtractor>>>() { |
| @Override |
| protected WeakFactory<Map<PsiFile, ChunkExtractor>> initialValue() { |
| return new WeakFactory<Map<PsiFile, ChunkExtractor>>() { |
| @NotNull |
| @Override |
| protected Map<PsiFile, ChunkExtractor> create() { |
| return new FactoryMap<PsiFile, ChunkExtractor>() { |
| @Override |
| protected ChunkExtractor create(PsiFile psiFile) { |
| return new ChunkExtractor(psiFile); |
| } |
| }; |
| } |
| }; |
| } |
| }; |
| |
| @NotNull |
| public static TextChunk[] extractChunks(@NotNull PsiFile file, @NotNull UsageInfo2UsageAdapter usageAdapter) { |
| return getExtractor(file).extractChunks(usageAdapter, file); |
| } |
| |
| @NotNull |
| public static ChunkExtractor getExtractor(@NotNull PsiFile file) { |
| return ourExtractors.get().getValue().get(file); |
| } |
| |
| private ChunkExtractor(@NotNull PsiFile file) { |
| myColorsScheme = UsageTreeColorsScheme.getInstance().getScheme(); |
| |
| Project project = file.getProject(); |
| myDocument = PsiDocumentManager.getInstance(project).getDocument(file); |
| LOG.assertTrue(myDocument != null); |
| final FileType fileType = file.getFileType(); |
| SyntaxHighlighter highlighter = SyntaxHighlighterFactory.getSyntaxHighlighter(fileType, project, file.getVirtualFile()); |
| highlighter = highlighter == null ? new PlainSyntaxHighlighter() : highlighter; |
| myHighlighter = new SyntaxHighlighterOverEditorHighlighter(highlighter, file.getVirtualFile(), project); |
| myDocumentStamp = -1; |
| } |
| |
| public static int getStartOffset(final List<RangeMarker> rangeMarkers) { |
| LOG.assertTrue(!rangeMarkers.isEmpty()); |
| int minStart = Integer.MAX_VALUE; |
| for (RangeMarker rangeMarker : rangeMarkers) { |
| if (!rangeMarker.isValid()) continue; |
| final int startOffset = rangeMarker.getStartOffset(); |
| if (startOffset < minStart) minStart = startOffset; |
| } |
| return minStart == Integer.MAX_VALUE ? -1 : minStart; |
| } |
| |
| @NotNull |
| private TextChunk[] extractChunks(@NotNull UsageInfo2UsageAdapter usageInfo2UsageAdapter, @NotNull PsiFile file) { |
| int absoluteStartOffset = usageInfo2UsageAdapter.getNavigationOffset(); |
| if (absoluteStartOffset == -1) return TextChunk.EMPTY_ARRAY; |
| |
| Document visibleDocument = myDocument instanceof DocumentWindow ? ((DocumentWindow)myDocument).getDelegate() : myDocument; |
| int visibleStartOffset = myDocument instanceof DocumentWindow ? ((DocumentWindow)myDocument).injectedToHost(absoluteStartOffset) : absoluteStartOffset; |
| |
| int lineNumber = myDocument.getLineNumber(absoluteStartOffset); |
| int visibleLineNumber = visibleDocument.getLineNumber(visibleStartOffset); |
| int visibleColumnNumber = visibleStartOffset - visibleDocument.getLineStartOffset(visibleLineNumber); |
| final List<TextChunk> result = new ArrayList<TextChunk>(); |
| appendPrefix(result, visibleLineNumber, visibleColumnNumber); |
| |
| int fragmentToShowStart = myDocument.getLineStartOffset(lineNumber); |
| int fragmentToShowEnd = fragmentToShowStart < myDocument.getTextLength() ? myDocument.getLineEndOffset(lineNumber) : 0; |
| if (fragmentToShowStart > fragmentToShowEnd) return TextChunk.EMPTY_ARRAY; |
| |
| final CharSequence chars = myDocument.getCharsSequence(); |
| if (fragmentToShowEnd - fragmentToShowStart > MAX_LINE_LENGTH_TO_SHOW) { |
| final int lineStartOffset = fragmentToShowStart; |
| fragmentToShowStart = Math.max(lineStartOffset, absoluteStartOffset - OFFSET_BEFORE_TO_SHOW_WHEN_LONG_LINE); |
| |
| final int lineEndOffset = fragmentToShowEnd; |
| Segment segment = usageInfo2UsageAdapter.getUsageInfo().getSegment(); |
| int usage_length = segment != null ? segment.getEndOffset() - segment.getStartOffset():0; |
| fragmentToShowEnd = Math.min(lineEndOffset, absoluteStartOffset + usage_length + OFFSET_AFTER_TO_SHOW_WHEN_LONG_LINE); |
| |
| // if we search something like a word, then expand shown context from one symbol before / after at least for word boundary |
| // this should not cause restarts of the lexer as the tokens are usually words |
| if (usage_length > 0 && |
| StringUtil.isJavaIdentifierStart(chars.charAt(absoluteStartOffset)) && |
| StringUtil.isJavaIdentifierStart(chars.charAt(absoluteStartOffset + usage_length - 1))) { |
| while(fragmentToShowEnd < lineEndOffset && StringUtil.isJavaIdentifierStart(chars.charAt(fragmentToShowEnd - 1))) ++fragmentToShowEnd; |
| while(fragmentToShowStart > lineStartOffset && StringUtil.isJavaIdentifierStart(chars.charAt(fragmentToShowStart))) --fragmentToShowStart; |
| if (fragmentToShowStart != lineStartOffset) ++fragmentToShowStart; |
| if (fragmentToShowEnd != lineEndOffset) --fragmentToShowEnd; |
| } |
| } |
| if (myDocument instanceof DocumentWindow) { |
| List<TextRange> editable = InjectedLanguageManager.getInstance(file.getProject()) |
| .intersectWithAllEditableFragments(file, new TextRange(fragmentToShowStart, fragmentToShowEnd)); |
| for (TextRange range : editable) { |
| createTextChunks(usageInfo2UsageAdapter, chars, range.getStartOffset(), range.getEndOffset(), true, result); |
| } |
| return result.toArray(new TextChunk[result.size()]); |
| } |
| return createTextChunks(usageInfo2UsageAdapter, chars, fragmentToShowStart, fragmentToShowEnd, true, result); |
| } |
| |
| @NotNull |
| public TextChunk[] createTextChunks(@NotNull UsageInfo2UsageAdapter usageInfo2UsageAdapter, |
| @NotNull CharSequence chars, |
| int start, |
| int end, |
| boolean selectUsageWithBold, |
| @NotNull List<TextChunk> result) { |
| final Lexer lexer = myHighlighter.getHighlightingLexer(); |
| final SyntaxHighlighterOverEditorHighlighter highlighter = myHighlighter; |
| |
| LOG.assertTrue(start <= end); |
| |
| int i = StringUtil.indexOf(chars, '\n', start, end); |
| if (i != -1) end = i; |
| |
| if (myDocumentStamp != myDocument.getModificationStamp()) { |
| highlighter.restart(chars); |
| myDocumentStamp = myDocument.getModificationStamp(); |
| } else if(lexer.getTokenType() == null || lexer.getTokenStart() > start) { |
| highlighter.resetPosition(0); // todo restart from nearest position with initial state |
| } |
| |
| boolean isBeginning = true; |
| |
| for(;lexer.getTokenType() != null; lexer.advance()) { |
| int hiStart = lexer.getTokenStart(); |
| int hiEnd = lexer.getTokenEnd(); |
| |
| if (hiStart >= end) break; |
| |
| hiStart = Math.max(hiStart, start); |
| hiEnd = Math.min(hiEnd, end); |
| if (hiStart >= hiEnd) { continue; } |
| |
| if (isBeginning) { |
| String text = chars.subSequence(hiStart, hiEnd).toString(); |
| if(text.trim().isEmpty()) continue; |
| } |
| isBeginning = false; |
| IElementType tokenType = lexer.getTokenType(); |
| TextAttributesKey[] tokenHighlights = highlighter.getTokenHighlights(tokenType); |
| |
| processIntersectingRange(usageInfo2UsageAdapter, chars, hiStart, hiEnd, tokenHighlights, selectUsageWithBold, result); |
| } |
| |
| return result.toArray(new TextChunk[result.size()]); |
| } |
| |
| private void processIntersectingRange(@NotNull UsageInfo2UsageAdapter usageInfo2UsageAdapter, |
| @NotNull final CharSequence chars, |
| int hiStart, |
| final int hiEnd, |
| @NotNull final TextAttributesKey[] tokenHighlights, |
| final boolean selectUsageWithBold, |
| @NotNull final List<TextChunk> result) { |
| final TextAttributes originalAttrs = convertAttributes(tokenHighlights); |
| if (selectUsageWithBold) { |
| originalAttrs.setFontType(Font.PLAIN); |
| } |
| |
| final int[] lastOffset = {hiStart}; |
| usageInfo2UsageAdapter.processRangeMarkers(new Processor<Segment>() { |
| @Override |
| public boolean process(Segment segment) { |
| int usageStart = segment.getStartOffset(); |
| int usageEnd = segment.getEndOffset(); |
| if (rangeIntersect(lastOffset[0], hiEnd, usageStart, usageEnd)) { |
| addChunk(chars, lastOffset[0], Math.max(lastOffset[0], usageStart), originalAttrs, false, null, result); |
| |
| UsageType usageType = isHighlightedAsString(tokenHighlights) |
| ? UsageType.LITERAL_USAGE |
| : isHighlightedAsComment(tokenHighlights) ? UsageType.COMMENT_USAGE : null; |
| addChunk(chars, Math.max(lastOffset[0], usageStart), Math.min(hiEnd, usageEnd), originalAttrs, selectUsageWithBold, usageType, result); |
| lastOffset[0] = usageEnd; |
| if (usageEnd > hiEnd) { |
| return false; |
| } |
| } |
| return true; |
| } |
| }); |
| if (lastOffset[0] < hiEnd) { |
| addChunk(chars, lastOffset[0], hiEnd, originalAttrs, false, null, result); |
| } |
| } |
| |
| public static boolean isHighlightedAsComment(TextAttributesKey... keys) { |
| for (TextAttributesKey key : keys) { |
| if (key == DefaultLanguageHighlighterColors.DOC_COMMENT || |
| key == SyntaxHighlighterColors.DOC_COMMENT || |
| key == DefaultLanguageHighlighterColors.LINE_COMMENT || |
| key == SyntaxHighlighterColors.LINE_COMMENT || |
| key == DefaultLanguageHighlighterColors.BLOCK_COMMENT || |
| key == SyntaxHighlighterColors.JAVA_BLOCK_COMMENT |
| ) { |
| return true; |
| } |
| final TextAttributesKey fallbackAttributeKey = key.getFallbackAttributeKey(); |
| if (fallbackAttributeKey != null && isHighlightedAsComment(fallbackAttributeKey)) { |
| return true; |
| } |
| } |
| return false; |
| } |
| |
| public static boolean isHighlightedAsString(TextAttributesKey... keys) { |
| for (TextAttributesKey key : keys) { |
| if (key == DefaultLanguageHighlighterColors.STRING || key == SyntaxHighlighterColors.STRING) { |
| return true; |
| } |
| final TextAttributesKey fallbackAttributeKey = key.getFallbackAttributeKey(); |
| if (fallbackAttributeKey != null && isHighlightedAsString(fallbackAttributeKey)) { |
| return true; |
| } |
| } |
| return false; |
| } |
| |
| private static void addChunk(@NotNull CharSequence chars, |
| int start, |
| int end, |
| @NotNull TextAttributes originalAttrs, |
| boolean bold, |
| @Nullable UsageType usageType, |
| @NotNull List<TextChunk> result) { |
| if (start >= end) return; |
| |
| TextAttributes attrs = bold |
| ? TextAttributes.merge(originalAttrs, new TextAttributes(null, null, null, null, Font.BOLD)) |
| : originalAttrs; |
| result.add(new TextChunk(attrs, StringFactory.createShared(CharArrayUtil.fromSequence(chars, start, end)), usageType)); |
| } |
| |
| private static boolean rangeIntersect(int s1, int e1, int s2, int e2) { |
| return s2 < s1 && s1 < e2 || s2 < e1 && e1 < e2 |
| || s1 < s2 && s2 < e1 || s1 < e2 && e2 < e1 |
| || s1 == s2 && e1 == e2; |
| } |
| |
| @NotNull |
| private TextAttributes convertAttributes(@NotNull TextAttributesKey[] keys) { |
| TextAttributes attrs = myColorsScheme.getAttributes(HighlighterColors.TEXT); |
| |
| for (TextAttributesKey key : keys) { |
| TextAttributes attrs2 = myColorsScheme.getAttributes(key); |
| if (attrs2 != null) { |
| attrs = TextAttributes.merge(attrs, attrs2); |
| } |
| } |
| |
| attrs = attrs.clone(); |
| return attrs; |
| } |
| |
| private void appendPrefix(@NotNull List<TextChunk> result, int lineNumber, int columnNumber) { |
| String prefix = "(" + (lineNumber + 1) + ": " + (columnNumber + 1) + ") "; |
| TextChunk prefixChunk = new TextChunk(myColorsScheme.getAttributes(UsageTreeColors.USAGE_LOCATION), prefix); |
| result.add(prefixChunk); |
| } |
| } |