mirror of
https://bitbucket.org/smil3y/kde-workspace.git
synced 2025-02-24 19:02:51 +00:00
606 lines
18 KiB
C++
606 lines
18 KiB
C++
/* This file is part of the KDE libraries and the Kate part.
|
|
*
|
|
* Copyright (C) 2003 Anders Lund <anders.lund@lund.tdcadsl.dk>
|
|
* Copyright (C) 2010 Christoph Cullmann <cullmann@kde.org>
|
|
*
|
|
* This library is free software; you can redistribute it and/or
|
|
* modify it under the terms of the GNU Library General Public
|
|
* License as published by the Free Software Foundation; either
|
|
* version 2 of the License, or (at your option) any later version.
|
|
*
|
|
* This library is distributed in the hope that it will be useful,
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
|
* Library General Public License for more details.
|
|
*
|
|
* You should have received a copy of the GNU Library General Public License
|
|
* along with this library; see the file COPYING.LIB. If not, write to
|
|
* the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
|
|
* Boston, MA 02110-1301, USA.
|
|
*/
|
|
|
|
//BEGIN includes
|
|
#include "katewordcompletion.h"
|
|
#include "kateview.h"
|
|
#include "kateconfig.h"
|
|
#include "katedocument.h"
|
|
#include "kateglobal.h"
|
|
#include <katehighlight.h>
|
|
#include <katehighlighthelpers.h>
|
|
|
|
#include <ktexteditor/variableinterface.h>
|
|
#include <ktexteditor/movingrange.h>
|
|
#include <ktexteditor/range.h>
|
|
|
|
#include <kconfig.h>
|
|
#include <kdialog.h>
|
|
#include <kpluginfactory.h>
|
|
#include <klocale.h>
|
|
#include <kaction.h>
|
|
#include <kactioncollection.h>
|
|
#include <knotification.h>
|
|
#include <kparts/part.h>
|
|
#include <kiconloader.h>
|
|
#include <kpagedialog.h>
|
|
#include <kpagewidgetmodel.h>
|
|
#include <ktoggleaction.h>
|
|
#include <kconfiggroup.h>
|
|
#include <kcolorscheme.h>
|
|
#include <kaboutdata.h>
|
|
|
|
#include <QtCore/QRegExp>
|
|
#include <QtCore/QString>
|
|
#include <QtCore/QSet>
|
|
#include <QtGui/QSpinBox>
|
|
#include <QtGui/QLabel>
|
|
#include <QtGui/QLayout>
|
|
|
|
#include <kvbox.h>
|
|
#include <QtGui/QCheckBox>
|
|
|
|
#include <kdebug.h>
|
|
//END
|
|
|
|
/// Amount of characters the document may have to enable automatic invocation (1MB)
|
|
static const int autoInvocationMaxFilesize = 1000000;
|
|
|
|
//BEGIN KateWordCompletionModel
|
|
KateWordCompletionModel::KateWordCompletionModel( QObject *parent )
|
|
: CodeCompletionModel2( parent ), m_automatic(false)
|
|
{
|
|
setHasGroups(false);
|
|
}
|
|
|
|
KateWordCompletionModel::~KateWordCompletionModel()
|
|
{
|
|
}
|
|
|
|
void KateWordCompletionModel::saveMatches( KTextEditor::View* view,
|
|
const KTextEditor::Range& range)
|
|
{
|
|
m_matches = allMatches( view, range );
|
|
m_matches.sort();
|
|
}
|
|
|
|
QVariant KateWordCompletionModel::data(const QModelIndex& index, int role) const
|
|
{
|
|
if( role == UnimportantItemRole )
|
|
return QVariant(true);
|
|
if( role == InheritanceDepth )
|
|
return 10000;
|
|
|
|
if( !index.parent().isValid() ) {
|
|
//It is the group header
|
|
switch ( role )
|
|
{
|
|
case Qt::DisplayRole:
|
|
return i18n("Auto Word Completion");
|
|
case GroupRole:
|
|
return Qt::DisplayRole;
|
|
}
|
|
}
|
|
|
|
if( index.column() == KTextEditor::CodeCompletionModel::Name && role == Qt::DisplayRole )
|
|
return m_matches.at( index.row() );
|
|
|
|
if( index.column() == KTextEditor::CodeCompletionModel::Icon && role == Qt::DecorationRole ) {
|
|
static QIcon icon(KIcon("insert-text").pixmap(QSize(16, 16)));
|
|
return icon;
|
|
}
|
|
|
|
return QVariant();
|
|
}
|
|
|
|
QModelIndex KateWordCompletionModel::parent(const QModelIndex& index) const
|
|
{
|
|
if(index.internalId())
|
|
return createIndex(0, 0, 0);
|
|
else
|
|
return QModelIndex();
|
|
}
|
|
|
|
QModelIndex KateWordCompletionModel::index(int row, int column, const QModelIndex& parent) const
|
|
{
|
|
if( !parent.isValid()) {
|
|
if(row == 0)
|
|
return createIndex(row, column, 0);
|
|
else
|
|
return QModelIndex();
|
|
|
|
}else if(parent.parent().isValid())
|
|
return QModelIndex();
|
|
|
|
|
|
if (row < 0 || row >= m_matches.count() || column < 0 || column >= ColumnCount )
|
|
return QModelIndex();
|
|
|
|
return createIndex(row, column, 1);
|
|
}
|
|
|
|
int KateWordCompletionModel::rowCount ( const QModelIndex & parent ) const
|
|
{
|
|
if( !parent.isValid() && !m_matches.isEmpty() )
|
|
return 1; //One root node to define the custom group
|
|
else if(parent.parent().isValid())
|
|
return 0; //Completion-items have no children
|
|
else
|
|
return m_matches.count();
|
|
}
|
|
|
|
|
|
bool KateWordCompletionModel::shouldStartCompletion(KTextEditor::View* view, const QString &insertedText, bool userInsertion, const KTextEditor::Cursor &position)
|
|
{
|
|
if (!userInsertion) return false;
|
|
if (insertedText.isEmpty())
|
|
return false;
|
|
|
|
|
|
KateView *v = qobject_cast<KateView*> (view);
|
|
|
|
if (view->document()->totalCharacters() > autoInvocationMaxFilesize) {
|
|
// Disable automatic invocation for files larger than 1MB (see benchmarks)
|
|
return false;
|
|
}
|
|
|
|
const QString& text = view->document()->line(position.line()).left(position.column());
|
|
const uint check = v->config()->wordCompletionMinimalWordLength();
|
|
// Start completion immediately if min. word size is zero
|
|
if (!check) return true;
|
|
// Otherwise, check if user has typed long enough text...
|
|
const int start = text.length();
|
|
const int end = start - check;
|
|
if (end < 0) return false;
|
|
for (int i = start - 1; i >= end; i--) {
|
|
const QChar c = text.at(i);
|
|
if (!(c.isLetter() || (c.isNumber()) || c=='_')) return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
bool KateWordCompletionModel::shouldAbortCompletion(KTextEditor::View* view, const KTextEditor::Range &range, const QString ¤tCompletion) {
|
|
|
|
if (m_automatic) {
|
|
KateView *v = qobject_cast<KateView*> (view);
|
|
if (currentCompletion.length()<v->config()->wordCompletionMinimalWordLength()) return true;
|
|
}
|
|
|
|
return CodeCompletionModelControllerInterface4::shouldAbortCompletion(view,range,currentCompletion);
|
|
}
|
|
|
|
|
|
|
|
void KateWordCompletionModel::completionInvoked(KTextEditor::View* view, const KTextEditor::Range& range, InvocationType it)
|
|
{
|
|
m_automatic = it == AutomaticInvocation;
|
|
saveMatches( view, range );
|
|
}
|
|
|
|
|
|
/**
|
|
* Scan throughout the entire document for possible completions,
|
|
* ignoring any dublets and words shorter than configured and/or
|
|
* reasonable minimum length.
|
|
*/
|
|
QStringList KateWordCompletionModel::allMatches( KTextEditor::View *view, const KTextEditor::Range &range ) const
|
|
{
|
|
QSet<QString> result;
|
|
const int minWordSize = qMax(2, qobject_cast<KateView*>(view)->config()->wordCompletionMinimalWordLength());
|
|
const int lines = view->document()->lines();
|
|
for ( int line = 0; line < lines; line++ ) {
|
|
const QString& text = view->document()->line(line);
|
|
int wordBegin = 0;
|
|
int offset = 0;
|
|
const int end = text.size();
|
|
while ( offset < end ) {
|
|
const QChar c = text.at(offset);
|
|
// increment offset when at line end, so we take the last character too
|
|
if ( ( ! c.isLetterOrNumber() && c != '_' ) || (offset == end - 1 && offset++) ) {
|
|
if ( offset - wordBegin > minWordSize && ( line != range.end().line() || offset != range.end().column() ) ) {
|
|
result.insert(text.mid(wordBegin, offset - wordBegin));
|
|
}
|
|
wordBegin = offset + 1;
|
|
}
|
|
if ( c.isSpace() ) {
|
|
wordBegin = offset + 1;
|
|
}
|
|
offset += 1;
|
|
}
|
|
}
|
|
return result.values();
|
|
}
|
|
|
|
void KateWordCompletionModel::executeCompletionItem2(
|
|
KTextEditor::Document* document
|
|
, const KTextEditor::Range& word
|
|
, const QModelIndex& index
|
|
) const
|
|
{
|
|
KateView *v = qobject_cast<KateView*> (document->activeView());
|
|
if (v->config()->wordCompletionRemoveTail())
|
|
{
|
|
int tailStart = word.end().column();
|
|
const QString& line = document->line(word.end().line());
|
|
int tailEnd = line.length();
|
|
for (int i = word.end().column(); i < tailEnd; ++i)
|
|
{
|
|
// Letters, numbers and underscore are part of a word!
|
|
/// \todo Introduce configurable \e word-separators??
|
|
if (!line[i].isLetterOrNumber() && line[i] != '_')
|
|
{
|
|
tailEnd = i;
|
|
}
|
|
}
|
|
|
|
int sizeDiff = m_matches.at(index.row()).size() - (word.end().column() - word.start().column());
|
|
|
|
tailStart += sizeDiff;
|
|
tailEnd += sizeDiff;
|
|
|
|
KTextEditor::Range tail = word;
|
|
tail.start().setColumn(tailStart);
|
|
tail.end().setColumn(tailEnd);
|
|
|
|
document->replaceText(word, m_matches.at(index.row()));
|
|
v->doc()->editEnd();
|
|
v->doc()->editStart();
|
|
document->replaceText(tail, "");
|
|
}
|
|
else
|
|
{
|
|
document->replaceText(word, m_matches.at(index.row()));
|
|
}
|
|
}
|
|
|
|
KTextEditor::CodeCompletionModelControllerInterface::MatchReaction KateWordCompletionModel::matchingItem(const QModelIndex& /*matched*/)
|
|
{
|
|
return HideListIfAutomaticInvocation;
|
|
}
|
|
|
|
bool KateWordCompletionModel::shouldHideItemsWithEqualNames() const
|
|
{
|
|
// We don't want word-completion items if the same items
|
|
// are available through more sophisticated completion models
|
|
return true;
|
|
}
|
|
|
|
// Return the range containing the word left of the cursor
|
|
KTextEditor::Range KateWordCompletionModel::completionRange(KTextEditor::View* view, const KTextEditor::Cursor &position)
|
|
{
|
|
int line = position.line();
|
|
int col = position.column();
|
|
|
|
KTextEditor::Document *doc = view->document();
|
|
while ( col > 0 )
|
|
{
|
|
const QChar c = ( doc->character( KTextEditor::Cursor( line, col-1 ) ) );
|
|
if ( c.isLetterOrNumber() || c.isMark() || c == '_' )
|
|
{
|
|
col--;
|
|
continue;
|
|
}
|
|
|
|
break;
|
|
}
|
|
|
|
return KTextEditor::Range( KTextEditor::Cursor( line, col ), position );
|
|
}
|
|
//END KateWordCompletionModel
|
|
|
|
|
|
//BEGIN KateWordCompletionView
|
|
struct KateWordCompletionViewPrivate
|
|
{
|
|
KTextEditor::MovingRange* liRange; // range containing last inserted text
|
|
KTextEditor::Range dcRange; // current range to be completed by directional completion
|
|
KTextEditor::Cursor dcCursor; // directional completion search cursor
|
|
QRegExp re; // hrm
|
|
int directionalPos; // be able to insert "" at the correct time
|
|
bool isCompleting; // true when the directional completion is doing a completion
|
|
};
|
|
|
|
KateWordCompletionView::KateWordCompletionView( KTextEditor::View *view, KActionCollection* ac )
|
|
: QObject( view ),
|
|
m_view( view ),
|
|
m_dWCompletionModel( KateGlobal::self()->wordCompletionModel() ),
|
|
d( new KateWordCompletionViewPrivate )
|
|
{
|
|
d->isCompleting = false;
|
|
d->dcRange = KTextEditor::Range::invalid();
|
|
|
|
d->liRange = static_cast<KateDocument*>(m_view->document())->newMovingRange(KTextEditor::Range::invalid(), KTextEditor::MovingRange::DoNotExpand);
|
|
|
|
KColorScheme colors(QPalette::Active);
|
|
KTextEditor::Attribute::Ptr a = KTextEditor::Attribute::Ptr( new KTextEditor::Attribute() );
|
|
a->setBackground( colors.background(KColorScheme::ActiveBackground) );
|
|
a->setForeground( colors.foreground(KColorScheme::ActiveText) ); // ### this does 0
|
|
d->liRange->setAttribute( a );
|
|
|
|
KTextEditor::CodeCompletionInterface *cci = qobject_cast<KTextEditor::CodeCompletionInterface *>(view);
|
|
|
|
KAction *action;
|
|
|
|
if (cci)
|
|
{
|
|
cci->registerCompletionModel( m_dWCompletionModel );
|
|
|
|
action = new KAction( i18n("Shell Completion"), this );
|
|
ac->addAction( "doccomplete_sh", action );
|
|
connect( action, SIGNAL(triggered()), this, SLOT(shellComplete()) );
|
|
}
|
|
|
|
|
|
action = new KAction( i18n("Reuse Word Above"), this );
|
|
ac->addAction( "doccomplete_bw", action );
|
|
action->setShortcut( Qt::CTRL+Qt::Key_8 );
|
|
connect( action, SIGNAL(triggered()), this, SLOT(completeBackwards()) );
|
|
|
|
action = new KAction( i18n("Reuse Word Below"), this );
|
|
ac->addAction( "doccomplete_fw", action );
|
|
action->setShortcut( Qt::CTRL+Qt::Key_9 );
|
|
connect( action, SIGNAL(triggered()), this, SLOT(completeForwards()) );
|
|
}
|
|
|
|
KateWordCompletionView::~KateWordCompletionView()
|
|
{
|
|
KTextEditor::CodeCompletionInterface *cci = qobject_cast<KTextEditor::CodeCompletionInterface *>(m_view);
|
|
|
|
if (cci) cci->unregisterCompletionModel(m_dWCompletionModel);
|
|
|
|
delete d;
|
|
}
|
|
|
|
void KateWordCompletionView::completeBackwards()
|
|
{
|
|
complete( false );
|
|
}
|
|
|
|
void KateWordCompletionView::completeForwards()
|
|
{
|
|
complete();
|
|
}
|
|
|
|
// Pop up the editors completion list if applicable
|
|
void KateWordCompletionView::popupCompletionList()
|
|
{
|
|
kDebug( 13035 ) << "entered ...";
|
|
KTextEditor::Range r = range();
|
|
|
|
KTextEditor::CodeCompletionInterface *cci = qobject_cast<KTextEditor::CodeCompletionInterface *>( m_view );
|
|
if(!cci || cci->isCompletionActive())
|
|
return;
|
|
|
|
m_dWCompletionModel->saveMatches( m_view, r );
|
|
|
|
kDebug( 13035 ) << "after save matches ...";
|
|
|
|
if ( ! m_dWCompletionModel->rowCount(QModelIndex()) ) return;
|
|
|
|
cci->startCompletion( r, m_dWCompletionModel );
|
|
}
|
|
|
|
// Contributed by <brain@hdsnet.hu>
|
|
void KateWordCompletionView::shellComplete()
|
|
{
|
|
KTextEditor::Range r = range();
|
|
|
|
QStringList matches = m_dWCompletionModel->allMatches( m_view, r );
|
|
|
|
if (matches.size() == 0)
|
|
return;
|
|
|
|
QString partial = findLongestUnique( matches, r.columnWidth() );
|
|
|
|
if ( ! partial.length() )
|
|
popupCompletionList();
|
|
|
|
else
|
|
{
|
|
m_view->document()->insertText( r.end(), partial.mid( r.columnWidth() ) );
|
|
d->liRange->setView(m_view);
|
|
d->liRange->setRange( KTextEditor::Range( r.end(), partial.length() - r.columnWidth() ) );
|
|
connect( m_view, SIGNAL(cursorPositionChanged(KTextEditor::View*,KTextEditor::Cursor)), this, SLOT(slotCursorMoved()) );
|
|
}
|
|
}
|
|
|
|
// Do one completion, searching in the desired direction,
|
|
// if possible
|
|
void KateWordCompletionView::complete( bool fw )
|
|
{
|
|
KTextEditor::Range r = range();
|
|
|
|
int inc = fw ? 1 : -1;
|
|
KTextEditor::Document *doc = m_view->document();
|
|
|
|
if ( d->dcRange.isValid() )
|
|
{
|
|
//kDebug( 13035 )<<"CONTINUE "<<d->dcRange;
|
|
// this is a repeted activation
|
|
|
|
// if we are back to where we started, reset.
|
|
if ( ( fw && d->directionalPos == -1 ) ||
|
|
( !fw && d->directionalPos == 1 ) )
|
|
{
|
|
const int spansColumns = d->liRange->end().column() - d->liRange->start().column();
|
|
if ( spansColumns > 0 )
|
|
doc->removeText( d->liRange->toRange() );
|
|
|
|
d->liRange->setRange( KTextEditor::Range::invalid() );
|
|
d->dcCursor = r.end();
|
|
d->directionalPos = 0;
|
|
|
|
return;
|
|
}
|
|
|
|
if ( fw ) {
|
|
const int spansColumns = d->liRange->end().column() - d->liRange->start().column();
|
|
d->dcCursor.setColumn( d->dcCursor.column() + spansColumns );
|
|
}
|
|
|
|
d->directionalPos += inc;
|
|
}
|
|
else // new completion, reset all
|
|
{
|
|
//kDebug( 13035 )<<"RESET FOR NEW";
|
|
d->dcRange = r;
|
|
d->liRange->setRange( KTextEditor::Range::invalid() );
|
|
d->dcCursor = r.start();
|
|
d->directionalPos = inc;
|
|
|
|
d->liRange->setView( m_view );
|
|
|
|
connect( m_view, SIGNAL(cursorPositionChanged(KTextEditor::View*,KTextEditor::Cursor)), this, SLOT(slotCursorMoved()) );
|
|
|
|
}
|
|
|
|
d->re.setPattern( "\\b" + doc->text( d->dcRange ) + "(\\w+)" );
|
|
int pos ( 0 );
|
|
QString ln = doc->line( d->dcCursor.line() );
|
|
|
|
while ( true )
|
|
{
|
|
//kDebug( 13035 )<<"SEARCHING FOR "<<d->re.pattern()<<" "<<ln<<" at "<<d->dcCursor;
|
|
pos = fw ?
|
|
d->re.indexIn( ln, d->dcCursor.column() ) :
|
|
d->re.lastIndexIn( ln, d->dcCursor.column() );
|
|
|
|
if ( pos > -1 ) // we matched a word
|
|
{
|
|
//kDebug( 13035 )<<"USABLE MATCH";
|
|
QString m = d->re.cap( 1 );
|
|
if ( m != doc->text( d->liRange->toRange() ) && (d->dcCursor.line() != d->dcRange.start().line() || pos != d->dcRange.start().column() ) )
|
|
{
|
|
// we got good a match! replace text and return.
|
|
d->isCompleting = true;
|
|
KTextEditor::Range replaceRange(d->liRange->toRange());
|
|
if (!replaceRange.isValid()) {
|
|
replaceRange.setRange(r.end(), r.end());
|
|
}
|
|
doc->replaceText( replaceRange, m );
|
|
d->liRange->setRange( KTextEditor::Range( d->dcRange.end(), m.length() ) );
|
|
|
|
d->dcCursor.setColumn( pos ); // for next try
|
|
|
|
d->isCompleting = false;
|
|
return;
|
|
}
|
|
|
|
// equal to last one, continue
|
|
else
|
|
{
|
|
//kDebug( 13035 )<<"SKIPPING, EQUAL MATCH";
|
|
d->dcCursor.setColumn( pos ); // for next try
|
|
|
|
if ( fw )
|
|
d->dcCursor.setColumn( pos + m.length() );
|
|
|
|
else
|
|
{
|
|
if ( pos == 0 )
|
|
{
|
|
if ( d->dcCursor.line() > 0 )
|
|
{
|
|
int l = d->dcCursor.line() + inc;
|
|
ln = doc->line( l );
|
|
d->dcCursor.setPosition( l, ln.length() );
|
|
}
|
|
else
|
|
{
|
|
KNotification::beep();
|
|
return;
|
|
}
|
|
}
|
|
|
|
else
|
|
d->dcCursor.setColumn( d->dcCursor.column()-1 );
|
|
}
|
|
}
|
|
}
|
|
|
|
else // no match
|
|
{
|
|
//kDebug( 13035 )<<"NO MATCH";
|
|
if ( (! fw && d->dcCursor.line() == 0 ) || ( fw && d->dcCursor.line() >= doc->lines() ) )
|
|
{
|
|
KNotification::beep();
|
|
return;
|
|
}
|
|
|
|
int l = d->dcCursor.line() + inc;
|
|
ln = doc->line( l );
|
|
d->dcCursor.setPosition( l, fw ? 0 : ln.length() );
|
|
}
|
|
} // while true
|
|
}
|
|
|
|
void KateWordCompletionView::slotCursorMoved()
|
|
{
|
|
if ( d->isCompleting) return;
|
|
|
|
d->dcRange = KTextEditor::Range::invalid();
|
|
|
|
disconnect( m_view, SIGNAL(cursorPositionChanged(KTextEditor::View*,KTextEditor::Cursor)), this, SLOT(slotCursorMoved()) );
|
|
|
|
d->liRange->setView(0);
|
|
d->liRange->setRange(KTextEditor::Range::invalid());
|
|
}
|
|
|
|
// Contributed by <brain@hdsnet.hu> FIXME
|
|
QString KateWordCompletionView::findLongestUnique( const QStringList &matches, int lead ) const
|
|
{
|
|
QString partial = matches.first();
|
|
|
|
foreach ( const QString& current, matches )
|
|
{
|
|
if ( !current.startsWith( partial ) )
|
|
{
|
|
while( partial.length() > lead )
|
|
{
|
|
partial.remove( partial.length() - 1, 1 );
|
|
if ( current.startsWith( partial ) )
|
|
break;
|
|
}
|
|
|
|
if ( partial.length() == lead )
|
|
return QString();
|
|
}
|
|
}
|
|
|
|
return partial;
|
|
}
|
|
|
|
// Return the string to complete (the letters behind the cursor)
|
|
QString KateWordCompletionView::word() const
|
|
{
|
|
return m_view->document()->text( range() );
|
|
}
|
|
|
|
// Return the range containing the word behind the cursor
|
|
KTextEditor::Range KateWordCompletionView::range() const
|
|
{
|
|
return m_dWCompletionModel->completionRange(m_view, m_view->cursorPosition());
|
|
}
|
|
//END
|
|
|
|
#include "moc_katewordcompletion.cpp"
|
|
// kate: space-indent on; indent-width 2; replace-tabs on; mixed-indent off;
|