File indexing completed on 2024-04-28 15:30:22

0001 /*
0002     SPDX-FileCopyrightText: 2003 Anders Lund <anders.lund@lund.tdcadsl.dk>
0003     SPDX-FileCopyrightText: 2010 Christoph Cullmann <cullmann@kde.org>
0004 
0005     SPDX-License-Identifier: LGPL-2.0-or-later
0006 */
0007 
0008 // BEGIN includes
0009 #include "katewordcompletion.h"
0010 #include "kateconfig.h"
0011 #include "katedocument.h"
0012 #include "kateglobal.h"
0013 #include "katerenderer.h"
0014 #include "kateview.h"
0015 
0016 #include <ktexteditor/movingrange.h>
0017 #include <ktexteditor/range.h>
0018 
0019 #include <KAboutData>
0020 #include <KActionCollection>
0021 #include <KConfigGroup>
0022 #include <KLocalizedString>
0023 #include <KPageDialog>
0024 #include <KPageWidgetModel>
0025 #include <KParts/Part>
0026 #include <KToggleAction>
0027 
0028 #include <QAction>
0029 #include <QCheckBox>
0030 #include <QLabel>
0031 #include <QLayout>
0032 #include <QRegularExpression>
0033 #include <QSet>
0034 #include <QSpinBox>
0035 #include <QString>
0036 
0037 // END
0038 
0039 /// Amount of characters the document may have to enable automatic invocation (1MB)
0040 static const int autoInvocationMaxFilesize = 1000000;
0041 
0042 // BEGIN KateWordCompletionModel
0043 KateWordCompletionModel::KateWordCompletionModel(QObject *parent)
0044     : CodeCompletionModel(parent)
0045     , m_automatic(false)
0046 {
0047     setHasGroups(false);
0048 }
0049 
0050 KateWordCompletionModel::~KateWordCompletionModel()
0051 {
0052 }
0053 
0054 void KateWordCompletionModel::saveMatches(KTextEditor::View *view, const KTextEditor::Range &range)
0055 {
0056     m_matches = allMatches(view, range);
0057     m_matches.sort();
0058 }
0059 
0060 QVariant KateWordCompletionModel::data(const QModelIndex &index, int role) const
0061 {
0062     if (role == UnimportantItemRole) {
0063         return QVariant(true);
0064     }
0065     if (role == InheritanceDepth) {
0066         return 10000;
0067     }
0068 
0069     if (!index.parent().isValid()) {
0070         // It is the group header
0071         switch (role) {
0072         case Qt::DisplayRole:
0073             return i18n("Auto Word Completion");
0074         case GroupRole:
0075             return Qt::DisplayRole;
0076         }
0077     }
0078 
0079     if (index.column() == KTextEditor::CodeCompletionModel::Name && role == Qt::DisplayRole) {
0080         return m_matches.at(index.row());
0081     }
0082 
0083     if (index.column() == KTextEditor::CodeCompletionModel::Icon && role == Qt::DecorationRole) {
0084         static QIcon icon(QIcon::fromTheme(QStringLiteral("insert-text")).pixmap(QSize(16, 16)));
0085         return icon;
0086     }
0087 
0088     return QVariant();
0089 }
0090 
0091 QModelIndex KateWordCompletionModel::parent(const QModelIndex &index) const
0092 {
0093     if (index.internalId()) {
0094         return createIndex(0, 0, quintptr(0));
0095     } else {
0096         return QModelIndex();
0097     }
0098 }
0099 
0100 QModelIndex KateWordCompletionModel::index(int row, int column, const QModelIndex &parent) const
0101 {
0102     if (!parent.isValid()) {
0103         if (row == 0) {
0104             return createIndex(row, column, quintptr(0));
0105         } else {
0106             return QModelIndex();
0107         }
0108 
0109     } else if (parent.parent().isValid()) {
0110         return QModelIndex();
0111     }
0112 
0113     if (row < 0 || row >= m_matches.count() || column < 0 || column >= ColumnCount) {
0114         return QModelIndex();
0115     }
0116 
0117     return createIndex(row, column, 1);
0118 }
0119 
0120 int KateWordCompletionModel::rowCount(const QModelIndex &parent) const
0121 {
0122     if (!parent.isValid() && !m_matches.isEmpty()) {
0123         return 1; // One root node to define the custom group
0124     } else if (parent.parent().isValid()) {
0125         return 0; // Completion-items have no children
0126     } else {
0127         return m_matches.count();
0128     }
0129 }
0130 
0131 bool KateWordCompletionModel::shouldStartCompletion(KTextEditor::View *view,
0132                                                     const QString &insertedText,
0133                                                     bool userInsertion,
0134                                                     const KTextEditor::Cursor &position)
0135 {
0136     if (!userInsertion) {
0137         return false;
0138     }
0139     if (insertedText.isEmpty()) {
0140         return false;
0141     }
0142 
0143     KTextEditor::ViewPrivate *v = qobject_cast<KTextEditor::ViewPrivate *>(view);
0144 
0145     if (view->document()->totalCharacters() > autoInvocationMaxFilesize) {
0146         // Disable automatic invocation for files larger than 1MB (see benchmarks)
0147         return false;
0148     }
0149 
0150     const QString &text = view->document()->line(position.line()).left(position.column());
0151     const uint check = v->config()->wordCompletionMinimalWordLength();
0152     // Start completion immediately if min. word size is zero
0153     if (!check) {
0154         return true;
0155     }
0156     // Otherwise, check if user has typed long enough text...
0157     const int start = text.length();
0158     const int end = start - check;
0159     if (end < 0) {
0160         return false;
0161     }
0162     for (int i = start - 1; i >= end; i--) {
0163         const QChar c = text.at(i);
0164         if (!(c.isLetter() || (c.isNumber()) || c == QLatin1Char('_'))) {
0165             return false;
0166         }
0167     }
0168 
0169     return true;
0170 }
0171 
0172 bool KateWordCompletionModel::shouldAbortCompletion(KTextEditor::View *view, const KTextEditor::Range &range, const QString &currentCompletion)
0173 {
0174     if (m_automatic) {
0175         KTextEditor::ViewPrivate *v = qobject_cast<KTextEditor::ViewPrivate *>(view);
0176         if (currentCompletion.length() < v->config()->wordCompletionMinimalWordLength()) {
0177             return true;
0178         }
0179     }
0180 
0181     return CodeCompletionModelControllerInterface::shouldAbortCompletion(view, range, currentCompletion);
0182 }
0183 
0184 void KateWordCompletionModel::completionInvoked(KTextEditor::View *view, const KTextEditor::Range &range, InvocationType it)
0185 {
0186     m_automatic = it == AutomaticInvocation;
0187     saveMatches(view, range);
0188 }
0189 
0190 /**
0191  * Scan throughout the entire document for possible completions,
0192  * ignoring any dublets and words shorter than configured and/or
0193  * reasonable minimum length.
0194  */
0195 QStringList KateWordCompletionModel::allMatches(KTextEditor::View *view, const KTextEditor::Range &range)
0196 {
0197     QSet<QString> result;
0198     const int minWordSize = qMax(2, qobject_cast<KTextEditor::ViewPrivate *>(view)->config()->wordCompletionMinimalWordLength());
0199     const auto cursorPosition = view->cursorPosition();
0200     const int lines = view->document()->lines();
0201     const auto document = view->document();
0202     for (int line = 0; line < lines; line++) {
0203         const QString &text = document->line(line);
0204         int wordBegin = 0;
0205         int offset = 0;
0206         const int end = text.size();
0207         const bool cursorLine = cursorPosition.line() == line;
0208         while (offset < end) {
0209             const QChar c = text.at(offset);
0210             // increment offset when at line end, so we take the last character too
0211             if ((!c.isLetterOrNumber() && c != QLatin1Char('_')) || (offset == end - 1 && offset++)) {
0212                 if (offset - wordBegin > minWordSize && (line != range.end().line() || offset != range.end().column())) {
0213                     // don't add the word we are inside with cursor!
0214                     if (!cursorLine || (cursorPosition.column() < wordBegin || cursorPosition.column() > offset)) {
0215                         result.insert(text.mid(wordBegin, offset - wordBegin));
0216                     }
0217                 }
0218                 wordBegin = offset + 1;
0219             }
0220             if (c.isSpace()) {
0221                 wordBegin = offset + 1;
0222             }
0223             offset += 1;
0224         }
0225     }
0226     return result.values();
0227 }
0228 
0229 void KateWordCompletionModel::executeCompletionItem(KTextEditor::View *view, const KTextEditor::Range &word, const QModelIndex &index) const
0230 {
0231     view->document()->replaceText(word, m_matches.at(index.row()));
0232 }
0233 
0234 KTextEditor::CodeCompletionModelControllerInterface::MatchReaction KateWordCompletionModel::matchingItem(const QModelIndex & /*matched*/)
0235 {
0236     return HideListIfAutomaticInvocation;
0237 }
0238 
0239 bool KateWordCompletionModel::shouldHideItemsWithEqualNames() const
0240 {
0241     // We don't want word-completion items if the same items
0242     // are available through more sophisticated completion models
0243     return true;
0244 }
0245 
0246 // END KateWordCompletionModel
0247 
0248 // BEGIN KateWordCompletionView
0249 struct KateWordCompletionViewPrivate {
0250     KTextEditor::MovingRange *liRange; // range containing last inserted text
0251     KTextEditor::Range dcRange; // current range to be completed by directional completion
0252     KTextEditor::Cursor dcCursor; // directional completion search cursor
0253     int directionalPos; // be able to insert "" at the correct time
0254     bool isCompleting; // true when the directional completion is doing a completion
0255 };
0256 
0257 KateWordCompletionView::KateWordCompletionView(KTextEditor::View *view, KActionCollection *ac)
0258     : QObject(view)
0259     , m_view(view)
0260     , m_dWCompletionModel(KTextEditor::EditorPrivate::self()->wordCompletionModel())
0261     , d(new KateWordCompletionViewPrivate)
0262 {
0263     d->isCompleting = false;
0264     d->dcRange = KTextEditor::Range::invalid();
0265 
0266     d->liRange =
0267         static_cast<KTextEditor::DocumentPrivate *>(m_view->document())->newMovingRange(KTextEditor::Range::invalid(), KTextEditor::MovingRange::DoNotExpand);
0268 
0269     KTextEditor::Attribute::Ptr a = KTextEditor::Attribute::Ptr(new KTextEditor::Attribute());
0270     a->setBackground(static_cast<KTextEditor::ViewPrivate *>(view)->renderer()->config()->selectionColor());
0271     d->liRange->setAttribute(a);
0272 
0273     QAction *action;
0274 
0275     if (qobject_cast<KTextEditor::CodeCompletionInterface *>(view)) {
0276         action = new QAction(i18n("Shell Completion"), this);
0277         ac->addAction(QStringLiteral("doccomplete_sh"), action);
0278         action->setShortcutContext(Qt::WidgetWithChildrenShortcut);
0279         connect(action, &QAction::triggered, this, &KateWordCompletionView::shellComplete);
0280     }
0281 
0282     action = new QAction(i18n("Reuse Word Above"), this);
0283     ac->addAction(QStringLiteral("doccomplete_bw"), action);
0284     ac->setDefaultShortcut(action, Qt::CTRL | Qt::Key_8);
0285     action->setShortcutContext(Qt::WidgetWithChildrenShortcut);
0286     connect(action, &QAction::triggered, this, &KateWordCompletionView::completeBackwards);
0287 
0288     action = new QAction(i18n("Reuse Word Below"), this);
0289     ac->addAction(QStringLiteral("doccomplete_fw"), action);
0290     ac->setDefaultShortcut(action, Qt::CTRL | Qt::Key_9);
0291     action->setShortcutContext(Qt::WidgetWithChildrenShortcut);
0292     connect(action, &QAction::triggered, this, &KateWordCompletionView::completeForwards);
0293 }
0294 
0295 KateWordCompletionView::~KateWordCompletionView()
0296 {
0297     delete d;
0298 }
0299 
0300 void KateWordCompletionView::completeBackwards()
0301 {
0302     complete(false);
0303 }
0304 
0305 void KateWordCompletionView::completeForwards()
0306 {
0307     complete();
0308 }
0309 
0310 // Pop up the editors completion list if applicable
0311 void KateWordCompletionView::popupCompletionList()
0312 {
0313     qCDebug(LOG_KTE) << "entered ...";
0314     KTextEditor::Range r = range();
0315 
0316     KTextEditor::CodeCompletionInterface *cci = qobject_cast<KTextEditor::CodeCompletionInterface *>(m_view);
0317     if (!cci || cci->isCompletionActive()) {
0318         return;
0319     }
0320 
0321     m_dWCompletionModel->saveMatches(m_view, r);
0322 
0323     qCDebug(LOG_KTE) << "after save matches ...";
0324 
0325     if (!m_dWCompletionModel->rowCount(QModelIndex())) {
0326         return;
0327     }
0328 
0329     cci->startCompletion(r, m_dWCompletionModel);
0330 }
0331 
0332 // Contributed by <brain@hdsnet.hu>
0333 void KateWordCompletionView::shellComplete()
0334 {
0335     KTextEditor::Range r = range();
0336 
0337     QStringList matches = m_dWCompletionModel->allMatches(m_view, r);
0338 
0339     if (matches.size() == 0) {
0340         return;
0341     }
0342 
0343     QString partial = findLongestUnique(matches, r.columnWidth());
0344 
0345     if (partial.isEmpty()) {
0346         popupCompletionList();
0347     }
0348 
0349     else {
0350         m_view->document()->insertText(r.end(), partial.mid(r.columnWidth()));
0351         d->liRange->setView(m_view);
0352         d->liRange->setRange(KTextEditor::Range(r.end(), partial.length() - r.columnWidth()));
0353         connect(m_view, &KTextEditor::View::cursorPositionChanged, this, &KateWordCompletionView::slotCursorMoved);
0354     }
0355 }
0356 
0357 // Do one completion, searching in the desired direction,
0358 // if possible
0359 void KateWordCompletionView::complete(bool fw)
0360 {
0361     KTextEditor::Range r = range();
0362 
0363     int inc = fw ? 1 : -1;
0364     KTextEditor::Document *doc = m_view->document();
0365 
0366     if (d->dcRange.isValid()) {
0367         // qCDebug(LOG_KTE)<<"CONTINUE "<<d->dcRange;
0368         // this is a repeated activation
0369 
0370         // if we are back to where we started, reset.
0371         if ((fw && d->directionalPos == -1) || (!fw && d->directionalPos == 1)) {
0372             const int spansColumns = d->liRange->end().column() - d->liRange->start().column();
0373             if (spansColumns > 0) {
0374                 doc->removeText(*d->liRange);
0375             }
0376 
0377             d->liRange->setRange(KTextEditor::Range::invalid());
0378             d->dcCursor = r.end();
0379             d->directionalPos = 0;
0380 
0381             return;
0382         }
0383 
0384         if (fw) {
0385             const int spansColumns = d->liRange->end().column() - d->liRange->start().column();
0386             d->dcCursor.setColumn(d->dcCursor.column() + spansColumns);
0387         }
0388 
0389         d->directionalPos += inc;
0390     } else { // new completion, reset all
0391         // qCDebug(LOG_KTE)<<"RESET FOR NEW";
0392         d->dcRange = r;
0393         d->liRange->setRange(KTextEditor::Range::invalid());
0394         d->dcCursor = r.start();
0395         d->directionalPos = inc;
0396 
0397         d->liRange->setView(m_view);
0398 
0399         connect(m_view, &KTextEditor::View::cursorPositionChanged, this, &KateWordCompletionView::slotCursorMoved);
0400     }
0401 
0402     const QRegularExpression wordRegEx(QLatin1String("\\b") + doc->text(d->dcRange) + QLatin1String("(\\w+)"), QRegularExpression::UseUnicodePropertiesOption);
0403     int pos(0);
0404     QString ln = doc->line(d->dcCursor.line());
0405 
0406     while (true) {
0407         // qCDebug(LOG_KTE)<<"SEARCHING FOR "<<wordRegEx.pattern()<<" "<<ln<<" at "<<d->dcCursor;
0408         QRegularExpressionMatch match;
0409         pos = fw ? ln.indexOf(wordRegEx, d->dcCursor.column(), &match) : ln.lastIndexOf(wordRegEx, d->dcCursor.column(), &match);
0410 
0411         if (match.hasMatch()) { // we matched a word
0412             // qCDebug(LOG_KTE)<<"USABLE MATCH";
0413             const QStringView m = match.capturedView(1);
0414             if (m != doc->text(*d->liRange) && (d->dcCursor.line() != d->dcRange.start().line() || pos != d->dcRange.start().column())) {
0415                 // we got good a match! replace text and return.
0416                 d->isCompleting = true;
0417                 KTextEditor::Range replaceRange(d->liRange->toRange());
0418                 if (!replaceRange.isValid()) {
0419                     replaceRange.setRange(r.end(), r.end());
0420                 }
0421                 doc->replaceText(replaceRange, m.toString());
0422                 d->liRange->setRange(KTextEditor::Range(d->dcRange.end(), m.length()));
0423 
0424                 d->dcCursor.setColumn(pos); // for next try
0425 
0426                 d->isCompleting = false;
0427                 return;
0428             }
0429 
0430             // equal to last one, continue
0431             else {
0432                 // qCDebug(LOG_KTE)<<"SKIPPING, EQUAL MATCH";
0433                 d->dcCursor.setColumn(pos); // for next try
0434 
0435                 if (fw) {
0436                     d->dcCursor.setColumn(pos + m.length());
0437                 }
0438 
0439                 else {
0440                     if (pos == 0) {
0441                         if (d->dcCursor.line() > 0) {
0442                             int l = d->dcCursor.line() + inc;
0443                             ln = doc->line(l);
0444                             d->dcCursor.setPosition(l, ln.length());
0445                         } else {
0446                             return;
0447                         }
0448                     }
0449 
0450                     else {
0451                         d->dcCursor.setColumn(d->dcCursor.column() - 1);
0452                     }
0453                 }
0454             }
0455         }
0456 
0457         else { // no match
0458             // qCDebug(LOG_KTE)<<"NO MATCH";
0459             if ((!fw && d->dcCursor.line() == 0) || (fw && d->dcCursor.line() >= doc->lines())) {
0460                 return;
0461             }
0462 
0463             int l = d->dcCursor.line() + inc;
0464             ln = doc->line(l);
0465             d->dcCursor.setPosition(l, fw ? 0 : ln.length());
0466         }
0467     } // while true
0468 }
0469 
0470 void KateWordCompletionView::slotCursorMoved()
0471 {
0472     if (d->isCompleting) {
0473         return;
0474     }
0475 
0476     d->dcRange = KTextEditor::Range::invalid();
0477 
0478     disconnect(m_view, &KTextEditor::View::cursorPositionChanged, this, &KateWordCompletionView::slotCursorMoved);
0479 
0480     d->liRange->setView(nullptr);
0481     d->liRange->setRange(KTextEditor::Range::invalid());
0482 }
0483 
0484 // Contributed by <brain@hdsnet.hu> FIXME
0485 QString KateWordCompletionView::findLongestUnique(const QStringList &matches, int lead)
0486 {
0487     QString partial = matches.first();
0488 
0489     for (const QString &current : matches) {
0490         if (!current.startsWith(partial)) {
0491             while (partial.length() > lead) {
0492                 partial.remove(partial.length() - 1, 1);
0493                 if (current.startsWith(partial)) {
0494                     break;
0495                 }
0496             }
0497 
0498             if (partial.length() == lead) {
0499                 return QString();
0500             }
0501         }
0502     }
0503 
0504     return partial;
0505 }
0506 
0507 // Return the string to complete (the letters behind the cursor)
0508 QString KateWordCompletionView::word() const
0509 {
0510     return m_view->document()->text(range());
0511 }
0512 
0513 // Return the range containing the word behind the cursor
0514 KTextEditor::Range KateWordCompletionView::range() const
0515 {
0516     return m_dWCompletionModel->completionRange(m_view, m_view->cursorPosition());
0517 }
0518 // END
0519 
0520 #include "moc_katewordcompletion.cpp"