blob: cbad541fe12934b67193de9ca93ecb8fa91f220a [file] [log] [blame]
/*
* Copyright 2000-2012 JetBrains s.r.o.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.intellij.openapi.diff.impl.incrementalMerge.ui;
import com.intellij.icons.AllIcons;
import com.intellij.openapi.Disposable;
import com.intellij.openapi.actionSystem.ActionManager;
import com.intellij.openapi.actionSystem.DataContext;
import com.intellij.openapi.actionSystem.IdeActions;
import com.intellij.openapi.actionSystem.PlatformDataKeys;
import com.intellij.openapi.components.ServiceManager;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.diff.*;
import com.intellij.openapi.diff.actions.NextDiffAction;
import com.intellij.openapi.diff.actions.PreviousDiffAction;
import com.intellij.openapi.diff.actions.ToggleAutoScrollAction;
import com.intellij.openapi.diff.impl.*;
import com.intellij.openapi.diff.impl.highlighting.FragmentSide;
import com.intellij.openapi.diff.impl.incrementalMerge.ChangeCounter;
import com.intellij.openapi.diff.impl.incrementalMerge.ChangeList;
import com.intellij.openapi.diff.impl.incrementalMerge.MergeList;
import com.intellij.openapi.diff.impl.mergeTool.MergeRequestImpl;
import com.intellij.openapi.diff.impl.mergeTool.MergeTool;
import com.intellij.openapi.diff.impl.settings.DiffMergeEditorSetting;
import com.intellij.openapi.diff.impl.settings.DiffMergeSettings;
import com.intellij.openapi.diff.impl.settings.DiffMergeSettingsAction;
import com.intellij.openapi.diff.impl.settings.MergeToolSettings;
import com.intellij.openapi.diff.impl.splitter.DiffDividerPaint;
import com.intellij.openapi.diff.impl.splitter.LineBlocks;
import com.intellij.openapi.diff.impl.util.*;
import com.intellij.openapi.editor.Document;
import com.intellij.openapi.editor.Editor;
import com.intellij.openapi.editor.colors.EditorColorsManager;
import com.intellij.openapi.editor.colors.EditorColorsScheme;
import com.intellij.openapi.editor.ex.EditorEx;
import com.intellij.openapi.editor.ex.EditorMarkupModel;
import com.intellij.openapi.editor.highlighter.EditorHighlighter;
import com.intellij.openapi.editor.highlighter.EditorHighlighterFactory;
import com.intellij.openapi.fileTypes.FileType;
import com.intellij.openapi.fileTypes.FileTypes;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.ui.DialogBuilder;
import com.intellij.openapi.ui.DialogWrapper;
import com.intellij.openapi.ui.LabeledComponent;
import com.intellij.openapi.ui.Messages;
import com.intellij.openapi.util.Disposer;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.ui.EditorNotificationPanel;
import com.intellij.util.containers.Convertor;
import gnu.trove.TIntHashSet;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import javax.swing.*;
import java.awt.*;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
public class MergePanel2 implements DiffViewer {
private static final Logger LOG = Logger.getInstance("#com.intellij.openapi.diff.impl.incrementalMerge.ui.MergePanel2");
private final DiffPanelOuterComponent myPanel;
private DiffRequest myData;
private MergeList myMergeList;
private boolean myDuringCreation = false;
private final SyncScrollSupport myScrollSupport = new SyncScrollSupport();
private final DiffDivider[] myDividers = {new DiffDivider(FragmentSide.SIDE2), new DiffDivider(FragmentSide.SIDE1)};
private boolean myScrollToFirstDiff = true;
private final LabeledComponent[] myEditorsPanels = new LabeledComponent[EDITORS_COUNT];
public static final int EDITORS_COUNT = 3;
private final DividersRepainter myDividersRepainter = new DividersRepainter();
private StatusUpdater myStatusUpdater;
private final DialogBuilder myBuilder;
private final MyDataProvider myProvider;
public MergePanel2(DialogBuilder builder, @NotNull Disposable parent) {
ArrayList<EditorPlace> editorPlaces = new ArrayList<EditorPlace>();
EditorPlace.EditorListener placeListener = new EditorPlace.EditorListener() {
public void onEditorCreated(EditorPlace place) {
if (myDuringCreation) return;
disposeMergeList();
myDuringCreation = true;
try {
tryInitView();
}
finally {
myDuringCreation = false;
}
}
public void onEditorReleased(Editor releasedEditor) {
LOG.assertTrue(!myDuringCreation);
disposeMergeList();
}
};
for (int i = 0; i < EDITORS_COUNT; i++) {
EditorPlace editorPlace = new EditorPlace(new DiffEditorState(i), indexToColumn(i), this);
Disposer.register(parent, editorPlace);
editorPlaces.add(editorPlace);
editorPlace.addListener(placeListener);
myEditorsPanels[i] = new LabeledComponent();
myEditorsPanels[i].setLabelLocation(BorderLayout.NORTH);
myEditorsPanels[i].setComponent(editorPlace);
}
FontSizeSynchronizer.attachTo(editorPlaces);
myPanel = new DiffPanelOuterComponent(TextDiffType.MERGE_TYPES, createToolbar());
myPanel.insertDiffComponent(new ThreePanels(myEditorsPanels, myDividers), new MyScrollingPanel());
myProvider = new MyDataProvider();
myPanel.setDataProvider(myProvider);
myBuilder = builder;
}
/**
* Convert legacy-style editor (or panel) number to the {@link MergePanelColumn}.
* @param i 0, 1 or 2
* @return Left, base or right, respectively.
*/
private static MergePanelColumn indexToColumn(int i) {
switch (i) {
case 0: return MergePanelColumn.LEFT;
case 1: return MergePanelColumn.BASE;
case 2: return MergePanelColumn.RIGHT;
default: throw new IllegalStateException("Incorrect value for a merge column: " + i);
}
}
@NotNull
private DiffRequest.ToolbarAddons createToolbar() {
return new DiffRequest.ToolbarAddons() {
public void customize(DiffToolbar toolbar) {
toolbar.addAction(PreviousDiffAction.find());
toolbar.addAction(NextDiffAction.find());
toolbar.addSeparator();
toolbar.addAction(new OpenPartialDiffAction(0, 1, AllIcons.Diff.LeftDiff));
toolbar.addAction(new OpenPartialDiffAction(1, 2, AllIcons.Diff.RightDiff));
toolbar.addAction(new OpenPartialDiffAction(0, 2, AllIcons.Diff.BranchDiff));
toolbar.addSeparator();
toolbar.addAction(new ApplyNonConflicts(myPanel));
toolbar.addSeparator();
toolbar.addAction(new ToggleAutoScrollAction());
Project project = myData.getProject();
if (project != null) {
toolbar.addSeparator();
toolbar.addAction(new DiffMergeSettingsAction(getEditors(), ServiceManager.getService(project, MergeToolSettings.class)));
}
}
};
}
@NotNull
private Collection<Editor> getEditors() {
Collection<Editor> editors = new ArrayList<Editor>(3);
for (EditorPlace place : getEditorPlaces()) {
editors.add(place.getEditor());
}
return editors;
}
@NotNull
private Collection<EditorPlace> getEditorPlaces() {
Collection<EditorPlace> editorPlaces = new ArrayList<EditorPlace>(3);
for (LabeledComponent editorsPanel : myEditorsPanels) {
editorPlaces.add((EditorPlace) editorsPanel.getComponent());
}
return editorPlaces;
}
public void setScrollToFirstDiff(final boolean scrollToFirstDiff) {
myScrollToFirstDiff = scrollToFirstDiff;
}
/**
* @deprecated Because it references by index.
*/
@Nullable
@Deprecated
public Editor getEditor(int index) {
return getEditorPlace(index).getEditor();
}
public FileType getContentType() {
return myData == null ? FileTypes.PLAIN_TEXT : getContentType(myData);
}
/**
* @deprecated Because it references by index.
*/
@Deprecated
public String getVersionTitle(int index) {
return myEditorsPanels[index].getRawText();
}
/**
* @deprecated Because it references by index.
*/
@Deprecated
public EditorPlace getEditorPlace(int index) {
return (EditorPlace)myEditorsPanels[index].getComponent();
}
private void createMergeList() {
if (myData == null) return;
DiffContent[] contents = myData.getContents();
for (int i = 0; i < EDITORS_COUNT; i++) {
EditorPlace editorPlace = getEditorPlace(i);
editorPlace.setDocument(contents[i].getDocument());
setHighlighterSettings(null, editorPlace);
}
tryInitView();
}
private void tryInitView() {
if (!hasAllEditors()) return;
if (myMergeList != null) return;
myMergeList = MergeList.create(myData);
myMergeList.addListener(myDividersRepainter);
myStatusUpdater = StatusUpdater.install(myMergeList, myPanel);
Editor left = getEditor(0);
Editor base = getEditor(1);
Editor right = getEditor(2);
setupHighlighterSettings(left, base, right);
myMergeList.setMarkups(left, base, right);
EditingSides[] sides = {getFirstEditingSide(), getSecondEditingSide()};
myScrollSupport.install(sides);
for (int i = 0; i < myDividers.length; i++) {
myDividers[i].listenEditors(sides[i]);
}
if (myScrollToFirstDiff) {
myPanel.requestScrollEditors();
}
if (myMergeList.getErrorMessage() != null) {
myPanel.insertTopComponent(new EditorNotificationPanel() {
{
myLabel.setText(myMergeList.getErrorMessage());
}
});
}
}
@NotNull
EditingSides getFirstEditingSide() {
return new MyEditingSides(FragmentSide.SIDE1);
}
@NotNull
EditingSides getSecondEditingSide() {
return new MyEditingSides(FragmentSide.SIDE2);
}
public void setAutoScrollEnabled(boolean enabled) {
myScrollSupport.setEnabled(enabled);
}
public boolean isAutoScrollEnabled() {
return myScrollSupport.isEnabled();
}
private void setupHighlighterSettings(Editor left, Editor base, Editor right) {
Editor[] editors = new Editor[]{left, base, right};
DiffContent[] contents = myData.getContents();
FileType[] types = DiffUtil.chooseContentTypes(contents);
VirtualFile fallbackFile = contents[1].getFile();
FileType fallbackType = contents[1].getContentType();
for (int i = 0; i < 3; i++) {
Editor editor = editors[i];
DiffContent content = contents[i];
EditorHighlighter highlighter =
createHighlighter(types[i], content.getFile(), fallbackFile, fallbackType, myData.getProject()).createHighlighter();
if (highlighter != null) {
((EditorEx)editor).setHighlighter(highlighter);
}
}
}
private static DiffHighlighterFactory createHighlighter(FileType contentType,
VirtualFile file,
VirtualFile otherFile,
FileType otherType,
Project project) {
if (file == null) file = otherFile;
if (contentType == null) contentType = otherType;
return new DiffHighlighterFactoryImpl(contentType, file, project);
}
public void setHighlighterSettings(@Nullable EditorColorsScheme settings) {
for (EditorPlace place : getEditorPlaces()) {
setHighlighterSettings(settings, place);
}
}
private void setHighlighterSettings(@Nullable EditorColorsScheme settings, @NotNull EditorPlace place) {
if (settings == null) {
settings = EditorColorsManager.getInstance().getGlobalScheme();
}
Editor editor = place.getEditor();
DiffEditorState editorState = place.getState();
if (editor != null) {
((EditorEx)editor).setHighlighter(EditorHighlighterFactory.getInstance().
createEditorHighlighter(editorState.getFileType(), settings, editorState.getProject()));
}
}
private static void initEditorSettings(@NotNull Editor editor) {
Project project = editor.getProject();
DiffMergeSettings settings = project == null ? null : ServiceManager.getService(project, MergeToolSettings.class);
for (DiffMergeEditorSetting property : DiffMergeEditorSetting.values()) {
property.apply(editor, settings == null ? property.getDefault() : settings.getPreference(property));
}
editor.getSettings().setLineMarkerAreaShown(true);
}
private void disposeMergeList() {
if (myMergeList == null) return;
if (myStatusUpdater != null) {
myStatusUpdater.dispose(myMergeList);
myStatusUpdater = null;
}
myMergeList.removeListener(myDividersRepainter);
myMergeList = null;
for (DiffDivider myDivider : myDividers) {
myDivider.stopListenEditors();
}
}
@Override
public boolean canShowRequest(DiffRequest request) {
return MergeTool.canShowRequest(request);
}
public void setDiffRequest(DiffRequest data) {
setTitle(data.getWindowTitle());
disposeMergeList();
for (int i = 0; i < EDITORS_COUNT; i++) {
getEditorPlace(i).setDocument(null);
}
LOG.assertTrue(!myDuringCreation);
myDuringCreation = true;
myProvider.putData(data.getGenericData());
try {
myData = data;
String[] titles = myData.getContentTitles();
for (int i = 0; i < myEditorsPanels.length; i++) {
LabeledComponent editorsPanel = myEditorsPanels[i];
editorsPanel.getLabel().setText(titles[i].isEmpty() ? " " : titles[i]);
}
createMergeList();
data.customizeToolbar(myPanel.resetToolbar());
myPanel.registerToolbarActions();
if ( data instanceof MergeRequestImpl && myBuilder != null){
Convertor<DialogWrapper, Boolean> preOkHook = new Convertor<DialogWrapper, Boolean>() {
@Override
public Boolean convert(DialogWrapper dialog) {
ChangeCounter counter = ChangeCounter.getOrCreate(myMergeList);
int changes = counter.getChangeCounter();
int conflicts = counter.getConflictCounter();
if (changes == 0 && conflicts == 0) return true;
return Messages.showYesNoDialog(dialog.getRootPane(),
DiffBundle.message("merge.dialog.apply.partially.resolved.changes.confirmation.message", changes, conflicts),
DiffBundle.message("apply.partially.resolved.merge.dialog.title"),
Messages.getQuestionIcon()) == Messages.YES;
}
};
((MergeRequestImpl)data).setActions(myBuilder, this, preOkHook);
}
}
finally {
myDuringCreation = false;
}
}
private void setTitle(String windowTitle) {
JDialog parent = getDialogWrapperParent();
if (parent == null) return;
parent.setTitle(windowTitle);
}
@Nullable
private JDialog getDialogWrapperParent() {
Component panel = myPanel;
while (panel != null){
if (panel instanceof JDialog) return (JDialog)panel;
panel = panel.getParent();
}
return null;
}
public JComponent getComponent() {
return myPanel;
}
@Nullable
public JComponent getPreferredFocusedComponent() {
return getEditorPlace(1).getContentComponent();
}
public int getContentsNumber() {
return 3;
}
@Override
public boolean acceptsType(DiffViewerType type) {
return DiffViewerType.merge.equals(type);
}
private boolean hasAllEditors() {
for (int i = 0; i < EDITORS_COUNT; i++) {
if (getEditor(i) == null) return false;
}
return true;
}
@Nullable
public MergeRequestImpl getMergeRequest() {
return (MergeRequestImpl)(myData instanceof MergeRequestImpl ? myData : null);
}
private class MyEditingSides implements EditingSides {
private final FragmentSide mySide;
private MyEditingSides(FragmentSide side) {
mySide = side;
}
@Nullable
public Editor getEditor(FragmentSide side) {
return MergePanel2.this.getEditor(mySide.getIndex() + side.getIndex());
}
public LineBlocks getLineBlocks() {
return myMergeList.getChanges(mySide).getLineBlocks();
}
}
private class MyScrollingPanel implements DiffPanelOuterComponent.ScrollingPanel {
public void scrollEditors() {
Editor centerEditor = getEditor(1);
JComponent centerComponent = centerEditor.getContentComponent();
if (centerComponent.isShowing()) {
centerComponent.requestFocus();
}
int[] toLeft = getPrimaryBeginnings(myDividers[0].getPaint());
int[] toRight = getPrimaryBeginnings(myDividers[1].getPaint());
int line;
if (toLeft.length > 0 && toRight.length > 0) {
line = Math.min(toLeft[0], toRight[0]);
}
else if (toLeft.length > 0) {
line = toLeft[0];
}
else if (toRight.length > 0) {
line = toRight[0];
}
else {
return;
}
SyncScrollSupport.scrollEditor(centerEditor, line);
}
private int[] getPrimaryBeginnings(DiffDividerPaint paint) {
FragmentSide primarySide = paint.getLeftSide();
LOG.assertTrue(getEditor(1) == paint.getSides().getEditor(primarySide));
return paint.getSides().getLineBlocks().getBeginnings(primarySide, true);
}
}
class DiffEditorState {
private final int myIndex;
private Document myDocument;
private DiffEditorState(int index) {
myIndex = index;
}
public void setDocument(Document document) {
myDocument = document;
}
public Document getDocument() {
return myDocument;
}
@Nullable
public EditorEx createEditor() {
Document document = getDocument();
if (document == null) return null;
Project project = myData.getProject();
EditorEx editor = DiffUtil.createEditor(document, project, myIndex != 1);
if (editor == null) return editor;
//FileType type = getFileType();
//editor.setHighlighter(HighlighterFactory.createHighlighter(project, type));
if (myIndex == 0) editor.setVerticalScrollbarOrientation(EditorEx.VERTICAL_SCROLLBAR_LEFT);
if (myIndex != 1) ((EditorMarkupModel)editor.getMarkupModel()).setErrorStripeVisible(true);
editor.getSettings().setFoldingOutlineShown(false);
editor.getFoldingModel().setFoldingEnabled(false);
editor.getSettings().setLineMarkerAreaShown(false);
editor.getSettings().setFoldingOutlineShown(false);
editor.getGutterComponentEx().setShowDefaultGutterPopup(false);
initEditorSettings(editor);
return editor;
}
public FileType getFileType() {
return getContentType();
}
@Nullable
public Project getProject() {
return myData == null ? null : myData.getProject();
}
}
private static FileType getContentType(DiffRequest diffData) {
FileType contentType = diffData.getContents()[1].getContentType();
if (contentType == null) contentType = FileTypes.PLAIN_TEXT;
return contentType;
}
private class MyDataProvider extends GenericDataProvider {
public Object getData(String dataId) {
if (FocusDiffSide.DATA_KEY.is(dataId)) {
int index = getFocusedEditorIndex();
if (index < 0) return null;
switch (index) {
case 0:
return new BranchFocusedSide(FragmentSide.SIDE1);
case 1:
return new MergeFocusedSide();
case 2:
return new BranchFocusedSide(FragmentSide.SIDE2);
}
}
else if (PlatformDataKeys.DIFF_VIEWER.is(dataId)) return MergePanel2.this;
return super.getData(dataId);
}
private int getFocusedEditorIndex() {
for (int i = 0; i < EDITORS_COUNT; i++) {
Editor editor = getEditor(i);
if (editor == null) continue;
if (editor.getContentComponent().isFocusOwner()) return i;
}
return -1;
}
}
private class BranchFocusedSide implements FocusDiffSide {
private final FragmentSide mySide;
private BranchFocusedSide(FragmentSide side) {
mySide = side;
}
@Nullable
public Editor getEditor() {
return MergePanel2.this.getEditor(mySide.getMergeIndex());
}
public int[] getFragmentStartingLines() {
return myMergeList.getChanges(mySide).getLineBlocks().getBeginnings(MergeList.BRANCH_SIDE);
}
}
private class MergeFocusedSide implements FocusDiffSide {
public Editor getEditor() {
return MergePanel2.this.getEditor(1);
}
public int[] getFragmentStartingLines() {
TIntHashSet beginnings = new TIntHashSet();
if (myMergeList != null) {
for (int i = 0; i < 2; i++) {
FragmentSide branchSide = FragmentSide.fromIndex(i);
beginnings.addAll(myMergeList.getChanges(branchSide).getLineBlocks().getBeginnings(MergeList.BASE_SIDE));
}
}
int[] result = beginnings.toArray();
Arrays.sort(result);
return result;
}
}
@Nullable
public static MergePanel2 fromDataContext(DataContext dataContext) {
DiffViewer diffComponent = PlatformDataKeys.DIFF_VIEWER.getData(dataContext);
return diffComponent instanceof MergePanel2 ? (MergePanel2)diffComponent : null;
}
public MergeList getMergeList() {
return myMergeList;
}
public void setColorScheme(EditorColorsScheme scheme) {
for (Editor editor : getEditors()) {
if (editor != null) {
((EditorEx)editor).setColorsScheme(scheme);
}
}
myPanel.setColorScheme(scheme);
}
private class DividersRepainter implements ChangeList.Listener {
@Override
public void onChangeApplied(ChangeList source) {
FragmentSide side = myMergeList.getSideOf(source);
myDividers[side.getIndex()].repaint();
}
public void onChangeRemoved(ChangeList source) {
FragmentSide side = myMergeList.getSideOf(source);
myDividers[side.getIndex()].repaint();
}
}
private static class StatusUpdater implements ChangeCounter.Listener {
private final DiffPanelOuterComponent myPanel;
private StatusUpdater(DiffPanelOuterComponent panel) {
myPanel = panel;
}
public void onCountersChanged(ChangeCounter counter) {
int changes = counter.getChangeCounter();
int conflicts = counter.getConflictCounter();
String text;
if (changes == 0 && conflicts == 0) {
text = DiffBundle.message("merge.dialog.all.conflicts.resolved.message.text");
}
else {
// The Bundle doesn't support such complex formats. Until that is fixed, constructing manually
//text = DiffBundle.message("merge.statistics.message", changes, conflicts);
text = makeCountersText(changes, conflicts);
}
myPanel.setStatusBarText(text);
}
@NotNull
private static String makeCountersText(int changes, int conflicts) {
return makeCounterWord(changes, "change") + ". " + makeCounterWord(conflicts, "conflict");
}
@NotNull
private static String makeCounterWord(int number, @NotNull String word) {
if (number == 0) {
return "No " + StringUtil.pluralize(word);
}
return number + " " + StringUtil.pluralize(word, number);
}
public void dispose(@NotNull MergeList mergeList) {
ChangeCounter.getOrCreate(mergeList).removeListener(this);
}
public static StatusUpdater install(MergeList mergeList, DiffPanelOuterComponent panel) {
ChangeCounter counters = ChangeCounter.getOrCreate(mergeList);
StatusUpdater updater = new StatusUpdater(panel);
counters.addListener(updater);
updater.onCountersChanged(counters);
return updater;
}
}
public static class AsComponent extends JPanel{
private final MergePanel2 myMergePanel;
public AsComponent(@NotNull Disposable parent) {
super(new BorderLayout());
myMergePanel = new MergePanel2(null, parent);
add(myMergePanel.getComponent(), BorderLayout.CENTER);
}
public MergePanel2 getMergePanel() {
return myMergePanel;
}
@SuppressWarnings({"UnusedDeclaration"})
public boolean isToolbarEnabled() {
return myMergePanel.myPanel.isToolbarEnabled();
}
public void setToolbarEnabled(boolean enabled) {
myMergePanel.myPanel.disableToolbar(!enabled);
}
}
}