diff: add annotate action to diff viewers
[idea/community.git] / platform / diff-impl / src / com / intellij / diff / tools / fragmented / UnifiedDiffChange.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.fragmented;
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.DiffUtil.UpdatedLineRange;
23 import com.intellij.diff.util.Side;
24 import com.intellij.diff.util.TextDiffType;
25 import com.intellij.icons.AllIcons;
26 import com.intellij.openapi.actionSystem.AnAction;
27 import com.intellij.openapi.actionSystem.AnActionEvent;
28 import com.intellij.openapi.editor.Document;
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 com.intellij.openapi.util.TextRange;
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 UnifiedDiffChange {
42   @NotNull private final UnifiedDiffViewer myViewer;
43   @NotNull private final EditorEx myEditor;
44
45   // Boundaries of this change in myEditor. If current state is out-of-date - approximate value.
46   private int myLine1;
47   private int myLine2;
48
49   @NotNull private final LineFragment myLineFragment;
50
51   @NotNull private final List<RangeHighlighter> myHighlighters = new ArrayList<RangeHighlighter>();
52   @NotNull private final List<MyGutterOperation> myOperations = new ArrayList<MyGutterOperation>();
53
54   public UnifiedDiffChange(@NotNull UnifiedDiffViewer viewer, @NotNull ChangedBlock block, boolean innerFragments) {
55     myViewer = viewer;
56     myEditor = viewer.getEditor();
57
58     myLine1 = block.getLine1();
59     myLine2 = block.getLine2();
60     myLineFragment = block.getLineFragment();
61
62     TextRange deleted = new TextRange(block.getStartOffset1(), block.getEndOffset1());
63     TextRange inserted = new TextRange(block.getStartOffset2(), block.getEndOffset2());
64
65     installHighlighter(deleted, inserted, innerFragments);
66   }
67
68   public void destroyHighlighter() {
69     for (RangeHighlighter highlighter : myHighlighters) {
70       highlighter.dispose();
71     }
72     myHighlighters.clear();
73
74     for (MyGutterOperation operation : myOperations) {
75       operation.dispose();
76     }
77     myOperations.clear();
78   }
79
80   private void installHighlighter(@NotNull TextRange deleted, @NotNull TextRange inserted, boolean innerFragments) {
81     assert myHighlighters.isEmpty();
82
83     if (innerFragments && myLineFragment.getInnerFragments() != null) {
84       doInstallHighlighterWithInner(deleted, inserted);
85     }
86     else {
87       doInstallHighlighterSimple(deleted, inserted);
88     }
89     doInstallActionHighlighters();
90   }
91
92   private void doInstallActionHighlighters() {
93     boolean leftEditable = myViewer.isEditable(Side.LEFT, false);
94     boolean rightEditable = myViewer.isEditable(Side.RIGHT, false);
95
96     if (rightEditable) myOperations.add(createOperation(Side.LEFT, false));
97     if (leftEditable) myOperations.add(createOperation(Side.RIGHT, rightEditable));
98   }
99
100   private void doInstallHighlighterSimple(@NotNull TextRange deleted, @NotNull TextRange inserted) {
101     createLineHighlighters(deleted, inserted, false);
102   }
103
104   private void doInstallHighlighterWithInner(@NotNull TextRange deleted, @NotNull TextRange inserted) {
105     List<DiffFragment> innerFragments = myLineFragment.getInnerFragments();
106     assert innerFragments != null;
107
108     createLineHighlighters(deleted, inserted, true);
109
110     for (DiffFragment fragment : innerFragments) {
111       createInlineHighlighter(TextDiffType.DELETED,
112                               deleted.getStartOffset() + fragment.getStartOffset1(),
113                               deleted.getStartOffset() + fragment.getEndOffset1());
114       createInlineHighlighter(TextDiffType.INSERTED,
115                               inserted.getStartOffset() + fragment.getStartOffset2(),
116                               inserted.getStartOffset() + fragment.getEndOffset2());
117     }
118   }
119
120   private void createLineHighlighters(@NotNull TextRange deleted, @NotNull TextRange inserted, boolean ignored) {
121     if (!inserted.isEmpty() && !deleted.isEmpty()) {
122       createLineMarker(TextDiffType.DELETED, getLine1(), SeparatorPlacement.TOP);
123       createHighlighter(TextDiffType.DELETED, deleted.getStartOffset(), deleted.getEndOffset(), ignored);
124       createHighlighter(TextDiffType.INSERTED, inserted.getStartOffset(), inserted.getEndOffset(), ignored);
125       createLineMarker(TextDiffType.INSERTED, getLine2() - 1, SeparatorPlacement.BOTTOM);
126     }
127     else if (!inserted.isEmpty()) {
128       createLineMarker(TextDiffType.INSERTED, getLine1(), SeparatorPlacement.TOP);
129       createHighlighter(TextDiffType.INSERTED, inserted.getStartOffset(), inserted.getEndOffset(), ignored);
130       createLineMarker(TextDiffType.INSERTED, getLine2() - 1, SeparatorPlacement.BOTTOM);
131     }
132     else if (!deleted.isEmpty()) {
133       createLineMarker(TextDiffType.DELETED, getLine1(), SeparatorPlacement.TOP);
134       createHighlighter(TextDiffType.DELETED, deleted.getStartOffset(), deleted.getEndOffset(), ignored);
135       createLineMarker(TextDiffType.DELETED, getLine2() - 1, SeparatorPlacement.BOTTOM);
136     }
137   }
138
139   private void createHighlighter(@NotNull TextDiffType type, int start, int end, boolean ignored) {
140     myHighlighters.addAll(DiffDrawUtil.createHighlighter(myEditor, start, end, type, ignored));
141   }
142
143   private void createInlineHighlighter(@NotNull TextDiffType type, int start, int end) {
144     myHighlighters.addAll(DiffDrawUtil.createInlineHighlighter(myEditor, start, end, type));
145   }
146
147   private void createLineMarker(@NotNull TextDiffType type, int line, @NotNull SeparatorPlacement placement) {
148     myHighlighters.addAll(DiffDrawUtil.createLineMarker(myEditor, line, type, placement));
149   }
150
151   public int getLine1() {
152     return myLine1;
153   }
154
155   public int getLine2() {
156     return myLine2;
157   }
158
159   /*
160    * Warning: It does not updated on document change. Check myViewer.isStateInconsistent() before use.
161    */
162   @NotNull
163   public LineFragment getLineFragment() {
164     return myLineFragment;
165   }
166
167   public void processChange(int oldLine1, int oldLine2, int shift) {
168     UpdatedLineRange newRange = DiffUtil.updateRangeOnModification(myLine1, myLine2, oldLine1, oldLine2, shift);
169     myLine1 = newRange.startLine;
170     myLine2 = newRange.endLine;
171   }
172
173   //
174   // Gutter
175   //
176
177   public void updateGutterActions() {
178     for (MyGutterOperation operation : myOperations) {
179       operation.update();
180     }
181   }
182
183   @NotNull
184   private MyGutterOperation createOperation(@NotNull Side side, boolean secondAction) {
185     int line = secondAction ? Math.min(myLine1 + 1, myLine2 - 1) : myLine1;
186     int offset = myEditor.getDocument().getLineStartOffset(line);
187
188     RangeHighlighter highlighter = myEditor.getMarkupModel().addRangeHighlighter(offset, offset,
189                                                                                  HighlighterLayer.ADDITIONAL_SYNTAX,
190                                                                                  null,
191                                                                                  HighlighterTargetArea.LINES_IN_RANGE);
192     return new MyGutterOperation(side, highlighter);
193   }
194
195   private class MyGutterOperation {
196     @NotNull private final Side mySide;
197     @NotNull private final RangeHighlighter myHighlighter;
198
199     private MyGutterOperation(@NotNull Side sourceSide, @NotNull RangeHighlighter highlighter) {
200       mySide = sourceSide;
201       myHighlighter = highlighter;
202
203       update();
204     }
205
206     public void dispose() {
207       myHighlighter.dispose();
208     }
209
210     public void update() {
211       if (myHighlighter.isValid()) myHighlighter.setGutterIconRenderer(createRenderer());
212     }
213
214     @Nullable
215     public GutterIconRenderer createRenderer() {
216       if (myViewer.isStateIsOutOfDate()) return null;
217       if (!myViewer.isEditable(mySide.other(), true)) return null;
218       boolean bothEditable = myViewer.isEditable(mySide, true);
219
220       if (bothEditable) {
221         if (mySide.isLeft()) {
222           return createIconRenderer(mySide, "Apply Before", AllIcons.Diff.ArrowRight);
223         }
224         else {
225           return createIconRenderer(mySide, "Apply After", AllIcons.Diff.Arrow);
226         }
227       }
228       else {
229         if (mySide.isLeft()) {
230           return createIconRenderer(mySide, "Revert", AllIcons.Diff.Remove);
231         }
232         else {
233           return createIconRenderer(mySide, "Apply", AllIcons.Diff.Arrow);
234         }
235       }
236     }
237   }
238
239   @Nullable
240   private GutterIconRenderer createIconRenderer(@NotNull final Side sourceSide,
241                                                 @NotNull final String tooltipText,
242                                                 @NotNull final Icon icon) {
243     return new GutterIconRenderer() {
244       @NotNull
245       @Override
246       public Icon getIcon() {
247         return icon;
248       }
249
250       public boolean isNavigateAction() {
251         return true;
252       }
253
254       @Nullable
255       @Override
256       public AnAction getClickAction() {
257         return new DumbAwareAction() {
258           @Override
259           public void actionPerformed(AnActionEvent e) {
260             if (myViewer.isStateIsOutOfDate()) return;
261             if (!myViewer.isEditable(sourceSide.other(), true)) return;
262
263             final Project project = e.getProject();
264             final Document document = myViewer.getDocument(sourceSide.other());
265
266             DiffUtil.executeWriteCommand(document, project, "Replace change", new Runnable() {
267               @Override
268               public void run() {
269                 myViewer.replaceChange(UnifiedDiffChange.this, sourceSide);
270                 myViewer.scheduleRediff();
271               }
272             });
273             // applyChange() will schedule rediff, but we want to try to do it in sync
274             // and we can't do it inside write action
275             myViewer.rediff();
276           }
277         };
278       }
279
280       @Override
281       public boolean equals(Object obj) {
282         return obj == this;
283       }
284
285       @Override
286       public int hashCode() {
287         return System.identityHashCode(this);
288       }
289
290       @Nullable
291       @Override
292       public String getTooltipText() {
293         return tooltipText;
294       }
295
296       @Override
297       public boolean isDumbAware() {
298         return true;
299       }
300     };
301   }
302 }