org.eclipse.egit.ui.internal.commit.DiffViewer.java Source code

Java tutorial

Introduction

Here is the source code for org.eclipse.egit.ui.internal.commit.DiffViewer.java

Source

/*******************************************************************************
 *  Copyright (c) 2011, 2016 GitHub Inc. and others.
 *  All rights reserved. This program and the accompanying materials
 *  are made available under the terms of the Eclipse Public License v1.0
 *  which accompanies this distribution, and is available at
 *  http://www.eclipse.org/legal/epl-v10.html
 *
 *  Contributors:
 *    Kevin Sawicki (GitHub Inc.) - initial API and implementation
 *    Tobias Pfeifer (SAP AG) - customizable font and color for the first header line - https://bugs.eclipse.org/397723
 *    Thomas Wolf <thomas.wolf@paranor.ch> - add hyperlinks, and use JFace syntax coloring
 *******************************************************************************/
package org.eclipse.egit.ui.internal.commit;

import static org.eclipse.egit.ui.UIPreferences.THEME_DiffAddBackgroundColor;
import static org.eclipse.egit.ui.UIPreferences.THEME_DiffAddForegroundColor;
import static org.eclipse.egit.ui.UIPreferences.THEME_DiffHeadlineBackgroundColor;
import static org.eclipse.egit.ui.UIPreferences.THEME_DiffHeadlineFont;
import static org.eclipse.egit.ui.UIPreferences.THEME_DiffHeadlineForegroundColor;
import static org.eclipse.egit.ui.UIPreferences.THEME_DiffHunkBackgroundColor;
import static org.eclipse.egit.ui.UIPreferences.THEME_DiffHunkForegroundColor;
import static org.eclipse.egit.ui.UIPreferences.THEME_DiffRemoveBackgroundColor;
import static org.eclipse.egit.ui.UIPreferences.THEME_DiffRemoveForegroundColor;

import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.function.Supplier;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import org.eclipse.compare.ITypedElement;
import org.eclipse.core.resources.IFile;
import org.eclipse.core.runtime.Assert;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IPath;
import org.eclipse.core.runtime.NullProgressMonitor;
import org.eclipse.core.runtime.Path;
import org.eclipse.egit.core.internal.util.ResourceUtil;
import org.eclipse.egit.ui.Activator;
import org.eclipse.egit.ui.internal.CompareUtils;
import org.eclipse.egit.ui.internal.EgitUiEditorUtils;
import org.eclipse.egit.ui.internal.UIText;
import org.eclipse.egit.ui.internal.commit.DiffRegionFormatter.DiffRegion;
import org.eclipse.egit.ui.internal.commit.DiffRegionFormatter.FileDiffRegion;
import org.eclipse.egit.ui.internal.dialogs.HyperlinkSourceViewer;
import org.eclipse.egit.ui.internal.history.FileDiff;
import org.eclipse.egit.ui.internal.revision.GitCompareFileRevisionEditorInput;
import org.eclipse.jface.preference.IPreferenceStore;
import org.eclipse.jface.preference.PreferenceConverter;
import org.eclipse.jface.resource.ColorDescriptor;
import org.eclipse.jface.resource.ColorRegistry;
import org.eclipse.jface.resource.DeviceResourceManager;
import org.eclipse.jface.resource.FontRegistry;
import org.eclipse.jface.resource.JFaceResources;
import org.eclipse.jface.text.BadLocationException;
import org.eclipse.jface.text.Document;
import org.eclipse.jface.text.IDocument;
import org.eclipse.jface.text.IRegion;
import org.eclipse.jface.text.ITextViewer;
import org.eclipse.jface.text.ITypedRegion;
import org.eclipse.jface.text.Region;
import org.eclipse.jface.text.TextAttribute;
import org.eclipse.jface.text.TextUtilities;
import org.eclipse.jface.text.hyperlink.AbstractHyperlinkDetector;
import org.eclipse.jface.text.hyperlink.IHyperlink;
import org.eclipse.jface.text.hyperlink.IHyperlinkDetector;
import org.eclipse.jface.text.hyperlink.IHyperlinkDetectorExtension2;
import org.eclipse.jface.text.presentation.IPresentationReconciler;
import org.eclipse.jface.text.presentation.PresentationReconciler;
import org.eclipse.jface.text.rules.DefaultDamagerRepairer;
import org.eclipse.jface.text.rules.IToken;
import org.eclipse.jface.text.rules.ITokenScanner;
import org.eclipse.jface.text.rules.Token;
import org.eclipse.jface.text.source.IOverviewRuler;
import org.eclipse.jface.text.source.ISourceViewer;
import org.eclipse.jface.text.source.IVerticalRuler;
import org.eclipse.jface.text.source.SourceViewerConfiguration;
import org.eclipse.jface.util.IPropertyChangeListener;
import org.eclipse.jface.util.PropertyChangeEvent;
import org.eclipse.jgit.annotations.NonNull;
import org.eclipse.jgit.diff.DiffEntry;
import org.eclipse.jgit.diff.DiffEntry.ChangeType;
import org.eclipse.jgit.lib.ObjectId;
import org.eclipse.jgit.lib.Repository;
import org.eclipse.jgit.revwalk.RevCommit;
import org.eclipse.osgi.util.NLS;
import org.eclipse.swt.SWT;
import org.eclipse.swt.custom.StyledText;
import org.eclipse.swt.events.DisposeEvent;
import org.eclipse.swt.events.DisposeListener;
import org.eclipse.swt.graphics.Color;
import org.eclipse.swt.graphics.Rectangle;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.swt.widgets.Layout;
import org.eclipse.team.core.history.IFileRevision;
import org.eclipse.ui.IEditorPart;
import org.eclipse.ui.IWorkbenchPage;
import org.eclipse.ui.IWorkbenchWindow;
import org.eclipse.ui.PlatformUI;
import org.eclipse.ui.editors.text.EditorsUI;
import org.eclipse.ui.texteditor.AbstractTextEditor;
import org.eclipse.ui.themes.IThemeManager;

