1// Copyright (C) 2022 The Qt Company Ltd.
2// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only
3
4#include "codehelper.h"
5#include "effectmanager.h"
6#include "syntaxhighlighterdata.h"
7#include <QString>
8
9CodeHelper::CodeHelper(QObject *parent)
10 : QObject{parent}
11{
12 m_effectManager = static_cast<EffectManager *>(parent);
13 m_codeCompletionModel = new CodeCompletionModel(this);
14 m_codeCompletionTimer.setInterval(500);
15 m_codeCompletionTimer.setSingleShot(true);
16 connect(sender: &m_codeCompletionTimer, signal: &QTimer::timeout, context: this, slot: &CodeHelper::showCodeCompletion);
17
18 // Cache syntax highlight data
19 auto args = SyntaxHighlighterData::reservedArgumentNames();
20 for (const auto &arg : args)
21 m_reservedArgumentNames << QString::fromUtf8(utf8: arg);
22 auto funcs = SyntaxHighlighterData::reservedFunctionNames();
23 for (const auto &func : funcs)
24 m_reservedFunctionNames << QString::fromUtf8(utf8: func);
25 auto tags = SyntaxHighlighterData::reservedTagNames();
26 for (const auto &tag : tags)
27 m_reservedTagNames << QString::fromUtf8(utf8: tag);
28
29 std::sort(first: m_reservedArgumentNames.begin(), last: m_reservedArgumentNames.end());
30 std::sort(first: m_reservedFunctionNames.begin(), last: m_reservedFunctionNames.end());
31 std::sort(first: m_reservedTagNames.begin(), last: m_reservedTagNames.end());
32}
33
34// Process the keyCode and return true if key was handled and
35// TextEdit itself shouldn't append it.
36bool CodeHelper::processKey(QQuickTextEdit *textEdit, int keyCode, int modifiers)
37{
38 int position = textEdit->cursorPosition();
39 const QString &code = textEdit->text();
40 if (m_codeCompletionVisible) {
41 // When code completition is visible, some keys have special meening
42 if (keyCode == Qt::Key_Escape || keyCode == Qt::Key_Left || keyCode == Qt::Key_Right) {
43 setCodeCompletionVisible(false);
44 return true;
45 } else if (keyCode == Qt::Key_Tab || keyCode == Qt::Key_Return) {
46 applyCodeCompletion(textEdit);
47 return true;
48 } else if (keyCode == Qt::Key_Down) {
49 m_codeCompletionModel->nextItem();
50 return true;
51 } else if (keyCode == Qt::Key_Up) {
52 m_codeCompletionModel->previousItem();
53 return true;
54 } else {
55 updateCodeCompletion(textEdit);
56 }
57 }
58 if (keyCode == Qt::Key_Return) {
59 // See if we are inside multiline comments
60 bool multilineComment = false;
61 QString prevCode = code.left(n: position);
62 int codeStartPos = prevCode.lastIndexOf(s: "/*");
63 int codeEndPos = prevCode.lastIndexOf(s: "*/");
64 if (codeStartPos > codeEndPos)
65 multilineComment = true;
66
67 // Get only the previous line before pressing return
68 QString singleLine = code.left(n: position);
69 int lineStartPos = singleLine.lastIndexOf(c: '\n');
70 if (lineStartPos > -1)
71 singleLine = singleLine.remove(i: 0, len: lineStartPos + 1);
72
73 QString indent = autoIndentGLSLNextLine(codeLine: singleLine, multilineComment);
74 textEdit->insert(position, text: indent);
75 return true;
76 } else if (keyCode == Qt::Key_BraceRight) {
77 // Get only the current line, without the last '}'
78 QString singleLine = code.left(n: position);
79 int lineStartPos = singleLine.lastIndexOf(c: '\n');
80 if (lineStartPos > -1)
81 singleLine = singleLine.remove(i: 0, len: lineStartPos + 1);
82
83 if (singleLine.trimmed().isEmpty()) {
84 // Line only had '}' so unindent it (max) one step
85 int startPos = std::max(a: int(position - singleLine.size()), b: position - 4);
86 textEdit->remove(start: startPos, end: position);
87 }
88 return false;
89 } else if (keyCode == Qt::Key_Tab) {
90 // Replace tabs with spaces
91 QString indent = QStringLiteral(" ");
92 textEdit->insert(position, text: indent);
93 return true;
94 } else if ((modifiers & Qt::ControlModifier) && keyCode == Qt::Key_Space) {
95 updateCodeCompletion(textEdit, force: true);
96 }
97 return false;
98}
99
100
101// Formats GLSL code with simple indent rules.
102// Not a full parser so doesn't work with more complex code.
103// For that, consider ClangFormat etc.
104QString CodeHelper::autoIndentGLSLCode(const QString &code)
105{
106 QStringList out;
107 // Index (indent level) currently
108 int index = 0;
109 // Index (indent level) for previous line
110 int prevIndex = 0;
111 // Indent spaces
112 QString indent;
113 // True when we are inside multiline comments (/* .. */)
114 bool multilineComment = false;
115 // Increased when command continues to next line (e.g. "if ()")
116 int nextLineIndents = 0;
117 // Stores indent before nextLineIndents started
118 int indentBeforeSingles = 0;
119
120 QStringList codeList = code.split(sep: '\n');
121 for (const auto &cOrig : codeList) {
122 QString c = cOrig.trimmed();
123
124 if (c.isEmpty()) {
125 // Lines with only spaces are emptied but kept
126 out << c;
127 continue;
128 }
129
130 bool isPreprocessor = c.startsWith(c: '#');
131 if (isPreprocessor) {
132 // Preprocesser lines have zero intent
133 out << c;
134 continue;
135 }
136
137 bool isTag = c.startsWith(c: '@');
138
139 // Separate content after "//" to commenPart
140 QString commentPart;
141 int lineCommentIndex = c.indexOf(s: "//");
142 if (lineCommentIndex > -1) {
143 commentPart = c.last(n: c.length() - lineCommentIndex);
144 c = c.first(n: lineCommentIndex);
145 // Move spaces from c to comment part (to also trim c)
146 while (!c.isEmpty() && c.endsWith(c: ' ')) {
147 commentPart.prepend(c: ' ');
148 c.chop(n: 1);
149 }
150 }
151
152 // Multiline comments state
153 int startComments = c.count(s: "/*");
154 int endComments = c.count(s: "*/");
155 int commendsChange = startComments - endComments;
156 if (commendsChange > 0)
157 multilineComment = true;
158 else if (commendsChange < 0)
159 multilineComment = false;
160
161 if (multilineComment || endComments > startComments) {
162 // Lines inside /* .. */ are not touched
163 out << cOrig;
164 continue;
165 }
166
167 // Check indent for next line
168 int indexChange = 0;
169 int startBlocks = c.count(c: '{');
170 int endBlocks = c.count(c: '}');
171 indexChange += startBlocks - endBlocks;
172 int startBrackets = c.count(c: '(');
173 int endBrackets = c.count(c: ')');
174 indexChange += startBrackets - endBrackets;
175 index += indexChange;
176 index = std::max(a: index, b: 0);
177 indent.clear();
178 int currentIndex = indexChange > 0 ? prevIndex : index;
179 if (!isTag) {
180 if (currentIndex > 0 && startBlocks > 0 && endBlocks > 0) {
181 // Note: "} else {", "} else if () {"
182 // Indent one step lower
183 currentIndex--;
184 } else if (endBrackets > startBrackets) {
185 // Note: "variable)"
186 // Indent one step higher
187 currentIndex++;
188 }
189 if (!c.startsWith(c: '{'))
190 currentIndex += nextLineIndents;
191
192 if (!c.isEmpty() && startBlocks == 0 && endBlocks == 0 && indexChange == 0 && !c.endsWith(c: ';') && !c.endsWith(c: ',') && !c.endsWith(c: '(') && !c.endsWith(s: "*/")) {
193 // Note: "if ()", "else if ()", "else", "for (..)"
194 // Something that should continue to next line
195 nextLineIndents++;
196 } else if (nextLineIndents > 0) {
197 // Return to indent before e.g. "if (thing) \n if (thing2) \n something;"
198 nextLineIndents = indentBeforeSingles;
199 indentBeforeSingles = nextLineIndents;
200 }
201 }
202
203 // Apply indent
204 QString singleIndentStep = QStringLiteral(" ");
205 for (int i = 0; i < currentIndex; i++)
206 indent += singleIndentStep;
207 c.prepend(s: indent);
208
209 out << (c + commentPart);
210 prevIndex = index;
211 }
212 return out.join(sep: '\n');
213}
214
215// Takes in the previous line (before pressing return key)
216// and returns suitable indent string with spaces.
217QString CodeHelper::autoIndentGLSLNextLine(const QString &codeLine, bool multilineComment)
218{
219 int spaces = 0;
220
221 // Check how many spaces previous line had
222 for (int i = 0; i < codeLine.size() ; i++) {
223 if (codeLine.at(i) == ' ')
224 spaces++;
225 else
226 break;
227 }
228
229 QString c = codeLine.trimmed();
230 bool isPreprocessor = c.startsWith(c: '#');
231 bool isTag = c.startsWith(c: '@');
232
233 // Remove content after "//"
234 int lineCommentIndex = c.indexOf(s: "//");
235 if (lineCommentIndex > -1) {
236 c = c.first(n: lineCommentIndex);
237 c = c.trimmed();
238 }
239
240 if (!c.isEmpty() && !isPreprocessor && !isTag && !multilineComment) {
241 // Check indent for next line
242 int index = 0;
243 int indexChange = 0;
244 int startBlocks = c.count(c: '{');
245 int endBlocks = c.count(c: '}');
246 indexChange += startBlocks - endBlocks;
247 int startBrackets = c.count(c: '(');
248 int endBrackets = c.count(c: ')');
249 indexChange += startBrackets - endBrackets;
250 if (indexChange > 0) {
251 // Note: "{" "if () {", "vec4 x = vec4("
252 index++;
253 } else if (indexChange < 0 && c.trimmed().size() > 1) {
254 // Note: "something; }", but NOT "}" as it has be unindented already
255 index--;
256 }
257
258 if (index == 0 && !c.isEmpty() && !c.endsWith(c: ';') && !c.endsWith(s: "*/") && !c.endsWith(c: '}')) {
259 // Note: "if ()", "else if ()", "else", "for (..)"
260 // Something that should continue to next line
261 index++;
262 }
263 spaces += 4 * index;
264 }
265
266 QString indent;
267 indent.fill(c: ' ', size: spaces);
268 return '\n' + indent;
269}
270
271// Get current word under the cursor
272// Word is letters, digits and "_".
273// Also if word starts with "//" that is included
274QString CodeHelper::getCurrentWord(QQuickTextEdit *textEdit)
275{
276 if (!textEdit)
277 return QString();
278 int cursorPosition = textEdit->cursorPosition();
279 int cPos = cursorPosition - 1;
280 int maxPos = textEdit->text().size();
281 QString currentWord;
282 QChar c = textEdit->getText(start: cPos, end: cPos + 1).front();
283 while (cPos >= 0 && (c.isLetterOrNumber() || c == '_')) {
284 currentWord.prepend(c);
285 cPos--;
286 c = textEdit->getText(start: cPos, end: cPos + 1).front();
287 }
288 // Special case of "@" tags
289 if (cPos >= 1) {
290 QString s = textEdit->getText(start: cPos, end: cPos + 1);
291 if (s == QStringLiteral("@"))
292 currentWord.prepend(QStringLiteral("@"));
293 }
294 cPos = cursorPosition;
295 c = textEdit->getText(start: cPos, end: cPos + 1).front();
296 while (cPos <= maxPos && (c.isLetterOrNumber() || c == '_')) {
297 currentWord.append(c);
298 cPos++;
299 c = textEdit->getText(start: cPos, end: cPos + 1).front();
300 }
301 return currentWord;
302}
303
304// Remove the current word under cursor
305void CodeHelper::removeCurrentWord(QQuickTextEdit *textEdit)
306{
307 if (!textEdit)
308 return;
309 int cursorPosition = textEdit->cursorPosition();
310 int cPos = cursorPosition - 1;
311 int maxPos = textEdit->text().size();
312 int firstPos = 0;
313 int lastPos = 0;
314 QChar c = textEdit->getText(start: cPos, end: cPos + 1).front();
315 while (cPos >= 0 && (c.isLetterOrNumber() || c == '_')) {
316 cPos--;
317 c = textEdit->getText(start: cPos, end: cPos + 1).front();
318 }
319 // Special case of "@" tags
320 if (cPos >= 1) {
321 QString s = textEdit->getText(start: cPos, end: cPos + 1);
322 if (s == QStringLiteral("@"))
323 cPos--;
324 }
325 firstPos = cPos + 1;
326 cPos = cursorPosition;
327 c = textEdit->getText(start: cPos, end: cPos + 1).front();
328 while (cPos <= maxPos && (c.isLetterOrNumber() || c == '_')) {
329 cPos++;
330 c = textEdit->getText(start: cPos, end: cPos + 1).front();
331 }
332 lastPos = cPos;
333 textEdit->remove(start: firstPos, end: lastPos);
334}
335
336bool CodeHelper::codeCompletionVisible() const
337{
338 return m_codeCompletionVisible;
339}
340
341void CodeHelper::setCodeCompletionVisible(bool visible)
342{
343 if (m_codeCompletionVisible == visible)
344 return;
345
346 m_codeCompletionVisible = visible;
347 if (!m_codeCompletionVisible)
348 m_codeCompletionModel->setCurrentIndex(0);
349
350 Q_EMIT codeCompletionVisibleChanged();
351}
352
353// Update and show code completion popup
354// When force is true, do this without a delay.
355void CodeHelper::updateCodeCompletion(QQuickTextEdit *textEdit, bool force)
356{
357 m_textEdit = textEdit;
358 if (force)
359 showCodeCompletion();
360 else
361 m_codeCompletionTimer.start();
362}
363
364void CodeHelper::showCodeCompletion()
365{
366 if (!m_textEdit)
367 return;
368
369 QString currentWord = getCurrentWord(textEdit: m_textEdit);
370 QString currentWordCleaned = currentWord;
371 bool isComment = false;
372
373 // Check if word is comment/tag
374 if (currentWordCleaned.size() >= 2 && currentWordCleaned.left(n: 2) == "//") {
375 isComment = true;
376 currentWordCleaned = currentWordCleaned.right(n: currentWordCleaned.size() - 2);
377 }
378
379 m_codeCompletionModel->beginResetModel();
380 m_codeCompletionModel->clearItems();
381
382 if (!isComment) {
383 for (const auto &a : m_reservedArgumentNames) {
384 if (a.startsWith(s: currentWordCleaned, cs: Qt::CaseInsensitive) && a.size() > currentWordCleaned.size())
385 m_codeCompletionModel->addItem(text: a, type: CodeCompletionModel::TypeArgument);
386 }
387
388 for (const auto &a : m_reservedFunctionNames) {
389 if (a.startsWith(s: currentWordCleaned, cs: Qt::CaseInsensitive) && a.size() > currentWordCleaned.size())
390 m_codeCompletionModel->addItem(text: a, type: CodeCompletionModel::TypeFunction);
391 }
392
393 for (const auto &a : m_reservedTagNames) {
394 if (a.startsWith(s: currentWord, cs: Qt::CaseInsensitive) && a.size() > currentWord.size())
395 m_codeCompletionModel->addItem(text: a, type: CodeCompletionModel::TypeTag);
396 }
397 }
398
399 m_codeCompletionModel->endResetModel();
400
401 if (!m_codeCompletionModel->m_modelList.isEmpty()) {
402 m_codeCompletionModel->setCurrentIndex(0);
403 setCodeCompletionVisible(true);
404 } else {
405 setCodeCompletionVisible(false);
406 }
407}
408
409CodeCompletionModel *CodeHelper::codeCompletionModel() const
410{
411 return m_codeCompletionModel;
412}
413
414void CodeHelper::applyCodeCompletion(QQuickTextEdit *textEdit)
415{
416 CodeCompletionModel::ModelData t = m_codeCompletionModel->currentItem();
417 if (!t.name.isEmpty()) {
418 // Replace the current word with code completion one
419 removeCurrentWord(textEdit);
420 textEdit->insert(position: textEdit->cursorPosition(), text: t.name);
421 if (t.type == CodeCompletionModel::TypeFunction) {
422 // For functions, place cursor between parentheses, e.g. "sin(|)".
423 textEdit->setCursorPosition(textEdit->cursorPosition() - 1);
424 }
425 }
426 setCodeCompletionVisible(false);
427}
428

source code of qtquickeffectmaker/tools/qqem/codehelper.cpp