Add a basic syntax highlighter.
[kdbg.git] / kdbg / sourcewnd.cpp
blob2d38a86eb34f2f7ad64fe9bbeb2c07c7e4712175
1 // $Id$
3 // Copyright by Johannes Sixt
4 // This file is under GPL, the GNU General Public Licence
6 #include "debugger.h"
7 #include "sourcewnd.h"
8 #include <qtextstream.h>
9 #include <qpainter.h>
10 #include <qbrush.h>
11 #include <qfile.h>
12 #include <qkeycode.h>
13 #include <qpopupmenu.h>
14 #include <kapp.h>
15 #include <kiconloader.h>
16 #include <kglobalsettings.h>
17 #include <kmainwindow.h>
18 #ifdef HAVE_CONFIG_H
19 #include "config.h"
20 #endif
21 #include "mydebug.h"
24 SourceWindow::SourceWindow(const char* fileName, QWidget* parent, const char* name) :
25 QTextEdit(parent, name),
26 m_fileName(fileName),
27 m_curRow(-1),
28 m_widthItems(16),
29 m_widthPlus(12)
31 // load pixmaps
32 m_pcinner = UserIcon("pcinner");
33 m_pcup = UserIcon("pcup");
34 m_brkena = UserIcon("brkena");
35 m_brkdis = UserIcon("brkdis");
36 m_brktmp = UserIcon("brktmp");
37 m_brkcond = UserIcon("brkcond");
38 m_brkorph = UserIcon("brkorph");
39 setFont(KGlobalSettings::fixedFont());
40 setReadOnly(true);
41 setMargins(m_widthItems+m_widthPlus, 0, 0 ,0);
42 setAutoFormatting(AutoNone);
43 setTextFormat(PlainText);
44 setWordWrap(NoWrap);
45 connect(verticalScrollBar(), SIGNAL(valueChanged(int)),
46 this, SLOT(update()));
47 connect(this, SIGNAL(cursorPositionChanged(int,int)), this, SLOT(cursorChanged(int)));
48 viewport()->installEventFilter(this);
50 // add a syntax highlighter
51 if (QRegExp("\\.(c(pp|c|\\+\\+)?|CC?|h(\\+\\+|h)?|HH?)$").search(m_fileName))
53 new HighlightCpp(this);
57 SourceWindow::~SourceWindow()
59 delete syntaxHighlighter();
62 bool SourceWindow::loadFile()
64 // first we load the code into QTextEdit
65 QFile f(m_fileName);
66 if (!f.open(IO_ReadOnly)) {
67 return false;
70 QTextStream t(&f);
71 setText(t.read());
72 f.close();
74 // then we copy it into our own m_sourceCode
75 int n = paragraphs();
76 m_sourceCode.resize(n);
77 m_rowToLine.resize(n);
78 for (int i = 0; i < n; i++) {
79 m_sourceCode[i].code = text(i);
80 m_rowToLine[i] = i;
82 m_lineItems.resize(n, 0);
84 return true;
87 void SourceWindow::reloadFile()
89 QFile f(m_fileName);
90 if (!f.open(IO_ReadOnly)) {
91 // open failed; leave alone
92 return;
95 // read text into m_sourceCode
96 m_sourceCode.clear(); /* clear old text */
98 QTextStream t(&f);
99 setText(t.read());
100 f.close();
102 m_sourceCode.resize(paragraphs());
103 for (size_t i = 0; i < m_sourceCode.size(); i++) {
104 m_sourceCode[i].code = text(i);
106 // allocate line items
107 m_lineItems.resize(m_sourceCode.size(), 0);
109 m_rowToLine.resize(m_sourceCode.size());
110 for (size_t i = 0; i < m_sourceCode.size(); i++)
111 m_rowToLine[i] = i;
114 void SourceWindow::scrollTo(int lineNo, const DbgAddr& address)
116 if (lineNo < 0 || lineNo >= int(m_sourceCode.size()))
117 return;
119 int row = lineToRow(lineNo, address);
120 scrollToRow(row);
123 void SourceWindow::scrollToRow(int row)
125 setCursorPosition(row, 0);
126 ensureCursorVisible();
129 void SourceWindow::drawFrame(QPainter* p)
131 QTextEdit::drawFrame(p);
133 // and paragraph at the top is...
134 int top = paragraphAt(QPoint(0,contentsY()));
135 int bot = paragraphAt(QPoint(0,contentsY()+visibleHeight()-1));
136 if (bot < 0)
137 bot = paragraphs()-1;
139 p->save();
141 // set a clip rectangle
142 int fw = frameWidth();
143 QRect inside = rect();
144 inside.addCoords(fw,fw,-fw,-fw);
145 QRegion clip = p->clipRegion();
146 clip &= QRegion(inside);
147 p->setClipRegion(clip);
149 p->setPen(colorGroup().text());
150 p->eraseRect(inside);
152 for (int row = top; row <= bot; row++)
154 uchar item = m_lineItems[row];
155 p->save();
157 QRect r = paragraphRect(row);
158 QPoint pt = contentsToViewport(r.topLeft());
159 int h = r.height();
160 p->translate(fw, pt.y()+viewport()->y());
162 if (item & liBP) {
163 // enabled breakpoint
164 int y = (h - m_brkena.height())/2;
165 if (y < 0) y = 0;
166 p->drawPixmap(0,y,m_brkena);
168 if (item & liBPdisabled) {
169 // disabled breakpoint
170 int y = (h - m_brkdis.height())/2;
171 if (y < 0) y = 0;
172 p->drawPixmap(0,y,m_brkdis);
174 if (item & liBPtemporary) {
175 // temporary breakpoint marker
176 int y = (h - m_brktmp.height())/2;
177 if (y < 0) y = 0;
178 p->drawPixmap(0,y,m_brktmp);
180 if (item & liBPconditional) {
181 // conditional breakpoint marker
182 int y = (h - m_brkcond.height())/2;
183 if (y < 0) y = 0;
184 p->drawPixmap(0,y,m_brkcond);
186 if (item & liBPorphan) {
187 // orphaned breakpoint marker
188 int y = (h - m_brkcond.height())/2;
189 if (y < 0) y = 0;
190 p->drawPixmap(0,y,m_brkorph);
192 if (item & liPC) {
193 // program counter in innermost frame
194 int y = (h - m_pcinner.height())/2;
195 if (y < 0) y = 0;
196 p->drawPixmap(0,y,m_pcinner);
198 if (item & liPCup) {
199 // program counter somewhere up the stack
200 int y = (h - m_pcup.height())/2;
201 if (y < 0) y = 0;
202 p->drawPixmap(0,y,m_pcup);
204 if (!isRowDisassCode(row) && m_sourceCode[rowToLine(row)].canDisass) {
205 int w = m_widthPlus;
206 p->translate(m_widthItems, 0);
207 int x = w/2;
208 int y = h/2;
209 p->drawLine(x-2, y, x+2, y);
210 if (!isRowExpanded(row)) {
211 p->drawLine(x, y-2, x, y+2);
214 p->restore();
216 p->restore();
219 void SourceWindow::updateLineItems(const KDebugger* dbg)
221 // clear outdated breakpoints
222 for (int i = m_lineItems.size()-1; i >= 0; i--) {
223 if (m_lineItems[i] & liBPany) {
224 // check if this breakpoint still exists
225 int line = rowToLine(i);
226 TRACE(QString().sprintf("checking for bp at %d", line));
227 int j;
228 for (j = dbg->numBreakpoints()-1; j >= 0; j--) {
229 const Breakpoint* bp = dbg->breakpoint(j);
230 if (bp->lineNo == line &&
231 fileNameMatches(bp->fileName) &&
232 lineToRow(line, bp->address) == i)
234 // yes it exists; mode is changed below
235 break;
238 if (j < 0) {
239 /* doesn't exist anymore, remove it */
240 m_lineItems[i] &= ~liBPany;
241 update();
246 // add new breakpoints
247 for (int j = dbg->numBreakpoints()-1; j >= 0; j--) {
248 const Breakpoint* bp = dbg->breakpoint(j);
249 if (fileNameMatches(bp->fileName)) {
250 TRACE(QString().sprintf("updating %s:%d", bp->fileName.data(), bp->lineNo));
251 int i = bp->lineNo;
252 if (i < 0 || i >= int(m_sourceCode.size()))
253 continue;
254 // compute new line item flags for breakpoint
255 uchar flags = bp->enabled ? liBP : liBPdisabled;
256 if (bp->temporary)
257 flags |= liBPtemporary;
258 if (!bp->condition.isEmpty() || bp->ignoreCount != 0)
259 flags |= liBPconditional;
260 if (bp->isOrphaned())
261 flags |= liBPorphan;
262 // update if changed
263 int row = lineToRow(i, bp->address);
264 if ((m_lineItems[row] & liBPany) != flags) {
265 m_lineItems[row] &= ~liBPany;
266 m_lineItems[row] |= flags;
267 update();
273 void SourceWindow::setPC(bool set, int lineNo, const DbgAddr& address, int frameNo)
275 if (lineNo < 0 || lineNo >= int(m_sourceCode.size())) {
276 return;
279 int row = lineToRow(lineNo, address);
281 uchar flag = frameNo == 0 ? liPC : liPCup;
282 if (set) {
283 // set only if not already set
284 if ((m_lineItems[row] & flag) == 0) {
285 m_lineItems[row] |= flag;
286 update();
288 } else {
289 // clear only if not set
290 if ((m_lineItems[row] & flag) != 0) {
291 m_lineItems[row] &= ~flag;
292 update();
297 void SourceWindow::find(const QString& text, bool caseSensitive, FindDirection dir)
299 ASSERT(dir == 1 || dir == -1);
300 if (m_sourceCode.size() == 0 || text.isEmpty())
301 return;
303 int line;
304 DbgAddr dummyAddr;
305 activeLine(line, dummyAddr);
306 if (line < 0)
307 line = 0;
308 int curLine = line; /* remember where we started */
309 bool found = false;
310 do {
311 // advance and wrap around
312 line += dir;
313 if (line < 0)
314 line = m_sourceCode.size()-1;
315 else if (line >= int(m_sourceCode.size()))
316 line = 0;
317 // search text
318 found = m_sourceCode[line].code.find(text, 0, caseSensitive) >= 0;
319 } while (!found && line != curLine);
321 scrollTo(line, DbgAddr());
324 void SourceWindow::mousePressEvent(QMouseEvent* ev)
326 // we handle left and middle button
327 if (ev->button() != LeftButton && ev->button() != MidButton)
329 QTextEdit::mousePressEvent(ev);
330 return;
333 // get row
334 QPoint p = viewportToContents(QPoint(0, ev->y() - viewport()->y()));
335 int row = paragraphAt(p);
336 if (row < 0)
337 return;
339 if (ev->x() > m_widthItems+frameWidth())
341 if (isRowExpanded(row)) {
342 actionCollapseRow(row);
343 } else {
344 actionExpandRow(row);
346 return;
349 int sourceRow;
350 int line = rowToLine(row, &sourceRow);
352 // find address if row is disassembled code
353 DbgAddr address;
354 if (row > sourceRow) {
355 // get offset from source code line
356 int off = row - sourceRow;
357 address = m_sourceCode[line].disassAddr[off-1];
360 switch (ev->button()) {
361 case LeftButton:
362 TRACE(QString().sprintf("left-clicked line %d", line));
363 emit clickedLeft(m_fileName, line, address,
364 (ev->state() & ShiftButton) != 0);
365 break;
366 case MidButton:
367 TRACE(QString().sprintf("mid-clicked row %d", line));
368 emit clickedMid(m_fileName, line, address);
369 break;
370 default:;
374 void SourceWindow::keyPressEvent(QKeyEvent* ev)
376 int top1, top2;
377 QPoint top;
378 switch (ev->key()) {
379 case Key_Plus:
380 actionExpandRow(m_curRow);
381 return;
382 case Key_Minus:
383 actionCollapseRow(m_curRow);
384 return;
385 case Key_Up:
386 if (m_curRow > 0) {
387 setCursorPosition(m_curRow-1, 0);
389 return;
390 case Key_Down:
391 if (m_curRow < paragraphs()-1) {
392 setCursorPosition(m_curRow+1, 0);
394 return;
395 case Key_Home:
396 setCursorPosition(0, 0);
397 return;
398 case Key_End:
399 setCursorPosition(paragraphs()-1, 0);
400 return;
401 case Key_Next:
402 case Key_Prior:
403 top = viewportToContents(QPoint(0,0));
404 top1 = paragraphAt(top);
407 QTextEdit::keyPressEvent(ev);
409 switch (ev->key()) {
410 case Key_Next:
411 case Key_Prior:
412 top = viewportToContents(QPoint(0,0));
413 top2 = paragraphAt(top);
414 setCursorPosition(m_curRow+(top2-top1), 0);
418 static inline bool isident(QChar c)
420 return c.isLetterOrNumber() || c.latin1() == '_';
423 bool SourceWindow::wordAtPoint(const QPoint& p, QString& word, QRect& r)
425 QPoint pv = viewportToContents(p - viewport()->pos());
426 int row, col = charAt(pv, &row);
427 if (row < 0 || col < 0)
428 return false;
430 // isolate the word at row, col
431 QString line = text(row);
432 if (!isident(line[col]))
433 return false;
435 int begin = col;
436 while (begin > 0 && isident(line[begin-1]))
437 --begin;
439 ++col;
440 while (col < int(line.length()) && isident(line[col]));
442 r = QRect(p, p);
443 r.addCoords(-5,-5,5,5);
444 word = line.mid(begin, col-begin);
445 return true;
448 void SourceWindow::paletteChange(const QPalette& oldPal)
450 setFont(KGlobalSettings::fixedFont());
451 QTextEdit::paletteChange(oldPal);
455 * Two file names (possibly full paths) match if the last parts - the file
456 * names - match.
458 bool SourceWindow::fileNameMatches(const QString& other)
460 const QString& me = fileName();
462 // check for null file names first
463 if (me.isNull() || other.isNull()) {
464 return me.isNull() && other.isNull();
468 * Get file names. Note: Either there is a slash, then skip it, or
469 * there is no slash, then -1 + 1 = 0!
471 int sme = me.findRev('/') + 1;
472 int sother = other.findRev('/') + 1;
473 return strcmp(me.data() + sme, other.data() + sother) == 0;
476 void SourceWindow::disassembled(int lineNo, const QList<DisassembledCode>& disass)
478 TRACE("disassembled line " + QString().setNum(lineNo));
479 if (lineNo < 0 || lineNo >= int(m_sourceCode.size()))
480 return;
482 SourceLine& sl = m_sourceCode[lineNo];
484 // copy disassembled code and its addresses
485 sl.disass.resize(disass.count());
486 sl.disassAddr.resize(disass.count());
487 sl.canDisass = disass.count() > 0;
488 for (uint i = 0; i < disass.count(); i++) {
489 const DisassembledCode* c =
490 const_cast<QList<DisassembledCode>&>(disass).at(i);
491 QString code = c->code;
492 while (code.endsWith("\n"))
493 code.truncate(code.length()-1);
494 sl.disass[i] = c->address.asString() + ' ' + code;
495 sl.disassAddr[i] = c->address;
498 int row = lineToRow(lineNo);
499 if (sl.canDisass) {
500 expandRow(row);
501 } else {
502 // clear expansion marker
503 update();
507 int SourceWindow::rowToLine(int row, int* sourceRow)
509 int line = row >= 0 ? m_rowToLine[row] : -1;
510 if (sourceRow != 0) {
511 // search back until we hit the first entry with the current line number
512 while (row > 0 && m_rowToLine[row-1] == line)
513 row--;
514 *sourceRow = row;
516 return line;
520 * Rows showing diassembled code have the same line number as the
521 * corresponding source code line number. Therefore, the line numbers in
522 * m_rowToLine are monotonically increasing with blocks of equal line
523 * numbers for a source line and its disassembled code that follows it.
525 * Hence, m_rowToLine always obeys the following condition:
527 * m_rowToLine[i] <= i
530 int SourceWindow::lineToRow(int line)
532 // line is zero-based!
534 assert(line < int(m_rowToLine.size()));
536 // quick test for common case
537 if (line < 0 || m_rowToLine[line] == line)
538 return line;
540 assert(m_rowToLine[line] < line);
543 * Binary search between row == line and end of list. In the loop below
544 * we use the fact that the line numbers m_rowToLine do not contain
545 * holes.
547 int l = line;
548 int h = m_rowToLine.size();
549 while (l < h && m_rowToLine[l] != line)
551 assert(h == int(m_rowToLine.size()) || m_rowToLine[l] < m_rowToLine[h]);
554 * We want to round down the midpoint so that we find the
555 * lowest row that belongs to the line we seek.
557 int mid = (l+h)/2;
558 if (m_rowToLine[mid] <= line)
559 l = mid;
560 else
561 h = mid;
563 // Found! Result is in l:
564 assert(m_rowToLine[l] == line);
567 * We might not have hit the lowest index for the line.
569 while (l > 0 && m_rowToLine[l-1] == line)
570 --l;
572 return l;
575 int SourceWindow::lineToRow(int line, const DbgAddr& address)
577 int row = lineToRow(line);
578 if (isRowExpanded(row)) {
579 row += m_sourceCode[line].findAddressRowOffset(address);
581 return row;
584 bool SourceWindow::isRowExpanded(int row)
586 assert(row >= 0);
587 return row < int(m_rowToLine.size())-1 &&
588 m_rowToLine[row] == m_rowToLine[row+1];
591 bool SourceWindow::isRowDisassCode(int row)
593 return row > 0 && row < int(m_rowToLine.size()) &&
594 m_rowToLine[row] == m_rowToLine[row-1];
597 void SourceWindow::expandRow(int row)
599 TRACE("expanding row " + QString().setNum(row));
600 // get disassembled code
601 int line = rowToLine(row);
602 const std::vector<QString>& disass = m_sourceCode[line].disass;
604 // remove PC (must be set again in slot of signal expanded())
605 m_lineItems[row] &= ~(liPC|liPCup);
607 // adjust current row
608 if (m_curRow > row) {
609 m_curRow += disass.size();
610 // highlight is moved automatically
613 // insert new lines
614 ++row;
615 for (size_t i = 0; i < disass.size(); i++) {
616 m_rowToLine.insert(m_rowToLine.begin()+row, line);
617 m_lineItems.insert(m_lineItems.begin()+row, 0);
618 insertParagraph(disass[i], row++);
620 update(); // line items
622 emit expanded(line); /* must set PC */
625 void SourceWindow::collapseRow(int row)
627 TRACE("collapsing row " + QString().setNum(row));
628 int line = rowToLine(row);
630 // find end of this block
631 int end = row+1;
632 while (end < int(m_rowToLine.size()) && m_rowToLine[end] == m_rowToLine[row]) {
633 end++;
635 ++row;
636 // adjust current row
637 if (m_curRow >= row) {
638 m_curRow -= end-row;
639 if (m_curRow < row) // was m_curRow in disassembled code?
640 m_curRow = -1;
642 while (--end >= row) {
643 m_rowToLine.erase(m_rowToLine.begin()+end);
644 m_lineItems.erase(m_lineItems.begin()+end);
645 removeParagraph(end);
647 update(); // line items
649 emit collapsed(line);
652 void SourceWindow::activeLine(int& line, DbgAddr& address)
654 int row = m_curRow;
656 int sourceRow;
657 line = rowToLine(row, &sourceRow);
658 if (row > sourceRow) {
659 int off = row - sourceRow; /* offset from source line */
660 address = m_sourceCode[line].disassAddr[off-1];
665 * Returns the offset from the line displaying the source code to
666 * the line containing the specified address. If the address is not
667 * found, 0 is returned.
669 int SourceWindow::SourceLine::findAddressRowOffset(const DbgAddr& address) const
671 if (address.isEmpty())
672 return 0;
674 for (size_t i = 0; i < disassAddr.size(); i++) {
675 if (disassAddr[i] == address) {
676 // found exact address
677 return i+1;
679 if (disassAddr[i] > address) {
681 * We have already advanced too far; the address is before this
682 * index, but obviously we haven't found an exact match
683 * earlier. address is somewhere between the displayed
684 * addresses. We return the previous line.
686 return i;
689 // not found
690 return 0;
693 void SourceWindow::actionExpandRow(int row)
695 if (row < 0 || isRowExpanded(row) || isRowDisassCode(row))
696 return;
698 // disassemble
699 int line = rowToLine(row);
700 const SourceLine& sl = m_sourceCode[line];
701 if (!sl.canDisass)
702 return;
703 if (sl.disass.size() == 0) {
704 emit disassemble(m_fileName, line);
705 } else {
706 expandRow(row);
710 void SourceWindow::actionCollapseRow(int row)
712 if (row < 0 || !isRowExpanded(row) || isRowDisassCode(row))
713 return;
715 collapseRow(row);
718 void SourceWindow::setTabWidth(int numChars)
720 if (numChars <= 0)
721 numChars = 8;
722 QFontMetrics fm(currentFont());
723 QString s;
724 int w = fm.width(s.fill('x', numChars));
725 setTabStopWidth(w);
728 void SourceWindow::cursorChanged(int row)
730 if (row == m_curRow)
731 return;
733 if (m_curRow >= 0 && m_curRow < paragraphs())
734 clearParagraphBackground(m_curRow);
735 m_curRow = row;
736 setParagraphBackgroundColor(row, colorGroup().background());
738 emit lineChanged();
742 * We must override the context menu handling because QTextEdit's handling
743 * requires that it receives ownership of the popup menu; but the popup menu
744 * returned from the GUI factory is owned by the factory.
747 void SourceWindow::contextMenuEvent(QContextMenuEvent* e)
749 // get the context menu from the GUI factory
750 QWidget* top = this;
752 top = top->parentWidget();
753 while (!top->isTopLevel());
754 KMainWindow* mw = static_cast<KMainWindow*>(top);
755 QPopupMenu* m =
756 static_cast<QPopupMenu*>(mw->factory()->container("popup_files", mw));
757 m->exec(e->globalPos());
760 bool SourceWindow::eventFilter(QObject* watched, QEvent* e)
762 if (e->type() == QEvent::ContextMenu && watched == viewport())
764 contextMenuEvent(static_cast<QContextMenuEvent*>(e));
765 return true;
767 return QTextEdit::eventFilter(watched, e);
770 HighlightCpp::HighlightCpp(SourceWindow* srcWnd) :
771 QSyntaxHighlighter(srcWnd),
772 m_srcWnd(srcWnd)
776 int HighlightCpp::highlightParagraph(const QString& text, int endStateOfLastPara)
778 int row = currentParagraph();
779 // highlight assembly lines
780 if (m_srcWnd->isRowDisassCode(row))
782 setFormat(0, text.length(), blue);
783 return endStateOfLastPara;
785 setFormat(0, text.length(), m_srcWnd->colorGroup().text());
786 return 0;
789 #include "sourcewnd.moc"