Qt5 Syntax Highlighting in QML

Qt Quick's TextEdit item exposes a textDocument property, of type QQuickTextDocument. This is explicitly exposed so you can use QSyntaxHighlighter directly with the document.

QtQuick textEdit documentation for Qt 5.3


I have two answers:

  1. a pure QML answer
  2. a C++ answer involving QSyntaxHighlighter

For a pure QML answer, we can use a TextArea one can set TextArea::textFormat to textFormat: TextEdit.RichText for formatting. We can use TextArea::getText() to get the plain text and set TextArea::text with the rich text. Here's a mock example that:

  • color uppercase identifiers (e.g. Button) as purple
  • color lowercase identifiers (e.g. x y z) as red
  • color numbers (e.g. 123 456) as blue
  • but leave symbols (e.g. = + ;) as black

Here's the snippet:

    TextArea {
        id: output

        property bool processing: false

        text: "<p>x = 123;</p><p>y = 456;</p><p>z = x + y;</p>"
        textFormat: TextEdit.RichText
        selectByMouse: true

        onTextChanged: {
            if (!processing) {
                processing = true;
                let p = cursorPosition;
                let markUp = getText(0, length).replace(
                  /([A-Z][A-Za-z]*|[a-z][A-Za-z]*|[0-9]+|[ \t\n]|['][^']*[']|[^A-Za-z0-9\t\n ])/g,
                    function (f) {
                        console.log("f: ", JSON.stringify(f));
                        if (f.match(/^[A-Z][A-Za-z]*$/))
                            return "<span style='color:#800080'>" + f + "</span>";
                        if (f.match(/^[a-z][A-Za-z]*$/))
                            return "<span style='color:#800000'>" + f + "</span>";
                        else if (f.match(/^[0-9]+$/))
                            return "<span style='color:#0000ff'>" + f + "</span>";
                        else if (f.match(/^[ ]/))
                            return "&nbsp;"
                        else if (f.match(/^[\t\n]/))
                            return f;
                        else if (f.match(/^[']/))
                            return "<span style='color:#008000'>" + f + "</span>";
                        else
                            return f;
                    }
                );
                text = markUp;
                cursorPosition = p;
                processing = false;
            }
        }
    }

To use Qt's QSyntaxHighlighter, you need the following:

  1. In QML, use TextEdit QML type in your application
  2. In C++, define a QSyntaxHighlighter and connect TextEdit QML type to it via the TextEdit::textDocument property
  3. In C++, implement QSyntaxHighlighter::highlightBlock( const QString& text ) in your derived class, calling QSyntaxHighlighter::setFormat() as often as needed to tokenize the text found.

To make things easier, I create a sample app https://github.com/stephenquan/QtSyntaxHighlighterApp which wraps QSyntaxHighlighter and QTextFormat as SyntaxHighlighter and TextFormat QML Types. That way, one can handle onHighlightBlock signal and put the business logic of the syntax highlighter in Javascript instead of C++:

TextEdit {
    id: textEdit
    selectByMouse: true
    text: [
        "import QtQuick 2.12",
        "",
        "Item {",
        "    Rectangle {",
        "        width: 50",
        "        height: 50",
        "        color: '#800000'",
        "    }",
        "}",
    ].join("\n") + "\n"
    font.pointSize: 12
}

SyntaxHighlighter {
    id: syntaxHighlighter
    textDocument: textEdit.textDocument
    onHighlightBlock: {
        let rx = /\/\/.*|[A-Za-z.]+(\s*:)?|\d+(.\d*)?|'[^']*?'|"[^"]*?"/g;
        let m;
        while ( ( m = rx.exec(text) ) !== null ) {
            if (m[0].match(/^\/\/.*/)) {
                setFormat(m.index, m[0].length, commentFormat);
                continue;
            }
            if (m[0].match(/^[a-z][A-Za-z.]*\s*:/)) {
                setFormat(m.index, m[0].match(/^[a-z][A-Za-z.]*/)[0].length, propertyFormat);
                continue;
            }
            if (m[0].match(/^[a-z]/)) {
                let keywords = [ 'import', 'function', 'bool', 'var',
                                'int', 'string', 'let', 'const', 'property',
                                'if', 'continue', 'for', 'break', 'while',
                    ];
                if (keywords.includes(m[0])) {
                    setFormat(m.index, m[0].length, keywordFormat);
                    continue;
                }
                continue;
            }
            if (m[0].match(/^[A-Z]/)) {
                setFormat(m.index, m[0].length, componentFormat);
                continue;
            }
            if (m[0].match(/^\d/)) {
                setFormat(m.index, m[0].length, numberFormat);
                continue;
            }
            if (m[0].match(/^'/)) {
                setFormat(m.index, m[0].length, stringFormat);
                continue;
            }
            if (m[0].match(/^"/)) {
                setFormat(m.index, m[0].length, stringFormat);
                continue;
            }
        }
    }
}

TextCharFormat { id: keywordFormat; foreground: "#808000" }
TextCharFormat { id: componentFormat; foreground: "#aa00aa"; font.pointSize: 12; font.bold: true; font.italic: true }
TextCharFormat { id: numberFormat; foreground: "#0055af" }
TextCharFormat { id: propertyFormat; foreground: "#800000" }
TextCharFormat { id: stringFormat; foreground: "green" }
TextCharFormat { id: commentFormat; foreground: "green" }