diff: add annotate action to diff viewers
[idea/community.git] / platform / diff-impl / src / com / intellij / diff / tools / simple / SimpleDiffChange.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.fragments.DiffFragment;
19 import com.intellij.diff.fragments.LineFragment;
20 import com.intellij.diff.util.DiffDrawUtil;
21 import com.intellij.diff.util.DiffUtil;
22 import com.intellij.diff.util.Side;
23 import com.intellij.diff.util.TextDiffType;
24 import com.intellij.icons.AllIcons;
25 import com.intellij.openapi.actionSystem.AnAction;
26 import com.intellij.openapi.actionSystem.AnActionEvent;
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.openapi.project.Project;
33 import org.jetbrains.annotations.NotNull;
34 import org.jetbrains.annotations.Nullable;
35
36 import javax.swing.*;
37 import java.util.ArrayList;
38 import java.util.List;
39
40 public class SimpleDiffChange {
41   @NotNull private final SimpleDiffViewer myViewer;
42
43   @NotNull private final LineFragment myFragment;
44   @Nullable private final List<DiffFragment> myInnerFragments;
45
46   @NotNull private final List<RangeHighlighter> myHighlighters = new ArrayList<RangeHighlighter>();
47   @NotNull private final List<MyGutterOperation> myOperations = new ArrayList<MyGutterOperation>();
48
49   private boolean myIsValid = true;
50   private int[] myLineStartShifts = new int[2];
51   private int[] myLineEndShifts = new int[2];
52
53   // TODO: adjust color from inner fragments - configurable
54   public SimpleDiffChange(@NotNull SimpleDiffViewer viewer,
55                           @NotNull LineFragment fragment,
56                           boolean inlineHighlight) {
57     myViewer = viewer;
58
59     myFragment = fragment;
60     myInnerFragments = inlineHighlight ? fragment.getInnerFragments() : null;
61
62     installHighlighter();
63   }
64
65   public void installHighlighter() {
66     assert myHighlighters.isEmpty();
67
68     if (myInnerFragments != null) {
69       doInstallHighlighterWithInner();
70     }
71     else {
72       doInstallHighlighterSimple();
73     }
74     doInstallActionHighlighters();
75   }
76
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   private void doInstallHighlighterSimple() {
90     createHighlighter(Side.LEFT, false);
91     createHighlighter(Side.RIGHT, false);
92   }
93
94   private void doInstallHighlighterWithInner() {
95     assert myInnerFragments != null;
96
97     createHighlighter(Side.LEFT, true);
98     createHighlighter(Side.RIGHT, true);
99
100     for (DiffFragment fragment : myInnerFragments) {
101       createInlineHighlighter(fragment, Side.LEFT);
102       createInlineHighlighter(fragment, Side.RIGHT);
103     }
104   }
105
106   private void doInstallActionHighlighters() {
107     myOperations.add(createOperation(Side.LEFT));
108     myOperations.add(createOperation(Side.RIGHT));
109   }
110
111   private void createHighlighter(@NotNull Side side, boolean ignored) {
112     Editor editor = myViewer.getEditor(side);
113
114     int start = side.getStartOffset(myFragment);
115     int end = side.getEndOffset(myFragment);
116     TextDiffType type = DiffUtil.getLineDiffType(myFragment);
117
118     myHighlighters.addAll(DiffDrawUtil.createHighlighter(editor, start, end, type, ignored));
119
120     int startLine = side.getStartLine(myFragment);
121     int endLine = side.getEndLine(myFragment);
122
123     if (startLine == endLine) {
124       if (startLine != 0) myHighlighters.addAll(DiffDrawUtil.createLineMarker(editor, endLine - 1, type, SeparatorPlacement.BOTTOM, true));
125     }
126     else {
127       myHighlighters.addAll(DiffDrawUtil.createLineMarker(editor, startLine, type, SeparatorPlacement.TOP));
128       myHighlighters.addAll(DiffDrawUtil.createLineMarker(editor, endLine - 1, type, SeparatorPlacement.BOTTOM));
129     }
130   }
131
132   private void createInlineHighlighter(@NotNull DiffFragment fragment, @NotNull Side side) {
133     int start = side.getStartOffset(fragment);
134     int end = side.getEndOffset(fragment);
135     TextDiffType type = DiffUtil.getDiffType(fragment);
136
137     int startOffset = side.getStartOffset(myFragment);
138     start += startOffset;
139     end += startOffset;
140
141     Editor editor = myViewer.getEditor(side);
142     myHighlighters.addAll(DiffDrawUtil.createInlineHighlighter(editor, start, end, type));
143   }
144
145   public void updateGutterActions(boolean force) {
146     for (MyGutterOperation operation : myOperations) {
147       operation.update(force);
148     }
149   }
150
151   //
152   // Getters
153   //
154
155   public int getStartLine(@NotNull Side side) {
156     return side.getStartLine(myFragment) + side.select(myLineStartShifts);
157   }
158
159   public int getEndLine(@NotNull Side side) {
160     return side.getEndLine(myFragment) + side.select(myLineEndShifts);
161   }
162
163   @NotNull
164   public TextDiffType getDiffType() {
165     return DiffUtil.getLineDiffType(myFragment);
166   }
167
168   public boolean isValid() {
169     return myIsValid;
170   }
171
172   //
173   // Shift
174   //
175
176   public boolean processChange(int oldLine1, int oldLine2, int shift, @NotNull Side side) {
177     int line1 = getStartLine(side);
178     int line2 = getEndLine(side);
179     int sideIndex = side.getIndex();
180
181     DiffUtil.UpdatedLineRange newRange = DiffUtil.updateRangeOnModification(line1, line2, oldLine1, oldLine2, shift);
182     myLineStartShifts[sideIndex] += newRange.startLine - line1;
183     myLineEndShifts[sideIndex] += newRange.endLine - line2;
184
185     if (newRange.damaged) {
186       for (MyGutterOperation operation : myOperations) {
187         operation.dispose();
188       }
189       myOperations.clear();
190
191       myIsValid = false;
192     }
193
194     return newRange.damaged;
195   }
196
197   //
198   // Change applying
199   //
200
201   public boolean isSelectedByLine(int line, @NotNull Side side) {
202     int line1 = getStartLine(side);
203     int line2 = getEndLine(side);
204
205     return DiffUtil.isSelectedByLine(line, line1, line2);
206   }
207
208   //
209   // Helpers
210   //
211
212   @NotNull
213   private MyGutterOperation createOperation(@NotNull Side side) {
214     int offset = side.getStartOffset(myFragment);
215     EditorEx editor = myViewer.getEditor(side);
216     RangeHighlighter highlighter = editor.getMarkupModel().addRangeHighlighter(offset, offset,
217                                                                                HighlighterLayer.ADDITIONAL_SYNTAX,
218                                                                                null,
219                                                                                HighlighterTargetArea.LINES_IN_RANGE);
220     return new MyGutterOperation(side, highlighter);
221   }
222
223   private class MyGutterOperation {
224     @NotNull private final Side mySide;
225     @NotNull private final RangeHighlighter myHighlighter;
226
227     private boolean myCtrlPressed;
228     private boolean myShiftPressed;
229
230     private MyGutterOperation(@NotNull Side side, @NotNull RangeHighlighter highlighter) {
231       mySide = side;
232       myHighlighter = highlighter;
233
234       update(true);
235     }
236
237     public void dispose() {
238       myHighlighter.dispose();
239     }
240
241     public void update(boolean force) {
242       if (!force && !areModifiersChanged()) {
243         return;
244       }
245       if (myHighlighter.isValid()) myHighlighter.setGutterIconRenderer(createRenderer());
246     }
247
248     private boolean areModifiersChanged() {
249       return myCtrlPressed != myViewer.getModifierProvider().isCtrlPressed() ||
250              myShiftPressed != myViewer.getModifierProvider().isShiftPressed();
251     }
252
253     @Nullable
254     public GutterIconRenderer createRenderer() {
255       myCtrlPressed = myViewer.getModifierProvider().isCtrlPressed();
256       myShiftPressed = myViewer.getModifierProvider().isShiftPressed();
257
258       boolean isEditable = DiffUtil.isEditable(myViewer.getEditor(mySide));
259       boolean isOtherEditable = DiffUtil.isEditable(myViewer.getEditor(mySide.other()));
260       boolean isAppendable = myFragment.getStartLine1() != myFragment.getEndLine1() &&
261                              myFragment.getStartLine2() != myFragment.getEndLine2();
262
263       if ((myShiftPressed || !isOtherEditable) && isEditable) {
264         return createRevertRenderer(mySide);
265       }
266       if (myCtrlPressed && isAppendable) {
267         return createAppendRenderer(mySide);
268       }
269       return createApplyRenderer(mySide);
270     }
271   }
272
273   @Nullable
274   private GutterIconRenderer createApplyRenderer(@NotNull final Side side) {
275     return createIconRenderer(side, "Replace", AllIcons.Diff.Arrow, new Runnable() {
276       @Override
277       public void run() {
278         myViewer.replaceChange(SimpleDiffChange.this, side);
279       }
280     });
281   }
282
283   @Nullable
284   private GutterIconRenderer createAppendRenderer(@NotNull final Side side) {
285     return createIconRenderer(side, "Insert", AllIcons.Diff.ArrowLeftDown, new Runnable() {
286       @Override
287       public void run() {
288         myViewer.appendChange(SimpleDiffChange.this, side);
289       }
290     });
291   }
292
293   @Nullable
294   private GutterIconRenderer createRevertRenderer(@NotNull final Side side) {
295     return createIconRenderer(side.other(), "Revert", AllIcons.Diff.Remove, new Runnable() {
296       @Override
297       public void run() {
298         myViewer.replaceChange(SimpleDiffChange.this, side.other());
299       }
300     });
301   }
302
303   @Nullable
304   private GutterIconRenderer createIconRenderer(@NotNull final Side sourceSide,
305                                                 @NotNull final String tooltipText,
306                                                 @NotNull final Icon icon,
307                                                 @NotNull final Runnable perform) {
308     if (!DiffUtil.isEditable(myViewer.getEditor(sourceSide.other()))) return null;
309     return new GutterIconRenderer() {
310       @NotNull
311       @Override
312       public Icon getIcon() {
313         return icon;
314       }
315
316       public boolean isNavigateAction() {
317         return true;
318       }
319
320       @Nullable
321       @Override
322       public AnAction getClickAction() {
323         return new DumbAwareAction() {
324           @Override
325           public void actionPerformed(AnActionEvent e) {
326             if (!myIsValid) return;
327             final Project project = e.getProject();
328             final Document document = myViewer.getEditor(sourceSide.other()).getDocument();
329             DiffUtil.executeWriteCommand(document, project, "Replace change", new Runnable() {
330               @Override
331               public void run() {
332                 perform.run();
333               }
334             });
335           }
336         };
337       }
338
339       @Override
340       public boolean equals(Object obj) {
341         return obj == this;
342       }
343
344       @Override
345       public int hashCode() {
346         return System.identityHashCode(this);
347       }
348
349       @Nullable
350       @Override
351       public String getTooltipText() {
352         return tooltipText;
353       }
354
355       @Override
356       public boolean isDumbAware() {
357         return true;
358       }
359     };
360   }
361 }