blob: c327d760e10df17df1e184b99712da3ae684a852 [file] [log] [blame]
/*
* Copyright (c) 2000-2006 JetBrains s.r.o. All Rights Reserved.
*/
package com.intellij.coverage;
import com.intellij.codeInsight.CodeInsightBundle;
import com.intellij.history.FileRevisionTimestampComparator;
import com.intellij.history.LocalHistory;
import com.intellij.icons.AllIcons;
import com.intellij.openapi.Disposable;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.editor.Document;
import com.intellij.openapi.editor.Editor;
import com.intellij.openapi.editor.colors.EditorColorsManager;
import com.intellij.openapi.editor.colors.EditorColorsScheme;
import com.intellij.openapi.editor.event.DocumentAdapter;
import com.intellij.openapi.editor.event.DocumentEvent;
import com.intellij.openapi.editor.event.DocumentListener;
import com.intellij.openapi.editor.impl.DocumentMarkupModel;
import com.intellij.openapi.editor.markup.*;
import com.intellij.openapi.fileEditor.FileEditor;
import com.intellij.openapi.fileEditor.FileEditorManager;
import com.intellij.openapi.fileEditor.TextEditor;
import com.intellij.openapi.fileEditor.impl.LoadTextUtil;
import com.intellij.openapi.module.Module;
import com.intellij.openapi.module.ModuleUtilCore;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.roots.ProjectFileIndex;
import com.intellij.openapi.roots.ProjectRootManager;
import com.intellij.openapi.util.Computable;
import com.intellij.openapi.util.Key;
import com.intellij.openapi.util.Ref;
import com.intellij.openapi.util.TextRange;
import com.intellij.openapi.util.text.LineTokenizer;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.psi.PsiFile;
import com.intellij.reference.SoftReference;
import com.intellij.rt.coverage.data.ClassData;
import com.intellij.rt.coverage.data.LineCoverage;
import com.intellij.rt.coverage.data.LineData;
import com.intellij.rt.coverage.data.ProjectData;
import com.intellij.ui.EditorNotificationPanel;
import com.intellij.util.Alarm;
import com.intellij.util.Function;
import com.intellij.util.diff.Diff;
import com.intellij.util.diff.FilesTooBigForDiffException;
import gnu.trove.TIntIntHashMap;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.io.File;
import java.util.*;
/**
* @author ven
*/
public class SrcFileAnnotator implements Disposable {
private static final Logger LOG = Logger.getInstance("#com.intellij.coverage.SrcFileAnnotator");
public static final Key<List<RangeHighlighter>> COVERAGE_HIGHLIGHTERS = Key.create("COVERAGE_HIGHLIGHTERS");
private static final Key<DocumentListener> COVERAGE_DOCUMENT_LISTENER = Key.create("COVERAGE_DOCUMENT_LISTENER");
public static final Key<Map<FileEditor, EditorNotificationPanel>> NOTIFICATION_PANELS = Key.create("NOTIFICATION_PANELS");
private PsiFile myFile;
private Editor myEditor;
private Document myDocument;
private final Project myProject;
private SoftReference<TIntIntHashMap> myNewToOldLines;
private SoftReference<TIntIntHashMap> myOldToNewLines;
private SoftReference<byte[]> myOldContent;
private final static Object LOCK = new Object();
private final Alarm myUpdateAlarm = new Alarm(Alarm.ThreadToUse.POOLED_THREAD, this);
public SrcFileAnnotator(final PsiFile file, final Editor editor) {
myFile = file;
myEditor = editor;
myProject = file.getProject();
myDocument = myEditor.getDocument();
}
public void hideCoverageData() {
if (myEditor == null) return;
final FileEditorManager fileEditorManager = FileEditorManager.getInstance(myProject);
final List<RangeHighlighter> highlighters = myEditor.getUserData(COVERAGE_HIGHLIGHTERS);
if (highlighters != null) {
for (final RangeHighlighter highlighter : highlighters) {
ApplicationManager.getApplication().invokeLater(new Runnable() {
@Override
public void run() {
highlighter.dispose();
}
});
}
myEditor.putUserData(COVERAGE_HIGHLIGHTERS, null);
}
final Map<FileEditor, EditorNotificationPanel> map = myFile.getCopyableUserData(NOTIFICATION_PANELS);
if (map != null) {
final VirtualFile vFile = myFile.getVirtualFile();
LOG.assertTrue(vFile != null);
boolean freeAll = !fileEditorManager.isFileOpen(vFile);
myFile.putCopyableUserData(NOTIFICATION_PANELS, null);
for (FileEditor fileEditor : map.keySet()) {
if (!freeAll && !isCurrentEditor(fileEditor)) {
continue;
}
fileEditorManager.removeTopComponent(fileEditor, map.get(fileEditor));
}
}
final DocumentListener documentListener = myEditor.getUserData(COVERAGE_DOCUMENT_LISTENER);
if (documentListener != null) {
myDocument.removeDocumentListener(documentListener);
myEditor.putUserData(COVERAGE_DOCUMENT_LISTENER, null);
}
}
private static
@NotNull
String[] getCoveredLines(@NotNull byte[] oldContent, VirtualFile vFile) {
final String text = LoadTextUtil.getTextByBinaryPresentation(oldContent, vFile, false, false).toString();
return LineTokenizer.tokenize(text, false);
}
private
@NotNull
String[] getUpToDateLines() {
final Ref<String[]> linesRef = new Ref<String[]>();
final Runnable runnable = new Runnable() {
public void run() {
final int lineCount = myDocument.getLineCount();
final String[] lines = new String[lineCount];
final CharSequence chars = myDocument.getCharsSequence();
for (int i = 0; i < lineCount; i++) {
lines[i] = chars.subSequence(myDocument.getLineStartOffset(i), myDocument.getLineEndOffset(i)).toString();
}
linesRef.set(lines);
}
};
ApplicationManager.getApplication().runReadAction(runnable);
return linesRef.get();
}
private static TIntIntHashMap getCoverageVersionToCurrentLineMapping(Diff.Change change, int firstNLines) {
TIntIntHashMap result = new TIntIntHashMap();
int prevLineInFirst = 0;
int prevLineInSecond = 0;
while (change != null) {
for (int l = 0; l < change.line0 - prevLineInFirst; l++) {
result.put(prevLineInFirst + l, prevLineInSecond + l);
}
prevLineInFirst = change.line0 + change.deleted;
prevLineInSecond = change.line1 + change.inserted;
change = change.link;
}
for (int i = prevLineInFirst; i < firstNLines; i++) {
result.put(i, prevLineInSecond + i - prevLineInFirst);
}
return result;
}
@Nullable
private TIntIntHashMap getOldToNewLineMapping(final long date) {
if (myOldToNewLines == null) {
myOldToNewLines = doGetLineMapping(date, true);
if (myOldToNewLines == null) return null;
}
return myOldToNewLines.get();
}
@Nullable
private TIntIntHashMap getNewToOldLineMapping(final long date) {
if (myNewToOldLines == null) {
myNewToOldLines = doGetLineMapping(date, false);
if (myNewToOldLines == null) return null;
}
return myNewToOldLines.get();
}
@Nullable
private SoftReference<TIntIntHashMap> doGetLineMapping(final long date, boolean oldToNew) {
final VirtualFile f = getVirtualFile();
final byte[] oldContent;
synchronized (LOCK) {
if (myOldContent == null) {
if (ApplicationManager.getApplication().isDispatchThread()) return null;
final byte[] byteContent = LocalHistory.getInstance().getByteContent(f, new FileRevisionTimestampComparator() {
public boolean isSuitable(long revisionTimestamp) {
return revisionTimestamp < date;
}
});
myOldContent = new SoftReference<byte[]>(byteContent);
}
oldContent = myOldContent.get();
}
if (oldContent == null) return null;
String[] coveredLines = getCoveredLines(oldContent, f);
String[] currentLines = getUpToDateLines();
String[] oldLines = oldToNew ? coveredLines : currentLines;
String[] newLines = oldToNew ? currentLines : coveredLines;
Diff.Change change = null;
try {
change = Diff.buildChanges(oldLines, newLines);
}
catch (FilesTooBigForDiffException e) {
LOG.info(e);
return null;
}
return new SoftReference<TIntIntHashMap>(getCoverageVersionToCurrentLineMapping(change, oldLines.length));
}
public void showCoverageInformation(final CoverageSuitesBundle suite) {
if (myEditor == null || myFile == null) return;
final MarkupModel markupModel = DocumentMarkupModel.forDocument(myDocument, myProject, true);
final List<RangeHighlighter> highlighters = new ArrayList<RangeHighlighter>();
final ProjectData data = suite.getCoverageData();
if (data == null) {
coverageDataNotFound(suite);
return;
}
final CoverageEngine engine = suite.getCoverageEngine();
final Set<String> qualifiedNames = engine.getQualifiedNames(myFile);
// let's find old content in local history and build mapping from old lines to new one
// local history doesn't index libraries, so let's distinguish libraries content with other one
final ProjectFileIndex projectFileIndex = ProjectRootManager.getInstance(myProject).getFileIndex();
final VirtualFile file = getVirtualFile();
final long fileTimeStamp = file.getTimeStamp();
final long coverageTimeStamp = suite.getLastCoverageTimeStamp();
final TIntIntHashMap oldToNewLineMapping;
//do not show coverage info over cls
if (engine.isInLibraryClasses(myProject, file)) {
return;
}
// if in libraries content
if (projectFileIndex.isInLibrarySource(file)) {
// compare file and coverage timestamps
if (fileTimeStamp > coverageTimeStamp) {
showEditorWarningMessage(CodeInsightBundle.message("coverage.data.outdated"));
return;
}
oldToNewLineMapping = null;
}
else {
// check local history
oldToNewLineMapping = getOldToNewLineMapping(coverageTimeStamp);
if (oldToNewLineMapping == null) {
// if history for file isn't available let's check timestamps
if (fileTimeStamp > coverageTimeStamp && classesArePresentInCoverageData(data, qualifiedNames)) {
showEditorWarningMessage(CodeInsightBundle.message("coverage.data.outdated"));
return;
}
}
}
if (myEditor.getUserData(COVERAGE_HIGHLIGHTERS) != null) {
//highlighters already collected - no need to do it twice
return;
}
final Module module = ApplicationManager.getApplication().runReadAction(new Computable<Module>() {
@Nullable
@Override
public Module compute() {
return ModuleUtilCore.findModuleForPsiElement(myFile);
}
});
if (module != null) {
if (engine.recompileProjectAndRerunAction(module, suite, new Runnable() {
public void run() {
CoverageDataManager.getInstance(myProject).chooseSuitesBundle(suite);
}
})) {
return;
}
}
// now if oldToNewLineMapping is null we should use f(x)=id(x) mapping
// E.g. all *.class files for java source file with several classes
final Set<File> outputFiles = engine.getCorrespondingOutputFiles(myFile, module, suite);
final boolean subCoverageActive = CoverageDataManager.getInstance(myProject).isSubCoverageActive();
final boolean coverageByTestApplicable = suite.isCoverageByTestApplicable() && !(subCoverageActive && suite.isCoverageByTestEnabled());
final TreeMap<Integer, LineData> executableLines = new TreeMap<Integer, LineData>();
final TreeMap<Integer, Object[]> classLines = new TreeMap<Integer, Object[]>();
final TreeMap<Integer, String> classNames = new TreeMap<Integer, String>();
class HighlightersCollector {
private void collect(File outputFile, final String qualifiedName) {
final ClassData fileData = data.getClassData(qualifiedName);
if (fileData != null) {
final Object[] lines = fileData.getLines();
if (lines != null) {
final Object[] postProcessedLines = suite.getCoverageEngine().postProcessExecutableLines(lines, myEditor);
for (Object lineData : postProcessedLines) {
if (lineData instanceof LineData) {
final int line = ((LineData)lineData).getLineNumber() - 1;
final int lineNumberInCurrent;
if (oldToNewLineMapping != null) {
// use mapping based on local history
if (!oldToNewLineMapping.contains(line)) {
continue;
}
lineNumberInCurrent = oldToNewLineMapping.get(line);
}
else {
// use id mapping
lineNumberInCurrent = line;
}
LOG.assertTrue(lineNumberInCurrent < myDocument.getLineCount());
executableLines.put(line, (LineData)lineData);
classLines.put(line, postProcessedLines);
classNames.put(line, qualifiedName);
ApplicationManager.getApplication().invokeLater(new Runnable() {
public void run() {
if (myDocument == null || lineNumberInCurrent >= myDocument.getLineCount()) return;
final RangeHighlighter highlighter =
createRangeHighlighter(suite.getLastCoverageTimeStamp(), markupModel, coverageByTestApplicable, executableLines,
qualifiedName, line, lineNumberInCurrent, suite, postProcessedLines);
highlighters.add(highlighter);
}
});
}
}
}
}
else if (outputFile != null &&
!subCoverageActive &&
engine.includeUntouchedFileInCoverage(qualifiedName, outputFile, myFile, suite)) {
collectNonCoveredFileInfo(outputFile, highlighters, markupModel, executableLines, coverageByTestApplicable);
}
}
}
final HighlightersCollector collector = new HighlightersCollector();
if (!outputFiles.isEmpty()) {
for (File outputFile : outputFiles) {
final String qualifiedName = engine.getQualifiedName(outputFile, myFile);
if (qualifiedName != null) {
collector.collect(outputFile, qualifiedName);
}
}
}
else { //check non-compilable classes which present in ProjectData
for (String qName : qualifiedNames) {
collector.collect(null, qName);
}
}
ApplicationManager.getApplication().invokeLater(new Runnable() {
public void run() {
if (myEditor != null && highlighters.size() > 0) {
myEditor.putUserData(COVERAGE_HIGHLIGHTERS, highlighters);
}
}
});
final DocumentListener documentListener = new DocumentAdapter() {
@Override
public void documentChanged(final DocumentEvent e) {
myNewToOldLines = null;
myOldToNewLines = null;
List<RangeHighlighter> rangeHighlighters = myEditor.getUserData(COVERAGE_HIGHLIGHTERS);
if (rangeHighlighters == null) rangeHighlighters = new ArrayList<RangeHighlighter>();
int offset = e.getOffset();
final int lineNumber = myDocument.getLineNumber(offset);
final int lastLineNumber = myDocument.getLineNumber(offset + e.getNewLength());
final TextRange changeRange =
new TextRange(myDocument.getLineStartOffset(lineNumber), myDocument.getLineEndOffset(lastLineNumber));
for (Iterator<RangeHighlighter> it = rangeHighlighters.iterator(); it.hasNext(); ) {
final RangeHighlighter highlighter = it.next();
if (!highlighter.isValid() || TextRange.create(highlighter).intersects(changeRange)) {
highlighter.dispose();
it.remove();
}
}
final List<RangeHighlighter> highlighters = rangeHighlighters;
myUpdateAlarm.cancelAllRequests();
if (!myUpdateAlarm.isDisposed()) {
myUpdateAlarm.addRequest(new Runnable() {
@Override
public void run() {
final TIntIntHashMap newToOldLineMapping = getNewToOldLineMapping(suite.getLastCoverageTimeStamp());
if (newToOldLineMapping != null) {
ApplicationManager.getApplication().invokeLater(new Runnable() {
public void run() {
if (myEditor == null) return;
for (int line = lineNumber; line <= lastLineNumber; line++) {
final int oldLineNumber = newToOldLineMapping.get(line);
final LineData lineData = executableLines.get(oldLineNumber);
if (lineData != null) {
RangeHighlighter rangeHighlighter =
createRangeHighlighter(suite.getLastCoverageTimeStamp(), markupModel, coverageByTestApplicable, executableLines,
classNames.get(oldLineNumber), oldLineNumber, line, suite,
classLines.get(oldLineNumber));
highlighters.add(rangeHighlighter);
}
}
myEditor.putUserData(COVERAGE_HIGHLIGHTERS, highlighters.size() > 0 ? highlighters : null);
}
});
}
}
}, 100);
}
}
};
myDocument.addDocumentListener(documentListener);
myEditor.putUserData(COVERAGE_DOCUMENT_LISTENER, documentListener);
}
private static boolean classesArePresentInCoverageData(ProjectData data, Set<String> qualifiedNames) {
for (String qualifiedName : qualifiedNames) {
if (data.getClassData(qualifiedName) != null) {
return true;
}
}
return false;
}
private RangeHighlighter createRangeHighlighter(final long date, final MarkupModel markupModel,
final boolean coverageByTestApplicable,
final TreeMap<Integer, LineData> executableLines, @Nullable final String className,
final int line,
final int lineNumberInCurrent,
@NotNull final CoverageSuitesBundle coverageSuite, Object[] lines) {
EditorColorsScheme scheme = EditorColorsManager.getInstance().getGlobalScheme();
final TextAttributes attributes = scheme.getAttributes(CoverageLineMarkerRenderer.getAttributesKey(line, executableLines));
TextAttributes textAttributes = null;
if (attributes.getBackgroundColor() != null) {
textAttributes = attributes;
}
final int startOffset = myDocument.getLineStartOffset(lineNumberInCurrent);
final int endOffset = myDocument.getLineEndOffset(lineNumberInCurrent);
final RangeHighlighter highlighter =
markupModel.addRangeHighlighter(startOffset, endOffset, HighlighterLayer.SELECTION - 1, textAttributes, HighlighterTargetArea.LINES_IN_RANGE);
final Function<Integer, Integer> newToOldConverter = new Function<Integer, Integer>() {
public Integer fun(final Integer newLine) {
if (myEditor == null) return -1;
final TIntIntHashMap oldLineMapping = getNewToOldLineMapping(date);
return oldLineMapping != null ? oldLineMapping.get(newLine.intValue()) : newLine.intValue();
}
};
final Function<Integer, Integer> oldToNewConverter = new Function<Integer, Integer>() {
public Integer fun(final Integer newLine) {
if (myEditor == null) return -1;
final TIntIntHashMap newLineMapping = getOldToNewLineMapping(date);
return newLineMapping != null ? newLineMapping.get(newLine.intValue()) : newLine.intValue();
}
};
final CoverageLineMarkerRenderer markerRenderer = coverageSuite.getCoverageEngine()
.getLineMarkerRenderer(line, className, executableLines, coverageByTestApplicable, coverageSuite, newToOldConverter,
oldToNewConverter, CoverageDataManager.getInstance(myProject).isSubCoverageActive());
highlighter.setLineMarkerRenderer(markerRenderer);
final LineData lineData = className != null ? (LineData)lines[line + 1] : null;
if (lineData != null && lineData.getStatus() == LineCoverage.NONE) {
highlighter.setErrorStripeMarkColor(markerRenderer.getErrorStripeColor(myEditor));
highlighter.setThinErrorStripeMark(true);
highlighter.setGreedyToLeft(true);
highlighter.setGreedyToRight(true);
}
return highlighter;
}
private void showEditorWarningMessage(final String message) {
ApplicationManager.getApplication().invokeLater(new Runnable() {
public void run() {
if (myEditor == null) return;
final FileEditorManager fileEditorManager = FileEditorManager.getInstance(myProject);
final VirtualFile vFile = myFile.getVirtualFile();
assert vFile != null;
Map<FileEditor, EditorNotificationPanel> map = myFile.getCopyableUserData(NOTIFICATION_PANELS);
if (map == null) {
map = new HashMap<FileEditor, EditorNotificationPanel>();
myFile.putCopyableUserData(NOTIFICATION_PANELS, map);
}
final FileEditor[] editors = fileEditorManager.getAllEditors(vFile);
for (final FileEditor editor : editors) {
if (isCurrentEditor(editor)) {
final EditorNotificationPanel panel = new EditorNotificationPanel() {
{
myLabel.setIcon(AllIcons.General.ExclMark);
myLabel.setText(message);
}
};
panel.createActionLabel("Close", new Runnable() {
@Override
public void run() {
fileEditorManager.removeTopComponent(editor, panel);
}
});
map.put(editor, panel);
fileEditorManager.addTopComponent(editor, panel);
break;
}
}
}
});
}
private boolean isCurrentEditor(FileEditor editor) {
return editor instanceof TextEditor && ((TextEditor)editor).getEditor() == myEditor;
}
private void collectNonCoveredFileInfo(final File outputFile,
final List<RangeHighlighter> highlighters, final MarkupModel markupModel,
final TreeMap<Integer, LineData> executableLines,
final boolean coverageByTestApplicable) {
final CoverageSuitesBundle coverageSuite = CoverageDataManager.getInstance(myProject).getCurrentSuitesBundle();
if (coverageSuite == null) return;
final TIntIntHashMap mapping;
if (outputFile.lastModified() < getVirtualFile().getTimeStamp()) {
mapping = getOldToNewLineMapping(outputFile.lastModified());
if (mapping == null) return;
}
else {
mapping = null;
}
final List<Integer> uncoveredLines = coverageSuite.getCoverageEngine().collectSrcLinesForUntouchedFile(outputFile, coverageSuite);
final int lineCount = myDocument.getLineCount();
if (uncoveredLines == null) {
for (int lineNumber = 0; lineNumber < lineCount; lineNumber++) {
addHighlighter(outputFile, highlighters, markupModel, executableLines, coverageByTestApplicable, coverageSuite,
lineNumber, lineNumber);
}
}
else {
for (int lineNumber : uncoveredLines) {
if (lineNumber >= lineCount) {
continue;
}
final int updatedLineNumber = mapping != null ? mapping.get(lineNumber) : lineNumber;
addHighlighter(outputFile, highlighters, markupModel, executableLines, coverageByTestApplicable, coverageSuite,
lineNumber, updatedLineNumber);
}
}
}
private void addHighlighter(final File outputFile,
final List<RangeHighlighter> highlighters,
final MarkupModel markupModel,
final TreeMap<Integer, LineData> executableLines,
final boolean coverageByTestApplicable,
final CoverageSuitesBundle coverageSuite,
final int lineNumber,
final int updatedLineNumber) {
executableLines.put(updatedLineNumber, null);
ApplicationManager.getApplication().invokeLater(new Runnable() {
public void run() {
if (myEditor == null) return;
final RangeHighlighter highlighter =
createRangeHighlighter(outputFile.lastModified(), markupModel, coverageByTestApplicable, executableLines, null, lineNumber,
updatedLineNumber, coverageSuite, null);
highlighters.add(highlighter);
}
});
}
private VirtualFile getVirtualFile() {
final VirtualFile vFile = myFile.getVirtualFile();
LOG.assertTrue(vFile != null);
return vFile;
}
private void coverageDataNotFound(final CoverageSuitesBundle suite) {
showEditorWarningMessage(CodeInsightBundle.message("coverage.data.not.found"));
for (CoverageSuite coverageSuite : suite.getSuites()) {
CoverageDataManager.getInstance(myProject).removeCoverageSuite(coverageSuite);
}
}
public void dispose() {
hideCoverageData();
myEditor = null;
myDocument = null;
myFile = null;
}
}