Merge pull request #13280 from jordan-woyak/input-expressions-highlighting

InputCommon/DolphinQt: Fix sometimes broken syntax highlighting in IOWindow.
This commit is contained in:
JMC47 2025-02-02 02:01:34 -05:00 committed by GitHub
commit 8291cff46d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 90 additions and 71 deletions

View File

@ -19,6 +19,7 @@
#include <QSlider> #include <QSlider>
#include <QSpinBox> #include <QSpinBox>
#include <QTableWidget> #include <QTableWidget>
#include <QTextBlock>
#include <QTimer> #include <QTimer>
#include <QVBoxLayout> #include <QVBoxLayout>
@ -111,8 +112,9 @@ QTextCharFormat GetCommentCharFormat()
} // namespace } // namespace
ControlExpressionSyntaxHighlighter::ControlExpressionSyntaxHighlighter(QTextDocument* parent) ControlExpressionSyntaxHighlighter::ControlExpressionSyntaxHighlighter(QTextDocument* parent)
: QSyntaxHighlighter(parent) : QObject(parent)
{ {
connect(parent, &QTextDocument::contentsChanged, this, [this, parent]() { Highlight(parent); });
} }
void QComboBoxWithMouseWheelDisabled::wheelEvent(QWheelEvent* event) void QComboBoxWithMouseWheelDisabled::wheelEvent(QWheelEvent* event)
@ -120,39 +122,32 @@ void QComboBoxWithMouseWheelDisabled::wheelEvent(QWheelEvent* event)
// Do nothing // Do nothing
} }
void ControlExpressionSyntaxHighlighter::highlightBlock(const QString&) void ControlExpressionSyntaxHighlighter::Highlight(QTextDocument* document)
{ {
// TODO: This is going to result in improper highlighting with non-ascii characters: // toLatin1 converts multi-byte unicode characters to a single-byte character,
ciface::ExpressionParser::Lexer lexer(document()->toPlainText().toStdString()); // so Token string_position values are the character counts that Qt's FormatRange expects.
ciface::ExpressionParser::Lexer lexer(document->toPlainText().toLatin1().toStdString());
std::vector<ciface::ExpressionParser::Token> tokens; std::vector<ciface::ExpressionParser::Token> tokens;
const auto tokenize_status = lexer.Tokenize(tokens); const auto tokenize_status = lexer.Tokenize(tokens);
using ciface::ExpressionParser::TokenType; if (ciface::ExpressionParser::ParseStatus::Successful == tokenize_status)
const auto set_block_format = [this](int start, int count, const QTextCharFormat& format) {
if (start + count <= currentBlock().position() ||
start >= currentBlock().position() + currentBlock().length())
{ {
// This range is not within the current block. const auto parse_status = ciface::ExpressionParser::ParseTokens(tokens);
return; if (ciface::ExpressionParser::ParseStatus::Successful != parse_status.status)
{
auto token = *parse_status.token;
// Add invalid version of token where parsing failed for appropriate error-highlighting.
token.type = ciface::ExpressionParser::TOK_INVALID;
tokens.emplace_back(token);
}
} }
int block_start = start - currentBlock().position(); auto get_token_char_format = [](const ciface::ExpressionParser::Token& token) {
if (block_start < 0)
{
count += block_start;
block_start = 0;
}
setFormat(block_start, count, format);
};
for (auto& token : tokens)
{
std::optional<QTextCharFormat> char_format; std::optional<QTextCharFormat> char_format;
using ciface::ExpressionParser::TokenType;
switch (token.type) switch (token.type)
{ {
case TokenType::TOK_INVALID: case TokenType::TOK_INVALID:
@ -186,22 +181,50 @@ void ControlExpressionSyntaxHighlighter::highlightBlock(const QString&)
break; break;
} }
return char_format;
};
// FYI, formatting needs to be done at the block level to prevent altering of undo/redo history.
for (QTextBlock block = document->begin(); block.isValid(); block = block.next())
{
block.layout()->clearFormats();
const int block_position = block.position();
const int block_length = block_position + block.length();
QList<QTextLayout::FormatRange> format_ranges;
for (auto& token : tokens)
{
int token_length = int(token.string_length);
int token_start = int(token.string_position) - block_position;
if (token_start < 0)
{
token_length += token_start;
token_start = 0;
}
if (token_length <= 0)
{
// Token is in a previous block.
continue;
}
if (token_start >= block_length)
{
// Token is in a following block.
break;
}
const auto char_format = get_token_char_format(token);
if (char_format.has_value()) if (char_format.has_value())
set_block_format(int(token.string_position), int(token.string_length), *char_format); {
format_ranges.emplace_back(QTextLayout::FormatRange{
.start = token_start, .length = token_length, .format = *char_format});
}
} }
// This doesn't need to be run for every "block", but it works. block.layout()->setFormats(format_ranges);
if (ciface::ExpressionParser::ParseStatus::Successful == tokenize_status)
{
ciface::ExpressionParser::RemoveInertTokens(&tokens);
const auto parse_status = ciface::ExpressionParser::ParseTokens(tokens);
if (ciface::ExpressionParser::ParseStatus::Successful != parse_status.status)
{
const auto token = *parse_status.token;
set_block_format(int(token.string_position), int(token.string_length),
GetInvalidCharFormat());
}
} }
} }

