| /* |
| * 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.ide.SelectInEditorManager; |
| import com.intellij.injected.editor.VirtualFileWindow; |
| import com.intellij.openapi.actionSystem.DataKey; |
| import com.intellij.openapi.actionSystem.DataSink; |
| import com.intellij.openapi.actionSystem.TypeSafeDataProvider; |
| import com.intellij.openapi.application.ApplicationManager; |
| import com.intellij.openapi.editor.Document; |
| import com.intellij.openapi.editor.Editor; |
| import com.intellij.openapi.editor.markup.TextAttributes; |
| import com.intellij.openapi.fileEditor.*; |
| import com.intellij.openapi.module.Module; |
| import com.intellij.openapi.project.Project; |
| import com.intellij.openapi.roots.*; |
| import com.intellij.openapi.util.Comparing; |
| import com.intellij.openapi.util.Computable; |
| import com.intellij.openapi.util.Pair; |
| import com.intellij.openapi.util.Segment; |
| import com.intellij.openapi.vfs.VirtualFile; |
| import com.intellij.psi.*; |
| import com.intellij.reference.SoftReference; |
| import com.intellij.ui.SimpleTextAttributes; |
| import com.intellij.usageView.UsageInfo; |
| import com.intellij.usageView.UsageViewBundle; |
| import com.intellij.usages.impl.rules.UsageType; |
| import com.intellij.usages.rules.*; |
| import com.intellij.util.*; |
| import org.jetbrains.annotations.NotNull; |
| import org.jetbrains.annotations.Nullable; |
| |
| import javax.swing.*; |
| import java.lang.ref.Reference; |
| import java.util.Arrays; |
| import java.util.Comparator; |
| import java.util.List; |
| |
| /** |
| * @author max |
| */ |
| public class UsageInfo2UsageAdapter implements UsageInModule, |
| UsageInLibrary, UsageInFile, PsiElementUsage, |
| MergeableUsage, Comparable<UsageInfo2UsageAdapter>, |
| RenameableUsage, TypeSafeDataProvider, UsagePresentation { |
| public static final NotNullFunction<UsageInfo, Usage> CONVERTER = new NotNullFunction<UsageInfo, Usage>() { |
| @Override |
| @NotNull |
| public Usage fun(UsageInfo usageInfo) { |
| return new UsageInfo2UsageAdapter(usageInfo); |
| } |
| }; |
| private static final Comparator<UsageInfo> BY_NAVIGATION_OFFSET = new Comparator<UsageInfo>() { |
| @Override |
| public int compare(UsageInfo o1, UsageInfo o2) { |
| return o1.getNavigationOffset() - o2.getNavigationOffset(); |
| } |
| }; |
| |
| private final UsageInfo myUsageInfo; |
| @NotNull |
| private Object myMergedUsageInfos; // contains all merged infos, including myUsageInfo. Either UsageInfo or UsageInfo[] |
| private final int myLineNumber; |
| private final int myOffset; |
| protected Icon myIcon; |
| private volatile Reference<TextChunk[]> myTextChunks; // allow to be gced and recreated on-demand because it requires a lot of memory |
| private volatile UsageType myUsageType; |
| |
| public UsageInfo2UsageAdapter(@NotNull final UsageInfo usageInfo) { |
| myUsageInfo = usageInfo; |
| myMergedUsageInfos = usageInfo; |
| |
| Pair<Integer,Integer> data = |
| ApplicationManager.getApplication().runReadAction(new Computable<Pair<Integer,Integer>>() { |
| @Override |
| public Pair<Integer, Integer> compute() { |
| PsiElement element = getElement(); |
| PsiFile psiFile = usageInfo.getFile(); |
| Document document = psiFile == null ? null : PsiDocumentManager.getInstance(getProject()).getDocument(psiFile); |
| |
| int offset; |
| int lineNumber; |
| if (document == null) { |
| // element over light virtual file |
| offset = element.getTextOffset(); |
| lineNumber = -1; |
| } |
| else { |
| int startOffset = myUsageInfo.getNavigationOffset(); |
| if (startOffset == -1) { |
| offset = element == null ? 0 : element.getTextOffset(); |
| lineNumber = -1; |
| } |
| else { |
| offset = -1; |
| lineNumber = getLineNumber(document, startOffset); |
| } |
| } |
| return Pair.create(offset, lineNumber); |
| } |
| }); |
| myOffset = data.first; |
| myLineNumber = data.second; |
| myModificationStamp = getCurrentModificationStamp(); |
| } |
| |
| private static int getLineNumber(@NotNull Document document, final int startOffset) { |
| if (document.getTextLength() == 0) return 0; |
| if (startOffset >= document.getTextLength()) return document.getLineCount(); |
| return document.getLineNumber(startOffset); |
| } |
| |
| @NotNull |
| private TextChunk[] initChunks() { |
| PsiFile psiFile = getPsiFile(); |
| Document document = psiFile == null ? null : PsiDocumentManager.getInstance(getProject()).getDocument(psiFile); |
| TextChunk[] chunks; |
| if (document == null) { |
| // element over light virtual file |
| PsiElement element = getElement(); |
| if (element == null) { |
| chunks = new TextChunk[]{new TextChunk(SimpleTextAttributes.ERROR_ATTRIBUTES.toTextAttributes(), UsageViewBundle.message("node.invalid"))}; |
| } |
| else { |
| chunks = new TextChunk[] {new TextChunk(new TextAttributes(), element.getText())}; |
| } |
| } |
| else { |
| chunks = ChunkExtractor.extractChunks(psiFile, this); |
| } |
| |
| myTextChunks = new SoftReference<TextChunk[]>(chunks); |
| return chunks; |
| } |
| |
| @Override |
| @NotNull |
| public UsagePresentation getPresentation() { |
| return this; |
| } |
| |
| @Override |
| public boolean isValid() { |
| PsiElement element = getElement(); |
| if (element == null || !element.isValid()) { |
| return false; |
| } |
| for (UsageInfo usageInfo : getMergedInfos()) { |
| if (!usageInfo.isValid()) return false; |
| } |
| return true; |
| } |
| |
| @Override |
| public boolean isReadOnly() { |
| PsiFile psiFile = getPsiFile(); |
| return psiFile == null || psiFile.isValid() && !psiFile.isWritable(); |
| } |
| |
| @Override |
| @Nullable |
| public FileEditorLocation getLocation() { |
| VirtualFile virtualFile = getFile(); |
| if (virtualFile == null) return null; |
| FileEditor editor = FileEditorManager.getInstance(getProject()).getSelectedEditor(virtualFile); |
| if (!(editor instanceof TextEditor)) return null; |
| |
| Segment segment = getUsageInfo().getSegment(); |
| if (segment == null) return null; |
| return new TextEditorLocation(segment.getStartOffset(), (TextEditor)editor); |
| } |
| |
| @Override |
| public void selectInEditor() { |
| if (!isValid()) return; |
| Editor editor = openTextEditor(true); |
| Segment marker = getFirstSegment(); |
| editor.getSelectionModel().setSelection(marker.getStartOffset(), marker.getEndOffset()); |
| } |
| |
| @Override |
| public void highlightInEditor() { |
| if (!isValid()) return; |
| |
| Segment marker = getFirstSegment(); |
| SelectInEditorManager.getInstance(getProject()).selectInEditor(getFile(), marker.getStartOffset(), marker.getEndOffset(), false, false); |
| } |
| |
| private Segment getFirstSegment() { |
| return getUsageInfo().getSegment(); |
| } |
| |
| // must iterate in start offset order |
| public boolean processRangeMarkers(@NotNull Processor<Segment> processor) { |
| for (UsageInfo usageInfo : getMergedInfos()) { |
| Segment segment = usageInfo.getSegment(); |
| if (segment != null && !processor.process(segment)) { |
| return false; |
| } |
| } |
| return true; |
| } |
| |
| public Document getDocument() { |
| PsiFile file = getUsageInfo().getFile(); |
| if (file == null) return null; |
| return PsiDocumentManager.getInstance(getProject()).getDocument(file); |
| } |
| |
| |
| @Override |
| public void navigate(boolean focus) { |
| if (canNavigate()) { |
| openTextEditor(focus); |
| } |
| } |
| |
| public Editor openTextEditor(boolean focus) { |
| return FileEditorManager.getInstance(getProject()).openTextEditor(getDescriptor(), focus); |
| } |
| |
| @Override |
| public boolean canNavigate() { |
| VirtualFile file = getFile(); |
| return file != null && file.isValid(); |
| } |
| |
| @Override |
| public boolean canNavigateToSource() { |
| return canNavigate(); |
| } |
| |
| @Nullable |
| private OpenFileDescriptor getDescriptor() { |
| VirtualFile file = getFile(); |
| if(file == null) return null; |
| int offset = getNavigationOffset(); |
| return new OpenFileDescriptor(getProject(), file, offset); |
| } |
| |
| int getNavigationOffset() { |
| Document document = getDocument(); |
| if (document == null) return -1; |
| int offset = getUsageInfo().getNavigationOffset(); |
| if (offset == -1) offset = myOffset; |
| if (offset >= document.getTextLength()) { |
| int line = Math.max(0, Math.min(myLineNumber, document.getLineCount() - 1)); |
| offset = document.getLineStartOffset(line); |
| } |
| return offset; |
| } |
| |
| @NotNull |
| private Project getProject() { |
| return getUsageInfo().getProject(); |
| } |
| |
| public String toString() { |
| TextChunk[] textChunks = getPresentation().getText(); |
| StringBuilder result = new StringBuilder(); |
| for (int j = 0; j < textChunks.length; j++) { |
| if (j > 0) result.append("|"); |
| TextChunk textChunk = textChunks[j]; |
| result.append(textChunk); |
| } |
| |
| return result.toString(); |
| } |
| |
| @Override |
| public Module getModule() { |
| if (!isValid()) return null; |
| VirtualFile virtualFile = getFile(); |
| if (virtualFile == null) return null; |
| |
| ProjectRootManager projectRootManager = ProjectRootManager.getInstance(getProject()); |
| ProjectFileIndex fileIndex = projectRootManager.getFileIndex(); |
| return fileIndex.getModuleForFile(virtualFile); |
| } |
| |
| @Override |
| public OrderEntry getLibraryEntry() { |
| if (!isValid()) return null; |
| PsiFile psiFile = getPsiFile(); |
| VirtualFile virtualFile = getFile(); |
| if (virtualFile == null) return null; |
| |
| ProjectRootManager projectRootManager = ProjectRootManager.getInstance(getProject()); |
| ProjectFileIndex fileIndex = projectRootManager.getFileIndex(); |
| |
| if (psiFile instanceof PsiCompiledElement || fileIndex.isInLibrarySource(virtualFile)) { |
| List<OrderEntry> orders = fileIndex.getOrderEntriesForFile(virtualFile); |
| for (OrderEntry order : orders) { |
| if (order instanceof LibraryOrderEntry || order instanceof JdkOrderEntry) { |
| return order; |
| } |
| } |
| } |
| |
| return null; |
| } |
| |
| @Override |
| public VirtualFile getFile() { |
| return getUsageInfo().getVirtualFile(); |
| } |
| private PsiFile getPsiFile() { |
| return getUsageInfo().getFile(); |
| } |
| |
| public int getLine() { |
| return myLineNumber; |
| } |
| |
| @Override |
| public boolean merge(@NotNull MergeableUsage other) { |
| if (!(other instanceof UsageInfo2UsageAdapter)) return false; |
| UsageInfo2UsageAdapter u2 = (UsageInfo2UsageAdapter)other; |
| assert u2 != this; |
| if (myLineNumber != u2.myLineNumber || !Comparing.equal(getFile(), u2.getFile())) return false; |
| UsageInfo[] merged = ArrayUtil.mergeArrays(getMergedInfos(), u2.getMergedInfos()); |
| myMergedUsageInfos = merged.length == 1 ? merged[0] : merged; |
| Arrays.sort(getMergedInfos(), BY_NAVIGATION_OFFSET); |
| myTextChunks = null; // chunks will be rebuilt lazily (IDEA-126048) |
| return true; |
| } |
| |
| @Override |
| public void reset() { |
| ApplicationManager.getApplication().assertIsDispatchThread(); |
| myMergedUsageInfos = myUsageInfo; |
| initChunks(); |
| } |
| |
| @Override |
| public final PsiElement getElement() { |
| return getUsageInfo().getElement(); |
| } |
| |
| public PsiReference getReference() { |
| return getElement().getReference(); |
| } |
| |
| @Override |
| public boolean isNonCodeUsage() { |
| return getUsageInfo().isNonCodeUsage; |
| } |
| |
| @NotNull |
| public UsageInfo getUsageInfo() { |
| return myUsageInfo; |
| } |
| |
| // by start offset |
| @Override |
| public int compareTo(@NotNull final UsageInfo2UsageAdapter o) { |
| VirtualFile containingFile = getFile(); |
| int shift1 = 0; |
| if (containingFile instanceof VirtualFileWindow) { |
| shift1 = ((VirtualFileWindow)containingFile).getDocumentWindow().injectedToHost(0); |
| containingFile = ((VirtualFileWindow)containingFile).getDelegate(); |
| } |
| VirtualFile oContainingFile = o.getFile(); |
| int shift2 = 0; |
| if (oContainingFile instanceof VirtualFileWindow) { |
| shift2 = ((VirtualFileWindow)oContainingFile).getDocumentWindow().injectedToHost(0); |
| oContainingFile = ((VirtualFileWindow)oContainingFile).getDelegate(); |
| } |
| if (containingFile == null && oContainingFile == null || !Comparing.equal(containingFile, oContainingFile)) { |
| return 0; |
| } |
| Segment s1 = getFirstSegment(); |
| Segment s2 = o.getFirstSegment(); |
| if (s1 == null || s2 == null) return 0; |
| return s1.getStartOffset() + shift1 - s2.getStartOffset() - shift2; |
| } |
| |
| @Override |
| public boolean equals(Object obj) { |
| return super.equals(obj); |
| } |
| |
| @Override |
| public void rename(String newName) throws IncorrectOperationException { |
| final PsiReference reference = getUsageInfo().getReference(); |
| assert reference != null : this; |
| reference.handleElementRename(newName); |
| } |
| |
| @NotNull |
| public static UsageInfo2UsageAdapter[] convert(@NotNull UsageInfo[] usageInfos) { |
| UsageInfo2UsageAdapter[] result = new UsageInfo2UsageAdapter[usageInfos.length]; |
| for (int i = 0; i < result.length; i++) { |
| result[i] = new UsageInfo2UsageAdapter(usageInfos[i]); |
| } |
| |
| return result; |
| } |
| |
| @Override |
| public void calcData(final DataKey key, final DataSink sink) { |
| if (key == UsageView.USAGE_INFO_KEY) { |
| sink.put(UsageView.USAGE_INFO_KEY, getUsageInfo()); |
| } |
| if (key == UsageView.USAGE_INFO_LIST_KEY) { |
| List<UsageInfo> list = Arrays.asList(getMergedInfos()); |
| sink.put(UsageView.USAGE_INFO_LIST_KEY, list); |
| } |
| } |
| |
| @NotNull |
| private UsageInfo[] getMergedInfos() { |
| Object infos = myMergedUsageInfos; |
| return infos instanceof UsageInfo ? new UsageInfo[]{(UsageInfo)infos} : (UsageInfo[])infos; |
| } |
| |
| private long myModificationStamp; |
| private long getCurrentModificationStamp() { |
| final PsiFile containingFile = getPsiFile(); |
| return containingFile == null ? -1L : containingFile.getViewProvider().getModificationStamp(); |
| } |
| |
| @Override |
| @NotNull |
| public TextChunk[] getText() { |
| TextChunk[] chunks = SoftReference.dereference(myTextChunks); |
| final long currentModificationStamp = getCurrentModificationStamp(); |
| boolean isModified = currentModificationStamp != myModificationStamp; |
| if (chunks == null || isValid() && isModified) { |
| // the check below makes sense only for valid PsiElement |
| chunks = initChunks(); |
| myModificationStamp = currentModificationStamp; |
| } |
| return chunks; |
| } |
| |
| @Override |
| @NotNull |
| public String getPlainText() { |
| int startOffset = getNavigationOffset(); |
| final PsiElement element = getElement(); |
| if (element != null && startOffset != -1) { |
| final Document document = getDocument(); |
| if (document != null) { |
| int lineNumber = document.getLineNumber(startOffset); |
| int lineStart = document.getLineStartOffset(lineNumber); |
| int lineEnd = document.getLineEndOffset(lineNumber); |
| String prefixSuffix = null; |
| |
| if (lineEnd - lineStart > ChunkExtractor.MAX_LINE_LENGTH_TO_SHOW) { |
| prefixSuffix = "..."; |
| lineStart = Math.max(startOffset - ChunkExtractor.OFFSET_BEFORE_TO_SHOW_WHEN_LONG_LINE, lineStart); |
| lineEnd = Math.min(startOffset + ChunkExtractor.OFFSET_AFTER_TO_SHOW_WHEN_LONG_LINE, lineEnd); |
| } |
| String s = document.getCharsSequence().subSequence(lineStart, lineEnd).toString(); |
| if (prefixSuffix != null) s = prefixSuffix + s + prefixSuffix; |
| return s; |
| } |
| } |
| return UsageViewBundle.message("node.invalid"); |
| } |
| |
| @Override |
| public Icon getIcon() { |
| Icon icon = myIcon; |
| if (icon == null) { |
| PsiElement psiElement = getElement(); |
| myIcon = icon = psiElement != null && psiElement.isValid() && !isFindInPathUsage(psiElement) ? psiElement.getIcon(0) : null; |
| } |
| return icon; |
| } |
| |
| private boolean isFindInPathUsage(PsiElement psiElement) { |
| return psiElement instanceof PsiFile && getUsageInfo().getPsiFileRange() != null; |
| } |
| |
| @Override |
| public String getTooltipText() { |
| return myUsageInfo.getTooltipText(); |
| } |
| |
| public @Nullable UsageType getUsageType() { |
| UsageType usageType = myUsageType; |
| |
| if (usageType == null) { |
| usageType = UsageType.UNCLASSIFIED; |
| PsiFile file = getPsiFile(); |
| |
| if (file != null) { |
| ChunkExtractor extractor = ChunkExtractor.getExtractor(file); |
| Segment segment = getFirstSegment(); |
| |
| if (segment != null) { |
| Document document = PsiDocumentManager.getInstance(getProject()).getDocument(file); |
| |
| if (document != null) { |
| SmartList<TextChunk> chunks = new SmartList<TextChunk>(); |
| extractor.createTextChunks( |
| this, |
| document.getCharsSequence(), |
| segment.getStartOffset(), |
| segment.getEndOffset(), |
| false, |
| chunks |
| ); |
| |
| for(TextChunk chunk:chunks) { |
| UsageType chunkUsageType = chunk.getType(); |
| if (chunkUsageType != null) { |
| usageType = chunkUsageType; |
| break; |
| } |
| } |
| } |
| } |
| } |
| myUsageType = usageType; |
| } |
| return usageType; |
| } |
| } |