1 /*
2     SPDX-FileCopyrightText: 2013-2016 Simon St James <kdedevel@etotheipiplusone.com>
3 
4     SPDX-License-Identifier: LGPL-2.0-or-later
5 */
6 
7 #include "completer.h"
8 #include "emulatedcommandbar.h"
9 
10 using namespace KateVi;
11 
12 #include "activemode.h"
13 #include "kateview.h"
14 
15 #include <QAbstractItemView>
16 #include <QCompleter>
17 #include <QLineEdit>
18 #include <QRegularExpression>
19 #include <QStringListModel>
20 
21 namespace
22 {
caseInsensitiveLessThan(const QString & s1,const QString & s2)23 bool caseInsensitiveLessThan(const QString &s1, const QString &s2)
24 {
25     return s1.toLower() < s2.toLower();
26 }
27 }
28 
Completer(EmulatedCommandBar * emulatedCommandBar,KTextEditor::ViewPrivate * view,QLineEdit * edit)29 Completer::Completer(EmulatedCommandBar *emulatedCommandBar, KTextEditor::ViewPrivate *view, QLineEdit *edit)
30     : m_edit(edit)
31     , m_view(view)
32 {
33     m_completer = new QCompleter(QStringList(), edit);
34     // Can't find a way to stop the QCompleter from auto-completing when attached to a QLineEdit,
35     // so don't actually set it as the QLineEdit's completer.
36     m_completer->setWidget(edit);
37     m_completer->setObjectName(QStringLiteral("completer"));
38     m_completionModel = new QStringListModel(emulatedCommandBar);
39     m_completer->setModel(m_completionModel);
40     m_completer->setCaseSensitivity(Qt::CaseInsensitive);
41     m_completer->popup()->installEventFilter(emulatedCommandBar);
42 }
43 
startCompletion(const CompletionStartParams & completionStartParams)44 void Completer::startCompletion(const CompletionStartParams &completionStartParams)
45 {
46     if (completionStartParams.completionType != CompletionStartParams::None) {
47         m_completionModel->setStringList(completionStartParams.completions);
48         const QString completionPrefix = m_edit->text().mid(completionStartParams.wordStartPos, m_edit->cursorPosition() - completionStartParams.wordStartPos);
49         m_completer->setCompletionPrefix(completionPrefix);
50         m_completer->complete();
51         m_currentCompletionStartParams = completionStartParams;
52         m_currentCompletionType = completionStartParams.completionType;
53     }
54 }
55 
deactivateCompletion()56 void Completer::deactivateCompletion()
57 {
58     m_completer->popup()->hide();
59     m_currentCompletionType = CompletionStartParams::None;
60 }
61 
isCompletionActive() const62 bool Completer::isCompletionActive() const
63 {
64     return m_currentCompletionType != CompletionStartParams::None;
65 }
66 
isNextTextChangeDueToCompletionChange() const67 bool Completer::isNextTextChangeDueToCompletionChange() const
68 {
69     return m_isNextTextChangeDueToCompletionChange;
70 }
71 
completerHandledKeypress(const QKeyEvent * keyEvent)72 bool Completer::completerHandledKeypress(const QKeyEvent *keyEvent)
73 {
74     if (!m_edit->isVisible()) {
75         return false;
76     }
77 
78     if (keyEvent->modifiers() == Qt::ControlModifier && (keyEvent->key() == Qt::Key_C || keyEvent->key() == Qt::Key_BracketLeft)) {
79         if (m_currentCompletionType != CompletionStartParams::None && m_completer->popup()->isVisible()) {
80             abortCompletionAndResetToPreCompletion();
81             return true;
82         }
83     }
84     if (keyEvent->modifiers() == Qt::ControlModifier && keyEvent->key() == Qt::Key_Space) {
85         CompletionStartParams completionStartParams = activateWordFromDocumentCompletion();
86         startCompletion(completionStartParams);
87         return true;
88     }
89     if ((keyEvent->modifiers() == Qt::ControlModifier && keyEvent->key() == Qt::Key_P) || keyEvent->key() == Qt::Key_Down) {
90         if (!m_completer->popup()->isVisible()) {
91             const CompletionStartParams completionStartParams = m_currentMode->completionInvoked(CompletionInvocation::ExtraContext);
92             startCompletion(completionStartParams);
93             if (m_currentCompletionType != CompletionStartParams::None) {
94                 setCompletionIndex(0);
95             }
96         } else {
97             // Descend to next row, wrapping around if necessary.
98             if (m_completer->currentRow() + 1 == m_completer->completionCount()) {
99                 setCompletionIndex(0);
100             } else {
101                 setCompletionIndex(m_completer->currentRow() + 1);
102             }
103         }
104         return true;
105     }
106     if ((keyEvent->modifiers() == Qt::ControlModifier && keyEvent->key() == Qt::Key_N) || keyEvent->key() == Qt::Key_Up) {
107         if (!m_completer->popup()->isVisible()) {
108             const CompletionStartParams completionStartParams = m_currentMode->completionInvoked(CompletionInvocation::NormalContext);
109             startCompletion(completionStartParams);
110             setCompletionIndex(m_completer->completionCount() - 1);
111         } else {
112             // Ascend to previous row, wrapping around if necessary.
113             if (m_completer->currentRow() == 0) {
114                 setCompletionIndex(m_completer->completionCount() - 1);
115             } else {
116                 setCompletionIndex(m_completer->currentRow() - 1);
117             }
118         }
119         return true;
120     }
121     if (keyEvent->key() == Qt::Key_Enter || keyEvent->key() == Qt::Key_Return) {
122         if (!m_completer->popup()->isVisible() || m_currentCompletionType != CompletionStartParams::WordFromDocument) {
123             m_currentMode->completionChosen();
124         }
125         deactivateCompletion();
126         return true;
127     }
128     return false;
129 }
130 
editTextChanged(const QString & newText)131 void Completer::editTextChanged(const QString &newText)
132 {
133     if (!m_isNextTextChangeDueToCompletionChange) {
134         m_textToRevertToIfCompletionAborted = newText;
135         m_cursorPosToRevertToIfCompletionAborted = m_edit->cursorPosition();
136     }
137     // If we edit the text after having selected a completion, this means we implicitly accept it,
138     // and so we should dismiss it.
139     if (!m_isNextTextChangeDueToCompletionChange && m_completer->popup()->currentIndex().row() != -1) {
140         deactivateCompletion();
141     }
142 
143     if (m_currentCompletionType != CompletionStartParams::None && !m_isNextTextChangeDueToCompletionChange) {
144         updateCompletionPrefix();
145     }
146 }
147 
setCurrentMode(ActiveMode * currentMode)148 void Completer::setCurrentMode(ActiveMode *currentMode)
149 {
150     m_currentMode = currentMode;
151 }
152 
setCompletionIndex(int index)153 void Completer::setCompletionIndex(int index)
154 {
155     const QModelIndex modelIndex = m_completer->popup()->model()->index(index, 0);
156     // Need to set both of these, for some reason.
157     m_completer->popup()->setCurrentIndex(modelIndex);
158     m_completer->setCurrentRow(index);
159 
160     m_completer->popup()->scrollTo(modelIndex);
161 
162     currentCompletionChanged();
163 }
164 
currentCompletionChanged()165 void Completer::currentCompletionChanged()
166 {
167     const QString newCompletion = m_completer->currentCompletion();
168     if (newCompletion.isEmpty()) {
169         return;
170     }
171     QString transformedCompletion = newCompletion;
172     if (m_currentCompletionStartParams.completionTransform) {
173         transformedCompletion = m_currentCompletionStartParams.completionTransform(newCompletion);
174     }
175 
176     m_isNextTextChangeDueToCompletionChange = true;
177     m_edit->setSelection(m_currentCompletionStartParams.wordStartPos, m_edit->cursorPosition() - m_currentCompletionStartParams.wordStartPos);
178     m_edit->insert(transformedCompletion);
179     m_isNextTextChangeDueToCompletionChange = false;
180 }
181 
updateCompletionPrefix()182 void Completer::updateCompletionPrefix()
183 {
184     const QString completionPrefix =
185         m_edit->text().mid(m_currentCompletionStartParams.wordStartPos, m_edit->cursorPosition() - m_currentCompletionStartParams.wordStartPos);
186     m_completer->setCompletionPrefix(completionPrefix);
187     // Seem to need a call to complete() else the size of the popup box is not altered appropriately.
188     m_completer->complete();
189 }
190 
activateWordFromDocumentCompletion()191 CompletionStartParams Completer::activateWordFromDocumentCompletion()
192 {
193     static const QRegularExpression wordRegEx(QStringLiteral("\\w+"), QRegularExpression::UseUnicodePropertiesOption);
194     QRegularExpressionMatch match;
195 
196     QStringList foundWords;
197     // Narrow the range of lines we search around the cursor so that we don't die on huge files.
198     const int startLine = qMax(0, m_view->cursorPosition().line() - 4096);
199     const int endLine = qMin(m_view->document()->lines(), m_view->cursorPosition().line() + 4096);
200     for (int lineNum = startLine; lineNum < endLine; lineNum++) {
201         const QString line = m_view->document()->line(lineNum);
202         int wordSearchBeginPos = 0;
203         while ((match = wordRegEx.match(line, wordSearchBeginPos)).hasMatch()) {
204             const QString foundWord = match.captured();
205             foundWords << foundWord;
206             wordSearchBeginPos = match.capturedEnd();
207         }
208     }
209     foundWords.removeDuplicates();
210     std::sort(foundWords.begin(), foundWords.end(), caseInsensitiveLessThan);
211     CompletionStartParams completionStartParams;
212     completionStartParams.completionType = CompletionStartParams::WordFromDocument;
213     completionStartParams.completions = foundWords;
214     completionStartParams.wordStartPos = wordBeforeCursorBegin();
215     return completionStartParams;
216 }
217 
wordBeforeCursor()218 QString Completer::wordBeforeCursor()
219 {
220     const int wordBeforeCursorBegin = this->wordBeforeCursorBegin();
221     return m_edit->text().mid(wordBeforeCursorBegin, m_edit->cursorPosition() - wordBeforeCursorBegin);
222 }
223 
wordBeforeCursorBegin()224 int Completer::wordBeforeCursorBegin()
225 {
226     int wordBeforeCursorBegin = m_edit->cursorPosition() - 1;
227     while (wordBeforeCursorBegin >= 0
228            && (m_edit->text()[wordBeforeCursorBegin].isLetterOrNumber() || m_edit->text()[wordBeforeCursorBegin] == QLatin1Char('_'))) {
229         wordBeforeCursorBegin--;
230     }
231     wordBeforeCursorBegin++;
232     return wordBeforeCursorBegin;
233 }
234 
abortCompletionAndResetToPreCompletion()235 void Completer::abortCompletionAndResetToPreCompletion()
236 {
237     deactivateCompletion();
238     m_isNextTextChangeDueToCompletionChange = true;
239     m_edit->setText(m_textToRevertToIfCompletionAborted);
240     m_edit->setCursorPosition(m_cursorPosToRevertToIfCompletionAborted);
241     m_isNextTextChangeDueToCompletionChange = false;
242 }
243