merge: update status line on change resolve
[idea/community.git] / platform / diff-impl / src / com / intellij / diff / merge / TextMergeChange.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.merge;
17
18 import com.intellij.diff.comparison.ComparisonPolicy;
19 import com.intellij.diff.fragments.MergeLineFragment;
20 import com.intellij.diff.tools.simple.ThreesideDiffChangeBase;
21 import com.intellij.diff.util.*;
22 import com.intellij.diff.util.DiffUtil.UpdatedLineRange;
23 import com.intellij.icons.AllIcons;
24 import com.intellij.openapi.actionSystem.AnAction;
25 import com.intellij.openapi.actionSystem.AnActionEvent;
26 import com.intellij.openapi.diff.DiffBundle;
27 import com.intellij.openapi.editor.Document;
28 import com.intellij.openapi.editor.Editor;
29 import com.intellij.openapi.editor.ex.EditorEx;
30 import com.intellij.openapi.editor.markup.*;
31 import com.intellij.openapi.project.DumbAwareAction;
32 import com.intellij.util.containers.ContainerUtil;
33 import org.jetbrains.annotations.CalledInAwt;
34 import org.jetbrains.annotations.NotNull;
35 import org.jetbrains.annotations.Nullable;
36
37 import javax.swing.*;
38 import java.util.ArrayList;
39 import java.util.List;
40
41 public class TextMergeChange extends ThreesideDiffChangeBase {
42   @NotNull private final TextMergeTool.TextMergeViewer myMergeViewer;
43   @NotNull private final TextMergeTool.TextMergeViewer.MyThreesideViewer myViewer;
44   @NotNull private final List<RangeHighlighter> myHighlighters = new ArrayList<RangeHighlighter>();
45
46   @NotNull private final List<MyGutterOperation> myOperations = new ArrayList<MyGutterOperation>();
47
48   private int[] myStartLines = new int[3];
49   private int[] myEndLines = new int[3];
50   private final boolean[] myResolved = new boolean[2];
51
52   @CalledInAwt
53   public TextMergeChange(@NotNull MergeLineFragment fragment, @NotNull TextMergeTool.TextMergeViewer viewer) {
54     super(fragment, viewer.getViewer().getEditors(), ComparisonPolicy.DEFAULT);
55     myMergeViewer = viewer;
56     myViewer = viewer.getViewer();
57
58     for (ThreeSide side : ThreeSide.values()) {
59       myStartLines[side.getIndex()] = fragment.getStartLine(side);
60       myEndLines[side.getIndex()] = fragment.getEndLine(side);
61     }
62
63     installHighlighter();
64   }
65
66   protected void installHighlighter() {
67     assert myHighlighters.isEmpty();
68
69     createHighlighter(ThreeSide.BASE);
70     if (getType().isLeftChange()) createHighlighter(ThreeSide.LEFT);
71     if (getType().isRightChange()) createHighlighter(ThreeSide.RIGHT);
72
73     doInstallActionHighlighters();
74   }
75
76   @CalledInAwt
77   public void destroyHighlighter() {
78     for (RangeHighlighter highlighter : myHighlighters) {
79       highlighter.dispose();
80     }
81     myHighlighters.clear();
82
83     for (MyGutterOperation operation : myOperations) {
84       operation.dispose();
85     }
86     myOperations.clear();
87   }
88
89   @CalledInAwt
90   public void doReinstallHighlighter() {
91     destroyHighlighter();
92     installHighlighter();
93     myViewer.repaintDividers();
94   }
95
96   private void createHighlighter(@NotNull ThreeSide side) {
97     Editor editor = side.select(myViewer.getEditors());
98     Document document = editor.getDocument();
99
100     TextDiffType type = getDiffType();
101     boolean resolved = isResolved(side);
102     int startLine = getStartLine(side);
103     int endLine = getEndLine(side);
104
105     int start;
106     int end;
107     if (startLine == endLine) {
108       start = end = startLine < DiffUtil.getLineCount(document) ? document.getLineStartOffset(startLine) : document.getTextLength();
109     }
110     else {
111       start = document.getLineStartOffset(startLine);
112       end = document.getLineEndOffset(endLine - 1);
113       if (end < document.getTextLength()) end++;
114     }
115
116     myHighlighters.add(DiffDrawUtil.createHighlighter(editor, start, end, type, false, HighlighterTargetArea.EXACT_RANGE, resolved));
117
118     if (startLine == endLine) {
119       if (startLine != 0) {
120         myHighlighters.add(DiffDrawUtil.createLineMarker(editor, endLine - 1, type, SeparatorPlacement.BOTTOM, true, resolved));
121       }
122     }
123     else {
124       myHighlighters.add(DiffDrawUtil.createLineMarker(editor, startLine, type, SeparatorPlacement.TOP, false, resolved));
125       myHighlighters.add(DiffDrawUtil.createLineMarker(editor, endLine - 1, type, SeparatorPlacement.BOTTOM, false, resolved));
126     }
127   }
128
129   //
130   // Getters
131   //
132
133   @CalledInAwt
134   public void markResolved() {
135     if (isResolved()) return;
136     myResolved[0] = true;
137     myResolved[1] = true;
138     myViewer.onChangeResolved(this);
139     myViewer.reinstallHighlighter(this);
140   }
141
142   @CalledInAwt
143   public void markResolved(@NotNull Side side) {
144     if (isResolved(side)) return;
145     myResolved[side.getIndex()] = true;
146     if (isResolved()) myViewer.onChangeResolved(this);
147     myViewer.reinstallHighlighter(this);
148   }
149
150   public boolean isResolved() {
151     return myResolved[0] && myResolved[1];
152   }
153
154   public boolean isResolved(@NotNull Side side) {
155     return side.select(myResolved);
156   }
157
158   public boolean isResolved(@NotNull ThreeSide side) {
159     switch (side) {
160       case LEFT:
161         return isResolved(Side.LEFT);
162       case BASE:
163         return isResolved();
164       case RIGHT:
165         return isResolved(Side.RIGHT);
166       default:
167         throw new IllegalArgumentException(side.toString());
168     }
169   }
170
171   public int getStartLine(@NotNull ThreeSide side) {
172     return side.select(myStartLines);
173   }
174
175   public int getEndLine(@NotNull ThreeSide side) {
176     return side.select(myEndLines);
177   }
178
179   public void setStartLine(@NotNull ThreeSide side, int value) {
180     myStartLines[side.getIndex()] = value;
181   }
182
183   public void setEndLine(@NotNull ThreeSide side, int value) {
184     myEndLines[side.getIndex()] = value;
185   }
186
187   //
188   // Shift
189   //
190
191   public boolean processBaseChange(int oldLine1, int oldLine2, int shift) {
192     int line1 = getStartLine(ThreeSide.BASE);
193     int line2 = getEndLine(ThreeSide.BASE);
194     int baseIndex = ThreeSide.BASE.getIndex();
195
196     UpdatedLineRange newRange = DiffUtil.updateRangeOnModification(line1, line2, oldLine1, oldLine2, shift);
197     myStartLines[baseIndex] = newRange.startLine;
198     myEndLines[baseIndex] = newRange.endLine;
199
200     boolean rangeAffected = oldLine2 >= line1 && oldLine1 <= line2; // RangeMarker can be updated in a different way
201
202     if (newRange.startLine == newRange.endLine && getDiffType() == TextDiffType.DELETED) {
203       markResolved();
204     }
205
206     return newRange.damaged || rangeAffected;
207   }
208
209   //
210   // Gutter actions
211   //
212
213   private void doInstallActionHighlighters() {
214     ContainerUtil.addIfNotNull(myOperations, createOperation(ThreeSide.LEFT));
215     ContainerUtil.addIfNotNull(myOperations, createOperation(ThreeSide.BASE));
216     ContainerUtil.addIfNotNull(myOperations, createOperation(ThreeSide.RIGHT));
217   }
218
219   @Nullable
220   private MyGutterOperation createOperation(@NotNull ThreeSide side) {
221     if (isResolved(side)) return null;
222
223     EditorEx editor = myViewer.getEditor(side);
224     Document document = editor.getDocument();
225
226     int line = getStartLine(side);
227     int offset = line == DiffUtil.getLineCount(document) ? document.getTextLength() : document.getLineStartOffset(line);
228
229     RangeHighlighter highlighter = editor.getMarkupModel().addRangeHighlighter(offset, offset,
230                                                                                HighlighterLayer.ADDITIONAL_SYNTAX,
231                                                                                null,
232                                                                                HighlighterTargetArea.LINES_IN_RANGE);
233     return new MyGutterOperation(side, highlighter);
234   }
235
236   public void updateGutterActions(boolean force) {
237     for (MyGutterOperation operation : myOperations) {
238       operation.update(force);
239     }
240   }
241
242   private class MyGutterOperation {
243     @NotNull private final ThreeSide mySide;
244     @NotNull private final RangeHighlighter myHighlighter;
245
246     private boolean myCtrlPressed;
247     private boolean myShiftPressed;
248
249     private MyGutterOperation(@NotNull ThreeSide side, @NotNull RangeHighlighter highlighter) {
250       mySide = side;
251       myHighlighter = highlighter;
252
253       update(true);
254     }
255
256     public void dispose() {
257       myHighlighter.dispose();
258     }
259
260     public void update(boolean force) {
261       if (!force && !areModifiersChanged()) {
262         return;
263       }
264       if (myHighlighter.isValid()) myHighlighter.setGutterIconRenderer(createRenderer());
265     }
266
267     private boolean areModifiersChanged() {
268       return myCtrlPressed != myViewer.getModifierProvider().isCtrlPressed() ||
269              myShiftPressed != myViewer.getModifierProvider().isShiftPressed();
270     }
271
272     @Nullable
273     public GutterIconRenderer createRenderer() {
274       myCtrlPressed = myViewer.getModifierProvider().isCtrlPressed();
275       myShiftPressed = myViewer.getModifierProvider().isShiftPressed();
276
277       if (mySide == ThreeSide.BASE) {
278         return createRevertRenderer();
279       }
280       else {
281         Side versionSide = mySide.select(Side.LEFT, null, Side.RIGHT);
282         assert versionSide != null;
283
284         if (!isChange(versionSide)) return null;
285
286         boolean isAppendable = getStartLine(mySide) != getEndLine(mySide) &&
287                                (getStartLine(ThreeSide.BASE) != getEndLine(ThreeSide.BASE) || isConflict());
288
289         if (myShiftPressed) {
290           return createRevertRenderer();
291         }
292         if (myCtrlPressed && isAppendable) {
293           return createAppendRenderer(versionSide);
294         }
295         return createApplyRenderer(versionSide);
296       }
297     }
298   }
299
300   @Nullable
301   private GutterIconRenderer createApplyRenderer(@NotNull final Side side) {
302     return createIconRenderer(DiffBundle.message("merge.dialog.apply.change.action.name"), AllIcons.Diff.Arrow, new Runnable() {
303       @Override
304       public void run() {
305         final Document document = myViewer.getEditor(ThreeSide.BASE).getDocument();
306         DiffUtil.executeWriteCommand(document, myViewer.getProject(), "Apply change", new Runnable() {
307           @Override
308           public void run() {
309             myViewer.replaceChange(TextMergeChange.this, side);
310           }
311         });
312       }
313     });
314   }
315
316   @Nullable
317   private GutterIconRenderer createAppendRenderer(@NotNull final Side side) {
318     return createIconRenderer(DiffBundle.message("merge.dialog.append.change.action.name"), AllIcons.Diff.ArrowLeftDown, new Runnable() {
319       @Override
320       public void run() {
321         final Document document = myViewer.getEditor(ThreeSide.BASE).getDocument();
322         DiffUtil.executeWriteCommand(document, myViewer.getProject(), "Apply change", new Runnable() {
323           @Override
324           public void run() {
325             myViewer.appendChange(TextMergeChange.this, side);
326           }
327         });
328       }
329     });
330   }
331
332   @Nullable
333   private GutterIconRenderer createRevertRenderer() {
334     return createIconRenderer(DiffBundle.message("merge.dialog.ignore.change.action.name"), AllIcons.Diff.Remove, new Runnable() {
335       @Override
336       public void run() {
337         markResolved();
338       }
339     });
340   }
341
342   @Nullable
343   private GutterIconRenderer createIconRenderer(@NotNull final String tooltipText,
344                                                 @NotNull final Icon icon,
345                                                 @NotNull final Runnable perform) {
346     return new GutterIconRenderer() {
347       @NotNull
348       @Override
349       public Icon getIcon() {
350         return icon;
351       }
352
353       public boolean isNavigateAction() {
354         return true;
355       }
356
357       @Nullable
358       @Override
359       public AnAction getClickAction() {
360         return new DumbAwareAction() {
361           @Override
362           public void actionPerformed(AnActionEvent e) {
363             perform.run();
364           }
365         };
366       }
367
368       @Override
369       public boolean equals(Object obj) {
370         return obj == this;
371       }
372
373       @Override
374       public int hashCode() {
375         return System.identityHashCode(this);
376       }
377
378       @Nullable
379       @Override
380       public String getTooltipText() {
381         return tooltipText;
382       }
383     };
384   }
385 }