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

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