/**
 * Source viewer to display one or more file differences using standard editor
 * colors and fonts preferences. Should be used together with a
 * {@link DiffDocument} to get proper coloring and hyperlink support.
 */
public class DiffViewer extends HyperlinkSourceViewer {

    private final DeviceResourceManager colors = new DeviceResourceManager(PlatformUI.getWorkbench().getDisplay());

    private final Map<String, IToken> tokens = new HashMap<>();

    private final Map<String, Color> backgroundColors = new HashMap<>();

    private IPropertyChangeListener themeListener = new IPropertyChangeListener() {

        @Override
        public void propertyChange(PropertyChangeEvent event) {
            String property = event.getProperty();
            if (IThemeManager.CHANGE_CURRENT_THEME.equals(property) || THEME_DiffAddBackgroundColor.equals(property)
                    || THEME_DiffAddForegroundColor.equals(property)
                    || THEME_DiffHunkBackgroundColor.equals(property)
                    || THEME_DiffHunkForegroundColor.equals(property)
                    || THEME_DiffHeadlineBackgroundColor.equals(property)
                    || THEME_DiffHeadlineForegroundColor.equals(property) || THEME_DiffHeadlineFont.equals(property)
                    || THEME_DiffRemoveBackgroundColor.equals(property)
                    || THEME_DiffRemoveForegroundColor.equals(property)) {
                refreshDiffStyles();
                invalidateTextPresentation();
            }
        }
    };

    private IPropertyChangeListener editorPrefListener = new IPropertyChangeListener() {

        @Override
        public void propertyChange(PropertyChangeEvent event) {
            styleViewer();
        }
    };

    /**
     * A configuration to use with a {@link DiffViewer}, setting up the syntax
     * coloring for a diff and adding the {@link IHyperlinkDetector} for the
     * links.
     */
    public static class Configuration extends HyperlinkSourceViewer.Configuration {

        /**
         * Creates a new {@link Configuration} connected to the given
         * {@link IPreferenceStore}.
         *
         * @param preferenceStore
         *            to connect to
         */
        public Configuration(IPreferenceStore preferenceStore) {
            super(preferenceStore);
        }

