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)
65static const int autoInvocationMaxFilesize = 1000000;
66
67//BEGIN KateWordCompletionModel
68KateWordCompletionModel::KateWordCompletionModel( QObject *parent )
69 : CodeCompletionModel2( parent ), m_automatic(false)
70{
71 setHasGroups(false);
72}
73
74KateWordCompletionModel::~KateWordCompletionModel()
75{
76}
77
78void KateWordCompletionModel::saveMatches( KTextEditor::View* view,
79 const KTextEditor::Range& range)
80{
81 m_matches = allMatches( view, range );
82 m_matches.sort();
83}
84
85QVariant 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
114QModelIndex KateWordCompletionModel::parent(const QModelIndex& index) const
115{
116 if(index.internalId())
117 return createIndex(0, 0, 0);
118 else
119 return QModelIndex();
120}
121
122QModelIndex 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
140int 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
151bool 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
181bool KateWordCompletionModel::shouldAbortCompletion(KTextEditor::View* view, const KTextEditor::Range &range, const QString &currentCompletion) {
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
193void 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 */
205QStringList 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
233void 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
275KTextEditor::CodeCompletionModelControllerInterface3::MatchReaction KateWordCompletionModel::matchingItem(const QModelIndex& /*matched*/)
276{
277 return HideListIfAutomaticInvocation;
278}
279
280bool 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
288KTextEditor::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
312struct 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
322KateWordCompletionView::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
364KateWordCompletionView::~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
373void KateWordCompletionView::completeBackwards()
374{
375 complete( false );
376}
377
378void KateWordCompletionView::completeForwards()
379{
380 complete();
381}
382
383// Pop up the editors completion list if applicable
384void KateWordCompletionView::popupCompletionList()
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>
403void 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
428void 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
556void 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
569QString 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)
593QString KateWordCompletionView::word() const
594{
595 return m_view->document()->text( range() );
596}
597
598// Return the range containing the word behind the cursor
599KTextEditor::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