Blob Blame History Raw
/*
    Copyright (c) 2017, Lukas Holecek <hluk@email.cz>

    This file is part of CopyQ.

    CopyQ is free software: you can redistribute it and/or modify
    it under the terms of the GNU General Public License as published by
    the Free Software Foundation, either version 3 of the License, or
    (at your option) any later version.

    CopyQ is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU General Public License for more details.

    You should have received a copy of the GNU General Public License
    along with CopyQ.  If not, see <http://www.gnu.org/licenses/>.
*/

#include "commandsyntaxhighlighter.h"

#include "gui/iconfactory.h"
#include "scriptable/scriptable.h"

#include <QMetaMethod>
#include <QMetaObject>
#include <QPalette>
#include <QRegExp>
#include <QScriptEngine>
#include <QScriptValue>
#include <QScriptValueIterator>
#include <QSyntaxHighlighter>
#include <QTextEdit>
#include <QPlainTextEdit>

namespace {

QString methodName(const QMetaMethod &method)
{
    QString name =
#if QT_VERSION < 0x050000
            method.signature();
#else
            QString::fromLatin1(method.methodSignature());
#endif

    const int index = name.indexOf('(');

    if (index <= 0)
        return QString();

    return name.remove(index, name.length() - index);
}

QRegExp commandLabelRegExp()
{
    return QRegExp(
            "\\bcopyq:"
            "|\\bsh:"
            "|\\bbash:"
            "|\\bpowershell:"
            "|\\bperl:"
            "|\\bpython:"
            "|\\bruby:"
            "|\\bpowershell:"
                );
}

QRegExp createRegExp(const QStringList &list)
{
    QRegExp re;
    re.setPattern("\\b" + list.join("\\b|\\b") + "\\b");
    return re;
}

int mixColorComponent(int a, int b)
{
    return qMin(255, qMax(0, a + b));
}

QColor mixColor(const QColor &color, int r, int g, int b)
{
    return QColor(
                mixColorComponent(color.red(), r),
                mixColorComponent(color.green(), g),
                mixColorComponent(color.blue(), b),
                color.alpha()
                );
}

class CommandSyntaxHighlighter : public QSyntaxHighlighter
{
public:
    explicit CommandSyntaxHighlighter(QWidget *editor, QTextDocument *parent)
        : QSyntaxHighlighter(parent)
        , m_editor(editor)
        , m_reObjects(createRegExp(scriptableObjects()))
        , m_reProperties(createRegExp(scriptableProperties()))
        , m_reFunctions(createRegExp(scriptableFunctions()))
        , m_reKeywords(createRegExp(scriptableKeywords()))
        , m_reLabels(commandLabelRegExp())
        , m_reNumbers("(?:\\b|%)\\d+")
    {
    }

protected:
    void highlightBlock(const QString &text) override
    {
        m_bgColor = getDefaultIconColor(*m_editor);

        QTextCharFormat objectsFormat;
        objectsFormat.setForeground(mixColor(m_bgColor, 40, -60, 40));
        objectsFormat.setToolTip("Object");
        highlight(text, m_reObjects, objectsFormat);

        QTextCharFormat propertyFormat;
        propertyFormat.setForeground(mixColor(m_bgColor, -60, 40, 40));
        highlight(text, m_reProperties, propertyFormat);

        QTextCharFormat functionFormat;
        functionFormat.setForeground(mixColor(m_bgColor, -40, -40, 40));
        highlight(text, m_reFunctions, functionFormat);

        QTextCharFormat keywordFormat;
        keywordFormat.setFontWeight(QFont::Bold);
        highlight(text, m_reKeywords, keywordFormat);

        QTextCharFormat labelsFormat;
        labelsFormat.setFontWeight(QFont::Bold);
        labelsFormat.setForeground(mixColor(m_bgColor, 40, 40, -40));
        highlight(text, m_reLabels, labelsFormat);

        QTextCharFormat numberFormat;
        numberFormat.setForeground(mixColor(m_bgColor, 40, -40, -40));
        highlight(text, m_reNumbers, numberFormat);

        highlightBlocks(text);
    }

private:
    enum State {
        Code,
        SingleQuote,
        DoubleQuote,
        RegExp,
        Comment
    };

    void highlight(const QString &text, QRegExp &re, const QTextCharFormat &format)
    {
        int index = text.indexOf(re);

        while (index >= 0) {
            const int length = re.matchedLength();
            setFormat(index, length, format);
            index = text.indexOf( re, index + qMax(1, length) );
        }
    }