        @Override
        public int getHyperlinkStateMask(ISourceViewer sourceViewer) {
            return SWT.NONE;
        }

        @Override
        protected IHyperlinkDetector[] internalGetHyperlinkDetectors(ISourceViewer sourceViewer) {
            Assert.isTrue(sourceViewer instanceof DiffViewer);
            IHyperlinkDetector[] result = { new HyperlinkDetector() };
            return result;
        }

        @Override
        public String[] getConfiguredContentTypes(ISourceViewer sourceViewer) {
            Assert.isTrue(sourceViewer instanceof DiffViewer);
            DiffViewer viewer = (DiffViewer) sourceViewer;
            return viewer.tokens.keySet().toArray(new String[viewer.tokens.size()]);
        }

        @Override
        public IPresentationReconciler getPresentationReconciler(ISourceViewer sourceViewer) {
            Assert.isTrue(sourceViewer instanceof DiffViewer);
            DiffViewer viewer = (DiffViewer) sourceViewer;
            PresentationReconciler reconciler = new PresentationReconciler();
            reconciler.setDocumentPartitioning(getConfiguredDocumentPartitioning(viewer));
            for (String contentType : viewer.tokens.keySet()) {
                DefaultDamagerRepairer damagerRepairer = new DefaultDamagerRepairer(
                        new SingleTokenScanner(() -> viewer.tokens.get(contentType)));
                reconciler.setDamager(damagerRepairer, contentType);
                reconciler.setRepairer(damagerRepairer, contentType);
            }
            return reconciler;
        }

    }

    /**
     * Creates a new {@link DiffViewer}.
     *
     * @param parent
     *            to contain the viewer
     * @param ruler
     *            for the viewer (left side)
     * @param styles
     *            for the viewer
     */
    public DiffViewer(Composite parent, IVerticalRuler ruler, int styles) {
        this(parent, ruler, null, false, styles);
    }

    /**
     * Creates a new {@link DiffViewer}.
     *
     * @param parent
     *            to contain the viewer
     * @param ruler
     *            for the viewer (left side)
     * @param overviewRuler
     *            ruler for overview annotations
     * @param showsAnnotationOverview
     *            whether to show overview annotations
     * @param styles
     *            for the viewer
     */
    public DiffViewer(Composite parent, IVerticalRuler ruler, IOverviewRuler overviewRuler,
            boolean showsAnnotationOverview, int styles) {
        super(parent, ruler, overviewRuler, showsAnnotationOverview, styles);
        getTextWidget().setAlwaysShowScrollBars(false);
        setEditable(false);
        setDocument(new Document());
        initListeners();
        getControl().addDisposeListener(new DisposeListener() {

            @Override
            public void widgetDisposed(DisposeEvent e) {
                EditorsUI.getPreferenceStore().removePropertyChangeListener(editorPrefListener);
                PlatformUI.getWorkbench().getThemeManager().removePropertyChangeListener(themeListener);
                colors.dispose();
            }
        });
        refreshDiffStyles();
        styleViewer();
    }

    @Override
    public void configure(SourceViewerConfiguration config) {
        Assert.isTrue(config instanceof Configuration);
        super.configure(config);
    }

    @Override
    public void refresh() {
        // Don't lose the annotation model, if there is one!
        // (The super implementation ignores it.)
        setDocument(getDocument(), getAnnotationModel());
    }

    @Override
    protected Layout createLayout() {
        return new FixedRulerLayout(GAP_SIZE_1);
    }

    private class FixedRulerLayout extends RulerLayout {

        public FixedRulerLayout(int gap) {
            super(gap);
        }

        @Override
        protected void layout(Composite composite, boolean flushCache) {
            Rectangle bounds = composite.getBounds();
            if (bounds.width == 0 || bounds.height == 0) {
                // The overview ruler is laid out wrongly in the DiffEditorPage:
                // it ends up with a negative y-coordinate. This seems to be
                // caused by layout attempts while the page is not visible,
                // which cache that bogus negative offset in RulerLayout, which
                // will re-use it even when the viewer is laid out again when
                // the page is visible. So don't layout if the containing
                // composite has no extent.
                return;
            }
            super.layout(composite, flushCache);
        }
    }

