diff: add annotate action to diff viewers
[idea/community.git] / platform / diff-impl / src / com / intellij / diff / tools / simple / ThreesideTextDiffViewerEx.java
1 /*
2  * Copyright 2000-2015 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.diff.tools.simple;
17
18 import com.intellij.diff.DiffContext;
19 import com.intellij.diff.fragments.MergeLineFragment;
20 import com.intellij.diff.requests.ContentDiffRequest;
21 import com.intellij.diff.tools.util.*;
22 import com.intellij.diff.tools.util.base.TextDiffViewerUtil;
23 import com.intellij.diff.tools.util.side.ThreesideTextDiffViewer;
24 import com.intellij.diff.util.*;
25 import com.intellij.diff.util.DiffDividerDrawUtil.DividerPaintable;
26 import com.intellij.diff.util.DiffUserDataKeysEx.ScrollToPolicy;
27 import com.intellij.openapi.Disposable;
28 import com.intellij.openapi.diagnostic.Logger;
29 import com.intellij.openapi.diff.DiffBundle;
30 import com.intellij.openapi.editor.Editor;
31 import com.intellij.openapi.editor.event.DocumentEvent;
32 import com.intellij.openapi.editor.ex.EditorEx;
33 import com.intellij.openapi.util.UserDataHolder;
34 import com.intellij.openapi.util.text.StringUtil;
35 import com.intellij.util.Function;
36 import com.intellij.util.ui.ButtonlessScrollBarUI;
37 import org.jetbrains.annotations.CalledInAwt;
38 import org.jetbrains.annotations.NonNls;
39 import org.jetbrains.annotations.NotNull;
40 import org.jetbrains.annotations.Nullable;
41
42 import javax.swing.*;
43 import java.awt.*;
44 import java.util.Iterator;
45 import java.util.List;
46
47 public abstract class ThreesideTextDiffViewerEx extends ThreesideTextDiffViewer {
48   public static final Logger LOG = Logger.getInstance(ThreesideTextDiffViewerEx.class);
49
50   @NotNull private final SyncScrollSupport.SyncScrollable mySyncScrollable1;
51   @NotNull private final SyncScrollSupport.SyncScrollable mySyncScrollable2;
52
53   @NotNull protected final PrevNextDifferenceIterable myPrevNextDifferenceIterable;
54   @NotNull protected final MyStatusPanel myStatusPanel;
55
56   @NotNull protected final MyFoldingModel myFoldingModel;
57   @NotNull protected final MyInitialScrollHelper myInitialScrollHelper = new MyInitialScrollHelper();
58
59   private int myChangesCount = -1;
60   private int myConflictsCount = -1;
61
62   public ThreesideTextDiffViewerEx(@NotNull DiffContext context, @NotNull ContentDiffRequest request) {
63     super(context, request);
64
65     mySyncScrollable1 = new MySyncScrollable(Side.LEFT);
66     mySyncScrollable2 = new MySyncScrollable(Side.RIGHT);
67     myPrevNextDifferenceIterable = new MyPrevNextDifferenceIterable();
68     myStatusPanel = new MyStatusPanel();
69     myFoldingModel = new MyFoldingModel(getEditors().toArray(new EditorEx[3]), this);
70   }
71
72   @Override
73   @CalledInAwt
74   protected void onInit() {
75     super.onInit();
76     myContentPanel.setPainter(new MyDividerPainter(Side.LEFT), Side.LEFT);
77     myContentPanel.setPainter(new MyDividerPainter(Side.RIGHT), Side.RIGHT);
78     myContentPanel.setScrollbarPainter(new MyScrollbarPainter());
79   }
80
81   @Override
82   @CalledInAwt
83   protected void onDispose() {
84     destroyChangedBlocks();
85     super.onDispose();
86   }
87
88   @Override
89   @CalledInAwt
90   protected void processContextHints() {
91     super.processContextHints();
92     myInitialScrollHelper.processContext(myRequest);
93   }
94
95   @Override
96   @CalledInAwt
97   protected void updateContextHints() {
98     super.updateContextHints();
99     myFoldingModel.updateContext(myRequest, getFoldingModelSettings());
100     myInitialScrollHelper.updateContext(myRequest);
101   }
102
103   //
104   // Diff
105   //
106
107   @NotNull
108   public FoldingModelSupport.Settings getFoldingModelSettings() {
109     return TextDiffViewerUtil.getFoldingModelSettings(myContext);
110   }
111
112   @NotNull
113   protected Runnable applyNotification(@Nullable final JComponent notification) {
114     return new Runnable() {
115       @Override
116       public void run() {
117         clearDiffPresentation();
118         if (notification != null) myPanel.addNotification(notification);
119       }
120     };
121   }
122
123   protected void clearDiffPresentation() {
124     myStatusPanel.setBusy(false);
125     myPanel.resetNotifications();
126     destroyChangedBlocks();
127
128     myContentPanel.repaintDividers();
129     myStatusPanel.update();
130   }
131
132   protected void destroyChangedBlocks() {
133     myFoldingModel.destroy();
134   }
135
136   //
137   // Impl
138   //
139
140   @Override
141   protected void onDocumentChange(@NotNull DocumentEvent e) {
142     super.onDocumentChange(e);
143     myFoldingModel.onDocumentChanged(e);
144   }
145
146   @CalledInAwt
147   protected boolean doScrollToChange(@NotNull ScrollToPolicy scrollToPolicy) {
148     ThreesideDiffChangeBase targetChange = scrollToPolicy.select(getChanges());
149     if (targetChange == null) return false;
150
151     doScrollToChange(targetChange, false);
152     return true;
153   }
154
155   protected void doScrollToChange(@NotNull ThreesideDiffChangeBase change, boolean animated) {
156     int[] startLines = new int[3];
157     int[] endLines = new int[3];
158
159     for (int i = 0; i < 3; i++) {
160       ThreeSide side = ThreeSide.fromIndex(i);
161       startLines[i] = change.getStartLine(side);
162       endLines[i] = change.getEndLine(side);
163       DiffUtil.moveCaret(getEditor(side), startLines[i]);
164     }
165
166     getSyncScrollSupport().makeVisible(getCurrentSide(), startLines, endLines, animated);
167   }
168
169   //
170   // Counters
171   //
172
173   public int getChangesCount() {
174     return myChangesCount;
175   }
176
177   public int getConflictsCount() {
178     return myConflictsCount;
179   }
180
181   protected void resetChangeCounters() {
182     myChangesCount = 0;
183     myConflictsCount = 0;
184   }
185
186   protected void onChangeAdded(@NotNull ThreesideDiffChangeBase change) {
187     if (change.isConflict()) {
188       myConflictsCount++;
189     }
190     else {
191       myChangesCount++;
192     }
193     myStatusPanel.update();
194   }
195
196   protected void onChangeRemoved(@NotNull ThreesideDiffChangeBase change) {
197     if (change.isConflict()) {
198       myConflictsCount--;
199     }
200     else {
201       myChangesCount--;
202     }
203     myStatusPanel.update();
204   }
205
206   //
207   // Getters
208   //
209
210   @NotNull
211   protected abstract DividerPaintable getDividerPaintable(@NotNull Side side);
212
213   /*
214    * Some changes (ex: applied ones) can be excluded from general processing, but should be painted/used for synchronized scrolling
215    */
216   @NotNull
217   protected List<? extends ThreesideDiffChangeBase> getAllChanges() {
218     return getChanges();
219   }
220
221   @NotNull
222   protected abstract List<? extends ThreesideDiffChangeBase> getChanges();
223
224   @NotNull
225   @Override
226   protected SyncScrollSupport.SyncScrollable getSyncScrollable(@NotNull Side side) {
227     return side.select(mySyncScrollable1, mySyncScrollable2);
228   }
229
230   @NotNull
231   @Override
232   protected JComponent getStatusPanel() {
233     return myStatusPanel;
234   }
235
236   @NotNull
237   public SyncScrollSupport.ThreesideSyncScrollSupport getSyncScrollSupport() {
238     //noinspection ConstantConditions
239     return mySyncScrollSupport;
240   }
241
242   //
243   // Misc
244   //
245
246   @Nullable
247   @CalledInAwt
248   protected ThreesideDiffChangeBase getSelectedChange(@NotNull ThreeSide side) {
249     int caretLine = getEditor(side).getCaretModel().getLogicalPosition().line;
250
251     for (ThreesideDiffChangeBase change : getChanges()) {
252       int line1 = change.getStartLine(side);
253       int line2 = change.getEndLine(side);
254
255       if (DiffUtil.isSelectedByLine(caretLine, line1, line2)) return change;
256     }
257     return null;
258   }
259
260   //
261   // Actions
262   //
263
264   protected class MyPrevNextDifferenceIterable extends PrevNextDifferenceIterableBase<ThreesideDiffChangeBase> {
265     @NotNull
266     @Override
267     protected List<? extends ThreesideDiffChangeBase> getChanges() {
268       return ThreesideTextDiffViewerEx.this.getChanges();
269     }
270
271     @NotNull
272     @Override
273     protected EditorEx getEditor() {
274       return getCurrentEditor();
275     }
276
277     @Override
278     protected int getStartLine(@NotNull ThreesideDiffChangeBase change) {
279       return change.getStartLine(getCurrentSide());
280     }
281
282     @Override
283     protected int getEndLine(@NotNull ThreesideDiffChangeBase change) {
284       return change.getEndLine(getCurrentSide());
285     }
286
287     @Override
288     protected void scrollToChange(@NotNull ThreesideDiffChangeBase change) {
289       doScrollToChange(change, true);
290     }
291   }
292
293   protected class MyToggleExpandByDefaultAction extends TextDiffViewerUtil.ToggleExpandByDefaultAction {
294     public MyToggleExpandByDefaultAction() {
295       super(getTextSettings());
296     }
297
298     @Override
299     protected void expandAll(boolean expand) {
300       myFoldingModel.expandAll(expand);
301     }
302   }
303
304   //
305   // Helpers
306   //
307
308   @Nullable
309   @Override
310   public Object getData(@NonNls String dataId) {
311     if (DiffDataKeys.PREV_NEXT_DIFFERENCE_ITERABLE.is(dataId)) {
312       return myPrevNextDifferenceIterable;
313     }
314     else if (DiffDataKeys.CURRENT_CHANGE_RANGE.is(dataId)) {
315       ThreesideDiffChangeBase change = getSelectedChange(getCurrentSide());
316       if (change != null) {
317         return new LineRange(change.getStartLine(getCurrentSide()), change.getEndLine(getCurrentSide()));
318       }
319     }
320     return super.getData(dataId);
321   }
322
323   protected class MySyncScrollable extends BaseSyncScrollable {
324     @NotNull private final Side mySide;
325
326     public MySyncScrollable(@NotNull Side side) {
327       mySide = side;
328     }
329
330     @Override
331     public boolean isSyncScrollEnabled() {
332       return getTextSettings().isEnableSyncScroll();
333     }
334
335     @Override
336     protected void processHelper(@NotNull ScrollHelper helper) {
337       ThreeSide left = mySide.select(ThreeSide.LEFT, ThreeSide.BASE);
338       ThreeSide right = mySide.select(ThreeSide.BASE, ThreeSide.RIGHT);
339
340       if (!helper.process(0, 0)) return;
341       for (ThreesideDiffChangeBase diffChange : getAllChanges()) {
342         if (!helper.process(diffChange.getStartLine(left), diffChange.getStartLine(right))) return;
343         if (!helper.process(diffChange.getEndLine(left), diffChange.getEndLine(right))) return;
344       }
345       helper.process(getEditor(left).getDocument().getLineCount(), getEditor(right).getDocument().getLineCount());
346     }
347   }
348
349   protected class MyDividerPainter implements DiffSplitter.Painter {
350     @NotNull private final Side mySide;
351     @NotNull private final DividerPaintable myPaintable;
352
353     public MyDividerPainter(@NotNull Side side) {
354       mySide = side;
355       myPaintable = getDividerPaintable(side);
356     }
357
358     @Override
359     public void paint(@NotNull Graphics g, @NotNull JComponent divider) {
360       Graphics2D gg = DiffDividerDrawUtil.getDividerGraphics(g, divider, getEditor(ThreeSide.BASE).getComponent());
361
362       gg.setColor(DiffDrawUtil.getDividerColor(getEditor(ThreeSide.BASE)));
363       gg.fill(gg.getClipBounds());
364
365       Editor editor1 = mySide.select(getEditor(ThreeSide.LEFT), getEditor(ThreeSide.BASE));
366       Editor editor2 = mySide.select(getEditor(ThreeSide.BASE), getEditor(ThreeSide.RIGHT));
367
368       //DividerPolygonUtil.paintSimplePolygons(gg, divider.getWidth(), editor1, editor2, myPaintable);
369       DiffDividerDrawUtil.paintPolygons(gg, divider.getWidth(), editor1, editor2, myPaintable);
370
371       myFoldingModel.paintOnDivider(gg, divider, mySide);
372
373       gg.dispose();
374     }
375   }
376
377   protected class MyScrollbarPainter implements ButtonlessScrollBarUI.ScrollbarRepaintCallback {
378     @NotNull private final DividerPaintable myPaintable = getDividerPaintable(Side.RIGHT);
379
380     @Override
381     public void call(Graphics g) {
382       EditorEx editor1 = getEditor(ThreeSide.BASE);
383       EditorEx editor2 = getEditor(ThreeSide.RIGHT);
384
385       int width = editor1.getScrollPane().getVerticalScrollBar().getWidth();
386       DiffDividerDrawUtil.paintPolygonsOnScrollbar((Graphics2D)g, width, editor1, editor2, myPaintable);
387
388       myFoldingModel.paintOnScrollbar((Graphics2D)g, width);
389     }
390   }
391
392   protected class MyStatusPanel extends StatusPanel {
393     @Nullable
394     @Override
395     protected String getMessage() {
396       if (myChangesCount < 0 || myConflictsCount < 0) return null;
397       if (myChangesCount == 0 && myConflictsCount == 0) {
398         return DiffBundle.message("merge.dialog.all.conflicts.resolved.message.text");
399       }
400       return makeCounterWord(myChangesCount, "change") + ". " + makeCounterWord(myConflictsCount, "conflict");
401     }
402
403     @NotNull
404     private String makeCounterWord(int number, @NotNull String word) {
405       if (number == 0) {
406         return "No " + StringUtil.pluralize(word);
407       }
408       return number + " " + StringUtil.pluralize(word, number);
409     }
410   }
411
412   protected static class MyFoldingModel extends FoldingModelSupport {
413     private final MyPaintable myPaintable1 = new MyPaintable(0, 1);
414     private final MyPaintable myPaintable2 = new MyPaintable(1, 2);
415
416     public MyFoldingModel(@NotNull EditorEx[] editors, @NotNull Disposable disposable) {
417       super(editors, disposable);
418       assert editors.length == 3;
419     }
420
421     public void install(@Nullable List<MergeLineFragment> fragments,
422                         @NotNull UserDataHolder context,
423                         @NotNull FoldingModelSupport.Settings settings) {
424       Iterator<int[]> it = map(fragments, new Function<MergeLineFragment, int[]>() {
425         @Override
426         public int[] fun(MergeLineFragment fragment) {
427           return new int[]{
428             fragment.getStartLine(ThreeSide.LEFT),
429             fragment.getEndLine(ThreeSide.LEFT),
430             fragment.getStartLine(ThreeSide.BASE),
431             fragment.getEndLine(ThreeSide.BASE),
432             fragment.getStartLine(ThreeSide.RIGHT),
433             fragment.getEndLine(ThreeSide.RIGHT)};
434         }
435       });
436       install(it, context, settings);
437     }
438
439     public void paintOnDivider(@NotNull Graphics2D gg, @NotNull Component divider, @NotNull Side side) {
440       MyPaintable paintable = side.select(myPaintable1, myPaintable2);
441       paintable.paintOnDivider(gg, divider);
442     }
443
444     public void paintOnScrollbar(@NotNull Graphics2D gg, int width) {
445       myPaintable2.paintOnScrollbar(gg, width);
446     }
447   }
448
449   protected class MyInitialScrollHelper extends MyInitialScrollPositionHelper {
450     @Override
451     protected boolean doScrollToChange() {
452       if (myScrollToChange == null) return false;
453       return ThreesideTextDiffViewerEx.this.doScrollToChange(myScrollToChange);
454     }
455
456     @Override
457     protected boolean doScrollToFirstChange() {
458       return ThreesideTextDiffViewerEx.this.doScrollToChange(ScrollToPolicy.FIRST_CHANGE);
459     }
460   }
461 }