From 22bc70cd1af579c24da5d8433ebdbd63c574fd49 Mon Sep 17 00:00:00 2001 From: Mark Liversedge Date: Sat, 15 Aug 2015 23:47:04 +0100 Subject: [PATCH] Formula Editor Improvements 1 of 2 .. update the editor to do some basic syntax highlighting; literals in red, comments in blue and so on. .. next commit will focus on highlighting errors with a wavy line and some form of error list. --- src/DataFilter.cpp | 169 ++++++++++++++++++++++++++++++++++++++++++++- src/DataFilter.h | 8 ++- src/DataFilter.l | 8 +++ src/DataFilter.y | 55 ++++++++------- src/LTMTool.cpp | 32 ++++++++- src/LTMTool.h | 7 +- 6 files changed, 246 insertions(+), 33 deletions(-) diff --git a/src/DataFilter.cpp b/src/DataFilter.cpp index 6a832c02b..880e01477 100644 --- a/src/DataFilter.cpp +++ b/src/DataFilter.cpp @@ -155,6 +155,144 @@ Leaf::isDynamic(Leaf *leaf) return false; } +void +DataFilter::colorSyntax(QTextDocument *document) +{ + // for looking for comments + QString string = document->toPlainText(); + + // clear formatting and color comments + QTextCharFormat normal; + normal.setUnderlineStyle(QTextCharFormat::NoUnderline); + normal.setForeground(Qt::black); + + QTextCharFormat comment; + comment.setUnderlineStyle(QTextCharFormat::NoUnderline); + comment.setForeground(Qt::blue); + + QTextCursor cursor(document); + + // set all black + cursor.movePosition(QTextCursor::Start, QTextCursor::MoveAnchor); + cursor.selectionStart(); + cursor.movePosition(QTextCursor::End, QTextCursor::KeepAnchor); + cursor.selectionEnd(); + cursor.setCharFormat(normal); + + // color comments + bool instring=false; + bool incomment=false; + int commentstart=0; + + for(int i=0; icolor(treeRoot, document); +} + +void Leaf::color(Leaf *leaf, QTextDocument *document) +{ + // nope + if (leaf == NULL) return; + + QTextCharFormat apply; + apply.setForeground(Qt::red); // default to yucky + + switch(leaf->type) { + case Leaf::Float : + case Leaf::Integer : + case Leaf::String : + apply.setForeground(Qt::red); + break; + + case Leaf::Symbol : + apply.setForeground(Qt::green); + break; + + case Leaf::Logical : + leaf->color(leaf->lvalue.l, document); + if (leaf->op) leaf->color(leaf->rvalue.l, document); + return; + break; + + case Leaf::Operation : + leaf->color(leaf->lvalue.l, document); + leaf->color(leaf->rvalue.l, document); + return; + break; + + case Leaf::BinaryOperation : + leaf->color(leaf->lvalue.l, document); + leaf->color(leaf->rvalue.l, document); + return; + break; + + case Leaf::Function : + if (leaf->series) { + if (leaf->lvalue.l) leaf->color(leaf->lvalue.l, document); + return; + } else { + foreach(Leaf*l, leaf->fparms) leaf->color(l, document); + apply.setForeground(Qt::green); + } + break; + + case Leaf::Conditional : + { + leaf->color(leaf->cond.l, document); + leaf->color(leaf->lvalue.l, document); + leaf->color(leaf->rvalue.l, document); + return; + } + break; + + default: + return; + break; + + } + + // lets apply that then + QTextCursor cursor(document); + + // highlight this token and apply + cursor.setPosition(leaf->loc, QTextCursor::MoveAnchor); + cursor.selectionStart(); + cursor.setPosition(leaf->leng+1, QTextCursor::KeepAnchor); + cursor.selectionEnd(); + + cursor.setCharFormat(apply); +} + void Leaf::print(Leaf *leaf, int level) { qDebug()<<"LEVEL"<function == "config" && !configValidSymbols.exactMatch(symbol)) - DataFiltererrors << QString(QObject::tr("invalid data series for config(): %1")).arg(symbol); + DataFiltererrors << QString(QObject::tr("invalid literal for config(): %1")).arg(symbol); if (leaf->function == "const") { if (!constValidSymbols.exactMatch(symbol)) @@ -473,6 +611,35 @@ Result DataFilter::evaluate(RideItem *item) return res; } +QStringList DataFilter::check(QString query) +{ + // remember where we apply + isdynamic=false; + + // Parse from string + DataFiltererrors.clear(); // clear out old errors + DataFilter_setString(query); + DataFilterparse(); + DataFilter_clearString(); + + // save away the results + treeRoot = root; + + // if it passed syntax lets check semantics + if (treeRoot && DataFiltererrors.count() == 0) treeRoot->validateFilter(this, treeRoot); + + // ok, did it pass all tests? + if (!treeRoot || DataFiltererrors.count() > 0) { // nope + + // no errors just failed to finish + if (!treeRoot) DataFiltererrors << tr("malformed expression."); + + } + + errors = DataFiltererrors; + return errors; +} + QStringList DataFilter::parseFilter(QString query, QStringList *list) { // remember where we apply diff --git a/src/DataFilter.h b/src/DataFilter.h index 995f37ac8..155a09082 100644 --- a/src/DataFilter.h +++ b/src/DataFilter.h @@ -24,6 +24,7 @@ #include #include #include +#include #include "RideCache.h" #include "RideFile.h" //for SeriesType @@ -50,13 +51,14 @@ class Leaf { public: - Leaf() : type(none),op(0),series(NULL),dynamic(false) { } + Leaf(int loc, int leng) : type(none),op(0),series(NULL),dynamic(false),loc(loc),leng(leng) { } // evaluate against a RideItem Result eval(Context *context, DataFilter *df, Leaf *, RideItem *m); // tree traversal etc void print(Leaf *, int level); // print leaf and all children + void color(Leaf *, QTextDocument *); // update the document to match bool isDynamic(Leaf *); void validateFilter(DataFilter *, Leaf*); // validate bool isNumber(DataFilter *df, Leaf *leaf); @@ -78,6 +80,7 @@ class Leaf { Leaf *series; // is a symbol bool dynamic; RideFile::SeriesType seriesType; // for ridefilecache + int loc, leng; }; class DataFilter : public QObject @@ -100,11 +103,14 @@ class DataFilter : public QObject // when used for formulas Result evaluate(RideItem *rideItem); + QStringList getErrors() { return errors; }; + void colorSyntax(QTextDocument *content); static QStringList functions(); // return list of functions supported public slots: QStringList parseFilter(QString query, QStringList *list=0); + QStringList check(QString query); void clearFilter(); void configChanged(qint32); void dynamicParse(); diff --git a/src/DataFilter.l b/src/DataFilter.l index cd501456e..f2308d3b4 100644 --- a/src/DataFilter.l +++ b/src/DataFilter.l @@ -25,11 +25,18 @@ // tokens #include "DataFilter_yacc.h"/* generated by the scanner */ +int DataFiltercolumn = 1; + +#define YY_USER_ACTION DataFilterlloc.first_line = DataFilterlloc.last_line = DataFilterlineno; \ + DataFilterlloc.first_column = DataFiltercolumn; DataFilterlloc.last_column = DataFiltercolumn + DataFilterleng - 1; \ + DataFiltercolumn += DataFilterleng; + %} %option noyywrap %option nounput %option noinput %option 8bit +%option yylineno %% "=" DataFilterlval.op = EQ; return EQ; @@ -104,6 +111,7 @@ int DataFilterlex_destroy(void) { return 0; } void DataFilter_setString(QString p) { BEGIN(0); + DataFiltercolumn = 0; DataFilter_scan_string(p.toLatin1().data()); } diff --git a/src/DataFilter.y b/src/DataFilter.y index 792aea894..a409cb109 100644 --- a/src/DataFilter.y +++ b/src/DataFilter.y @@ -71,6 +71,8 @@ extern Leaf *root; // root node for parsed statement char function[32]; } +%locations + %type symbol value lexpr expr parms; %type lop cop bop; @@ -84,7 +86,7 @@ extern Leaf *root; // root node for parsed statement filter: lexpr { root = $1; } ; -parms: lexpr { $$ = new Leaf(); +parms: lexpr { $$ = new Leaf(@1.first_column, @1.last_column); $$->type = Leaf::Parameters; $$->fparms << $1; } @@ -92,42 +94,42 @@ parms: lexpr { $$ = new Leaf(); | parms ',' lexpr { $1->fparms << $3; } ; -lexpr : expr lop expr { $$ = new Leaf(); +lexpr : expr lop expr { $$ = new Leaf(@1.first_column, @3.last_column); $$->type = Leaf::Logical; $$->lvalue.l = $1; $$->op = $2; $$->rvalue.l = $3; } - | lexpr lop lexpr { $$ = new Leaf(); + | lexpr lop lexpr { $$ = new Leaf(@1.first_column, @3.last_column); $$->type = Leaf::Logical; $$->lvalue.l = $1; $$->op = $2; $$->rvalue.l = $3; } - | '(' expr ')' { $$ = new Leaf(); + | '(' expr ')' { $$ = new Leaf(@2.first_column, @2.last_column); $$->type = Leaf::Logical; $$->lvalue.l = $2; $$->op = 0; } - | expr + | expr { $$ = $1; } ; -expr : '(' expr ')' { $$ = new Leaf(); +expr : '(' expr ')' { $$ = new Leaf(@2.first_column, @2.last_column); $$->type = Leaf::Logical; $$->lvalue.l = $2; $$->op = 0; } - | expr cop expr { $$ = new Leaf(); + | expr cop expr { $$ = new Leaf(@1.first_column, @3.last_column); $$->type = Leaf::Operation; $$->lvalue.l = $1; $$->op = $2; $$->rvalue.l = $3; } - | expr bop expr { $$ = new Leaf(); + | expr bop expr { $$ = new Leaf(@1.first_column, @3.last_column); $$->type = Leaf::BinaryOperation; $$->lvalue.l = $1; $$->op = $2; $$->rvalue.l = $3; } - | expr Q expr COL expr { $$ = new Leaf(); + | expr Q expr COL expr { $$ = new Leaf(@1.first_column, @5.last_column); $$->type = Leaf::Conditional; $$->op = 0; // unused $$->lvalue.l = $3; @@ -179,63 +181,64 @@ bop : ADD | POW ; -symbol : SYMBOL { $$ = new Leaf(); $$->type = Leaf::Symbol; - if (QString(DataFiltertext) == "BikeScore") - $$->lvalue.n = new QString("BikeScore™"); - else - $$->lvalue.n = new QString(DataFiltertext); - } +symbol : SYMBOL { $$ = new Leaf(@1.first_column, @1.last_column); + $$->type = Leaf::Symbol; + if (QString(DataFiltertext) == "BikeScore") + $$->lvalue.n = new QString("BikeScore™"); + else + $$->lvalue.n = new QString(DataFiltertext); + } ; value : symbol { $$ = $1; } - | DF_STRING { $$ = new Leaf(); $$->type = Leaf::String; + | DF_STRING { $$ = new Leaf(@1.first_column, @1.last_column); $$->type = Leaf::String; QString s2(DataFiltertext); $$->lvalue.s = new QString(s2.mid(1,s2.length()-2)); } - | DF_FLOAT { $$ = new Leaf(); $$->type = Leaf::Float; + | DF_FLOAT { $$ = new Leaf(@1.first_column, @1.last_column); $$->type = Leaf::Float; $$->lvalue.f = QString(DataFiltertext).toFloat(); } - | DF_INTEGER { $$ = new Leaf(); $$->type = Leaf::Integer; + | DF_INTEGER { $$ = new Leaf(@1.first_column, @1.last_column); $$->type = Leaf::Integer; $$->lvalue.i = QString(DataFiltertext).toInt(); } - | BEST '(' symbol ',' lexpr ')' { $$ = new Leaf(); $$->type = Leaf::Function; + | BEST '(' symbol ',' lexpr ')' { $$ = new Leaf(@1.first_column, @6.last_column); $$->type = Leaf::Function; $$->function = QString($1); $$->series = $3; $$->lvalue.l = $5; } - | TIZ '(' symbol ',' lexpr ')' { $$ = new Leaf(); $$->type = Leaf::Function; + | TIZ '(' symbol ',' lexpr ')' { $$ = new Leaf(@1.first_column, @6.last_column); $$->type = Leaf::Function; $$->function = QString($1); $$->series = $3; $$->lvalue.l = $5; } - | LTS '(' symbol ')' { $$ = new Leaf(); $$->type = Leaf::Function; + | LTS '(' symbol ')' { $$ = new Leaf(@1.first_column, @4.last_column); $$->type = Leaf::Function; $$->function = QString($1); $$->series = $3; $$->lvalue.l = NULL; } - | STS '(' symbol ')' { $$ = new Leaf(); $$->type = Leaf::Function; + | STS '(' symbol ')' { $$ = new Leaf(@1.first_column, @4.last_column); $$->type = Leaf::Function; $$->function = QString($1); $$->series = $3; $$->lvalue.l = NULL; } - | RR '(' symbol ')' { $$ = new Leaf(); $$->type = Leaf::Function; + | RR '(' symbol ')' { $$ = new Leaf(@1.first_column, @4.last_column); $$->type = Leaf::Function; $$->function = QString($1); $$->series = $3; $$->lvalue.l = NULL; } - | SB '(' symbol ')' { $$ = new Leaf(); $$->type = Leaf::Function; + | SB '(' symbol ')' { $$ = new Leaf(@1.first_column, @4.last_column); $$->type = Leaf::Function; $$->function = QString($1); $$->series = $3; $$->lvalue.l = NULL; } - | CONFIG '(' symbol ')' { $$ = new Leaf(); $$->type = Leaf::Function; + | CONFIG '(' symbol ')' { $$ = new Leaf(@1.first_column, @4.last_column); $$->type = Leaf::Function; $$->function = QString($1); $$->series = $3; $$->lvalue.l = NULL; } - | CONST_ '(' symbol ')' { $$ = new Leaf(); $$->type = Leaf::Function; + | CONST_ '(' symbol ')' { $$ = new Leaf(@1.first_column, @4.last_column); $$->type = Leaf::Function; $$->function = QString($1); $$->series = $3; $$->lvalue.l = NULL; diff --git a/src/LTMTool.cpp b/src/LTMTool.cpp index e8ac0cb8b..a883cea41 100644 --- a/src/LTMTool.cpp +++ b/src/LTMTool.cpp @@ -1703,7 +1703,7 @@ EditMetricDetailDialog::EditMetricDetailDialog(Context *context, LTMTool *ltmToo //formulaWidget->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding); QVBoxLayout *formulaLayout = new QVBoxLayout(formulaWidget); formulaLayout->addStretch(); - formulaEdit = new DataFilterEdit(this); + formulaEdit = new DataFilterEdit(this, context); //formulaEdit->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding); formulaType = new QComboBox(this); formulaType->addItem(tr("Total"), static_cast(RideMetric::Total)); @@ -2370,8 +2370,8 @@ LTMTool::setFilter(QStringList files) emit filterChanged(); } -DataFilterEdit::DataFilterEdit(QWidget *parent) -: QTextEdit(parent), c(0) +DataFilterEdit::DataFilterEdit(QWidget *parent, Context *context) +: QTextEdit(parent), context(context), c(0) { } @@ -2379,6 +2379,18 @@ DataFilterEdit::~DataFilterEdit() { } +void +DataFilterEdit::checkErrors() +{ + // parse and present errors to user + DataFilter checker(this, context); + QStringList errors = checker.check(toPlainText()); + checker.colorSyntax(document()); + + // need to fixup for errors! + // XXX next commit +} + void DataFilterEdit::setCompleter(QCompleter *completer) { if (c) @@ -2411,6 +2423,16 @@ void DataFilterEdit::insertCompletion(const QString& completion) tc.movePosition(QTextCursor::EndOfWord); tc.insertText(completion.right(extra)); setTextCursor(tc); + + checkErrors(); +} + +void +DataFilterEdit::setText(const QString &text) +{ + // set text.. + QTextEdit::setText(text); + checkErrors(); } QString DataFilterEdit::textUnderCursor() const @@ -2428,6 +2450,7 @@ void DataFilterEdit::focusInEvent(QFocusEvent *e) void DataFilterEdit::keyPressEvent(QKeyEvent *e) { + // wait a couple of seconds before checking the changes.... if (c && c->popup()->isVisible()) { // The following keys are forwarded by the completer to the widget switch (e->key()) { @@ -2447,6 +2470,9 @@ void DataFilterEdit::keyPressEvent(QKeyEvent *e) if (!c || !isShortcut) // do not process the shortcut when we have a completer QTextEdit::keyPressEvent(e); + // check + checkErrors(); + const bool ctrlOrShift = e->modifiers() & (Qt::ControlModifier | Qt::ShiftModifier); if (!c || (ctrlOrShift && e->text().isEmpty())) return; diff --git a/src/LTMTool.h b/src/LTMTool.h index 27496a9b4..bef3451a9 100644 --- a/src/LTMTool.h +++ b/src/LTMTool.h @@ -237,7 +237,7 @@ class DataFilterEdit : public QTextEdit Q_OBJECT public: - DataFilterEdit(QWidget *parent = 0); + DataFilterEdit(QWidget *parent, Context *context); ~DataFilterEdit(); void setCompleter(QCompleter *c); @@ -247,14 +247,17 @@ protected: void keyPressEvent(QKeyEvent *e); void focusInEvent(QFocusEvent *e); -private slots: +public slots: + void setText(const QString&); void insertCompletion(const QString &completion); + void checkErrors(); private: QString textUnderCursor() const; private: QCompleter *c; + Context *context; }; #endif // _GC_LTMTool_h