package com.jbirdvegas.mgerrit.views;
/*
* Copyright (C) 2014 Android Open Kang Project (AOKP)
* Author: Evan Conway (P4R4N01D), 2014
*
* 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.
*/
import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Color;
import android.text.SpannableString;
import android.text.Spanned;
import android.text.style.BackgroundColorSpan;
import android.text.style.CharacterStyle;
import android.text.style.ForegroundColorSpan;
import android.util.AttributeSet;
import android.util.Log;
import android.widget.TextView;
import com.jbirdvegas.mgerrit.R;
import org.jetbrains.annotations.Contract;
import org.jetbrains.annotations.NotNull;
import java.util.LinkedList;
public class DiffTextView extends TextView {
private static final String TAG = "DiffTextView";
private int mLineAdded_color;
private int mLineRemoved_color;
private final int mRangeInfo_color;
private final int mOrigHeader_color;
private final int mNewHeader_color;
private final int mPathInfo_color;
private SpannableString mColorizedSpan;
public DiffTextView(Context context, AttributeSet attrs) {
this(context, attrs, R.attr.diffStyle);
}
public DiffTextView(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
TypedArray a = context.getTheme().obtainStyledAttributes(
attrs,R.styleable.DiffTextView, defStyle, R.style.Diff_Light);
mLineAdded_color = a.getColor(R.styleable.DiffTextView_added, R.color.text_green);
mLineRemoved_color = a.getColor(R.styleable.DiffTextView_removed, R.color.text_red);
mRangeInfo_color = a.getColor(R.styleable.DiffTextView_rangeInfo, R.color.text_purple);
mOrigHeader_color = a.getColor(R.styleable.DiffTextView_origHeader, R.color.text_brown);
mNewHeader_color = a.getColor(R.styleable.DiffTextView_newHeader, Color.BLUE);
mPathInfo_color = a.getColor(R.styleable.DiffTextView_pathInfo, R.color.text_orange);
a.recycle();
}
/**
* Given a line from a diff, determine which color in which to highlight it.
* @param line A line from a diff comparison
* @return A CharacterStyle containing a color in which to highlight the text
*/
public CharacterStyle setColor(@NotNull String line) {
if (line.startsWith("+++")) return new ForegroundColorSpan(mNewHeader_color);
else if (line.startsWith("---")) return new ForegroundColorSpan(mOrigHeader_color);
else if (line.startsWith("+") && !line.startsWith("+++")) return new ForegroundColorSpan(mLineAdded_color);
else if (line.startsWith("-")) return new ForegroundColorSpan(mLineRemoved_color);
else if (line.startsWith("@@")) return new ForegroundColorSpan(mRangeInfo_color);
else if (line.startsWith("a/")) return new ForegroundColorSpan(mPathInfo_color);
return null;
}
/**
* @return The CharacterStyle containing what color to highlight trailing spaces
*/
public CharacterStyle getTrailingSpaceColor() {
return new BackgroundColorSpan(getResources().getColor(R.color.text_red));
}
public void setDiffText(String text) {
mColorizedSpan = spanColoredText(unescape(text.replaceAll("\\\\n", "\\\n").trim()));
if (mColorizedSpan != null && mColorizedSpan.length() > 0) {
this.setText(mColorizedSpan, BufferType.SPANNABLE);
} else {
this.setText("Failed to load diff :(");
}
}
@Contract("null -> null")
protected SpannableString spanColoredText(String incoming) {
if (incoming == null) return null;
String[] split = incoming.split("\n");
SpannableString spannableString = new SpannableString(incoming);
int charCounter = 0;
int lineTracker = 0;
// colorize added/removed lines
colorizeDiffs(split, spannableString, charCounter, lineTracker);
highlightUnwantedChars(spannableString);
return spannableString;
}
private void colorizeDiffs(String[] split, SpannableString spannableString,
int charCounter, int lineTracker) {
int end;
for (String string : split) {
charCounter += 1;
lineTracker += 1;
end = charCounter + string.length() > spannableString.length()
? spannableString.length()
: charCounter + string.length();
String trimmed = string.trim();
CharacterStyle style = setColor(trimmed);
if (style != null) {
spannableString.setSpan(style, charCounter - 1, end, 0);
}
// highlight trailing whitespace
int startWhitespace = findLastNonSpace(string);
if (startWhitespace > 0) {
Log.d(TAG, String.format("Trailing whitespace at line: %d index: %d through %d in diff view",
lineTracker,
startWhitespace,
string.length()));
spannableString.setSpan(getTrailingSpaceColor(),
charCounter + startWhitespace - 1, end, 0);
}
// test line with trailing whitespaces ->
// Here are 3 tabs -> - - <- this line ends with four whitespaces ->
charCounter += string.length();
}
}
@Contract("null -> fail")
private int findLastNonSpace(String s) {
int startWhitespace = -1;
if (s.endsWith(" ")) {
// count backwards and highlight the trailing whitespace
int lineLength = s.length();
for (int i = lineLength - 1; 0 <= i; i--) {
if (s.charAt(i) == ' ') {
startWhitespace = i;
} else {
break;
}
}
}
return startWhitespace;
}
/**
* Highlights whitespace issues
*/
private void highlightUnwantedChars(SpannableString spannableString) {
for (Integer ints : tabs) {
Log.d(TAG, "Index of tab: " + ints);
if (ints + 1 < spannableString.length()) {
spannableString.setSpan(getTrailingSpaceColor(),
ints - 1, ints + 1,
Spanned.SPAN_INTERMEDIATE);
spannableString.setSpan(new ForegroundColorSpan(Color.WHITE),
ints - 1, ints + 1,
Spanned.SPAN_INTERMEDIATE);
}
}
}
// used to track index of tab chars
private LinkedList<Integer> tabs = new LinkedList<>();
protected String unescape(String s) {
int i = 0, len = s.length();
char c;
StringBuilder sb = new StringBuilder(len);
while (i < len) {
c = s.charAt(i++);
if (c == '\\') {
if (i < len) {
c = s.charAt(i++);
if (c == 'u') {
// TODO: check that 4 more chars exist and are all hex digits
//noinspection MagicNumber
c = (char) Integer.parseInt(s.substring(i, i + 4), 16);
i += 4;
} else if (c == 't') {
// leave \t so we can highlight
sb.append("\\t");
continue;
}
// add other cases here as desired...
}
} // fall through: \ escapes itself, quotes any character but u
if (c == '\t') {
sb.append("\\t");
tabs.add(sb.length() - 1);
} else {
sb.append(c);
}
}
return sb.toString();
}
}