View File

@ -10,7 +10,6 @@
#include <QComboBox> #include <QComboBox>
#include <QDialog> #include <QDialog>
#include <QString> #include <QString>
#include <QSyntaxHighlighter>
#include "InputCommon/ControllerInterface/CoreDevice.h" #include "InputCommon/ControllerInterface/CoreDevice.h"
@ -26,6 +25,7 @@ class QPlainTextEdit;
class QPushButton; class QPushButton;
class QSlider; class QSlider;
class QSpinBox; class QSpinBox;
class QTextDocument;
namespace ControllerEmu namespace ControllerEmu
{ {
@ -34,14 +34,14 @@ class EmulatedController;
class InputStateLineEdit; class InputStateLineEdit;
class ControlExpressionSyntaxHighlighter final : public QSyntaxHighlighter class ControlExpressionSyntaxHighlighter final : public QObject
{ {
Q_OBJECT Q_OBJECT
public: public:
explicit ControlExpressionSyntaxHighlighter(QTextDocument* parent); explicit ControlExpressionSyntaxHighlighter(QTextDocument* parent);
protected: private:
void highlightBlock(const QString& text) final override; void Highlight(QTextDocument* text_edit);
}; };
class QComboBoxWithMouseWheelDisabled : public QComboBox class QComboBoxWithMouseWheelDisabled : public QComboBox

View File

@ -188,7 +188,19 @@ Token Lexer::NextToken()
case '*': case '*':
return Token(TOK_MUL); return Token(TOK_MUL);
case '/': case '/':
{
// Handle /* */ style comments.
if (it != expr.end() && *it == '*')
{
++it;
const auto end_of_comment = expr.find("*/", it - expr.begin());
if (end_of_comment == std::string::npos)
return Token(TOK_INVALID);
it = expr.begin() + end_of_comment + 2;
return Token(TOK_COMMENT);
}
return Token(TOK_DIV); return Token(TOK_DIV);
}
case '%': case '%':
return Token(TOK_MOD); return Token(TOK_MOD);
case '=': case '=':
@ -221,26 +233,10 @@ ParseStatus Lexer::Tokenize(std::vector<Token>& tokens)
{ {
while (true) while (true)
{ {
const std::size_t string_position = it - expr.begin(); const std::string::iterator prev_it = it;
Token tok = NextToken(); Token tok = NextToken();
tok.string_position = prev_it - expr.begin();
tok.string_position = string_position; tok.string_length = it - prev_it;
tok.string_length = it - expr.begin();
// Handle /* */ style comments.
if (tok.type == TOK_DIV && PeekToken().type == TOK_MUL)
{
const auto end_of_comment = expr.find("*/", it - expr.begin());
if (end_of_comment == std::string::npos)
return ParseStatus::SyntaxError;
tok.type = TOK_COMMENT;
tok.string_length = end_of_comment + 4;
it = expr.begin() + end_of_comment + 2;
}
tokens.push_back(tok); tokens.push_back(tok);
if (tok.type == TOK_INVALID) if (tok.type == TOK_INVALID)
@ -682,6 +678,11 @@ ParseResult ParseResult::MakeErrorResult(Token token, std::string description)
return result; return result;
} }
bool IsInertToken(const Token& tok)
{
return tok.type == TOK_COMMENT || tok.type == TOK_WHITESPACE;
}
class Parser class Parser
{ {
public: public:
@ -711,7 +712,12 @@ private:
return tok; return tok;
} }
Token Peek() { return *m_it; } Token Peek()
{
while (IsInertToken(*m_it))
++m_it;
return *m_it;
}
bool Expects(TokenType type) bool Expects(TokenType type)
{ {
@ -1000,18 +1006,9 @@ static ParseResult ParseComplexExpression(const std::string& str)
if (tokenize_status != ParseStatus::Successful) if (tokenize_status != ParseStatus::Successful)
return ParseResult::MakeErrorResult(Token(TOK_INVALID), return ParseResult::MakeErrorResult(Token(TOK_INVALID),
Common::GetStringT("Tokenizing failed.")); Common::GetStringT("Tokenizing failed."));
RemoveInertTokens(&tokens);
return ParseTokens(tokens); return ParseTokens(tokens);
} }
void RemoveInertTokens(std::vector<Token>* tokens)
{
std::erase_if(*tokens, [](const Token& tok) {
return tok.type == TOK_COMMENT || tok.type == TOK_WHITESPACE;
});
}
static std::unique_ptr<Expression> ParseBarewordExpression(const std::string& str) static std::unique_ptr<Expression> ParseBarewordExpression(const std::string& str)
{ {
ControlQualifier qualifier; ControlQualifier qualifier;

View File

@ -197,6 +197,5 @@ private:
ParseResult ParseExpression(const std::string& expr); ParseResult ParseExpression(const std::string& expr);
ParseResult ParseTokens(const std::vector<Token>& tokens); ParseResult ParseTokens(const std::vector<Token>& tokens);
void RemoveInertTokens(std::vector<Token>* tokens);
} // namespace ciface::ExpressionParser } // namespace ciface::ExpressionParser