void (final CoverageSuitesBundle suite) { // Store the values of myFile and myEditor in local variables to avoid an NPE after dispose() has been called in the EDT. final PsiFile psiFile = myFile; final Editor editor = myEditor; final Document document = myDocument; if (editor == null || psiFile == null || document == null) return; final VirtualFile file = getVirtualFile(psiFile); if (file == null || !file.isValid()) return; if (getOrCreateHighlighters(false) != null) { //highlighters already collected - no need to do it twice return; } final CoverageEngine engine = suite.getCoverageEngine(); if (engine.isInLibraryClasses(myProject, file)) { return; } final Module module = ReadAction.compute(() -> ModuleUtilCore.findModuleForPsiElement(psiFile)); if (module != null) { if (engine.recompileProjectAndRerunAction(module, suite, () -> CoverageDataManager.getInstance(myProject).chooseSuitesBundle(suite))) { return; } } // let's find old content in local history and build mapping from old lines to new one final long fileTimeStamp = file.getTimeStamp(); final long coverageTimeStamp = suite.getLastCoverageTimeStamp(); synchronized (this) { if (myMapper == null || myMapper.getTimeStamp() != coverageTimeStamp) { myMapper = new LineHistoryMapper(myProject, file, document, coverageTimeStamp); } } final Int2IntMap oldToNewLineMapping; // local history doesn't index libraries, so let's distinguish libraries content with other one if (engine.isInLibrarySource(myProject, file)) { if (fileTimeStamp > coverageTimeStamp) { showEditorWarningMessage(CoverageBundle.message("coverage.data.outdated")); return; } oldToNewLineMapping = null; } else if (myMapper.canGetFastMapping()) { oldToNewLineMapping = myMapper.getOldToNewLineMapping(); if (oldToNewLineMapping == null) { // if history for file isn't available let's check timestamps if (fileTimeStamp > coverageTimeStamp && classesArePresentInCoverageData(suite, engine.getQualifiedNames(psiFile))) { showEditorWarningMessage(CoverageBundle.message("coverage.data.outdated")); return; } } } else { oldToNewLineMapping = null; ApplicationManager.getApplication().executeOnPooledThread(() -> { var mapping = myMapper.getOldToNewLineMapping(); if (mapping == null) return; removeHighlighters(); myUpdateAlarm.cancelAllRequests(); showCoverage(suite); }); } // now if oldToNewLineMapping is null we should use f(x) = x mapping final MarkupModel markupModel = DocumentMarkupModel.forDocument(document, myProject, true); if (getOrCreateHighlighters(true) == null) return; final TreeMap<Integer, LineData> executableLines = new TreeMap<>(); final TreeMap<Integer, String> classNames = new TreeMap<>(); collectLinesInFile(suite, psiFile, module, oldToNewLineMapping, markupModel, executableLines, classNames); if (editor.getUserData(COVERAGE_DOCUMENT_LISTENER) == null) { final DocumentListener documentListener = new DocumentListener() { @Override public void documentChanged(@NotNull final DocumentEvent e) { myMapper.clear(); int offset = e.getOffset(); final int lineNumber = document.getLineNumber(offset); final int lastLineNumber = document.getLineNumber(offset + e.getNewLength()); if (!removeChangedHighlighters(lineNumber, lastLineNumber, document)) return; if (!myUpdateAlarm.isDisposed()) { myUpdateAlarm.addRequest(() -> { Int2IntMap newToOldLineMapping = myMapper.canGetFastMapping() ? myMapper.getNewToOldLineMapping() : null; if (newToOldLineMapping != null) { final int lastLine = Math.min(document.getLineCount() - 1, lastLineNumber); for (int line = lineNumber; line <= lastLine; line++) { if (!newToOldLineMapping.containsKey(line)) continue; int oldLineNumber = newToOldLineMapping.get(line); var lineData = executableLines.get(oldLineNumber); if (lineData == null) continue; addHighlighter(markupModel, executableLines, suite, oldLineNumber, line, classNames.get(oldLineNumber)); } } }, 100); } } }; document.addDocumentListener(documentListener); editor.putUserData(COVERAGE_DOCUMENT_LISTENER, documentListener); } }