28ca45abe012242f5cd804a11d2e2b28484f3f08
[idea/community.git] / platform / vcs-impl / src / com / intellij / openapi / vcs / ex / LineStatusTracker.java
1 /*
2  * Copyright 2000-2014 JetBrains s.r.o.
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  * http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 package com.intellij.openapi.vcs.ex;
17
18 import com.intellij.diff.util.DiffUtil;
19 import com.intellij.openapi.application.Application;
20 import com.intellij.openapi.application.ApplicationManager;
21 import com.intellij.openapi.command.undo.UndoConstants;
22 import com.intellij.openapi.diagnostic.Logger;
23 import com.intellij.openapi.editor.Document;
24 import com.intellij.openapi.editor.event.DocumentAdapter;
25 import com.intellij.openapi.editor.event.DocumentEvent;
26 import com.intellij.openapi.editor.impl.DocumentImpl;
27 import com.intellij.openapi.editor.impl.DocumentMarkupModel;
28 import com.intellij.openapi.editor.markup.*;
29 import com.intellij.openapi.fileEditor.FileDocumentManager;
30 import com.intellij.openapi.fileEditor.FileEditor;
31 import com.intellij.openapi.fileEditor.FileEditorManager;
32 import com.intellij.openapi.progress.ProcessCanceledException;
33 import com.intellij.openapi.project.Project;
34 import com.intellij.openapi.util.Key;
35 import com.intellij.openapi.util.TextRange;
36 import com.intellij.openapi.util.registry.Registry;
37 import com.intellij.openapi.util.text.StringUtil;
38 import com.intellij.openapi.vcs.VcsBundle;
39 import com.intellij.openapi.vcs.changes.VcsDirtyScopeManager;
40 import com.intellij.openapi.vcs.history.VcsRevisionNumber;
41 import com.intellij.openapi.vfs.VirtualFile;
42 import com.intellij.ui.EditorNotificationPanel;
43 import com.intellij.util.containers.ContainerUtil;
44 import com.intellij.util.diff.FilesTooBigForDiffException;
45 import org.jetbrains.annotations.NotNull;
46 import org.jetbrains.annotations.Nullable;
47
48 import java.util.ArrayList;
49 import java.util.BitSet;
50 import java.util.Collections;
51 import java.util.List;
52
53 import static com.intellij.diff.util.DiffUtil.getLineCount;
54
55 /**
56  * @author irengrig
57  *         author: lesya
58  */
59 public class LineStatusTracker {
60   public enum Mode {DEFAULT, SMART, SILENT}
61
62   public static final Logger LOG = Logger.getInstance("#com.intellij.openapi.vcs.ex.LineStatusTracker");
63   private static final Key<CanNotCalculateDiffPanel> PANEL_KEY =
64     new Key<CanNotCalculateDiffPanel>("LineStatusTracker.CanNotCalculateDiffPanel");
65
66   private final Object myLock = new Object();
67   private boolean myInitialized;
68
69   @NotNull private final Project myProject;
70   @NotNull private final Document myDocument;
71   @NotNull private final Document myVcsDocument;
72   @NotNull private final VirtualFile myVirtualFile;
73
74   @NotNull private final Application myApplication;
75   @NotNull private final FileEditorManager myFileEditorManager;
76   @NotNull private final VcsDirtyScopeManager myVcsDirtyScopeManager;
77
78   private MyDocumentListener myDocumentListener;
79   @Nullable private RevisionPack myBaseRevisionNumber;
80
81   private boolean mySuppressUpdate;
82   private boolean myBulkUpdate;
83   private boolean myAnathemaThrown;
84   private boolean myReleased;
85
86   @NotNull private Mode myMode;
87
88   @NotNull private List<Range> myRanges;
89
90   private LineStatusTracker(@NotNull final Document document,
91                             @NotNull final Document vcsDocument,
92                             @NotNull final Project project,
93                             @NotNull final VirtualFile virtualFile,
94                             @NotNull final Mode mode) {
95     myDocument = document;
96     myVcsDocument = vcsDocument;
97     myProject = project;
98     myVirtualFile = virtualFile;
99
100     myApplication = ApplicationManager.getApplication();
101     myFileEditorManager = FileEditorManager.getInstance(myProject);
102     myVcsDirtyScopeManager = VcsDirtyScopeManager.getInstance(myProject);
103
104     myMode = mode;
105
106     myRanges = new ArrayList<Range>();
107   }
108
109   public void initialize(@NotNull final String vcsContent, @NotNull RevisionPack baseRevisionNumber) {
110     myApplication.assertIsDispatchThread();
111
112     synchronized (myLock) {
113       try {
114         if (myReleased) return;
115         if (myBaseRevisionNumber != null && myBaseRevisionNumber.contains(baseRevisionNumber)) return;
116
117         myBaseRevisionNumber = baseRevisionNumber;
118
119         myVcsDocument.setReadOnly(false);
120         myVcsDocument.setText(vcsContent);
121         myVcsDocument.setReadOnly(true);
122         reinstallRanges();
123
124         if (myDocumentListener == null) {
125           myDocumentListener = new MyDocumentListener();
126           myDocument.addDocumentListener(myDocumentListener);
127         }
128       }
129       finally {
130         myInitialized = true;
131       }
132     }
133   }
134
135   private void reinstallRanges() {
136     myApplication.assertIsDispatchThread();
137
138     synchronized (myLock) {
139       if (myReleased) return;
140
141       removeAnathema();
142       removeHighlightersFromMarkupModel();
143       try {
144         myRanges = new RangesBuilder(myDocument, myVcsDocument, myMode).getRanges();
145       }
146       catch (FilesTooBigForDiffException e) {
147         installAnathema();
148         return;
149       }
150       for (final Range range : myRanges) {
151         range.setHighlighter(createHighlighter(range));
152       }
153     }
154   }
155
156   private void installAnathema() {
157     myAnathemaThrown = true;
158     final FileEditor[] editors = myFileEditorManager.getAllEditors(myVirtualFile);
159     for (FileEditor editor : editors) {
160       CanNotCalculateDiffPanel panel = editor.getUserData(PANEL_KEY);
161       if (panel == null) {
162         final CanNotCalculateDiffPanel newPanel = new CanNotCalculateDiffPanel();
163         editor.putUserData(PANEL_KEY, newPanel);
164         myFileEditorManager.addTopComponent(editor, newPanel);
165       }
166     }
167   }
168
169   private void removeAnathema() {
170     if (!myAnathemaThrown) return;
171     myAnathemaThrown = false;
172     final FileEditor[] editors = myFileEditorManager.getEditors(myVirtualFile);
173     for (FileEditor editor : editors) {
174       final CanNotCalculateDiffPanel panel = editor.getUserData(PANEL_KEY);
175       if (panel != null) {
176         myFileEditorManager.removeTopComponent(editor, panel);
177         editor.putUserData(PANEL_KEY, null);
178       }
179     }
180   }
181
182   public void setMode(@NotNull Mode mode) {
183     synchronized (myLock) {
184       if (myMode == mode) return;
185       myMode = mode;
186       reinstallRanges();
187     }
188   }
189
190   @Nullable
191   private RangeHighlighter createHighlighter(@NotNull Range range) {
192     myApplication.assertIsDispatchThread();
193
194     LOG.assertTrue(!myReleased, "Already released");
195
196     if (myMode == Mode.SILENT) return null;
197
198     int first =
199       range.getLine1() >= getLineCount(myDocument) ? myDocument.getTextLength() : myDocument.getLineStartOffset(range.getLine1());
200
201     int second =
202       range.getLine2() >= getLineCount(myDocument) ? myDocument.getTextLength() : myDocument.getLineStartOffset(range.getLine2());
203
204     final TextAttributes attr = LineStatusTrackerDrawing.getAttributesFor(range);
205     final RangeHighlighter highlighter = DocumentMarkupModel.forDocument(myDocument, myProject, true)
206       .addRangeHighlighter(first, second, HighlighterLayer.FIRST - 1, attr, HighlighterTargetArea.LINES_IN_RANGE);
207
208     highlighter.setThinErrorStripeMark(true);
209     highlighter.setGreedyToLeft(true);
210     highlighter.setGreedyToRight(true);
211     highlighter.setLineMarkerRenderer(LineStatusTrackerDrawing.createRenderer(range, this));
212     highlighter.setEditorFilter(MarkupEditorFilterFactory.createIsNotDiffFilter());
213
214     final String tooltip;
215     if (range.getLine1() == range.getLine2()) {
216       if (range.getVcsLine1() + 1 == range.getVcsLine2()) {
217         tooltip = VcsBundle.message("tooltip.text.line.before.deleted", range.getLine1() + 1);
218       }
219       else {
220         tooltip = VcsBundle.message("tooltip.text.lines.before.deleted", range.getLine1() + 1, range.getVcsLine2() - range.getVcsLine1());
221       }
222     }
223     else if (range.getLine1() + 1 == range.getLine2()) {
224       tooltip = VcsBundle.message("tooltip.text.line.changed", range.getLine1() + 1);
225     }
226     else {
227       tooltip = VcsBundle.message("tooltip.text.lines.changed", range.getLine1() + 1, range.getLine2());
228     }
229
230     highlighter.setErrorStripeTooltip(tooltip);
231     return highlighter;
232   }
233
234   public void release() {
235     synchronized (myLock) {
236       myReleased = true;
237       if (myDocumentListener != null) {
238         myDocument.removeDocumentListener(myDocumentListener);
239       }
240
241       if (myApplication.isDispatchThread()) {
242         removeAnathema();
243         removeHighlightersFromMarkupModel();
244       }
245       else {
246         invalidateRanges();
247         myApplication.invokeLater(new Runnable() {
248           @Override
249           public void run() {
250             removeAnathema();
251             removeHighlightersFromMarkupModel();
252           }
253         });
254       }
255     }
256   }
257
258   @NotNull
259   public Project getProject() {
260     return myProject;
261   }
262
263   @NotNull
264   public Document getDocument() {
265     return myDocument;
266   }
267
268   @NotNull
269   public Document getVcsDocument() {
270     return myVcsDocument;
271   }
272
273   @NotNull
274   public VirtualFile getVirtualFile() {
275     return myVirtualFile;
276   }
277
278   @NotNull
279   public Mode getMode() {
280     return myMode;
281   }
282
283   public boolean isSilentMode() {
284     return myMode == Mode.SILENT;
285   }
286
287   @NotNull
288   public List<Range> getRanges() {
289     synchronized (myLock) {
290       return Collections.unmodifiableList(myRanges);
291     }
292   }
293
294   public void startBulkUpdate() {
295     synchronized (myLock) {
296       if (myReleased) return;
297
298       myBulkUpdate = true;
299       removeAnathema();
300       removeHighlightersFromMarkupModel();
301     }
302   }
303
304   private void removeHighlightersFromMarkupModel() {
305     myApplication.assertIsDispatchThread();
306
307     synchronized (myLock) {
308       for (Range range : myRanges) {
309         if (range.getHighlighter() != null) {
310           range.getHighlighter().dispose();
311         }
312         range.invalidate();
313       }
314       myRanges.clear();
315     }
316   }
317
318   private void invalidateRanges() {
319     synchronized (myLock) {
320       for (Range range : myRanges) {
321         range.invalidate();
322       }
323     }
324   }
325
326   public void finishBulkUpdate() {
327     synchronized (myLock) {
328       if (myReleased) return;
329
330       myBulkUpdate = false;
331       reinstallRanges();
332     }
333   }
334
335   private void markFileUnchanged() {
336     ApplicationManager.getApplication().invokeLater(new Runnable() {
337       @Override
338       public void run() {
339         FileDocumentManager.getInstance().saveDocument(myDocument);
340         boolean stillEmpty;
341         synchronized (myLock) {
342           stillEmpty = myRanges.isEmpty();
343         }
344         if (stillEmpty) {
345           // file was modified, and now it's not -> dirty local change
346           myVcsDirtyScopeManager.fileDirty(myVirtualFile);
347         }
348       }
349     });
350   }
351
352   private class MyDocumentListener extends DocumentAdapter {
353     // We have 3 document versions:
354     // * VCS version
355     // * before change
356     // * after change
357
358     private int myLine1;
359     private int myBeforeChangedLines;
360     private int myBeforeTotalLines;
361
362     @Override
363     public void beforeDocumentChange(DocumentEvent e) {
364       myApplication.assertIsDispatchThread();
365
366       synchronized (myLock) {
367         if (myReleased) return;
368         if (myBulkUpdate || mySuppressUpdate || myAnathemaThrown || !myInitialized) return;
369         assert myDocument == e.getDocument();
370
371         try {
372           myLine1 = myDocument.getLineNumber(e.getOffset());
373           if (e.getOldLength() == 0) {
374             myBeforeChangedLines = 1;
375           }
376           else {
377             int line1 = myLine1;
378             int line2 = myDocument.getLineNumber(e.getOffset() + e.getOldLength());
379             myBeforeChangedLines = line2 - line1 + 1;
380           }
381
382           myBeforeTotalLines = getLineCount(myDocument);
383         }
384         catch (ProcessCanceledException ignore) {
385         }
386       }
387     }
388
389     @Override
390     public void documentChanged(final DocumentEvent e) {
391       myApplication.assertIsDispatchThread();
392
393       synchronized (myLock) {
394         if (myReleased) return;
395         if (myBulkUpdate || mySuppressUpdate || myAnathemaThrown || !myInitialized) return;
396         assert myDocument == e.getDocument();
397
398         int afterChangedLines;
399         if (e.getNewLength() == 0) {
400           afterChangedLines = 1;
401         }
402         else {
403           int line1 = myLine1;
404           int line2 = myDocument.getLineNumber(e.getOffset() + e.getNewLength());
405           afterChangedLines = line2 - line1 + 1;
406         }
407
408         int linesShift = afterChangedLines - myBeforeChangedLines;
409
410         int line1 = myLine1;
411         int line2 = line1 + myBeforeChangedLines;
412
413         int[] fixed = fixRanges(e, line1, line2);
414         line1 = fixed[0];
415         line2 = fixed[1];
416
417         doUpdateRanges(line1, line2, linesShift, myBeforeTotalLines);
418       }
419     }
420   }
421
422   @NotNull
423   private int[] fixRanges(@NotNull DocumentEvent e, int line1, int line2) {
424     CharSequence document = myDocument.getCharsSequence();
425     int offset = e.getOffset();
426
427     if (e.getOldLength() == 0 && e.getNewLength() != 0) {
428       if (StringUtil.endsWithChar(e.getNewFragment(), '\n') && isNewline(offset - 1, document)) {
429         return new int[]{line1, line2 - 1};
430       }
431       if (StringUtil.startsWithChar(e.getNewFragment(), '\n') && isNewline(offset + e.getNewLength(), document)) {
432         return new int[]{line1 + 1, line2};
433       }
434     }
435     if (e.getOldLength() != 0 && e.getNewLength() == 0) {
436       if (StringUtil.endsWithChar(e.getOldFragment(), '\n') && isNewline(offset - 1, document)) {
437         return new int[]{line1, line2 - 1};
438       }
439       if (StringUtil.startsWithChar(e.getOldFragment(), '\n') && isNewline(offset + e.getNewLength(), document)) {
440         return new int[]{line1 + 1, line2};
441       }
442     }
443
444     return new int[]{line1, line2};
445   }
446
447   private static boolean isNewline(int offset, @NotNull CharSequence sequence) {
448     if (offset < 0) return false;
449     if (offset >= sequence.length()) return false;
450     return sequence.charAt(offset) == '\n';
451   }
452
453   private void doUpdateRanges(int beforeChangedLine1,
454                               int beforeChangedLine2,
455                               int linesShift,
456                               int beforeTotalLines) {
457     List<Range> rangesBeforeChange = new ArrayList<Range>();
458     List<Range> rangesAfterChange = new ArrayList<Range>();
459     List<Range> changedRanges = new ArrayList<Range>();
460
461     sortRanges(beforeChangedLine1, beforeChangedLine2, linesShift, rangesBeforeChange, changedRanges, rangesAfterChange);
462
463     Range firstChangedRange = ContainerUtil.getFirstItem(changedRanges);
464     Range lastChangedRange = ContainerUtil.getLastItem(changedRanges);
465
466     if (firstChangedRange != null && firstChangedRange.getLine1() < beforeChangedLine1) {
467       beforeChangedLine1 = firstChangedRange.getLine1();
468     }
469     if (lastChangedRange != null && lastChangedRange.getLine2() > beforeChangedLine2) {
470       beforeChangedLine2 = lastChangedRange.getLine2();
471     }
472
473     doUpdateRanges(beforeChangedLine1, beforeChangedLine2, linesShift, beforeTotalLines,
474                    rangesBeforeChange, changedRanges, rangesAfterChange);
475   }
476
477   private void doUpdateRanges(int beforeChangedLine1,
478                               int beforeChangedLine2,
479                               int linesShift, // before -> after
480                               int beforeTotalLines,
481                               @NotNull List<Range> rangesBefore,
482                               @NotNull List<Range> changedRanges,
483                               @NotNull List<Range> rangesAfter) {
484     try {
485       int vcsTotalLines = getLineCount(myVcsDocument);
486
487       Range lastRangeBefore = ContainerUtil.getLastItem(rangesBefore);
488       Range firstRangeAfter = ContainerUtil.getFirstItem(rangesAfter);
489
490       //noinspection UnnecessaryLocalVariable
491       int afterChangedLine1 = beforeChangedLine1;
492       int afterChangedLine2 = beforeChangedLine2 + linesShift;
493
494       int vcsLine1 = getVcsLine1(lastRangeBefore, beforeChangedLine1);
495       int vcsLine2 = getVcsLine2(firstRangeAfter, beforeChangedLine2, beforeTotalLines, vcsTotalLines);
496
497       List<Range> newChangedRanges = getNewChangedRanges(afterChangedLine1, afterChangedLine2, vcsLine1, vcsLine2);
498
499       shiftRanges(rangesAfter, linesShift);
500
501       if (!changedRanges.equals(newChangedRanges)) {
502         replaceRanges(changedRanges, newChangedRanges);
503
504         myRanges = new ArrayList<Range>(rangesBefore.size() + newChangedRanges.size() + rangesAfter.size());
505
506         myRanges.addAll(rangesBefore);
507         myRanges.addAll(newChangedRanges);
508         myRanges.addAll(rangesAfter);
509
510         for (Range range : myRanges) {
511           if (!range.hasHighlighter()) range.setHighlighter(createHighlighter(range));
512         }
513
514         if (myRanges.isEmpty()) {
515           markFileUnchanged();
516         }
517       }
518     }
519     catch (ProcessCanceledException ignore) {
520     }
521     catch (FilesTooBigForDiffException e1) {
522       installAnathema();
523       removeHighlightersFromMarkupModel();
524     }
525   }
526
527   private static int getVcsLine1(@Nullable Range range, int line) {
528     return range == null ? line : line + range.getVcsLine2() - range.getLine2();
529   }
530
531   private static int getVcsLine2(@Nullable Range range, int line, int totalLinesBefore, int totalLinesAfter) {
532     return range == null ? totalLinesAfter - totalLinesBefore + line : line + range.getVcsLine1() - range.getLine1();
533   }
534
535   private List<Range> getNewChangedRanges(int changedLine1, int changedLine2, int vcsLine1, int vcsLine2)
536     throws FilesTooBigForDiffException {
537
538     if (changedLine1 == changedLine2 && vcsLine1 == vcsLine2) {
539       return Collections.emptyList();
540     }
541     if (changedLine1 == changedLine2) {
542       return Collections.singletonList(new Range(changedLine1, changedLine2, vcsLine1, vcsLine2));
543     }
544     if (vcsLine1 == vcsLine2) {
545       return Collections.singletonList(new Range(changedLine1, changedLine2, vcsLine1, vcsLine2));
546     }
547
548     List<String> lines = new DocumentWrapper(myDocument).getLines(changedLine1, changedLine2 - 1);
549     List<String> vcsLines = new DocumentWrapper(myVcsDocument).getLines(vcsLine1, vcsLine2 - 1);
550
551     return new RangesBuilder(lines, vcsLines, changedLine1, vcsLine1, myMode).getRanges();
552   }
553
554   private void replaceRanges(@NotNull List<Range> rangesInChange, @NotNull List<Range> newRangesInChange) {
555     for (Range range : rangesInChange) {
556       if (range.getHighlighter() != null) {
557         range.getHighlighter().dispose();
558       }
559       range.setHighlighter(null);
560       range.invalidate();
561     }
562     for (Range range : newRangesInChange) {
563       range.setHighlighter(createHighlighter(range));
564     }
565   }
566
567   private static void shiftRanges(@NotNull List<Range> rangesAfterChange, int shift) {
568     for (final Range range : rangesAfterChange) {
569       range.shift(shift);
570     }
571   }
572
573   private void sortRanges(int beforeChangedLine1,
574                           int beforeChangedLine2,
575                           int linesShift,
576                           @NotNull List<Range> rangesBeforeChange,
577                           @NotNull List<Range> changedRanges,
578                           @NotNull List<Range> rangesAfterChange) {
579     if (!Registry.is("diff.status.tracker.skip.spaces")) {
580       for (Range range : myRanges) {
581         if (range.getLine2() < beforeChangedLine1) {
582           rangesBeforeChange.add(range);
583         }
584         else if (range.getLine1() > beforeChangedLine2) {
585           rangesAfterChange.add(range);
586         }
587         else {
588           changedRanges.add(range);
589         }
590       }
591     }
592     else {
593       int lastBefore = -1;
594       int firstAfter = myRanges.size();
595       for (int i = 0; i < myRanges.size(); i++) {
596         Range range = myRanges.get(i);
597
598         if (range.getLine2() < beforeChangedLine1) {
599           lastBefore = i;
600         }
601         else if (range.getLine1() > beforeChangedLine2) {
602           firstAfter = i;
603           break;
604         }
605       }
606
607
608       // Expand on ranges, that are separated from changes only by empty/whitespaces lines
609       // This is needed to reduce amount of confusing cases, when changed blocks are matched wrong due to matched empty lines between them
610       // TODO: try to simplify logic, it's too high change that current one is broken somehow
611       CharSequence sequence = myDocument.getCharsSequence();
612       int lineCount = getLineCount(myDocument);
613
614       while (true) {
615         if (lastBefore == -1) break;
616
617         if (lastBefore < myRanges.size() - 1 && firstAfter - lastBefore > 1) {
618           Range firstChangedRange = myRanges.get(lastBefore + 1);
619           if (firstChangedRange.getLine1() < beforeChangedLine1) {
620             beforeChangedLine1 = firstChangedRange.getLine1();
621           }
622         }
623
624         if (beforeChangedLine1 < 0) break;
625         if (beforeChangedLine1 >= lineCount) break;
626         int offset1 = myDocument.getLineStartOffset(beforeChangedLine1) - 2;
627
628         int deltaLines = 0;
629         while (offset1 > 0) {
630           char c = sequence.charAt(offset1);
631           if (!StringUtil.isWhiteSpace(c)) break;
632           if (c == '\n') deltaLines++;
633           offset1--;
634         }
635
636         if (deltaLines == 0) break;
637         beforeChangedLine1 -= deltaLines;
638
639         if (myRanges.get(lastBefore).getLine2() < beforeChangedLine1) break;
640         while (lastBefore != -1 && myRanges.get(lastBefore).getLine2() >= beforeChangedLine1) {
641           lastBefore--;
642         }
643       }
644
645       while (true) {
646         if (firstAfter == myRanges.size()) break;
647
648         if (firstAfter > 0 && firstAfter - lastBefore > 1) {
649           Range lastChangedRange = myRanges.get(firstAfter - 1);
650           if (lastChangedRange.getLine2() > beforeChangedLine2) {
651             beforeChangedLine2 = lastChangedRange.getLine2();
652           }
653         }
654
655         // TODO: "afterChangedLine2 >= getLineCount(myDocument)" shouldn't ever be true, but it is sometimes for some reason
656         int afterChangedLine2 = beforeChangedLine2 + linesShift - 1;
657         if (afterChangedLine2 < 0) break;
658         if (afterChangedLine2 >= lineCount) break;
659         int offset2 = myDocument.getLineEndOffset(afterChangedLine2) + 1;
660
661         int deltaLines = 0;
662         while (offset2 < sequence.length()) {
663           char c = sequence.charAt(offset2);
664           if (!StringUtil.isWhiteSpace(c)) break;
665           if (c == '\n') deltaLines++;
666           offset2++;
667         }
668
669         if (deltaLines == 0) break;
670         beforeChangedLine2 += deltaLines;
671
672         if (myRanges.get(firstAfter).getLine1() > beforeChangedLine2) break;
673         while (firstAfter != myRanges.size() && myRanges.get(firstAfter).getLine1() <= beforeChangedLine2) {
674           firstAfter++;
675         }
676       }
677
678
679       for (int i = 0; i < myRanges.size(); i++) {
680         Range range = myRanges.get(i);
681         if (i <= lastBefore) {
682           rangesBeforeChange.add(range);
683         }
684         else if (i >= firstAfter) {
685           rangesAfterChange.add(range);
686         }
687         else {
688           changedRanges.add(range);
689         }
690       }
691     }
692   }
693
694   @Nullable
695   public Range getNextRange(Range range) {
696     synchronized (myLock) {
697       final int index = myRanges.indexOf(range);
698       if (index == myRanges.size() - 1) return null;
699       return myRanges.get(index + 1);
700     }
701   }
702
703   @Nullable
704   public Range getPrevRange(Range range) {
705     synchronized (myLock) {
706       final int index = myRanges.indexOf(range);
707       if (index <= 0) return null;
708       return myRanges.get(index - 1);
709     }
710   }
711
712   @Nullable
713   public Range getNextRange(int line) {
714     synchronized (myLock) {
715       for (Range range : myRanges) {
716         if (line < range.getLine2() && !range.isSelectedByLine(line)) {
717           return range;
718         }
719       }
720       return null;
721     }
722   }
723
724   @Nullable
725   public Range getPrevRange(int line) {
726     synchronized (myLock) {
727       for (int i = myRanges.size() - 1; i >= 0; i--) {
728         Range range = myRanges.get(i);
729         if (line > range.getLine1() && !range.isSelectedByLine(line)) {
730           return range;
731         }
732       }
733       return null;
734     }
735   }
736
737   @Nullable
738   public Range getRangeForLine(int line) {
739     synchronized (myLock) {
740       for (final Range range : myRanges) {
741         if (range.isSelectedByLine(line)) return range;
742       }
743       return null;
744     }
745   }
746
747   private void doRollbackRange(@NotNull Range range) {
748     DiffUtil.applyModification(myDocument, range.getLine1(), range.getLine2(), myVcsDocument, range.getVcsLine1(), range.getVcsLine2());
749
750     markLinesUnchanged(range.getLine1(), range.getLine1() + range.getVcsLine2() - range.getVcsLine1());
751   }
752
753   private void markLinesUnchanged(int startLine, int endLine) {
754     if (myDocument.getTextLength() == 0) return; // empty document has no lines
755     ((DocumentImpl)myDocument).clearLineModificationFlags(startLine, endLine);
756   }
757
758   public void rollbackChanges(@NotNull Range range) {
759     myApplication.assertWriteAccessAllowed();
760
761     synchronized (myLock) {
762       if (myBulkUpdate) return;
763
764       if (!range.isValid()) {
765         LOG.warn("Rollback of invalid range");
766         return;
767       }
768
769       doRollbackRange(range);
770     }
771   }
772
773   public void rollbackChanges(@NotNull final BitSet lines) {
774     runBulkRollback(new Runnable() {
775       @Override
776       public void run() {
777         Range first = null;
778         Range last = null;
779
780         int shift = 0;
781         for (Range range : myRanges) {
782           if (!range.isValid()) {
783             LOG.warn("Rollback of invalid range");
784             break;
785           }
786
787           boolean check = DiffUtil.isSelectedByLine(lines, range.getLine1(), range.getLine2());
788
789           if (check) {
790             if (first == null) {
791               first = range;
792             }
793             last = range;
794
795             Range shiftedRange = new Range(range);
796             shiftedRange.shift(shift);
797
798             doRollbackRange(shiftedRange);
799
800             shift += (range.getVcsLine2() - range.getVcsLine1()) - (range.getLine2() - range.getLine1());
801           }
802         }
803
804         if (first != null) {
805           int beforeChangedLine1 = first.getLine1();
806           int beforeChangedLine2 = last.getLine2();
807
808           int beforeTotalLines = getLineCount(myDocument) - shift;
809
810           doUpdateRanges(beforeChangedLine1, beforeChangedLine2, shift, beforeTotalLines);
811         }
812       }
813     });
814   }
815
816   public void rollbackAllChanges() {
817     runBulkRollback(new Runnable() {
818       @Override
819       public void run() {
820         myDocument.setText(myVcsDocument.getText());
821
822         removeAnathema();
823         removeHighlightersFromMarkupModel();
824
825         markFileUnchanged();
826       }
827     });
828   }
829
830   private void runBulkRollback(@NotNull Runnable task) {
831     myApplication.assertWriteAccessAllowed();
832
833     synchronized (myLock) {
834       if (myBulkUpdate) return;
835
836       try {
837         mySuppressUpdate = true;
838
839         task.run();
840       }
841       catch (Error e) {
842         reinstallRanges();
843         throw e;
844       }
845       catch (RuntimeException e) {
846         reinstallRanges();
847         throw e;
848       }
849       finally {
850         mySuppressUpdate = false;
851       }
852     }
853   }
854
855   @NotNull
856   public CharSequence getCurrentContent(@NotNull Range range) {
857     synchronized (myLock) {
858       TextRange textRange = getCurrentTextRange(range);
859       final int startOffset = textRange.getStartOffset();
860       final int endOffset = textRange.getEndOffset();
861       return myDocument.getImmutableCharSequence().subSequence(startOffset, endOffset);
862     }
863   }
864
865   @NotNull
866   public CharSequence getVcsContent(@NotNull Range range) {
867     synchronized (myLock) {
868       TextRange textRange = getVcsTextRange(range);
869       final int startOffset = textRange.getStartOffset();
870       final int endOffset = textRange.getEndOffset();
871       return myVcsDocument.getImmutableCharSequence().subSequence(startOffset, endOffset);
872     }
873   }
874
875   @NotNull
876   public TextRange getCurrentTextRange(@NotNull Range range) {
877     myApplication.assertReadAccessAllowed();
878
879     synchronized (myLock) {
880       if (!range.isValid()) {
881         LOG.warn("Current TextRange of invalid range");
882       }
883
884       return DiffUtil.getLinesRange(myDocument, range.getLine1(), range.getLine2());
885     }
886   }
887
888   @NotNull
889   public TextRange getVcsTextRange(@NotNull Range range) {
890     synchronized (myLock) {
891       if (!range.isValid()) {
892         LOG.warn("Vcs TextRange of invalid range");
893       }
894
895       return DiffUtil.getLinesRange(myVcsDocument, range.getVcsLine1(), range.getVcsLine2());
896     }
897   }
898
899   public static LineStatusTracker createOn(@NotNull VirtualFile virtualFile, @NotNull final Document document, final Project project,
900                                            @NotNull Mode mode) {
901     final Document vcsDocument = new DocumentImpl("", true);
902     vcsDocument.putUserData(UndoConstants.DONT_RECORD_UNDO, Boolean.TRUE);
903     return new LineStatusTracker(document, vcsDocument, project, virtualFile, mode);
904   }
905
906   public static class RevisionPack {
907     private final long myNumber;
908     private final VcsRevisionNumber myRevision;
909
910     public RevisionPack(long number, VcsRevisionNumber revision) {
911       myNumber = number;
912       myRevision = revision;
913     }
914
915     public long getNumber() {
916       return myNumber;
917     }
918
919     public VcsRevisionNumber getRevision() {
920       return myRevision;
921     }
922
923     public boolean contains(final RevisionPack previous) {
924       if (myRevision.equals(previous.getRevision()) && !myRevision.equals(VcsRevisionNumber.NULL)) return true;
925       return myNumber >= previous.getNumber();
926     }
927
928     @Override
929     public boolean equals(Object o) {
930       if (this == o) return true;
931       if (o == null || getClass() != o.getClass()) return false;
932
933       RevisionPack that = (RevisionPack)o;
934
935       return myRevision.equals(that.getRevision());
936     }
937
938     @Override
939     public int hashCode() {
940       return myRevision.hashCode();
941     }
942   }
943
944   private static class CanNotCalculateDiffPanel extends EditorNotificationPanel {
945     public CanNotCalculateDiffPanel() {
946       myLabel.setText("Can not highlight changed lines. File is too big and there are too many changes.");
947     }
948   }
949 }