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