    private void refreshDiffStyles() {
        ColorRegistry col = PlatformUI.getWorkbench().getThemeManager().getCurrentTheme().getColorRegistry();
        FontRegistry reg = PlatformUI.getWorkbench().getThemeManager().getCurrentTheme().getFontRegistry();
        // We do the foreground via syntax coloring and the background via a
        // line background listener. If we did the background also via the
        // TextAttributes, this would take precedence over the line background
        // resulting in strange display if the current line is highlighted:
        // that highlighting would appear only beyond the end of the actual
        // text content (i.e., beyond the end-of-line), while actual text
        // would still get the background from the attribute.
        tokens.put(IDocument.DEFAULT_CONTENT_TYPE, new Token(null));
        tokens.put(DiffDocument.HEADLINE_CONTENT_TYPE,
                new Token(new TextAttribute(col.get(THEME_DiffHeadlineForegroundColor), null, SWT.NORMAL,
                        reg.get(THEME_DiffHeadlineFont))));
        tokens.put(DiffDocument.HUNK_CONTENT_TYPE,
                new Token(new TextAttribute(col.get(THEME_DiffHunkForegroundColor))));
        tokens.put(DiffDocument.ADDED_CONTENT_TYPE,
                new Token(new TextAttribute(col.get(THEME_DiffAddForegroundColor))));
        tokens.put(DiffDocument.REMOVED_CONTENT_TYPE,
                new Token(new TextAttribute(col.get(THEME_DiffRemoveForegroundColor))));
        backgroundColors.put(DiffDocument.HEADLINE_CONTENT_TYPE, col.get(THEME_DiffHeadlineBackgroundColor));
        backgroundColors.put(DiffDocument.HUNK_CONTENT_TYPE, col.get(THEME_DiffHunkBackgroundColor));
        backgroundColors.put(DiffDocument.ADDED_CONTENT_TYPE, col.get(THEME_DiffAddBackgroundColor));
        backgroundColors.put(DiffDocument.REMOVED_CONTENT_TYPE, col.get(THEME_DiffRemoveBackgroundColor));
    }

    private void initListeners() {
        PlatformUI.getWorkbench().getThemeManager().addPropertyChangeListener(this.themeListener);
        EditorsUI.getPreferenceStore().addPropertyChangeListener(this.editorPrefListener);
        getTextWidget().addLineBackgroundListener((event) -> {
            IDocument document = getDocument();
            if (document instanceof DiffDocument) {
                try {
                    // We are in SWT land here: we get widget offsets.
                    int modelOffset = widgetOffset2ModelOffset(event.lineOffset);
                    ITypedRegion partition = ((DiffDocument) document).getPartition(modelOffset);
                    if (partition != null) {
                        Color color = backgroundColors.get(partition.getType());
                        if (color != null) {
                            event.lineBackground = color;
                        }
                    }
                } catch (BadLocationException e) {
                    // Ignore
                }
            }
        });
    }

    private ColorDescriptor createEditorColorDescriptor(String key) {
        return ColorDescriptor.createFrom(PreferenceConverter.getColor(EditorsUI.getPreferenceStore(), key));
    }

    private Color getEditorColor(String key) {
        return (Color) colors.get(createEditorColorDescriptor(key));
    }

    private void styleViewer() {
        IPreferenceStore store = EditorsUI.getPreferenceStore();
        Color foreground = null;
        if (!store.getBoolean(AbstractTextEditor.PREFERENCE_COLOR_FOREGROUND_SYSTEM_DEFAULT))
            foreground = getEditorColor(AbstractTextEditor.PREFERENCE_COLOR_FOREGROUND);

        Color background = null;
        if (!store.getBoolean(AbstractTextEditor.PREFERENCE_COLOR_BACKGROUND_SYSTEM_DEFAULT))
            background = getEditorColor(AbstractTextEditor.PREFERENCE_COLOR_BACKGROUND);

        Color selectionForeground = null;
        if (!store.getBoolean(AbstractTextEditor.PREFERENCE_COLOR_SELECTION_FOREGROUND_SYSTEM_DEFAULT))
            selectionForeground = getEditorColor(AbstractTextEditor.PREFERENCE_COLOR_SELECTION_FOREGROUND);

        Color selectionBackground = null;
        if (!store.getBoolean(AbstractTextEditor.PREFERENCE_COLOR_SELECTION_BACKGROUND_SYSTEM_DEFAULT))
            selectionBackground = getEditorColor(AbstractTextEditor.PREFERENCE_COLOR_SELECTION_BACKGROUND);

        StyledText text = getTextWidget();
        text.setForeground(foreground);
        text.setBackground(background);
        text.setSelectionForeground(selectionForeground);
        text.setSelectionBackground(selectionBackground);
        text.setFont(JFaceResources.getFont(JFaceResources.TEXT_FONT));
    }

