/*******************************************************************************
* Copyright (C) 2016, Thomas Wolf <thomas.wolf@paranor.ch>
*
* 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
*******************************************************************************/
package org.eclipse.egit.ui.internal.commit;
import java.util.Arrays;
import java.util.regex.Pattern;
import org.eclipse.core.runtime.Assert;
import org.eclipse.egit.ui.internal.commit.DiffRegionFormatter.DiffRegion;
import org.eclipse.egit.ui.internal.commit.DiffRegionFormatter.FileDiffRegion;
import org.eclipse.egit.ui.internal.history.FileDiff;
import org.eclipse.jface.text.BadLocationException;
import org.eclipse.jface.text.Document;
import org.eclipse.jface.text.IDocument;
import org.eclipse.jface.text.IDocumentPartitioner;
import org.eclipse.jface.text.TextUtilities;
import org.eclipse.jface.text.rules.FastPartitioner;
import org.eclipse.jface.text.rules.IPartitionTokenScanner;
import org.eclipse.jface.text.rules.IToken;
import org.eclipse.jface.text.rules.Token;
import org.eclipse.jgit.annotations.NonNull;
import org.eclipse.jgit.diff.DiffEntry;
import org.eclipse.jgit.lib.Repository;
/**
* A {@link Document} specialized for displaying unified diffs generated by a
* {@link DiffRegionFormatter}. Intended usage is to create a DiffDocument, let
* a DiffRegionFormatter generate into it, and then
* {@link #connect(DiffRegionFormatter) connect()} the formatter. This will
* partition the document into regions for file headlines, hunks, and added or
* removed lines.
*/
public class DiffDocument extends Document {
static final String HEADLINE_CONTENT_TYPE = "_egit_diff_headline"; //$NON-NLS-1$
static final String HUNK_CONTENT_TYPE = "_egit_diff_hunk"; //$NON-NLS-1$
static final String ADDED_CONTENT_TYPE = "_egit_diff_added"; //$NON-NLS-1$
static final String REMOVED_CONTENT_TYPE = "_egit_diff_removed"; //$NON-NLS-1$
private DiffRegion[] regions;
private FileDiffRegion[] fileRegions;
private Pattern newPathPattern;
private Pattern oldPathPattern;
private Repository defaultRepository;
private FileDiff defaultFileDiff;
private int[] maximumLineNumbers;
/**
* Creates a new {@link DiffDocument}.
*/
public DiffDocument() {
super();
}
/**
* Creates a new {@link DiffDocument} with initial text.
*
* @param text
* to set on the document
*/
public DiffDocument(String text) {
super(text);
}
/**
* Sets up the document to use information from the given
* {@link DiffRegionFormatter} for partitioning the document into
* partitions for file headlines, hunk headers, and added or removed lines.
* It is assumed that the given formatter has been used to generate content
* into the document.
*
* @param formatter
* to obtain information from
*/
public void connect(DiffRegionFormatter formatter) {
regions = formatter.getRegions();
fileRegions = formatter.getFileRegions();
if ((fileRegions == null || fileRegions.length == 0)
&& defaultRepository != null && defaultFileDiff != null) {
fileRegions = new FileDiffRegion[] { new FileDiffRegion(
defaultRepository, defaultFileDiff, 0, getLength()) };
}
newPathPattern = Pattern.compile(
Pattern.quote(formatter.getNewPrefix()) + "\\S+"); //$NON-NLS-1$
oldPathPattern = Pattern.compile(
Pattern.quote(formatter.getOldPrefix()) + "\\S+"); //$NON-NLS-1$
maximumLineNumbers = formatter.getMaximumLineNumbers();
// Connect a new partitioner.
IDocumentPartitioner partitioner = new FastPartitioner(
new DiffPartitionTokenScanner(),
new String[] { IDocument.DEFAULT_CONTENT_TYPE,
HEADLINE_CONTENT_TYPE, HUNK_CONTENT_TYPE,
ADDED_CONTENT_TYPE, REMOVED_CONTENT_TYPE });
IDocumentPartitioner oldPartitioner = getDocumentPartitioner();
if (oldPartitioner != null) {
oldPartitioner.disconnect();
}
partitioner.connect(this);
setDocumentPartitioner(partitioner);
}
/**
* Provide default settings about the {@link Repository} and
* {@link FileDiff}, to be used in the absence of explicit information from
* a connected {@link DiffRegionFormatter}. Useful if the document is
* used for only individual edits from a file.
*
* @param repository
* to use if none set explicitly
* @param fileDiff
* to use if none set explicitly
*/
public void setDefault(Repository repository, FileDiff fileDiff) {
defaultRepository = repository;
defaultFileDiff = fileDiff;
}
DiffRegion[] getRegions() {
return regions;
}
FileDiffRegion[] getFileRegions() {
return fileRegions;
}
int getMaximumLineNumber(@NonNull DiffEntry.Side side) {
if (maximumLineNumbers == null) {
return DiffRegion.NO_LINE;
}
if (DiffEntry.Side.OLD.equals(side)) {
return maximumLineNumbers[0];
}
return maximumLineNumbers[1];
}
private int findRegionIndex(int offset) {
DiffRegion key = new DiffRegion(offset, 0);
return Arrays.binarySearch(regions, key, (a, b) -> {
if (!TextUtilities.overlaps(a, b)) {
return a.getOffset() - b.getOffset();
}
return 0;
});
}
DiffRegion findRegion(int offset) {
int i = findRegionIndex(offset);
return i >= 0 ? regions[i] : null;
}
FileDiffRegion findFileRegion(int offset) {
FileDiffRegion key = new FileDiffRegion(null, null, offset, 0);
int i = Arrays.binarySearch(fileRegions, key, (a, b) -> {
if (!TextUtilities.overlaps(a, b)) {
return a.getOffset() - b.getOffset();
}
return 0;
});
return i >= 0 ? fileRegions[i] : null;
}
int getLogicalLine(int physicalLine, @NonNull DiffEntry.Side side) {
int offset;
try {
offset = getLineOffset(physicalLine);
DiffRegion region = findRegion(offset);
if (region == null) {
return DiffRegion.NO_LINE;
}
int logicalStart = region.getLine(side);
if (logicalStart == DiffRegion.NO_LINE) {
return DiffRegion.NO_LINE;
}
int physicalStart = getLineOfOffset(region.getOffset());
return logicalStart + (physicalLine - physicalStart);
} catch (BadLocationException e) {
return DiffRegion.NO_LINE;
}
}
Pattern getPathPattern(@NonNull DiffEntry.Side side) {
switch (side) {
case OLD:
return oldPathPattern;
default:
return newPathPattern;
}
}
private class DiffPartitionTokenScanner implements IPartitionTokenScanner {
private final Token HEADLINE_TOKEN = new Token(HEADLINE_CONTENT_TYPE);
private final Token HUNK_TOKEN = new Token(HUNK_CONTENT_TYPE);
private final Token ADDED_TOKEN = new Token(ADDED_CONTENT_TYPE);
private final Token DELETED_TOKEN = new Token(REMOVED_CONTENT_TYPE);
private final Token OTHER_TOKEN = new Token(
IDocument.DEFAULT_CONTENT_TYPE);
private int currentOffset;
private int end;
private int tokenStart;
private int currIdx;
@Override
public void setRange(IDocument document, int offset, int length) {
Assert.isLegal(document == DiffDocument.this);
currentOffset = offset;
end = offset + length;
tokenStart = -1;
}
@Override
public IToken nextToken() {
if (tokenStart < 0) {
currIdx = findRegionIndex(currentOffset);
if (currIdx < 0) {
currIdx = -(currIdx + 1);
}
}
tokenStart = currentOffset;
if (currentOffset < end) {
if (currIdx >= DiffDocument.this.regions.length) {
currentOffset = end;
return OTHER_TOKEN;
}
if (currentOffset < DiffDocument.this.regions[currIdx]
.getOffset()) {
currentOffset = DiffDocument.this.regions[currIdx]
.getOffset();
return OTHER_TOKEN;
}
// We're in range[currIdx]. Typically at the beginning, but if
// called via setPartialRange, we may also be somewhere in the
// middle.
currentOffset += DiffDocument.this.regions[currIdx].getLength()
- (currentOffset
- DiffDocument.this.regions[currIdx]
.getOffset());
switch (DiffDocument.this.regions[currIdx++].getType()) {
case HEADLINE:
return HEADLINE_TOKEN;
case HUNK:
return HUNK_TOKEN;
case ADD:
return ADDED_TOKEN;
case REMOVE:
return DELETED_TOKEN;
default:
return OTHER_TOKEN;
}
}
return Token.EOF;
}
@Override
public int getTokenOffset() {
return tokenStart;
}
@Override
public int getTokenLength() {
return currentOffset - tokenStart;
}
@Override
public void setPartialRange(IDocument document, int offset, int length,
String contentType, int partitionOffset) {
setRange(document, offset, length);
}
}
}