/******************************************************************************* * 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 *******************************************************************************/ package org.eclipse.egit.ui.internal.commit; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.OutputStream; import java.util.ArrayList; import java.util.List; import org.eclipse.egit.core.internal.CompareCoreUtils; import org.eclipse.egit.ui.internal.UIText; import org.eclipse.egit.ui.internal.commit.DiffRegionFormatter.DiffRegion.Type; import org.eclipse.egit.ui.internal.history.FileDiff; import org.eclipse.jface.text.BadLocationException; import org.eclipse.jface.text.IDocument; import org.eclipse.jface.text.Region; import org.eclipse.jgit.annotations.NonNull; import org.eclipse.jgit.diff.DiffEntry; import org.eclipse.jgit.diff.DiffEntry.ChangeType; import org.eclipse.jgit.diff.DiffFormatter; import org.eclipse.jgit.diff.EditList; import org.eclipse.jgit.diff.RawText; import org.eclipse.jgit.lib.Repository; import org.eclipse.osgi.util.NLS; /** * Diff region formatter class that builds up a list of * {@link DiffRegion} instances as each {@link FileDiff} is being written to * an {@link IDocument}. */ public class DiffRegionFormatter extends DiffFormatter { /** * A text {@link Region} describing an interesting region in a unified diff. */ public static class DiffRegion extends Region { /** Constant {@value} indicating that no line number exists. */ public static final int NO_LINE = -1; /** * The type of a {@link DiffRegion}. */ public enum Type { /** Added line. */ ADD, /** Removed line. */ REMOVE, /** Hunk line. */ HUNK, /** Headline. */ HEADLINE, /** Header (after HEADLINE). */ HEADER, /** A context line in a hunk. */ CONTEXT, /** Other line. */ OTHER, } private final @NonNull Type type; private final int aLine; private final int bLine; /** * @param offset * @param length */ public DiffRegion(int offset, int length) { this(offset, length, NO_LINE, NO_LINE, Type.OTHER); } /** * @param offset * @param length * @param aLine * @param bLine * @param type */ public DiffRegion(int offset, int length, int aLine, int bLine, @NonNull Type type) { super(offset, length); this.type = type; this.aLine = aLine; this.bLine = bLine; } /** * @return the {@link Type} of the region */ public @NonNull Type getType() { return type; } /** * Returns the first logical line number of the region. * * @param side * to get the line number of * @return the line number; -1 indicates that the range has no line * number for the given side. */ public int getLine(@NonNull DiffEntry.Side side) { if (DiffEntry.Side.NEW.equals(side)) { return bLine; } return aLine; } @Override public boolean equals(Object object) { return super.equals(object); } @Override public int hashCode() { return super.hashCode(); } } /** * Region giving access to the {@link FileDiff} and its {@link Repository} * that generated the content. */ public static class FileDiffRegion extends Region { private final FileDiff diff; private final Repository repository; /** * Creates a new {@link FileDiffRegion}. * * @param repository * the {@link FileDiff} belongs to * @param fileDiff * the range belongs to * @param start * of the range * @param length * of the range */ public FileDiffRegion(Repository repository, FileDiff fileDiff, int start, int length) { super(start, length); this.diff = fileDiff; this.repository = repository; } /** * Retrieves the {@link FileDiff}. * * @return the {@link FileDiff} */ public FileDiff getDiff() { return diff; } /** * Retrieves the {@link Repository}. * * @return the {@link Repository} */ public Repository getRepository() { return repository; } @Override public boolean equals(Object object) { return super.equals(object); } @Override public int hashCode() { return super.hashCode(); } @Override public String toString() { return "[FileDiffRange " + (diff == null ? "null" : diff.getPath()) //$NON-NLS-1$ //$NON-NLS-2$ + ' ' + super.toString() + ']'; } } private static class DocumentOutputStream extends OutputStream { private String charset; private IDocument document; private int offset; private StringBuilder lineBuffer = new StringBuilder(); public DocumentOutputStream(IDocument document, int offset) { this.document = document; this.offset = offset; } private void write(String content) throws IOException { try { this.document.replace(this.offset, 0, content); this.offset += content.length(); } catch (BadLocationException e) { throw new IOException(e.getMessage()); } } @Override public void write(byte[] b, int off, int len) throws IOException { if (charset == null) lineBuffer.append(new String(b, off, len, "UTF-8")); //$NON-NLS-1$ else lineBuffer.append(new String(b, off, len, charset)); } @Override public void write(byte[] b) throws IOException { write(b, 0, b.length); } @Override public void write(int b) throws IOException { write(new byte[] { (byte) b }); } @Override public void flush() throws IOException { flushLine(); } protected void flushLine() throws IOException { if (lineBuffer.length() > 0) { write(lineBuffer.toString()); lineBuffer.setLength(0); } } } private DocumentOutputStream stream; private List<DiffRegion> regions = new ArrayList<>(); private List<FileDiffRegion> fileRegions = new ArrayList<>(); private final int maxLines; private int linesWritten; private int lastNewLine; private int[] maximumLineNumbers = new int[] { DiffRegion.NO_LINE, DiffRegion.NO_LINE }; /** * @param document * @param offset */ public DiffRegionFormatter(IDocument document, int offset) { this(document, offset, -1); } /** * @param document */ public DiffRegionFormatter(IDocument document) { this(document, document.getLength(), -1); } /** * @param document * @param offset * @param maxLines */ public DiffRegionFormatter(IDocument document, int offset, int maxLines) { super(new DocumentOutputStream(document, offset)); this.stream = (DocumentOutputStream) getOutputStream(); this.maxLines = maxLines; this.lastNewLine = DiffRegion.NO_LINE; } /** * Write diff * * @param repository * @param diff * @return this formatter * @throws IOException */ public DiffRegionFormatter write(Repository repository, FileDiff diff) throws IOException { this.stream.charset = CompareCoreUtils.getResourceEncoding(repository, diff.getPath()); int start = stream.offset; diff.outputDiff(null, repository, this, true); flush(); fileRegions .add(new FileDiffRegion(repository, diff, start, stream.offset - start)); return this; } /** * Get diff regions, sorted by offset * * @return non-null but possibly empty array */ public DiffRegion[] getRegions() { return this.regions.toArray(new DiffRegion[this.regions.size()]); } /** * Gets the file diff regions, sorted by offset. * * @return the regions; non-null but possibly empty */ public FileDiffRegion[] getFileRegions() { return this.fileRegions .toArray(new FileDiffRegion[this.fileRegions.size()]); } /** * Retrieves the maximum line numbers for hunk lines. * * @return an array with two elements, index 0 being the maximum old line * number and index 1 the maximum new line number */ public int[] getMaximumLineNumbers() { return maximumLineNumbers.clone(); } /** * Create and add a new {@link DiffRegion} without line number information, * coalescing it with the previous region,if any, if that has the same type * and the two regions are adjacent. * * @param type * the {@link Type} * @param start * start offset * @param end * end offset * @return added range */ protected DiffRegion addRegion(@NonNull Type type, int start, int end) { return addRegion(type, start, end, DiffRegion.NO_LINE, DiffRegion.NO_LINE); } /** * Create and add a new {@link DiffRegion}, coalescing it with the previous * region,if any, if that has the same type and the two regions are * adjacent. * * @param type * the {@link Type} * @param start * start offset * @param end * end offset * @param aLine * line number in the old version, or {@link DiffRegion#NO_LINE} * @param bLine * line number in the new version, or {@link DiffRegion#NO_LINE} * @return added range */ protected DiffRegion addRegion(@NonNull Type type, int start, int end, int aLine, int bLine) { maximumLineNumbers[0] = Math.max(aLine, maximumLineNumbers[0]); maximumLineNumbers[1] = Math.max(bLine, maximumLineNumbers[1]); if (bLine != DiffRegion.NO_LINE) { lastNewLine = bLine; } if (!regions.isEmpty()) { DiffRegion last = regions.get(regions.size() - 1); if (last.getType().equals(type) && start == last.getOffset() + last.getLength()) { regions.remove(regions.size() - 1); start = last.getOffset(); aLine = last.getLine(DiffEntry.Side.OLD); bLine = last.getLine(DiffEntry.Side.NEW); } } DiffRegion range = new DiffRegion(start, end - start, aLine, bLine, type); regions.add(range); return range; } @Override protected void writeHunkHeader(int aStartLine, int aEndLine, int bStartLine, int bEndLine) throws IOException { int start = stream.offset; if (!regions.isEmpty()) { DiffRegion last = regions.get(regions.size() - 1); int lastEnd = last.getOffset() + last.getLength(); if (last.getType().equals(Type.HEADLINE) && lastEnd < start) { addRegion(Type.HEADER, lastEnd, start); } } super.writeHunkHeader(aStartLine, aEndLine, bStartLine, bEndLine); stream.flushLine(); addRegion(Type.HUNK, start, stream.offset); lastNewLine = bStartLine - 1; } @Override protected void writeLine(char prefix, RawText text, int cur) throws IOException { if (maxLines > 0 && linesWritten > maxLines) { if (linesWritten == maxLines + 1) { int start = stream.offset; stream.flushLine(); stream.write( NLS.bind(UIText.DiffStyleRangeFormatter_diffTruncated, Integer.valueOf(maxLines))); stream.write("\n"); //$NON-NLS-1$ addRegion(Type.HEADLINE, start, stream.offset); linesWritten++; } return; } int start = stream.offset; super.writeLine(prefix, text, cur); stream.flushLine(); if (prefix == ' ') { addRegion(Type.CONTEXT, start, stream.offset, cur, ++lastNewLine); } else if (prefix == '+') { addRegion(Type.ADD, start, stream.offset, DiffRegion.NO_LINE, cur); } else { addRegion(Type.REMOVE, start, stream.offset, cur, DiffRegion.NO_LINE); } linesWritten++; } /** * @see org.eclipse.jgit.diff.DiffFormatter#formatGitDiffFirstHeaderLine(ByteArrayOutputStream * o, ChangeType type, String oldPath, String newPath) */ @Override protected void formatGitDiffFirstHeaderLine(ByteArrayOutputStream o, final ChangeType type, final String oldPath, final String newPath) throws IOException { stream.flushLine(); int offset = stream.offset; int start = o.size(); super.formatGitDiffFirstHeaderLine(o, type, oldPath, newPath); int end = o.size(); addRegion(Type.HEADLINE, offset + start, offset + end); } @Override public void format(final EditList edits, final RawText a, final RawText b) throws IOException { // Flush header before formatting of edits begin stream.flushLine(); super.format(edits, a, b); } }