    private static class SingleTokenScanner implements ITokenScanner {

        private final Supplier<IToken> token;

        private int currentOffset;

        private int end;

        private int tokenStart;

        public SingleTokenScanner(Supplier<IToken> supplier) {
            this.token = supplier;
        }

        @Override
        public void setRange(IDocument document, int offset, int length) {
            currentOffset = offset;
            end = offset + length;
            tokenStart = -1;
        }

        @Override
        public IToken nextToken() {
            tokenStart = currentOffset;
            if (currentOffset < end) {
                currentOffset = end;
                return token.get();
            }
            return Token.EOF;
        }

        @Override
        public int getTokenOffset() {
            return tokenStart;
        }

        @Override
        public int getTokenLength() {
            return currentOffset - tokenStart;
        }

    }

    private static class HyperlinkDetector extends AbstractHyperlinkDetector
            implements IHyperlinkDetectorExtension2 {

        private final Pattern HUNK_LINE_PATTERN = Pattern.compile("@@ ([-+]?(\\d+),\\d+) ([-+]?(\\d+),\\d+) @@"); //$NON-NLS-1$

        @Override
        public IHyperlink[] detectHyperlinks(ITextViewer textViewer, IRegion region,
                boolean canShowMultipleHyperlinks) {
            IDocument document = textViewer.getDocument();
            if (!(document instanceof DiffDocument) || document.getLength() == 0) {
                return null;
            }
            DiffDocument diffDocument = (DiffDocument) document;
            DiffRegion[] regions = diffDocument.getRegions();
            FileDiffRegion[] fileRegions = diffDocument.getFileRegions();
            if (regions == null || regions.length == 0 || fileRegions == null || fileRegions.length == 0) {
                return null;
            }
            int start = region.getOffset();
            int end = region.getOffset() + region.getLength();
            DiffRegion key = new DiffRegion(start, 0);
            int i = Arrays.binarySearch(regions, key, (a, b) -> {
                if (a.getOffset() > b.getOffset() + b.getLength()) {
                    return 1;
                }
                if (a.getOffset() + a.getLength() < b.getOffset()) {
                    return -1;
                }
                return 0;
            });
            List<IHyperlink> links = new ArrayList<>();
            FileDiffRegion fileRange = null;
            for (; i >= 0 && i < regions.length; i++) {
                DiffRegion range = regions[i];
                if (range.getOffset() >= end) {
                    break;
                }
                if (range.getOffset() + range.getLength() <= start) {
                    continue;
                }
                // Range overlaps region
                switch (range.getType()) {
                case HEADLINE:
                    fileRange = findFileRange(diffDocument, fileRange, range.getOffset());
                    if (fileRange != null) {
                        DiffEntry.ChangeType change = fileRange.getDiff().getChange();
                        switch (change) {
                        case ADD:
                        case DELETE:
                            break;
                        default:
                            if (getString(document, range.getOffset(), range.getLength()).startsWith("diff")) { //$NON-NLS-1$
                                // "diff" is at the beginning
                                IRegion linkRegion = new Region(range.getOffset(), 4);
                                if (TextUtilities.overlaps(region, linkRegion)) {
                                    links.add(new CompareLink(linkRegion, fileRange, -1));
                                }
                            }
                            break;
                        }
                    }
                    break;
                case HEADER:
                    fileRange = findFileRange(diffDocument, fileRange, range.getOffset());
                    if (fileRange != null) {
                        String line = getString(document, range.getOffset(), range.getLength());
                        createHeaderLinks((DiffDocument) document, region, fileRange, range, line,
                                DiffEntry.Side.OLD, links);
                        createHeaderLinks((DiffDocument) document, region, fileRange, range, line,
                                DiffEntry.Side.NEW, links);
                    }
                    break;
                case HUNK:
                    fileRange = findFileRange(diffDocument, fileRange, range.getOffset());
                    if (fileRange != null) {
                        String line = getString(document, range.getOffset(), range.getLength());
                        Matcher m = HUNK_LINE_PATTERN.matcher(line);
                        if (m.find()) {
                            int lineOffset = getContextLines(document, range,
                                    i + 1 < regions.length ? regions[i + 1] : null);
                            createHunkLinks(region, fileRange, range, m, lineOffset, links);
                        }
                    }
                    break;
                default:
                    break;
                }
            }
            if (links.isEmpty()) {
                return null;
            }
            return links.toArray(new IHyperlink[links.size()]);
        }

        private String getString(IDocument document, int offset, int length) {
            try {
                return document.get(offset, length);
            } catch (BadLocationException e) {
                return ""; //$NON-NLS-1$
            }
        }

        private int getContextLines(IDocument document, DiffRegion hunk, DiffRegion next) {
            if (next != null) {
                try {
                    switch (next.getType()) {
                    case CONTEXT:
                        int nofLines = document.getNumberOfLines(next.getOffset(), next.getLength());
                        return nofLines - 1;
                    case ADD:
                    case REMOVE:
                        int hunkLine = document.getLineOfOffset(hunk.getOffset());
                        int diffLine = document.getLineOfOffset(next.getOffset());
                        return diffLine - hunkLine - 1;
                    default:
                        break;
                    }
                } catch (BadLocationException e) {
                    // Ignore
                }
            }
            return 0;
        }

        private FileDiffRegion findFileRange(DiffDocument document, FileDiffRegion candidate, int offset) {
            if (candidate != null && TextUtilities.overlaps(candidate, new Region(offset, 0))) {
                return candidate;
            }
            return document.findFileRegion(offset);
        }

        private void createHeaderLinks(DiffDocument document, IRegion region, FileDiffRegion fileRange,
                DiffRegion range, String line, @NonNull DiffEntry.Side side, List<IHyperlink> links) {
            Pattern p = document.getPathPattern(side);
            if (p == null) {
                return;
            }
            DiffEntry.ChangeType change = fileRange.getDiff().getChange();
            switch (side) {
            case OLD:
                if (change == DiffEntry.ChangeType.ADD) {
                    return;
                }
                break;
            default:
                if (change == DiffEntry.ChangeType.DELETE) {
                    return;
                }
                break;

            }
            Matcher m = p.matcher(line);
            if (m.find()) {
                IRegion linkRegion = new Region(range.getOffset() + m.start(), m.end() - m.start());
                if (TextUtilities.overlaps(region, linkRegion)) {
                    if (side == DiffEntry.Side.NEW) {
                        File file = new Path(fileRange.getRepository().getWorkTree().getAbsolutePath())
                                .append(fileRange.getDiff().getNewPath()).toFile();
                        if (file.exists()) {
                            links.add(new FileLink(linkRegion, file, -1));
                        }
                    }
                    links.add(new OpenLink(linkRegion, fileRange, side, -1));
                }
            }
        }

        private void createHunkLinks(IRegion region, FileDiffRegion fileRange, DiffRegion range, Matcher m,
                int lineOffset, List<IHyperlink> links) {
            DiffEntry.ChangeType change = fileRange.getDiff().getChange();
            if (change != DiffEntry.ChangeType.ADD) {
                IRegion linkRegion = new Region(range.getOffset() + m.start(1), m.end(1) - m.start(1));
                if (TextUtilities.overlaps(linkRegion, region)) {
                    int lineNo = Integer.parseInt(m.group(2)) - 1 + lineOffset;
                    if (change != DiffEntry.ChangeType.DELETE) {
                        links.add(new CompareLink(linkRegion, fileRange, lineNo));
                    }
                    links.add(new OpenLink(linkRegion, fileRange, DiffEntry.Side.OLD, lineNo));
                }
            }
            if (change != DiffEntry.ChangeType.DELETE) {
                IRegion linkRegion = new Region(range.getOffset() + m.start(3), m.end(3) - m.start(3));
                if (TextUtilities.overlaps(linkRegion, region)) {
                    int lineNo = Integer.parseInt(m.group(4)) - 1 + lineOffset;
                    if (change != DiffEntry.ChangeType.ADD) {
                        links.add(new CompareLink(linkRegion, fileRange, lineNo));
                    }
                    File file = new Path(fileRange.getRepository().getWorkTree().getAbsolutePath())
                            .append(fileRange.getDiff().getNewPath()).toFile();
                    if (file.exists()) {
                        links.add(new FileLink(linkRegion, file, lineNo));
                    }
                    links.add(new OpenLink(linkRegion, fileRange, DiffEntry.Side.NEW, lineNo));
                }
            }
        }

        @Override
        public int getStateMask() {
            return -1;
        }
    }

