File indexing completed on 2024-05-05 12:22:15
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 ¤tCompletion) 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 ¤t : 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"