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 ¤tCompletion) 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 ¤t : 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"