    private static abstract class RevealLink implements IHyperlink {

        private final IRegion region;

        protected final int lineNo;

        protected RevealLink(IRegion region, int lineNo) {
            this.region = region;
            this.lineNo = lineNo;
        }

        @Override
        public IRegion getHyperlinkRegion() {
            return region;
        }

        @Override
        public String getTypeLabel() {
            return null;
        }

    }

    private static class FileLink extends RevealLink {

        private final File file;

        public FileLink(IRegion region, File file, int lineNo) {
            super(region, lineNo);
            this.file = file;
        }

        @Override
        public String getHyperlinkText() {
            return UIText.DiffViewer_OpenWorkingTreeLinkLabel;
        }

        @Override
        public void open() {
            openFileInEditor(file, lineNo);
        }

    }

    private static class CompareLink extends RevealLink {

        protected final Repository repository;

        protected final FileDiff fileDiff;

        public CompareLink(IRegion region, FileDiffRegion fileRange, int lineNo) {
            super(region, lineNo);
            this.repository = fileRange.getRepository();
            this.fileDiff = fileRange.getDiff();
        }

        @Override
        public String getHyperlinkText() {
            return UIText.DiffViewer_OpenComparisonLinkLabel;
        }

        @Override
        public void open() {
            // No way to selectAndReveal a line or a diff node in a
            // CompareEditor?
            showTwoWayFileDiff(repository, fileDiff);
        }

    }

