1 | /* This file is part of the KDE libraries and the Kate part. |
2 | * |
3 | * Copyright (C) 2003 Anders Lund <anders.lund@lund.tdcadsl.dk> |
4 | * Copyright (C) 2010 Christoph Cullmann <cullmann@kde.org> |
5 | * |
6 | * This library is free software; you can redistribute it and/or |
7 | * modify it under the terms of the GNU Library General Public |
8 | * License as published by the Free Software Foundation; either |
9 | * version 2 of the License, or (at your option) any later version. |
10 | * |
11 | * This library is distributed in the hope that it will be useful, |
12 | * but WITHOUT ANY WARRANTY; without even the implied warranty of |
13 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU |
14 | * Library General Public License for more details. |
15 | * |
16 | * You should have received a copy of the GNU Library General Public License |
17 | * along with this library; see the file COPYING.LIB. If not, write to |
18 | * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, |
19 | * Boston, MA 02110-1301, USA. |
20 | */ |
21 | |
22 | //BEGIN includes |
23 | #include "katewordcompletion.h" |
24 | #include "kateview.h" |
25 | #include "kateconfig.h" |
26 | #include "katedocument.h" |
27 | #include "kateglobal.h" |
28 | #include <katehighlight.h> |
29 | #include <katehighlighthelpers.h> |
30 | |
31 | #include <ktexteditor/variableinterface.h> |
32 | #include <ktexteditor/movingrange.h> |
33 | #include <ktexteditor/range.h> |
34 | |
35 | #include <kconfig.h> |
36 | #include <kdialog.h> |
37 | #include <kpluginfactory.h> |
38 | #include <klocale.h> |
39 | #include <kaction.h> |
40 | #include <kactioncollection.h> |
41 | #include <knotification.h> |
42 | #include <kparts/part.h> |
43 | #include <kiconloader.h> |
44 | #include <kpagedialog.h> |
45 | #include <kpagewidgetmodel.h> |
46 | #include <ktoggleaction.h> |
47 | #include <kconfiggroup.h> |
48 | #include <kcolorscheme.h> |
49 | #include <kaboutdata.h> |
50 | |
51 | #include <QtCore/QRegExp> |
52 | #include <QtCore/QString> |
53 | #include <QtCore/QSet> |
54 | #include <QtGui/QSpinBox> |
55 | #include <QtGui/QLabel> |
56 | #include <QtGui/QLayout> |
57 | |
58 | #include <kvbox.h> |
59 | #include <QtGui/QCheckBox> |
60 | |
61 | #include <kdebug.h> |
62 | //END |
63 | |
64 | /// Amount of characters the document may have to enable automatic invocation (1MB) |
65 | static const int autoInvocationMaxFilesize = 1000000; |
66 | |
67 | //BEGIN KateWordCompletionModel |
68 | KateWordCompletionModel::KateWordCompletionModel( QObject *parent ) |
69 | : CodeCompletionModel2( parent ), m_automatic(false) |
70 | { |
71 | setHasGroups(false); |
72 | } |
73 | |
74 | KateWordCompletionModel::~KateWordCompletionModel() |
75 | { |
76 | } |
77 | |
78 | void KateWordCompletionModel::saveMatches( KTextEditor::View* view, |
79 | const KTextEditor::Range& range) |
80 | { |
81 | m_matches = allMatches( view, range ); |
82 | m_matches.sort(); |
83 | } |
84 | |
85 | QVariant KateWordCompletionModel::data(const QModelIndex& index, int role) const |
86 | { |
87 | if( role == UnimportantItemRole ) |
88 | return QVariant(true); |
89 | if( role == InheritanceDepth ) |
90 | return 10000; |
91 | |
92 | if( !index.parent().isValid() ) { |
93 | //It is the group header |
94 | switch ( role ) |
95 | { |
96 | case Qt::DisplayRole: |
97 | return i18n("Auto Word Completion" ); |
98 | case GroupRole: |
99 | return Qt::DisplayRole; |
100 | } |
101 | } |
102 | |
103 | if( index.column() == KTextEditor::CodeCompletionModel::Name && role == Qt::DisplayRole ) |
104 | return m_matches.at( index.row() ); |
105 | |
106 | if( index.column() == KTextEditor::CodeCompletionModel::Icon && role == Qt::DecorationRole ) { |
107 | static QIcon icon(KIcon("insert-text" ).pixmap(QSize(16, 16))); |
108 | return icon; |
109 | } |
110 | |
111 | return QVariant(); |
112 | } |
113 | |
114 | QModelIndex KateWordCompletionModel::parent(const QModelIndex& index) const |
115 | { |
116 | if(index.internalId()) |
117 | return createIndex(0, 0, 0); |
118 | else |
119 | return QModelIndex(); |
120 | } |
121 | |
122 | QModelIndex KateWordCompletionModel::index(int row, int column, const QModelIndex& parent) const |
123 | { |
124 | if( !parent.isValid()) { |
125 | if(row == 0) |
126 | return createIndex(row, column, 0); |
127 | else |
128 | return QModelIndex(); |
129 | |
130 | }else if(parent.parent().isValid()) |
131 | return QModelIndex(); |
132 | |
133 | |
134 | if (row < 0 || row >= m_matches.count() || column < 0 || column >= ColumnCount ) |
135 | return QModelIndex(); |
136 | |
137 | return createIndex(row, column, 1); |
138 | } |
139 | |
140 | int KateWordCompletionModel::rowCount ( const QModelIndex & parent ) const |
141 | { |
142 | if( !parent.isValid() && !m_matches.isEmpty() ) |
143 | return 1; //One root node to define the custom group |
144 | else if(parent.parent().isValid()) |
145 | return 0; //Completion-items have no children |
146 | else |
147 | return m_matches.count(); |
148 | } |
149 | |
150 | |
151 | bool KateWordCompletionModel::shouldStartCompletion(KTextEditor::View* view, const QString &insertedText, bool userInsertion, const KTextEditor::Cursor &position) |
152 | { |
153 | if (!userInsertion) return false; |
154 | if (insertedText.isEmpty()) |
155 | return false; |
156 | |
157 | |
158 | KateView *v = qobject_cast<KateView*> (view); |
159 | |
160 | if (view->document()->totalCharacters() > autoInvocationMaxFilesize) { |
161 | // Disable automatic invocation for files larger than 1MB (see benchmarks) |
162 | return false; |
163 | } |
164 | |
165 | const QString& text = view->document()->line(position.line()).left(position.column()); |
166 | const uint check = v->config()->wordCompletionMinimalWordLength(); |
167 | // Start completion immediately if min. word size is zero |
168 | if (!check) return true; |
169 | // Otherwise, check if user has typed long enough text... |
170 | const int start = text.length(); |
171 | const int end = start - check; |
172 | if (end < 0) return false; |
173 | for (int i = start - 1; i >= end; i--) { |
174 | const QChar c = text.at(i); |
175 | if (!(c.isLetter() || (c.isNumber()) || c=='_')) return false; |
176 | } |
177 | |
178 | return true; |
179 | } |
180 | |
181 | bool KateWordCompletionModel::shouldAbortCompletion(KTextEditor::View* view, const KTextEditor::Range &range, const QString ¤tCompletion) { |
182 | |
183 | if (m_automatic) { |
184 | KateView *v = qobject_cast<KateView*> (view); |
185 | if (currentCompletion.length()<v->config()->wordCompletionMinimalWordLength()) return true; |
186 | } |
187 | |
188 | return CodeCompletionModelControllerInterface4::shouldAbortCompletion(view,range,currentCompletion); |
189 | } |
190 | |
191 | |
192 | |
193 | void KateWordCompletionModel::completionInvoked(KTextEditor::View* view, const KTextEditor::Range& range, InvocationType it) |
194 | { |
195 | m_automatic = it == AutomaticInvocation; |
196 | saveMatches( view, range ); |
197 | } |
198 | |
199 | |
200 | /** |
201 | * Scan throughout the entire document for possible completions, |
202 | * ignoring any dublets and words shorter than configured and/or |
203 | * reasonable minimum length. |
204 | */ |
205 | QStringList KateWordCompletionModel::allMatches( KTextEditor::View *view, const KTextEditor::Range &range ) const |
206 | { |
207 | QSet<QString> result; |
208 | const int minWordSize = qMax(2, qobject_cast<KateView*>(view)->config()->wordCompletionMinimalWordLength()); |
209 | const int lines = view->document()->lines(); |
210 | for ( int line = 0; line < lines; line++ ) { |
211 | const QString& text = view->document()->line(line); |
212 | int wordBegin = 0; |
213 | int offset = 0; |
214 | const int end = text.size(); |
215 | while ( offset < end ) { |
216 | const QChar c = text.at(offset); |
217 | // increment offset when at line end, so we take the last character too |
218 | if ( ( ! c.isLetterOrNumber() && c != '_' ) || (offset == end - 1 && offset++) ) { |
219 | if ( offset - wordBegin > minWordSize && ( line != range.end().line() || offset != range.end().column() ) ) { |
220 | result.insert(text.mid(wordBegin, offset - wordBegin)); |
221 | } |
222 | wordBegin = offset + 1; |
223 | } |
224 | if ( c.isSpace() ) { |
225 | wordBegin = offset + 1; |
226 | } |
227 | offset += 1; |
228 | } |
229 | } |
230 | return result.values(); |
231 | } |
232 | |
233 | void KateWordCompletionModel::executeCompletionItem2( |
234 | KTextEditor::Document* document |
235 | , const KTextEditor::Range& word |
236 | , const QModelIndex& index |
237 | ) const |
238 | { |
239 | KateView *v = qobject_cast<KateView*> (document->activeView()); |
240 | if (v->config()->wordCompletionRemoveTail()) |
241 | { |
242 | int tailStart = word.end().column(); |
243 | const QString& line = document->line(word.end().line()); |
244 | int tailEnd = line.length(); |
245 | for (int i = word.end().column(); i < tailEnd; ++i) |
246 | { |
247 | // Letters, numbers and underscore are part of a word! |
248 | /// \todo Introduce configurable \e word-separators?? |
249 | if (!line[i].isLetterOrNumber() && line[i] != '_') |
250 | { |
251 | tailEnd = i; |
252 | } |
253 | } |
254 | |
255 | int sizeDiff = m_matches.at(index.row()).size() - (word.end().column() - word.start().column()); |
256 | |
257 | tailStart += sizeDiff; |
258 | tailEnd += sizeDiff; |
259 | |
260 | KTextEditor::Range tail = word; |
261 | tail.start().setColumn(tailStart); |
262 | tail.end().setColumn(tailEnd); |
263 | |
264 | document->replaceText(word, m_matches.at(index.row())); |
265 | v->doc()->editEnd(); |
266 | v->doc()->editStart(); |
267 | document->replaceText(tail, "" ); |
268 | } |
269 | else |
270 | { |
271 | document->replaceText(word, m_matches.at(index.row())); |
272 | } |
273 | } |
274 | |
275 | KTextEditor::CodeCompletionModelControllerInterface3::MatchReaction KateWordCompletionModel::matchingItem(const QModelIndex& /*matched*/) |
276 | { |
277 | return HideListIfAutomaticInvocation; |
278 | } |
279 | |
280 | bool KateWordCompletionModel::shouldHideItemsWithEqualNames() const |
281 | { |
282 | // We don't want word-completion items if the same items |
283 | // are available through more sophisticated completion models |
284 | return true; |
285 | } |
286 | |
287 | // Return the range containing the word left of the cursor |
288 | KTextEditor::Range KateWordCompletionModel::completionRange(KTextEditor::View* view, const KTextEditor::Cursor &position) |
289 | { |
290 | int line = position.line(); |
291 | int col = position.column(); |
292 | |
293 | KTextEditor::Document *doc = view->document(); |
294 | while ( col > 0 ) |
295 | { |
296 | const QChar c = ( doc->character( KTextEditor::Cursor( line, col-1 ) ) ); |
297 | if ( c.isLetterOrNumber() || c.isMark() || c == '_' ) |
298 | { |
299 | col--; |
300 | continue; |
301 | } |
302 | |
303 | break; |
304 | } |
305 | |
306 | return KTextEditor::Range( KTextEditor::Cursor( line, col ), position ); |
307 | } |
308 | //END KateWordCompletionModel |
309 | |
310 | |
311 | //BEGIN KateWordCompletionView |
312 | struct KateWordCompletionViewPrivate |
313 | { |
314 | KTextEditor::MovingRange* liRange; // range containing last inserted text |
315 | KTextEditor::Range dcRange; // current range to be completed by directional completion |
316 | KTextEditor::Cursor dcCursor; // directional completion search cursor |
317 | QRegExp re; // hrm |
318 | int directionalPos; // be able to insert "" at the correct time |
319 | bool isCompleting; // true when the directional completion is doing a completion |
320 | }; |
321 | |
322 | KateWordCompletionView::KateWordCompletionView( KTextEditor::View *view, KActionCollection* ac ) |
323 | : QObject( view ), |
324 | m_view( view ), |
325 | m_dWCompletionModel( KateGlobal::self()->wordCompletionModel() ), |
326 | d( new KateWordCompletionViewPrivate ) |
327 | { |
328 | d->isCompleting = false; |
329 | d->dcRange = KTextEditor::Range::invalid(); |
330 | |
331 | d->liRange = static_cast<KateDocument*>(m_view->document())->newMovingRange(KTextEditor::Range::invalid(), KTextEditor::MovingRange::DoNotExpand); |
332 | |
333 | KColorScheme colors(QPalette::Active); |
334 | KTextEditor::Attribute::Ptr a = KTextEditor::Attribute::Ptr( new KTextEditor::Attribute() ); |
335 | a->setBackground( colors.background(KColorScheme::ActiveBackground) ); |
336 | a->setForeground( colors.foreground(KColorScheme::ActiveText) ); // ### this does 0 |
337 | d->liRange->setAttribute( a ); |
338 | |
339 | KTextEditor::CodeCompletionInterface *cci = qobject_cast<KTextEditor::CodeCompletionInterface *>(view); |
340 | |
341 | KAction *action; |
342 | |
343 | if (cci) |
344 | { |
345 | cci->registerCompletionModel( m_dWCompletionModel ); |
346 | |
347 | action = new KAction( i18n("Shell Completion" ), this ); |
348 | ac->addAction( "doccomplete_sh" , action ); |
349 | connect( action, SIGNAL(triggered()), this, SLOT(shellComplete()) ); |
350 | } |
351 | |
352 | |
353 | action = new KAction( i18n("Reuse Word Above" ), this ); |
354 | ac->addAction( "doccomplete_bw" , action ); |
355 | action->setShortcut( Qt::CTRL+Qt::Key_8 ); |
356 | connect( action, SIGNAL(triggered()), this, SLOT(completeBackwards()) ); |
357 | |
358 | action = new KAction( i18n("Reuse Word Below" ), this ); |
359 | ac->addAction( "doccomplete_fw" , action ); |
360 | action->setShortcut( Qt::CTRL+Qt::Key_9 ); |
361 | connect( action, SIGNAL(triggered()), this, SLOT(completeForwards()) ); |
362 | } |
363 | |
364 | KateWordCompletionView::~KateWordCompletionView() |
365 | { |
366 | KTextEditor::CodeCompletionInterface *cci = qobject_cast<KTextEditor::CodeCompletionInterface *>(m_view); |
367 | |
368 | if (cci) cci->unregisterCompletionModel(m_dWCompletionModel); |
369 | |
370 | delete d; |
371 | } |
372 | |
373 | void KateWordCompletionView::completeBackwards() |
374 | { |
375 | complete( false ); |
376 | } |
377 | |
378 | void KateWordCompletionView::completeForwards() |
379 | { |
380 | complete(); |
381 | } |
382 | |
383 | // Pop up the editors completion list if applicable |
384 | void KateWordCompletionView::() |
385 | { |
386 | kDebug( 13040 ) << "entered ..." ; |
387 | KTextEditor::Range r = range(); |
388 | |
389 | KTextEditor::CodeCompletionInterface *cci = qobject_cast<KTextEditor::CodeCompletionInterface *>( m_view ); |
390 | if(!cci || cci->isCompletionActive()) |
391 | return; |
392 | |
393 | m_dWCompletionModel->saveMatches( m_view, r ); |
394 | |
395 | kDebug( 13040 ) << "after save matches ..." ; |
396 | |
397 | if ( ! m_dWCompletionModel->rowCount(QModelIndex()) ) return; |
398 | |
399 | cci->startCompletion( r, m_dWCompletionModel ); |
400 | } |
401 | |
402 | // Contributed by <brain@hdsnet.hu> |
403 | void KateWordCompletionView::shellComplete() |
404 | { |
405 | KTextEditor::Range r = range(); |
406 | |
407 | QStringList matches = m_dWCompletionModel->allMatches( m_view, r ); |
408 | |
409 | if (matches.size() == 0) |
410 | return; |
411 | |
412 | QString partial = findLongestUnique( matches, r.columnWidth() ); |
413 | |
414 | if ( ! partial.length() ) |
415 | popupCompletionList(); |
416 | |
417 | else |
418 | { |
419 | m_view->document()->insertText( r.end(), partial.mid( r.columnWidth() ) ); |
420 | d->liRange->setView(m_view); |
421 | d->liRange->setRange( KTextEditor::Range( r.end(), partial.length() - r.columnWidth() ) ); |
422 | connect( m_view, SIGNAL(cursorPositionChanged(KTextEditor::View*,KTextEditor::Cursor)), this, SLOT(slotCursorMoved()) ); |
423 | } |
424 | } |
425 | |
426 | // Do one completion, searching in the desired direction, |
427 | // if possible |
428 | void KateWordCompletionView::complete( bool fw ) |
429 | { |
430 | KTextEditor::Range r = range(); |
431 | |
432 | int inc = fw ? 1 : -1; |
433 | KTextEditor::Document *doc = m_view->document(); |
434 | |
435 | if ( d->dcRange.isValid() ) |
436 | { |
437 | //kDebug( 13040 )<<"CONTINUE "<<d->dcRange; |
438 | // this is a repeted activation |
439 | |
440 | // if we are back to where we started, reset. |
441 | if ( ( fw && d->directionalPos == -1 ) || |
442 | ( !fw && d->directionalPos == 1 ) ) |
443 | { |
444 | const int spansColumns = d->liRange->end().column() - d->liRange->start().column(); |
445 | if ( spansColumns > 0 ) |
446 | doc->removeText( *d->liRange ); |
447 | |
448 | d->liRange->setRange( KTextEditor::Range::invalid() ); |
449 | d->dcCursor = r.end(); |
450 | d->directionalPos = 0; |
451 | |
452 | return; |
453 | } |
454 | |
455 | if ( fw ) { |
456 | const int spansColumns = d->liRange->end().column() - d->liRange->start().column(); |
457 | d->dcCursor.setColumn( d->dcCursor.column() + spansColumns ); |
458 | } |
459 | |
460 | d->directionalPos += inc; |
461 | } |
462 | else // new completion, reset all |
463 | { |
464 | //kDebug( 13040 )<<"RESET FOR NEW"; |
465 | d->dcRange = r; |
466 | d->liRange->setRange( KTextEditor::Range::invalid() ); |
467 | d->dcCursor = r.start(); |
468 | d->directionalPos = inc; |
469 | |
470 | d->liRange->setView( m_view ); |
471 | |
472 | connect( m_view, SIGNAL(cursorPositionChanged(KTextEditor::View*,KTextEditor::Cursor)), this, SLOT(slotCursorMoved()) ); |
473 | |
474 | } |
475 | |
476 | d->re.setPattern( "\\b" + doc->text( d->dcRange ) + "(\\w+)" ); |
477 | int pos ( 0 ); |
478 | QString ln = doc->line( d->dcCursor.line() ); |
479 | |
480 | while ( true ) |
481 | { |
482 | //kDebug( 13040 )<<"SEARCHING FOR "<<d->re.pattern()<<" "<<ln<<" at "<<d->dcCursor; |
483 | pos = fw ? |
484 | d->re.indexIn( ln, d->dcCursor.column() ) : |
485 | d->re.lastIndexIn( ln, d->dcCursor.column() ); |
486 | |
487 | if ( pos > -1 ) // we matched a word |
488 | { |
489 | //kDebug( 13040 )<<"USABLE MATCH"; |
490 | QString m = d->re.cap( 1 ); |
491 | if ( m != doc->text( *d->liRange ) && (d->dcCursor.line() != d->dcRange.start().line() || pos != d->dcRange.start().column() ) ) |
492 | { |
493 | // we got good a match! replace text and return. |
494 | d->isCompleting = true; |
495 | KTextEditor::Range replaceRange(d->liRange->toRange()); |
496 | if (!replaceRange.isValid()) { |
497 | replaceRange.setRange(r.end(), r.end()); |
498 | } |
499 | doc->replaceText( replaceRange, m ); |
500 | d->liRange->setRange( KTextEditor::Range( d->dcRange.end(), m.length() ) ); |
501 | |
502 | d->dcCursor.setColumn( pos ); // for next try |
503 | |
504 | d->isCompleting = false; |
505 | return; |
506 | } |
507 | |
508 | // equal to last one, continue |
509 | else |
510 | { |
511 | //kDebug( 13040 )<<"SKIPPING, EQUAL MATCH"; |
512 | d->dcCursor.setColumn( pos ); // for next try |
513 | |
514 | if ( fw ) |
515 | d->dcCursor.setColumn( pos + m.length() ); |
516 | |
517 | else |
518 | { |
519 | if ( pos == 0 ) |
520 | { |
521 | if ( d->dcCursor.line() > 0 ) |
522 | { |
523 | int l = d->dcCursor.line() + inc; |
524 | ln = doc->line( l ); |
525 | d->dcCursor.setPosition( l, ln.length() ); |
526 | } |
527 | else |
528 | { |
529 | KNotification::beep(); |
530 | return; |
531 | } |
532 | } |
533 | |
534 | else |
535 | d->dcCursor.setColumn( d->dcCursor.column()-1 ); |
536 | } |
537 | } |
538 | } |
539 | |
540 | else // no match |
541 | { |
542 | //kDebug( 13040 )<<"NO MATCH"; |
543 | if ( (! fw && d->dcCursor.line() == 0 ) || ( fw && d->dcCursor.line() >= doc->lines() ) ) |
544 | { |
545 | KNotification::beep(); |
546 | return; |
547 | } |
548 | |
549 | int l = d->dcCursor.line() + inc; |
550 | ln = doc->line( l ); |
551 | d->dcCursor.setPosition( l, fw ? 0 : ln.length() ); |
552 | } |
553 | } // while true |
554 | } |
555 | |
556 | void KateWordCompletionView::slotCursorMoved() |
557 | { |
558 | if ( d->isCompleting) return; |
559 | |
560 | d->dcRange = KTextEditor::Range::invalid(); |
561 | |
562 | disconnect( m_view, SIGNAL(cursorPositionChanged(KTextEditor::View*,KTextEditor::Cursor)), this, SLOT(slotCursorMoved()) ); |
563 | |
564 | d->liRange->setView(0); |
565 | d->liRange->setRange(KTextEditor::Range::invalid()); |
566 | } |
567 | |
568 | // Contributed by <brain@hdsnet.hu> FIXME |
569 | QString KateWordCompletionView::findLongestUnique( const QStringList &matches, int lead ) const |
570 | { |
571 | QString partial = matches.first(); |
572 | |
573 | foreach ( const QString& current, matches ) |
574 | { |
575 | if ( !current.startsWith( partial ) ) |
576 | { |
577 | while( partial.length() > lead ) |
578 | { |
579 | partial.remove( partial.length() - 1, 1 ); |
580 | if ( current.startsWith( partial ) ) |
581 | break; |
582 | } |
583 | |
584 | if ( partial.length() == lead ) |
585 | return QString(); |
586 | } |
587 | } |
588 | |
589 | return partial; |
590 | } |
591 | |
592 | // Return the string to complete (the letters behind the cursor) |
593 | QString KateWordCompletionView::word() const |
594 | { |
595 | return m_view->document()->text( range() ); |
596 | } |
597 | |
598 | // Return the range containing the word behind the cursor |
599 | KTextEditor::Range KateWordCompletionView::range() const |
600 | { |
601 | return m_dWCompletionModel->completionRange(m_view, m_view->cursorPosition()); |
602 | } |
603 | //END |
604 | |
605 | #include "katewordcompletion.moc" |
606 | // kate: space-indent on; indent-width 2; replace-tabs on; mixed-indent off; |
607 | |