package limitedwip.watchdog.components; import com.intellij.openapi.application.ApplicationManager; import com.intellij.openapi.diff.impl.ComparisonPolicy; import com.intellij.openapi.diff.impl.fragments.LineFragment; import com.intellij.openapi.diff.impl.processing.DiffPolicy; import com.intellij.openapi.diff.impl.processing.HighlightMode; import com.intellij.openapi.diff.impl.processing.TextCompareProcessor; import com.intellij.openapi.editor.Document; import com.intellij.openapi.editor.event.DocumentEvent; import com.intellij.openapi.editor.event.DocumentListener; import com.intellij.openapi.fileEditor.FileDocumentManager; import com.intellij.openapi.project.Project; import com.intellij.openapi.util.Computable; import com.intellij.openapi.util.Pair; import com.intellij.openapi.vcs.VcsException; import com.intellij.openapi.vcs.changes.*; import com.intellij.openapi.vfs.VirtualFile; import com.intellij.util.diff.FilesTooBigForDiffException; import limitedwip.common.PluginId; import limitedwip.watchdog.ChangeSize; import org.jetbrains.annotations.Nullable; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; import static com.intellij.openapi.diff.impl.util.TextDiffTypeEnum.*; public class ChangeSizeCalculator { private final ChangeSizeCache changeSizeCache; private final Project project; private ChangeSize changeSize; private volatile boolean isRunningBackgroundDiff; public ChangeSizeCalculator(Project project) { this.project = project; this.changeSize = new ChangeSize(0, true); this.changeSizeCache = new ChangeSizeCache(); } public ChangeSize currentChangeListSizeInLines() { return changeSize; } public void onTimer() { calculateCurrentChangeListSizeInLines(); } /** * Can't use com.intellij.openapi.vcs.impl.LineStatusTrackerManager here because it only tracks changes for open files. */ private void calculateCurrentChangeListSizeInLines() { if (isRunningBackgroundDiff) return; final Pair<ChangeSize, List<Change>> pair = ApplicationManager.getApplication().runReadAction(new Computable<Pair<ChangeSize, List<Change>>>() { @Override public Pair<ChangeSize, List<Change>> compute() { LocalChangeList changeList = ChangeListManager.getInstance(project).getDefaultChangeList(); List<Change> changesToDiff = new ArrayList<Change>(); ChangeSize result = new ChangeSize(0); for (Change change : changeList.getChanges()) { Document document = getDocumentFor(change); ChangeSize changeSize = changeSizeCache.get(document); if (changeSize == null) { changesToDiff.add(change); } else { result = result.add(changeSize); } } return Pair.create(result, changesToDiff); } }); if (pair.second.isEmpty()) { changeSize = pair.first; return; } new Thread(new Runnable() { @Override public void run() { isRunningBackgroundDiff = true; TextCompareProcessor compareProcessor = new TextCompareProcessor( ComparisonPolicy.TRIM_SPACE, DiffPolicy.LINES_WO_FORMATTING, HighlightMode.BY_LINE ); final Map<Change, ChangeSize> changeSizeByChange = new HashMap<Change, ChangeSize>(); for (Change change : pair.second) { changeSizeByChange.put(change, currentChangeListSizeInLines(change, compareProcessor)); } ApplicationManager.getApplication().invokeLater(new Runnable() { @Override public void run() { changeSize = pair.first; for (ChangeSize it : changeSizeByChange.values()) { changeSize = changeSize.add(it); } for (Map.Entry<Change, ChangeSize> entry : changeSizeByChange.entrySet()) { Document document = getDocumentFor(entry.getKey()); if (document != null && !entry.getValue().isApproximate) { changeSizeCache.put(document, entry.getValue()); } } isRunningBackgroundDiff = false; } }); } }, PluginId.value + "-DiffThread").start(); } @Nullable private static Document getDocumentFor(Change change) { VirtualFile virtualFile = change.getVirtualFile(); return virtualFile == null ? null : FileDocumentManager.getInstance().getDocument(virtualFile); } private static ChangeSize currentChangeListSizeInLines(Change change, TextCompareProcessor compareProcessor) { try { return amountOfChangedLinesIn(change, compareProcessor); } catch (VcsException ignored) { return new ChangeSize(0, true); } catch (FilesTooBigForDiffException ignored) { return new ChangeSize(0, true); } } private static ChangeSize amountOfChangedLinesIn(Change change, TextCompareProcessor compareProcessor) throws VcsException, FilesTooBigForDiffException { ContentRevision beforeRevision = change.getBeforeRevision(); ContentRevision afterRevision = change.getAfterRevision(); if (beforeRevision instanceof FakeRevision || afterRevision instanceof FakeRevision) { return new ChangeSize(0, true); } ContentRevision revision = afterRevision; if (revision == null) revision = beforeRevision; if (revision == null || revision.getFile().getFileType().isBinary()) return new ChangeSize(0); String contentBefore = beforeRevision != null ? beforeRevision.getContent() : ""; String contentAfter = afterRevision != null ? afterRevision.getContent() : ""; if (contentBefore == null) contentBefore = ""; if (contentAfter == null) contentAfter = ""; int result = 0; for (LineFragment fragment : compareProcessor.process(contentBefore, contentAfter)) { if (fragment.getType() == DELETED) { result += fragment.getModifiedLines1(); } else if (fragment.getType() == CHANGED || fragment.getType() == CONFLICT || fragment.getType() == INSERT) { result += fragment.getModifiedLines2(); } } return new ChangeSize(result); } private static class ChangeSizeCache { private final Map<Document, ChangeSize> changeSizeByDocument = new HashMap<Document, ChangeSize>(); public void put(final Document document, ChangeSize changeSize) { if (document == null) return; changeSizeByDocument.put(document, changeSize); document.addDocumentListener(new DocumentListener() { @Override public void beforeDocumentChange(DocumentEvent event) {} @Override public void documentChanged(DocumentEvent event) { remove(document); document.removeDocumentListener(this); } }); } public void remove(Document document) { changeSizeByDocument.remove(document); } public ChangeSize get(Document document) { if (document == null) return null; return changeSizeByDocument.get(document); } } }