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
10using 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
24namespace
25{
26bool caseInsensitiveLessThan(const QString &s1, const QString &s2)
27{
28 return s1.toLower() < s2.toLower();
29}
30}
31
32Completer::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
47void 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
59void Completer::deactivateCompletion()
60{
61 m_completer->popup()->hide();
62 m_currentCompletionType = CompletionStartParams::None;
63}
64
65bool Completer::isCompletionActive() const
66{
67 return m_currentCompletionType != CompletionStartParams::None;
68}
69
70bool Completer::isNextTextChangeDueToCompletionChange() const
71{
72 return m_isNextTextChangeDueToCompletionChange;
73}
74
75bool 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
134void 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
151void Completer::setCurrentMode(ActiveMode *currentMode)
152{
153 m_currentMode = currentMode;
154}
155
156void 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
168void 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
185void 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
194CompletionStartParams 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
221QString Completer::wordBeforeCursor()
222{
223 const int wordBeforeCursorBegin = this->wordBeforeCursorBegin();
224 return m_edit->text().mid(position: wordBeforeCursorBegin, n: m_edit->cursorPosition() - wordBeforeCursorBegin);
225}
226
227int 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
238void 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

source code of ktexteditor/src/vimode/emulatedcommandbar/completer.cpp