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