IDEA-304705 Slow Operations: Editor ctor gets psi file on EDT
[idea/community.git] / platform / diff-impl / src / com / intellij / diff / tools / fragmented / UnifiedDiffViewer.java
1 // Copyright 2000-2022 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
2 package com.intellij.diff.tools.fragmented;
3
4 import com.intellij.codeInsight.breadcrumbs.FileBreadcrumbsCollector;
5 import com.intellij.diff.DiffContext;
6 import com.intellij.diff.actions.AllLinesIterator;
7 import com.intellij.diff.actions.BufferedLineIterator;
8 import com.intellij.diff.actions.impl.OpenInEditorWithMouseAction;
9 import com.intellij.diff.actions.impl.SetEditorSettingsAction;
10 import com.intellij.diff.comparison.DiffTooBigException;
11 import com.intellij.diff.contents.DocumentContent;
12 import com.intellij.diff.fragments.LineFragment;
13 import com.intellij.diff.impl.ui.DifferencesLabel;
14 import com.intellij.diff.requests.ContentDiffRequest;
15 import com.intellij.diff.requests.DiffRequest;
16 import com.intellij.diff.tools.fragmented.UnifiedDiffModel.ChangedBlockData;
17 import com.intellij.diff.tools.util.*;
18 import com.intellij.diff.tools.util.base.InitialScrollPositionSupport;
19 import com.intellij.diff.tools.util.base.ListenerDiffViewerBase;
20 import com.intellij.diff.tools.util.base.TextDiffSettingsHolder.TextDiffSettings;
21 import com.intellij.diff.tools.util.base.TextDiffViewerUtil;
22 import com.intellij.diff.tools.util.breadcrumbs.DiffBreadcrumbsPanel;
23 import com.intellij.diff.tools.util.side.OnesideContentPanel;
24 import com.intellij.diff.tools.util.side.TwosideTextDiffViewer;
25 import com.intellij.diff.tools.util.text.TwosideTextDiffProvider;
26 import com.intellij.diff.util.*;
27 import com.intellij.diff.util.DiffUserDataKeysEx.ScrollToPolicy;
28 import com.intellij.openapi.Disposable;
29 import com.intellij.openapi.actionSystem.*;
30 import com.intellij.openapi.application.ApplicationManager;
31 import com.intellij.openapi.application.ModalityState;
32 import com.intellij.openapi.application.ReadAction;
33 import com.intellij.openapi.command.undo.UndoManager;
34 import com.intellij.openapi.diff.DiffBundle;
35 import com.intellij.openapi.diff.LineTokenizer;
36 import com.intellij.openapi.editor.*;
37 import com.intellij.openapi.editor.actionSystem.EditorActionManager;
38 import com.intellij.openapi.editor.actionSystem.ReadonlyFragmentModificationHandler;
39 import com.intellij.openapi.editor.colors.EditorColors;
40 import com.intellij.openapi.editor.event.DocumentEvent;
41 import com.intellij.openapi.editor.event.DocumentListener;
42 import com.intellij.openapi.editor.ex.EditorEx;
43 import com.intellij.openapi.editor.ex.MarkupModelEx;
44 import com.intellij.openapi.editor.ex.RangeHighlighterEx;
45 import com.intellij.openapi.editor.highlighter.EditorHighlighter;
46 import com.intellij.openapi.editor.impl.DocumentMarkupModel;
47 import com.intellij.openapi.editor.impl.LineNumberConverterAdapter;
48 import com.intellij.openapi.editor.impl.event.MarkupModelListener;
49 import com.intellij.openapi.fileEditor.FileDocumentManager;
50 import com.intellij.openapi.progress.EmptyProgressIndicator;
51 import com.intellij.openapi.progress.ProcessCanceledException;
52 import com.intellij.openapi.progress.ProgressIndicator;
53 import com.intellij.openapi.progress.ProgressManager;
54 import com.intellij.openapi.progress.util.BackgroundTaskUtil;
55 import com.intellij.openapi.project.DumbAware;
56 import com.intellij.openapi.project.Project;
57 import com.intellij.openapi.util.Disposer;
58 import com.intellij.openapi.util.Pair;
59 import com.intellij.openapi.util.TextRange;
60 import com.intellij.openapi.util.text.StringUtil;
61 import com.intellij.openapi.vfs.VirtualFile;
62 import com.intellij.pom.Navigatable;
63 import com.intellij.ui.components.breadcrumbs.Crumb;
64 import com.intellij.util.concurrency.NonUrgentExecutor;
65 import com.intellij.util.concurrency.annotations.RequiresEdt;
66 import com.intellij.util.concurrency.annotations.RequiresWriteLock;
67 import com.intellij.util.containers.ContainerUtil;
68 import com.intellij.util.ui.update.Activatable;
69 import com.intellij.util.ui.update.MergingUpdateQueue;
70 import com.intellij.util.ui.update.UiNotifyConnector;
71 import com.intellij.util.ui.update.Update;
72 import com.intellij.xml.breadcrumbs.NavigatableCrumb;
73 import org.jetbrains.annotations.NonNls;
74 import org.jetbrains.annotations.NotNull;
75 import org.jetbrains.annotations.Nullable;
76
77 import javax.swing.*;
78 import java.util.*;
79 import java.util.function.IntUnaryOperator;
80
81 import static com.intellij.diff.util.DiffUtil.getLinesContent;
82
83 public class UnifiedDiffViewer extends ListenerDiffViewerBase implements DifferencesLabel.DifferencesCounter {
84   @NotNull protected final EditorEx myEditor;
85   @NotNull protected final Document myDocument;
86   @NotNull private final UnifiedDiffPanel myPanel;
87   @NotNull private final OnesideContentPanel myContentPanel;
88
89   @NotNull private final SetEditorSettingsAction myEditorSettingsAction;
90   @NotNull private final PrevNextDifferenceIterable myPrevNextDifferenceIterable;
91   @NotNull private final MyStatusPanel myStatusPanel;
92
93   @NotNull private final MyInitialScrollHelper myInitialScrollHelper = new MyInitialScrollHelper();
94   @NotNull private final MyFoldingModel myFoldingModel;
95   @NotNull private final MarkupUpdater myMarkupUpdater;
96
97   @NotNull protected final TwosideTextDiffProvider.NoIgnore myTextDiffProvider;
98
99   @NotNull protected Side myMasterSide = Side.RIGHT;
100
101   @NotNull private final UnifiedDiffModel myModel = new UnifiedDiffModel(this);
102
103   private final boolean[] myForceReadOnlyFlags;
104   private boolean myReadOnlyLockSet = false;
105
106   private boolean myDuringOnesideDocumentModification;
107   private boolean myDuringTwosideDocumentModification;
108
109   private boolean myStateIsOutOfDate; // whether something was changed since last rediff
110   private boolean mySuppressEditorTyping; // our state is inconsistent. No typing can be handled correctly
111
112   public UnifiedDiffViewer(@NotNull DiffContext context, @NotNull DiffRequest request) {
113     super(context, (ContentDiffRequest)request);
114
115     myPrevNextDifferenceIterable = new MyPrevNextDifferenceIterable();
116     myStatusPanel = new MyStatusPanel();
117
118     myForceReadOnlyFlags = TextDiffViewerUtil.checkForceReadOnly(myContext, myRequest);
119
120     boolean leftEditable = isEditable(Side.LEFT, false);
121     boolean rightEditable = isEditable(Side.RIGHT, false);
122     if (leftEditable && !rightEditable) myMasterSide = Side.LEFT;
123     if (!leftEditable && rightEditable) myMasterSide = Side.RIGHT;
124
125
126     myDocument = EditorFactory.getInstance().createDocument("");
127     myEditor = DiffUtil.createEditor(myDocument, myProject, true, true);
128
129     myContentPanel = new OnesideContentPanel(myEditor.getComponent());
130     if (getProject() != null) {
131       myContentPanel.setBreadcrumbs(new UnifiedBreadcrumbsPanel(), getTextSettings());
132     }
133
134     myPanel = new UnifiedDiffPanel(myProject, myContentPanel, this, myContext);
135
136     myFoldingModel = new MyFoldingModel(getProject(), myEditor, this);
137     myMarkupUpdater = new MarkupUpdater(getContents());
138
139     myEditorSettingsAction = new SetEditorSettingsAction(getTextSettings(), getEditors());
140     myEditorSettingsAction.applyDefaults();
141
142     myTextDiffProvider = DiffUtil.createNoIgnoreTextDiffProvider(getProject(), getRequest(), getTextSettings(), this::rediff, this);
143
144     new MyOpenInEditorWithMouseAction().install(getEditors());
145
146     TextDiffViewerUtil.checkDifferentDocuments(myRequest);
147
148     DiffUtil.registerAction(new AppendSelectedChangesAction(Side.LEFT), myPanel);
149     DiffUtil.registerAction(new AppendSelectedChangesAction(Side.RIGHT), myPanel);
150   }
151
152   @Override
153   @RequiresEdt
154   protected void onInit() {
155     super.onInit();
156     installEditorListeners();
157     installTypingSupport();
158     myPanel.setLoadingContent(); // We need loading panel only for initial rediff()
159     myPanel.setPersistentNotifications(DiffUtil.createCustomNotifications(this, myContext, myRequest));
160     myContentPanel.setTitle(createTitles());
161
162     new UiNotifyConnector(getComponent(), new Activatable() {
163       @Override
164       public void showNotify() {
165         myMarkupUpdater.scheduleUpdate();
166       }
167     });
168   }
169
170   @Override
171   @RequiresEdt
172   protected void onDispose() {
173     myModel.clear();
174     myFoldingModel.destroy();
175     super.onDispose();
176     EditorFactory.getInstance().releaseEditor(myEditor);
177   }
178
179   @Override
180   @RequiresEdt
181   protected void processContextHints() {
182     super.processContextHints();
183     Side side = DiffUtil.getUserData(myRequest, myContext, DiffUserDataKeys.MASTER_SIDE);
184     if (side != null) myMasterSide = side;
185
186     myInitialScrollHelper.processContext(myRequest);
187   }
188
189   @Override
190   @RequiresEdt
191   protected void updateContextHints() {
192     super.updateContextHints();
193     myInitialScrollHelper.updateContext(myRequest);
194     myFoldingModel.updateContext(myRequest, getFoldingModelSettings());
195   }
196
197   @Nullable
198   protected JComponent createTitles() {
199     List<JComponent> titles = DiffUtil.createTextTitles(this, myRequest, Arrays.asList(myEditor, myEditor));
200     assert titles.size() == 2;
201
202     titles = ContainerUtil.skipNulls(titles);
203     if (titles.isEmpty()) return null;
204
205     return DiffUtil.createStackedComponents(titles, DiffUtil.TITLE_GAP);
206   }
207
208   @RequiresEdt
209   protected void updateEditorCanBeTyped() {
210     myEditor.setViewer(mySuppressEditorTyping || !isEditable(myMasterSide, true));
211   }
212
213   private void installTypingSupport() {
214     if (!isEditable(myMasterSide, false)) return;
215
216     updateEditorCanBeTyped();
217     myEditor.getColorsScheme().setColor(EditorColors.READONLY_FRAGMENT_BACKGROUND_COLOR, null); // guarded blocks
218     EditorActionManager.getInstance().setReadonlyFragmentModificationHandler(myDocument, new MyReadonlyFragmentModificationHandler());
219     myDocument.putUserData(UndoManager.ORIGINAL_DOCUMENT, getDocument(myMasterSide)); // use undo of master document
220
221     myDocument.addDocumentListener(new MyOnesideDocumentListener());
222   }
223
224   @NotNull
225   @Override
226   @RequiresEdt
227   public List<AnAction> createToolbarActions() {
228     List<AnAction> group = new ArrayList<>(myTextDiffProvider.getToolbarActions());
229     group.add(new MyToggleExpandByDefaultAction());
230     group.add(new MyReadOnlyLockAction());
231     group.add(myEditorSettingsAction);
232
233     group.add(Separator.getInstance());
234     group.addAll(super.createToolbarActions());
235
236     return group;
237   }
238
239   @NotNull
240   @Override
241   @RequiresEdt
242   public List<AnAction> createPopupActions() {
243     List<AnAction> group = new ArrayList<>(myTextDiffProvider.getPopupActions());
244     group.add(new MyToggleExpandByDefaultAction());
245
246     group.add(Separator.getInstance());
247     group.addAll(super.createPopupActions());
248
249     return group;
250   }
251
252   @NotNull
253   protected List<AnAction> createEditorPopupActions() {
254     List<AnAction> group = new ArrayList<>();
255
256     if (isEditable(Side.RIGHT, false)) {
257       group.add(new ReplaceSelectedChangesAction(Side.LEFT));
258       group.add(new ReplaceSelectedChangesAction(Side.RIGHT));
259     }
260
261     group.add(Separator.getInstance());
262     group.addAll(TextDiffViewerUtil.createEditorPopupActions());
263
264     return group;
265   }
266
267   @RequiresEdt
268   protected void installEditorListeners() {
269     new TextDiffViewerUtil.EditorActionsPopup(createEditorPopupActions()).install(getEditors(), myPanel);
270   }
271
272   @NotNull
273   protected UnifiedDiffChangeUi createUi(@NotNull UnifiedDiffChange change) {
274     return new UnifiedDiffChangeUi(this, change);
275   }
276
277   //
278   // Diff
279   //
280
281   @Override
282   protected void onBeforeDocumentChange(@NotNull DocumentEvent event) {
283     super.onBeforeDocumentChange(event);
284     myMarkupUpdater.suspendUpdate();
285   }
286
287   @Override
288   protected void onBeforeRediff() {
289     super.onBeforeRediff();
290     myMarkupUpdater.suspendUpdate();
291   }
292
293   @Override
294   @RequiresEdt
295   protected void onSlowRediff() {
296     super.onSlowRediff();
297     myStatusPanel.setBusy(true);
298   }
299
300   @Override
301   @NotNull
302   protected Runnable performRediff(@NotNull final ProgressIndicator indicator) {
303     try {
304       return computeDifferences(indicator);
305     }
306     catch (DiffTooBigException e) {
307       return () -> {
308         clearDiffPresentation();
309         myPanel.setTooBigContent();
310       };
311     }
312     catch (ProcessCanceledException e) {
313       throw e;
314     }
315     catch (Throwable e) {
316       LOG.error(e);
317       return applyErrorNotification();
318     }
319   }
320
321   @NotNull
322   protected Runnable applyErrorNotification() {
323     return () -> {
324       clearDiffPresentation();
325       myPanel.setErrorContent();
326     };
327   }
328
329   @NotNull
330   protected Runnable computeDifferences(@NotNull ProgressIndicator indicator) {
331     final Document document1 = getContent1().getDocument();
332     final Document document2 = getContent2().getDocument();
333
334     final CharSequence[] texts = ReadAction.compute(
335       () -> new CharSequence[]{document1.getImmutableCharSequence(), document2.getImmutableCharSequence()});
336
337     final List<LineFragment> fragments = myTextDiffProvider.compare(texts[0], texts[1], indicator);
338
339     UnifiedFragmentBuilder builder = ReadAction.compute(() -> {
340       indicator.checkCanceled();
341       return new UnifiedFragmentBuilder(fragments, document1, document2, myMasterSide).exec();
342     });
343
344     return apply(builder, texts, indicator);
345   }
346
347   private void clearDiffPresentation() {
348     myPanel.resetNotifications();
349     myStatusPanel.setBusy(false);
350     destroyChangedBlockData();
351
352     myStateIsOutOfDate = false;
353     mySuppressEditorTyping = false;
354     updateEditorCanBeTyped();
355   }
356
357   @RequiresEdt
358   protected void markSuppressEditorTyping() {
359     mySuppressEditorTyping = true;
360     updateEditorCanBeTyped();
361   }
362
363   @RequiresEdt
364   protected void markStateIsOutOfDate() {
365     myStateIsOutOfDate = true;
366     myFoldingModel.disposeLineConvertor();
367     myModel.updateGutterActions();
368   }
369
370   @Nullable
371   private static EditorHighlighter buildHighlighter(@Nullable Project project,
372                                                     @NotNull Document document,
373                                                     @NotNull DocumentContent content1,
374                                                     @NotNull DocumentContent content2,
375                                                     @NotNull CharSequence text1,
376                                                     @NotNull CharSequence text2,
377                                                     @NotNull List<HighlightRange> ranges,
378                                                     int textLength) {
379     EditorHighlighter highlighter1 = DiffUtil.initEditorHighlighter(project, content1, text1);
380     EditorHighlighter highlighter2 = DiffUtil.initEditorHighlighter(project, content2, text2);
381
382     if (highlighter1 == null && highlighter2 == null) return null;
383     if (highlighter1 == null) highlighter1 = DiffUtil.initEmptyEditorHighlighter(text1);
384     if (highlighter2 == null) highlighter2 = DiffUtil.initEmptyEditorHighlighter(text2);
385
386     return new UnifiedEditorHighlighter(document, highlighter1, highlighter2, ranges, textLength);
387   }
388
389   @NotNull
390   protected Runnable apply(@NotNull UnifiedFragmentBuilder builder,
391                            CharSequence @NotNull [] texts,
392                            @NotNull ProgressIndicator indicator) {
393     final DocumentContent content1 = getContent1();
394     final DocumentContent content2 = getContent2();
395
396     HighlightersData highlightersData = BackgroundTaskUtil.tryComputeFast(___ -> {
397       return ReadAction.compute(() -> {
398         EditorHighlighter highlighter =
399           buildHighlighter(myProject, myDocument, content1, content2,
400                            texts[0], texts[1], builder.getRanges(),
401                            builder.getText().length());
402         UnifiedEditorRangeHighlighter rangeHighlighter =
403           new UnifiedEditorRangeHighlighter(myProject, content1.getDocument(),
404                                             content2.getDocument(), builder.getRanges());
405         return new HighlightersData(highlighter, rangeHighlighter);
406       });
407     }, 500);
408
409     LineNumberConvertor convertor1 = builder.getConvertor1();
410     LineNumberConvertor convertor2 = builder.getConvertor2();
411     List<LineRange> changedLines = builder.getChangedLines();
412     boolean isContentsEqual = changedLines.isEmpty() && StringUtil.equals(texts[0], texts[1]);
413
414     Side masterSide = builder.getMasterSide();
415     FoldingModelSupport.Data foldingState = myFoldingModel.createState(changedLines, getFoldingModelSettings(),
416                                                                        getDocument(masterSide), masterSide.select(convertor1, convertor2),
417                                                                        StringUtil.countNewLines(builder.getText()) + 1);
418
419     return () -> {
420       myFoldingModel.updateContext(myRequest, getFoldingModelSettings());
421
422       LineCol oldCaretPosition = LineCol.fromOffset(myDocument, myEditor.getCaretModel().getPrimaryCaret().getOffset());
423       Pair<int[], Side> oldCaretLineTwoside = transferLineFromOneside(oldCaretPosition.line);
424
425
426       clearDiffPresentation();
427
428
429       if (isContentsEqual &&
430           !DiffUtil.isUserDataFlagSet(DiffUserDataKeysEx.DISABLE_CONTENTS_EQUALS_NOTIFICATION, myContext, myRequest)) {
431         myPanel.addNotification(TextDiffViewerUtil.createEqualContentsNotification(getContents()));
432       }
433
434       IntUnaryOperator foldingLineConvertor = myFoldingModel.getLineNumberConvertor();
435       IntUnaryOperator contentConvertor1 = DiffUtil.getContentLineConvertor(getContent1());
436       IntUnaryOperator contentConvertor2 = DiffUtil.getContentLineConvertor(getContent2());
437       IntUnaryOperator merged1 = mergeLineConverters(contentConvertor1, convertor1.createConvertor(), foldingLineConvertor);
438       IntUnaryOperator merged2 = mergeLineConverters(contentConvertor2, convertor2.createConvertor(), foldingLineConvertor);
439       myEditor.getGutter().setLineNumberConverter(merged1 == null ? LineNumberConverter.DEFAULT : new LineNumberConverterAdapter(merged1),
440                                                   merged2 == null ? null : new LineNumberConverterAdapter(merged2));
441
442       ApplicationManager.getApplication().runWriteAction(() -> {
443         myDuringOnesideDocumentModification = true;
444         try {
445           myDocument.setText(builder.getText());
446         }
447         finally {
448           myDuringOnesideDocumentModification = false;
449         }
450       });
451
452       DiffUtil.setEditorCodeStyle(myProject, myEditor, getContent(myMasterSide));
453
454       List<RangeMarker> guarderRangeBlocks = new ArrayList<>();
455       if (!myEditor.isViewer()) {
456         for (UnifiedDiffChange change : builder.getChanges()) {
457           LineRange range = myMasterSide.select(change.getInsertedRange(), change.getDeletedRange());
458           if (range.isEmpty()) continue;
459           TextRange textRange = DiffUtil.getLinesRange(myDocument, range.start, range.end);
460           guarderRangeBlocks.add(createGuardedBlock(textRange.getStartOffset(), textRange.getEndOffset()));
461         }
462         int textLength = myDocument.getTextLength(); // there are 'fake' newline at the very end
463         guarderRangeBlocks.add(createGuardedBlock(textLength, textLength));
464       }
465
466       myModel.setChanges(builder.getChanges(), isContentsEqual, guarderRangeBlocks, convertor1, convertor2, builder.getRanges());
467
468       int newCaretLine = transferLineToOneside(oldCaretLineTwoside.second,
469                                                oldCaretLineTwoside.second.select(oldCaretLineTwoside.first));
470       myEditor.getCaretModel().moveToOffset(LineCol.toOffset(myDocument, newCaretLine, oldCaretPosition.column));
471
472       myFoldingModel.install(foldingState, myRequest, getFoldingModelSettings());
473
474       HighlightersData.apply(myProject, myEditor, highlightersData);
475       myMarkupUpdater.resumeUpdate();
476
477       myInitialScrollHelper.onRediff();
478
479       myStatusPanel.update();
480       myPanel.setGoodContent();
481
482       myEditor.getGutterComponentEx().revalidateMarkup();
483     };
484   }
485
486   @NotNull
487   private RangeMarker createGuardedBlock(int start, int end) {
488     RangeMarker block = myDocument.createGuardedBlock(start, end);
489     block.setGreedyToLeft(true);
490     block.setGreedyToRight(true);
491     return block;
492   }
493
494   private static IntUnaryOperator mergeLineConverters(@Nullable IntUnaryOperator contentConvertor,
495                                                       @NotNull IntUnaryOperator unifiedConvertor,
496                                                       @NotNull IntUnaryOperator foldingConvertor) {
497     return DiffUtil.mergeLineConverters(DiffUtil.mergeLineConverters(contentConvertor, unifiedConvertor), foldingConvertor);
498   }
499
500   /*
501    * This convertor returns -1 if exact matching is impossible
502    */
503   public int transferLineToOnesideStrict(@NotNull Side side, int line) {
504     LineNumberConvertor convertor = myModel.getLineNumberConvertor(side);
505     return convertor != null ? convertor.convertInv(line) : -1;
506   }
507
508   /*
509    * This convertor returns -1 if exact matching is impossible
510    */
511   public int transferLineFromOnesideStrict(@NotNull Side side, int line) {
512     LineNumberConvertor convertor = myModel.getLineNumberConvertor(side);
513     return convertor != null ? convertor.convert(line) : -1;
514   }
515
516   /*
517    * This convertor returns 'good enough' position, even if exact matching is impossible
518    */
519   public int transferLineToOneside(@NotNull Side side, int line) {
520     LineNumberConvertor convertor = myModel.getLineNumberConvertor(side);
521     return convertor != null ? convertor.convertApproximateInv(line) : line;
522   }
523
524   public int transferLineFromOneside(@NotNull Side side, int line) {
525     return side.select(transferLineFromOneside(line).first);
526   }
527
528   /*
529    * This convertor returns 'good enough' position, even if exact matching is impossible
530    */
531   @NotNull
532   public Pair<int[], Side> transferLineFromOneside(int line) {
533     int[] lines = new int[2];
534
535     ChangedBlockData blockData = myModel.getData();
536     if (blockData == null) {
537       lines[0] = line;
538       lines[1] = line;
539       return Pair.create(lines, myMasterSide);
540     }
541
542     LineNumberConvertor lineConvertor1 = blockData.getLineNumberConvertor(Side.LEFT);
543     LineNumberConvertor lineConvertor2 = blockData.getLineNumberConvertor(Side.RIGHT);
544
545     Side side = myMasterSide;
546     lines[0] = lineConvertor1.convert(line);
547     lines[1] = lineConvertor2.convert(line);
548
549     if (lines[0] == -1 && lines[1] == -1) {
550       lines[0] = lineConvertor1.convertApproximate(line);
551       lines[1] = lineConvertor2.convertApproximate(line);
552     }
553     else if (lines[0] == -1) {
554       lines[0] = lineConvertor1.convertApproximate(line);
555       side = Side.RIGHT;
556     }
557     else if (lines[1] == -1) {
558       lines[1] = lineConvertor2.convertApproximate(line);
559       side = Side.LEFT;
560     }
561
562     return Pair.create(lines, side);
563   }
564
565   @RequiresEdt
566   private void destroyChangedBlockData() {
567     myModel.clear();
568
569     UnifiedEditorRangeHighlighter.erase(myProject, myDocument);
570
571     myFoldingModel.destroy();
572
573     myStatusPanel.update();
574   }
575
576   //
577   // Typing
578   //
579
580   private class MyOnesideDocumentListener implements DocumentListener {
581     @Override
582     public void beforeDocumentChange(@NotNull DocumentEvent e) {
583       if (myDuringOnesideDocumentModification) return;
584       ChangedBlockData blockData = myModel.getData();
585       if (blockData == null) {
586         LOG.warn("oneside beforeDocumentChange - model is invalid");
587         return;
588       }
589       // TODO: modify Document guard range logic - we can handle case, when whole read-only block is modified (ex: my replacing selection).
590
591       try {
592         myDuringTwosideDocumentModification = true;
593
594         Document twosideDocument = getDocument(myMasterSide);
595
596         LineCol onesideStartPosition = LineCol.fromOffset(myDocument, e.getOffset());
597         LineCol onesideEndPosition = LineCol.fromOffset(myDocument, e.getOffset() + e.getOldLength());
598
599         int line1 = onesideStartPosition.line;
600         int line2 = onesideEndPosition.line + 1;
601         int shift = DiffUtil.countLinesShift(e);
602
603         int twosideStartLine = transferLineFromOnesideStrict(myMasterSide, onesideStartPosition.line);
604         int twosideEndLine = transferLineFromOnesideStrict(myMasterSide, onesideEndPosition.line);
605         if (twosideStartLine == -1 || twosideEndLine == -1) {
606           // this should never happen
607           logDebugInfo(e, onesideStartPosition, onesideEndPosition, twosideStartLine, twosideEndLine);
608           markSuppressEditorTyping();
609           return;
610         }
611
612         int twosideStartOffset = twosideDocument.getLineStartOffset(twosideStartLine) + onesideStartPosition.column;
613         int twosideEndOffset = twosideDocument.getLineStartOffset(twosideEndLine) + onesideEndPosition.column;
614         twosideDocument.replaceString(twosideStartOffset, twosideEndOffset, e.getNewFragment());
615
616         for (UnifiedDiffChange change : blockData.getDiffChanges()) {
617           change.processChange(line1, line2, shift);
618         }
619
620         LineNumberConvertor masterConvertor = blockData.getLineNumberConvertor(myMasterSide);
621         LineNumberConvertor slaveConvertor = blockData.getLineNumberConvertor(myMasterSide.other());
622         masterConvertor.handleMasterChange(line1, line2, shift, true);
623         slaveConvertor.handleMasterChange(line1, line2, shift, false);
624       }
625       finally {
626         // TODO: we can avoid marking state out-of-date in some simple cases (like in SimpleDiffViewer)
627         // but this will greatly increase complexity, so let's wait if it's actually required by users
628         markStateIsOutOfDate();
629
630         scheduleRediff();
631
632         myDuringTwosideDocumentModification = false;
633       }
634     }
635
636     private void logDebugInfo(DocumentEvent e,
637                               LineCol onesideStartPosition, LineCol onesideEndPosition,
638                               int twosideStartLine, int twosideEndLine) {
639       @NonNls StringBuilder info = new StringBuilder();
640       Document document1 = getDocument(Side.LEFT);
641       Document document2 = getDocument(Side.RIGHT);
642       info.append("==== UnifiedDiffViewer Debug Info ====");
643       info.append("myMasterSide - ").append(myMasterSide).append('\n');
644       info.append("myLeftDocument.length() - ").append(document1.getTextLength()).append('\n');
645       info.append("myRightDocument.length() - ").append(document2.getTextLength()).append('\n');
646       info.append("myDocument.length() - ").append(myDocument.getTextLength()).append('\n');
647       info.append("e.getOffset() - ").append(e.getOffset()).append('\n');
648       info.append("e.getNewLength() - ").append(e.getNewLength()).append('\n');
649       info.append("e.getOldLength() - ").append(e.getOldLength()).append('\n');
650       info.append("onesideStartPosition - ").append(onesideStartPosition).append('\n');
651       info.append("onesideEndPosition - ").append(onesideEndPosition).append('\n');
652       info.append("twosideStartLine - ").append(twosideStartLine).append('\n');
653       info.append("twosideEndLine - ").append(twosideEndLine).append('\n');
654       Pair<int[], Side> pair1 = transferLineFromOneside(onesideStartPosition.line);
655       Pair<int[], Side> pair2 = transferLineFromOneside(onesideEndPosition.line);
656       info.append("non-strict transferStartLine - ").append(pair1.first[0]).append("-").append(pair1.first[1])
657         .append(":").append(pair1.second).append('\n');
658       info.append("non-strict transferEndLine - ").append(pair2.first[0]).append("-").append(pair2.first[1])
659         .append(":").append(pair2.second).append('\n');
660       info.append("---- UnifiedDiffViewer Debug Info ----");
661
662       LOG.warn(info.toString());
663     }
664   }
665
666   @Override
667   protected void onDocumentChange(@NotNull DocumentEvent e) {
668     if (myDuringTwosideDocumentModification) return;
669
670     markStateIsOutOfDate();
671     markSuppressEditorTyping();
672
673     scheduleRediff();
674   }
675
676   //
677   // Modification operations
678   //
679
680   private abstract class ApplySelectedChangesActionBase extends AnAction implements DumbAware {
681     @NotNull protected final Side myModifiedSide;
682
683     ApplySelectedChangesActionBase(@NotNull Side modifiedSide) {
684       myModifiedSide = modifiedSide;
685     }
686
687     @Override
688     public @NotNull ActionUpdateThread getActionUpdateThread() {
689       return ActionUpdateThread.EDT;
690     }
691
692     @Override
693     public void update(@NotNull AnActionEvent e) {
694       if (DiffUtil.isFromShortcut(e)) {
695         // consume shortcut even if there are nothing to do - avoid calling some other action
696         e.getPresentation().setEnabledAndVisible(true);
697         return;
698       }
699
700       Editor editor = e.getData(CommonDataKeys.EDITOR);
701       if (editor != getEditor()) {
702         e.getPresentation().setEnabledAndVisible(false);
703         return;
704       }
705
706       if (!isEditable(myModifiedSide, true) || isStateIsOutOfDate()) {
707         e.getPresentation().setEnabledAndVisible(false);
708         return;
709       }
710
711       e.getPresentation().setVisible(true);
712       e.getPresentation().setEnabled(isSomeChangeSelected());
713     }
714
715     @Override
716     public void actionPerformed(@NotNull final AnActionEvent e) {
717       final List<UnifiedDiffChange> selectedChanges = getSelectedChanges();
718       if (selectedChanges.isEmpty()) return;
719
720       if (!isEditable(myModifiedSide, true)) return;
721       if (isStateIsOutOfDate()) return;
722
723       String title = DiffBundle.message("message.use.selected.changes.command", e.getPresentation().getText());
724       DiffUtil.executeWriteCommand(getDocument(myModifiedSide), e.getProject(), title, () -> {
725         // state is invalidated during apply(), but changes are in reverse order, so they should not conflict with each other
726         apply(ContainerUtil.reverse(selectedChanges));
727         scheduleRediff();
728       });
729     }
730
731     protected boolean isSomeChangeSelected() {
732       List<UnifiedDiffChange> changes = myModel.getDiffChanges();
733       if (changes == null || changes.isEmpty()) return false;
734
735       return DiffUtil.isSomeRangeSelected(getEditor(), lines -> ContainerUtil.exists(changes, change -> isChangeSelected(change, lines)));
736     }
737
738     @RequiresWriteLock
739     protected abstract void apply(@NotNull List<? extends UnifiedDiffChange> changes);
740   }
741
742   private class ReplaceSelectedChangesAction extends ApplySelectedChangesActionBase {
743     ReplaceSelectedChangesAction(@NotNull Side focusedSide) {
744       super(focusedSide.other());
745
746       copyShortcutFrom(ActionManager.getInstance().getAction(focusedSide.select("Diff.ApplyLeftSide", "Diff.ApplyRightSide")));
747       getTemplatePresentation().setText(UnifiedDiffChangeUi.getApplyActionText(UnifiedDiffViewer.this, focusedSide));
748       getTemplatePresentation().setIcon(UnifiedDiffChangeUi.getApplyIcon(focusedSide));
749     }
750
751     @Override
752     protected void apply(@NotNull List<? extends UnifiedDiffChange> changes) {
753       for (UnifiedDiffChange change : changes) {
754         replaceChange(change, myModifiedSide.other());
755       }
756     }
757   }
758
759   private class AppendSelectedChangesAction extends ApplySelectedChangesActionBase {
760     AppendSelectedChangesAction(@NotNull Side focusedSide) {
761       super(focusedSide.other());
762
763       copyShortcutFrom(ActionManager.getInstance().getAction(focusedSide.select("Diff.AppendLeftSide", "Diff.AppendRightSide")));
764       getTemplatePresentation().setText(DiffBundle.messagePointer("action.presentation.diff.append.text"));
765       getTemplatePresentation().setIcon(DiffUtil.getArrowDownIcon(focusedSide));
766     }
767
768     @Override
769     protected void apply(@NotNull List<? extends UnifiedDiffChange> changes) {
770       for (UnifiedDiffChange change : changes) {
771         appendChange(change, myModifiedSide.other());
772       }
773     }
774   }
775
776   @RequiresWriteLock
777   public void replaceChange(@NotNull UnifiedDiffChange change, @NotNull Side sourceSide) {
778     Side outputSide = sourceSide.other();
779
780     Document document1 = getDocument(Side.LEFT);
781     Document document2 = getDocument(Side.RIGHT);
782
783     LineFragment lineFragment = change.getLineFragment();
784
785     boolean isLastWithLocal = DiffUtil.isUserDataFlagSet(DiffUserDataKeysEx.LAST_REVISION_WITH_LOCAL, myContext);
786     boolean isLocalChangeRevert = sourceSide == Side.LEFT && isLastWithLocal;
787     TextDiffViewerUtil.applyModification(outputSide.select(document1, document2),
788                                          outputSide.getStartLine(lineFragment), outputSide.getEndLine(lineFragment),
789                                          sourceSide.select(document1, document2),
790                                          sourceSide.getStartLine(lineFragment), sourceSide.getEndLine(lineFragment),
791                                          isLocalChangeRevert);
792
793     // no need to mark myStateIsOutOfDate - it will be made by DocumentListener
794     // TODO: we can apply change manually, without marking state out-of-date. But we'll have to schedule rediff anyway.
795   }
796
797   @RequiresWriteLock
798   public void appendChange(@NotNull UnifiedDiffChange change, @NotNull final Side sourceSide) {
799     Side outputSide = sourceSide.other();
800
801     Document document1 = getDocument(Side.LEFT);
802     Document document2 = getDocument(Side.RIGHT);
803
804     LineFragment lineFragment = change.getLineFragment();
805     if (sourceSide.getStartLine(lineFragment) == sourceSide.getEndLine(lineFragment)) return;
806
807     DiffUtil.applyModification(outputSide.select(document1, document2),
808                                outputSide.getEndLine(lineFragment), outputSide.getEndLine(lineFragment),
809                                sourceSide.select(document1, document2),
810                                sourceSide.getStartLine(lineFragment), sourceSide.getEndLine(lineFragment));
811   }
812
813   @NotNull
814   @RequiresEdt
815   protected List<UnifiedDiffChange> getSelectedChanges() {
816     final BitSet lines = DiffUtil.getSelectedLines(myEditor);
817     List<UnifiedDiffChange> changes = ContainerUtil.notNullize(myModel.getDiffChanges());
818     return ContainerUtil.filter(changes, change -> isChangeSelected(change, lines));
819   }
820
821   private static boolean isChangeSelected(@NotNull UnifiedDiffChange change, @NotNull BitSet lines) {
822     return DiffUtil.isSelectedByLine(lines, change.getLine1(), change.getLine2());
823   }
824
825   //
826   // Impl
827   //
828
829
830   @NotNull
831   public TextDiffSettings getTextSettings() {
832     return TextDiffViewerUtil.getTextSettings(myContext);
833   }
834
835   @NotNull
836   public FoldingModelSupport.Settings getFoldingModelSettings() {
837     return TextDiffViewerUtil.getFoldingModelSettings(myContext);
838   }
839
840   @NotNull
841   public FoldingModelSupport getFoldingModel() {
842     return myFoldingModel;
843   }
844
845   //
846   // Getters
847   //
848
849
850   @NotNull
851   public Side getMasterSide() {
852     return myMasterSide;
853   }
854
855   @NotNull
856   public EditorEx getEditor() {
857     return myEditor;
858   }
859
860   @NotNull
861   protected List<? extends EditorEx> getEditors() {
862     return Collections.singletonList(myEditor);
863   }
864
865   @NotNull
866   public List<? extends DocumentContent> getContents() {
867     //noinspection unchecked,rawtypes
868     return (List)myRequest.getContents();
869   }
870
871   @NotNull
872   public DocumentContent getContent(@NotNull Side side) {
873     return side.select(getContents());
874   }
875
876   @NotNull
877   public DocumentContent getContent1() {
878     return getContent(Side.LEFT);
879   }
880
881   @NotNull
882   public DocumentContent getContent2() {
883     return getContent(Side.RIGHT);
884   }
885
886   @Nullable
887   public List<UnifiedDiffChange> getDiffChanges() {
888     return myModel.getDiffChanges();
889   }
890
891   @NotNull
892   private List<UnifiedDiffChange> getNonSkippedDiffChanges() {
893     return ContainerUtil.filter(ContainerUtil.notNullize(getDiffChanges()), it -> !it.isSkipped());
894   }
895
896   @NotNull
897   @Override
898   public JComponent getComponent() {
899     return myPanel;
900   }
901
902   @Nullable
903   @Override
904   public JComponent getPreferredFocusedComponent() {
905     if (!myPanel.isGoodContent()) return null;
906     return myEditor.getContentComponent();
907   }
908
909   @NotNull
910   @Override
911   protected StatusPanel getStatusPanel() {
912     return myStatusPanel;
913   }
914
915   @Override
916   public int getTotalDifferences() {
917     return getNonSkippedDiffChanges().size();
918   }
919
920   @RequiresEdt
921   public boolean isEditable(@NotNull Side side, boolean respectReadOnlyLock) {
922     if (myReadOnlyLockSet && respectReadOnlyLock) return false;
923     if (side.select(myForceReadOnlyFlags)) return false;
924     return DiffUtil.canMakeWritable(getDocument(side));
925   }
926
927   @NotNull
928   public Document getDocument(@NotNull Side side) {
929     return getContent(side).getDocument();
930   }
931
932   public boolean isStateIsOutOfDate() {
933     return myStateIsOutOfDate;
934   }
935
936   //
937   // Misc
938   //
939
940   @Nullable
941   @Override
942   protected Navigatable getNavigatable() {
943     return getNavigatable(LineCol.fromCaret(myEditor));
944   }
945
946   @RequiresEdt
947   @Nullable
948   protected UnifiedDiffChange getCurrentChange() {
949     List<UnifiedDiffChange> changes = myModel.getDiffChanges();
950     if (changes == null) return null;
951
952     int caretLine = myEditor.getCaretModel().getLogicalPosition().line;
953
954     for (UnifiedDiffChange change : changes) {
955       if (DiffUtil.isSelectedByLine(caretLine, change.getLine1(), change.getLine2())) return change;
956     }
957     return null;
958   }
959
960   @RequiresEdt
961   @Nullable
962   protected Navigatable getNavigatable(@NotNull LineCol position) {
963     Pair<int[], Side> pair = transferLineFromOneside(position.line);
964     int line1 = pair.first[0];
965     int line2 = pair.first[1];
966
967     Navigatable navigatable1 = getContent1().getNavigatable(new LineCol(line1, position.column));
968     Navigatable navigatable2 = getContent2().getNavigatable(new LineCol(line2, position.column));
969     if (navigatable1 == null) return navigatable2;
970     if (navigatable2 == null) return navigatable1;
971     return pair.second.select(navigatable1, navigatable2);
972   }
973
974   public boolean isContentGood() {
975     return myPanel.isGoodContent() && myModel.isValid();
976   }
977
978   public static boolean canShowRequest(@NotNull DiffContext context, @NotNull DiffRequest request) {
979     return TwosideTextDiffViewer.canShowRequest(context, request);
980   }
981
982   //
983   // Actions
984   //
985
986   private class MyPrevNextDifferenceIterable extends PrevNextDifferenceIterableBase<UnifiedDiffChange> {
987     @NotNull
988     @Override
989     protected List<UnifiedDiffChange> getChanges() {
990       return getNonSkippedDiffChanges();
991     }
992
993     @NotNull
994     @Override
995     protected EditorEx getEditor() {
996       return myEditor;
997     }
998
999     @Override
1000     protected int getStartLine(@NotNull UnifiedDiffChange change) {
1001       return change.getLine1();
1002     }
1003
1004     @Override
1005     protected int getEndLine(@NotNull UnifiedDiffChange change) {
1006       return change.getLine2();
1007     }
1008   }
1009
1010   private class MyOpenInEditorWithMouseAction extends OpenInEditorWithMouseAction {
1011     @Override
1012     protected Navigatable getNavigatable(@NotNull Editor editor, int line) {
1013       if (editor != myEditor) return null;
1014
1015       return UnifiedDiffViewer.this.getNavigatable(new LineCol(line));
1016     }
1017   }
1018
1019   private class MyToggleExpandByDefaultAction extends TextDiffViewerUtil.ToggleExpandByDefaultAction {
1020     MyToggleExpandByDefaultAction() {
1021       super(getTextSettings(), myFoldingModel);
1022     }
1023   }
1024
1025   private class MyReadOnlyLockAction extends TextDiffViewerUtil.ReadOnlyLockAction {
1026     MyReadOnlyLockAction() {
1027       super(getContext());
1028       applyDefaults();
1029     }
1030
1031     @Override
1032     protected void doApply(boolean readOnly) {
1033       myReadOnlyLockSet = readOnly;
1034       myModel.updateGutterActions();
1035       updateEditorCanBeTyped();
1036       putEditorHint(myEditor, readOnly && isEditable(myMasterSide, false));
1037     }
1038
1039     @Override
1040     protected boolean canEdit() {
1041       return !myForceReadOnlyFlags[0] && DiffUtil.canMakeWritable(getContent1().getDocument()) ||
1042              !myForceReadOnlyFlags[1] && DiffUtil.canMakeWritable(getContent2().getDocument());
1043     }
1044   }
1045
1046   //
1047   // Scroll from annotate
1048   //
1049
1050   private final class ChangedLinesIterator extends BufferedLineIterator {
1051     @NotNull private final List<? extends UnifiedDiffChange> myChanges;
1052
1053     private int myIndex = 0;
1054
1055     private ChangedLinesIterator(@NotNull List<? extends UnifiedDiffChange> changes) {
1056       myChanges = changes;
1057       init();
1058     }
1059
1060     @Override
1061     public boolean hasNextBlock() {
1062       return myIndex < myChanges.size();
1063     }
1064
1065     @Override
1066     public void loadNextBlock() {
1067       LOG.assertTrue(!myStateIsOutOfDate);
1068
1069       UnifiedDiffChange change = myChanges.get(myIndex);
1070       myIndex++;
1071
1072       LineFragment lineFragment = change.getLineFragment();
1073
1074       Document document = getContent2().getDocument();
1075       CharSequence insertedText = getLinesContent(document, lineFragment.getStartLine2(), lineFragment.getEndLine2());
1076
1077       int lineNumber = lineFragment.getStartLine2();
1078
1079       LineTokenizer tokenizer = new LineTokenizer(insertedText.toString());
1080       for (String line : tokenizer.execute()) {
1081         addLine(lineNumber, line);
1082         lineNumber++;
1083       }
1084     }
1085   }
1086
1087   //
1088   // Helpers
1089   //
1090
1091   @Nullable
1092   @Override
1093   public Object getData(@NotNull @NonNls String dataId) {
1094     if (DiffDataKeys.PREV_NEXT_DIFFERENCE_ITERABLE.is(dataId)) {
1095       return myPrevNextDifferenceIterable;
1096     }
1097     else if (DiffDataKeys.CURRENT_EDITOR.is(dataId)) {
1098       return myEditor;
1099     }
1100     else if (DiffDataKeys.CURRENT_CHANGE_RANGE.is(dataId)) {
1101       UnifiedDiffChange change = getCurrentChange();
1102       if (change != null) {
1103         return new LineRange(change.getLine1(), change.getLine2());
1104       }
1105     }
1106     return super.getData(dataId);
1107   }
1108
1109   private class MyStatusPanel extends StatusPanel {
1110     @Nullable
1111     @Override
1112     protected String getMessage() {
1113       ChangedBlockData blockData = myModel.getData();
1114       if (blockData == null) return null;
1115
1116       List<UnifiedDiffChange> allChanges = blockData.getDiffChanges();
1117       return DiffUtil.getStatusText(allChanges.size(),
1118                                     ContainerUtil.count(allChanges, it -> it.isExcluded()),
1119                                     myModel.isContentsEqual());
1120     }
1121   }
1122
1123   private class MyInitialScrollHelper extends InitialScrollPositionSupport.TwosideInitialScrollHelper {
1124     @NotNull
1125     @Override
1126     protected List<? extends Editor> getEditors() {
1127       return UnifiedDiffViewer.this.getEditors();
1128     }
1129
1130     @Override
1131     protected void disableSyncScroll(boolean value) {
1132     }
1133
1134     @Override
1135     public void onSlowRediff() {
1136       // Will not happen for initial rediff
1137     }
1138
1139     @Override
1140     protected LogicalPosition @Nullable [] getCaretPositions() {
1141       LogicalPosition position = myEditor.getCaretModel().getLogicalPosition();
1142       Pair<int[], Side> pair = transferLineFromOneside(position.line);
1143       LogicalPosition[] carets = new LogicalPosition[2];
1144       carets[0] = getPosition(pair.first[0], position.column);
1145       carets[1] = getPosition(pair.first[1], position.column);
1146       return carets;
1147     }
1148
1149     @Override
1150     protected boolean doScrollToPosition() {
1151       if (myCaretPosition == null) return false;
1152
1153       LogicalPosition twosidePosition = myMasterSide.selectNotNull(myCaretPosition);
1154       int onesideLine = transferLineToOneside(myMasterSide, twosidePosition.line);
1155       LogicalPosition position = new LogicalPosition(onesideLine, twosidePosition.column);
1156
1157       myEditor.getCaretModel().moveToLogicalPosition(position);
1158
1159       if (myEditorsPosition != null && myEditorsPosition.isSame(position)) {
1160         DiffUtil.scrollToPoint(myEditor, myEditorsPosition.myPoints[0], false);
1161       }
1162       else {
1163         DiffUtil.scrollToCaret(myEditor, false);
1164       }
1165       return true;
1166     }
1167
1168     @NotNull
1169     private LogicalPosition getPosition(int line, int column) {
1170       if (line == -1) return new LogicalPosition(0, 0);
1171       return new LogicalPosition(line, column);
1172     }
1173
1174     private void doScrollToLine(@NotNull Side side, @NotNull LogicalPosition position) {
1175       int onesideLine = transferLineToOneside(side, position.line);
1176       DiffUtil.scrollEditor(myEditor, onesideLine, position.column, false);
1177     }
1178
1179     @Override
1180     protected boolean doScrollToLine() {
1181       if (myScrollToLine == null) return false;
1182       doScrollToLine(myScrollToLine.first, new LogicalPosition(myScrollToLine.second, 0));
1183       return true;
1184     }
1185
1186     private boolean doScrollToChange(@NotNull ScrollToPolicy scrollToChangePolicy) {
1187       List<UnifiedDiffChange> changes = myModel.getDiffChanges();
1188       if (changes == null) return false;
1189
1190       UnifiedDiffChange targetChange = scrollToChangePolicy.select(ContainerUtil.filter(changes, it -> !it.isSkipped()));
1191       if (targetChange == null) targetChange = scrollToChangePolicy.select(changes);
1192       if (targetChange == null) return false;
1193
1194       DiffUtil.scrollEditor(myEditor, targetChange.getLine1(), false);
1195       return true;
1196     }
1197
1198     @Override
1199     protected boolean doScrollToChange() {
1200       if (myScrollToChange == null) return false;
1201       return doScrollToChange(myScrollToChange);
1202     }
1203
1204     @Override
1205     protected boolean doScrollToFirstChange() {
1206       return doScrollToChange(ScrollToPolicy.FIRST_CHANGE);
1207     }
1208
1209     @Override
1210     protected boolean doScrollToContext() {
1211       if (myNavigationContext == null) return false;
1212
1213       List<UnifiedDiffChange> changes = myModel.getDiffChanges();
1214       if (changes == null) return false;
1215
1216       ChangedLinesIterator changedLinesIterator = new ChangedLinesIterator(changes);
1217       int line = myNavigationContext.contextMatchCheck(changedLinesIterator);
1218       if (line == -1) {
1219         // this will work for the case, when spaces changes are ignored, and corresponding fragments are not reported as changed
1220         // just try to find target line  -> +-
1221         AllLinesIterator allLinesIterator = new AllLinesIterator(getContent2().getDocument());
1222         line = myNavigationContext.contextMatchCheck(allLinesIterator);
1223       }
1224       if (line == -1) return false;
1225
1226       doScrollToLine(Side.RIGHT, new LogicalPosition(line, 0));
1227       return true;
1228     }
1229   }
1230
1231   private static class MyFoldingModel extends FoldingModelSupport {
1232     @NotNull private DisposableLineNumberConvertor myLineNumberConvertor = new DisposableLineNumberConvertor(null);
1233
1234     MyFoldingModel(@Nullable Project project, @NotNull EditorEx editor, @NotNull Disposable disposable) {
1235       super(project, new EditorEx[]{editor}, disposable);
1236     }
1237
1238     @Nullable
1239     public Data createState(@Nullable List<? extends LineRange> changedLines,
1240                             @NotNull Settings settings,
1241                             @NotNull Document document,
1242                             @NotNull LineNumberConvertor lineConvertor,
1243                             int lineCount) {
1244       Iterator<int[]> it = map(changedLines, line -> new int[]{
1245         line.start,
1246         line.end
1247       });
1248
1249       if (it == null || settings.range == -1) return null;
1250
1251       myLineNumberConvertor = new DisposableLineNumberConvertor(lineConvertor);
1252       MyFoldingBuilder builder = new MyFoldingBuilder(document, myLineNumberConvertor, lineCount, settings);
1253       return builder.build(it);
1254     }
1255
1256     @NotNull
1257     public IntUnaryOperator getLineNumberConvertor() {
1258       return getLineConvertor(0);
1259     }
1260
1261     public void disposeLineConvertor() {
1262       myLineNumberConvertor.dispose();
1263     }
1264
1265     private static final class MyFoldingBuilder extends FoldingBuilderBase {
1266       @NotNull private final Document myDocument;
1267       @NotNull private final DisposableLineNumberConvertor myLineConvertor;
1268
1269       private MyFoldingBuilder(@NotNull Document document,
1270                                @NotNull DisposableLineNumberConvertor lineConvertor,
1271                                int lineCount,
1272                                @NotNull Settings settings) {
1273         super(new int[]{lineCount}, settings);
1274         myDocument = document;
1275         myLineConvertor = lineConvertor;
1276       }
1277
1278       @Nullable
1279       @Override
1280       protected FoldedRangeDescription getDescription(@NotNull Project project, int lineNumber, int index) {
1281         int masterLine = myLineConvertor.convert(lineNumber);
1282         if (masterLine == -1) return null;
1283         return getLineSeparatorDescription(project, myDocument, masterLine);
1284       }
1285     }
1286
1287     private static final class DisposableLineNumberConvertor {
1288       @Nullable private volatile LineNumberConvertor myConvertor;
1289
1290       private DisposableLineNumberConvertor(@Nullable LineNumberConvertor convertor) {
1291         myConvertor = convertor;
1292       }
1293
1294       public int convert(int lineNumber) {
1295         LineNumberConvertor convertor = myConvertor;
1296         return convertor != null ? convertor.convert(lineNumber) : -1;
1297       }
1298
1299       public void dispose() {
1300         myConvertor = null;
1301       }
1302     }
1303   }
1304
1305   private static class MyReadonlyFragmentModificationHandler implements ReadonlyFragmentModificationHandler {
1306     @Override
1307     public void handle(ReadOnlyFragmentModificationException e) {
1308       // do nothing
1309     }
1310   }
1311
1312   private final class MarkupUpdater implements Disposable {
1313     @NotNull private final MergingUpdateQueue myUpdateQueue =
1314       new MergingUpdateQueue("UnifiedDiffViewer.MarkupUpdater", 300, true, myPanel, this);
1315
1316     @NotNull private ProgressIndicator myUpdateIndicator = new EmptyProgressIndicator();
1317     private boolean mySuspended;
1318
1319     private MarkupUpdater(@NotNull List<? extends DocumentContent> contents) {
1320       Disposer.register(UnifiedDiffViewer.this, this);
1321
1322       MyMarkupModelListener markupListener = new MyMarkupModelListener();
1323       for (DocumentContent content : contents) {
1324         Document document = content.getDocument();
1325         MarkupModelEx model = (MarkupModelEx)DocumentMarkupModel.forDocument(document, myProject, true);
1326         model.addMarkupModelListener(this, markupListener);
1327       }
1328     }
1329
1330     @Override
1331     public void dispose() {
1332       myUpdateIndicator.cancel();
1333     }
1334
1335     @RequiresEdt
1336     public void suspendUpdate() {
1337       myUpdateIndicator.cancel();
1338       myUpdateQueue.cancelAllUpdates();
1339       mySuspended = true;
1340     }
1341
1342     @RequiresEdt
1343     public void resumeUpdate() {
1344       mySuspended = false;
1345       scheduleUpdate();
1346     }
1347
1348     @RequiresEdt
1349     public void scheduleUpdate() {
1350       if (myProject == null) return;
1351       if (mySuspended) return;
1352       if (!getComponent().isShowing()) return;
1353       myUpdateIndicator.cancel();
1354
1355       myUpdateQueue.queue(new Update("update") {
1356         @Override
1357         public void run() {
1358           if (myStateIsOutOfDate || !myModel.isValid()) return;
1359
1360           myUpdateIndicator.cancel();
1361           myUpdateIndicator = new EmptyProgressIndicator();
1362
1363           ChangedBlockData blockData = Objects.requireNonNull(myModel.getData());
1364
1365           ReadAction
1366             .nonBlocking(() -> updateHighlighters(blockData))
1367             .finishOnUiThread(ModalityState.stateForComponent(myPanel), result -> {
1368               if (myStateIsOutOfDate || blockData != myModel.getData()) return;
1369
1370               HighlightersData.apply(myProject, myEditor, result);
1371             })
1372             .withDocumentsCommitted(myProject)
1373             .wrapProgress(myUpdateIndicator)
1374             .submit(NonUrgentExecutor.getInstance());
1375         }
1376       });
1377     }
1378
1379     @NotNull
1380     private HighlightersData updateHighlighters(@NotNull ChangedBlockData blockData) {
1381       List<HighlightRange> ranges = blockData.getRanges();
1382       Document document1 = getContent1().getDocument();
1383       Document document2 = getContent2().getDocument();
1384
1385       ProgressManager.checkCanceled();
1386       EditorHighlighter highlighter = buildHighlighter(myProject, myDocument, getContent1(), getContent2(),
1387                                                        document1.getCharsSequence(), document2.getCharsSequence(), ranges,
1388                                                        myDocument.getTextLength());
1389
1390       ProgressManager.checkCanceled();
1391       UnifiedEditorRangeHighlighter rangeHighlighter = new UnifiedEditorRangeHighlighter(myProject, document1, document2, ranges);
1392
1393       return new HighlightersData(highlighter, rangeHighlighter);
1394     }
1395
1396     private class MyMarkupModelListener implements MarkupModelListener {
1397       @Override
1398       public void afterAdded(@NotNull RangeHighlighterEx highlighter) {
1399         scheduleUpdate();
1400       }
1401
1402       @Override
1403       public void beforeRemoved(@NotNull RangeHighlighterEx highlighter) {
1404         scheduleUpdate();
1405       }
1406
1407       @Override
1408       public void attributesChanged(@NotNull RangeHighlighterEx highlighter, boolean renderersChanged, boolean fontStyleOrColorChanged) {
1409         scheduleUpdate();
1410       }
1411     }
1412   }
1413
1414   private static class HighlightersData {
1415     @Nullable private final EditorHighlighter myHighlighter;
1416     @Nullable private final UnifiedEditorRangeHighlighter myRangeHighlighter;
1417
1418     private HighlightersData(@Nullable EditorHighlighter highlighter,
1419                              @Nullable UnifiedEditorRangeHighlighter rangeHighlighter) {
1420       myHighlighter = highlighter;
1421       myRangeHighlighter = rangeHighlighter;
1422     }
1423
1424     public static void apply(@Nullable Project project, @NotNull EditorEx editor, @Nullable HighlightersData highlightersData) {
1425       EditorHighlighter highlighter = highlightersData != null ? highlightersData.myHighlighter : null;
1426       UnifiedEditorRangeHighlighter rangeHighlighter = highlightersData != null ? highlightersData.myRangeHighlighter : null;
1427
1428       if (highlighter != null) {
1429         editor.setHighlighter(highlighter);
1430       }
1431       else {
1432         editor.setHighlighter(DiffUtil.createEmptyEditorHighlighter());
1433       }
1434
1435       UnifiedEditorRangeHighlighter.erase(project, editor.getDocument());
1436       if (rangeHighlighter != null) {
1437         rangeHighlighter.apply(project, editor.getDocument());
1438       }
1439     }
1440   }
1441
1442   private final class UnifiedBreadcrumbsPanel extends DiffBreadcrumbsPanel {
1443     private final VirtualFile myFile1;
1444     private final VirtualFile myFile2;
1445
1446     private UnifiedBreadcrumbsPanel() {
1447       super(getEditor(), UnifiedDiffViewer.this);
1448
1449       myFile1 = FileDocumentManager.getInstance().getFile(getDocument(Side.LEFT));
1450       myFile2 = FileDocumentManager.getInstance().getFile(getDocument(Side.RIGHT));
1451     }
1452
1453     @Override
1454     protected boolean updateCollectors(boolean enabled) {
1455       return enabled && (findCollector(myFile1) != null || findCollector(myFile2) != null);
1456     }
1457
1458     @Nullable
1459     @Override
1460     protected Iterable<? extends Crumb> computeCrumbs(int offset) {
1461       Pair<Integer, Side> pair = transferOffsetToTwoside(offset);
1462       if (pair == null) return null;
1463
1464       Side side = pair.second;
1465       int twosideOffset = pair.first;
1466
1467       VirtualFile file = side.select(myFile1, myFile2);
1468       FileBreadcrumbsCollector collector = side.select(findCollector(myFile1), findCollector(myFile2));
1469       if (file == null || collector == null) return null;
1470
1471       Iterable<? extends Crumb> crumbs = collector.computeCrumbs(file, getDocument(side), twosideOffset, null);
1472       return ContainerUtil.map(crumbs, it -> it instanceof NavigatableCrumb ? new UnifiedNavigatableCrumb((NavigatableCrumb)it, side) : it);
1473     }
1474
1475     @Override
1476     protected void navigateToCrumb(Crumb crumb, boolean withSelection) {
1477       if (crumb instanceof UnifiedNavigatableCrumb) {
1478         super.navigateToCrumb(crumb, withSelection);
1479       }
1480     }
1481
1482     @Nullable
1483     private Pair<Integer, Side> transferOffsetToTwoside(int offset) {
1484       LineCol onesidePosition = LineCol.fromOffset(myDocument, offset);
1485
1486       Pair<int[], Side> pair = transferLineFromOneside(onesidePosition.line);
1487       Side side = pair.second;
1488       int twosideLine = side.select(pair.first);
1489       if (twosideLine == -1) return null;
1490
1491       Document twosideDocument = getDocument(side);
1492       LineCol twosidePosition = new LineCol(twosideLine, onesidePosition.column);
1493       return Pair.create(twosidePosition.toOffset(twosideDocument), side);
1494     }
1495
1496     private int transferOffsetFromTwoside(@NotNull Side side, int offset) {
1497       LineCol twosidePosition = LineCol.fromOffset(getDocument(side), offset);
1498
1499       int onesideLine = transferLineToOneside(side, twosidePosition.line);
1500       if (onesideLine == -1) return -1;
1501
1502       LineCol onesidePosition = new LineCol(onesideLine, twosidePosition.column);
1503       return onesidePosition.toOffset(myDocument);
1504     }
1505
1506
1507     private final class UnifiedNavigatableCrumb implements NavigatableCrumb {
1508       @NotNull private final NavigatableCrumb myDelegate;
1509       @NotNull private final Side mySide;
1510
1511       private UnifiedNavigatableCrumb(@NotNull NavigatableCrumb delegate, @NotNull Side side) {
1512         myDelegate = delegate;
1513         mySide = side;
1514       }
1515
1516       @Override
1517       public int getAnchorOffset() {
1518         int offset = myDelegate.getAnchorOffset();
1519         return offset != -1 ? transferOffsetFromTwoside(mySide, offset) : -1;
1520       }
1521
1522       @Override
1523       @Nullable
1524       public TextRange getHighlightRange() {
1525         TextRange range = myDelegate.getHighlightRange();
1526         if (range == null) return null;
1527         int start = transferOffsetFromTwoside(mySide, range.getStartOffset());
1528         int end = transferOffsetFromTwoside(mySide, range.getEndOffset());
1529         if (start == -1 || end == -1) return null;
1530         return new TextRange(start, end);
1531       }
1532
1533       @Override
1534       public void navigate(@NotNull Editor editor, boolean withSelection) {
1535         int offset = getAnchorOffset();
1536         if (offset != -1) {
1537           editor.getCaretModel().moveToOffset(offset);
1538           editor.getScrollingModel().scrollToCaret(ScrollType.MAKE_VISIBLE);
1539         }
1540
1541         if (withSelection) {
1542           final TextRange range = getHighlightRange();
1543           if (range != null) {
1544             editor.getSelectionModel().setSelection(range.getStartOffset(), range.getEndOffset());
1545           }
1546         }
1547       }
1548
1549       @Override
1550       public Icon getIcon() {
1551         return myDelegate.getIcon();
1552       }
1553
1554       @Override
1555       public String getText() {
1556         return myDelegate.getText();
1557       }
1558
1559       @Override
1560       @Nullable
1561       public String getTooltip() {
1562         return myDelegate.getTooltip();
1563       }
1564
1565       @Override
1566       @NotNull
1567       public List<? extends Action> getContextActions() {
1568         return myDelegate.getContextActions();
1569       }
1570     }
1571   }
1572 }