    private static class OpenLink extends CompareLink {

        private final DiffEntry.Side side;

        public OpenLink(IRegion region, FileDiffRegion fileRange, DiffEntry.Side side, int lineNo) {
            super(region, fileRange, lineNo);
            this.side = side;
        }

        @Override
        public String getHyperlinkText() {
            switch (side) {
            case OLD:
                return UIText.DiffViewer_OpenPreviousLinkLabel;
            default:
                return UIText.DiffViewer_OpenInEditorLinkLabel;
            }
        }

        @Override
        public void open() {
            openInEditor(repository, fileDiff, side, lineNo);
        }

    }

    /**
     * Opens the file, if it exists, in an editor.
     *
     * @param file
     *            to open
     * @param lineNoToReveal
     *            if >= 0, select and reveals the given line
     */
    public static void openFileInEditor(File file, int lineNoToReveal) {
        if (!file.exists()) {
            Activator.showError(NLS.bind(UIText.DiffViewer_FileDoesNotExist, file.getPath()), null);
            return;
        }
        IWorkbenchPage page = PlatformUI.getWorkbench().getActiveWorkbenchWindow().getActivePage();
        IEditorPart editor = EgitUiEditorUtils.openEditor(file, page);
        EgitUiEditorUtils.revealLine(editor, lineNoToReveal);
    }

