File indexing completed on 2024-05-05 03:58:35

0001 /*
0002     SPDX-FileCopyrightText: 2013-2016 Simon St James <kdedevel@etotheipiplusone.com>
0003 
0004     SPDX-License-Identifier: LGPL-2.0-or-later
0005 */
0006 
0007 #include "completer.h"
0008 #include "emulatedcommandbar.h"
0009 
0010 using namespace KateVi;
0011 
0012 #include "activemode.h"
0013 #include "kateview.h"
0014 #include "vimode/definitions.h"
0015 #include <ktexteditor/document.h>
0016 
0017 #include <QAbstractItemView>
0018 #include <QCompleter>
0019 #include <QKeyEvent>
0020 #include <QLineEdit>
0021 #include <QRegularExpression>
0022 #include <QStringListModel>
0023 
0024 namespace
0025 {
0026 bool caseInsensitiveLessThan(const QString &s1, const QString &s2)
0027 {
0028     return s1.toLower() < s2.toLower();
0029 }
0030 }
0031 
0032 Completer::Completer(EmulatedCommandBar *emulatedCommandBar, KTextEditor::ViewPrivate *view, QLineEdit *edit)
0033     : m_edit(edit)
0034     , m_view(view)
0035 {
0036     m_completer = new QCompleter(QStringList(), edit);
0037     // Can't find a way to stop the QCompleter from auto-completing when attached to a QLineEdit,
0038     // so don't actually set it as the QLineEdit's completer.
0039     m_completer->setWidget(edit);
0040     m_completer->setObjectName(QStringLiteral("completer"));
0041     m_completionModel = new QStringListModel(emulatedCommandBar);
0042     m_completer->setModel(m_completionModel);
0043     m_completer->setCaseSensitivity(Qt::CaseInsensitive);
0044     m_completer->popup()->installEventFilter(emulatedCommandBar);
0045 }
0046 
0047 void Completer::startCompletion(const CompletionStartParams &completionStartParams)
0048 {
0049     if (completionStartParams.completionType != CompletionStartParams::None) {
0050         m_completionModel->setStringList(completionStartParams.completions);
0051         const QString completionPrefix = m_edit->text().mid(completionStartParams.wordStartPos, m_edit->cursorPosition() - completionStartParams.wordStartPos);
0052         m_completer->setCompletionPrefix(completionPrefix);
0053         m_completer->complete();
0054         m_currentCompletionStartParams = completionStartParams;
0055         m_currentCompletionType = completionStartParams.completionType;
0056     }
0057 }
0058 
0059 void Completer::deactivateCompletion()
0060 {
0061     m_completer->popup()->hide();
0062     m_currentCompletionType = CompletionStartParams::None;
0063 }
0064 
0065 bool Completer::isCompletionActive() const
0066 {
0067     return m_currentCompletionType != CompletionStartParams::None;
0068 }
0069 
0070 bool Completer::isNextTextChangeDueToCompletionChange() const
0071 {
0072     return m_isNextTextChangeDueToCompletionChange;
0073 }
0074 
0075 bool Completer::completerHandledKeypress(const QKeyEvent *keyEvent)
0076 {
0077     if (!m_edit->isVisible()) {
0078         return false;
0079     }
0080 
0081     if (keyEvent->modifiers() == CONTROL_MODIFIER && (keyEvent->key() == Qt::Key_C || keyEvent->key() == Qt::Key_BracketLeft)) {
0082         if (m_currentCompletionType != CompletionStartParams::None && m_completer->popup()->isVisible()) {
0083             abortCompletionAndResetToPreCompletion();
0084             return true;
0085         }
0086     }
0087     if (keyEvent->modifiers() == CONTROL_MODIFIER && keyEvent->key() == Qt::Key_Space) {
0088         CompletionStartParams completionStartParams = activateWordFromDocumentCompletion();
0089         startCompletion(completionStartParams);
0090         return true;
0091     }
0092     if ((keyEvent->modifiers() == CONTROL_MODIFIER && keyEvent->key() == Qt::Key_P) || keyEvent->key() == Qt::Key_Down) {
0093         if (!m_completer->popup()->isVisible()) {
0094             const CompletionStartParams completionStartParams = m_currentMode->completionInvoked(CompletionInvocation::ExtraContext);
0095             startCompletion(completionStartParams);
0096             if (m_currentCompletionType != CompletionStartParams::None) {
0097                 setCompletionIndex(0);
0098             }
0099         } else {
0100             // Descend to next row, wrapping around if necessary.
0101             if (m_completer->currentRow() + 1 == m_completer->completionCount()) {
0102                 setCompletionIndex(0);
0103             } else {
0104                 setCompletionIndex(m_completer->currentRow() + 1);
0105             }
0106         }
0107         return true;
0108     }
0109     if ((keyEvent->modifiers() == CONTROL_MODIFIER && keyEvent->key() == Qt::Key_N) || keyEvent->key() == Qt::Key_Up) {
0110         if (!m_completer->popup()->isVisible()) {
0111             const CompletionStartParams completionStartParams = m_currentMode->completionInvoked(CompletionInvocation::NormalContext);
0112             startCompletion(completionStartParams);
0113             setCompletionIndex(m_completer->completionCount() - 1);
0114         } else {
0115             // Ascend to previous row, wrapping around if necessary.
0116             if (m_completer->currentRow() == 0) {
0117                 setCompletionIndex(m_completer->completionCount() - 1);
0118             } else {
0119                 setCompletionIndex(m_completer->currentRow() - 1);
0120             }
0121         }
0122         return true;
0123     }
0124     if (keyEvent->key() == Qt::Key_Enter || keyEvent->key() == Qt::Key_Return) {
0125         if (!m_completer->popup()->isVisible() || m_currentCompletionType != CompletionStartParams::WordFromDocument) {
0126             m_currentMode->completionChosen();
0127         }
0128         deactivateCompletion();
0129         return true;
0130     }
0131     return false;
0132 }
0133 
0134 void Completer::editTextChanged(const QString &newText)
0135 {
0136     if (!m_isNextTextChangeDueToCompletionChange) {
0137         m_textToRevertToIfCompletionAborted = newText;
0138         m_cursorPosToRevertToIfCompletionAborted = m_edit->cursorPosition();
0139     }
0140     // If we edit the text after having selected a completion, this means we implicitly accept it,
0141     // and so we should dismiss it.
0142     if (!m_isNextTextChangeDueToCompletionChange && m_completer->popup()->currentIndex().row() != -1) {
0143         deactivateCompletion();
0144     }
0145 
0146     if (m_currentCompletionType != CompletionStartParams::None && !m_isNextTextChangeDueToCompletionChange) {
0147         updateCompletionPrefix();
0148     }
0149 }
0150 
0151 void Completer::setCurrentMode(ActiveMode *currentMode)
0152 {
0153     m_currentMode = currentMode;
0154 }
0155 
0156 void Completer::setCompletionIndex(int index)
0157 {
0158     const QModelIndex modelIndex = m_completer->popup()->model()->index(index, 0);
0159     // Need to set both of these, for some reason.
0160     m_completer->popup()->setCurrentIndex(modelIndex);
0161     m_completer->setCurrentRow(index);
0162 
0163     m_completer->popup()->scrollTo(modelIndex);
0164 
0165     currentCompletionChanged();
0166 }
0167 
0168 void Completer::currentCompletionChanged()
0169 {
0170     const QString newCompletion = m_completer->currentCompletion();
0171     if (newCompletion.isEmpty()) {
0172         return;
0173     }
0174     QString transformedCompletion = newCompletion;
0175     if (m_currentCompletionStartParams.completionTransform) {
0176         transformedCompletion = m_currentCompletionStartParams.completionTransform(newCompletion);
0177     }
0178 
0179     m_isNextTextChangeDueToCompletionChange = true;
0180     m_edit->setSelection(m_currentCompletionStartParams.wordStartPos, m_edit->cursorPosition() - m_currentCompletionStartParams.wordStartPos);
0181     m_edit->insert(transformedCompletion);
0182     m_isNextTextChangeDueToCompletionChange = false;
0183 }
0184 
0185 void Completer::updateCompletionPrefix()
0186 {
0187     const QString completionPrefix =
0188         m_edit->text().mid(m_currentCompletionStartParams.wordStartPos, m_edit->cursorPosition() - m_currentCompletionStartParams.wordStartPos);
0189     m_completer->setCompletionPrefix(completionPrefix);
0190     // Seem to need a call to complete() else the size of the popup box is not altered appropriately.
0191     m_completer->complete();
0192 }
0193 
0194 CompletionStartParams Completer::activateWordFromDocumentCompletion()
0195 {
0196     static const QRegularExpression wordRegEx(QStringLiteral("\\w+"), QRegularExpression::UseUnicodePropertiesOption);
0197     QRegularExpressionMatch match;
0198 
0199     QStringList foundWords;
0200     // Narrow the range of lines we search around the cursor so that we don't die on huge files.
0201     const int startLine = qMax(0, m_view->cursorPosition().line() - 4096);
0202     const int endLine = qMin(m_view->document()->lines(), m_view->cursorPosition().line() + 4096);
0203     for (int lineNum = startLine; lineNum < endLine; lineNum++) {
0204         const QString line = m_view->document()->line(lineNum);
0205         int wordSearchBeginPos = 0;
0206         while ((match = wordRegEx.match(line, wordSearchBeginPos)).hasMatch()) {
0207             const QString foundWord = match.captured();
0208             foundWords << foundWord;
0209             wordSearchBeginPos = match.capturedEnd();
0210         }
0211     }
0212     foundWords.removeDuplicates();
0213     std::sort(foundWords.begin(), foundWords.end(), caseInsensitiveLessThan);
0214     CompletionStartParams completionStartParams;
0215     completionStartParams.completionType = CompletionStartParams::WordFromDocument;
0216     completionStartParams.completions = foundWords;
0217     completionStartParams.wordStartPos = wordBeforeCursorBegin();
0218     return completionStartParams;
0219 }
0220 
0221 QString Completer::wordBeforeCursor()
0222 {
0223     const int wordBeforeCursorBegin = this->wordBeforeCursorBegin();
0224     return m_edit->text().mid(wordBeforeCursorBegin, m_edit->cursorPosition() - wordBeforeCursorBegin);
0225 }
0226 
0227 int Completer::wordBeforeCursorBegin()
0228 {
0229     int wordBeforeCursorBegin = m_edit->cursorPosition() - 1;
0230     while (wordBeforeCursorBegin >= 0
0231            && (m_edit->text()[wordBeforeCursorBegin].isLetterOrNumber() || m_edit->text()[wordBeforeCursorBegin] == QLatin1Char('_'))) {
0232         wordBeforeCursorBegin--;
0233     }
0234     wordBeforeCursorBegin++;
0235     return wordBeforeCursorBegin;
0236 }
0237 
0238 void Completer::abortCompletionAndResetToPreCompletion()
0239 {
0240     deactivateCompletion();
0241     m_isNextTextChangeDueToCompletionChange = true;
0242     m_edit->setText(m_textToRevertToIfCompletionAborted);
0243     m_edit->setCursorPosition(m_cursorPosToRevertToIfCompletionAborted);
0244     m_isNextTextChangeDueToCompletionChange = false;
0245 }