patchview.cpp

Go to the documentation of this file.
00001 /*
00002         Description: patch viewer window
00003 
00004         Author: Marco Costalba (C) 2005-2006
00005 
00006         Copyright: See COPYING file that comes with this distribution
00007 
00008 */
00009 #include <qtextedit.h>
00010 #include <qlineedit.h>
00011 #include <qapplication.h>
00012 #include <qsyntaxhighlighter.h>
00013 #include <qradiobutton.h>
00014 #include <qbuttongroup.h>
00015 #include <qtoolbutton.h>
00016 #include <qtabwidget.h>
00017 #include <qaction.h>
00018 #include "common.h"
00019 #include "git.h"
00020 #include "domain.h"
00021 #include "mainimpl.h"
00022 #include "revdesc.h"
00023 #include "filelist.h"
00024 #include "patchbase.h"
00025 #include "patchview.h"
00026 
00027 class DiffHighlighter : public QSyntaxHighlighter {
00028 public:
00029         DiffHighlighter(PatchView* p, QTextEdit* te) :
00030                         QSyntaxHighlighter(te), pv(p), combinedLenght(0) {}
00031 
00032         void setCombinedLength(uint cl) { combinedLenght = cl; }
00033         virtual int highlightParagraph (const QString& text, int) {
00034 
00035                 QColor myColor;
00036                 const char firstChar = text[0].latin1();
00037                 switch (firstChar) {
00038                 case '@':
00039                         myColor = Qt::darkMagenta;
00040                         break;
00041                 case '+':
00042                         myColor = Qt::darkGreen;
00043                         break;
00044                 case '-':
00045                         myColor = Qt::red;
00046                         break;
00047                 case 'c':
00048                 case 'd':
00049                 case 'i':
00050                 case 'n':
00051                 case 'o':
00052                 case 'r':
00053                 case 's':
00054                         if (   text.startsWith("diff --git a/")
00055                             || text.startsWith("copy ")
00056                             || text.startsWith("index ")
00057                             || text.startsWith("new ")
00058                             || text.startsWith("old ")
00059                             || text.startsWith("rename ")
00060                             || text.startsWith("similarity "))
00061                                 myColor = Qt::darkBlue;
00062                         else if (combinedLenght > 0 && text.startsWith("diff --combined"))
00063                                 myColor = Qt::darkBlue;
00064                         break;
00065                 case ' ':
00066                         if (combinedLenght > 0) {
00067                                 if (text.left(combinedLenght).contains('+'))
00068                                         myColor = Qt::darkGreen;
00069                                 else if (text.left(combinedLenght).contains('-'))
00070                                         myColor = Qt::red;
00071                         }
00072                         break;
00073                 }
00074                 if (myColor.isValid())
00075                         setFormat(0, text.length(), myColor);
00076 
00077                 if (pv->matches.count() > 0) {
00078                         int indexFrom, indexTo;
00079                         if (pv->getMatch(currentParagraph(), &indexFrom, &indexTo)) {
00080 
00081                                 QFont f = textEdit()->currentFont();
00082                                 f.setUnderline(true);
00083                                 f.setBold(true);
00084                                 if (indexTo == 0)
00085                                         indexTo = text.length();
00086 
00087                                 setFormat(indexFrom, indexTo - indexFrom, f, Qt::blue);
00088                         }
00089                 }
00090                 return 0;
00091         }
00092 private:
00093         PatchView* pv;
00094         uint combinedLenght;
00095 };
00096 
00097 PatchView::PatchView(MainImpl* mi, Git* g) : Domain(mi, g) {
00098 
00099         seekTarget = diffLoaded = false;
00100         pickAxeRE.setMinimal(true);
00101         pickAxeRE.setCaseSensitive(false);
00102 
00103         patchTab = new TabPatch(m());
00104         patchTab->toolButton_all->hide();
00105         patchTab->toolButton_added->hide();
00106         patchTab->toolButton_removed->hide();
00107         patchTab->textEditDiff->setFont(QGit::TYPE_WRITER_FONT);
00108         patchTab->textBrowserDesc->setDomain(this);
00109         patchTab->buttonFilterPatch->setIconSet(patchTab->toolButton_all->iconSet());
00110         curFilter = prevFilter = VIEW_ALL;
00111 
00112         listBoxFiles = new ListBoxFiles(this, git, patchTab->listBoxFiles);
00113         diffHighlighter = new DiffHighlighter(this, patchTab->textEditDiff);
00114 
00115         m()->tabWdg->addTab(patchTab, "&Patch");
00116         tabPosition = m()->tabWdg->count() - 1;
00117 
00118         connect(patchTab->lineEditDiff, SIGNAL(returnPressed()),
00119                 this, SLOT(on_lineEditDiff_returnPressed()));
00120 
00121         connect(patchTab->buttonGroupDiff, SIGNAL(clicked(int)),
00122                 this, SLOT(on_buttonGroupDiff_clicked(int)));
00123 
00124         connect(patchTab->buttonFilterPatch, SIGNAL(clicked()),
00125                 this, SLOT(on_buttonFilterPatch_clicked()));
00126 
00127         connect(listBoxFiles, SIGNAL(contextMenu(const QString&, int)),
00128                 this, SLOT(on_contextMenu(const QString&, int)));
00129 }
00130 
00131 PatchView::~PatchView() {
00132 
00133         if (!parent())
00134                 return;
00135 
00136         git->cancelProcess(proc);
00137         delete diffHighlighter;
00138         delete listBoxFiles;
00139 
00140         // remove before to delete, avoids a Qt warning in QInputContext()
00141         m()->tabWdg->removePage(patchTab);
00142         delete patchTab;
00143 }
00144 
00145 void PatchView::clear(bool complete) {
00146 
00147         if (complete) {
00148                 st.clear();
00149                 patchTab->textBrowserDesc->clear();
00150                 listBoxFiles->clear();
00151         }
00152         patchTab->textEditDiff->clear();
00153         patchRowData.resize(0);
00154         partialParagraphs = "";
00155         matches.clear();
00156         diffLoaded = false;
00157         seekTarget = !target.isEmpty();
00158 }
00159 
00160 void PatchView::on_buttonFilterPatch_clicked() {
00161 
00162         QIconSet ic;
00163         prevFilter = curFilter;
00164         if (curFilter == VIEW_ALL) {
00165                 curFilter = VIEW_ADDED;
00166                 ic = patchTab->toolButton_added->iconSet();
00167 
00168         } else if (curFilter == VIEW_ADDED) {
00169                 curFilter = VIEW_REMOVED;
00170                 ic = patchTab->toolButton_removed->iconSet();
00171 
00172         } else if (curFilter == VIEW_REMOVED) {
00173                 curFilter = VIEW_ALL;
00174                 ic = patchTab->toolButton_all->iconSet();
00175         }
00176         patchTab->buttonFilterPatch->setIconSet(ic);
00177         QTextEdit* te = patchTab->textEditDiff;
00178         int topPara = te->paragraphAt(QPoint(te->contentsX(), te->contentsY()));
00179         partialParagraphs = "";
00180         patchTab->textEditDiff->setUpdatesEnabled(false);
00181         patchTab->textEditDiff->setText(processData(patchRowData, &topPara));
00182         int t = te->paragraphRect(topPara).bottom(); // slow for big files
00183         te->setContentsPos(0, t);
00184         patchTab->textEditDiff->setUpdatesEnabled(true);
00185 }
00186 
00187 void PatchView::centerOnFileHeader(const QString& fileName) {
00188 
00189         if (st.fileName().isEmpty())
00190                 return;
00191 
00192         target = fileName;
00193         bool combined = (st.isMerge() && !st.allMergeFiles());
00194         git->formatPatchFileHeader(&target, st.sha(), st.diffToSha(), combined, st.allMergeFiles());
00195         seekTarget = !target.isEmpty();
00196         if (seekTarget)
00197                 centerTarget();
00198 }
00199 
00200 void PatchView::on_contextMenu(const QString& data, int type) {
00201 
00202         if (isLinked()) // skip if not linked to main view
00203                 Domain::on_contextMenu(data, type);
00204 }
00205 
00206 void PatchView::centerTarget() {
00207 
00208         patchTab->textEditDiff->setCursorPosition(0, 0);
00209         if (!patchTab->textEditDiff->find(target, true, true)) // updates cursor position
00210                 return;
00211 
00212         // target found
00213         seekTarget = false;
00214         int para, index;
00215         patchTab->textEditDiff->getCursorPosition(&para, &index);
00216         QPoint p = patchTab->textEditDiff->paragraphRect(para).topLeft();
00217         patchTab->textEditDiff->setContentsPos(p.x(), p.y());
00218         patchTab->textEditDiff->removeSelection();
00219 }
00220 
00221 void PatchView::centerMatch(uint id) {
00222 
00223         if (matches.count() <= id)
00224                 return;
00225 
00226         patchTab->textEditDiff->setSelection(matches[id].paraFrom, matches[id].indexFrom,
00227                                              matches[id].paraTo, matches[id].indexTo);
00228 }
00229 
00230 void PatchView::on_procDataReady(const QByteArray& data) {
00231 
00232         int X = patchTab->textEditDiff->contentsX();
00233         int Y = patchTab->textEditDiff->contentsY();
00234         bool targetInNewChunk = false;
00235 
00236         QGit::baAppend(patchRowData, data);
00237 
00238         // QTextEdit::append() adds a new paragraph, i.e. inserts a LF
00239         // if not already present. For performance reasons we cannot use
00240         // QTextEdit::text() + QString::append() + QTextEdit::setText()
00241         // so we append only \n terminating text
00242         SCRef newLines = processData(data);
00243         patchTab->textEditDiff->append(newLines);
00244 
00245         if (seekTarget)
00246                 targetInNewChunk = (newLines.find(target) != -1);
00247 
00248         if (targetInNewChunk)
00249                 centerTarget();
00250         else {
00251                 patchTab->textEditDiff->setContentsPos(X, Y);
00252                 patchTab->textEditDiff->sync();
00253         }
00254 }
00255 
00256 const QString PatchView::processData(const QByteArray& fileChunk, int* prevLineNum) {
00257 
00258         QString newLines;
00259         if (!QGit::stripPartialParaghraps(fileChunk, &newLines, &partialParagraphs))
00260                 return newLines;
00261 
00262         if (!prevLineNum && curFilter == VIEW_ALL)
00263                 goto skip_filter; // optimize common case
00264 
00265         { // scoped code because of goto
00266 
00267         QString filteredLines;
00268         int notNegCnt = 0, notPosCnt = 0;
00269         QValueVector<int> toAdded(1, 0), toRemoved(1, 0); // lines count from 1
00270 
00271         // prevLineNum will be set to the number of corresponding
00272         // line in full patch. Number is negative just for algorithm
00273         // reasons, prevLineNum counts lines from 1
00274         if (prevLineNum && prevFilter == VIEW_ALL)
00275                 *prevLineNum = -(*prevLineNum); // set once
00276 
00277         const QStringList sl(QStringList::split('\n', newLines, true));
00278         FOREACH_SL (it, sl) {
00279 
00280                 // do not remove diff header because of centerTarget
00281                 bool n = (*it).startsWith("-") && !(*it).startsWith("---");
00282                 bool p = (*it).startsWith("+") && !(*it).startsWith("+++");
00283 
00284                 if (!p)
00285                         notPosCnt++;
00286                 if (!n)
00287                         notNegCnt++;
00288 
00289                 toAdded.append(notNegCnt);
00290                 toRemoved.append(notPosCnt);
00291 
00292                 int curLineNum = toAdded.count() - 1;
00293 
00294                 bool toRemove = (n && curFilter == VIEW_ADDED) || (p && curFilter == VIEW_REMOVED);
00295                 if (!toRemove)
00296                         filteredLines.append(*it).append('\n');
00297 
00298                 if (prevLineNum && *prevLineNum == notNegCnt && prevFilter == VIEW_ADDED)
00299                         *prevLineNum = -curLineNum; // set once
00300 
00301                 if (prevLineNum && *prevLineNum == notPosCnt && prevFilter == VIEW_REMOVED)
00302                         *prevLineNum = -curLineNum; // set once
00303         }
00304         if (prevLineNum && *prevLineNum <= 0) {
00305                 if (curFilter == VIEW_ALL)
00306                         *prevLineNum = -(*prevLineNum);
00307 
00308                 else if (curFilter == VIEW_ADDED)
00309                         *prevLineNum = toAdded.at(-(*prevLineNum));
00310 
00311                 else if (curFilter == VIEW_REMOVED)
00312                         *prevLineNum = toRemoved.at(-(*prevLineNum));
00313 
00314                 if (*prevLineNum < 0)
00315                         *prevLineNum = 0;
00316         }
00317         newLines = filteredLines;
00318 
00319         } // end of scoped code
00320 
00321 skip_filter:
00322 
00323         return newLines;
00324 }
00325 
00326 void PatchView::on_eof() {
00327 
00328         if (  !patchRowData.isEmpty()
00329             && patchRowData.at(patchRowData.size() - 1) != '\n')
00330                 patchTab->textEditDiff->append(processData('\n')); // flush pending half lines
00331 
00332         diffLoaded = true;
00333         computeMatches();
00334         diffHighlighter->rehighlight();
00335         centerMatch();
00336 }
00337 
00338 int PatchView::doSearch(SCRef txt, int pos) {
00339 
00340         if (isRegExp)
00341                 return pickAxeRE.search(txt, pos);
00342 
00343         return txt.find(pickAxeRE.pattern(), pos, true);
00344 }
00345 
00346 void PatchView::computeMatches() {
00347 
00348         matches.clear();
00349         if (pickAxeRE.isEmpty())
00350                 return;
00351 
00352         SCRef txt = patchTab->textEditDiff->text();
00353         int pos, lastPos = 0, lastPara = 0;
00354 
00355         // must be at the end to catch patterns across more the one chunk
00356         while ((pos = doSearch(txt, lastPos)) != -1) {
00357 
00358                 matches.append(MatchSelection());
00359                 MatchSelection& s = matches.last();
00360 
00361                 s.paraFrom = txt.mid(lastPos, pos - lastPos).contains('\n');
00362                 s.paraFrom += lastPara;
00363                 s.indexFrom = pos - txt.findRev('\n', pos) - 1; // index starts from 0
00364 
00365                 lastPos = pos;
00366                 pos += (isRegExp) ? pickAxeRE.matchedLength() : pickAxeRE.pattern().length();
00367                 pos--;
00368 
00369                 s.paraTo = s.paraFrom + txt.mid(lastPos, pos - lastPos).contains('\n');
00370                 s.indexTo = pos - txt.findRev('\n', pos) - 1;
00371                 s.indexTo++; // in QTextEdit::setSelection() indexTo is not included
00372 
00373                 lastPos = pos;
00374                 lastPara = s.paraTo;
00375         }
00376 }
00377 
00378 bool PatchView::getMatch(int para, int* indexFrom, int* indexTo) {
00379 
00380         for (uint i = 0; i < matches.count(); i++)
00381                 if (matches[i].paraFrom <= para && matches[i].paraTo >= para) {
00382 
00383                         *indexFrom = (para == matches[i].paraFrom) ? matches[i].indexFrom : 0;
00384                         *indexTo = (para == matches[i].paraTo) ? matches[i].indexTo : 0;
00385                         return true;
00386                 }
00387         return false;
00388 }
00389 
00390 void PatchView::on_highlightPatch(const QString& exp, bool re) {
00391 
00392         pickAxeRE.setPattern(exp);
00393         isRegExp = re;
00394         if (diffLoaded)
00395                 on_eof();
00396 }
00397 
00398 void PatchView::on_lineEditDiff_returnPressed() {
00399 
00400         if (patchTab->lineEditDiff->text().isEmpty())
00401                 return;
00402 
00403         patchTab->radioButtonSha->setChecked(true); // could be called by code
00404         on_buttonGroupDiff_clicked(DIFF_TO_SHA);
00405 }
00406 
00407 void PatchView::on_buttonGroupDiff_clicked(int diffType) {
00408 
00409         QString sha;
00410         switch (diffType) {
00411         case DIFF_TO_PARENT:
00412                 break;
00413         case DIFF_TO_HEAD:
00414                 sha = "HEAD";
00415                 break;
00416         case DIFF_TO_SHA:
00417                 sha = patchTab->lineEditDiff->text();
00418                 break;
00419         }
00420         if (sha == QGit::ZERO_SHA)
00421                 return;
00422 
00423         // check for a ref name or an abbreviated form
00424         normalizedSha = (sha.length() != 40 && !sha.isEmpty()) ? git->getRefSha(sha) : sha;
00425 
00426         if (normalizedSha != st.diffToSha()) { // avoid looping
00427                 st.setDiffToSha(normalizedSha); // could be empty
00428                 UPDATE();
00429         }
00430 }
00431 
00432 void PatchView::on_updateRevDesc() {
00433 
00434         bool showHeader = m()->ActShowDescHeader->isOn();
00435         SCRef d(git->getDesc(st.sha(), m()->shortLogRE, m()->longLogRE, showHeader));
00436         patchTab->textBrowserDesc->setText(d);
00437         patchTab->textBrowserDesc->setCursorPosition(0, 0);
00438 }
00439 
00440 void PatchView::updatePatch() {
00441 
00442         git->cancelProcess(proc);
00443         clear(false); // only patch content
00444 
00445         bool combined = (st.isMerge() && !st.allMergeFiles());
00446         if (combined) {
00447                 const Rev* r = git->revLookup(st.sha());
00448                 if (r)
00449                         diffHighlighter->setCombinedLength(r->parentsCount());
00450         } else
00451                 diffHighlighter->setCombinedLength(0);
00452 
00453         if (normalizedSha != st.diffToSha()) { // note <(null)> != <(empty)>
00454 
00455                 if (!st.diffToSha().isEmpty()) {
00456                         patchTab->lineEditDiff->setText(st.diffToSha());
00457                         on_lineEditDiff_returnPressed();
00458 
00459                 } else if (!normalizedSha.isEmpty()) {
00460                         normalizedSha = "";
00461                         // we cannot uncheck radioButtonSha directly
00462                         // because "Parent" button will stay off
00463                         patchTab->radioButtonSha->group()->find(0)->toggle();
00464                 }
00465         }
00466         proc = git->getDiff(st.sha(), this, st.diffToSha(), combined); // non blocking
00467 }
00468 
00469 bool PatchView::doUpdate(bool force) {
00470 
00471         const RevFile* files = NULL;
00472         bool newFiles = false;
00473 
00474         if (st.isChanged(StateInfo::SHA) || force) {
00475 
00476                 if (!isLinked()) {
00477                         QString caption(git->getShortLog(st.sha()));
00478                         if (caption.length() > 30)
00479                                 caption = caption.left(30 - 3).stripWhiteSpace().append("...");
00480 
00481                         m()->tabWdg->changeTab(patchTab, caption);
00482                 }
00483                 on_updateRevDesc();
00484         }
00485 
00486         if (st.isChanged(StateInfo::ANY & ~StateInfo::FILE_NAME) || force) {
00487 
00488                 updatePatch();
00489                 listBoxFiles->clear();
00490                 files = git->getFiles(st.sha(), st.diffToSha(), st.allMergeFiles());
00491                 newFiles = true;
00492         }
00493         // call always to allow a simple refresh
00494         listBoxFiles->update(files, newFiles);
00495 
00496         if (st.isChanged() || force)
00497                 centerOnFileHeader(st.fileName());
00498 
00499         return true;
00500 }

Generated on Fri Dec 7 21:57:38 2007 for QGit by  doxygen 1.5.3