/*
* Copyright 2000-2015 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.application.ReadAction;
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.*;
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 consulo.ide.IconDescriptorUpdaters;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import javax.swing.*;
import java.awt.*;
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 = UsageInfo2UsageAdapter::new;
private static final Comparator<UsageInfo> BY_NAVIGATION_OFFSET = Comparator.comparingInt(UsageInfo::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;
Point data = ReadAction.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 == null ? 0 : 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 new Point(offset, lineNumber);
});
myOffset = data.x;
myLineNumber = data.y;
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<>(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 true;
}
return false;
}
@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();
if (marker != null) {
editor.getSelectionModel().setSelection(marker.getStartOffset(), marker.getEndOffset());
}
}
@Override
public void highlightInEditor() {
if (!isValid()) return;
Segment marker = getFirstSegment();
if (marker != null) {
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();
}
private OpenFileDescriptor getDescriptor() {
VirtualFile file = getFile();
if (file == null) return null;
Segment range = getNavigationRange();
if (range != null && file instanceof VirtualFileWindow && range.getStartOffset() >= 0) {
// have to use injectedToHost(TextRange) to calculate right offset in case of multiple shreds
range = ((VirtualFileWindow)file).getDocumentWindow().injectedToHost(TextRange.create(range));
file = ((VirtualFileWindow)file).getDelegate();
}
return new OpenFileDescriptor(getProject(), file, range == null ? getNavigationOffset() : range.getStartOffset());
}
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;
}
private Segment getNavigationRange() {
Document document = getDocument();
if (document == null) return null;
Segment range = getUsageInfo().getNavigationRange();
if (range == null) {
ProperTextRange rangeInElement = getUsageInfo().getRangeInElement();
range = myOffset < 0 ? new UnfairTextRange(-1, -1) : rangeInElement == null ? TextRange.from(myOffset, 1) : rangeInElement.shiftRight(myOffset);
}
if (range.getEndOffset() >= document.getTextLength()) {
int line = Math.max(0, Math.min(myLineNumber, document.getLineCount() - 1));
range = TextRange.from(document.getLineStartOffset(line), 1);
}
return range;
}
@NotNull
private Project getProject() {
return getUsageInfo().getProject();
}
@Override
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 ModuleExtensionWithSdkOrderEntry) {
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) {
return getUsageInfo().compareToByStartOffset(o.getUsageInfo());
}
@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() ? IconDescriptorUpdaters.getIcon(psiElement, 0) : null;
}
return icon;
}
private boolean isFindInPathUsage(PsiElement psiElement) {
return psiElement instanceof PsiFile && getUsageInfo().getPsiFileRange() != null;
}
@Override
public String getTooltipText() {
return myUsageInfo.getTooltipText();
}
@Nullable
public UsageType getUsageType() {
UsageType usageType = myUsageType;
if (usageType == null) {
usageType = UsageType.UNCLASSIFIED;
PsiFile file = getPsiFile();
if (file != null) {
Segment segment = getFirstSegment();
if (segment != null) {
Document document = PsiDocumentManager.getInstance(getProject()).getDocument(file);
if (document != null) {
ChunkExtractor extractor = ChunkExtractor.getExtractor(file);
SmartList<TextChunk> chunks = new SmartList<>();
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;
}
}