// 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 CodeHelper::CodeHelper(QObject *parent) : QObject{parent} { m_effectManager = static_cast(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); }