/* * Copyright 2000-2009 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.openapi.vcs.ex; import com.intellij.openapi.application.Application; import com.intellij.openapi.application.ApplicationManager; import com.intellij.openapi.command.undo.UndoConstants; import com.intellij.openapi.diagnostic.Logger; import com.intellij.openapi.editor.Document; import com.intellij.openapi.editor.event.DocumentAdapter; import com.intellij.openapi.editor.event.DocumentEvent; import com.intellij.openapi.editor.impl.DocumentImpl; import com.intellij.openapi.editor.impl.DocumentMarkupModel; import com.intellij.openapi.editor.markup.*; import com.intellij.openapi.fileEditor.FileDocumentManager; import com.intellij.openapi.fileEditor.FileEditor; import com.intellij.openapi.fileEditor.FileEditorManager; import com.intellij.openapi.progress.ProcessCanceledException; import com.intellij.openapi.project.Project; import com.intellij.openapi.util.Key; import com.intellij.openapi.util.TextRange; import com.intellij.openapi.util.registry.Registry; import com.intellij.openapi.util.text.StringUtil; import com.intellij.openapi.vcs.VcsBundle; import com.intellij.openapi.vcs.changes.VcsDirtyScopeManager; import com.intellij.openapi.vcs.history.VcsRevisionNumber; import com.intellij.openapi.vfs.VirtualFile; import com.intellij.ui.EditorNotificationPanel; import com.intellij.util.ExceptionUtil; import com.intellij.util.containers.ContainerUtil; import com.intellij.util.diff.FilesTooBigForDiffException; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import javax.swing.*; import java.util.*; /** * @author irengrig * author: lesya */ public class LineStatusTracker { private static final Logger LOG = Logger.getInstance("#com.intellij.openapi.vcs.ex.LineStatusTracker"); private static final Key PANEL_KEY = new Key("LineStatusTracker.CanNotCalculateDiffPanel"); private final Object myLock = new Object(); private BaseLoadState myBaseLoaded; private final Document myDocument; private final Document myVcsDocument; private List myRanges; private final Project myProject; private MyDocumentListener myDocumentListener; private boolean mySuppressUpdate; private boolean myBulkUpdate; private final Application myApplication; @Nullable private RevisionPack myBaseRevisionNumber; private String myPreviousBaseRevision; private boolean myAnathemaThrown; private FileEditorManager myFileEditorManager; private final VcsDirtyScopeManager myVcsDirtyScopeManager; private final VirtualFile myVirtualFile; private boolean myReleased = false; private LineStatusTracker(@NotNull final Document document, @NotNull final Document vcsDocument, final Project project, @Nullable final VirtualFile virtualFile) { myVirtualFile = virtualFile; myApplication = ApplicationManager.getApplication(); myDocument = document; myVcsDocument = vcsDocument; myVcsDocument.putUserData(UndoConstants.DONT_RECORD_UNDO, Boolean.TRUE); myProject = project; myBaseLoaded = BaseLoadState.LOADING; myRanges = new ArrayList(); myAnathemaThrown = false; myFileEditorManager = FileEditorManager.getInstance(myProject); myVcsDirtyScopeManager = VcsDirtyScopeManager.getInstance(myProject); } public void initialize(@NotNull final String vcsContent, @NotNull RevisionPack baseRevisionNumber) { ApplicationManager.getApplication().assertIsDispatchThread(); synchronized (myLock) { try { if (myReleased) return; if (myBaseRevisionNumber != null && myBaseRevisionNumber.after(baseRevisionNumber)) return; myBaseRevisionNumber = baseRevisionNumber; myPreviousBaseRevision = null; myVcsDocument.setReadOnly(false); myVcsDocument.replaceString(0, myVcsDocument.getTextLength(), vcsContent); myVcsDocument.setReadOnly(true); reinstallRanges(); if (myDocumentListener == null) { myDocumentListener = new MyDocumentListener(); myDocument.addDocumentListener(myDocumentListener); } } finally { myBaseLoaded = BaseLoadState.LOADED; } } } public void useCachedBaseRevision(final RevisionPack number) { synchronized (myLock) { assert myBaseRevisionNumber != null; if (myPreviousBaseRevision == null || myBaseRevisionNumber.after(number)) return; initialize(myPreviousBaseRevision, number); } } public boolean canUseBaseRevision(final RevisionPack number) { synchronized (myLock) { return myBaseRevisionNumber != null && myBaseRevisionNumber.equals(number) && myPreviousBaseRevision != null; } } private void reinstallRanges() { myApplication.assertReadAccessAllowed(); synchronized (myLock) { removeAnathema(); removeHighlightersFromMarkupModel(); try { myRanges = new RangesBuilder(myDocument, myVcsDocument).getRanges(); } catch (FilesTooBigForDiffException e) { myRanges.clear(); installAnathema(); return; } for (final Range range : myRanges) { range.setHighlighter(createHighlighter(range)); } } } private void removeAnathema() { if (!myAnathemaThrown) return; myAnathemaThrown = false; final FileEditor[] editors = myFileEditorManager.getEditors(myVirtualFile); for (FileEditor editor : editors) { final CanNotCalculateDiffPanel panel = editor.getUserData(PANEL_KEY); if (panel != null) { myFileEditorManager.removeTopComponent(editor, panel); editor.putUserData(PANEL_KEY, null); } } } @SuppressWarnings({"AutoBoxing"}) private RangeHighlighter createHighlighter(final Range range) { LOG.assertTrue(!myReleased, "Already released"); int first = range.getLine1() >= getLineCount(myDocument) ? myDocument.getTextLength() : myDocument.getLineStartOffset(range.getLine1()); int second = range.getLine2() >= getLineCount(myDocument) ? myDocument.getTextLength() : myDocument.getLineStartOffset(range.getLine2()); final RangeHighlighter highlighter = DocumentMarkupModel.forDocument(myDocument, myProject, true) .addRangeHighlighter(first, second, HighlighterLayer.FIRST - 1, null, HighlighterTargetArea.LINES_IN_RANGE); final TextAttributes attr = LineStatusTrackerDrawing.getAttributesFor(range); highlighter.setErrorStripeMarkColor(attr.getErrorStripeColor()); highlighter.setThinErrorStripeMark(true); highlighter.setGreedyToLeft(true); highlighter.setGreedyToRight(true); highlighter.setLineMarkerRenderer(LineStatusTrackerDrawing.createRenderer(range, this)); highlighter.setEditorFilter(MarkupEditorFilterFactory.createIsNotDiffFilter()); final String tooltip; if (range.getLine1() == range.getLine2()) { if (range.getVcsLine1() + 1 == range.getVcsLine2()) { tooltip = VcsBundle.message("tooltip.text.line.before.deleted", range.getLine1() + 1); } else { tooltip = VcsBundle.message("tooltip.text.lines.before.deleted", range.getLine1() + 1, range.getVcsLine2() - range.getVcsLine1()); } } else if (range.getLine1() + 1 == range.getLine2()) { tooltip = VcsBundle.message("tooltip.text.line.changed", range.getLine1() + 1); } else { tooltip = VcsBundle.message("tooltip.text.lines.changed", range.getLine1() + 1, range.getLine2()); } highlighter.setErrorStripeTooltip(tooltip); return highlighter; } public void release() { synchronized (myLock) { if (myDocumentListener != null) { myDocument.removeDocumentListener(myDocumentListener); } removeAnathema(); removeHighlightersFromMarkupModel(); myReleased = true; } } public Document getDocument() { return myDocument; } public VirtualFile getVirtualFile() { return myVirtualFile; } public List getRanges() { myApplication.assertReadAccessAllowed(); synchronized (myLock) { return myRanges; } } public Document getVcsDocument() { myApplication.assertIsDispatchThread(); return myVcsDocument; } public void startBulkUpdate() { synchronized (myLock) { if (myReleased) return; myBulkUpdate = true; removeAnathema(); removeHighlightersFromMarkupModel(); } } private void removeHighlightersFromMarkupModel() { synchronized (myLock) { for (Range range : myRanges) { if (range.getHighlighter() != null) { range.getHighlighter().dispose(); } range.invalidate(); } myRanges.clear(); } } public void finishBulkUpdate() { synchronized (myLock) { if (myReleased) return; myBulkUpdate = false; reinstallRanges(); } } /** * @return true if was cleared and base revision contents load should be started * false -> load was already started; after contents is loaded, */ public void resetForBaseRevisionLoad() { myApplication.assertReadAccessAllowed(); synchronized (myLock) { // there can be multiple resets before init -> take from document only firts time -> when right after install(), // where myPreviousBaseRevision become null if (BaseLoadState.LOADED.equals(myBaseLoaded) && myPreviousBaseRevision == null) { myPreviousBaseRevision = myVcsDocument.getText(); } myVcsDocument.setReadOnly(false); myVcsDocument.setText(""); myVcsDocument.setReadOnly(true); removeAnathema(); removeHighlightersFromMarkupModel(); myBaseLoaded = BaseLoadState.LOADING; } } private class MyDocumentListener extends DocumentAdapter { // We have 3 document versions: // * VCS version // * before change // * after change private int myLine1; private int myBeforeChangedLines; private int myBeforeTotalLines; @Override public void beforeDocumentChange(DocumentEvent e) { myApplication.assertWriteAccessAllowed(); synchronized (myLock) { if (myReleased) return; if (myBulkUpdate || mySuppressUpdate || myAnathemaThrown || BaseLoadState.LOADED != myBaseLoaded) return; assert myDocument == e.getDocument(); try { myLine1 = myDocument.getLineNumber(e.getOffset()); if (e.getOldLength() == 0) { myBeforeChangedLines = 1; } else { int line1 = myLine1; int line2 = myDocument.getLineNumber(e.getOffset() + e.getOldLength()); myBeforeChangedLines = line2 - line1 + 1; } myBeforeTotalLines = getLineCount(myDocument); } catch (ProcessCanceledException ignore) { } } } @Override public void documentChanged(final DocumentEvent e) { myApplication.assertWriteAccessAllowed(); synchronized (myLock) { if (myReleased) return; if (myBulkUpdate || mySuppressUpdate || myAnathemaThrown || BaseLoadState.LOADED != myBaseLoaded) return; assert myDocument == e.getDocument(); int afterChangedLines; if (e.getNewLength() == 0) { afterChangedLines = 1; } else { int line1 = myLine1; int line2 = myDocument.getLineNumber(e.getOffset() + e.getNewLength()); afterChangedLines = line2 - line1 + 1; } int linesShift = afterChangedLines - myBeforeChangedLines; int line1 = myLine1; int line2 = line1 + myBeforeChangedLines; // TODO: optimize some whole-line-changed cases int[] fixed = fixRanges(e, line1, line2); line1 = fixed[0]; line2 = fixed[1]; doUpdateRanges(line1, line2, linesShift, myBeforeTotalLines); } } } @NotNull private int[] fixRanges(@NotNull DocumentEvent e, int line1, int line2) { CharSequence document = myDocument.getCharsSequence(); int offset = e.getOffset(); if (e.getOldLength() == 0 && e.getNewLength() != 0) { if (StringUtil.endsWithChar(e.getNewFragment(), '\n') && isNewline(offset - 1, document)) { return new int[]{line1, line2 - 1}; } if (StringUtil.startsWithChar(e.getNewFragment(), '\n') && isNewline(offset + e.getNewLength(), document)) { return new int[]{line1 + 1, line2}; } } if (e.getOldLength() != 0 && e.getNewLength() == 0) { if (StringUtil.endsWithChar(e.getOldFragment(), '\n') && isNewline(offset - 1, document)) { return new int[]{line1, line2 - 1}; } if (StringUtil.startsWithChar(e.getOldFragment(), '\n') && isNewline(offset + e.getNewLength(), document)) { return new int[]{line1 + 1, line2}; } } return new int[]{line1, line2}; } private static boolean isNewline(int offset, @NotNull CharSequence sequence) { if (offset < 0) return false; if (offset >= sequence.length()) return false; return sequence.charAt(offset) == '\n'; } private void doUpdateRanges(int beforeChangedLine1, int beforeChangedLine2, int linesShift, int beforeTotalLines) { List rangesBeforeChange = new ArrayList(); List rangesAfterChange = new ArrayList(); List changedRanges = new ArrayList(); sortRanges(beforeChangedLine1, beforeChangedLine2, linesShift, rangesBeforeChange, changedRanges, rangesAfterChange); Range firstChangedRange = ContainerUtil.getFirstItem(changedRanges); Range lastChangedRange = ContainerUtil.getLastItem(changedRanges); if (firstChangedRange != null && firstChangedRange.getLine1() < beforeChangedLine1) { beforeChangedLine1 = firstChangedRange.getLine1(); } if (lastChangedRange != null && lastChangedRange.getLine2() > beforeChangedLine2) { beforeChangedLine2 = lastChangedRange.getLine2(); } doUpdateRanges(beforeChangedLine1, beforeChangedLine2, linesShift, beforeTotalLines, rangesBeforeChange, changedRanges, rangesAfterChange); } private void doUpdateRanges(int beforeChangedLine1, int beforeChangedLine2, int linesShift, // before -> after int beforeTotalLines, @NotNull List rangesBefore, @NotNull List changedRanges, @NotNull List rangesAfter) { try { int vcsTotalLines = getLineCount(myVcsDocument); Range lastRangeBefore = ContainerUtil.getLastItem(rangesBefore); Range firstRangeAfter = ContainerUtil.getFirstItem(rangesAfter); int afterChangedLine1 = beforeChangedLine1; int afterChangedLine2 = beforeChangedLine2 + linesShift; int vcsLine1 = getVcsLine1(lastRangeBefore, beforeChangedLine1); int vcsLine2 = getVcsLine2(firstRangeAfter, beforeChangedLine2, beforeTotalLines, vcsTotalLines); List newChangedRanges = getNewChangedRanges(afterChangedLine1, afterChangedLine2, vcsLine1, vcsLine2); shiftRanges(rangesAfter, linesShift); if (!changedRanges.equals(newChangedRanges)) { replaceRanges(changedRanges, newChangedRanges); myRanges = new ArrayList(rangesBefore.size() + newChangedRanges.size() + rangesAfter.size()); myRanges.addAll(rangesBefore); myRanges.addAll(newChangedRanges); myRanges.addAll(rangesAfter); for (Range range : myRanges) { if (!range.hasHighlighter()) range.setHighlighter(createHighlighter(range)); } if (myRanges.isEmpty() && myVirtualFile != null) { SwingUtilities.invokeLater(new Runnable() { @Override public void run() { FileDocumentManager.getInstance().saveDocument(myDocument); boolean stillEmpty; synchronized (myLock) { stillEmpty = myRanges.isEmpty(); } if (stillEmpty) { // file was modified, and now it's not -> dirty local change myVcsDirtyScopeManager.fileDirty(myVirtualFile); } } }); } } } catch (ProcessCanceledException ignore) { } catch (FilesTooBigForDiffException e1) { installAnathema(); removeHighlightersFromMarkupModel(); } } private static int getVcsLine1(@Nullable Range range, int line) { return range == null ? line : line + range.getVcsLine2() - range.getLine2(); } private static int getVcsLine2(@Nullable Range range, int line, int totalLinesBefore, int totalLinesAfter) { return range == null ? totalLinesAfter - totalLinesBefore + line : line + range.getVcsLine1() - range.getLine1(); } private List getNewChangedRanges(int changedLine1, int changedLine2, int vcsLine1, int vcsLine2) throws FilesTooBigForDiffException { if (changedLine1 == changedLine2 && vcsLine1 == vcsLine2) { return Collections.emptyList(); } if (changedLine1 == changedLine2) { return Collections.singletonList(new Range(changedLine1, changedLine2, vcsLine1, vcsLine2, Range.DELETED)); } if (vcsLine1 == vcsLine2) { return Collections.singletonList(new Range(changedLine1, changedLine2, vcsLine1, vcsLine2, Range.INSERTED)); } List lines = new DocumentWrapper(myDocument).getLines(changedLine1, changedLine2 - 1); List vcsLines = new DocumentWrapper(myVcsDocument).getLines(vcsLine1, vcsLine2 - 1); return new RangesBuilder(lines, vcsLines, changedLine1, vcsLine1).getRanges(); } private void replaceRanges(@NotNull List rangesInChange, @NotNull List newRangesInChange) { for (Range range : rangesInChange) { if (range.getHighlighter() != null) { range.getHighlighter().dispose(); } range.setHighlighter(null); range.invalidate(); } for (Range range : newRangesInChange) { range.setHighlighter(createHighlighter(range)); } } private static void shiftRanges(@NotNull List rangesAfterChange, int shift) { for (final Range range : rangesAfterChange) { range.shift(shift); } } private void sortRanges(int beforeChangedLine1, int beforeChangedLine2, int linesShift, @NotNull List rangesBeforeChange, @NotNull List changedRanges, @NotNull List rangesAfterChange) { if (!Registry.is("diff.status.tracker.skip.spaces")) { for (Range range : myRanges) { if (range.getLine2() < beforeChangedLine1) { rangesBeforeChange.add(range); } else if (range.getLine1() > beforeChangedLine2) { rangesAfterChange.add(range); } else { changedRanges.add(range); } } } else { int lastBefore = -1; int firstAfter = myRanges.size(); for (int i = 0; i < myRanges.size(); i++) { Range range = myRanges.get(i); if (range.getLine2() < beforeChangedLine1) { lastBefore = i; } else if (range.getLine1() > beforeChangedLine2) { firstAfter = i; break; } } // Expand on ranges, that are separated from changes only by empty/whitespaces lines // This is needed to reduce amount of confusing cases, when changed blocks are matched wrong due to matched empty lines between them CharSequence sequence = myDocument.getCharsSequence(); while (true) { if (lastBefore == -1) break; if (lastBefore < myRanges.size() - 1 && firstAfter - lastBefore > 1) { Range firstChangedRange = myRanges.get(lastBefore + 1); if (firstChangedRange.getLine1() < beforeChangedLine1) { beforeChangedLine1 = firstChangedRange.getLine1(); } } if (beforeChangedLine1 >= getLineCount(myDocument)) break; int offset1 = myDocument.getLineStartOffset(beforeChangedLine1) - 2; int deltaLines = 0; while (offset1 > 0) { char c = sequence.charAt(offset1); if (!StringUtil.isWhiteSpace(c)) break; if (c == '\n') deltaLines++; offset1--; } if (deltaLines == 0) break; beforeChangedLine1 -= deltaLines; if (myRanges.get(lastBefore).getLine2() < beforeChangedLine1) break; while (lastBefore != -1 && myRanges.get(lastBefore).getLine2() >= beforeChangedLine1) { lastBefore--; } } while (true) { if (firstAfter == myRanges.size()) break; if (firstAfter > 0 && firstAfter - lastBefore > 1) { Range lastChangedRange = myRanges.get(firstAfter - 1); if (lastChangedRange.getLine2() > beforeChangedLine2) { beforeChangedLine2 = lastChangedRange.getLine2(); } } if (beforeChangedLine2 + linesShift < 1) break; int offset2 = myDocument.getLineEndOffset(beforeChangedLine2 + linesShift - 1) + 1; int deltaLines = 0; while (offset2 < sequence.length()) { char c = sequence.charAt(offset2); if (!StringUtil.isWhiteSpace(c)) break; if (c == '\n') deltaLines++; offset2++; } if (deltaLines == 0) break; beforeChangedLine2 += deltaLines; if (myRanges.get(firstAfter).getLine1() > beforeChangedLine2) break; while (firstAfter != myRanges.size() && myRanges.get(firstAfter).getLine1() <= beforeChangedLine2) { firstAfter++; } } for (int i = 0; i < myRanges.size(); i++) { Range range = myRanges.get(i); if (i <= lastBefore) { rangesBeforeChange.add(range); } else if (i >= firstAfter) { rangesAfterChange.add(range); } else { changedRanges.add(range); } } } } @Nullable Range getNextRange(final Range range) { synchronized (myLock) { final int index = myRanges.indexOf(range); if (index == myRanges.size() - 1) return null; return myRanges.get(index + 1); } } @Nullable Range getPrevRange(final Range range) { synchronized (myLock) { final int index = myRanges.indexOf(range); if (index <= 0) return null; return myRanges.get(index - 1); } } @Nullable public Range getNextRange(final int line) { synchronized (myLock) { final Range currentRange = getRangeForLine(line); if (currentRange != null) { return getNextRange(currentRange); } for (final Range range : myRanges) { if (line > range.getLine1() || line > range.getLine2()) { continue; } return range; } return null; } } @Nullable public Range getPrevRange(final int line) { synchronized (myLock) { final Range currentRange = getRangeForLine(line); if (currentRange != null) { return getPrevRange(currentRange); } for (ListIterator iterator = myRanges.listIterator(myRanges.size()); iterator.hasPrevious(); ) { final Range range = iterator.previous(); if (range.getLine1() > line) { continue; } return range; } return null; } } @Nullable public Range getRangeForLine(final int line) { synchronized (myLock) { for (final Range range : myRanges) { if (range.getType() == Range.DELETED && line == range.getLine1()) { return range; } else if (line >= range.getLine1() && line < range.getLine2()) { return range; } } return null; } } private void doRollbackRange(@NotNull Range range) { if (range.getType() == Range.MODIFIED) { TextRange currentTextRange = getCurrentTextRange(range); int offset1 = currentTextRange.getStartOffset(); int offset2 = currentTextRange.getEndOffset(); CharSequence vcsContent = getVcsContent(range); myDocument.replaceString(offset1, offset2, vcsContent); } else if (range.getType() == Range.INSERTED) { TextRange currentTextRange = getCurrentTextRange(range); int offset1 = currentTextRange.getStartOffset(); int offset2 = currentTextRange.getEndOffset(); if (offset1 > 0) { offset1--; } else if (offset2 < myDocument.getTextLength()) { offset2++; } myDocument.deleteString(offset1, offset2); } else if (range.getType() == Range.DELETED) { CharSequence content = getVcsContent(range); if (range.getLine2() == getLineCount(myDocument)) { myDocument.insertString(myDocument.getTextLength(), "\n" + content); } else { myDocument.insertString(myDocument.getLineStartOffset(range.getLine2()), content + "\n"); } } else { throw new IllegalArgumentException("Unknown range type: " + range.getType()); } } public void rollbackChanges(@NotNull Range range) { myApplication.assertWriteAccessAllowed(); synchronized (myLock) { if (myBulkUpdate) return; if (!range.isValid()) { LOG.warn("Rollback of invalid range"); return; } doRollbackRange(range); } } public void rollbackChanges(@NotNull BitSet lines) { myApplication.assertWriteAccessAllowed(); synchronized (myLock) { if (myBulkUpdate) return; try { mySuppressUpdate = true; Range first = null; Range last = null; int shift = 0; for (Range range : myRanges) { if (!range.isValid()) { LOG.warn("Rollback of invalid range"); break; } boolean check; if (range.getLine1() == range.getLine2()) { check = lines.get(range.getLine1()); } else { int next = lines.nextSetBit(range.getLine1()); check = next != -1 && next < range.getLine2(); } if (check) { if (first == null) { first = range; } last = range; Range shiftedRange = new Range(range); shiftedRange.shift(shift); doRollbackRange(shiftedRange); shift += (range.getVcsLine2() - range.getVcsLine1()) - (range.getLine2() - range.getLine1()); } } if (first != null) { int beforeChangedLine1 = first.getLine1(); int beforeChangedLine2 = last.getLine2(); int beforeTotalLines = getLineCount(myDocument) - shift; doUpdateRanges(beforeChangedLine1, beforeChangedLine2, shift, beforeTotalLines); } } catch (Throwable e) { reinstallRanges(); if (e instanceof Error) throw ((Error)e); if (e instanceof RuntimeException) throw ((RuntimeException)e); throw new RuntimeException(e); } finally { mySuppressUpdate = false; } } } public CharSequence getVcsContent(@NotNull Range range) { synchronized (myLock) { TextRange textRange = getVcsRange(range); final int startOffset = textRange.getStartOffset(); final int endOffset = textRange.getEndOffset(); return myVcsDocument.getCharsSequence().subSequence(startOffset, endOffset); } } @NotNull TextRange getCurrentTextRange(@NotNull Range range) { synchronized (myLock) { if (!range.isValid()) { LOG.warn("Current TextRange of invalid range"); } return getRange(range.getLine1(), range.getLine2(), myDocument); } } @NotNull TextRange getVcsRange(@NotNull Range range) { synchronized (myLock) { if (!range.isValid()) { LOG.warn("Vcs TextRange of invalid range"); } return getRange(range.getVcsLine1(), range.getVcsLine2(), myVcsDocument); } } /** * Return affected range, without non-internal '\n' * so if last line is not empty, the last symbol will be not '\n' *

* So we consider '\n' not as a part of line, but a separator between lines */ @NotNull private static TextRange getRange(int line1, int line2, @NotNull Document document) { if (line1 == line2) { int lineStartOffset = line1 < getLineCount(document) ? document.getLineStartOffset(line1) : document.getTextLength(); return new TextRange(lineStartOffset, lineStartOffset); } else { int startOffset = document.getLineStartOffset(line1); int endOffset = document.getLineEndOffset(line2 - 1); return new TextRange(startOffset, endOffset); } } public static LineStatusTracker createOn(@Nullable VirtualFile virtualFile, @NotNull final Document doc, final Project project) { final Document document = new DocumentImpl("", true); return new LineStatusTracker(doc, document, project, virtualFile); } public void baseRevisionLoadFailed() { synchronized (myLock) { myBaseLoaded = BaseLoadState.FAILED; } } Project getProject() { return myProject; } public enum BaseLoadState { LOADING, FAILED, LOADED } public static class RevisionPack { private final long myNumber; private final VcsRevisionNumber myRevision; public RevisionPack(long number, VcsRevisionNumber revision) { myNumber = number; myRevision = revision; } public long getNumber() { return myNumber; } public VcsRevisionNumber getRevision() { return myRevision; } public boolean after(final RevisionPack previous) { if (myRevision.equals(previous.getRevision())) return false; return myNumber > previous.getNumber(); } @Override public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; RevisionPack that = (RevisionPack)o; return myRevision.equals(that.getRevision()); } @Override public int hashCode() { return myRevision.hashCode(); } } private void installAnathema() { myAnathemaThrown = true; final FileEditor[] editors = myFileEditorManager.getAllEditors(myVirtualFile); for (FileEditor editor : editors) { CanNotCalculateDiffPanel panel = editor.getUserData(PANEL_KEY); if (panel == null) { final CanNotCalculateDiffPanel newPanel = new CanNotCalculateDiffPanel(); editor.putUserData(PANEL_KEY, newPanel); myFileEditorManager.addTopComponent(editor, newPanel); } } } public static class CanNotCalculateDiffPanel extends EditorNotificationPanel { public CanNotCalculateDiffPanel() { myLabel.setText("Can not highlight changed lines. File is too big and there are too many changes."); } } private static int getLineCount(@NotNull Document document) { return Math.max(document.getLineCount(), 1); } }