    void format(int a, int b)
    {
        QTextCharFormat format;

        if (currentBlockState() == SingleQuote || currentBlockState() == DoubleQuote) {
            format.setForeground(mixColor(m_bgColor, -40, 40, -40));
        } else if (currentBlockState() == Comment) {
            const int x = m_bgColor.lightness() > 100 ? -40 : 40;
            format.setForeground( mixColor(m_bgColor, x, x, x) );
        } else if (currentBlockState() == RegExp) {
            format.setForeground(mixColor(m_bgColor, 40, -40, -40));
        } else {
            return;
        }

        setFormat(a, b - a + 1, format);
    }

    bool peek(const QString &text, int i, const QString &what)
    {
        return text.midRef(i, what.size()) == what;
    }

    void highlightBlocks(const QString &text)
    {
        bool escape = false;

        setCurrentBlockState(previousBlockState());

        int a = 0;

        for (int i = 0; i < text.size(); ++i) {
            const QChar c = text[i];
            if (escape) {
                escape = false;
            } else if (c == '\\') {
                escape = true;
            } else if (currentBlockState() == SingleQuote) {
                if (c == '\'') {
                    format(a, i);
                    setCurrentBlockState(Code);
                }
            } else if (currentBlockState() == DoubleQuote) {
                if (c == '"') {
                    format(a, i);
                    setCurrentBlockState(Code);
                }
            } else if (currentBlockState() == Comment) {
                if ( peek(text, i, "*/") ) {
                    ++i;
                    format(a, i);
                    setCurrentBlockState(Code);
                }
            } else if (currentBlockState() == RegExp) {
                if (c == '/') {
                    // Highlight paths outside code as regexps.
                    i = text.indexOf(QRegExp("[^a-zA-Z0-9./_-]"), i);
                    if (i == -1)
                        i = text.size();

                    format(a, i);

                    setCurrentBlockState(Code);
                }
            } else if (c == '\\') {
                escape = true;
            } else if ( c == '#' || peek(text, i, "//") ) {
                setCurrentBlockState(Comment);
                format(i, text.size());
                setCurrentBlockState(Code);
                return;
            } else if ( peek(text, i, "/*") ) {
                a = i;
                ++i;
                setCurrentBlockState(Comment);
            } else if (c == '\'') {
                a = i;
                setCurrentBlockState(SingleQuote);
            } else if (c == '"') {
                a = i;
                setCurrentBlockState(DoubleQuote);
            } else if (c == '/') {
                a = i;
                setCurrentBlockState(RegExp);
            }
        }

        format(a, text.size());
    }

    QWidget *m_editor;
    QRegExp m_reObjects;
    QRegExp m_reProperties;
    QRegExp m_reFunctions;
    QRegExp m_reKeywords;
    QRegExp m_reLabels;
    QRegExp m_reNumbers;
    QColor m_bgColor;
};

} // namespace

QStringList scriptableKeywords()
{
    return QStringList()
            << "arguments"
            << "break"
            << "do"
            << "instanceof"
            << "typeof"
            << "case"
            << "else"
            << "new"
            << "var"
            << "catch"
            << "finally"
            << "return"
            << "void"
            << "continue"
            << "for"
            << "switch"
            << "while"
            << "debugger"
            << "function"
            << "this"
            << "with"
            << "default"
            << "if"
            << "throw"
            << "delete"
            << "in"
            << "try"
               ;
}

QStringList scriptableProperties()
{
    QStringList result;

    QMetaObject scriptableMetaObject = Scriptable::staticMetaObject;
    for (int i = 0; i < scriptableMetaObject.propertyCount(); ++i) {
        QMetaProperty property = scriptableMetaObject.property(i);
        result.append(property.name());
    }

    result.removeOne("objectName");

    return result;
}

QStringList scriptableFunctions()
{
    QStringList result;

    QMetaObject scriptableMetaObject = Scriptable::staticMetaObject;
    for (int i = 0; i < scriptableMetaObject.methodCount(); ++i) {
        QMetaMethod method = scriptableMetaObject.method(i);

        if (method.methodType() == QMetaMethod::Slot && method.access() == QMetaMethod::Public) {
            const QString name = methodName(method);
            result.append(name);
        }
    }

    result.removeOne("deleteLater");

    return result;
}

QStringList scriptableObjects()
{
    QStringList result;
    result.append("ByteArray");
    result.append("Dir");
    result.append("File");
    result.append("TemporaryFile");

    QScriptEngine engine;

    QScriptValue globalObject = engine.globalObject();
    QScriptValueIterator it(globalObject);

    while (it.hasNext()) {
        it.next();
        result.append(it.name());
    }

    return result;
}

void installCommandSyntaxHighlighter(QTextEdit *editor)
{
    new CommandSyntaxHighlighter(editor, editor->document());
}

void installCommandSyntaxHighlighter(QPlainTextEdit *editor)
{
    new CommandSyntaxHighlighter(editor, editor->document());
}