2 * Copyright Johannes Sixt
3 * This file is licensed under the GNU General Public License Version 2.
4 * See the file COPYING in the toplevel directory of the source directory.
10 #include <QTextStream>
15 #include <QContextMenuEvent>
17 #include <QMouseEvent>
18 #include <kiconloader.h>
19 #include <kglobalsettings.h>
20 #include <kxmlguiwindow.h>
21 #include <kxmlguifactory.h>
26 SourceWindow::SourceWindow(const QString
& fileName
, QWidget
* parent
) :
27 QPlainTextEdit(parent
),
33 m_lineInfoArea(new LineInfoArea(this))
36 m_pcinner
= UserIcon("pcinner");
37 m_pcup
= UserIcon("pcup");
38 m_brkena
= UserIcon("brkena");
39 m_brkdis
= UserIcon("brkdis");
40 m_brktmp
= UserIcon("brktmp");
41 m_brkcond
= UserIcon("brkcond");
42 m_brkorph
= UserIcon("brkorph");
43 setFont(KGlobalSettings::fixedFont());
45 setViewportMargins(lineInfoAreaWidth(), 0, 0 ,0);
46 setWordWrapMode(QTextOption::NoWrap
);
47 connect(this, SIGNAL(updateRequest(const QRect
&, int)),
48 m_lineInfoArea
, SLOT(update()));
49 connect(this, SIGNAL(cursorPositionChanged()), this, SLOT(cursorChanged()));
51 // add a syntax highlighter
52 if (QRegExp("\\.(c(pp|c|\\+\\+)?|CC?|h(\\+\\+|h|pp)?|HH?)$").indexIn(m_fileName
) >= 0)
54 m_highlighter
= new HighlightCpp(this);
58 SourceWindow::~SourceWindow()
63 int SourceWindow::lineInfoAreaWidth() const
65 return 3 + m_widthItems
+ m_widthPlus
+ m_widthLineNo
;
68 bool SourceWindow::loadFile()
71 if (!f
.open(QIODevice::ReadOnly
)) {
76 setPlainText(t
.readAll());
80 m_sourceCode
.resize(n
);
81 m_rowToLine
.resize(n
);
82 for (int i
= 0; i
< n
; i
++) {
85 m_lineItems
.resize(n
, 0);
87 // set a font for line numbers
88 m_lineNoFont
= currentCharFormat().font();
89 m_lineNoFont
.setPixelSize(11);
94 void SourceWindow::reloadFile()
97 if (!f
.open(QIODevice::ReadOnly
)) {
98 // open failed; leave alone
102 m_sourceCode
.clear(); /* clear old text */
105 setPlainText(t
.readAll());
108 m_sourceCode
.resize(blockCount());
109 // expanded lines are collapsed: move existing line items up
110 for (size_t i
= 0; i
< m_lineItems
.size(); i
++) {
111 if (m_rowToLine
[i
] != int(i
)) {
112 m_lineItems
[m_rowToLine
[i
]] |= m_lineItems
[i
];
116 // allocate line items
117 m_lineItems
.resize(m_sourceCode
.size(), 0);
119 m_rowToLine
.resize(m_sourceCode
.size());
120 for (size_t i
= 0; i
< m_sourceCode
.size(); i
++)
123 // Highlighting was applied above when the text was inserted into widget,
124 // but at that time m_rowToLine was not corrected, yet, so that lines
125 // that previously were assembly were painted incorrectly.
127 m_highlighter
->rehighlight();
130 void SourceWindow::scrollTo(int lineNo
, const DbgAddr
& address
)
132 if (lineNo
< 0 || lineNo
>= int(m_sourceCode
.size()))
135 int row
= lineToRow(lineNo
, address
);
139 void SourceWindow::scrollToRow(int row
)
141 QTextCursor
cursor(document()->findBlockByNumber(row
));
142 setTextCursor(cursor
);
143 ensureCursorVisible();
146 void SourceWindow::resizeEvent(QResizeEvent
* e
)
148 QPlainTextEdit::resizeEvent(e
);
150 QRect cr
= contentsRect();
151 cr
.setRight(lineInfoAreaWidth());
152 m_lineInfoArea
->setGeometry(cr
);
155 void SourceWindow::drawLineInfoArea(QPainter
* p
, QPaintEvent
* event
)
157 QTextBlock block
= firstVisibleBlock();
159 p
->setFont(m_lineNoFont
);
161 for (; block
.isValid(); block
= block
.next())
163 if (!block
.isVisible())
166 QRect r
= blockBoundingGeometry(block
).translated(contentOffset()).toRect();
167 if (r
.bottom() < event
->rect().top())
168 continue; // skip blocks that are higher than the region being updated
169 else if (r
.top() > event
->rect().bottom())
170 break; // all the following blocks are lower then the region being updated
172 int row
= block
.blockNumber();
173 uchar item
= m_lineItems
[row
];
177 p
->translate(0, r
.top());
180 // enabled breakpoint
181 int y
= (h
- m_brkena
.height())/2;
183 p
->drawPixmap(0,y
,m_brkena
);
185 if (item
& liBPdisabled
) {
186 // disabled breakpoint
187 int y
= (h
- m_brkdis
.height())/2;
189 p
->drawPixmap(0,y
,m_brkdis
);
191 if (item
& liBPtemporary
) {
192 // temporary breakpoint marker
193 int y
= (h
- m_brktmp
.height())/2;
195 p
->drawPixmap(0,y
,m_brktmp
);
197 if (item
& liBPconditional
) {
198 // conditional breakpoint marker
199 int y
= (h
- m_brkcond
.height())/2;
201 p
->drawPixmap(0,y
,m_brkcond
);
203 if (item
& liBPorphan
) {
204 // orphaned breakpoint marker
205 int y
= (h
- m_brkcond
.height())/2;
207 p
->drawPixmap(0,y
,m_brkorph
);
210 // program counter in innermost frame
211 int y
= (h
- m_pcinner
.height())/2;
213 p
->drawPixmap(0,y
,m_pcinner
);
216 // program counter somewhere up the stack
217 int y
= (h
- m_pcup
.height())/2;
219 p
->drawPixmap(0,y
,m_pcup
);
221 p
->translate(m_widthItems
, 0);
222 if (!isRowDisassCode(row
) && m_sourceCode
[rowToLine(row
)].canDisass
) {
226 p
->drawLine(x
-2, y
, x
+2, y
);
227 if (!isRowExpanded(row
)) {
228 p
->drawLine(x
, y
-2, x
, y
+2);
231 p
->translate(m_widthPlus
, 0);
232 if (!isRowDisassCode(row
)) {
233 p
->drawText(0, 0, m_widthLineNo
, h
, Qt::AlignRight
|Qt::AlignVCenter
,
234 QString().setNum(rowToLine(row
)+1));
240 void SourceWindow::updateLineItems(const KDebugger
* dbg
)
242 // clear outdated breakpoints
243 for (int i
= m_lineItems
.size()-1; i
>= 0; i
--) {
244 if (m_lineItems
[i
] & liBPany
) {
245 // check if this breakpoint still exists
246 int line
= rowToLine(i
);
247 TRACE(QString().sprintf("checking for bp at %d", line
));
248 KDebugger::BrkptROIterator bp
= dbg
->breakpointsBegin();
249 for (; bp
!= dbg
->breakpointsEnd(); ++bp
)
251 if (bp
->lineNo
== line
&&
252 fileNameMatches(bp
->fileName
) &&
253 lineToRow(line
, bp
->address
) == i
)
255 // yes it exists; mode is changed below
259 if (bp
== dbg
->breakpointsEnd()) {
260 /* doesn't exist anymore, remove it */
261 m_lineItems
[i
] &= ~liBPany
;
266 // add new breakpoints
267 for (KDebugger::BrkptROIterator bp
= dbg
->breakpointsBegin(); bp
!= dbg
->breakpointsEnd(); ++bp
)
269 if (fileNameMatches(bp
->fileName
)) {
270 TRACE(QString("updating %2:%1").arg(bp
->lineNo
).arg(bp
->fileName
));
272 if (i
< 0 || i
>= int(m_sourceCode
.size()))
274 // compute new line item flags for breakpoint
275 uchar flags
= bp
->enabled
? liBP
: liBPdisabled
;
277 flags
|= liBPtemporary
;
278 if (!bp
->condition
.isEmpty() || bp
->ignoreCount
!= 0)
279 flags
|= liBPconditional
;
280 if (bp
->isOrphaned())
283 int row
= lineToRow(i
, bp
->address
);
284 if ((m_lineItems
[row
] & liBPany
) != flags
) {
285 m_lineItems
[row
] &= ~liBPany
;
286 m_lineItems
[row
] |= flags
;
290 m_lineInfoArea
->update();
293 void SourceWindow::setPC(bool set
, int lineNo
, const DbgAddr
& address
, int frameNo
)
295 if (lineNo
< 0 || lineNo
>= int(m_sourceCode
.size())) {
299 int row
= lineToRow(lineNo
, address
);
301 uchar flag
= frameNo
== 0 ? liPC
: liPCup
;
303 // set only if not already set
304 if ((m_lineItems
[row
] & flag
) == 0) {
305 m_lineItems
[row
] |= flag
;
306 m_lineInfoArea
->update();
309 // clear only if not set
310 if ((m_lineItems
[row
] & flag
) != 0) {
311 m_lineItems
[row
] &= ~flag
;
312 m_lineInfoArea
->update();
317 void SourceWindow::find(const QString
& text
, bool caseSensitive
, FindDirection dir
)
319 ASSERT(dir
== 1 || dir
== -1);
320 QTextDocument::FindFlags flags
= 0;
322 flags
|= QTextDocument::FindCaseSensitively
;
324 flags
|= QTextDocument::FindBackward
;
325 if (QPlainTextEdit::find(text
, flags
))
327 // not found; wrap around
328 QTextCursor
cursor(document());
330 cursor
.movePosition(QTextCursor::End
);
331 cursor
= document()->find(text
, cursor
, flags
);
332 if (!cursor
.isNull())
333 setTextCursor(cursor
);
336 void SourceWindow::infoMousePress(QMouseEvent
* ev
)
338 // we handle left and middle button
339 if (ev
->button() != Qt::LeftButton
&& ev
->button() != Qt::MidButton
)
345 int row
= cursorForPosition(QPoint(0, ev
->y())).blockNumber();
349 if (ev
->x() > m_widthItems
)
351 if (isRowExpanded(row
)) {
352 actionCollapseRow(row
);
354 actionExpandRow(row
);
360 int line
= rowToLine(row
, &sourceRow
);
362 // find address if row is disassembled code
364 if (row
> sourceRow
) {
365 // get offset from source code line
366 int off
= row
- sourceRow
;
367 address
= m_sourceCode
[line
].disassAddr
[off
-1];
370 switch (ev
->button()) {
372 TRACE(QString().sprintf("left-clicked line %d", line
));
373 emit
clickedLeft(m_fileName
, line
, address
,
374 (ev
->modifiers() & Qt::ShiftModifier
) != 0);
377 TRACE(QString().sprintf("mid-clicked row %d", line
));
378 emit
clickedMid(m_fileName
, line
, address
);
384 void SourceWindow::keyPressEvent(QKeyEvent
* ev
)
389 actionExpandRow(textCursor().blockNumber());
392 actionCollapseRow(textCursor().blockNumber());
396 moveCursor(QTextCursor::PreviousBlock
);
400 moveCursor(QTextCursor::NextBlock
);
403 moveCursor(QTextCursor::Start
);
406 moveCursor(QTextCursor::End
);
409 case Qt::Key_PageDown
:
410 top1
= firstVisibleBlock().blockNumber();
413 QPlainTextEdit::keyPressEvent(ev
);
417 case Qt::Key_PageDown
:
418 top2
= firstVisibleBlock().blockNumber();
420 QTextCursor cursor
= textCursor();
422 cursor
.movePosition(QTextCursor::NextBlock
,
423 QTextCursor::MoveAnchor
, top2
-top1
);
425 cursor
.movePosition(QTextCursor::PreviousBlock
,
426 QTextCursor::MoveAnchor
, top1
-top2
);
427 setTextCursor(cursor
);
432 QString
SourceWindow::extendExpr(const QString
&plainText
,
436 QString document
= plainText
.left(wordEnd
);
437 QString word
= plainText
.mid(wordStart
, wordEnd
- wordStart
);
438 QRegExp regex
= QRegExp("(::)?([A-Za-z_]{1}\\w*\\s*(->|\\.|::)\\s*)*" + word
+ "$");
440 #define IDENTIFIER_MAX_SIZE 256
441 // cut the document to reduce size of string to scan
442 // because of this only identifiefs of length <= IDENTIFIER_MAX_SIZE are supported
443 if (document
.length() > IDENTIFIER_MAX_SIZE
) {
444 document
.right(IDENTIFIER_MAX_SIZE
);
447 const int index
= regex
.indexIn(document
);
451 TRACE("No match, returning " + word
);
455 const int length
= regex
.matchedLength();
457 word
= plainText
.mid(index
, length
);
458 TRACE("Matched, returning " + word
);
464 bool SourceWindow::wordAtPoint(const QPoint
& p
, QString
& word
, QRect
& r
)
466 QTextCursor cursor
= cursorForPosition(viewport()->mapFrom(this, p
));
470 cursor
.select(QTextCursor::WordUnderCursor
);
471 word
= cursor
.selectedText();
476 // keep only letters and digits
477 QRegExp
w("[A-Za-z_]{1}[\\dA-Za-z_]*");
478 if (w
.indexIn(word
) < 0)
485 // if cpp highlighter is enabled - c/c++ file is being displayed
487 // check that word is not a c/c++ keyword
488 if (m_highlighter
->isCppKeyword(word
))
491 // TODO check that cursor is on top of a string literal
492 // and don't display any tooltips in this case
494 // try to extend selected word under the cursor to get a full variable name
495 word
= extendExpr(cursor
.document()->toPlainText(),
496 cursor
.selectionStart(),
497 cursor
.selectionEnd());
508 void SourceWindow::paletteChange(const QPalette
& oldPal
)
510 setFont(KGlobalSettings::fixedFont());
511 QPlainTextEdit::paletteChange(oldPal
);
515 * Two file names (possibly full paths) match if the last parts - the file
518 bool SourceWindow::fileNameMatches(const QString
& other
)
520 return QFileInfo(other
).fileName() == QFileInfo(m_fileName
).fileName();
523 void SourceWindow::disassembled(int lineNo
, const std::list
<DisassembledCode
>& disass
)
525 TRACE("disassembled line " + QString().setNum(lineNo
));
526 if (lineNo
< 0 || lineNo
>= int(m_sourceCode
.size()))
529 SourceLine
& sl
= m_sourceCode
[lineNo
];
531 // copy disassembled code and its addresses
532 sl
.disass
.resize(disass
.size());
533 sl
.disassAddr
.resize(disass
.size());
534 sl
.canDisass
= !disass
.empty();
536 for (std::list
<DisassembledCode
>::const_iterator c
= disass
.begin(); c
!= disass
.end(); ++c
, ++i
)
538 QString code
= c
->code
;
539 while (code
.endsWith("\n"))
540 code
.truncate(code
.length()-1);
541 sl
.disass
[i
] = c
->address
.asString() + ' ' + code
;
542 sl
.disassAddr
[i
] = c
->address
;
545 int row
= lineToRow(lineNo
);
549 // clear expansion marker
550 m_lineInfoArea
->update();
554 int SourceWindow::rowToLine(int row
, int* sourceRow
)
556 int line
= row
>= 0 ? m_rowToLine
[row
] : -1;
557 if (sourceRow
!= 0) {
558 // search back until we hit the first entry with the current line number
559 while (row
> 0 && m_rowToLine
[row
-1] == line
)
567 * Rows showing diassembled code have the same line number as the
568 * corresponding source code line number. Therefore, the line numbers in
569 * m_rowToLine are monotonically increasing with blocks of equal line
570 * numbers for a source line and its disassembled code that follows it.
572 * Hence, m_rowToLine always obeys the following condition:
574 * m_rowToLine[i] <= i
577 int SourceWindow::lineToRow(int line
)
579 // line is zero-based!
581 assert(line
< int(m_rowToLine
.size()));
583 // quick test for common case
584 if (line
< 0 || m_rowToLine
[line
] == line
)
587 assert(m_rowToLine
[line
] < line
);
590 * Binary search between row == line and end of list. In the loop below
591 * we use the fact that the line numbers m_rowToLine do not contain
595 int h
= m_rowToLine
.size();
596 while (l
< h
&& m_rowToLine
[l
] != line
)
598 assert(h
== int(m_rowToLine
.size()) || m_rowToLine
[l
] < m_rowToLine
[h
]);
601 * We want to round down the midpoint so that we find the
602 * lowest row that belongs to the line we seek.
605 if (m_rowToLine
[mid
] <= line
)
610 // Found! Result is in l:
611 assert(m_rowToLine
[l
] == line
);
614 * We might not have hit the lowest index for the line.
616 while (l
> 0 && m_rowToLine
[l
-1] == line
)
622 int SourceWindow::lineToRow(int line
, const DbgAddr
& address
)
624 int row
= lineToRow(line
);
625 if (isRowExpanded(row
)) {
626 row
+= m_sourceCode
[line
].findAddressRowOffset(address
);
631 bool SourceWindow::isRowExpanded(int row
)
634 return row
< int(m_rowToLine
.size())-1 &&
635 m_rowToLine
[row
] == m_rowToLine
[row
+1];
638 bool SourceWindow::isRowDisassCode(int row
)
640 return row
> 0 && row
< int(m_rowToLine
.size()) &&
641 m_rowToLine
[row
] == m_rowToLine
[row
-1];
644 void SourceWindow::expandRow(int row
)
646 TRACE("expanding row " + QString().setNum(row
));
647 // get disassembled code
648 int line
= rowToLine(row
);
649 const std::vector
<QString
>& disass
= m_sourceCode
[line
].disass
;
651 // remove PC (must be set again in slot of signal expanded())
652 m_lineItems
[row
] &= ~(liPC
|liPCup
);
655 setUpdatesEnabled(false);
657 m_rowToLine
.insert(m_rowToLine
.begin()+row
, disass
.size(), line
);
658 m_lineItems
.insert(m_lineItems
.begin()+row
, disass
.size(), 0);
660 QTextCursor
cursor(document()->findBlockByNumber(row
));
661 for (size_t i
= 0; i
< disass
.size(); i
++) {
662 cursor
.insertText(disass
[i
]);
663 cursor
.insertBlock();
665 setUpdatesEnabled(true);
667 emit
expanded(line
); /* must set PC */
670 void SourceWindow::collapseRow(int row
)
672 TRACE("collapsing row " + QString().setNum(row
));
673 int line
= rowToLine(row
);
675 // find end of this block
677 while (end
< int(m_rowToLine
.size()) && m_rowToLine
[end
] == m_rowToLine
[row
]) {
681 setUpdatesEnabled(false);
682 QTextCursor
cursor(document()->findBlockByNumber(end
-1));
683 while (--end
>= row
) {
684 m_rowToLine
.erase(m_rowToLine
.begin()+end
);
685 m_lineItems
.erase(m_lineItems
.begin()+end
);
686 cursor
.select(QTextCursor::BlockUnderCursor
);
687 cursor
.removeSelectedText();
689 setUpdatesEnabled(true);
691 emit
collapsed(line
);
694 void SourceWindow::activeLine(int& line
, DbgAddr
& address
)
696 int row
= textCursor().blockNumber();
699 line
= rowToLine(row
, &sourceRow
);
700 if (row
> sourceRow
) {
701 int off
= row
- sourceRow
; /* offset from source line */
702 address
= m_sourceCode
[line
].disassAddr
[off
-1];
707 * Returns the offset from the line displaying the source code to
708 * the line containing the specified address. If the address is not
709 * found, 0 is returned.
711 int SourceWindow::SourceLine::findAddressRowOffset(const DbgAddr
& address
) const
713 if (address
.isEmpty())
716 for (size_t i
= 0; i
< disassAddr
.size(); i
++) {
717 if (disassAddr
[i
] == address
) {
718 // found exact address
721 if (disassAddr
[i
] > address
) {
723 * We have already advanced too far; the address is before this
724 * index, but obviously we haven't found an exact match
725 * earlier. address is somewhere between the displayed
726 * addresses. We return the previous line.
735 void SourceWindow::actionExpandRow(int row
)
737 if (row
< 0 || isRowExpanded(row
) || isRowDisassCode(row
))
741 int line
= rowToLine(row
);
742 const SourceLine
& sl
= m_sourceCode
[line
];
745 if (sl
.disass
.size() == 0) {
746 emit
disassemble(m_fileName
, line
);
752 void SourceWindow::actionCollapseRow(int row
)
754 if (row
< 0 || !isRowExpanded(row
) || isRowDisassCode(row
))
760 void SourceWindow::setTabWidth(int numChars
)
764 QFontMetrics
fm(document()->defaultFont());
766 int w
= fm
.width(s
.fill('x', numChars
));
770 void SourceWindow::cursorChanged()
772 QList
<QTextEdit::ExtraSelection
> extraSelections
;
773 QTextEdit::ExtraSelection selection
;
775 selection
.format
.setBackground(QColor("#E7E7E7"));
776 selection
.format
.setProperty(QTextFormat::FullWidthSelection
, true);
777 selection
.cursor
= textCursor();
778 selection
.cursor
.clearSelection();
779 extraSelections
.append(selection
);
780 setExtraSelections(extraSelections
);
784 * Show our own context menu.
786 void SourceWindow::contextMenuEvent(QContextMenuEvent
* e
)
788 // get the context menu from the GUI factory
791 top
= top
->parentWidget();
792 while (!top
->isTopLevel());
793 KXmlGuiWindow
* mw
= static_cast<KXmlGuiWindow
*>(top
);
794 QMenu
* m
= static_cast<QMenu
*>(mw
->factory()->container("popup_files", mw
));
796 m
->exec(e
->globalPos());
799 void LineInfoArea::paintEvent(QPaintEvent
* e
)
802 static_cast<SourceWindow
*>(parent())->drawLineInfoArea(&p
, e
);
805 void LineInfoArea::mousePressEvent(QMouseEvent
* ev
)
807 static_cast<SourceWindow
*>(parent())->infoMousePress(ev
);
810 void LineInfoArea::contextMenuEvent(QContextMenuEvent
* e
)
812 static_cast<SourceWindow
*>(parent())->contextMenuEvent(e
);
815 HighlightCpp::HighlightCpp(SourceWindow
* srcWnd
) :
816 QSyntaxHighlighter(srcWnd
->document()),
828 static const QString ckw
[] =
906 void HighlightCpp::highlightBlock(const QString
& text
)
908 int state
= previousBlockState();
909 state
= highlight(text
, state
);
910 setCurrentBlockState(state
);
913 int HighlightCpp::highlight(const QString
& text
, int state
)
915 // highlight assembly lines
916 if (m_srcWnd
->isRowDisassCode(currentBlock().blockNumber()))
918 setFormat(0, text
.length(), Qt::blue
);
922 if (state
== -2) // initial state
925 // check for preprocessor line
926 if (state
== 0 && text
.trimmed().startsWith("#"))
928 setFormat(0, text
.length(), QColor("darkgreen"));
932 // a font for keywords
933 QTextCharFormat identFont
;
934 identFont
.setFontWeight(QFont::Bold
);
937 while (start
< text
.length())
944 setFormat(start
, end
-start
, QColor("gray"));
947 end
= text
.indexOf("*/", start
);
952 setFormat(start
, end
-start
, QColor("gray"));
955 for (end
= start
+1; end
< int(text
.length()); end
++) {
956 if (text
[end
] == '\\') {
957 if (end
< int(text
.length()))
959 } else if (text
[end
] == text
[start
]) {
965 setFormat(start
, end
-start
, QColor("darkred"));
968 for (end
= start
+1; end
< int(text
.length()); end
++) {
969 if (!text
[end
].isLetterOrNumber() && text
[end
] != '_')
973 if (isCppKeyword(text
.mid(start
, end
-start
)))
975 setFormat(start
, end
-start
, identFont
);
977 setFormat(start
, end
-start
, m_srcWnd
->palette().color(QPalette::WindowText
));
981 for (end
= start
; end
< int(text
.length()); end
++)
983 if (text
[end
] == '/')
985 if (end
+1 < int(text
.length())) {
986 if (text
[end
+1] == '/') {
987 state
= hlCommentLine
;
989 } else if (text
[end
+1] == '*') {
990 state
= hlCommentBlock
;
995 else if (text
[end
] == '"' || text
[end
] == '\'')
1000 else if ((text
[end
] >= 'A' && text
[end
] <= 'Z') ||
1001 (text
[end
] >= 'a' && text
[end
] <= 'z') ||
1008 setFormat(start
, end
-start
, m_srcWnd
->palette().color(QPalette::WindowText
));
1015 bool HighlightCpp::isCppKeyword(const QString
& word
)
1017 return std::binary_search(ckw
, ckw
+ sizeof(ckw
)/sizeof(ckw
[0]), word
);
1020 #include "sourcewnd.moc"