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 }