EA-37573 NPE
[idea/community.git] / platform / platform-impl / src / com / intellij / openapi / diff / impl / incrementalMerge / ui / MergePanel2.java
1 /*
2  * Copyright 2000-2012 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.openapi.diff.impl.incrementalMerge.ui;
17
18 import com.intellij.icons.AllIcons;
19 import com.intellij.openapi.Disposable;
20 import com.intellij.openapi.actionSystem.ActionManager;
21 import com.intellij.openapi.actionSystem.DataContext;
22 import com.intellij.openapi.actionSystem.IdeActions;
23 import com.intellij.openapi.actionSystem.PlatformDataKeys;
24 import com.intellij.openapi.components.ServiceManager;
25 import com.intellij.openapi.diagnostic.Logger;
26 import com.intellij.openapi.diff.*;
27 import com.intellij.openapi.diff.actions.NextDiffAction;
28 import com.intellij.openapi.diff.actions.PreviousDiffAction;
29 import com.intellij.openapi.diff.impl.DiffUtil;
30 import com.intellij.openapi.diff.impl.EditingSides;
31 import com.intellij.openapi.diff.impl.GenericDataProvider;
32 import com.intellij.openapi.diff.impl.highlighting.FragmentSide;
33 import com.intellij.openapi.diff.impl.incrementalMerge.ChangeCounter;
34 import com.intellij.openapi.diff.impl.incrementalMerge.ChangeList;
35 import com.intellij.openapi.diff.impl.incrementalMerge.MergeList;
36 import com.intellij.openapi.diff.impl.mergeTool.MergeRequestImpl;
37 import com.intellij.openapi.diff.impl.splitter.DiffDividerPaint;
38 import com.intellij.openapi.diff.impl.splitter.LineBlocks;
39 import com.intellij.openapi.diff.impl.util.*;
40 import com.intellij.openapi.editor.Document;
41 import com.intellij.openapi.editor.Editor;
42 import com.intellij.openapi.editor.colors.EditorColorsManager;
43 import com.intellij.openapi.editor.colors.EditorColorsScheme;
44 import com.intellij.openapi.editor.ex.EditorEx;
45 import com.intellij.openapi.editor.ex.EditorMarkupModel;
46 import com.intellij.openapi.editor.highlighter.EditorHighlighterFactory;
47 import com.intellij.openapi.fileTypes.FileType;
48 import com.intellij.openapi.fileTypes.FileTypes;
49 import com.intellij.openapi.project.Project;
50 import com.intellij.openapi.ui.DialogBuilder;
51 import com.intellij.openapi.ui.LabeledComponent;
52 import com.intellij.openapi.util.Disposer;
53 import com.intellij.openapi.util.text.StringUtil;
54 import com.intellij.ui.EditorNotificationPanel;
55 import com.intellij.util.diff.FilesTooBigForDiffException;
56 import gnu.trove.TIntHashSet;
57 import org.jetbrains.annotations.NotNull;
58 import org.jetbrains.annotations.Nullable;
59
60 import javax.swing.*;
61 import java.awt.*;
62 import java.util.ArrayList;
63 import java.util.Arrays;
64 import java.util.Collection;
65
66 public class MergePanel2 implements DiffViewer {
67   private static final Logger LOG = Logger.getInstance("#com.intellij.openapi.diff.impl.incrementalMerge.ui.MergePanel2");
68   private final DiffPanelOuterComponent myPanel;
69   private DiffRequest myData;
70   private MergeList myMergeList;
71   private boolean myDuringCreation = false;
72   private final SyncScrollSupport myScrollSupport = new SyncScrollSupport();
73   private final DiffDivider[] myDividers = {new DiffDivider(FragmentSide.SIDE2), new DiffDivider(FragmentSide.SIDE1)};
74   private boolean myScrollToFirstDiff = true;
75
76   private final LabeledComponent[] myEditorsPanels = new LabeledComponent[EDITORS_COUNT];
77   public static final int EDITORS_COUNT = 3;
78   private final DividersRepainter myDividersRepainter = new DividersRepainter();
79   private StatusUpdater myStatusUpdater;
80   private final DialogBuilder myBuilder;
81   private final MyDataProvider myProvider;
82
83   public MergePanel2(DialogBuilder builder, Disposable parent) {
84     ArrayList<EditorPlace> editorPlaces = new ArrayList<EditorPlace>();
85     EditorPlace.EditorListener placeListener = new EditorPlace.EditorListener() {
86       public void onEditorCreated(EditorPlace place) {
87         if (myDuringCreation) return;
88         disposeMergeList();
89         myDuringCreation = true;
90         try {
91           tryInitView();
92         }
93         finally {
94           myDuringCreation = false;
95         }
96       }
97
98       public void onEditorReleased(Editor releasedEditor) {
99         LOG.assertTrue(!myDuringCreation);
100         disposeMergeList();
101       }
102     };
103     for (int i = 0; i < EDITORS_COUNT; i++) {
104       EditorPlace editorPlace = new EditorPlace(new DiffEditorState(i), indexToColumn(i), this);
105       Disposer.register(parent, editorPlace);
106       editorPlaces.add(editorPlace);
107       editorPlace.addListener(placeListener);
108       myEditorsPanels[i] = new LabeledComponent();
109       myEditorsPanels[i].setLabelLocation(BorderLayout.NORTH);
110       myEditorsPanels[i].setComponent(editorPlace);
111     }
112     FontSizeSynchronizer.attachTo(editorPlaces);
113     myPanel = new DiffPanelOuterComponent(TextDiffType.MERGE_TYPES, createToolbar());
114     myPanel.insertDiffComponent(new ThreePanels(myEditorsPanels, myDividers), new MyScrollingPanel());
115     myProvider = new MyDataProvider();
116     myPanel.setDataProvider(myProvider);
117     myBuilder = builder;
118   }
119
120   /**
121    * Convert legacy-style editor (or panel) number to the {@link MergePanelColumn}.
122    * @param i 0, 1 or 2
123    * @return  Left, base or right, respectively.
124    */
125   private static MergePanelColumn indexToColumn(int i) {
126     switch (i) {
127       case 0: return MergePanelColumn.LEFT;
128       case 1: return MergePanelColumn.BASE;
129       case 2: return MergePanelColumn.RIGHT;
130       default: throw new IllegalStateException("Incorrect value for a merge column: " + i);
131     }
132   }
133
134   @NotNull
135   private DiffRequest.ToolbarAddons createToolbar() {
136     return new DiffRequest.ToolbarAddons() {
137       public void customize(DiffToolbar toolbar) {
138         ActionManager actionManager = ActionManager.getInstance();
139         toolbar.addAction(actionManager.getAction(IdeActions.ACTION_COPY));
140         toolbar.addAction(actionManager.getAction(IdeActions.ACTION_FIND));
141         toolbar.addAction(PreviousDiffAction.find());
142         toolbar.addAction(NextDiffAction.find());
143         toolbar.addSeparator();
144         toolbar.addAction(new OpenPartialDiffAction(1, 0, AllIcons.Diff.LeftDiff));
145         toolbar.addAction(new OpenPartialDiffAction(1, 2, AllIcons.Diff.RightDiff));
146         toolbar.addAction(new OpenPartialDiffAction(0, 2, AllIcons.Diff.BranchDiff));
147         toolbar.addSeparator();
148         toolbar.addAction(new ApplyNonConflicts(myPanel));
149         toolbar.addSeparator();
150         toolbar.addAction(new MergeToolSettingsAction(getEditors()));
151       }
152     };
153   }
154
155   @NotNull
156   private Collection<Editor> getEditors() {
157     Collection<Editor> editors = new ArrayList<Editor>(3);
158     for (EditorPlace place : getEditorPlaces()) {
159       editors.add(place.getEditor());
160     }
161     return editors;
162   }
163
164   @NotNull
165   private Collection<EditorPlace> getEditorPlaces() {
166     Collection<EditorPlace> editorPlaces = new ArrayList<EditorPlace>(3);
167     for (LabeledComponent editorsPanel : myEditorsPanels) {
168       editorPlaces.add((EditorPlace) editorsPanel.getComponent());
169     }
170     return editorPlaces;
171   }
172
173   public void setScrollToFirstDiff(final boolean scrollToFirstDiff) {
174     myScrollToFirstDiff = scrollToFirstDiff;
175   }
176
177   /**
178    * @deprecated Because it references by index.
179    */
180   @Nullable
181   @Deprecated
182   public Editor getEditor(int index) {
183     return getEditorPlace(index).getEditor();
184   }
185
186   public FileType getContentType() {
187     return myData == null ? FileTypes.PLAIN_TEXT : getContentType(myData);
188   }
189
190   /**
191    * @deprecated Because it references by index.
192    */
193   @Deprecated
194   public String getVersionTitle(int index) {
195     return myEditorsPanels[index].getRawText();
196   }
197
198   /**
199    * @deprecated Because it references by index.
200    */
201   @Deprecated
202   public EditorPlace getEditorPlace(int index) {
203     return (EditorPlace)myEditorsPanels[index].getComponent();
204   }
205
206   private void createMergeList() {
207     if (myData == null) return;
208     DiffContent[] contents = myData.getContents();
209     for (int i = 0; i < EDITORS_COUNT; i++) {
210       EditorPlace editorPlace = getEditorPlace(i);
211       editorPlace.setDocument(contents[i].getDocument());
212       setHighlighterSettings(null, editorPlace);
213     }
214     tryInitView();
215   }
216
217   private void tryInitView() {
218     if (!hasAllEditors()) return;
219     if (myMergeList != null) return;
220     try {
221       myMergeList = MergeList.create(myData);
222       myMergeList.addListener(myDividersRepainter);
223       myStatusUpdater = StatusUpdater.install(myMergeList, myPanel);
224       Editor left = getEditor(0);
225       Editor base = getEditor(1);
226       Editor right = getEditor(2);
227
228       myMergeList.setMarkups(left, base, right);
229       EditingSides[] sides = {getFirstEditingSide(), getSecondEditingSide()};
230       myScrollSupport.install(sides);
231       for (int i = 0; i < myDividers.length; i++) {
232         myDividers[i].listenEditors(sides[i]);
233       }
234       if (myScrollToFirstDiff) {
235         myPanel.requestScrollEditors();
236       }
237     }
238     catch (final FilesTooBigForDiffException e) {
239       myPanel.insertTopComponent(new EditorNotificationPanel() {
240         {
241           myLabel.setText(e.getMessage());
242         }
243       });
244     }
245   }
246
247   @NotNull
248   EditingSides getFirstEditingSide() {
249     return new MyEditingSides(FragmentSide.SIDE1);
250   }
251
252   @NotNull
253   EditingSides getSecondEditingSide() {
254     return new MyEditingSides(FragmentSide.SIDE2);
255   }
256
257   public void setHighlighterSettings(@Nullable EditorColorsScheme settings) {
258     for (EditorPlace place : getEditorPlaces()) {
259       setHighlighterSettings(settings, place);
260     }
261   }
262
263   private void setHighlighterSettings(@Nullable EditorColorsScheme settings, @NotNull EditorPlace place) {
264     if (settings == null) {
265       settings = EditorColorsManager.getInstance().getGlobalScheme();
266     }
267     Editor editor = place.getEditor();
268     DiffEditorState editorState = place.getState();
269     if (editor != null) {
270       ((EditorEx)editor).setHighlighter(EditorHighlighterFactory.getInstance().
271         createEditorHighlighter(editorState.getFileType(), settings, editorState.getProject()));
272     }
273   }
274
275   private static void initEditorSettings(@NotNull Editor editor) {
276     Project project = editor.getProject();
277     MergeToolSettings settings = project == null ? null : ServiceManager.getService(project, MergeToolSettings.class);
278     for (MergeToolEditorSetting property : MergeToolEditorSetting.values()) {
279       property.apply(editor, settings == null ? property.getDefault() : settings.getPreference(property));
280     }
281     editor.getSettings().setLineMarkerAreaShown(true);
282   }
283
284   private void disposeMergeList() {
285     if (myMergeList == null) return;
286     if (myStatusUpdater != null) {
287       myStatusUpdater.dispose(myMergeList);
288       myStatusUpdater = null;
289     }
290     myMergeList.removeListener(myDividersRepainter);
291
292     myMergeList = null;
293     for (DiffDivider myDivider : myDividers) {
294       myDivider.stopListenEditors();
295     }
296   }
297
298   public void setDiffRequest(DiffRequest data) {
299     setTitle(data.getWindowTitle());
300     disposeMergeList();
301     for (int i = 0; i < EDITORS_COUNT; i++) {
302       getEditorPlace(i).setDocument(null);
303     }
304     LOG.assertTrue(!myDuringCreation);
305     myDuringCreation = true;
306     myProvider.putData(data.getGenericData());
307     try {
308       myData = data;
309       String[] titles = myData.getContentTitles();
310       for (int i = 0; i < myEditorsPanels.length; i++) {
311         LabeledComponent editorsPanel = myEditorsPanels[i];
312         editorsPanel.getLabel().setText(titles[i]);
313       }
314       createMergeList();
315       data.customizeToolbar(myPanel.resetToolbar());
316       myPanel.registerToolbarActions();
317       if ( data instanceof MergeRequestImpl && myBuilder != null){
318         ((MergeRequestImpl)data).setActions(myBuilder, this);
319       }
320     }
321     finally {
322       myDuringCreation = false;
323     }
324   }
325
326   private void setTitle(String windowTitle) {
327     JDialog parent = getDialogWrapperParent();
328     if (parent == null) return;
329     parent.setTitle(windowTitle);
330   }
331
332   @Nullable
333   private JDialog getDialogWrapperParent() {
334     Component panel = myPanel;
335     while (panel != null){
336       if (panel instanceof JDialog) return (JDialog)panel;
337       panel = panel.getParent();
338     }
339     return null;
340   }
341
342   public JComponent getComponent() {
343     return myPanel;
344   }
345
346   @Nullable
347   public JComponent getPreferredFocusedComponent() {
348     return getEditorPlace(1).getContentComponent();
349   }
350
351   public int getContentsNumber() {
352     return 3;
353   }
354
355   @Override
356   public boolean acceptsType(DiffViewerType type) {
357     return DiffViewerType.merge.equals(type);
358   }
359
360   private boolean hasAllEditors() {
361     for (int i = 0; i < EDITORS_COUNT; i++) {
362       if (getEditor(i) == null) return false;
363     }
364     return true;
365   }
366
367   @Nullable
368   public MergeRequestImpl getMergeRequest() {
369     return (MergeRequestImpl)(myData instanceof MergeRequestImpl ? myData : null);
370   }
371
372   private class MyEditingSides implements EditingSides {
373     private final FragmentSide mySide;
374
375     private MyEditingSides(FragmentSide side) {
376       mySide = side;
377     }
378
379     @Nullable
380     public Editor getEditor(FragmentSide side) {
381       return MergePanel2.this.getEditor(mySide.getIndex() + side.getIndex());
382     }
383
384     public LineBlocks getLineBlocks() {
385       return myMergeList.getChanges(mySide).getLineBlocks();
386     }
387   }
388
389   private class MyScrollingPanel implements DiffPanelOuterComponent.ScrollingPanel {
390     public void scrollEditors() {
391       Editor centerEditor = getEditor(1);
392       JComponent centerComponent = centerEditor.getContentComponent();
393       if (centerComponent.isShowing()) {
394         centerComponent.requestFocus();
395       }
396       int[] toLeft = getPrimaryBeginnings(myDividers[0].getPaint());
397       int[] toRight = getPrimaryBeginnings(myDividers[1].getPaint());
398       int line;
399       if (toLeft.length > 0 && toRight.length > 0) {
400         line = Math.min(toLeft[0], toRight[0]);
401       }
402       else if (toLeft.length > 0) {
403         line = toLeft[0];
404       }
405       else if (toRight.length > 0) {
406         line = toRight[0];
407       }
408       else {
409         return;
410       }
411       SyncScrollSupport.scrollEditor(centerEditor, line);
412     }
413
414     private int[] getPrimaryBeginnings(DiffDividerPaint paint) {
415       FragmentSide primarySide = paint.getLeftSide();
416       LOG.assertTrue(getEditor(1) == paint.getSides().getEditor(primarySide));
417       return paint.getSides().getLineBlocks().getBeginnings(primarySide, true);
418     }
419   }
420
421   class DiffEditorState {
422     private final int myIndex;
423     private Document myDocument;
424
425     private DiffEditorState(int index) {
426       myIndex = index;
427     }
428
429     public void setDocument(Document document) {
430       myDocument = document;
431     }
432
433     public Document getDocument() {
434       return myDocument;
435     }
436
437     @Nullable
438     public EditorEx createEditor() {
439       Document document = getDocument();
440       if (document == null) return null;
441       Project project = myData.getProject();
442       EditorEx editor = DiffUtil.createEditor(document, project, myIndex != 1);
443
444       if (editor == null) return editor;
445       //FileType type = getFileType();
446       //editor.setHighlighter(HighlighterFactory.createHighlighter(project, type));
447       if (myIndex == 0) editor.setVerticalScrollbarOrientation(EditorEx.VERTICAL_SCROLLBAR_LEFT);
448       if (myIndex != 1) ((EditorMarkupModel)editor.getMarkupModel()).setErrorStripeVisible(true);
449       editor.getSettings().setFoldingOutlineShown(false);
450       editor.getFoldingModel().setFoldingEnabled(false);
451       editor.getSettings().setLineMarkerAreaShown(false);
452       editor.getSettings().setFoldingOutlineShown(false);
453       editor.getGutterComponentEx().setShowDefaultGutterPopup(false);
454       initEditorSettings(editor);
455
456       return editor;
457     }
458
459     public FileType getFileType() {
460       return getContentType();
461     }
462
463     @Nullable
464     public Project getProject() {
465       return myData == null ? null : myData.getProject();
466     }
467   }
468
469   private static FileType getContentType(DiffRequest diffData) {
470     FileType contentType = diffData.getContents()[1].getContentType();
471     if (contentType == null) contentType = FileTypes.PLAIN_TEXT;
472     return contentType;
473   }
474
475   private class MyDataProvider extends GenericDataProvider {
476     public Object getData(String dataId) {
477       if (FocusDiffSide.DATA_KEY.is(dataId)) {
478         int index = getFocusedEditorIndex();
479         if (index < 0) return null;
480         switch (index) {
481           case 0:
482             return new BranchFocusedSide(FragmentSide.SIDE1);
483           case 1:
484             return new MergeFocusedSide();
485           case 2:
486             return new BranchFocusedSide(FragmentSide.SIDE2);
487         }
488       }
489       else if (PlatformDataKeys.DIFF_VIEWER.is(dataId)) return MergePanel2.this;
490       return super.getData(dataId);
491     }
492
493     private int getFocusedEditorIndex() {
494       for (int i = 0; i < EDITORS_COUNT; i++) {
495         Editor editor = getEditor(i);
496         if (editor == null) continue;
497         if (editor.getContentComponent().isFocusOwner()) return i;
498       }
499       return -1;
500     }
501   }
502
503   private class BranchFocusedSide implements FocusDiffSide {
504     private final FragmentSide mySide;
505
506     private BranchFocusedSide(FragmentSide side) {
507       mySide = side;
508     }
509
510     @Nullable
511     public Editor getEditor() {
512       return MergePanel2.this.getEditor(mySide.getMergeIndex());
513     }
514
515     public int[] getFragmentStartingLines() {
516       return myMergeList.getChanges(mySide).getLineBlocks().getBeginnings(MergeList.BRANCH_SIDE);
517     }
518   }
519
520   private class MergeFocusedSide implements FocusDiffSide {
521     public Editor getEditor() {
522       return MergePanel2.this.getEditor(1);
523     }
524
525     public int[] getFragmentStartingLines() {
526       TIntHashSet beginnings = new TIntHashSet();
527       if (myMergeList != null) {
528         for (int i = 0; i < 2; i++) {
529           FragmentSide branchSide = FragmentSide.fromIndex(i);
530           beginnings.addAll(myMergeList.getChanges(branchSide).getLineBlocks().getBeginnings(MergeList.BASE_SIDE));
531         }
532       }
533       int[] result = beginnings.toArray();
534       Arrays.sort(result);
535       return result;
536     }
537   }
538
539   @Nullable
540   public static MergePanel2 fromDataContext(DataContext dataContext) {
541     DiffViewer diffComponent = PlatformDataKeys.DIFF_VIEWER.getData(dataContext);
542     return diffComponent instanceof MergePanel2 ? (MergePanel2)diffComponent : null;
543   }
544
545   public MergeList getMergeList() {
546     return myMergeList;
547   }
548
549   public void setColorScheme(EditorColorsScheme scheme) {
550     for (Editor editor : getEditors()) {
551       if (editor != null) {
552         ((EditorEx)editor).setColorsScheme(scheme);
553       }
554     }
555     myPanel.setColorScheme(scheme);
556   }
557
558   private class DividersRepainter implements ChangeList.Listener {
559
560     @Override
561     public void onChangeApplied(ChangeList source) {
562       FragmentSide side = myMergeList.getSideOf(source);
563       myDividers[side.getIndex()].repaint();
564     }
565
566     public void onChangeRemoved(ChangeList source) {
567       FragmentSide side = myMergeList.getSideOf(source);
568       myDividers[side.getIndex()].repaint();
569     }
570   }
571
572   private static class StatusUpdater implements ChangeCounter.Listener {
573     private final DiffPanelOuterComponent myPanel;
574
575     private StatusUpdater(DiffPanelOuterComponent panel) {
576       myPanel = panel;
577     }
578
579     public void onCountersChanged(ChangeCounter counter) {
580       int changes = counter.getChangeCounter();
581       int conflicts = counter.getConflictCounter();
582       String text;
583       if (changes == 0 && conflicts == 0) {
584         text = DiffBundle.message("merge.dialog.all.conflicts.resolved.message.text");
585       }
586       else {
587         // The Bundle doesn't support such complex formats. Until that is fixed, constructing manually
588         //text = DiffBundle.message("merge.statistics.message", changes, conflicts);
589         text = makeCountersText(changes, conflicts);
590       }
591       myPanel.setStatusBarText(text);
592     }
593
594     @NotNull
595     private static String makeCountersText(int changes, int conflicts) {
596       return makeCounterWord(changes, "change") + ". " + makeCounterWord(conflicts, "conflict");
597     }
598
599     @NotNull
600     private static String makeCounterWord(int number, @NotNull String word) {
601       if (number == 0) {
602         return "No " + StringUtil.pluralize(word);
603       }
604       return number + " " + StringUtil.pluralize(word, number);
605     }
606
607     public void dispose(@NotNull MergeList mergeList) {
608       ChangeCounter.getOrCreate(mergeList).removeListener(this);
609     }
610
611     public static StatusUpdater install(MergeList mergeList, DiffPanelOuterComponent panel) {
612       ChangeCounter counters = ChangeCounter.getOrCreate(mergeList);
613       StatusUpdater updater = new StatusUpdater(panel);
614       counters.addListener(updater);
615       updater.onCountersChanged(counters);
616       return updater;
617     }
618   }
619
620   public static class AsComponent extends JPanel{
621     private final MergePanel2 myMergePanel;
622
623     public AsComponent(Disposable parent) {
624       super(new BorderLayout());
625       myMergePanel = new MergePanel2(null, parent);
626       add(myMergePanel.getComponent(), BorderLayout.CENTER);
627     }
628
629     public MergePanel2 getMergePanel() {
630       return myMergePanel;
631     }
632
633     @SuppressWarnings({"UnusedDeclaration"})
634     public boolean isToolbarEnabled() {
635       return myMergePanel.myPanel.isToolbarEnabled();
636     }
637
638     public void setToolbarEnabled(boolean enabled) {
639       myMergePanel.myPanel.disableToolbar(!enabled);
640     }
641   }
642 }