/*
Author: Marco Costalba (C) 2005-2007
Copyright: See COPYING file that comes with this distribution
*/
#include
#include
#include "common.h"
#include "domain.h"
#include "git.h"
#include "myprocess.h"
#include "patchcontent.h"
void DiffHighlighter::highlightBlock(const QString& text) {
// state is used to count paragraphs, starting from 0
setCurrentBlockState(previousBlockState() + 1);
if (text.isEmpty())
return;
const bool useDark = QPalette().color(QPalette::Window).value() > QPalette().color(QPalette::WindowText).value();
QBrush blue = useDark ? Qt::darkBlue : QColor(Qt::cyan);
QBrush green = useDark ? Qt::darkGreen : QColor(Qt::green);
QBrush magenta = useDark ? Qt::darkMagenta : QColor(Qt::magenta);
QBrush backgroundPurple = useDark ? QGit::PURPLE : QGit::PURPLE.darker(600);
QTextCharFormat myFormat;
const char firstChar = text.at(0).toLatin1();
switch (firstChar) {
case '@':
myFormat.setForeground(magenta);
break;
case '+':
myFormat.setForeground(green);
break;
case '-':
myFormat.setForeground(Qt::red);
break;
case 'c':
case 'd':
case 'i':
case 'n':
case 'o':
case 'r':
case 's':
if (text.startsWith("diff --git a/")) {
myFormat.setForeground(blue);
myFormat.setBackground(backgroundPurple);
} else if (text.startsWith("copy ")
|| text.startsWith("index ")
|| text.startsWith("new ")
|| text.startsWith("old ")
|| text.startsWith("rename ")
|| text.startsWith("similarity "))
myFormat.setForeground(blue);
else if (cl > 0 && text.startsWith("diff --combined")) {
myFormat.setForeground(blue);
myFormat.setBackground(backgroundPurple);
}
break;
case ' ':
if (cl > 0) {
if (text.left(cl).contains('+'))
myFormat.setForeground(green);
else if (text.left(cl).contains('-'))
myFormat.setForeground(Qt::red);
}
break;
}
if (myFormat.isValid())
setFormat(0, text.length(), myFormat);
PatchContent* pc = static_cast(parent());
if (pc->matches.count() > 0) {
int indexFrom, indexTo;
if (pc->getMatch(currentBlockState(), &indexFrom, &indexTo)) {
QTextEdit* te = dynamic_cast(parent());
QTextCharFormat fmt;
fmt.setFont(te->currentFont());
fmt.setFontWeight(QFont::Bold);
fmt.setForeground(Qt::blue);
if (indexTo == 0)
indexTo = text.length();
setFormat(indexFrom, indexTo - indexFrom, fmt);
}
}
}
PatchContent::PatchContent(QWidget* parent) : QTextEdit(parent) {
diffLoaded = seekTarget = false;
curFilter = prevFilter = VIEW_ALL;
pickAxeRE.setMinimal(true);
pickAxeRE.setCaseSensitivity(Qt::CaseInsensitive);
setFont(QGit::TYPE_WRITER_FONT);
diffHighlighter = new DiffHighlighter(this);
}
void PatchContent::setup(Domain*, Git* g) {
git = g;
}
void PatchContent::clear() {
git->cancelProcess(proc);
QTextEdit::clear();
patchRowData.clear();
halfLine = "";
matches.clear();
diffLoaded = false;
seekTarget = !target.isEmpty();
}
void PatchContent::refresh() {
int topPara = topToLineNum();
setUpdatesEnabled(false);
QByteArray tmp(patchRowData);
clear();
patchRowData = tmp;
processData(patchRowData, &topPara);
scrollLineToTop(topPara);
setUpdatesEnabled(true);
}
void PatchContent::scrollCursorToTop() {
QRect r = cursorRect();
QScrollBar* vsb = verticalScrollBar();
vsb->setValue(vsb->value() + r.top());
}
void PatchContent::scrollLineToTop(int lineNum) {
QTextCursor tc = textCursor();
tc.movePosition(QTextCursor::Start);
tc.movePosition(QTextCursor::NextBlock, QTextCursor::MoveAnchor, lineNum);
setTextCursor(tc);
scrollCursorToTop();
}
int PatchContent::positionToLineNum(int pos) {
QTextCursor tc = textCursor();
tc.setPosition(pos);
return tc.blockNumber();
}
int PatchContent::topToLineNum() {
return cursorForPosition(QPoint(1, 1)).blockNumber();
}
bool PatchContent::centerTarget(SCRef target) {
moveCursor(QTextCursor::Start);
// find() updates cursor position
if (!find(target, QTextDocument::FindCaseSensitively | QTextDocument::FindWholeWords))
return false;
// move to the beginning of the line
moveCursor(QTextCursor::StartOfLine);
// grap copy of current cursor state
QTextCursor tc = textCursor();
// move the target line to the top
moveCursor(QTextCursor::End);
setTextCursor(tc);
return true;
}
void PatchContent::centerOnFileHeader(StateInfo& st) {
if (st.fileName().isEmpty())
return;
target = st.fileName();
bool combined = (st.isMerge() && !st.allMergeFiles());
git->formatPatchFileHeader(&target, st.sha(), st.diffToSha(), combined, st.allMergeFiles());
seekTarget = !target.isEmpty();
if (seekTarget)
seekTarget = !centerTarget(target);
}
void PatchContent::centerMatch(int id) {
if (matches.count() <= id)
return;
//FIXME
// patchTab->textEditDiff->setSelection(matches[id].paraFrom, matches[id].indexFrom,
// matches[id].paraTo, matches[id].indexTo);
}
void PatchContent::procReadyRead(const QByteArray& data) {
patchRowData.append(data);
if (document()->isEmpty() && isVisible())
processData(data);
}
void PatchContent::typeWriterFontChanged() {
setFont(QGit::TYPE_WRITER_FONT);
setPlainText(toPlainText());
}
void PatchContent::processData(const QByteArray& fileChunk, int* prevLineNum) {
QString newLines;
if (!QGit::stripPartialParaghraps(fileChunk, &newLines, &halfLine))
return;
if (!prevLineNum && curFilter == VIEW_ALL)
goto skip_filter; // optimize common case
{ // scoped code because of goto
QString filteredLines;
int notNegCnt = 0, notPosCnt = 0;
QVector toAdded(1), toRemoved(1); // lines count from 1
// prevLineNum will be set to the number of corresponding
// line in full patch. Number is negative just for algorithm
// reasons, prevLineNum counts lines from 1
if (prevLineNum && prevFilter == VIEW_ALL)
*prevLineNum = -(*prevLineNum); // set once
const QStringList sl(newLines.split('\n', QString::KeepEmptyParts));
FOREACH_SL (it, sl) {
// do not remove diff header because of centerTarget
bool n = (*it).startsWith('-') && !(*it).startsWith("---");
bool p = (*it).startsWith('+') && !(*it).startsWith("+++");
if (!p)
notPosCnt++;
if (!n)
notNegCnt++;
toAdded.append(notNegCnt);
toRemoved.append(notPosCnt);
int curLineNum = toAdded.count() - 1;
bool toRemove = (n && curFilter == VIEW_ADDED) || (p && curFilter == VIEW_REMOVED);
if (!toRemove)
filteredLines.append(*it).append('\n');
if (prevLineNum && *prevLineNum == notNegCnt && prevFilter == VIEW_ADDED)
*prevLineNum = -curLineNum; // set once
if (prevLineNum && *prevLineNum == notPosCnt && prevFilter == VIEW_REMOVED)
*prevLineNum = -curLineNum; // set once
}
if (prevLineNum && *prevLineNum <= 0) {
if (curFilter == VIEW_ALL)
*prevLineNum = -(*prevLineNum);
else if (curFilter == VIEW_ADDED)
*prevLineNum = toAdded.at(-(*prevLineNum));
else if (curFilter == VIEW_REMOVED)
*prevLineNum = toRemoved.at(-(*prevLineNum));
if (*prevLineNum < 0)
*prevLineNum = 0;
}
newLines = filteredLines;
} // end of scoped code
skip_filter:
setUpdatesEnabled(false);
if (prevLineNum || document()->isEmpty()) { // use the faster setPlainText()
setPlainText(newLines);
moveCursor(QTextCursor::Start);
} else {
int topLine = cursorForPosition(QPoint(1, 1)).blockNumber();
append(newLines);
if (topLine > 0)
scrollLineToTop(topLine);
}
QScrollBar* vsb = verticalScrollBar();
vsb->setValue(vsb->value() + cursorRect().top());
setUpdatesEnabled(true);
}
void PatchContent::procFinished() {
if (!patchRowData.endsWith("\n"))
patchRowData.append('\n'); // flush pending half lines
refresh(); // show patchRowData content
if (seekTarget)
seekTarget = !centerTarget(target);
diffLoaded = true;
if (computeMatches()) {
diffHighlighter->rehighlight(); // slow on big data
centerMatch();
}
}
int PatchContent::doSearch(SCRef txt, int pos) {
if (isRegExp)
return txt.indexOf(pickAxeRE, pos);
return txt.indexOf(pickAxeRE.pattern(), pos, Qt::CaseInsensitive);
}
bool PatchContent::computeMatches() {
matches.clear();
if (pickAxeRE.isEmpty())
return false;
SCRef txt = toPlainText();
int pos, lastPos = 0, lastPara = 0;
// must be at the end to catch patterns across more the one chunk
while ((pos = doSearch(txt, lastPos)) != -1) {
matches.append(MatchSelection());
MatchSelection& s = matches.last();
s.paraFrom = txt.mid(lastPos, pos - lastPos).count('\n');
s.paraFrom += lastPara;
s.indexFrom = pos - txt.lastIndexOf('\n', pos) - 1; // index starts from 0
lastPos = pos;
pos += (isRegExp ? pickAxeRE.matchedLength() : pickAxeRE.pattern().length());
pos--;
s.paraTo = s.paraFrom + txt.mid(lastPos, pos - lastPos).count('\n');
s.indexTo = pos - txt.lastIndexOf('\n', pos) - 1;
s.indexTo++; // in QTextEdit::setSelection() indexTo is not included
lastPos = pos;
lastPara = s.paraTo;
}
return !matches.isEmpty();
}
bool PatchContent::getMatch(int para, int* indexFrom, int* indexTo) {
for (int i = 0; i < matches.count(); i++)
if (matches[i].paraFrom <= para && matches[i].paraTo >= para) {
*indexFrom = (para == matches[i].paraFrom ? matches[i].indexFrom : 0);
*indexTo = (para == matches[i].paraTo ? matches[i].indexTo : 0);
return true;
}
return false;
}
void PatchContent::on_highlightPatch(const QString& exp, bool re) {
pickAxeRE.setPattern(exp);
isRegExp = re;
if (diffLoaded)
procFinished();
}
void PatchContent::update(StateInfo& st) {
bool combined = (st.isMerge() && !st.allMergeFiles());
if (combined) {
const Rev* r = git->revLookup(st.sha());
if (r)
diffHighlighter->setCombinedLength(r->parentsCount());
} else
diffHighlighter->setCombinedLength(0);
clear();
proc = git->getDiff(st.sha(), this, st.diffToSha(), combined); // non blocking
}