kde-workspace/kate/part/completion/katewordcompletion.cpp
Ivailo Monev bc155644ac kate: log KateWordCompletionView debug messages to Kate (Code Completion) area
13040 is reserved for Kate (Plugins)

Signed-off-by: Ivailo Monev <xakepa10@gmail.com>
2022-10-22 21:32:08 +03:00

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 &currentCompletion) {
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;