diff options
Diffstat (limited to 'platform/vcs-impl/src/com/intellij/openapi/vcs/ex/LineStatusTracker.java')
-rw-r--r-- | platform/vcs-impl/src/com/intellij/openapi/vcs/ex/LineStatusTracker.java | 701 |
1 files changed, 416 insertions, 285 deletions
diff --git a/platform/vcs-impl/src/com/intellij/openapi/vcs/ex/LineStatusTracker.java b/platform/vcs-impl/src/com/intellij/openapi/vcs/ex/LineStatusTracker.java index 44a56e7df517..9d8d73a92be9 100644 --- a/platform/vcs-impl/src/com/intellij/openapi/vcs/ex/LineStatusTracker.java +++ b/platform/vcs-impl/src/com/intellij/openapi/vcs/ex/LineStatusTracker.java @@ -32,21 +32,21 @@ 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.ArrayList; -import java.util.List; -import java.util.ListIterator; +import java.util.*; /** * @author irengrig @@ -61,7 +61,7 @@ public class LineStatusTracker { private BaseLoadState myBaseLoaded; private final Document myDocument; - private final Document myUpToDateDocument; + private final Document myVcsDocument; private List<Range> myRanges; @@ -69,34 +69,35 @@ public class LineStatusTracker { 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 upToDateDocument, + @NotNull final Document vcsDocument, final Project project, @Nullable final VirtualFile virtualFile) { myVirtualFile = virtualFile; myApplication = ApplicationManager.getApplication(); myDocument = document; - myUpToDateDocument = upToDateDocument; - myUpToDateDocument.putUserData(UndoConstants.DONT_RECORD_UNDO, Boolean.TRUE); + myVcsDocument = vcsDocument; + myVcsDocument.putUserData(UndoConstants.DONT_RECORD_UNDO, Boolean.TRUE); myProject = project; myBaseLoaded = BaseLoadState.LOADING; - synchronized (myLock) { - myRanges = new ArrayList<Range>(); - } + myRanges = new ArrayList<Range>(); myAnathemaThrown = false; myFileEditorManager = FileEditorManager.getInstance(myProject); + myVcsDirtyScopeManager = VcsDirtyScopeManager.getInstance(myProject); } - public void initialize(@NotNull final String upToDateContent, @NotNull RevisionPack baseRevisionNumber) { + public void initialize(@NotNull final String vcsContent, @NotNull RevisionPack baseRevisionNumber) { ApplicationManager.getApplication().assertIsDispatchThread(); synchronized (myLock) { @@ -107,9 +108,9 @@ public class LineStatusTracker { myBaseRevisionNumber = baseRevisionNumber; myPreviousBaseRevision = null; - myUpToDateDocument.setReadOnly(false); - myUpToDateDocument.replaceString(0, myUpToDateDocument.getTextLength(), upToDateContent); - myUpToDateDocument.setReadOnly(true); + myVcsDocument.setReadOnly(false); + myVcsDocument.replaceString(0, myVcsDocument.getTextLength(), vcsContent); + myVcsDocument.setReadOnly(true); reinstallRanges(); if (myDocumentListener == null) { @@ -144,7 +145,7 @@ public class LineStatusTracker { removeAnathema(); removeHighlightersFromMarkupModel(); try { - myRanges = new RangesBuilder(myDocument, myUpToDateDocument).getRanges(); + myRanges = new RangesBuilder(myDocument, myVcsDocument).getRanges(); } catch (FilesTooBigForDiffException e) { myRanges.clear(); @@ -175,10 +176,10 @@ public class LineStatusTracker { LOG.assertTrue(!myReleased, "Already released"); int first = - range.getOffset1() >= getLineCount(myDocument) ? myDocument.getTextLength() : myDocument.getLineStartOffset(range.getOffset1()); + range.getLine1() >= getLineCount(myDocument) ? myDocument.getTextLength() : myDocument.getLineStartOffset(range.getLine1()); int second = - range.getOffset2() >= getLineCount(myDocument) ? myDocument.getTextLength() : myDocument.getLineStartOffset(range.getOffset2()); + 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); @@ -192,19 +193,19 @@ public class LineStatusTracker { highlighter.setEditorFilter(MarkupEditorFilterFactory.createIsNotDiffFilter()); final String tooltip; - if (range.getOffset1() == range.getOffset2()) { - if (range.getUOffset1() + 1 == range.getUOffset2()) { - tooltip = VcsBundle.message("tooltip.text.line.before.deleted", range.getOffset1() + 1); + 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.getOffset1() + 1, range.getUOffset2() - range.getUOffset1()); + tooltip = VcsBundle.message("tooltip.text.lines.before.deleted", range.getLine1() + 1, range.getVcsLine2() - range.getVcsLine1()); } } - else if (range.getOffset1() + 1 == range.getOffset2()) { - tooltip = VcsBundle.message("tooltip.text.line.changed", range.getOffset1() + 1); + 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.getOffset1() + 1, range.getOffset2()); + tooltip = VcsBundle.message("tooltip.text.lines.changed", range.getLine1() + 1, range.getLine2()); } highlighter.setErrorStripeTooltip(tooltip); @@ -238,9 +239,9 @@ public class LineStatusTracker { } } - public Document getUpToDateDocument() { + public Document getVcsDocument() { myApplication.assertIsDispatchThread(); - return myUpToDateDocument; + return myVcsDocument; } public void startBulkUpdate() { @@ -285,11 +286,11 @@ public class LineStatusTracker { // 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 = myUpToDateDocument.getText(); + myPreviousBaseRevision = myVcsDocument.getText(); } - myUpToDateDocument.setReadOnly(false); - myUpToDateDocument.setText(""); - myUpToDateDocument.setReadOnly(true); + myVcsDocument.setReadOnly(false); + myVcsDocument.setText(""); + myVcsDocument.setReadOnly(true); removeAnathema(); removeHighlightersFromMarkupModel(); myBaseLoaded = BaseLoadState.LOADING; @@ -298,15 +299,13 @@ public class LineStatusTracker { private class MyDocumentListener extends DocumentAdapter { // We have 3 document versions: - // * VCS version - upToDate* - // * before change - my* - // * after change - current* + // * VCS version + // * before change + // * after change - private int myFirstChangedLine; - private int myLastChangedLine; - private int myChangedLines; - private int myTotalLines; - private final VcsDirtyScopeManager myVcsDirtyScopeManager = VcsDirtyScopeManager.getInstance(myProject); + private int myLine1; + private int myBeforeChangedLines; + private int myBeforeTotalLines; @Override public void beforeDocumentChange(DocumentEvent e) { @@ -314,13 +313,21 @@ public class LineStatusTracker { synchronized (myLock) { if (myReleased) return; - if (myBulkUpdate || myAnathemaThrown || BaseLoadState.LOADED != myBaseLoaded) return; + if (myBulkUpdate || mySuppressUpdate || myAnathemaThrown || BaseLoadState.LOADED != myBaseLoaded) return; + assert myDocument == e.getDocument(); + try { - myFirstChangedLine = myDocument.getLineNumber(e.getOffset()); - myLastChangedLine = e.getOldLength() == 0 ? myFirstChangedLine : myDocument.getLineNumber(e.getOffset() + e.getOldLength() - 1); - if (StringUtil.endsWithChar(e.getOldFragment(), '\n')) myLastChangedLine++; - myChangedLines = myLastChangedLine - myFirstChangedLine; - myTotalLines = getLineCount(e.getDocument()); + 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) { } @@ -333,138 +340,307 @@ public class LineStatusTracker { synchronized (myLock) { if (myReleased) return; - if (myBulkUpdate || myAnathemaThrown || BaseLoadState.LOADED != myBaseLoaded) return; - try { - int currentFirstChangedLine = myFirstChangedLine; - int currentLastChangedLine = - e.getNewLength() == 0 ? currentFirstChangedLine : myDocument.getLineNumber(e.getOffset() + e.getNewLength() - 1); - if (StringUtil.endsWithChar(e.getNewFragment(), '\n')) currentLastChangedLine++; - int currentChangedLines = currentLastChangedLine - currentFirstChangedLine; - int upToDateTotalLine = getLineCount(myUpToDateDocument); - - int linesShift = currentChangedLines - myChangedLines; - - List<Range> rangesBeforeChange = new ArrayList<Range>(); - List<Range> rangesAfterChange = new ArrayList<Range>(); - List<Range> changedRanges = new ArrayList<Range>(); - sortRanges(myRanges, myFirstChangedLine, myLastChangedLine, rangesBeforeChange, changedRanges, rangesAfterChange); - - Range firstChangedRange = ContainerUtil.getFirstItem(changedRanges); - Range lastChangedRange = ContainerUtil.getLastItem(changedRanges); - Range lastRangeBefore = ContainerUtil.getLastItem(rangesBeforeChange); - Range firstRangeAfter = ContainerUtil.getFirstItem(rangesAfterChange); - - if (firstChangedRange != null && firstChangedRange.getOffset1() < myFirstChangedLine) { - myFirstChangedLine = firstChangedRange.getOffset1(); - } - if (lastChangedRange != null && lastChangedRange.getOffset2() > myLastChangedLine) { - myLastChangedLine = lastChangedRange.getOffset2() - 1; - } + if (myBulkUpdate || mySuppressUpdate || myAnathemaThrown || BaseLoadState.LOADED != myBaseLoaded) return; + assert myDocument == e.getDocument(); - currentFirstChangedLine = myFirstChangedLine; - currentLastChangedLine = myLastChangedLine + linesShift; + 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 upToDateFirstLine = getUpToDateLine1(lastRangeBefore, myFirstChangedLine); - int upToDateLastLine = getUpToDateLine2(firstRangeAfter, myLastChangedLine, myTotalLines, upToDateTotalLine); + int linesShift = afterChangedLines - myBeforeChangedLines; - List<Range> newChangedRanges = - getNewChangedRanges(currentFirstChangedLine, currentLastChangedLine, upToDateFirstLine, upToDateLastLine); + int line1 = myLine1; + int line2 = line1 + myBeforeChangedLines; // TODO: optimize some whole-line-changed cases - shiftRanges(rangesAfterChange, linesShift); + int[] fixed = fixRanges(e, line1, line2); + line1 = fixed[0]; + line2 = fixed[1]; - if (!changedRanges.equals(newChangedRanges)) { - replaceRanges(changedRanges, newChangedRanges); + doUpdateRanges(line1, line2, linesShift, myBeforeTotalLines); + } + } + } - myRanges = new ArrayList<Range>(rangesBeforeChange.size() + newChangedRanges.size() + rangesAfterChange.size()); + @NotNull + private int[] fixRanges(@NotNull DocumentEvent e, int line1, int line2) { + CharSequence document = myDocument.getCharsSequence(); + int offset = e.getOffset(); - myRanges.addAll(rangesBeforeChange); - myRanges.addAll(newChangedRanges); - myRanges.addAll(rangesAfterChange); + 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}; + } + } - for (Range range : myRanges) { - if (!range.hasHighlighter()) range.setHighlighter(createHighlighter(range)); - } + return new int[]{line1, line2}; + } - if (myRanges.isEmpty() && myVirtualFile != null) { - SwingUtilities.invokeLater(new Runnable() { - @Override - public void run() { - FileDocumentManager.getInstance().saveDocument(e.getDocument()); - boolean[] stillEmpty = new boolean[1]; - synchronized (myLock) { - stillEmpty[0] = myRanges.isEmpty(); - } - if (stillEmpty[0]) { - // file was modified, and now it's not -> dirty local change - myVcsDirtyScopeManager.fileDirty(myVirtualFile); - } - } - }); - } - } - } - catch (ProcessCanceledException ignore) { + 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<Range> rangesBeforeChange = new ArrayList<Range>(); + List<Range> rangesAfterChange = new ArrayList<Range>(); + List<Range> changedRanges = new ArrayList<Range>(); + + 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<Range> rangesBefore, + @NotNull List<Range> changedRanges, + @NotNull List<Range> 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<Range> newChangedRanges = getNewChangedRanges(afterChangedLine1, afterChangedLine2, vcsLine1, vcsLine2); + + shiftRanges(rangesAfter, linesShift); + + if (!changedRanges.equals(newChangedRanges)) { + replaceRanges(changedRanges, newChangedRanges); + + myRanges = new ArrayList<Range>(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)); } - catch (FilesTooBigForDiffException e1) { - installAnathema(); - removeHighlightersFromMarkupModel(); + + 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 int getUpToDateLine1(@Nullable Range range, int line) { - return range == null ? line : line + range.getUOffset2() - range.getOffset2(); + 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<Range> 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<String> lines = new DocumentWrapper(myDocument).getLines(changedLine1, changedLine2 - 1); + List<String> vcsLines = new DocumentWrapper(myVcsDocument).getLines(vcsLine1, vcsLine2 - 1); - private int getUpToDateLine2(@Nullable Range range, int line, int totalLinesBefore, int totalLinesAfter) { - return range == null ? totalLinesAfter - totalLinesBefore + line : line + range.getUOffset1() - range.getOffset1(); + return new RangesBuilder(lines, vcsLines, changedLine1, vcsLine1).getRanges(); + } + + private void replaceRanges(@NotNull List<Range> rangesInChange, @NotNull List<Range> 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 List<Range> getNewChangedRanges(int firstChangedLine, int lastChangedLine, int upToDateFirstLine, int upToDateLastLine) - throws FilesTooBigForDiffException { - List<String> lines = new DocumentWrapper(myDocument).getLines(firstChangedLine, lastChangedLine); - List<String> uLines = new DocumentWrapper(myUpToDateDocument).getLines(upToDateFirstLine, upToDateLastLine); - return new RangesBuilder(lines, uLines, firstChangedLine, upToDateFirstLine).getRanges(); + private static void shiftRanges(@NotNull List<Range> rangesAfterChange, int shift) { + for (final Range range : rangesAfterChange) { + range.shift(shift); } + } - private void replaceRanges(@NotNull List<Range> rangesInChange, @NotNull List<Range> newRangesInChange) { - for (Range range : rangesInChange) { - if (range.getHighlighter() != null) { - range.getHighlighter().dispose(); + private void sortRanges(int beforeChangedLine1, + int beforeChangedLine2, + int linesShift, + @NotNull List<Range> rangesBeforeChange, + @NotNull List<Range> changedRanges, + @NotNull List<Range> 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); } - range.setHighlighter(null); - range.invalidate(); - } - for (Range range : newRangesInChange) { - range.setHighlighter(createHighlighter(range)); } } + else { + int lastBefore = -1; + int firstAfter = myRanges.size(); + for (int i = 0; i < myRanges.size(); i++) { + Range range = myRanges.get(i); - private void shiftRanges(@NotNull List<Range> rangesAfterChange, int shift) { - for (final Range aRangesAfterChange : rangesAfterChange) { - aRangesAfterChange.shift(shift); + if (range.getLine2() < beforeChangedLine1) { + lastBefore = i; + } + else if (range.getLine1() > beforeChangedLine2) { + firstAfter = i; + break; + } } - } - } - public static void sortRanges(@NotNull List<Range> ranges, - int firstChangedLine, - int lastChangedLine, - @NotNull List<Range> rangesBeforeChange, - @NotNull List<Range> changedRanges, - @NotNull List<Range> rangesAfterChange) { - for (Range range : ranges) { - int offset1 = range.getOffset1() - 1; - int offset2 = range.getOffset2(); - if (offset2 < firstChangedLine) { - rangesBeforeChange.add(range); + // 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--; + } } - else if (offset1 > lastChangedLine) { - rangesAfterChange.add(range); + + 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++; + } } - else { - changedRanges.add(range); + + + 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); + } } } } @@ -496,7 +672,7 @@ public class LineStatusTracker { } for (final Range range : myRanges) { - if (line > range.getOffset1() || line > range.getOffset2()) { + if (line > range.getLine1() || line > range.getLine2()) { continue; } return range; @@ -515,7 +691,7 @@ public class LineStatusTracker { for (ListIterator<Range> iterator = myRanges.listIterator(myRanges.size()); iterator.hasPrevious(); ) { final Range range = iterator.previous(); - if (range.getOffset1() > line) { + if (range.getLine1() > line) { continue; } return range; @@ -528,10 +704,10 @@ public class LineStatusTracker { public Range getRangeForLine(final int line) { synchronized (myLock) { for (final Range range : myRanges) { - if (range.getType() == Range.DELETED && line == range.getOffset1()) { + if (range.getType() == Range.DELETED && line == range.getLine1()) { return range; } - else if (line >= range.getOffset1() && line < range.getOffset2()) { + else if (line >= range.getLine1() && line < range.getLine2()) { return range; } } @@ -539,172 +715,127 @@ public class LineStatusTracker { } } - public void rollbackChanges(@NotNull Range range) { - myApplication.assertWriteAccessAllowed(); + private void doRollbackRange(@NotNull Range range) { + if (range.getType() == Range.MODIFIED) { + TextRange currentTextRange = getCurrentTextRange(range); + int offset1 = currentTextRange.getStartOffset(); + int offset2 = currentTextRange.getEndOffset(); - synchronized (myLock) { - if (!range.isValid()) { - LOG.warn("Rollback of invalid range"); - return; - } - - 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(); - CharSequence upToDateContent = getUpToDateContent(range); - myDocument.replaceString(offset1, offset2, upToDateContent); + if (offset1 > 0) { + offset1--; } - 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 (offset2 < myDocument.getTextLength()) { + offset2++; } - else if (range.getType() == Range.DELETED) { - CharSequence content = getUpToDateContent(range); - if (range.getOffset2() == getLineCount(myDocument)) { - myDocument.insertString(myDocument.getTextLength(), "\n" + content); - } - else { - myDocument.insertString(myDocument.getLineStartOffset(range.getOffset2()), content + "\n"); - } + 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 { - throw new IllegalArgumentException("Unknown range type: " + range.getType()); + myDocument.insertString(myDocument.getLineStartOffset(range.getLine2()), content + "\n"); } } + else { + throw new IllegalArgumentException("Unknown range type: " + range.getType()); + } } - public void rollbackChanges(@NotNull SegmentTree lines) { + public void rollbackChanges(@NotNull Range range) { myApplication.assertWriteAccessAllowed(); synchronized (myLock) { - List<Range> affectedRanges = new ArrayList<Range>(); + if (myBulkUpdate) return; - boolean wasEnd = false; - boolean simple = true; - for (Range range : myRanges) { - if (!range.isValid()) { - LOG.warn("Rollback of invalid range"); - return; - } - - boolean check; - if (range.getOffset1() == range.getOffset2()) { - check = lines.check(range.getOffset1()); - } - else { - check = lines.check(range.getOffset1(), range.getOffset2()); - } - if (check) { - if (wasEnd) simple = false; - affectedRanges.add(range); - } - else { - if (!affectedRanges.isEmpty()) wasEnd = true; - } + if (!range.isValid()) { + LOG.warn("Rollback of invalid range"); + return; } - if (simple) { - rollbackChangesSimple(affectedRanges); - } - else { - rollbackChangesComplex(affectedRanges); - } + doRollbackRange(range); } } - private void rollbackChangesSimple(@NotNull List<Range> ranges) { - if (ranges.isEmpty()) return; - - Range first = ranges.get(0); - Range last = ranges.get(ranges.size() - 1); + public void rollbackChanges(@NotNull BitSet lines) { + myApplication.assertWriteAccessAllowed(); - byte type = first == last ? first.getType() : Range.MODIFIED; - final Range merged = new Range(first.getOffset1(), last.getOffset2(), first.getUOffset1(), last.getUOffset2(), type); + synchronized (myLock) { + if (myBulkUpdate) return; - // We don't expect complex Insertion/Deletion operation - they shouldn't exist - assert type != Range.MODIFIED || (first.getOffset1() != last.getOffset2() && first.getUOffset1() != last.getUOffset2()); + try { + mySuppressUpdate = true; - rollbackChanges(merged); - } + Range first = null; + Range last = null; - private void rollbackChangesComplex(@NotNull List<Range> ranges) { - // We can't relay on assumption, that revert of a single change will not affect any other. - // This, among the others, is because of 'magic' ranges for revert, that will affect nearby lines implicitly. - // So it's dangerous to apply ranges ony-by-one and we have to create single atomic modification. - // Usage of Bulk mode will lead to full rebuild of tracker, and therefore will be slow.. + int shift = 0; + for (Range range : myRanges) { + if (!range.isValid()) { + LOG.warn("Rollback of invalid range"); + break; + } - if (ranges.isEmpty()) return; - if (ranges.size() == 1) { - rollbackChanges(ranges.get(0)); - return; - } + 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(); + } - Range first = ranges.get(0); - Range last = ranges.get(ranges.size() - 1); + if (check) { + if (first == null) { + first = range; + } + last = range; - // We don't expect complex Insertion/Deletion operation - they shouldn't exist. - assert first != last && first.getOffset1() != last.getOffset2() && first.getUOffset1() != last.getUOffset2(); + Range shiftedRange = new Range(range); + shiftedRange.shift(shift); - final int start = getCurrentTextRange(first).getStartOffset(); - final int end = getCurrentTextRange(last).getEndOffset(); + doRollbackRange(shiftedRange); - StringBuilder builder = new StringBuilder(); + shift += (range.getVcsLine2() - range.getVcsLine1()) - (range.getLine2() - range.getLine1()); + } + } - int lastOffset = start; - for (Range range : ranges) { - TextRange textRange = getCurrentTextRange(range); + if (first != null) { + int beforeChangedLine1 = first.getLine1(); + int beforeChangedLine2 = last.getLine2(); - builder.append(myDocument.getText(new TextRange(lastOffset, textRange.getStartOffset()))); - lastOffset = textRange.getEndOffset(); + int beforeTotalLines = getLineCount(myDocument) - shift; - if (range.getType() == Range.MODIFIED) { - builder.append(getUpToDateContent(range)); - } - else if (range.getType() == Range.INSERTED) { - if (builder.length() > 0) { - builder.deleteCharAt(builder.length() - 1); - } - else { - lastOffset++; + doUpdateRanges(beforeChangedLine1, beforeChangedLine2, shift, beforeTotalLines); } } - else if (range.getType() == Range.DELETED) { - CharSequence content = getUpToDateContent(range); - if (range.getOffset2() == getLineCount(myDocument)) { - builder.append('\n').append(content); - } - else { - builder.append(content).append('\n'); - } + catch (Throwable e) { + reinstallRanges(); + if (e instanceof Error) throw ((Error)e); + if (e instanceof RuntimeException) throw ((RuntimeException)e); + throw new RuntimeException(e); } - else { - throw new IllegalArgumentException("Unknown range type: " + range.getType()); + finally { + mySuppressUpdate = false; } } - builder.append(myDocument.getText(new TextRange(lastOffset, end))); - - final String s = builder.toString(); - - myDocument.replaceString(start, end, s); } - public CharSequence getUpToDateContent(@NotNull Range range) { + public CharSequence getVcsContent(@NotNull Range range) { synchronized (myLock) { - TextRange textRange = getUpToDateRange(range); + TextRange textRange = getVcsRange(range); final int startOffset = textRange.getStartOffset(); final int endOffset = textRange.getEndOffset(); - return myUpToDateDocument.getCharsSequence().subSequence(startOffset, endOffset); + return myVcsDocument.getCharsSequence().subSequence(startOffset, endOffset); } } @@ -715,18 +846,18 @@ public class LineStatusTracker { LOG.warn("Current TextRange of invalid range"); } - return getRange(range.getOffset1(), range.getOffset2(), myDocument); + return getRange(range.getLine1(), range.getLine2(), myDocument); } } @NotNull - TextRange getUpToDateRange(@NotNull Range range) { + TextRange getVcsRange(@NotNull Range range) { synchronized (myLock) { if (!range.isValid()) { - LOG.warn("UpToDate TextRange of invalid range"); + LOG.warn("Vcs TextRange of invalid range"); } - return getRange(range.getUOffset1(), range.getUOffset2(), myUpToDateDocument); + return getRange(range.getVcsLine1(), range.getVcsLine2(), myVcsDocument); } } @@ -737,14 +868,14 @@ public class LineStatusTracker { * So we consider '\n' not as a part of line, but a separator between lines */ @NotNull - private static TextRange getRange(int offset1, int offset2, @NotNull Document document) { - if (offset1 == offset2) { - int lineStartOffset = offset1 < getLineCount(document) ? document.getLineStartOffset(offset1) : document.getTextLength(); + 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(offset1); - int endOffset = document.getLineEndOffset(offset2 - 1); + int startOffset = document.getLineStartOffset(line1); + int endOffset = document.getLineEndOffset(line2 - 1); return new TextRange(startOffset, endOffset); } } |