Revision control

++ // Copyright (C) 2022 The Qt Company Ltd.
++ // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only
++ // Qt-Security score:critical reason:data-parser
++
++ #include "codehelper.h"
++ #include "effectmanager.h"
++ #include "syntaxhighlighterdata.h"
++ #include <QString>
++
++ CodeHelper::CodeHelper(QObject *parent)
++ : QObject{parent}
++ {
++ m_effectManager = static_cast<EffectManager *>(parent);
++ m_codeCompletionModel = new CodeCompletionModel(this);
++ m_codeCompletionTimer.setInterval(500);
++ m_codeCompletionTimer.setSingleShot(true);
++ connect(&m_codeCompletionTimer, &QTimer::timeout, this, &CodeHelper::showCodeCompletion);
++
++ // Cache syntax highlight data
++ auto args = SyntaxHighlighterData::reservedArgumentNames();
++ for (const auto &arg : args)
++ m_reservedArgumentNames << QString::fromUtf8(arg);
++ auto funcs = SyntaxHighlighterData::reservedFunctionNames();
++ for (const auto &func : funcs)
++ m_reservedFunctionNames << QString::fromUtf8(func);
++ auto tags = SyntaxHighlighterData::reservedTagNames();
++ for (const auto &tag : tags)
++ m_reservedTagNames << QString::fromUtf8(tag);
++
++ std::sort(m_reservedArgumentNames.begin(), m_reservedArgumentNames.end());
++ std::sort(m_reservedFunctionNames.begin(), m_reservedFunctionNames.end());
++ std::sort(m_reservedTagNames.begin(), m_reservedTagNames.end());
++ }
++
++ // Process the keyCode and return true if key was handled and
++ // TextEdit itself shouldn't append it.
++ bool CodeHelper::processKey(QQuickTextEdit *textEdit, int keyCode, int modifiers)
++ {
++ int position = textEdit->cursorPosition();
++ const QString &code = textEdit->text();
++ if (m_codeCompletionVisible) {
++ // When code completition is visible, some keys have special meening
++ if (keyCode == Qt::Key_Escape || keyCode == Qt::Key_Left || keyCode == Qt::Key_Right) {
++ setCodeCompletionVisible(false);
++ return true;
++ } else if (keyCode == Qt::Key_Tab || keyCode == Qt::Key_Return) {
++ applyCodeCompletion(textEdit);
++ return true;
++ } else if (keyCode == Qt::Key_Down) {
++ m_codeCompletionModel->nextItem();
++ return true;
++ } else if (keyCode == Qt::Key_Up) {
++ m_codeCompletionModel->previousItem();
++ return true;
++ } else {
++ updateCodeCompletion(textEdit);
++ }
++ }
++ if (keyCode == Qt::Key_Return) {
++ // See if we are inside multiline comments
++ bool multilineComment = false;
++ QString prevCode = code.left(position);
++ int codeStartPos = prevCode.lastIndexOf("/*");
++ int codeEndPos = prevCode.lastIndexOf("*/");
++ if (codeStartPos > codeEndPos)
++ multilineComment = true;
++
++ // Get only the previous line before pressing return
++ QString singleLine = code.left(position);
++ int lineStartPos = singleLine.lastIndexOf('\n');
++ if (lineStartPos > -1)
++ singleLine = singleLine.remove(0, lineStartPos + 1);
++
++ QString indent = autoIndentGLSLNextLine(singleLine, multilineComment);
++ textEdit->insert(position, indent);
++ return true;
++ } else if (keyCode == Qt::Key_BraceRight) {
++ // Get only the current line, without the last '}'
++ QString singleLine = code.left(position);
++ int lineStartPos = singleLine.lastIndexOf('\n');
++ if (lineStartPos > -1)
++ singleLine = singleLine.remove(0, lineStartPos + 1);
++
++ if (singleLine.trimmed().isEmpty()) {
++ // Line only had '}' so unindent it (max) one step
++ int startPos = std::max(int(position - singleLine.size()), position - 4);
++ textEdit->remove(startPos, position);
++ }
++ return false;
++ } else if (keyCode == Qt::Key_Tab) {
++ // Replace tabs with spaces
++ QString indent = QStringLiteral(" ");
++ textEdit->insert(position, indent);
++ return true;
++ } else if ((modifiers & Qt::ControlModifier) && keyCode == Qt::Key_Space) {
++ updateCodeCompletion(textEdit, true);
++ }
++ return false;
++ }
++
++
++ // Formats GLSL code with simple indent rules.
++ // Not a full parser so doesn't work with more complex code.
++ // For that, consider ClangFormat etc.
++ QString CodeHelper::autoIndentGLSLCode(const QString &code)
++ {
++ QStringList out;
++ // Index (indent level) currently
++ int index = 0;
++ // Index (indent level) for previous line
++ int prevIndex = 0;
++ // Indent spaces
++ QString indent;
++ // True when we are inside multiline comments (/* .. */)
++ bool multilineComment = false;
++ // Increased when command continues to next line (e.g. "if ()")
++ int nextLineIndents = 0;
++ // Stores indent before nextLineIndents started
++ int indentBeforeSingles = 0;
++
++ QStringList codeList = code.split('\n');
++ for (const auto &cOrig : codeList) {
++ QString c = cOrig.trimmed();
++
++ if (c.isEmpty()) {
++ // Lines with only spaces are emptied but kept
++ out << c;
++ continue;
++ }
++
++ bool isPreprocessor = c.startsWith('#');
++ if (isPreprocessor) {
++ // Preprocesser lines have zero intent
++ out << c;
++ continue;
++ }
++
++ bool isTag = c.startsWith('@');
++
++ // Separate content after "//" to commenPart
++ QString commentPart;
++ int lineCommentIndex = c.indexOf("//");
++ if (lineCommentIndex > -1) {
++ commentPart = c.last(c.length() - lineCommentIndex);
++ c = c.first(lineCommentIndex);
++ // Move spaces from c to comment part (to also trim c)
++ while (!c.isEmpty() && c.endsWith(' ')) {
++ commentPart.prepend(' ');
++ c.chop(1);
++ }
++ }
++
++ // Multiline comments state
++ int startComments = c.count("/*");
++ int endComments = c.count("*/");
++ int commendsChange = startComments - endComments;
++ if (commendsChange > 0)
++ multilineComment = true;
++ else if (commendsChange < 0)
++ multilineComment = false;
++
++ if (multilineComment || endComments > startComments) {
++ // Lines inside /* .. */ are not touched
++ out << cOrig;
++ continue;
++ }
++
++ // Check indent for next line
++ int indexChange = 0;
++ int startBlocks = c.count('{');
++ int endBlocks = c.count('}');
++ indexChange += startBlocks - endBlocks;
++ int startBrackets = c.count('(');
++ int endBrackets = c.count(')');
++ indexChange += startBrackets - endBrackets;
++ index += indexChange;
++ index = std::max(index, 0);
++ indent.clear();
++ int currentIndex = indexChange > 0 ? prevIndex : index;
++ if (!isTag) {
++ if (currentIndex > 0 && startBlocks > 0 && endBlocks > 0) {
++ // Note: "} else {", "} else if () {"
++ // Indent one step lower
++ currentIndex--;
++ } else if (endBrackets > startBrackets) {
++ // Note: "variable)"
++ // Indent one step higher
++ currentIndex++;
++ }
++ if (!c.startsWith('{'))
++ currentIndex += nextLineIndents;
++
++ if (!c.isEmpty() && startBlocks == 0 && endBlocks == 0 && indexChange == 0 && !c.endsWith(';') && !c.endsWith(',') && !c.endsWith('(') && !c.endsWith("*/")) {
++ // Note: "if ()", "else if ()", "else", "for (..)"
++ // Something that should continue to next line
++ nextLineIndents++;
++ } else if (nextLineIndents > 0) {
++ // Return to indent before e.g. "if (thing) \n if (thing2) \n something;"
++ nextLineIndents = indentBeforeSingles;
++ indentBeforeSingles = nextLineIndents;
++ }
++ }
++
++ // Apply indent
++ QString singleIndentStep = QStringLiteral(" ");
++ for (int i = 0; i < currentIndex; i++)
++ indent += singleIndentStep;
++ c.prepend(indent);
++
++ out << (c + commentPart);
++ prevIndex = index;
++ }
++ return out.join('\n');
++ }
++
++ // Takes in the previous line (before pressing return key)
++ // and returns suitable indent string with spaces.
++ QString CodeHelper::autoIndentGLSLNextLine(const QString &codeLine, bool multilineComment)
++ {
++ int spaces = 0;
++
++ // Check how many spaces previous line had
++ for (int i = 0; i < codeLine.size() ; i++) {
++ if (codeLine.at(i) == ' ')
++ spaces++;
++ else
++ break;
++ }
++
++ QString c = codeLine.trimmed();
++ bool isPreprocessor = c.startsWith('#');
++ bool isTag = c.startsWith('@');
++
++ // Remove content after "//"
++ int lineCommentIndex = c.indexOf("//");
++ if (lineCommentIndex > -1) {
++ c = c.first(lineCommentIndex);
++ c = c.trimmed();
++ }
++
++ if (!c.isEmpty() && !isPreprocessor && !isTag && !multilineComment) {
++ // Check indent for next line
++ int index = 0;
++ int indexChange = 0;
++ int startBlocks = c.count('{');
++ int endBlocks = c.count('}');
++ indexChange += startBlocks - endBlocks;
++ int startBrackets = c.count('(');
++ int endBrackets = c.count(')');
++ indexChange += startBrackets - endBrackets;
++ if (indexChange > 0) {
++ // Note: "{" "if () {", "vec4 x = vec4("
++ index++;
++ } else if (indexChange < 0 && c.trimmed().size() > 1) {
++ // Note: "something; }", but NOT "}" as it has be unindented already
++ index--;
++ }
++
++ if (index == 0 && !c.isEmpty() && !c.endsWith(';') && !c.endsWith("*/") && !c.endsWith('}')) {
++ // Note: "if ()", "else if ()", "else", "for (..)"
++ // Something that should continue to next line
++ index++;
++ }
++ spaces += 4 * index;
++ }
++
++ QString indent;
++ indent.fill(' ', spaces);
++ return '\n' + indent;
++ }
++
++ // Get current word under the cursor
++ // Word is letters, digits and "_".
++ // Also if word starts with "//" that is included
++ QString CodeHelper::getCurrentWord(QQuickTextEdit *textEdit)
++ {
++ if (!textEdit)
++ return QString();
++ int cursorPosition = textEdit->cursorPosition();
++ int cPos = cursorPosition - 1;
++ int maxPos = textEdit->text().size();
++ QString currentWord;
++ QChar c = textEdit->getText(cPos, cPos + 1).front();
++ while (cPos >= 0 && (c.isLetterOrNumber() || c == '_')) {
++ currentWord.prepend(c);
++ cPos--;
++ c = textEdit->getText(cPos, cPos + 1).front();
++ }
++ // Special case of "@" tags
++ if (cPos >= 1) {
++ QString s = textEdit->getText(cPos, cPos + 1);
++ if (s == QStringLiteral("@"))
++ currentWord.prepend(QStringLiteral("@"));
++ }
++ cPos = cursorPosition;
++ c = textEdit->getText(cPos, cPos + 1).front();
++ while (cPos <= maxPos && (c.isLetterOrNumber() || c == '_')) {
++ currentWord.append(c);
++ cPos++;
++ c = textEdit->getText(cPos, cPos + 1).front();
++ }
++ return currentWord;
++ }
++
++ // Remove the current word under cursor
++ void CodeHelper::removeCurrentWord(QQuickTextEdit *textEdit)
++ {
++ if (!textEdit)
++ return;
++ int cursorPosition = textEdit->cursorPosition();
++ int cPos = cursorPosition - 1;
++ int maxPos = textEdit->text().size();
++ int firstPos = 0;
++ int lastPos = 0;
++ QChar c = textEdit->getText(cPos, cPos + 1).front();
++ while (cPos >= 0 && (c.isLetterOrNumber() || c == '_')) {
++ cPos--;
++ c = textEdit->getText(cPos, cPos + 1).front();
++ }
++ // Special case of "@" tags
++ if (cPos >= 1) {
++ QString s = textEdit->getText(cPos, cPos + 1);
++ if (s == QStringLiteral("@"))
++ cPos--;
++ }
++ firstPos = cPos + 1;
++ cPos = cursorPosition;
++ c = textEdit->getText(cPos, cPos + 1).front();
++ while (cPos <= maxPos && (c.isLetterOrNumber() || c == '_')) {
++ cPos++;
++ c = textEdit->getText(cPos, cPos + 1).front();
++ }
++ lastPos = cPos;
++ textEdit->remove(firstPos, lastPos);
++ }
++
++ bool CodeHelper::codeCompletionVisible() const
++ {
++ return m_codeCompletionVisible;
++ }
++
++ void CodeHelper::setCodeCompletionVisible(bool visible)
++ {
++ if (m_codeCompletionVisible == visible)
++ return;
++
++ m_codeCompletionVisible = visible;
++ if (!m_codeCompletionVisible)
++ m_codeCompletionModel->setCurrentIndex(0);
++
++ Q_EMIT codeCompletionVisibleChanged();
++ }
++
++ // Update and show code completion popup
++ // When force is true, do this without a delay.
++ void CodeHelper::updateCodeCompletion(QQuickTextEdit *textEdit, bool force)
++ {
++ m_textEdit = textEdit;
++ if (force)
++ showCodeCompletion();
++ else
++ m_codeCompletionTimer.start();
++ }
++
++ void CodeHelper::showCodeCompletion()
++ {
++ if (!m_textEdit)
++ return;
++
++ QString currentWord = getCurrentWord(m_textEdit);
++ QString currentWordCleaned = currentWord;
++ bool isComment = false;
++
++ // Check if word is comment/tag
++ if (currentWordCleaned.size() >= 2 && currentWordCleaned.left(2) == "//") {
++ isComment = true;
++ currentWordCleaned = currentWordCleaned.right(currentWordCleaned.size() - 2);
++ }
++
++ m_codeCompletionModel->beginResetModel();
++ m_codeCompletionModel->clearItems();
++
++ if (!isComment) {
++ for (const auto &a : m_reservedArgumentNames) {
++ if (a.startsWith(currentWordCleaned, Qt::CaseInsensitive) && a.size() > currentWordCleaned.size())
++ m_codeCompletionModel->addItem(a, CodeCompletionModel::TypeArgument);
++ }
++
++ for (const auto &a : m_reservedFunctionNames) {
++ if (a.startsWith(currentWordCleaned, Qt::CaseInsensitive) && a.size() > currentWordCleaned.size())
++ m_codeCompletionModel->addItem(a, CodeCompletionModel::TypeFunction);
++ }
++
++ for (const auto &a : m_reservedTagNames) {
++ if (a.startsWith(currentWord, Qt::CaseInsensitive) && a.size() > currentWord.size())
++ m_codeCompletionModel->addItem(a, CodeCompletionModel::TypeTag);
++ }
++ }
++
++ m_codeCompletionModel->endResetModel();
++
++ if (!m_codeCompletionModel->m_modelList.isEmpty()) {
++ m_codeCompletionModel->setCurrentIndex(0);
++ setCodeCompletionVisible(true);
++ } else {
++ setCodeCompletionVisible(false);
++ }
++ }
++
++ CodeCompletionModel *CodeHelper::codeCompletionModel() const
++ {
++ return m_codeCompletionModel;
++ }
++
++ void CodeHelper::applyCodeCompletion(QQuickTextEdit *textEdit)
++ {
++ CodeCompletionModel::ModelData t = m_codeCompletionModel->currentItem();
++ if (!t.name.isEmpty()) {
++ // Replace the current word with code completion one
++ removeCurrentWord(textEdit);
++ textEdit->insert(textEdit->cursorPosition(), t.name);
++ if (t.type == CodeCompletionModel::TypeFunction) {
++ // For functions, place cursor between parentheses, e.g. "sin(|)".
++ textEdit->setCursorPosition(textEdit->cursorPosition() - 1);
++ }
++ }
++ setCodeCompletionVisible(false);
++ }