    /**
     * Opens either the new or the old version of a {@link FileDiff} in an
     * editor.
     *
     * @param repository
     *            the {@link FileDiff} belongs to
     * @param d
     *            the {@link FileDiff}
     * @param side
     *            to show
     * @param lineNoToReveal
     *            if >= 0, select and reveals the given line
     */
    public static void openInEditor(Repository repository, FileDiff d, DiffEntry.Side side, int lineNoToReveal) {
        ObjectId[] blobs = d.getBlobs();
        switch (side) {
        case OLD:
            openInEditor(repository, d.getOldPath(), d.getCommit().getParent(0), blobs[0], lineNoToReveal);
            break;
        default:
            openInEditor(repository, d.getNewPath(), d.getCommit(), blobs[blobs.length - 1], lineNoToReveal);
            break;
        }
    }

    private static void openInEditor(Repository repository, String path, RevCommit commit, ObjectId blob,
            int reveal) {
        try {
            IFileRevision rev = CompareUtils.getFileRevision(path, commit, repository, blob);
            if (rev == null) {
                String message = NLS.bind(UIText.DiffViewer_notContainedInCommit, path, commit.getName());
                Activator.showError(message, null);
                return;
            }
            IWorkbenchWindow window = PlatformUI.getWorkbench().getActiveWorkbenchWindow();
            IWorkbenchPage page = window.getActivePage();
            IEditorPart editor = EgitUiEditorUtils.openEditor(page, rev, new NullProgressMonitor());
            EgitUiEditorUtils.revealLine(editor, reveal);
        } catch (IOException | CoreException e) {
            Activator.handleError(UIText.GitHistoryPage_openFailed, e, true);
        }
    }

    /**
     * Shows a two-way diff between the old and new versions of a
     * {@link FileDiff} in a compare editor.
     *
     * @param repository
     *            the {@link FileDiff} belongs to
     * @param d
     *            the {@link FileDiff} to show
     */
    public static void showTwoWayFileDiff(Repository repository, FileDiff d) {
        String np = d.getNewPath();
        String op = d.getOldPath();
        RevCommit c = d.getCommit();
        ObjectId[] blobs = d.getBlobs();

        // extract commits
        final RevCommit oldCommit;
        final ObjectId oldObjectId;
        if (!d.getChange().equals(ChangeType.ADD)) {
            oldCommit = c.getParent(0);
            oldObjectId = blobs[0];
        } else {
            // Initial import
            oldCommit = null;
            oldObjectId = null;
        }

        final RevCommit newCommit;
        final ObjectId newObjectId;
        if (d.getChange().equals(ChangeType.DELETE)) {
            newCommit = null;
            newObjectId = null;
        } else {
            newCommit = c;
            newObjectId = blobs[blobs.length - 1];
        }
        IWorkbenchWindow window = PlatformUI.getWorkbench().getActiveWorkbenchWindow();
        IWorkbenchPage page = window.getActivePage();
        if (oldCommit != null && newCommit != null && repository != null) {
            IFile file = np != null ? ResourceUtil.getFileForLocation(repository, np, false) : null;
            try {
                if (file != null) {
                    CompareUtils.compare(file, repository, np, op, newCommit.getName(), oldCommit.getName(), false,
                            page);
                } else {
                    IPath location = new Path(repository.getWorkTree().getAbsolutePath()).append(np);
                    CompareUtils.compare(location, repository, newCommit.getName(), oldCommit.getName(), false,
                            page);
                }
            } catch (IOException e) {
                Activator.handleError(UIText.GitHistoryPage_openFailed, e, true);
            }
            return;
        }

        // still happens on initial commits
        final ITypedElement oldSide = createTypedElement(repository, op, oldCommit, oldObjectId);
        final ITypedElement newSide = createTypedElement(repository, np, newCommit, newObjectId);
        CompareUtils.openInCompare(page, new GitCompareFileRevisionEditorInput(newSide, oldSide, null));
    }

    private static ITypedElement createTypedElement(Repository repository, String path, final RevCommit commit,
            final ObjectId objectId) {
        if (null != commit) {
            return CompareUtils.getFileRevisionTypedElement(path, commit, repository, objectId);
        } else {
            return new GitCompareFileRevisionEditorInput.EmptyTypedElement(""); //$NON-NLS-1$
        }
    }

}